@adhdev/daemon-core 0.9.82-rc.65 → 0.9.82-rc.67

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.
@@ -28,10 +28,7 @@ export interface InstallResult {
28
28
  alreadyInstalled: boolean;
29
29
  error?: string;
30
30
  }
31
- /**
32
- * Check if an extension is already installed
33
- */
34
- export declare function isExtensionInstalled(ide: IDEInfo, marketplaceId: string): boolean;
31
+ export declare function isExtensionInstalled(ide: IDEInfo, marketplaceId: string): Promise<boolean>;
35
32
  /**
36
33
  * Install a single extension
37
34
  */
package/dist/launch.d.ts CHANGED
@@ -18,7 +18,7 @@
18
18
  /** Kill IDE process (graceful → force) */
19
19
  export declare function killIdeProcess(ideId: string): Promise<boolean>;
20
20
  /** Check if IDE process is running */
21
- export declare function isIdeRunning(ideId: string): boolean;
21
+ export declare function isIdeRunning(ideId: string): Promise<boolean>;
22
22
  export interface LaunchOptions {
23
23
  ideId?: string;
24
24
  workspace?: string;
@@ -0,0 +1,10 @@
1
+ export declare class AsyncBatchWriter {
2
+ private static buffers;
3
+ private static writePromises;
4
+ private static flushTimer;
5
+ /**
6
+ * Queues data to be written to a file asynchronously in a batch.
7
+ */
8
+ static write(filePath: string, data: string): void;
9
+ private static flushAll;
10
+ }
@@ -0,0 +1,18 @@
1
+ import type { MeshTaskStatus, MeshWorkQueueEntry } from './mesh-work-queue.js';
2
+ export declare class BeadsDB {
3
+ private static instance;
4
+ private readonly db;
5
+ private readonly migratedMeshIds;
6
+ private constructor();
7
+ static getInstance(): BeadsDB;
8
+ static resetForTests(): void;
9
+ close(): void;
10
+ transaction<T>(fn: () => T): T;
11
+ private migrate;
12
+ private ensureLegacyQueueMigrated;
13
+ getQueueEntries(meshId: string, statuses?: MeshTaskStatus[]): MeshWorkQueueEntry[];
14
+ getQueueRevision(meshId: string): string;
15
+ replaceQueue(meshId: string, queue: MeshWorkQueueEntry[]): void;
16
+ deleteQueue(meshId: string): void;
17
+ private toRow;
18
+ }
@@ -66,6 +66,7 @@ export declare function enqueueTask(meshId: string, message: string, opts?: {
66
66
  export declare function getQueue(meshId: string, opts?: {
67
67
  status?: MeshTaskStatus[];
68
68
  }): MeshWorkQueueEntry[];
69
+ export declare function getMeshQueueRevision(meshId: string): string;
69
70
  /**
70
71
  * Find the next pending task that this node is allowed to claim, and mark it as assigned.
71
72
  */
@@ -123,3 +124,6 @@ export interface MeshWorkQueueStats {
123
124
  * Return aggregate queue statistics for the given mesh.
124
125
  */
125
126
  export declare function getMeshQueueStats(meshId: string): MeshWorkQueueStats;
127
+ export declare function __replaceMeshQueueForTests(meshId: string, queue: MeshWorkQueueEntry[]): void;
128
+ export declare function __clearMeshQueueForTests(meshId: string): void;
129
+ export declare function __resetBeadsDBForTests(): void;
@@ -46,6 +46,8 @@ export declare class DaemonStatusReporter {
46
46
  private lastStatusSentAt;
47
47
  private statusPendingThrottle;
48
48
  private lastP2PStatusHash;
49
+ private lastP2PStatusSentAt;
50
+ private p2pDebounceTimer;
49
51
  private lastServerStatusHash;
50
52
  private lastStatusSummary;
51
53
  private statusTimer;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.65",
3
+ "version": "0.9.82-rc.67",
4
4
  "description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -49,6 +49,7 @@
49
49
  "@adhdev/session-host-core": "*",
50
50
  "@agentclientprotocol/sdk": "^0.16.1",
51
51
  "@xterm/xterm": "^6.0.0",
52
+ "better-sqlite3": "^12.10.0",
52
53
  "chalk": "^5.3.0",
53
54
  "chokidar": "^4.0.3",
54
55
  "conf": "^13.0.0",
@@ -60,6 +61,7 @@
60
61
  "@adhdev/ghostty-vt-node": "*"
61
62
  },
62
63
  "devDependencies": {
64
+ "@types/better-sqlite3": "^7.6.13",
63
65
  "@types/js-yaml": "^4.0.9",
64
66
  "@types/node": "^22.0.0",
65
67
  "@types/ws": "^8.18.1",
@@ -484,17 +484,25 @@ export function findBinary(name: string): string {
484
484
  return path.isAbsolute(expanded) ? expanded : path.resolve(expanded);
485
485
  }
486
486
  const isWin = os.platform() === 'win32';
487
- try {
488
- const cmd = isWin ? `where ${trimmed}` : `which ${trimmed}`;
489
- return execSync(cmd, {
490
- encoding: 'utf-8',
491
- timeout: 5000,
492
- stdio: ['pipe', 'pipe', 'pipe'],
493
- ...(isWin ? { windowsHide: true } : {}),
494
- }).trim().split('\n')[0].trim();
495
- } catch {
496
- return isWin ? `${trimmed}.cmd` : trimmed;
487
+ const paths = (process.env.PATH || '').split(path.delimiter);
488
+ const exes = isWin ? ['.exe', '.cmd', '.bat', ''] : [''];
489
+
490
+ for (const p of paths) {
491
+ if (!p) continue;
492
+ for (const ext of exes) {
493
+ const fullPath = path.join(p, trimmed + ext);
494
+ try {
495
+ const fs = require('fs');
496
+ if (fs.existsSync(fullPath)) {
497
+ const stat = fs.statSync(fullPath);
498
+ if (stat.isFile() && (isWin || (stat.mode & 0o111))) {
499
+ return fullPath;
500
+ }
501
+ }
502
+ } catch { }
503
+ }
497
504
  }
505
+ return isWin ? `${trimmed}.cmd` : trimmed;
498
506
  }
499
507
 
500
508
  export function isScriptBinary(binaryPath: string): boolean {
@@ -1,9 +1,6 @@
1
- import { execFileSync } from 'node:child_process'
2
1
  import { createHash } from 'node:crypto'
3
- import { existsSync, readdirSync, realpathSync } from 'node:fs'
4
- import { createRequire } from 'node:module'
5
2
  import * as os from 'node:os'
6
- import { dirname, isAbsolute, join, resolve } from 'node:path'
3
+ import { isAbsolute, join, resolve } from 'node:path'
7
4
  import type { ProviderModule, MeshCoordinatorMcpConfigFormat } from '../providers/contracts.js'
8
5
 
9
6
  export interface MeshCoordinatorMcpServerLaunch {
@@ -55,7 +52,7 @@ export interface ResolveMeshCoordinatorSetupOptions {
55
52
  }
56
53
 
57
54
  const DEFAULT_SERVER_NAME = 'adhdev-mesh'
58
- const DEFAULT_ADHDEV_MCP_COMMAND = 'adhdev-mcp'
55
+ const DEFAULT_ADHDEV_MCP_COMMAND = 'adhdev'
59
56
  const HERMES_CLI_TYPE = 'hermes-cli'
60
57
  const HERMES_MCP_CONFIG_PATH = '~/.hermes/config.yaml'
61
58
 
@@ -67,8 +64,7 @@ function isHermesProvider(provider: ProviderModule | null | undefined, cliType?:
67
64
  function resolveHermesMeshCoordinatorSetup(options: ResolveMeshCoordinatorSetupOptions): MeshCoordinatorSetup {
68
65
  const mcpServer = resolveAdhdevMcpServerLaunch({
69
66
  meshId: options.meshId,
70
- nodeExecutable: options.nodeExecutable,
71
- adhdevMcpEntryPath: options.adhdevMcpEntryPath,
67
+ adhdevMcpCommand: options.adhdevMcpCommand,
72
68
  adhdevMcpTransport: options.adhdevMcpTransport,
73
69
  adhdevMcpPort: options.adhdevMcpPort,
74
70
  })
@@ -100,7 +96,7 @@ export function createHermesManualMeshCoordinatorSetup(meshId: string, workspace
100
96
  requiresRestart: true,
101
97
  instructions: 'Hermes CLI does not auto-import repo-local .mcp.json. Add this MCP server to Hermes config under mcp_servers, then start a fresh Hermes session.',
102
98
  template: renderMeshCoordinatorTemplate(
103
- 'mcp_servers:\n {{serverName}}:\n command: {{adhdevMcpCommand}}\n args:\n - --repo-mesh\n - {{meshId}}\n enabled: true\n',
99
+ 'mcp_servers:\n {{serverName}}:\n command: {{adhdevMcpCommand}}\n args:\n - mcp\n - --mode\n - ipc\n - --repo-mesh\n - {{meshId}}\n enabled: true\n',
104
100
  {
105
101
  meshId,
106
102
  workspace,
@@ -141,8 +137,7 @@ export function resolveMeshCoordinatorSetup(options: ResolveMeshCoordinatorSetup
141
137
  }
142
138
  const mcpServer = resolveAdhdevMcpServerLaunch({
143
139
  meshId,
144
- nodeExecutable: options.nodeExecutable,
145
- adhdevMcpEntryPath: options.adhdevMcpEntryPath,
140
+ adhdevMcpCommand: options.adhdevMcpCommand,
146
141
  adhdevMcpTransport: options.adhdevMcpTransport,
147
142
  adhdevMcpPort: options.adhdevMcpPort,
148
143
  })
@@ -222,25 +217,25 @@ function resolveMcpConfigPath(configPath: string, workspace: string): string {
222
217
 
223
218
  function resolveAdhdevMcpServerLaunch(options: {
224
219
  meshId: string
225
- nodeExecutable?: string
226
- adhdevMcpEntryPath?: string
220
+ adhdevMcpCommand?: string
227
221
  adhdevMcpTransport?: 'local' | 'ipc'
228
222
  adhdevMcpPort?: number
229
223
  }): MeshCoordinatorMcpServerLaunch | null {
230
- const entryPath = resolveAdhdevMcpEntryPath(options.adhdevMcpEntryPath)
231
- if (!entryPath) return null
232
- const nodeExecutable = resolveMcpNodeExecutable(options.nodeExecutable)
233
- if (!nodeExecutable) return null
224
+ const command = resolveAdhdevCommand(options.adhdevMcpCommand)
234
225
  const transport = resolveMcpTransport(options.adhdevMcpTransport)
235
- const args = [entryPath, '--mode', transport, '--repo-mesh', options.meshId]
226
+ const args = ['mcp', '--mode', transport, '--repo-mesh', options.meshId]
236
227
  const port = resolveMcpPort(options.adhdevMcpPort)
237
228
  if (port !== undefined) args.push('--port', String(port))
238
229
  return {
239
- command: nodeExecutable,
230
+ command,
240
231
  args,
241
232
  }
242
233
  }
243
234
 
235
+ function resolveAdhdevCommand(explicitCommand?: string): string {
236
+ return explicitCommand?.trim() || process.env.ADHDEV_COORDINATOR_MCP_COMMAND?.trim() || DEFAULT_ADHDEV_MCP_COMMAND
237
+ }
238
+
244
239
  function resolveMcpTransport(explicitTransport?: 'local' | 'ipc'): 'local' | 'ipc' {
245
240
  if (explicitTransport === 'local' || explicitTransport === 'ipc') return explicitTransport
246
241
  const envTransport = process.env.ADHDEV_COORDINATOR_MCP_TRANSPORT?.trim()
@@ -254,128 +249,3 @@ function resolveMcpPort(explicitPort?: number): number | undefined {
254
249
  const parsed = Number(raw)
255
250
  return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined
256
251
  }
257
-
258
- function resolveMcpNodeExecutable(explicitExecutable?: string): string | null {
259
- const explicit = explicitExecutable?.trim()
260
- if (explicit) return explicit
261
-
262
- const candidates: string[] = []
263
- const addCandidate = (candidate?: string | null) => {
264
- const trimmed = candidate?.trim()
265
- if (!trimmed) return
266
- const normalized = normalizeExistingPath(trimmed) || trimmed
267
- if (!candidates.includes(normalized)) candidates.push(normalized)
268
- }
269
-
270
- addCandidate(process.env.ADHDEV_MCP_NODE_EXECUTABLE)
271
- addCandidate(process.env.ADHDEV_NODE_EXECUTABLE)
272
- addCandidate(process.env.npm_node_execpath)
273
- addNodeCandidatesFromPath(process.env.PATH, addCandidate)
274
- addNodeCandidatesFromNvm(os.homedir(), addCandidate)
275
- addCandidate('/opt/homebrew/bin/node')
276
- addCandidate('/usr/local/bin/node')
277
- addCandidate('/usr/bin/node')
278
- addCandidate(process.execPath)
279
-
280
- for (const candidate of candidates) {
281
- if (nodeRuntimeSupportsWebSocket(candidate)) return candidate
282
- }
283
- return null
284
- }
285
-
286
- function addNodeCandidatesFromPath(pathValue: string | undefined, addCandidate: (candidate?: string | null) => void) {
287
- for (const entry of (pathValue || '').split(':')) {
288
- const dir = entry.trim()
289
- if (!dir) continue
290
- addCandidate(join(dir, 'node'))
291
- }
292
- }
293
-
294
- function addNodeCandidatesFromNvm(homeDir: string, addCandidate: (candidate?: string | null) => void) {
295
- const versionsDir = join(homeDir, '.nvm', 'versions', 'node')
296
- try {
297
- const versionDirs = readdirSync(versionsDir, { withFileTypes: true })
298
- .filter((entry) => entry.isDirectory())
299
- .map((entry) => entry.name)
300
- .sort(compareNodeVersionNamesDescending)
301
- for (const versionDir of versionDirs) {
302
- addCandidate(join(versionsDir, versionDir, 'bin', 'node'))
303
- }
304
- } catch {
305
- // nvm is optional; PATH and process.execPath candidates still cover normal installs.
306
- }
307
- }
308
-
309
- function compareNodeVersionNamesDescending(a: string, b: string): number {
310
- const parse = (value: string) => value.replace(/^v/, '').split('.').map((part) => Number.parseInt(part, 10) || 0)
311
- const left = parse(a)
312
- const right = parse(b)
313
- for (let i = 0; i < Math.max(left.length, right.length); i++) {
314
- const diff = (right[i] || 0) - (left[i] || 0)
315
- if (diff !== 0) return diff
316
- }
317
- return b.localeCompare(a)
318
- }
319
-
320
- function nodeRuntimeSupportsWebSocket(nodeExecutable: string): boolean {
321
- try {
322
- execFileSync(nodeExecutable, ['-e', "process.exit(typeof WebSocket === 'function' ? 0 : 42)"], {
323
- stdio: 'ignore',
324
- timeout: 3000,
325
- })
326
- return true
327
- } catch {
328
- return false
329
- }
330
- }
331
-
332
- function resolveAdhdevMcpEntryPath(explicitPath?: string): string | null {
333
- const explicit = explicitPath?.trim()
334
- if (explicit) return normalizeExistingPath(explicit) || explicit
335
-
336
- const envPath = process.env.ADHDEV_MCP_SERVER_PATH?.trim()
337
- if (envPath) return normalizeExistingPath(envPath) || envPath
338
-
339
- const candidates: string[] = []
340
- const addCandidate = (candidate: string) => {
341
- if (!candidates.includes(candidate)) candidates.push(candidate)
342
- }
343
- const addPackagedCandidates = (baseFile?: string) => {
344
- if (!baseFile) return
345
- const realBase = normalizeExistingPath(baseFile) || baseFile
346
- const dir = dirname(realBase)
347
- addCandidate(resolve(dir, '../vendor/mcp-server/index.js'))
348
- addCandidate(resolve(dir, '../../vendor/mcp-server/index.js'))
349
- addCandidate(resolve(dir, '../../../vendor/mcp-server/index.js'))
350
- // Source checkout/dev mode does not vendor the MCP server into daemon-standalone.
351
- // Resolve the sibling workspace build directly so Repo Mesh auto-import still
352
- // writes an absolute Node entrypoint instead of falling back to a PATH bin shim.
353
- addCandidate(resolve(dir, '../../mcp-server/dist/index.js'))
354
- addCandidate(resolve(dir, '../../../mcp-server/dist/index.js'))
355
- }
356
-
357
- addPackagedCandidates(process.argv[1])
358
-
359
- for (const candidate of candidates) {
360
- const normalized = normalizeExistingPath(candidate)
361
- if (normalized) return normalized
362
- }
363
-
364
- try {
365
- const requireBase = process.argv[1] ? (normalizeExistingPath(process.argv[1]) || process.argv[1]) : join(process.cwd(), 'adhdev-daemon.js')
366
- const req = createRequire(requireBase)
367
- const resolvedModule = req.resolve('@adhdev/mcp-server')
368
- return normalizeExistingPath(resolvedModule) || resolvedModule
369
- } catch {
370
- return null
371
- }
372
- }
373
-
374
- function normalizeExistingPath(filePath: string): string | null {
375
- try {
376
- if (!existsSync(filePath)) return null
377
- return realpathSync.native(filePath)
378
- } catch {
379
- return null
380
- }
381
- }
@@ -53,6 +53,7 @@ import {
53
53
  import { buildMachineInfo, buildStatusSnapshot } from '../status/snapshot.js';
54
54
  import { getSessionCompletionMarker } from '../status/snapshot.js';
55
55
  import { execNpmCommandSync, resolveCurrentGlobalInstallSurface, spawnDetachedDaemonUpgradeHelper } from './upgrade-helper.js';
56
+ import { getMeshQueueRevision } from '../mesh/mesh-work-queue.js';
56
57
  import type { RepoMeshSessionCleanupMode } from '../repo-mesh-types.js';
57
58
  import { homedir } from 'os';
58
59
  import { join as pathJoin, resolve as pathResolve } from 'path';
@@ -1084,6 +1085,24 @@ type MeshRefinePatchEquivalenceSummary = {
1084
1085
  stderr?: string;
1085
1086
  };
1086
1087
 
1088
+ type MeshRefineSubmoduleReachabilityEntry = {
1089
+ path: string;
1090
+ commit: string;
1091
+ reachable: boolean;
1092
+ checkedLocal?: boolean;
1093
+ fetchedFromOrigin?: boolean;
1094
+ error?: string;
1095
+ };
1096
+
1097
+ type MeshRefineSubmoduleReachabilitySummary = {
1098
+ status: MeshRefineStageStatus;
1099
+ checked: number;
1100
+ unreachable: MeshRefineSubmoduleReachabilityEntry[];
1101
+ entries: MeshRefineSubmoduleReachabilityEntry[];
1102
+ durationMs: number;
1103
+ error?: string;
1104
+ };
1105
+
1087
1106
  type MeshRefineAsyncJobStatus = 'accepted' | 'completed' | 'failed';
1088
1107
 
1089
1108
  type MeshRefineJobHandle = {
@@ -1216,6 +1235,95 @@ async function runMeshRefinePatchEquivalenceGate(
1216
1235
  }
1217
1236
  }
1218
1237
 
1238
+ async function runMeshRefineSubmoduleReachabilityGate(
1239
+ repoRoot: string,
1240
+ mergedTree: string,
1241
+ ): Promise<MeshRefineSubmoduleReachabilitySummary> {
1242
+ const startedAt = Date.now();
1243
+ const entries: MeshRefineSubmoduleReachabilityEntry[] = [];
1244
+ try {
1245
+ const { execFile } = await import('node:child_process');
1246
+ const { promisify } = await import('node:util');
1247
+ const execFileAsync = promisify(execFile);
1248
+ const runGit = async (cwd: string, args: string[]): Promise<string> => {
1249
+ const { stdout } = await execFileAsync('git', args, {
1250
+ cwd,
1251
+ encoding: 'utf8',
1252
+ timeout: 30_000,
1253
+ maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
1254
+ windowsHide: true,
1255
+ });
1256
+ return String(stdout || '');
1257
+ };
1258
+
1259
+ const treeOutput = await runGit(repoRoot, ['ls-tree', '-r', '-z', mergedTree]);
1260
+ const gitlinks = treeOutput
1261
+ .split('\0')
1262
+ .filter(Boolean)
1263
+ .map(record => {
1264
+ const match = /^160000\s+commit\s+([0-9a-f]{40})\t(.+)$/.exec(record);
1265
+ return match ? { commit: match[1], path: match[2] } : null;
1266
+ })
1267
+ .filter((entry): entry is { commit: string; path: string } => !!entry);
1268
+
1269
+ for (const gitlink of gitlinks) {
1270
+ const submodulePath = pathResolve(repoRoot, gitlink.path);
1271
+ const entry: MeshRefineSubmoduleReachabilityEntry = {
1272
+ path: gitlink.path,
1273
+ commit: gitlink.commit,
1274
+ reachable: false,
1275
+ };
1276
+ try {
1277
+ if (!fs.existsSync(submodulePath)) {
1278
+ entry.error = `Submodule checkout missing at ${gitlink.path}`;
1279
+ entries.push(entry);
1280
+ continue;
1281
+ }
1282
+
1283
+ entry.checkedLocal = true;
1284
+ try {
1285
+ await runGit(submodulePath, ['cat-file', '-e', `${gitlink.commit}^{commit}`]);
1286
+ entry.reachable = true;
1287
+ entries.push(entry);
1288
+ continue;
1289
+ } catch {
1290
+ // Probe the submodule remote before allowing cleanup/completion.
1291
+ }
1292
+
1293
+ try {
1294
+ await runGit(submodulePath, ['fetch', 'origin', gitlink.commit]);
1295
+ entry.fetchedFromOrigin = true;
1296
+ await runGit(submodulePath, ['cat-file', '-e', `${gitlink.commit}^{commit}`]);
1297
+ entry.reachable = true;
1298
+ } catch (e: any) {
1299
+ entry.error = truncateValidationOutput(e?.stderr || e?.message || String(e));
1300
+ }
1301
+ } catch (e: any) {
1302
+ entry.error = truncateValidationOutput(e?.message || String(e));
1303
+ }
1304
+ entries.push(entry);
1305
+ }
1306
+
1307
+ const unreachable = entries.filter(entry => !entry.reachable);
1308
+ return {
1309
+ status: unreachable.length ? 'failed' : 'passed',
1310
+ checked: entries.length,
1311
+ unreachable,
1312
+ entries,
1313
+ durationMs: Date.now() - startedAt,
1314
+ };
1315
+ } catch (e: any) {
1316
+ return {
1317
+ status: 'failed',
1318
+ checked: entries.length,
1319
+ unreachable: entries.filter(entry => !entry.reachable),
1320
+ entries,
1321
+ durationMs: Date.now() - startedAt,
1322
+ error: truncateValidationOutput(e?.message || String(e)),
1323
+ };
1324
+ }
1325
+ }
1326
+
1219
1327
  function buildMeshRefineValidationPlan(mesh: any, workspace: string): Record<string, unknown> {
1220
1328
  const plan = resolveMeshRefineValidationPlan(mesh, workspace);
1221
1329
  return {
@@ -1599,7 +1707,7 @@ export class DaemonCommandRouter {
1599
1707
  * the mesh doesn't exist in the local meshes.json file. */
1600
1708
  private inlineMeshCache = new Map<string, any>();
1601
1709
  /** Coordinator-owned whole-mesh aggregate status snapshots. Browser callers read this by default. */
1602
- private aggregateMeshStatusCache = new Map<string, { builtAt: number; snapshot: any }>();
1710
+ private aggregateMeshStatusCache = new Map<string, { builtAt: number; snapshot: any; queueRevision: string }>();
1603
1711
  /** In-memory async Refinery jobs keyed by meshId:nodeId to reject/return duplicate in-flight requests. */
1604
1712
  private runningRefineJobs = new Map<string, MeshRefineJobHandle>();
1605
1713
  /** Terminal async Refinery jobs preserve a clear answer after the worktree node has been removed. */
@@ -1689,6 +1797,7 @@ export class DaemonCommandRouter {
1689
1797
  private getCachedAggregateMeshStatus(meshId: string, mesh?: any, options?: { requireDirectPeerTruth?: boolean }): any | null {
1690
1798
  const cached = this.aggregateMeshStatusCache.get(meshId);
1691
1799
  if (!cached?.snapshot || cached.snapshot.success !== true || !Array.isArray(cached.snapshot.nodes)) return null;
1800
+ if (cached.queueRevision !== getMeshQueueRevision(meshId)) return null;
1692
1801
  let snapshot = this.cloneJsonValue(cached.snapshot);
1693
1802
  snapshot = this.hydrateCachedAggregateMeshStatusFromInline(snapshot, mesh, options);
1694
1803
  if (shouldRefreshStalePendingAggregate(snapshot, options)) return null;
@@ -1733,7 +1842,7 @@ export class DaemonCommandRouter {
1733
1842
  returnedAt: new Date(builtAt).toISOString(),
1734
1843
  },
1735
1844
  };
1736
- this.aggregateMeshStatusCache.set(meshId, { builtAt, snapshot: this.cloneJsonValue(next) });
1845
+ this.aggregateMeshStatusCache.set(meshId, { builtAt, snapshot: this.cloneJsonValue(next), queueRevision: getMeshQueueRevision(meshId) });
1737
1846
  return next;
1738
1847
  }
1739
1848
 
@@ -2566,6 +2675,38 @@ export class DaemonCommandRouter {
2566
2675
  };
2567
2676
  }
2568
2677
 
2678
+ const submoduleReachabilityStarted = Date.now();
2679
+ const submoduleReachability = await runMeshRefineSubmoduleReachabilityGate(repoRoot, patchEquivalence.mergedTree || branchHead);
2680
+ recordMeshRefineStage(refineStages, 'submodule_reachability', submoduleReachability.status, submoduleReachabilityStarted, {
2681
+ checked: submoduleReachability.checked,
2682
+ unreachable: submoduleReachability.unreachable.map(entry => ({ path: entry.path, commit: entry.commit, error: entry.error })),
2683
+ error: submoduleReachability.error,
2684
+ });
2685
+ if (submoduleReachability.status === 'failed') {
2686
+ return {
2687
+ success: false,
2688
+ code: 'submodule_reachability_failed',
2689
+ convergenceStatus: 'blocked_review',
2690
+ error: 'Refinery submodule reachability preflight failed; merge/refine cleanup was not attempted.',
2691
+ branch,
2692
+ into: baseBranch,
2693
+ validationSummary,
2694
+ patchEquivalence,
2695
+ submoduleReachability,
2696
+ refineStages,
2697
+ finalBranchConvergenceState: {
2698
+ branch,
2699
+ baseBranch,
2700
+ merged: false,
2701
+ removed: false,
2702
+ validation: 'passed',
2703
+ patchEquivalence: 'passed',
2704
+ submoduleReachability: 'failed',
2705
+ status: 'blocked_review',
2706
+ },
2707
+ };
2708
+ }
2709
+
2569
2710
  let mergeResult: Record<string, unknown> | undefined;
2570
2711
  const mergeStarted = Date.now();
2571
2712
  try {
@@ -4962,7 +5103,7 @@ export class DaemonCommandRouter {
4962
5103
 
4963
5104
  // 3. Kill OS process if requested
4964
5105
  if (killProcess) {
4965
- const running = isIdeRunning(ideType);
5106
+ const running = await isIdeRunning(ideType);
4966
5107
  if (running) {
4967
5108
  LOG.info('StopIDE', `Killing IDE process: ${ideType}`);
4968
5109
  const killed = await killIdeProcess(ideType);
@@ -7,8 +7,10 @@
7
7
  * Migrated from @adhdev/core — this is now the single source of truth.
8
8
  */
9
9
 
10
- import { execSync } from 'child_process';
11
- import { existsSync } from 'fs';
10
+ import { exec } from 'child_process';
11
+ import { promisify } from 'util';
12
+ const execAsync = promisify(exec);
13
+ import { existsSync, statSync } from 'fs';
12
14
  import { platform, homedir } from 'os';
13
15
  import * as path from 'path';
14
16
  import type { ProviderLoader } from '../providers/provider-loader.js';
@@ -73,25 +75,33 @@ function findCliCommand(command: string): string | null {
73
75
  const resolved = path.isAbsolute(candidate) ? candidate : path.resolve(candidate);
74
76
  return existsSync(resolved) ? resolved : null;
75
77
  }
76
- try {
77
- const result = execSync(
78
- platform() === 'win32' ? `where ${trimmed}` : `which ${trimmed}`,
79
- { encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }
80
- ).trim();
81
- return result.split('\n')[0] || null;
82
- } catch {
83
- return null;
78
+ const isWin = platform() === 'win32';
79
+ const paths = (process.env.PATH || '').split(isWin ? ';' : ':');
80
+ const exes = isWin ? ['.exe', '.cmd', '.bat', ''] : [''];
81
+ for (const p of paths) {
82
+ if (!p) continue;
83
+ for (const ext of exes) {
84
+ const fullPath = path.join(p, trimmed + ext);
85
+ try {
86
+ if (existsSync(fullPath)) {
87
+ const stat = statSync(fullPath);
88
+ if (stat.isFile() && (isWin || (stat.mode & 0o111))) {
89
+ return fullPath;
90
+ }
91
+ }
92
+ } catch { }
93
+ }
84
94
  }
95
+ return null;
85
96
  }
86
97
 
87
- function getIdeVersion(cliCommand: string): string | null {
98
+ async function getIdeVersion(cliCommand: string): Promise<string | null> {
88
99
  try {
89
- const result = execSync(`"${cliCommand}" --version`, {
100
+ const { stdout } = await execAsync(`"${cliCommand}" --version`, {
90
101
  encoding: 'utf-8',
91
102
  timeout: 10000,
92
- stdio: ['pipe', 'pipe', 'pipe'],
93
- }).trim();
94
- return result.split('\n')[0] || null;
103
+ });
104
+ return stdout.trim().split('\n')[0] || null;
95
105
  } catch {
96
106
  return null;
97
107
  }
@@ -152,7 +162,7 @@ export async function detectIDEs(providerLoader?: ProviderLoader): Promise<IDEIn
152
162
  const installed = os === 'darwin'
153
163
  ? !!(resolvedCli || appPath)
154
164
  : !!resolvedCli;
155
- const version = resolvedCli ? getIdeVersion(resolvedCli) : null;
165
+ const version = resolvedCli ? await getIdeVersion(resolvedCli) : null;
156
166
 
157
167
  results.push({
158
168
  id: def.id,
package/src/index.ts CHANGED
@@ -186,7 +186,7 @@ export { buildMeshLedgerReconciliationEvidence, buildMeshLedgerReplicaEvidence }
186
186
  export type { MeshLedgerReconciliationEvidence, MeshLedgerReplicaEvidence, MeshLedgerReplicaStatus } from './mesh/mesh-ledger-reconciliation.js';
187
187
 
188
188
  // ── Mesh Work Queue (GUPP) ──
189
- export { enqueueTask, getQueue, claimNextTask, updateTaskStatus, updateSessionTaskStatus, cancelTask, requeueTask, getMeshQueueStats, normalizeMeshTaskMode, validateMeshTaskModeRequest } from './mesh/mesh-work-queue.js';
189
+ export { enqueueTask, getQueue, claimNextTask, updateTaskStatus, updateSessionTaskStatus, cancelTask, requeueTask, getMeshQueueStats, getMeshQueueRevision, normalizeMeshTaskMode, validateMeshTaskModeRequest } from './mesh/mesh-work-queue.js';
190
190
  export type { MeshWorkQueueEntry, MeshTaskStatus, MeshTaskMode, MeshWorkQueueStats, MeshQueueMutationOptions, MeshTaskModeValidationResult } from './mesh/mesh-work-queue.js';
191
191
  export { buildMeshActiveWork, buildMeshActiveWorkSummary } from './mesh/mesh-active-work.js';
192
192
  export type { MeshActiveWorkRecord, MeshActiveWorkStatus, MeshActiveWorkSummary, MeshActiveWorkSource } from './mesh/mesh-active-work.js';
@@ -31,7 +31,7 @@ export interface InstallResult {
31
31
  /**
32
32
  * Check if an extension is already installed
33
33
  */
34
- export declare function isExtensionInstalled(ide: IDEInfo, marketplaceId: string): boolean;
34
+ export declare function isExtensionInstalled(ide: IDEInfo, marketplaceId: string): Promise<boolean>;
35
35
  /**
36
36
  * Install a single extension
37
37
  */