@adhdev/daemon-core 0.9.82-rc.13 → 0.9.82-rc.131

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 (82) hide show
  1. package/dist/chat/subscription-updates.d.ts +1 -0
  2. package/dist/cli-adapter-types.d.ts +4 -1
  3. package/dist/cli-adapters/provider-cli-adapter.d.ts +25 -1
  4. package/dist/cli-adapters/provider-cli-parse.d.ts +1 -0
  5. package/dist/cli-adapters/provider-cli-shared.d.ts +14 -0
  6. package/dist/commands/router.d.ts +22 -0
  7. package/dist/config/chat-history.d.ts +4 -0
  8. package/dist/config/mesh-config.d.ts +68 -1
  9. package/dist/git/git-commands.d.ts +5 -1
  10. package/dist/index.d.ts +15 -5
  11. package/dist/index.js +7538 -1380
  12. package/dist/index.js.map +1 -1
  13. package/dist/index.mjs +7494 -1364
  14. package/dist/index.mjs.map +1 -1
  15. package/dist/installer.d.ts +1 -4
  16. package/dist/launch.d.ts +1 -1
  17. package/dist/logging/async-batch-writer.d.ts +10 -0
  18. package/dist/mesh/beads-db.d.ts +18 -0
  19. package/dist/mesh/mesh-active-work.d.ts +73 -0
  20. package/dist/mesh/mesh-events.d.ts +54 -5
  21. package/dist/mesh/mesh-fast-forward.d.ts +39 -0
  22. package/dist/mesh/mesh-host-ownership.d.ts +9 -0
  23. package/dist/mesh/mesh-ledger.d.ts +38 -1
  24. package/dist/mesh/mesh-refine-status.d.ts +27 -0
  25. package/dist/mesh/mesh-work-queue.d.ts +27 -5
  26. package/dist/mesh/preview-freshness.d.ts +18 -0
  27. package/dist/mesh/refine-config.d.ts +193 -0
  28. package/dist/mesh/worktree-bootstrap-config.d.ts +115 -0
  29. package/dist/providers/chat-message-normalization.d.ts +1 -0
  30. package/dist/providers/cli-provider-instance.d.ts +6 -1
  31. package/dist/repo-mesh-types.d.ts +62 -0
  32. package/dist/status/reporter.d.ts +2 -0
  33. package/package.json +3 -1
  34. package/src/boot/daemon-lifecycle.ts +1 -0
  35. package/src/chat/subscription-updates.ts +5 -1
  36. package/src/cli-adapter-types.ts +2 -1
  37. package/src/cli-adapters/provider-cli-adapter.ts +473 -18
  38. package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
  39. package/src/cli-adapters/provider-cli-parse.ts +4 -0
  40. package/src/cli-adapters/provider-cli-runtime.ts +3 -1
  41. package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
  42. package/src/cli-adapters/provider-cli-shared.ts +32 -10
  43. package/src/commands/chat-commands.ts +1065 -40
  44. package/src/commands/cli-manager.ts +138 -2
  45. package/src/commands/handler.ts +8 -1
  46. package/src/commands/mesh-coordinator.ts +13 -143
  47. package/src/commands/router.ts +3238 -423
  48. package/src/config/chat-history.ts +37 -9
  49. package/src/config/mesh-config.ts +249 -2
  50. package/src/config/recent-activity.ts +8 -2
  51. package/src/daemon/dev-cli-debug.ts +10 -1
  52. package/src/detection/ide-detector.ts +26 -16
  53. package/src/git/git-commands.ts +17 -5
  54. package/src/index.ts +41 -4
  55. package/src/installer.d.ts +1 -1
  56. package/src/installer.ts +8 -6
  57. package/src/launch.d.ts +1 -1
  58. package/src/launch.ts +37 -28
  59. package/src/logging/async-batch-writer.ts +55 -0
  60. package/src/logging/logger.ts +2 -1
  61. package/src/mesh/beads-db.ts +176 -0
  62. package/src/mesh/coordinator-prompt.ts +31 -8
  63. package/src/mesh/mesh-active-work.ts +295 -0
  64. package/src/mesh/mesh-events.ts +595 -48
  65. package/src/mesh/mesh-fast-forward.ts +430 -0
  66. package/src/mesh/mesh-host-ownership.ts +73 -0
  67. package/src/mesh/mesh-ledger.ts +138 -1
  68. package/src/mesh/mesh-refine-status.ts +145 -0
  69. package/src/mesh/mesh-work-queue.ts +199 -137
  70. package/src/mesh/preview-freshness.ts +118 -0
  71. package/src/mesh/refine-config.ts +366 -0
  72. package/src/mesh/worktree-bootstrap-config.ts +234 -0
  73. package/src/providers/approval-utils.ts +12 -5
  74. package/src/providers/chat-message-normalization.ts +7 -12
  75. package/src/providers/cli-provider-instance.ts +289 -36
  76. package/src/providers/ide-provider-instance.ts +17 -3
  77. package/src/providers/provider-loader.ts +10 -4
  78. package/src/providers/read-chat-contract.ts +1 -1
  79. package/src/providers/version-archive.ts +38 -20
  80. package/src/repo-mesh-types.ts +67 -0
  81. package/src/status/reporter.ts +15 -0
  82. package/src/system/host-memory.ts +29 -12
