@adhdev/daemon-core 0.9.82-rc.186 → 0.9.82-rc.188

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 (55) hide show
  1. package/dist/boot/daemon-lifecycle.d.ts +1 -0
  2. package/dist/commands/cli-manager.d.ts +2 -1
  3. package/dist/commands/mesh-coordinator.d.ts +13 -0
  4. package/dist/commands/router.d.ts +5 -1
  5. package/dist/config/chat-history.d.ts +1 -0
  6. package/dist/git/git-commands.d.ts +2 -0
  7. package/dist/git/git-types.d.ts +2 -0
  8. package/dist/index.d.ts +2 -2
  9. package/dist/index.js +15461 -14552
  10. package/dist/index.js.map +1 -1
  11. package/dist/index.mjs +14411 -13502
  12. package/dist/index.mjs.map +1 -1
  13. package/dist/mesh/beads-db.d.ts +1 -0
  14. package/dist/mesh/mesh-events.d.ts +46 -1
  15. package/dist/mesh/mesh-work-queue.d.ts +1 -0
  16. package/dist/providers/cli-provider-instance.d.ts +4 -0
  17. package/dist/providers/contracts.d.ts +32 -1
  18. package/dist/providers/native-history/dispatcher.d.ts +2 -0
  19. package/dist/providers/sdk/v1/types/common/index.d.ts +35 -1
  20. package/dist/providers/spec/cli-adapter.d.ts +1 -0
  21. package/dist/providers/spec/driver.d.ts +6 -1
  22. package/dist/providers/spec/native-history-executor.d.ts +2 -0
  23. package/dist/providers/spec/schema.gen.d.ts +22 -0
  24. package/dist/providers/spec/types.d.ts +10 -0
  25. package/dist/repo-mesh-types.d.ts +6 -0
  26. package/package.json +1 -1
  27. package/src/boot/daemon-lifecycle.ts +2 -0
  28. package/src/commands/chat-commands.ts +206 -18
  29. package/src/commands/cli-manager.ts +56 -14
  30. package/src/commands/mesh-coordinator.ts +110 -5
  31. package/src/commands/router.ts +146 -21
  32. package/src/config/chat-history.ts +4 -0
  33. package/src/git/git-commands.ts +20 -2
  34. package/src/git/git-status.ts +35 -6
  35. package/src/git/git-types.ts +2 -0
  36. package/src/index.ts +2 -2
  37. package/src/mesh/beads-db.ts +4 -0
  38. package/src/mesh/mesh-events.ts +264 -4
  39. package/src/mesh/mesh-work-queue.ts +4 -0
  40. package/src/providers/cli-provider-instance.ts +122 -13
  41. package/src/providers/contracts.d.ts +55 -0
  42. package/src/providers/contracts.ts +36 -1
  43. package/src/providers/native-history/dispatcher.ts +126 -17
  44. package/src/providers/provider-loader.ts +4 -7
  45. package/src/providers/provider-schema.ts +56 -1
  46. package/src/providers/sdk/v1/schemas/cli/provider.schema.json +46 -0
  47. package/src/providers/sdk/v1/types/common/index.ts +19 -0
  48. package/src/providers/spec/cli-adapter.ts +32 -5
  49. package/src/providers/spec/driver.ts +68 -1
  50. package/src/providers/spec/evaluator.ts +11 -1
  51. package/src/providers/spec/native-history-executor.ts +93 -27
  52. package/src/providers/spec/schema.gen.ts +12 -1
  53. package/src/providers/spec/schema.json +21 -1
  54. package/src/providers/spec/types.ts +10 -0
  55. package/src/repo-mesh-types.ts +6 -0
@@ -1,7 +1,7 @@
1
1
  import { createHash } from 'node:crypto'
2
2
  import { existsSync, readFileSync, writeFileSync } from 'node:fs'
3
3
  import * as os from 'node:os'
4
- import { isAbsolute, join, resolve } from 'node:path'
4
+ import { basename, isAbsolute, join, resolve } from 'node:path'
5
5
  import { LOG } from '../logging/logger.js'
6
6
  import type {
7
7
  MeshCoordinatorMcpConfigFormat,
@@ -39,6 +39,7 @@ export type MeshCoordinatorSetup =
39
39
  command: string
40
40
  requiresRestart: boolean
41
41
  instructions: string
42
+ mcpServer: MeshCoordinatorMcpServerLaunch
42
43
  }