@@ -1345,7 +1345,15 @@ function rewriteCanonicalSavedHistory(agentType: string, historySessionId: strin
1345
1345
 
1346
1346
  export type ProviderNativeHistoryScripts = Record<string, ((input: any) => any) | undefined>;
1347
1347
 
1348
- type ProviderNativeHistoryReadResult = { records: HistoryMessage[]; sourcePath: string; sourceMtimeMs: number };
1348
+ type ProviderNativeHistoryReadResult = {
1349
+ records: HistoryMessage[];
1350
+ sourcePath: string;
1351
+ sourceMtimeMs: number;
1352
+ providerSessionId?: string;
1353
+ nativeHistoryCoverage?: string;
1354
+ partialReason?: string;
1355
+ unavailableReason?: string;
1356
+ };
1349
1357
 
1350
1358
  function getNativeHistoryScriptName(canonicalHistory: ProviderCanonicalHistoryConfig | undefined, key: 'readSession' | 'listSessions'): string {
1351
1359
  const configured = canonicalHistory?.scripts?.[key];
@@ -1387,27 +1395,32 @@ function callProviderNativeHistoryRead(
1387
1395
  agentType: string,
1388
1396
  canonicalHistory: ProviderCanonicalHistoryConfig | undefined,
1389
1397
  scripts: ProviderNativeHistoryScripts | undefined,
1390
- historySessionId: string,
1398
+ historySessionId: string | undefined,
1391
1399
  workspace?: string,
1392
1400
  ): ProviderNativeHistoryReadResult | null {
1393
1401
  const fn = getProviderNativeHistoryScript(scripts, canonicalHistory, 'readSession');
1394
1402
  if (!fn) return null;
1403
+ const normalizedSessionId = normalizeSavedHistorySessionId(historySessionId || '');
1395
1404
  const result = fn({
1396
1405
  agentType,
1397
- sessionId: historySessionId,
1398
- historySessionId,
1406
+ sessionId: normalizedSessionId,
1407
+ historySessionId: normalizedSessionId,
1399
1408
  workspace,
1400
1409
  format: canonicalHistory?.format,
1401
1410
  watchPath: canonicalHistory?.watchPath,
1402
- args: { sessionId: historySessionId, historySessionId, workspace },
1411
+ args: { sessionId: normalizedSessionId, historySessionId: normalizedSessionId, workspace },
1403
1412
  });
1404
1413
  if (!result || typeof result !== 'object') return null;
1405
- const records = normalizeProviderNativeHistoryRecords(agentType, historySessionId, (result as any).messages || (result as any).records);
1414
+ const records = normalizeProviderNativeHistoryRecords(agentType, normalizedSessionId, (result as any).messages || (result as any).records);
1406
1415
  if (records.length === 0) return null;
1407
1416
  return {
1408
1417
  records,
1409
1418
  sourcePath: typeof (result as any).sourcePath === 'string' ? (result as any).sourcePath : '',
1410
1419
  sourceMtimeMs: Number((result as any).sourceMtimeMs) || 0,
1420
+ providerSessionId: typeof (result as any).providerSessionId === 'string' ? (result as any).providerSessionId.trim() : undefined,
1421
+ nativeHistoryCoverage: typeof (result as any).nativeHistoryCoverage === 'string' ? (result as any).nativeHistoryCoverage.trim() : undefined,
1422
+ partialReason: typeof (result as any).partialReason === 'string' ? (result as any).partialReason.trim() : undefined,
1423
+ unavailableReason: typeof (result as any).unavailableReason === 'string' ? (result as any).unavailableReason.trim() : undefined,
1411
1424
  };
1412
1425
  }
1413
1426
 
@@ -1419,7 +1432,8 @@ function buildNativeHistoryReadResult(
1419
1432
  workspace?: string,
1420
1433
  ): ProviderNativeHistoryReadResult | null {
1421
1434
  const normalizedSessionId = normalizeSavedHistorySessionId(historySessionId || '');
1422
- if (!canonicalHistory || !normalizedSessionId || !isNativeSourceCanonicalHistory(canonicalHistory)) return null;
1435
+ const normalizedWorkspace = typeof workspace === 'string' ? workspace.trim() : '';
1436
+ if (!canonicalHistory || (!normalizedSessionId && !normalizedWorkspace) || !isNativeSourceCanonicalHistory(canonicalHistory)) return null;
1423
1437
  return callProviderNativeHistoryRead(agentType, canonicalHistory, scripts, normalizedSessionId, workspace);
1424
1438
  }
1425
1439
 
@@ -1477,8 +1491,18 @@ export function readProviderChatHistory(
1477
1491
  historyBehavior?: ProviderHistoryBehavior;
1478
1492
  scripts?: ProviderNativeHistoryScripts;
1479
1493
  } = {},
1480
- ): { messages: HistoryMessage[]; hasMore: boolean; source: 'provider-native' | 'adhdev-mirror' | 'native-unavailable'; sourcePath?: string; sourceMtimeMs?: number } {
1481
- if (isNativeSourceCanonicalHistory(options.canonicalHistory) && options.historySessionId) {
1494
+ ): {
1495
+ messages: HistoryMessage[];
1496
+ hasMore: boolean;
1497
+ source: 'provider-native' | 'adhdev-mirror' | 'native-unavailable';
1498
+ sourcePath?: string;
1499
+ sourceMtimeMs?: number;
1500
+ providerSessionId?: string;
1501
+ nativeHistoryCoverage?: string;
1502
+ partialReason?: string;
1503
+ unavailableReason?: string;
1504
+ } {
1505
+ if (isNativeSourceCanonicalHistory(options.canonicalHistory) && (options.historySessionId || options.workspace)) {
1482
1506
  const nativeResult = buildNativeHistoryReadResult(agentType, options.canonicalHistory, options.scripts, options.historySessionId, options.workspace);
1483
1507
  if (!nativeResult) return { messages: [], hasMore: false, source: 'native-unavailable' };
1484
1508
  return {
@@ -1486,6 +1510,10 @@ export function readProviderChatHistory(
1486
1510
  source: 'provider-native',
1487
1511
  sourcePath: nativeResult.sourcePath,
1488
1512
  sourceMtimeMs: nativeResult.sourceMtimeMs,
1513
+ providerSessionId: nativeResult.providerSessionId,
1514
+ nativeHistoryCoverage: nativeResult.nativeHistoryCoverage,
1515
+ partialReason: nativeResult.partialReason,
1516
+ unavailableReason: nativeResult.unavailableReason,
1489
1517
  };
1490
1518
  }
1491
1519
  return {
@@ -8,7 +8,7 @@
8
8
 
9
9
  import { existsSync, readFileSync, writeFileSync } from 'fs';
10
10
  import { join } from 'path';
11
- import { randomUUID } from 'crypto';
11
+ import { createHash, randomBytes, randomUUID } from 'crypto';
12
12
  import { getConfigDir } from './config.js';
13
13
  import type {
14
14
  LocalMeshConfig,
@@ -18,8 +18,11 @@ import type {
18
18
  RepoMeshNodePolicy,
19
19
  RepoMeshNodeCapabilities,
20
20
  RepoMeshCoordinatorConfig,
21
+ RepoMeshHostMetadata,
22
+ RepoMeshDaemonRole,
21
23
  } from '../repo-mesh-types.js';
22
24
  import { DEFAULT_MESH_POLICY } from '../repo-mesh-types.js';
25
+ import { createDefaultMeshHostMetadata } from '../mesh/mesh-host-ownership.js';
23
26
 
24
27
  // ─── Persistence ────────────────────────────────
25
28
 
@@ -84,6 +87,7 @@ function mergeMeshPolicy(base: RepoMeshPolicy | undefined, patch: Partial<RepoMe
84
87
  }
85
88
  const maxParallelTasks = Number(policy.maxParallelTasks);
86
89
  policy.maxParallelTasks = Number.isFinite(maxParallelTasks) ? Math.max(1, Math.min(8, Math.floor(maxParallelTasks))) : 2;
90
+ policy.allowAutoPublishSubmoduleMainCommits = policy.allowAutoPublishSubmoduleMainCommits === true;
87
91
  if (!SESSION_CLEANUP_MODES.has(String(policy.sessionCleanupOnNodeRemove))) {
88
92
  policy.sessionCleanupOnNodeRemove = 'preserve';
89
93
  }
@@ -112,6 +116,7 @@ export interface CreateMeshOptions {
112
116
  defaultBranch?: string;
113
117
  policy?: Partial<RepoMeshPolicy>;
114
118
  coordinator?: RepoMeshCoordinatorConfig;
119
+ meshHost?: RepoMeshHostMetadata;
115
120
  }
116
121
 
117
122
  export function createMesh(opts: CreateMeshOptions): LocalMeshEntry {
@@ -133,6 +138,7 @@ export function createMesh(opts: CreateMeshOptions): LocalMeshEntry {
133
138
  defaultBranch: opts.defaultBranch,
134
139
  policy: mergeMeshPolicy(undefined, opts.policy),
135
140
  coordinator: opts.coordinator || {},
141
+ meshHost: opts.meshHost || createDefaultMeshHostMetadata(),
136
142
  nodes: [],
137
143
  createdAt: now,
138
144
  updatedAt: now,
@@ -148,6 +154,7 @@ export interface UpdateMeshOptions {
148
154
  defaultBranch?: string;
149
155
  policy?: Partial<RepoMeshPolicy>;
150
156
  coordinator?: RepoMeshCoordinatorConfig;
157
+ meshHost?: RepoMeshHostMetadata;
151
158
  }
152
159
 
153
160
  export function updateMesh(meshId: string, opts: UpdateMeshOptions): LocalMeshEntry | undefined {
@@ -159,6 +166,7 @@ export function updateMesh(meshId: string, opts: UpdateMeshOptions): LocalMeshEn
159
166
  if (opts.defaultBranch !== undefined) mesh.defaultBranch = opts.defaultBranch;
160
167
  if (opts.policy) mesh.policy = mergeMeshPolicy(mesh.policy, opts.policy);
161
168
  if (opts.coordinator) mesh.coordinator = opts.coordinator;
169
+ if (opts.meshHost) mesh.meshHost = opts.meshHost;
162
170
  mesh.updatedAt = new Date().toISOString();
163
171
 
164
172
  saveMeshConfig(config);
@@ -174,6 +182,240 @@ export function deleteMesh(meshId: string): boolean {
174
182
  return true;
175
183
  }
176
184
 
185
+ function normalizeManualHostAddress(hostAddress: string): string {
186
+ const normalized = hostAddress.trim().replace(/\/+$/, '');
187
+ if (!normalized) throw new Error('hostAddress required');
188
+ let parsed: URL;
189
+ try {
190
+ parsed = new URL(normalized);
191
+ } catch {
192
+ throw new Error('hostAddress must be a valid http(s) or ws(s) URL');
193
+ }
194
+ if (!['http:', 'https:', 'ws:', 'wss:'].includes(parsed.protocol)) {
195
+ throw new Error('hostAddress must use http, https, ws, or wss');
196
+ }
197
+ return normalized;
198
+ }
199
+
200
+ export function tokenIdForManualPairing(token: string): string {
201
+ return `tok_${createHash('sha256').update(token).digest('hex').slice(0, 16)}`;
202
+ }
203
+
204
+ function normalizeTokenExpiry(value: unknown): string | undefined {
205
+ if (typeof value !== 'string' || !value.trim()) return undefined;
206
+ const date = new Date(value);
207
+ if (Number.isNaN(date.getTime())) throw new Error('expiresAt must be a valid ISO date');
208
+ return date.toISOString();
209
+ }
210
+
211
+ function assertPairingTokenValid(pairing: RepoMeshHostMetadata['pairing'], rawToken: string, nowIso: string): { ok: true; tokenId: string } | { ok: false; reason: string; expectedTokenId?: string; presentedTokenId?: string } {
212
+ const token = rawToken.trim();
213
+ if (!token) return { ok: false, reason: 'token required' };
214
+ const presentedTokenId = tokenIdForManualPairing(token);
215
+ const expectedTokenId = pairing?.tokenId;
216
+ if (!expectedTokenId || pairing?.status === 'not_configured' || pairing?.status === 'revoked') {
217
+ return { ok: false, reason: 'host pairing token is not configured', presentedTokenId };
218
+ }
219
+ if (pairing.expiresAt && new Date(pairing.expiresAt).getTime() <= new Date(nowIso).getTime()) {
220
+ return { ok: false, reason: 'host pairing token expired', expectedTokenId, presentedTokenId };
221
+ }
222
+ if (presentedTokenId !== expectedTokenId) {
223
+ return { ok: false, reason: 'invalid pairing token', expectedTokenId, presentedTokenId };
224
+ }
225
+ return { ok: true, tokenId: presentedTokenId };
226
+ }
227
+
228
+ export interface ConfigureMeshHostPairingOptions {
229
+ hostAddress: string;
230
+ token: string;
231
+ now?: string;
232
+ }
233
+
234
+ export function configureMeshHostPairing(
235
+ meshId: string,
236
+ opts: ConfigureMeshHostPairingOptions,
237
+ ): { mesh: LocalMeshEntry; meshHost: RepoMeshHostMetadata; hostAddress: string } | undefined {
238
+ const hostAddress = normalizeManualHostAddress(opts.hostAddress);
239
+ const token = opts.token.trim();
240
+ if (!token) throw new Error('token required');
241
+
242
+ const config = loadMeshConfig();
243
+ const mesh = config.meshes.find(m => m.id === meshId);
244
+ if (!mesh) return undefined;
245
+
246
+ const now = opts.now || new Date().toISOString();
247
+ const previous = mesh.meshHost || createDefaultMeshHostMetadata();
248
+ const meshHost: RepoMeshHostMetadata = {
249
+ ...previous,
250
+ role: 'member',
251
+ hostAddress,
252
+ pairing: {
253
+ status: 'pairing',
254
+ tokenId: tokenIdForManualPairing(token),
255
+ lastPairedAt: now,
256
+ },
257
+ };
258
+
259
+ mesh.meshHost = meshHost;
260
+ mesh.updatedAt = now;
261
+ saveMeshConfig(config);
262
+ return { mesh, meshHost, hostAddress };
263
+ }
264
+
265
+ export interface CreateMeshHostPairingTokenOptions {
266
+ token?: string;
267
+ expiresAt?: string;
268
+ now?: string;
269
+ }
270
+
271
+ export function createMeshHostPairingToken(
272
+ meshId: string,
273
+ opts: CreateMeshHostPairingTokenOptions = {},
274
+ ): { mesh: LocalMeshEntry; meshHost: RepoMeshHostMetadata; token: string; tokenId: string; expiresAt?: string } | undefined {
275
+ const config = loadMeshConfig();
276
+ const mesh = config.meshes.find(m => m.id === meshId);
277
+ if (!mesh) return undefined;
278
+ const now = opts.now || new Date().toISOString();
279
+ const token = (opts.token || `mhj_${randomBytes(24).toString('base64url')}`).trim();
280
+ if (!token) throw new Error('token required');
281
+ const tokenId = tokenIdForManualPairing(token);
282
+ const expiresAt = normalizeTokenExpiry(opts.expiresAt);
283
+ const previous = mesh.meshHost || createDefaultMeshHostMetadata();
284
+ if (previous.role === 'member') {
285
+ throw new Error('Mesh Host daemon required to create host pairing tokens; member daemons cannot mint host join tokens.');
286
+ }
287
+ const meshHost: RepoMeshHostMetadata = {
288
+ ...previous,
289
+ role: 'host',
290
+ pairing: {
291
+ status: 'pairing',
292
+ tokenId,
293
+ lastPairedAt: now,
294
+ ...(expiresAt ? { expiresAt } : {}),
295
+ },
296
+ };
297
+ mesh.meshHost = meshHost;
298
+ mesh.updatedAt = now;
299
+ saveMeshConfig(config);
300
+ return { mesh, meshHost, token, tokenId, ...(expiresAt ? { expiresAt } : {}) };
301
+ }
302
+
303
+ export interface MeshHostJoinMemberNodeInput {
304
+ id?: string;
305
+ workspace: string;
306
+ repoRoot?: string;
307
+ daemonId?: string;
308
+ machineId?: string;
309
+ userOverrides?: Partial<RepoMeshNodeCapabilities>;
310
+ policy?: RepoMeshNodePolicy;
311
+ role?: RepoMeshDaemonRole;
312
+ }
313
+
314
+ export interface ApplyMeshHostJoinOptions {
315
+ token: string;
316
+ memberNode: MeshHostJoinMemberNodeInput;
317
+ memberMeshId?: string;
318
+ now?: string;
319
+ }
320
+
321
+ export function applyMeshHostJoinRequest(
322
+ meshId: string,
323
+ opts: ApplyMeshHostJoinOptions,
324
+ ): { accepted: true; mesh: LocalMeshEntry; meshHost: RepoMeshHostMetadata; node: LocalMeshNodeEntry; tokenId: string } | { accepted: false; mesh?: LocalMeshEntry; meshHost?: RepoMeshHostMetadata; tokenId?: string; reason: string } | undefined {
325
+ const config = loadMeshConfig();
326
+ const mesh = config.meshes.find(m => m.id === meshId);
327
+ if (!mesh) return undefined;
328
+ const now = opts.now || new Date().toISOString();
329
+ const previous = mesh.meshHost || createDefaultMeshHostMetadata();
330
+ if (previous.role === 'member') {
331
+ return { accepted: false, mesh, meshHost: previous, reason: 'Mesh Host daemon required to accept join requests' };
332
+ }
333
+ const meshHost: RepoMeshHostMetadata = { ...previous, role: 'host' };
334
+ const validation = assertPairingTokenValid(meshHost.pairing, opts.token, now);
335
+ if (!validation.ok) {
336
+ mesh.meshHost = {
337
+ ...meshHost,
338
+ pairing: {
339
+ ...(meshHost.pairing || { status: 'not_configured' as const }),
340
+ status: 'rejected',
341
+ lastRejectedAt: now,
342
+ },
343
+ };
344
+ mesh.updatedAt = now;
345
+ saveMeshConfig(config);
346
+ return { accepted: false, mesh, meshHost: mesh.meshHost, tokenId: validation.presentedTokenId, reason: validation.reason };
347
+ }
348
+
349
+ const workspace = opts.memberNode.workspace.trim();
350
+ if (!workspace) throw new Error('memberNode.workspace required');
351
+ const memberId = opts.memberNode.id?.trim();
352
+ let node = mesh.nodes.find(n => (memberId && n.id === memberId) || n.workspace === workspace);
353
+ if (node) {
354
+ node.workspace = workspace;
355
+ node.repoRoot = opts.memberNode.repoRoot;
356
+ node.daemonId = opts.memberNode.daemonId;
357
+ node.machineId = opts.memberNode.machineId;
358
+ node.userOverrides = opts.memberNode.userOverrides || node.userOverrides || {};
359
+ node.policy = { ...(node.policy || {}), ...(opts.memberNode.policy || {}) };
360
+ node.role = 'member';
361
+ } else {
362
+ if (mesh.nodes.length >= 10) throw new Error('Maximum 10 nodes per mesh');
363
+ node = {
364
+ id: memberId || `node_${randomUUID().replace(/-/g, '')}`,
365
+ workspace,
366
+ repoRoot: opts.memberNode.repoRoot,
367
+ daemonId: opts.memberNode.daemonId,
368
+ machineId: opts.memberNode.machineId,
369
+ userOverrides: opts.memberNode.userOverrides || {},
370
+ policy: opts.memberNode.policy || {},
371
+ role: 'member',
372
+ };
373
+ mesh.nodes.push(node);
374
+ }
375
+ mesh.meshHost = {
376
+ ...meshHost,
377
+ pairing: {
378
+ ...(meshHost.pairing || {}),
379
+ status: 'paired',
380
+ tokenId: validation.tokenId,
381
+ joinedAt: now,
382
+ lastPairedAt: meshHost.pairing?.lastPairedAt || now,
383
+ ...(meshHost.pairing?.expiresAt ? { expiresAt: meshHost.pairing.expiresAt } : {}),
384
+ },
385
+ };
386
+ mesh.updatedAt = now;
387
+ saveMeshConfig(config);
388
+ return { accepted: true, mesh, meshHost: mesh.meshHost, node, tokenId: validation.tokenId };
389
+ }
390
+
391
+ export function markMeshHostPairingJoined(
392
+ meshId: string,
393
+ opts: { hostDaemonId?: string; hostNodeId?: string; joinedAt?: string; token?: string; tokenId?: string },
394
+ ): { mesh: LocalMeshEntry; meshHost: RepoMeshHostMetadata } | undefined {
395
+ const config = loadMeshConfig();
396
+ const mesh = config.meshes.find(m => m.id === meshId);
397
+ if (!mesh) return undefined;
398
+ const now = opts.joinedAt || new Date().toISOString();
399
+ const previous = mesh.meshHost || createDefaultMeshHostMetadata();
400
+ const tokenId = opts.tokenId || (opts.token ? tokenIdForManualPairing(opts.token) : previous.pairing?.tokenId);
401
+ mesh.meshHost = {
402
+ ...previous,
403
+ role: 'member',
404
+ ...(opts.hostDaemonId ? { hostDaemonId: opts.hostDaemonId } : {}),
405
+ ...(opts.hostNodeId ? { hostNodeId: opts.hostNodeId } : {}),
406
+ pairing: {
407
+ ...(previous.pairing || {}),
408
+ status: 'paired',
409
+ ...(tokenId ? { tokenId } : {}),
410
+ joinedAt: now,
411
+ lastPairedAt: previous.pairing?.lastPairedAt || now,
412
+ },
413
+ };
414
+ mesh.updatedAt = now;
415
+ saveMeshConfig(config);
416
+ return { mesh, meshHost: mesh.meshHost };
417
+ }
418
+
177
419
  // ─── Node Operations ────────────────────────────
178
420
 
179
421
  export interface AddNodeOptions {
@@ -186,6 +428,8 @@ export interface AddNodeOptions {
186
428
  isLocalWorktree?: boolean;
187
429
  worktreeBranch?: string;
188
430
  clonedFromNodeId?: string;
431
+ worktreeBootstrap?: LocalMeshNodeEntry['worktreeBootstrap'];
432
+ role?: RepoMeshDaemonRole;
189
433
  }
190
434
 
191
435
  export function addNode(meshId: string, opts: AddNodeOptions): LocalMeshNodeEntry | undefined {
@@ -213,6 +457,8 @@ export function addNode(meshId: string, opts: AddNodeOptions): LocalMeshNodeEntr
213
457
  isLocalWorktree: opts.isLocalWorktree,
214
458
  worktreeBranch: opts.worktreeBranch,
215
459
  clonedFromNodeId: opts.clonedFromNodeId,
460
+ worktreeBootstrap: opts.worktreeBootstrap,
461
+ role: opts.role,
216
462
  };
217
463
 
218
464
  mesh.nodes.push(node);
@@ -238,7 +484,7 @@ export function removeNode(meshId: string, nodeId: string): boolean {
238
484
  export function updateNode(
239
485
  meshId: string,
240
486
  nodeId: string,
241
- opts: { userOverrides?: Partial<RepoMeshNodeCapabilities>; policy?: RepoMeshNodePolicy },
487
+ opts: { userOverrides?: Partial<RepoMeshNodeCapabilities>; policy?: RepoMeshNodePolicy; worktreeBootstrap?: LocalMeshNodeEntry['worktreeBootstrap'] },
242
488
  ): LocalMeshNodeEntry | undefined {
243
489
  const config = loadMeshConfig();
244
490
  const mesh = config.meshes.find(m => m.id === meshId);
@@ -249,6 +495,7 @@ export function updateNode(
249
495
 
250
496
  if (opts.userOverrides) node.userOverrides = { ...node.userOverrides, ...opts.userOverrides };
251
497
  if (opts.policy) node.policy = { ...node.policy, ...opts.policy };
498
+ if (opts.worktreeBootstrap) node.worktreeBootstrap = opts.worktreeBootstrap;
252
499
  mesh.updatedAt = new Date().toISOString();
253
500
  saveMeshConfig(config);
254
501
  return node;
@@ -92,12 +92,18 @@ export function buildSessionReadStateKey(sessionId: string, providerSessionId?:
92
92
 
93
93
  export function getSessionSeenAt(state: DaemonState, sessionId: string, providerSessionId?: string | null): number {
94
94
  const providerKey = buildSessionReadStateKey(sessionId, providerSessionId);
95
- return state.sessionReads?.[providerKey] || state.sessionReads?.[sessionId] || 0;
95
+ return Math.max(state.sessionReads?.[providerKey] || 0, state.sessionReads?.[sessionId] || 0);
96
96
  }
97
97
 
98
98
  export function getSessionSeenMarker(state: DaemonState, sessionId: string, providerSessionId?: string | null): string {
99
99
  const providerKey = buildSessionReadStateKey(sessionId, providerSessionId);
100
- return state.sessionReadMarkers?.[providerKey] || state.sessionReadMarkers?.[sessionId] || '';
100
+ const providerSeenAt = state.sessionReads?.[providerKey] || 0;
101
+ const sessionSeenAt = state.sessionReads?.[sessionId] || 0;
102
+ const providerMarker = state.sessionReadMarkers?.[providerKey] || '';
103
+ const sessionMarker = state.sessionReadMarkers?.[sessionId] || '';
104
+ if (sessionSeenAt > providerSeenAt && sessionMarker) return sessionMarker;
105
+ if (providerSeenAt > sessionSeenAt && providerMarker) return providerMarker;
106
+ return providerMarker || sessionMarker;
101
107
  }
102
108
 
103
109
  export function getSessionNotificationDismissal(state: DaemonState, sessionId: string, providerSessionId?: string | null): string {
@@ -802,7 +802,16 @@ export async function handleCliSend(ctx: DevServerContext, req: http.IncomingMes
802
802
  }
803
803
 
804
804
  try {
805
- ctx.instanceManager!.sendEvent(target.instanceId, 'send_message', { text });
805
+ if (target.category === 'cli') {
806
+ const bundle = getCliTargetBundle(ctx, type, instanceId);
807
+ if (!bundle) {
808
+ ctx.json(res, 404, { error: `No running CLI adapter found for: ${type || instanceId}` });
809
+ return;
810
+ }
811
+ await bundle.adapter.sendMessage(text);
812
+ } else {
813
+ ctx.instanceManager!.sendEvent(target.instanceId, 'send_message', { text });
814
+ }
806
815
  ctx.json(res, 200, { sent: true, type: target.type, instanceId: target.instanceId });
807
816
  } catch (e: any) {
808
817
  ctx.json(res, 500, { error: `Send failed: ${e.message}` });
@@ -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,
@@ -42,8 +42,12 @@ export interface GitLogResult extends GitRepoIdentity {
42
42
  }
43
43
 
44
44
  export interface GitCheckpointResult extends GitRepoIdentity {
45
- commit: string;
45
+ commit?: string;
46
46
  message: string;
47
+ status?: 'created' | 'skipped';
48
+ skipped?: boolean;
49
+ noop?: boolean;
50
+ reason?: 'nothing_to_commit';
47
51
  lastCheckedAt: number;
48
52
  }
49
53
 
@@ -455,10 +459,17 @@ async function gitCheckpoint(
455
459
  } catch (err: any) {
456
460
  const output = (err?.stdout || '') + (err?.stderr || '');
457
461
  if (/nothing to commit/i.test(output)) {
458
- throw new GitCommandError('nothing_to_commit', 'Nothing to commit — working tree is clean.', {
459
- stdout: err?.stdout,
460
- stderr: err?.stderr,
461
- });
462
+ return {
463
+ workspace: repo.workspace,
464
+ repoRoot,
465
+ isGitRepo: true,
466
+ message: fullMsg,
467
+ status: 'skipped',
468
+ skipped: true,
469
+ noop: true,
470
+ reason: 'nothing_to_commit',
471
+ lastCheckedAt: Date.now(),
472
+ };
462
473
  }
463
474
  throw err;
464
475
  }
@@ -469,6 +480,7 @@ async function gitCheckpoint(
469
480
  isGitRepo: true,
470
481
  commit: commitSha,
471
482
  message: fullMsg,
483
+ status: 'created',
472
484
  lastCheckedAt: Date.now(),
473
485
  };
474
486
  }
package/src/index.ts CHANGED
@@ -88,6 +88,10 @@ export type {
88
88
  // ── Repo Mesh Types (cross-package) ──
89
89
  export type {
90
90
  RepoMesh,
91
+ RepoMeshDaemonRole,
92
+ RepoMeshHostMetadata,
93
+ RepoMeshHostPairingMetadata,
94
+ RepoMeshHostStatus,
91
95
  RepoMeshNode,
92
96
  RepoMeshNodeHealth,
93
97
  RepoMeshPolicy,
@@ -155,18 +159,51 @@ export type { CreateMeshOptions, UpdateMeshOptions, AddNodeOptions } from './con
155
159
  // ── Mesh Coordinator ──
156
160
  export { buildCoordinatorSystemPrompt } from './mesh/coordinator-prompt.js';
157
161
  export type { CoordinatorPromptContext } from './mesh/coordinator-prompt.js';
162
+ export {
163
+ MESH_REFINE_CONFIG_LOCATIONS,
164
+ MESH_REFINE_CONFIG_SCHEMA,
165
+ loadMeshRefineConfig,
166
+ resolveMeshRefineValidationPlan,
167
+ suggestMeshRefineConfig,
168
+ validateMeshRefineConfig,
169
+ } from './mesh/refine-config.js';
170
+ export {
171
+ MESH_WORKTREE_BOOTSTRAP_CONFIG_LOCATIONS,
172
+ MESH_WORKTREE_BOOTSTRAP_CONFIG_SCHEMA,
173
+ loadMeshWorktreeBootstrapConfig,
174
+ runMeshWorktreeBootstrap,
175
+ validateMeshWorktreeBootstrapConfig,
176
+ type RepoMeshWorktreeBootstrapConfig,
177
+ type WorktreeBootstrapState,
178
+ } from './mesh/worktree-bootstrap-config.js';
179
+ export type {
180
+ MeshRefineValidationCategory,
181
+ MeshRefineValidationCommandPlan,
182
+ MeshRefineValidationPlan,
183
+ RepoMeshRefineConfig,
184
+ RepoMeshRefineValidationCommandConfig,
185
+ } from './mesh/refine-config.js';
158
186
  export { syncMeshes } from './mesh/mesh-sync.js';
159
187
  export type { MeshSyncTransport, MeshSyncResult, RemoteMeshRecord } from './mesh/mesh-sync.js';
160
188
 
161
189
  // ── Mesh Task Ledger ──
162
- export { appendLedgerEntry, appendRemoteLedgerEntries, readLedgerEntries, readLedgerSlice, getLedgerSummary, getLedgerDir, getSessionRecoveryContext, MAX_LEDGER_SLICE_LIMIT } from './mesh/mesh-ledger.js';
163
- export type { AppendRemoteLedgerResult, MeshLedgerEntry, MeshLedgerKind, MeshLedgerSlice, MeshLedgerSummary, ReadLedgerOptions, ReadLedgerSliceOptions, SessionRecoveryContext } from './mesh/mesh-ledger.js';
190
+ export { appendLedgerEntry, appendRemoteLedgerEntries, buildTaskCompletionEvidence, normalizeMeshWorkerResult, readLedgerEntries, readLedgerSlice, getLedgerSummary, getLedgerDir, getSessionRecoveryContext, MAX_LEDGER_SLICE_LIMIT } from './mesh/mesh-ledger.js';
191
+ export type { AppendRemoteLedgerResult, MeshLedgerEntry, MeshLedgerKind, MeshLedgerSlice, MeshLedgerSummary, ReadLedgerOptions, ReadLedgerSliceOptions, SessionRecoveryContext, MeshTaskCompletionEvidence, MeshWorkerResultArtifact, MeshProcessArtifact, MeshValidationResultArtifact } from './mesh/mesh-ledger.js';
192
+ export { fastForwardMeshNode } from './mesh/mesh-fast-forward.js';
193
+ export type { MeshFastForwardNodeArgs, MeshFastForwardPlannedStep, MeshFastForwardResult } from './mesh/mesh-fast-forward.js';
164
194
  export { buildMeshLedgerReconciliationEvidence, buildMeshLedgerReplicaEvidence } from './mesh/mesh-ledger-reconciliation.js';
165
195
  export type { MeshLedgerReconciliationEvidence, MeshLedgerReplicaEvidence, MeshLedgerReplicaStatus } from './mesh/mesh-ledger-reconciliation.js';
166
196
 
167
197
  // ── Mesh Work Queue (GUPP) ──
168
- export { enqueueTask, getQueue, claimNextTask, updateTaskStatus, updateSessionTaskStatus, cancelTask, requeueTask, getMeshQueueStats } from './mesh/mesh-work-queue.js';
169
- export type { MeshWorkQueueEntry, MeshTaskStatus, MeshWorkQueueStats } from './mesh/mesh-work-queue.js';
198
+ export { enqueueTask, getQueue, claimNextTask, updateTaskStatus, updateSessionTaskStatus, cancelTask, requeueTask, getMeshQueueStats, getMeshQueueRevision, normalizeMeshTaskMode, validateMeshTaskModeRequest } from './mesh/mesh-work-queue.js';
199
+ export type { MeshWorkQueueEntry, MeshTaskStatus, MeshTaskMode, MeshWorkQueueStats, MeshQueueMutationOptions, MeshTaskModeValidationResult } from './mesh/mesh-work-queue.js';
200
+ export { buildCompactStaleDirectWorkSummary, buildMeshActiveWork, buildMeshActiveWorkSummary } from './mesh/mesh-active-work.js';
201
+ export type { MeshActiveWorkRecord, MeshActiveWorkStatus, MeshActiveWorkSummary, MeshActiveWorkSource, MeshStaleDirectWorkSummary } from './mesh/mesh-active-work.js';
202
+ export { buildMeshAsyncRefineJobs } from './mesh/mesh-refine-status.js';
203
+ export type { MeshAsyncRefineJobStatus, MeshAsyncRefineJobSummary } from './mesh/mesh-refine-status.js';
204
+
205
+ // ── Mesh Host Ownership ──
206
+ export { buildMeshHostRequiredFailure, createDefaultMeshHostMetadata, isMeshHostOwner, normalizeMeshDaemonRole, requireMeshHostQueueOwner, resolveMeshHostStatus } from './mesh/mesh-host-ownership.js';
170
207
 
171
208
  // ── Mesh Visualization ──
172
209
  // buildMeshGraph and MeshGraph types moved to @adhdev/web-core to avoid
@@ -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
  */