43
44
  | {
44
45
  kind: 'unsupported'
@@ -71,6 +72,8 @@ function resolveHermesMeshCoordinatorSetup(options: ResolveMeshCoordinatorSetupO
71
72
  const mcpServer = resolveAdhdevMcpServerLaunch({
72
73
  meshId: options.meshId,
73
74
  adhdevMcpCommand: options.adhdevMcpCommand,
75
+ adhdevMcpEntryPath: options.adhdevMcpEntryPath,
76
+ nodeExecutable: options.nodeExecutable,
74
77
  adhdevMcpTransport: options.adhdevMcpTransport,
75
78
  adhdevMcpPort: options.adhdevMcpPort,
76
79
  })
@@ -144,6 +147,8 @@ export function resolveMeshCoordinatorSetup(options: ResolveMeshCoordinatorSetup
144
147
  const mcpServer = resolveAdhdevMcpServerLaunch({
145
148
  meshId,
146
149
  adhdevMcpCommand: options.adhdevMcpCommand,
150
+ adhdevMcpEntryPath: options.adhdevMcpEntryPath,
151
+ nodeExecutable: options.nodeExecutable,
147
152
  adhdevMcpTransport: options.adhdevMcpTransport,
148
153
  adhdevMcpPort: options.adhdevMcpPort,
149
154
  })
@@ -168,22 +173,41 @@ export function resolveMeshCoordinatorSetup(options: ResolveMeshCoordinatorSetup
168
173
  if (!instructions || !template?.trim()) {
169
174
  return { kind: 'unsupported', reason: 'Provider manual MCP setup is missing instructions or template' }
170
175
  }
171
- const renderedTemplate = renderMeshCoordinatorTemplate(template, {
176
+ const mcpServer = resolveAdhdevMcpServerLaunch({
177
+ meshId,
178
+ adhdevMcpCommand: options.adhdevMcpCommand,
179
+ adhdevMcpEntryPath: options.adhdevMcpEntryPath,
180
+ nodeExecutable: options.nodeExecutable,
181
+ adhdevMcpTransport: options.adhdevMcpTransport,
182
+ adhdevMcpPort: options.adhdevMcpPort,
183
+ })
184
+ if (!mcpServer) {
185
+ return {
186
+ kind: 'unsupported',
187
+ reason: 'Could not resolve the ADHDev MCP server entrypoint and transport arguments',
188
+ }
189
+ }
190
+ let renderedTemplate = renderMeshCoordinatorTemplate(template, {
172
191
  meshId,
173
192
  workspace,
174
193
  serverName,
175
- adhdevMcpCommand: options.adhdevMcpCommand || DEFAULT_ADHDEV_MCP_COMMAND,
194
+ adhdevMcpCommand: mcpServer.command,
195
+ adhdevMcpArgs: mcpServer.args.join(' '),
176
196
  })
177
197
  // Detect if the template is a runnable CLI command (single line, no YAML/JSON structure).
178
198
  // If so, use cli_command kind so the daemon can execute it automatically.
179
199
  const isCliCommand = !renderedTemplate.trim().includes('\n') && !renderedTemplate.trim().startsWith('{')
180
200
  if (isCliCommand) {
201
+ if (!/\{\{\s*adhdevMcpArgs\s*\}\}/.test(template)) {
202
+ renderedTemplate = replaceLegacyCliCommandMcpArgs(renderedTemplate, mcpServer.args)
203
+ }
181
204
  return {
182
205
  kind: 'cli_command',
183
206
  serverName,
184
207
  command: renderedTemplate.trim(),
185
208
  requiresRestart: mcpConfig.requiresRestart === true,
186
209
  instructions: instructions,
210
+ mcpServer,
187
211
  }
188
212
  }
189
213
  return {
@@ -204,7 +228,14 @@ export function resolveMeshCoordinatorSetup(options: ResolveMeshCoordinatorSetup
204
228
  }
205
229
 
206
230
  function renderMeshCoordinatorTemplate(template: string, values: Record<string, string>): string {
207
- return template.replace(/\{\{\s*(meshId|workspace|serverName|adhdevMcpCommand)\s*\}\}/g, (_, key: string) => values[key] || '')
231
+ return template.replace(/\{\{\s*(meshId|workspace|serverName|adhdevMcpCommand|adhdevMcpArgs)\s*\}\}/g, (_, key: string) => values[key] || '')
232
+ }
233
+
234
+ function replaceLegacyCliCommandMcpArgs(command: string, args: string[]): string {
235
+ return command.replace(
236
+ /\bmcp\s+--mode\s+(?:ipc|local)\s+--repo-mesh\s+\S+(?:\s+--port\s+\d+)?\s*$/,
237
+ args.join(' '),
238
+ )
208
239
  }
209
240
 
210
241
  function resolveHermesCoordinatorHome(meshId: string, workspace: string): string {
@@ -224,12 +255,29 @@ function resolveMcpConfigPath(configPath: string, workspace: string): string {
224
255
  function resolveAdhdevMcpServerLaunch(options: {
225
256
  meshId: string
226
257
  adhdevMcpCommand?: string
258
+ adhdevMcpEntryPath?: string
259
+ nodeExecutable?: string
227
260
  adhdevMcpTransport?: 'local' | 'ipc'
228
261
  adhdevMcpPort?: number
229
262
  }): MeshCoordinatorMcpServerLaunch | null {
263
+ const directEntryPath = resolveAdhdevMcpEntryPath(options.adhdevMcpEntryPath)
264
+ if (directEntryPath) {
265
+ const transport = resolveMcpTransport(options.adhdevMcpTransport)
266
+ const args = [directEntryPath, '--mode', transport, '--repo-mesh', options.meshId]
267
+ const port = resolveMcpPort(options.adhdevMcpPort)
268
+ if (port !== undefined) args.push('--port', String(port))
269
+ return {
270
+ command: resolveNodeExecutable(options.nodeExecutable),
271
+ args,
272
+ }
273
+ }
274
+
230
275
  const command = resolveAdhdevCommand(options.adhdevMcpCommand)
231
276
  const transport = resolveMcpTransport(options.adhdevMcpTransport)
232
- const args = ['mcp', '--mode', transport, '--repo-mesh', options.meshId]
277
+ const directMcpEntrypoint = basename(command).startsWith('adhdev-mcp')
278
+ || command.includes('/vendor/mcp-server/')
279
+ || command.includes('\\vendor\\mcp-server\\')
280
+ const args = [...(directMcpEntrypoint ? [] : ['mcp']), '--mode', transport, '--repo-mesh', options.meshId]
233
281
  const port = resolveMcpPort(options.adhdevMcpPort)
234
282
  if (port !== undefined) args.push('--port', String(port))
235
283
  return {
@@ -242,6 +290,17 @@ function resolveAdhdevCommand(explicitCommand?: string): string {
242
290
  return explicitCommand?.trim() || process.env.ADHDEV_COORDINATOR_MCP_COMMAND?.trim() || DEFAULT_ADHDEV_MCP_COMMAND
243
291
  }
244
292
 
293
+ function resolveAdhdevMcpEntryPath(explicitEntryPath?: string): string | null {
294
+ const entryPath = explicitEntryPath?.trim() || process.env.ADHDEV_COORDINATOR_MCP_ENTRY_PATH?.trim()
295
+ return entryPath || null
296
+ }
297
+
298
+ function resolveNodeExecutable(explicitNodeExecutable?: string): string {
299
+ return explicitNodeExecutable?.trim()
300
+ || process.env.ADHDEV_COORDINATOR_NODE_EXECUTABLE?.trim()
301
+ || process.execPath
302
+ }
303
+
245
304
  function resolveMcpTransport(explicitTransport?: 'local' | 'ipc'): 'local' | 'ipc' {
246
305
  if (explicitTransport === 'local' || explicitTransport === 'ipc') return explicitTransport
247
306
  const envTransport = process.env.ADHDEV_COORDINATOR_MCP_TRANSPORT?.trim()
@@ -418,6 +477,52 @@ export interface PtyExecResult {
418
477
  timedOut: boolean
419
478
  }
420
479
 
480
+ export interface MeshCoordinatorRegistrationStep {
481
+ command: string
482
+ args: string[]
483
+ required: boolean
484
+ label: 'remove_existing' | 'register'
485
+ }
486
+
487
+ /**
488
+ * Codex rejects `mcp add` when a server with the same name already exists.
489
+ * Replace that entry before registering so transport/mesh changes do not leave
490
+ * a fresh coordinator attached to stale MCP launch arguments.
491
+ */
492
+ export function buildMeshCoordinatorRegistrationPlan(
493
+ cliType: string,
494
+ serverName: string,
495
+ registrationCommand: string,
496
+ ): MeshCoordinatorRegistrationStep[] {
497
+ const commandParts = registrationCommand.trim().split(/\s+/).filter(Boolean)
498
+ const [command, ...args] = commandParts
499
+ if (!command) return []
500
+
501
+ const register: MeshCoordinatorRegistrationStep = {
502
+ command,
503
+ args,
504
+ required: true,
505
+ label: 'register',
506
+ }
507
+ if (
508
+ cliType === 'codex-cli'
509
+ && basename(command) === 'codex'
510
+ && args[0] === 'mcp'
511
+ && args[1] === 'add'
512
+ ) {
513
+ return [
514
+ {
515
+ command,
516
+ args: ['mcp', 'remove', serverName],
517
+ required: false,
518
+ label: 'remove_existing',
519
+ },
520
+ register,
521
+ ]
522
+ }
523
+ return [register]
524
+ }
525
+
421
526
  /**
422
527
  * Run a one-shot CLI command under a real PTY and collect its output.
423
528
  *
@@ -849,7 +849,7 @@ function readCachedInlineMeshActiveSessions(node: any): string[] {
849
849
  return sessionId ? [sessionId] : [];
850
850
  }
851
851
 
852
- function readCachedInlineMeshActiveSessionDetails(node: any): Array<Record<string, unknown>> {
852
+ export function readCachedInlineMeshActiveSessionDetails(node: any): Array<Record<string, unknown>> {
853
853
  const cachedStatus = readObjectRecord(node?.cachedStatus);
854
854
  const activeSession = readObjectRecord(cachedStatus.activeSession);
855
855
  const fallbackSession = Object.keys(activeSession).length
@@ -877,9 +877,14 @@ function readCachedInlineMeshActiveSessionDetails(node: any): Array<Record<strin
877
877
  node?.provider_type,
878
878
  ),
879
879
  state: readStringValue(fallbackSession.status, fallbackSession.state, fallbackSession.lifecycle),
880
+ chatStatus: readStringValue(fallbackSession.chatStatus, fallbackSession.chat_status),
880
881
  lifecycle: readStringValue(fallbackSession.lifecycle),
881
882
  title: readStringValue(fallbackSession.title, fallbackSession.displayName, fallbackSession.display_name) ?? null,
882
883
  workspace: readStringValue(fallbackSession.workspace, node?.workspace) ?? null,
884
+ role: readStringValue(fallbackSession.role) ?? null,
885
+ isSelfCoordinator: fallbackSession.isSelfCoordinator === true || fallbackSession.is_self_coordinator === true,
886
+ createdAt: readStringValue(fallbackSession.createdAt, fallbackSession.created_at) ?? null,
887
+ startedAt: readStringValue(fallbackSession.startedAt, fallbackSession.started_at) ?? null,
883
888
  lastActivityAt: readStringValue(fallbackSession.lastActivityAt, fallbackSession.last_activity_at) ?? null,
884
889
  recoveryState: readStringValue(fallbackSession.recoveryState, fallbackSession.recovery_state) ?? null,
885
890
  isCached: true,
@@ -1067,15 +1072,28 @@ async function hydrateInlineMeshDirectTruth(args: {
1067
1072
  }
1068
1073
 
1069
1074
  function summarizeMeshSessionRecord(record: any): Record<string, unknown> {
1075
+ const meta = readObjectRecord(record?.meta);
1076
+ const isSelfCoordinator = Boolean(readStringValue(meta.meshCoordinatorFor));
1077
+ const chatStatus = readStringValue(record?.chatStatus, record?.activeChat?.status, meta.chatStatus, meta.sessionStatus);
1078
+ const state = readLiveMeshSessionState(record);
1079
+ const statusNote = isSelfCoordinator && (!chatStatus || chatStatus === 'idle' || state === 'idle')
1080
+ ? 'Coordinator self status is sampled from the session host and may read idle while the coordinator is generating this response.'
1081
+ : null;
1070
1082
  return {
1071
1083
  sessionId: readStringValue(record?.sessionId) || 'unknown',
1072
1084
  providerType: readStringValue(record?.providerType),
1073
- state: readLiveMeshSessionState(record),
1085
+ state,
1086
+ chatStatus,
1074
1087
  lifecycle: readStringValue(record?.lifecycle),
1075
1088
  surfaceKind: getSessionHostSurfaceKind(record as any),
1076
- recoveryState: readStringValue(record?.meta?.runtimeRecoveryState) ?? null,
1089
+ recoveryState: readStringValue(meta.runtimeRecoveryState) ?? null,
1077
1090
  workspace: readStringValue(record?.workspace) ?? null,
1078
1091
  title: readStringValue(record?.displayName, record?.workspaceLabel) ?? null,
1092
+ role: isSelfCoordinator ? 'coordinator' : readStringValue(meta.meshRole, meta.role) ?? null,
1093
+ isSelfCoordinator,
1094
+ statusNote,
1095
+ createdAt: toIsoTimestamp(record?.createdAt ?? record?.created_at),
1096
+ startedAt: toIsoTimestamp(record?.startedAt ?? record?.started_at ?? record?.spawnedAtMs ?? record?.spawned_at_ms),
1079
1097
  lastActivityAt: toIsoTimestamp(record?.updatedAt ?? record?.lastActivityAt ?? record?.last_activity_at),
1080
1098
  isCached: false,
1081
1099
  };
@@ -2130,12 +2148,14 @@ export interface CommandRouterDeps {
2130
2148
  /** Reference to detected IDEs array (mutable — router updates it) */
2131
2149
  detectedIdes: { value: any[] };
2132
2150
  sessionRegistry: SessionRegistry;
2133
- /** Callback for CDP manager creation after launch_ide */
2151
+ /** Callback after CDP manager created (transport-specific extras) */
2134
2152
  onCdpManagerCreated?: (ideType: string, manager: DaemonCdpManager) => void;
2135
2153
  /** Callback after IDE connected (e.g., startAgentStreamPolling) */
2136
2154
  onIdeConnected?: () => void;
2137
2155
  /** Callback after status change (stop_ide, restart) */
2138
2156
  onStatusChange?: () => void;
2157
+ /** Callback when a mesh state is invalidated */
2158
+ onMeshStateChange?: (meshId: string) => void;
2139
2159
  /** Callback after chat-related commands */
2140
2160
  onPostChatCommand?: () => void;
2141
2161
  /** Get a connected CDP manager (for agent stream reset check) */
@@ -2456,6 +2476,16 @@ export class DaemonCommandRouter {
2456
2476
  return next;
2457
2477
  }
2458
2478
 
2479
+ public getCachedInlineMeshNodes(): any[] {
2480
+ const nodes: any[] = [];
2481
+ for (const mesh of this.inlineMeshCache.values()) {
2482
+ if (Array.isArray(mesh?.nodes)) {
2483
+ nodes.push(...mesh.nodes);
2484
+ }
2485
+ }
2486
+ return nodes;
2487
+ }
2488
+
2459
2489
  public getCachedInlineMesh(meshId: string, inlineMesh?: unknown): any | undefined {
2460
2490
  if (inlineMesh && typeof inlineMesh === 'object') {
2461
2491
  return this.warmInlineMeshCache(meshId, inlineMesh);
@@ -2510,6 +2540,7 @@ export class DaemonCommandRouter {
2510
2540
 
2511
2541
  private invalidateAggregateMeshStatus(meshId: string): void {
2512
2542
  this.aggregateMeshStatusCache.delete(meshId);
2543
+ this.deps.onMeshStateChange?.(meshId);
2513
2544
  }
2514
2545
 
2515
2546
 
@@ -4852,6 +4883,13 @@ export class DaemonCommandRouter {
4852
4883
  ...(role ? { role } : {}),
4853
4884
  });
4854
4885
  if (!node) return { success: false, error: 'Mesh not found' };
4886
+ // mesh_status hands back a coordinator-memory aggregate
4887
+ // snapshot keyed on (meshId, queueRevision). Adding a
4888
+ // node touches neither, so without an explicit cache
4889
+ // bust the dashboard graph keeps rendering the pre-add
4890
+ // node list (empty for a fresh mesh) even after the
4891
+ // user clicks Refresh.
4892
+ this.invalidateAggregateMeshStatus(meshId);
4855
4893
  return { success: true, node };
4856
4894
  } catch (e: any) {
4857
4895
  return { success: false, error: e.message };
@@ -4889,6 +4927,11 @@ export class DaemonCommandRouter {
4889
4927
  }
4890
4928
  const node = updateNode(meshId, nodeId, patch as any);
4891
4929
  if (!node) return { success: false, error: 'Mesh node not found' };
4930
+ // Provider priority / systemPrompt changes don't touch
4931
+ // the queue revision, so without a manual bust the
4932
+ // cached aggregate keeps surfacing pre-update values
4933
+ // (priority chip, coordinator prompt preview, etc.).
4934
+ this.invalidateAggregateMeshStatus(meshId);
4892
4935
  return { success: true, node };
4893
4936
  } catch (e: any) {
4894
4937
  return { success: false, error: e.message };
@@ -5057,6 +5100,11 @@ export class DaemonCommandRouter {
5057
5100
  let removed = false;
5058
5101
  if (meshRecord?.inline) {
5059
5102
  removed = this.removeInlineMeshNode(meshId, mesh, nodeId);
5103
+ // Inline meshes share the same aggregate snapshot cache as
5104
+ // local-config meshes; without this bust the removed node
5105
+ // keeps showing up in the dashboard graph until the cache
5106
+ // ages out on its own.
5107
+ if (removed) this.invalidateAggregateMeshStatus(meshId);
5060
5108
  } else {
5061
5109
  const { removeNode } = await import('../config/mesh-config.js');
5062
5110
  removed = removeNode(meshId, nodeId);
@@ -5278,10 +5326,8 @@ export class DaemonCommandRouter {
5278
5326
  if (ownerFailure) return ownerFailure;
5279
5327
  try {
5280
5328
  const { triggerMeshQueue } = await import('../mesh/mesh-events.js');
5281
- if (meshId) {
5282
- triggerMeshQueue(this.deps as any, meshId);
5283
- }
5284
- return { success: true };
5329
+ const trigger = await triggerMeshQueue(this.deps as any, meshId);
5330
+ return { success: true, trigger };
5285
5331
  } catch (e: any) {
5286
5332
  return { success: false, error: e.message };
5287
5333
  }
@@ -5434,23 +5480,102 @@ export class DaemonCommandRouter {
5434
5480
  // a real PTY the registration goes through and the
5435
5481
  // exit code tells us whether it actually persisted.
5436
5482
  let mcpRegistrationOk = false;
5483
+ let mcpRegistrationFailure: {
5484
+ command: string;
5485
+ output: string;
5486
+ exitCode: number | null;
5487
+ signal: number | null;
5488
+ timedOut: boolean;
5489
+ } | null = null;
5437
5490
  try {
5438
- const { execUnderPty } = await import('./mesh-coordinator.js');
5439
- const cmdParts = coordinatorSetup.command.trim().split(/\s+/);
5440
- const [regCmd, ...regArgs] = cmdParts;
5441
- LOG.info('MeshCoordinator', `Running MCP registration (pty): ${coordinatorSetup.command}`);
5442
- const ptyResult = await execUnderPty(regCmd, regArgs, { cwd: workspace, timeoutMs: 20_000 });
5443
- if (ptyResult.timedOut) {
5444
- LOG.warn('MeshCoordinator', `MCP registration timed out — last output:\n${ptyResult.output.slice(-2000)}`);
5445
- } else if (ptyResult.exitCode === 0) {
5446
- mcpRegistrationOk = true;
5447
- LOG.info('MeshCoordinator', `MCP registration succeeded (exit=0)`);
5448
- } else {
5449
- // Non-fatal many providers return non-zero on duplicate registration.
5450
- LOG.warn('MeshCoordinator', `MCP registration exit=${ptyResult.exitCode} signal=${ptyResult.signal} — output:\n${ptyResult.output.slice(-2000)}`);
5491
+ const { buildMeshCoordinatorRegistrationPlan, execUnderPty } = await import('./mesh-coordinator.js');
5492
+ const registrationPlan = buildMeshCoordinatorRegistrationPlan(
5493
+ cliType,
5494
+ coordinatorSetup.serverName,
5495
+ coordinatorSetup.command,
5496
+ );
5497
+ for (const step of registrationPlan) {
5498
+ const renderedCommand = [step.command, ...step.args].join(' ');
5499
+ LOG.info('MeshCoordinator', `Running MCP ${step.label} (pty): ${renderedCommand}`);
5500
+ const ptyResult = await execUnderPty(step.command, step.args, { cwd: workspace, timeoutMs: 20_000 });
5501
+ if (ptyResult.exitCode === 0 && !ptyResult.timedOut) {
5502
+ if (step.required) mcpRegistrationOk = true;
5503
+ continue;
5504
+ }
5505
+ LOG.warn('MeshCoordinator', `MCP ${step.label} failed exit=${ptyResult.exitCode} signal=${ptyResult.signal} timedOut=${ptyResult.timedOut} — output:\n${ptyResult.output.slice(-2000)}`);
5506
+ if (step.required) {
5507
+ mcpRegistrationFailure = {
5508
+ command: renderedCommand,
5509
+ output: ptyResult.output.slice(-2000),
5510
+ exitCode: ptyResult.exitCode,
5511
+ signal: ptyResult.signal,
5512
+ timedOut: ptyResult.timedOut,
5513
+ };
5514
+ break;
5515
+ }
5451
5516
  }
5452
5517
  } catch (error: any) {
5453
5518
  LOG.warn('MeshCoordinator', `MCP registration command failed: ${error?.message || error}`);
5519
+ mcpRegistrationFailure = {
5520
+ command: coordinatorSetup.command,
5521
+ output: error?.message || String(error),
5522
+ exitCode: null,
5523
+ signal: null,
5524
+ timedOut: false,
5525
+ };
5526
+ }
5527
+
5528
+ if (!mcpRegistrationOk) {
5529
+ return {
5530
+ success: false,
5531
+ code: 'mesh_coordinator_mcp_registration_failed',
5532
+ error: `Could not register ${coordinatorSetup.serverName}; coordinator session was not launched`,
5533
+ meshId,
5534
+ cliType,
5535
+ workspace,
5536
+ registration: mcpRegistrationFailure,
5537
+ };
5538
+ }
5539
+
5540
+ // Codex gives repo-local .mcp.json precedence over its
5541
+ // global `codex mcp add` registration. Refresh an
5542
+ // existing ADHDev entry so a stale workspace command
5543
+ // cannot shadow the registration we just verified.
5544
+ if (cliType === 'codex-cli') {
5545
+ const repoMcpConfigPath = pathJoin(workspace, '.mcp.json');
5546
+ if (fs.existsSync(repoMcpConfigPath)) {
5547
+ try {
5548
+ const repoMcpConfig = parseMeshCoordinatorMcpConfig(
5549
+ fs.readFileSync(repoMcpConfigPath, 'utf-8'),
5550
+ 'claude_mcp_json',
5551
+ );
5552
+ const existingServers = repoMcpConfig.mcpServers;
5553
+ if (
5554
+ existingServers
5555
+ && typeof existingServers === 'object'
5556
+ && !Array.isArray(existingServers)
5557
+ && existingServers[coordinatorSetup.serverName]
5558
+ ) {
5559
+ fs.writeFileSync(repoMcpConfigPath, serializeMeshCoordinatorMcpConfig({
5560
+ ...repoMcpConfig,
5561
+ mcpServers: {
5562
+ ...existingServers,
5563
+ [coordinatorSetup.serverName]: coordinatorSetup.mcpServer,
5564
+ },
5565
+ }, 'claude_mcp_json'), 'utf-8');
5566
+ LOG.info('MeshCoordinator', `Refreshed repo-local ${repoMcpConfigPath} entry for ${coordinatorSetup.serverName}`);
5567
+ }
5568
+ } catch (error: any) {
5569
+ return {
5570
+ success: false,
5571
+ code: 'mesh_coordinator_config_write_failed',
5572
+ error: `Could not refresh repo-local MCP config: ${error?.message || error}`,
5573
+ meshId,
5574
+ cliType,
5575
+ workspace,
5576
+ };
5577
+ }
5578
+ }
5454
5579
  }
5455
5580
 
5456
5581
  // Inject system prompt declaratively from provider.v1.json.
@@ -1350,6 +1350,7 @@ type ProviderNativeHistoryReadResult = {
1350
1350
  sourcePath: string;
1351
1351
  sourceMtimeMs: number;
1352
1352
  providerSessionId?: string;
1353
+ workspace?: string;
1353
1354
  nativeHistoryCoverage?: string;
1354
1355
  partialReason?: string;
1355
1356
  unavailableReason?: string;
@@ -1449,6 +1450,7 @@ function callProviderNativeHistoryRead(
1449
1450
  sourcePath: typeof (result as any).sourcePath === 'string' ? (result as any).sourcePath : '',
1450
1451
  sourceMtimeMs: Number((result as any).sourceMtimeMs) || 0,
1451
1452
  providerSessionId: typeof (result as any).providerSessionId === 'string' ? (result as any).providerSessionId.trim() : undefined,
1453
+ workspace: typeof (result as any).workspace === 'string' ? (result as any).workspace.trim() : undefined,
1452
1454
  nativeHistoryCoverage: typeof (result as any).nativeHistoryCoverage === 'string' ? (result as any).nativeHistoryCoverage.trim() : undefined,
1453
1455
  partialReason: typeof (result as any).partialReason === 'string' ? (result as any).partialReason.trim() : undefined,
1454
1456
  unavailableReason: typeof (result as any).unavailableReason === 'string' ? (result as any).unavailableReason.trim() : undefined,
@@ -1537,6 +1539,7 @@ export function readProviderChatHistory(
1537
1539
  sourcePath?: string;
1538
1540
  sourceMtimeMs?: number;
1539
1541
  providerSessionId?: string;
1542
+ workspace?: string;
1540
1543
  nativeHistoryCoverage?: string;
1541
1544
  partialReason?: string;
1542
1545
  unavailableReason?: string;
@@ -1550,6 +1553,7 @@ export function readProviderChatHistory(
1550
1553
  sourcePath: nativeResult.sourcePath,
1551
1554
  sourceMtimeMs: nativeResult.sourceMtimeMs,
1552
1555
  providerSessionId: nativeResult.providerSessionId,
1556
+ workspace: nativeResult.workspace,
1553
1557
  nativeHistoryCoverage: nativeResult.nativeHistoryCoverage,
1554
1558
  partialReason: nativeResult.partialReason,
1555
1559
  unavailableReason: nativeResult.unavailableReason,
@@ -66,7 +66,7 @@ export interface GitPushResult extends GitRepoIdentity {
66
66
  }
67
67
 
68
68
  export interface GitCommandServices {
69
- getStatus?: (params: { workspace: string; refreshUpstream?: boolean }) => Promise<GitRepoStatus> | GitRepoStatus;
69
+ getStatus?: (params: { workspace: string; refreshUpstream?: boolean; includeSubmodules?: boolean; submoduleIgnorePaths?: string[] }) => Promise<GitRepoStatus> | GitRepoStatus;
70
70
  getDiffSummary?: (params: { workspace: string; staged?: boolean }) => Promise<GitDiffSummary> | GitDiffSummary;
71
71
  getDiffFile?: (params: { workspace: string; path: string; staged?: boolean }) => Promise<GitFileDiff> | GitFileDiff;
72
72
  createSnapshot?: (params: {
@@ -294,7 +294,16 @@ export async function handleGitCommand(
294
294
  switch (command) {
295
295
  case 'git_status': {
296
296
  if (!services.getStatus) return serviceNotImplemented(command);
297
- const status = await runService(() => services.getStatus!({ workspace, refreshUpstream: optionalBoolean(args?.refreshUpstream) }));
297
+ const submoduleIgnorePaths = Array.isArray(args?.submoduleIgnorePaths)
298
+ ? args.submoduleIgnorePaths.filter((value: unknown): value is string => typeof value === 'string' && value.trim().length > 0)
299
+ : undefined;
300
+ const statusParams: { workspace: string; refreshUpstream?: boolean; includeSubmodules?: boolean; submoduleIgnorePaths?: string[] } = { workspace };
301
+ const refreshUpstream = optionalBoolean(args?.refreshUpstream);
302
+ const includeSubmodules = optionalBoolean(args?.includeSubmodules);
303
+ if (refreshUpstream !== undefined) statusParams.refreshUpstream = refreshUpstream;
304
+ if (includeSubmodules !== undefined) statusParams.includeSubmodules = includeSubmodules;
305
+ if (submoduleIgnorePaths && submoduleIgnorePaths.length > 0) statusParams.submoduleIgnorePaths = submoduleIgnorePaths;
306
+ const status = await runService(() => services.getStatus!(statusParams));
298
307
  return 'success' in status ? status : { success: true, status };
299
308
  }
300
309
 
@@ -446,6 +455,15 @@ async function gitCheckpoint(
446
455
  if (statusResult.hasConflicts) {
447
456
  throw new GitCommandError('conflict', 'Repository has conflicts — resolve before checkpointing');
448
457
  }
458
+ const dirtySubmodules = (statusResult.submodules || []).filter(submodule => submodule.dirty);
459
+ if (dirtySubmodules.length > 0) {
460
+ const paths = dirtySubmodules.map(submodule => submodule.path).join(', ');
461
+ throw new GitCommandError(
462
+ 'dirty_index_required',
463
+ `Repository has dirty submodules that must be checkpointed first: ${paths}. ` +
464
+ 'Checkpoint or commit each dirty submodule, then checkpoint this repository to record gitlink changes.',
465
+ );
466
+ }
449
467
 
450
468
  const addArgs = includeUntracked ? ['-A'] : ['-u'];
451
469
  await runGit(repo, ['add', ...addArgs], { cwd: repoRoot });
@@ -48,6 +48,11 @@ export async function getGitRepoStatus(
48
48
  if (includeSubmodules) {
49
49
  submodules = await getSubmoduleStatuses(repo, options);
50
50
  }
51
+ const submoduleDirty = (submodules || []).some(submodule => submodule.dirty || submodule.outOfSync || !!submodule.error);
52
+ const dirty = parsed.staged + parsed.modified + parsed.untracked + parsed.deleted + parsed.renamed > 0
53
+ || parsed.conflictFiles.length > 0
54
+ || stashCount > 0
55
+ || submoduleDirty;
51
56
 
52
57
  return {
53
58
  workspace: repo.workspace,
@@ -67,6 +72,7 @@ export async function getGitRepoStatus(
67
72
  untracked: parsed.untracked,
68
73
  deleted: parsed.deleted,
69
74
  renamed: parsed.renamed,
75
+ dirty,
70
76
  hasConflicts: parsed.conflictFiles.length > 0,
71
77
  conflictFiles: parsed.conflictFiles,
72
78
  stashCount,
@@ -285,6 +291,7 @@ function emptyStatus(workspace: string, lastCheckedAt: number, error: GitCommand
285
291
  untracked: 0,
286
292
  deleted: 0,
287
293
  renamed: 0,
294
+ dirty: false,
288
295
  hasConflicts: false,
289
296
  conflictFiles: [],
290
297
  stashCount: 0,
@@ -304,12 +311,34 @@ async function getSubmoduleStatuses(
304
311
 
305
312
  try {
306
313
  const result = await runGit(repo, ['submodule', 'status', '--recursive'], options);
307
- return parseSubmoduleStatusOutput(result.stdout, repo.repoRoot, options.submoduleIgnorePaths);
314
+ const submodules = parseSubmoduleStatusOutput(result.stdout, repo.repoRoot, options.submoduleIgnorePaths);
315
+ await Promise.all(submodules.map(submodule => enrichSubmoduleWorktreeStatus(repo, submodule, options)));
316
+ return submodules;
308
317
  } catch {
309
318
  return [];
310
319
  }
311
320
  }
312
321
 
322
+ async function enrichSubmoduleWorktreeStatus(
323
+ repo: ResolvedGitRepo,
324
+ submodule: GitSubmoduleStatus,
325
+ options: GitStatusOptions,
326
+ ): Promise<void> {
327
+ try {
328
+ const result = await runGit(repo, ['status', '--porcelain=v2', '--branch'], {
329
+ ...options,
330
+ cwd: submodule.repoPath,
331
+ });
332
+ const parsed = parsePorcelainV2Status(result.stdout);
333
+ const dirty = parsed.staged + parsed.modified + parsed.untracked + parsed.deleted + parsed.renamed > 0
334
+ || parsed.conflictFiles.length > 0;
335
+ submodule.dirty = submodule.dirty || dirty;
336
+ } catch (error) {
337
+ submodule.dirty = true;
338
+ submodule.error = formatGitError(error);
339
+ }
340
+ }
341
+
313
342
  function parseSubmoduleStatusOutput(
314
343
  output: string,
315
344
  repoRoot: string,
@@ -321,9 +350,9 @@ function parseSubmoduleStatusOutput(
321
350
  for (const line of output.split('\n')) {
322
351
  if (!line.trim()) continue;
323
352
 
324
- // Format: [+- ]<commit> <path> (<branch>)
325
- // - = out of sync, + = dirty, ' ' = clean
326
- const match = line.match(/^([\-+\s])([0-9a-f]{40})\s+(\S+)(?:\s+\(([^)]+)\))?/);
353
+ // Format: [+-U ]<commit> <path> (<branch>)
354
+ // - = not initialized, + = gitlink out of sync, U = conflict, ' ' = aligned.
355
+ const match = line.match(/^([\-+U\s])([0-9a-f]{40})\s+(\S+)(?:\s+\(([^)]+)\))?/);
327
356
  if (!match) continue;
328
357
 
329
358
  const prefix = match[1];
@@ -336,8 +365,8 @@ function parseSubmoduleStatusOutput(
336
365
  path,
337
366
  commit,
338
367
  repoPath: repoRoot + '/' + path,
339
- dirty: prefix === '+',
340
- outOfSync: prefix === '-',
368
+ dirty: prefix === 'U',
369
+ outOfSync: prefix === '-' || prefix === '+',
341
370
  lastCheckedAt: Date.now(),
342
371
  });
343
372
  }
@@ -60,6 +60,8 @@ export interface GitRepoStatus extends GitRepoIdentity {
60
60
  untracked: number;
61
61
  deleted: number;
62
62
  renamed: number;
63
+ /** Aggregate dirty flag including root worktree changes, conflicts, stash, and submodule drift. */
64
+ dirty: boolean;
63
65
  hasConflicts: boolean;
64
66
  conflictFiles: string[];
65
67
  stashCount: number;
package/src/index.ts CHANGED
@@ -229,7 +229,7 @@ export { buildMeshHostRequiredFailure, createDefaultMeshHostMetadata, isMeshHost
229
229
  // export type { MeshGraph, MeshGraphNode, MeshGraphEdge, MeshGraphNodeType, MeshGraphEdgeType } from './mesh/mesh-visualization.js';
230
230
 
231
231
  // ── Mesh Events ──
232
- export { triggerMeshQueue, drainPendingMeshCoordinatorEvents, getPendingMeshCoordinatorEvents, clearPendingMeshCoordinatorEvents, queuePendingMeshCoordinatorEvent } from './mesh/mesh-events.js';
232
+ export { triggerMeshQueue, drainPendingMeshCoordinatorEvents, getPendingMeshCoordinatorEvents, clearPendingMeshCoordinatorEvents, queuePendingMeshCoordinatorEvent, reconcileDirectDispatchCompletionFromTranscript } from './mesh/mesh-events.js';
233
233
  export type { PendingMeshCoordinatorEvent } from './mesh/mesh-events.js';
234
234
 
235
235
  // ── Mesh P2P Relay Failure Classification ──
@@ -275,7 +275,7 @@ export type { CdpInitializerConfig } from './cdp/initializer.js';
275
275
  // ── Commands ──
276
276
  export { DaemonCommandHandler } from './commands/handler.js';
277
277
  export type { CommandResult, CommandContext } from './commands/handler.js';
278
- export { DaemonCommandRouter } from './commands/router.js';
278
+ export { DaemonCommandRouter, readCachedInlineMeshActiveSessionDetails } from './commands/router.js';
279
279
  export type { CommandRouterDeps, CommandRouterResult } from './commands/router.js';
280
280
  export {
281
281
  maybeRunDaemonUpgradeHelperFromEnv,
@@ -474,6 +474,10 @@ export class BeadsDB {
474
474
  `).run(cutoff);
475
475
  }
476
476
 
477
+ deleteDirectDispatches(meshId: string): void {
478
+ this.db.prepare(`DELETE FROM mesh_direct_dispatches WHERE mesh_id = ?`).run(meshId);
479
+ }
480
+
477
481
  markStaleDirectDispatches(meshId: string, olderThanMs: number): void {
478
482
  const cutoff = new Date(Date.now() - olderThanMs).toISOString();
479
483
  const now = new Date().toISOString();