@adhdev/daemon-core 0.9.82-rc.9 → 0.9.82-rc.90

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 (67) hide show
  1. package/dist/cli-adapters/provider-cli-adapter.d.ts +2 -0
  2. package/dist/cli-adapters/provider-cli-parse.d.ts +1 -0
  3. package/dist/cli-adapters/provider-cli-shared.d.ts +2 -0
  4. package/dist/commands/router.d.ts +22 -0
  5. package/dist/config/mesh-config.d.ts +66 -1
  6. package/dist/index.d.ts +13 -6
  7. package/dist/index.js +5395 -1197
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +5359 -1183
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/installer.d.ts +1 -4
  12. package/dist/launch.d.ts +1 -1
  13. package/dist/logging/async-batch-writer.d.ts +10 -0
  14. package/dist/mesh/beads-db.d.ts +18 -0
  15. package/dist/mesh/mesh-active-work.d.ts +60 -0
  16. package/dist/mesh/mesh-events.d.ts +29 -5
  17. package/dist/mesh/mesh-fast-forward.d.ts +39 -0
  18. package/dist/mesh/mesh-host-ownership.d.ts +9 -0
  19. package/dist/mesh/mesh-ledger.d.ts +38 -1
  20. package/dist/mesh/mesh-work-queue.d.ts +27 -5
  21. package/dist/mesh/refine-config.d.ts +176 -0
  22. package/dist/providers/chat-message-normalization.d.ts +1 -0
  23. package/dist/providers/cli-provider-instance.d.ts +2 -1
  24. package/dist/repo-mesh-types.d.ts +46 -0
  25. package/dist/status/reporter.d.ts +2 -0
  26. package/package.json +3 -1
  27. package/src/boot/daemon-lifecycle.ts +1 -0
  28. package/src/cli-adapters/provider-cli-adapter.ts +91 -3
  29. package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
  30. package/src/cli-adapters/provider-cli-parse.ts +4 -0
  31. package/src/cli-adapters/provider-cli-runtime.ts +3 -1
  32. package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
  33. package/src/cli-adapters/provider-cli-shared.ts +20 -10
  34. package/src/commands/chat-commands.ts +454 -15
  35. package/src/commands/cli-manager.ts +126 -0
  36. package/src/commands/handler.ts +8 -1
  37. package/src/commands/mesh-coordinator.ts +13 -143
  38. package/src/commands/router.ts +2687 -435
  39. package/src/config/chat-history.ts +9 -7
  40. package/src/config/mesh-config.ts +245 -1
  41. package/src/daemon/dev-cli-debug.ts +10 -1
  42. package/src/detection/ide-detector.ts +26 -16
  43. package/src/index.ts +31 -5
  44. package/src/installer.d.ts +1 -1
  45. package/src/installer.ts +8 -6
  46. package/src/launch.d.ts +1 -1
  47. package/src/launch.ts +37 -28
  48. package/src/logging/async-batch-writer.ts +55 -0
  49. package/src/logging/logger.ts +2 -1
  50. package/src/mesh/beads-db.ts +176 -0
  51. package/src/mesh/coordinator-prompt.ts +30 -7
  52. package/src/mesh/mesh-active-work.ts +243 -0
  53. package/src/mesh/mesh-events.ts +400 -47
  54. package/src/mesh/mesh-fast-forward.ts +430 -0
  55. package/src/mesh/mesh-host-ownership.ts +73 -0
  56. package/src/mesh/mesh-ledger.ts +138 -1
  57. package/src/mesh/mesh-work-queue.ts +199 -137
  58. package/src/mesh/refine-config.ts +356 -0
  59. package/src/providers/chat-message-normalization.ts +3 -1
  60. package/src/providers/cli-provider-instance.ts +91 -13
  61. package/src/providers/ide-provider-instance.ts +17 -3
  62. package/src/providers/provider-loader.ts +10 -4
  63. package/src/providers/read-chat-contract.ts +1 -1
  64. package/src/providers/version-archive.ts +38 -20
  65. package/src/repo-mesh-types.ts +51 -0
  66. package/src/status/reporter.ts +15 -0
  67. package/src/system/host-memory.ts +29 -12
package/src/launch.ts CHANGED
@@ -16,7 +16,16 @@
16
16
  * adhdev launch --workspace /path — Open specific workspace
17
17
  */
18
18
 
19
- import { execSync, spawn, spawnSync } from 'child_process';
19
+ import { exec, spawn, spawnSync } from 'child_process';
20
+
21
+ async function execQuiet(command: string, options: any = {}): Promise<string> {
22
+ return new Promise((resolve) => {
23
+ exec(command, options, (error, stdout) => {
24
+ if (error) return resolve('');
25
+ resolve(stdout.toString());
26
+ });
27
+ });
28
+ }
20
29
  import * as net from 'net';
21
30
  import * as os from 'os';
22
31
  import * as path from 'path';
@@ -76,11 +85,11 @@ function getIdePathCandidates(ideId: string): string[] {
76
85
  return getProviderLoader().getIdePathCandidates(ideId);
77
86
  }
78
87
 
79
- function getMacAppProcessPids(ideId: string): number[] {
88
+ async function getMacAppProcessPids(ideId: string): Promise<number[]> {
80
89
  const appPaths = getIdePathCandidates(ideId);
81
90
  if (appPaths.length === 0) return [];
82
91
  try {
83
- const output = execSync('ps axww -o pid=,args=', {
92
+ const output = await execQuiet('ps axww -o pid=,args=', {
84
93
  encoding: 'utf-8',
85
94
  timeout: 3000,
86
95
  stdio: ['pipe', 'pipe', 'pipe'],
@@ -91,8 +100,8 @@ function getMacAppProcessPids(ideId: string): number[] {
91
100
  }
92
101
  }
93
102
 
94
- function killMacAppPathProcesses(ideId: string, signal: NodeJS.Signals): boolean {
95
- const pids = getMacAppProcessPids(ideId);
103
+ async function killMacAppPathProcesses(ideId: string, signal: NodeJS.Signals): Promise<boolean> {
104
+ const pids = (await getMacAppProcessPids(ideId));
96
105
  let signalled = false;
97
106
  for (const pid of pids) {
98
107
  try {
@@ -163,49 +172,49 @@ export async function killIdeProcess(ideId: string): Promise<boolean> {
163
172
  if (plat === 'darwin' && appName) {
164
173
  // macOS: graceful quit via osascript
165
174
  try {
166
- execSync(`osascript -e 'tell application "${escapeForAppleScript(appName)}" to quit' 2>/dev/null`, {
175
+ await execQuiet(`osascript -e 'tell application "${escapeForAppleScript(appName)}" to quit' 2>/dev/null`, {
167
176
  timeout: 5000,
168
177
  });
169
178
  } catch {
170
- try { execSync(`pkill -x "${appName}" 2>/dev/null`, { timeout: 5000 }); } catch { }
179
+ try { await execQuiet(`pkill -x "${appName}" 2>/dev/null`, { timeout: 5000 }); } catch { }
171
180
  }
172
- killMacAppPathProcesses(ideId, 'SIGTERM');
181
+ await killMacAppPathProcesses(ideId, 'SIGTERM');
173
182
  } else if (plat === 'win32' && winProcesses) {
174
183
  // Windows: taskkill for each process name
175
184
  for (const proc of winProcesses) {
176
185
  try {
177
- execSync(`taskkill /IM "${proc}" /F 2>nul`, { timeout: 5000 });
186
+ await execQuiet(`taskkill /IM "${proc}" /F 2>nul`, { timeout: 5000 });
178
187
  } catch { }
179
188
  }
180
189
  // Process name may differ, so also try via WMIC
181
190
  try {
182
191
  const exeName = winProcesses[0].replace('.exe', '');
183
- execSync(`powershell -Command "Get-Process -Name '${exeName}' -ErrorAction SilentlyContinue | Stop-Process -Force"`, {
192
+ await execQuiet(`powershell -Command "Get-Process -Name '${exeName}' -ErrorAction SilentlyContinue | Stop-Process -Force"`, {
184
193
  timeout: 10000,
185
194
  });
186
195
  } catch { }
187
196
  } else {
188
- try { execSync(`pkill -f "${ideId}" 2>/dev/null`); } catch { }
197
+ try { await execQuiet(`pkill -f "${ideId}" 2>/dev/null`); } catch { }
189
198
  }
190
199
 
191
200
  // Wait for process kill (max 15 seconds)
192
201
  for (let i = 0; i < 30; i++) {
193
202
  await new Promise(r => setTimeout(r, 500));
194
- if (!isIdeRunning(ideId)) return true;
203
+ if (!(await isIdeRunning(ideId))) return true;
195
204
  }
196
205
 
197
206
  // Force terminate retry
198
207
  if (plat === 'darwin' && appName) {
199
- try { execSync(`pkill -9 -x "${appName}" 2>/dev/null`, { timeout: 5000 }); } catch { }
200
- killMacAppPathProcesses(ideId, 'SIGKILL');
208
+ try { await execQuiet(`pkill -9 -x "${appName}" 2>/dev/null`, { timeout: 5000 }); } catch { }
209
+ await killMacAppPathProcesses(ideId, 'SIGKILL');
201
210
  } else if (plat === 'win32' && winProcesses) {
202
211
  for (const proc of winProcesses) {
203
- try { execSync(`taskkill /IM "${proc}" /F 2>nul`); } catch { }
212
+ try { await execQuiet(`taskkill /IM "${proc}" /F 2>nul`); } catch { }
204
213
  }
205
214
  }
206
215
 
207
216
  await new Promise(r => setTimeout(r, 2000));
208
- return !isIdeRunning(ideId);
217
+ return !(await isIdeRunning(ideId));
209
218
 
210
219
  } catch {
211
220
  return false;
@@ -213,15 +222,15 @@ export async function killIdeProcess(ideId: string): Promise<boolean> {
213
222
  }
214
223
 
215
224
  /** Check if IDE process is running */
216
- export function isIdeRunning(ideId: string): boolean {
225
+ export async function isIdeRunning(ideId: string): Promise<boolean> {
217
226
  const plat = os.platform();
218
227
 
219
228
  try {
220
229
  if (plat === 'darwin') {
221
230
  const appName = getMacAppIdentifiers()[ideId];
222
- if (!appName) return getMacAppProcessPids(ideId).length > 0;
231
+ if (!appName) return (await getMacAppProcessPids(ideId)).length > 0;
223
232
  try {
224
- const result = execSync(`pgrep -x "${appName}" 2>/dev/null`, {
233
+ const result = await execQuiet(`pgrep -x "${appName}" 2>/dev/null`, {
225
234
  encoding: 'utf-8',
226
235
  timeout: 3000,
227
236
  });
@@ -229,7 +238,7 @@ export function isIdeRunning(ideId: string): boolean {
229
238
  } catch { }
230
239
 
231
240
  try {
232
- const result = execSync(
241
+ const result = await execQuiet(
233
242
  `osascript -e 'tell application "System Events" to count (every process whose name is "${escapeForAppleScript(appName)}")'`,
234
243
  {
235
244
  encoding: 'utf-8',
@@ -240,21 +249,21 @@ export function isIdeRunning(ideId: string): boolean {
240
249
  if (Number.parseInt(result.trim() || '0', 10) > 0) return true;
241
250
  } catch { }
242
251
 
243
- return getMacAppProcessPids(ideId).length > 0;
252
+ return (await getMacAppProcessPids(ideId)).length > 0;
244
253
  } else if (plat === 'win32') {
245
254
  const winProcesses = getWinProcessNames()[ideId];
246
255
  if (!winProcesses) return false;
247
256
  // Check each process name
248
257
  for (const proc of winProcesses) {
249
258
  try {
250
- const result = execSync(`tasklist /FI "IMAGENAME eq ${proc}" /NH 2>nul`, { encoding: 'utf-8' });
259
+ const result = await execQuiet(`tasklist /FI "IMAGENAME eq ${proc}" /NH 2>nul`, { encoding: 'utf-8' });
251
260
  if (result.includes(proc)) return true;
252
261
  } catch { }
253
262
  }
254
263
  // Also check via PowerShell (when tasklist cannot find)
255
264
  try {
256
265
  const exeName = winProcesses[0].replace('.exe', '');
257
- const result = execSync(
266
+ const result = await execQuiet(
258
267
  `powershell -Command "(Get-Process -Name '${exeName}' -ErrorAction SilentlyContinue).Count"`,
259
268
  { encoding: 'utf-8', timeout: 5000 }
260
269
  );
@@ -262,7 +271,7 @@ export function isIdeRunning(ideId: string): boolean {
262
271
  } catch { }
263
272
  return false;
264
273
  } else {
265
- const result = execSync(`pgrep -f "${ideId}" 2>/dev/null`, { encoding: 'utf-8' });
274
+ const result = await execQuiet(`pgrep -f "${ideId}" 2>/dev/null`, { encoding: 'utf-8' });
266
275
  return result.trim().length > 0;
267
276
  }
268
277
  } catch {
@@ -271,14 +280,14 @@ export function isIdeRunning(ideId: string): boolean {
271
280
  }
272
281
 
273
282
  /** Detect currently open workspace path */
274
- function detectCurrentWorkspace(ideId: string): string | undefined {
283
+ async function detectCurrentWorkspace(ideId: string): Promise<string | undefined> {
275
284
  const plat = os.platform();
276
285
 
277
286
  if (plat === 'darwin') {
278
287
  try {
279
288
  const appName = getMacAppIdentifiers()[ideId];
280
289
  if (!appName) return undefined;
281
- const result = execSync(
290
+ const result = await execQuiet(
282
291
  `lsof -c "${appName}" 2>/dev/null | grep cwd | head -1 | awk '{print $NF}'`,
283
292
  { encoding: 'utf-8', timeout: 3000 }
284
293
  );
@@ -392,8 +401,8 @@ export async function launchWithCdp(options: LaunchOptions = {}): Promise<Launch
392
401
  }
393
402
 
394
403
  // 4. Check if IDE is currently running
395
- const alreadyRunning = isIdeRunning(targetIde.id);
396
- const workspace = options.workspace || (alreadyRunning ? detectCurrentWorkspace(targetIde.id) : undefined);
404
+ const alreadyRunning = await isIdeRunning(targetIde.id);
405
+ const workspace = options.workspace || (alreadyRunning ? await detectCurrentWorkspace(targetIde.id) : undefined);
397
406
 
398
407
  // 5. If IDE is running, terminate it
399
408
  if (alreadyRunning) {
@@ -0,0 +1,55 @@
1
+ import * as fs from 'fs';
2
+
3
+ export class AsyncBatchWriter {
4
+ // Maps filePath -> string buffer
5
+ private static buffers: Map<string, string[]> = new Map();
6
+ private static writePromises: Map<string, Promise<void>> = new Map();
7
+ private static flushTimer: NodeJS.Timeout | null = null;
8
+
9
+ /**
10
+ * Queues data to be written to a file asynchronously in a batch.
11
+ */
12
+ public static write(filePath: string, data: string) {
13
+ let buf = this.buffers.get(filePath);
14
+ if (!buf) {
15
+ buf = [];
16
+ this.buffers.set(filePath, buf);
17
+ }
18
+ buf.push(data);
19
+
20
+ if (!this.flushTimer) {
21
+ this.flushTimer = setTimeout(() => {
22
+ this.flushTimer = null;
23
+ this.flushAll();
24
+ }, 50);
25
+ }
26
+ }
27
+
28
+ private static async flushAll() {
29
+ const entries = Array.from(this.buffers.entries());
30
+ this.buffers.clear();
31
+
32
+ for (const [filePath, buffer] of entries) {
33
+ const dataToWrite = buffer.join('');
34
+
35
+ const doWrite = async () => {
36
+ try {
37
+ const prevPromise = this.writePromises.get(filePath);
38
+ if (prevPromise) await prevPromise;
39
+ await fs.promises.appendFile(filePath, dataToWrite, { encoding: 'utf-8', mode: 0o600 });
40
+ } catch {
41
+ // Logging must never create secondary failures or late console noise.
42
+ }
43
+ };
44
+
45
+ const writePromise = doWrite();
46
+ this.writePromises.set(filePath, writePromise);
47
+
48
+ writePromise.finally(() => {
49
+ if (this.writePromises.get(filePath) === writePromise) {
50
+ this.writePromises.delete(filePath);
51
+ }
52
+ });
53
+ }
54
+ }
55
+ }
@@ -20,6 +20,7 @@
20
20
  import * as fs from 'fs';
21
21
  import * as path from 'path';
22
22
  import * as os from 'os';
23
+ import { AsyncBatchWriter } from './async-batch-writer.js';
23
24
 
24
25
  // ─── Log Level ──────────────────────────────
25
26
  export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
@@ -123,7 +124,7 @@ function writeToFile(line: string): void {
123
124
  checkDateRotation();
124
125
  rotateSizeIfNeeded();
125
126
  }
126
- fs.appendFileSync(currentLogFile, line + '\n');
127
+ AsyncBatchWriter.write(currentLogFile, line + '\n');
127
128
  } catch { }
128
129
  }
129
130
 
@@ -0,0 +1,176 @@
1
+ import { existsSync, mkdirSync, readFileSync } from 'fs';
2
+ import { dirname, join } from 'path';
3
+ import { createRequire } from 'module';
4
+ import { getLedgerDir } from './mesh-ledger.js';
5
+ import type { MeshTaskStatus, MeshWorkQueueEntry } from './mesh-work-queue.js';
6
+ import type BetterSqlite3 from 'better-sqlite3';
7
+ import type { Database as DatabaseHandle } from 'better-sqlite3';
8
+
9
+ let DatabaseCtor: typeof BetterSqlite3 | undefined;
10
+
11
+ function loadDatabaseCtor(): typeof BetterSqlite3 {
12
+ if (DatabaseCtor) return DatabaseCtor;
13
+ const runtimeRequire = typeof require === 'function'
14
+ ? require
15
+ : createRequire(import.meta.url);
16
+ DatabaseCtor = runtimeRequire('better-sqlite3') as typeof BetterSqlite3;
17
+ return DatabaseCtor;
18
+ }
19
+
20
+ function safeMeshId(meshId: string): string {
21
+ return meshId.replace(/[^a-zA-Z0-9_-]/g, '_');
22
+ }
23
+
24
+ function legacyQueuePath(meshId: string): string {
25
+ return join(getLedgerDir(), `${safeMeshId(meshId)}.queue.json`);
26
+ }
27
+
28
+ export class BeadsDB {
29
+ private static instance: BeadsDB | undefined;
30
+ private readonly db: DatabaseHandle;
31
+ private readonly migratedMeshIds = new Set<string>();
32
+
33
+ private constructor(dbPath: string) {
34
+ const dir = dirname(dbPath);
35
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
36
+
37
+ this.db = new (loadDatabaseCtor())(dbPath);
38
+ this.db.pragma('journal_mode = WAL');
39
+ this.db.pragma('synchronous = NORMAL');
40
+ this.db.pragma('foreign_keys = ON');
41
+ this.db.pragma('busy_timeout = 5000');
42
+ this.migrate();
43
+ }
44
+
45
+ static getInstance(): BeadsDB {
46
+ if (!this.instance) {
47
+ this.instance = new BeadsDB(join(getLedgerDir(), 'beads.db'));
48
+ }
49
+ return this.instance;
50
+ }
51
+
52
+ static resetForTests(): void {
53
+ this.instance?.close();
54
+ this.instance = undefined;
55
+ }
56
+
57
+ close(): void {
58
+ this.db.close();
59
+ }
60
+
61
+ transaction<T>(fn: () => T): T {
62
+ return this.db.transaction(fn).immediate();
63
+ }
64
+
65
+ private migrate(): void {
66
+ this.db.exec(`
67
+ CREATE TABLE IF NOT EXISTS mesh_queue (
68
+ id TEXT PRIMARY KEY,
69
+ mesh_id TEXT NOT NULL,
70
+ status TEXT NOT NULL,
71
+ target_node_id TEXT,
72
+ target_session_id TEXT,
73
+ assigned_node_id TEXT,
74
+ assigned_session_id TEXT,
75
+ created_at TEXT NOT NULL,
76
+ updated_at TEXT NOT NULL,
77
+ payload TEXT NOT NULL
78
+ );
79
+
80
+ CREATE INDEX IF NOT EXISTS idx_mesh_queue_mesh_status_created
81
+ ON mesh_queue(mesh_id, status, created_at);
82
+ CREATE INDEX IF NOT EXISTS idx_mesh_queue_assignment
83
+ ON mesh_queue(mesh_id, assigned_node_id, assigned_session_id, status);
84
+ `);
85
+ }
86
+
87
+ private ensureLegacyQueueMigrated(meshId: string): void {
88
+ if (this.migratedMeshIds.has(meshId)) return;
89
+ this.migratedMeshIds.add(meshId);
90
+
91
+ const count = this.db
92
+ .prepare('SELECT COUNT(*) AS count FROM mesh_queue WHERE mesh_id = ?')
93
+ .get(meshId) as { count: number };
94
+ if (count.count > 0) return;
95
+
96
+ const path = legacyQueuePath(meshId);
97
+ if (!existsSync(path)) return;
98
+
99
+ try {
100
+ const entries = JSON.parse(readFileSync(path, 'utf-8')) as MeshWorkQueueEntry[];
101
+ if (!Array.isArray(entries)) return;
102
+ const insert = this.db.prepare(`
103
+ INSERT OR REPLACE INTO mesh_queue (
104
+ id, mesh_id, status, target_node_id, target_session_id,
105
+ assigned_node_id, assigned_session_id, created_at, updated_at, payload
106
+ ) VALUES (
107
+ @id, @meshId, @status, @targetNodeId, @targetSessionId,
108
+ @assignedNodeId, @assignedSessionId, @createdAt, @updatedAt, @payload
109
+ )
110
+ `);
111
+ for (const entry of entries) {
112
+ insert.run(this.toRow(entry));
113
+ }
114
+ } catch {
115
+ return;
116
+ }
117
+ }
118
+
119
+ getQueueEntries(meshId: string, statuses?: MeshTaskStatus[]): MeshWorkQueueEntry[] {
120
+ this.ensureLegacyQueueMigrated(meshId);
121
+ if (statuses?.length) {
122
+ const placeholders = statuses.map(() => '?').join(', ');
123
+ const rows = this.db
124
+ .prepare(`SELECT payload FROM mesh_queue WHERE mesh_id = ? AND status IN (${placeholders}) ORDER BY created_at ASC`)
125
+ .all(meshId, ...statuses) as Array<{ payload: string }>;
126
+ return rows.map(row => JSON.parse(row.payload) as MeshWorkQueueEntry);
127
+ }
128
+ const rows = this.db
129
+ .prepare('SELECT payload FROM mesh_queue WHERE mesh_id = ? ORDER BY created_at ASC')
130
+ .all(meshId) as Array<{ payload: string }>;
131
+ return rows.map(row => JSON.parse(row.payload) as MeshWorkQueueEntry);
132
+ }
133
+
134
+ getQueueRevision(meshId: string): string {
135
+ this.ensureLegacyQueueMigrated(meshId);
136
+ const rows = this.db
137
+ .prepare('SELECT id, status, updated_at FROM mesh_queue WHERE mesh_id = ? ORDER BY id ASC')
138
+ .all(meshId) as Array<{ id: string; status: string; updated_at: string }>;
139
+ return rows.map(row => `${row.id}:${row.status}:${row.updated_at}`).join('|');
140
+ }
141
+
142
+ replaceQueue(meshId: string, queue: MeshWorkQueueEntry[]): void {
143
+ const deleteStmt = this.db.prepare('DELETE FROM mesh_queue WHERE mesh_id = ?');
144
+ const insert = this.db.prepare(`
145
+ INSERT INTO mesh_queue (
146
+ id, mesh_id, status, target_node_id, target_session_id,
147
+ assigned_node_id, assigned_session_id, created_at, updated_at, payload
148
+ ) VALUES (
149
+ @id, @meshId, @status, @targetNodeId, @targetSessionId,
150
+ @assignedNodeId, @assignedSessionId, @createdAt, @updatedAt, @payload
151
+ )
152
+ `);
153
+ deleteStmt.run(meshId);
154
+ for (const entry of queue) insert.run(this.toRow(entry));
155
+ }
156
+
157
+ deleteQueue(meshId: string): void {
158
+ this.db.prepare('DELETE FROM mesh_queue WHERE mesh_id = ?').run(meshId);
159
+ this.migratedMeshIds.delete(meshId);
160
+ }
161
+
162
+ private toRow(entry: MeshWorkQueueEntry): Record<string, unknown> {
163
+ return {
164
+ id: entry.id,
165
+ meshId: entry.meshId,
166
+ status: entry.status,
167
+ targetNodeId: entry.targetNodeId ?? null,
168
+ targetSessionId: entry.targetSessionId ?? null,
169
+ assignedNodeId: entry.assignedNodeId ?? null,
170
+ assignedSessionId: entry.assignedSessionId ?? null,
171
+ createdAt: entry.createdAt,
172
+ updatedAt: entry.updatedAt,
173
+ payload: JSON.stringify(entry),
174
+ };
175
+ }
176
+ }
@@ -76,7 +76,12 @@ Repository: \`${mesh.repoIdentity}\`${mesh.defaultBranch ? `\nDefault branch: \`
76
76
  // ─── Section Builders ───────────────────────────
77
77
 
78
78
  function buildNodeStatusSection(nodes: RepoMeshNodeStatus[]): string {
79
- const lines = ['## Current Node Status', ''];
79
+ const lines = [
80
+ '## Current Node Status',
81
+ '',
82
+ 'Node labels are display context, not aliases. Use exact `nodeId` values in mesh tool calls; do not invent shorthand names such as M1/M2 unless they are explicitly configured labels.',
83
+ '',
84
+ ];
80
85
  for (const n of nodes) {
81
86
  const healthIcon = n.health === 'online' ? '🟢' :
82
87
  n.health === 'dirty' ? '🟡' :
@@ -85,21 +90,33 @@ function buildNodeStatusSection(nodes: RepoMeshNodeStatus[]): string {
85
90
  ? `sessions: ${n.activeSessions.join(', ')}`
86
91
  : 'no active sessions';
87
92
  const branch = n.git?.branch ? `branch: \`${n.git.branch}\`` : '';
88
- lines.push(`- ${healthIcon} **${n.machineLabel}** (${n.nodeId})`);
89
- lines.push(` workspace: \`${n.workspace}\` | ${branch} | ${sessions}`);
93
+ const context = [
94
+ n.daemonId ? `daemon: \`${n.daemonId}\`` : '',
95
+ n.providers?.length ? `providers: ${n.providers.join(', ')}` : '',
96
+ ].filter(Boolean).join(' | ');
97
+ lines.push(`- ${healthIcon} **${n.machineLabel}** (nodeId: \`${n.nodeId}\`)`);
98
+ lines.push(` workspace: \`${n.workspace}\`${context ? ` | ${context}` : ''} | ${branch} | ${sessions}`);
90
99
  if (n.error) lines.push(` ⚠️ ${n.error}`);
91
100
  }
92
101
  return lines.join('\n');
93
102
  }
94
103
 
95
104
  function buildNodeConfigSection(mesh: LocalMeshEntry): string {
96
- const lines = ['## Configured Nodes', ''];
105
+ const lines = [
106
+ '## Configured Nodes',
107
+ '',
108
+ 'Node labels are display context, not aliases. Use exact `nodeId` values in mesh tool calls; do not invent shorthand names such as M1/M2 unless they are explicitly configured labels.',
109
+ '',
110
+ ];
97
111
  for (const n of mesh.nodes) {
98
112
  const labels: string[] = [];
99
113
  if (n.isLocalWorktree) labels.push('worktree');
100
114
  if (n.policy?.readOnly) labels.push('read-only');
101
115
  const suffix = labels.length ? ` [${labels.join(', ')}]` : '';
102
- lines.push(`- **${n.workspace}** (${n.id})${suffix}`);
116
+ const explicitMachineLabel = typeof (n as any).machineLabel === 'string' ? (n as any).machineLabel : '';
117
+ const explicitLabel = explicitMachineLabel ? ` label: **${explicitMachineLabel}** |` : '';
118
+ const providerPriority = n.policy?.providerPriority?.length ? ` | providers: ${n.policy.providerPriority.join(', ')}` : '';
119
+ lines.push(`- ${explicitLabel} nodeId: \`${n.id}\` | workspace: \`${n.workspace}\`${n.daemonId ? ` | daemon: \`${n.daemonId}\`` : ''}${providerPriority}${suffix}`);
103
120
  }
104
121
  lines.push('', '_Use `mesh_status` to probe live health before delegating work._');
105
122
  return lines.join('\n');
@@ -110,6 +127,9 @@ function buildPolicySection(policy: RepoMeshPolicy): string {
110
127
  if (policy.requirePreTaskCheckpoint) rules.push('- Create a git checkpoint **before** starting each task');
111
128
  if (policy.requirePostTaskCheckpoint) rules.push('- Create a git checkpoint **after** each task completes');
112
129
  if (policy.requireApprovalForPush) rules.push('- **Ask for user approval** before pushing to remote');
130
+ if (policy.allowAutoPublishSubmoduleMainCommits) {
131
+ rules.push('- Refinery may auto-publish unreachable submodule gitlink commits to submodule origin/main with non-force pushes after validation and patch-equivalence pass');
132
+ }
113
133
  if (policy.requireApprovalForDestructiveGit) rules.push('- **Ask for user approval** before destructive git operations (force push, reset, etc.)');
114
134
 
115
135
  const dirtyBehavior = {
@@ -140,6 +160,7 @@ const TOOLS_SECTION = `## Available Tools
140
160
  | \`mesh_read_debug\` | Collect a daemon-side chat/parser debug bundle for a session |
141
161
  | \`mesh_task_history\` | Read the task ledger — dispatches, completions, failures. Use to understand what has been done before deciding next steps |
142
162
  | \`mesh_git_status\` | Check git status on a specific node |
163
+ | \`mesh_fast_forward_node\` | Safely dry-run or explicitly execute an obvious clean fast-forward without launching an agent session |
143
164
  | \`mesh_checkpoint\` | Create a git checkpoint on a node |
144
165
  | \`mesh_approve\` | Approve/reject a pending agent action |
145
166
  | \`mesh_clone_node\` | Create a worktree node for isolated parallel branch work |
@@ -164,7 +185,7 @@ const WORKFLOW_SECTION = `## Orchestration Workflow
164
185
  4. **Monitor** — Prefer event-driven completion/status notifications. Do **not** poll \`mesh_read_chat\` repeatedly. Use \`mesh_view_queue\` to see the status of all pending, assigned, completed, and failed tasks. Do not call \`mesh_read_chat\` again within a few seconds for the same generating session. Use at most one compact \`mesh_read_chat\` check after a completion/approval signal. Handle approvals via \`mesh_approve\`.
165
186
  5. **Verify** — When a task reports completion or git work is visible, call \`mesh_git_status\` to verify changes were made.
166
187
  6. **Checkpoint** — Call \`mesh_checkpoint\` to save the work.
167
- 7. **Converge branches** — Before marking any task complete, classify every touched node/branch into exactly one final state: \`merged_to_main\`, \`pushed_feature_branch_needs_merge\`, \`blocked_review\`, \`cleanup_candidate\`, or \`not_mergeable\`. Use \`mesh_status\` branchConvergenceSummary and \`mesh_refine_node\` for clean worktree branches when safe. A task that remains on a non-main branch is not fully complete unless the final report names the follow-up state and next step.
188
+ 7. **Converge branches** — Before marking any task complete, classify every touched node/branch into exactly one final state: \`merged_to_main\`, \`pushed_feature_branch_needs_merge\`, \`blocked_review\`, \`cleanup_candidate\`, or \`not_mergeable\`. Use \`mesh_status\` branchConvergenceSummary. For obvious clean branch catch-up (ahead 0, behind > 0, upstream fresh, no dirty/stash/submodule issues), use \`mesh_fast_forward_node\` dry-run first and execute only when explicitly safe/approved; this avoids consuming an agent session. Use \`mesh_refine_node\` for clean worktree branches when safe. Before/refine merging root commits that contain submodule gitlink changes, require each submodule commit to be reachable from the configured submodule remote main branch, not merely present on a feature ref or local checkout. If \`mesh_refine_node\` returns \`submodule_reachability_failed\` or publish-required evidence, keep the public convergence bucket as \`blocked_review\`; unless \`allowAutoPublishSubmoduleMainCommits\` is explicitly enabled and Refinery reports successful non-force publish plus post-publish verification, ask the user for explicit approval to push/publish the unreachable submodule commit(s) to submodule main, then rerun \`mesh_refine_node\`. Do not merge the root branch until the submodule commit(s) are reachable from submodule origin/main. A task that remains on a non-main branch is not fully complete unless the final report names the follow-up state and next step.
168
189
  8. **Clean up** — Remove worktree nodes via \`mesh_remove_node\` after their work is merged or no longer needed.
169
190
  9. **Report** — Summarize what was done, what changed, any issues, and the branch convergence state.
170
191
 
@@ -201,6 +222,8 @@ function buildRulesSection(coordinatorCliType?: string): string {
201
222
  - **Respect node capabilities.** Don't send build tasks to read-only nodes. Don't push from nodes that aren't allowed to.
202
223
  - **Never fabricate tool results.** Always call the actual tool; never pretend you did.
203
224
  - **Clean up worktree nodes.** After a worktree task completes and its changes are merged or checkpointed, call \`mesh_remove_node\` to free resources.
204
- - **Do not strand completed branches.** A checkpointed or clean feature/worktree branch is not done by itself. Merge/refine it to the mesh default branch, or explicitly report one of \`pushed_feature_branch_needs_merge\`, \`blocked_review\`, \`cleanup_candidate\`, or \`not_mergeable\` with the next action.
225
+ - **Do not strand completed branches.** A checkpointed or clean feature/worktree branch is not done by itself. Merge/refine it to the mesh default branch, fast-forward obvious clean behind-only branches with \`mesh_fast_forward_node\`, or explicitly report one of \`pushed_feature_branch_needs_merge\`, \`blocked_review\`, \`cleanup_candidate\`, or \`not_mergeable\` with the next action.
226
+ - **Keep Refinery validation project-configurable.** \`mesh_refine_node\` must execute validation from repo mesh/refine config (for example \`.adhdev/refine.{json,yaml,yml}\`, \`.adhdev/repo-mesh-refine.*\`, or \`repo-mesh.refine.*\`). Heuristics are suggestions/scaffolding only, not the execution path.
227
+ - **Treat submodule main reachability as publish-needed.** A \`submodule_reachability_failed\` refine result means the root gitlink points at a submodule commit that is not reachable from the configured submodule remote main branch. Do not treat feature-branch reachability as complete, retry validation blindly, or start code review first. Classify it as \`blocked_review\`, request user approval to push/publish the submodule commit to submodule main, then rerun \`mesh_refine_node\`, unless the mesh or repo refine config explicitly enabled \`allowAutoPublishSubmoduleMainCommits\` and Refinery reports exact path/commit/remote/branch evidence with post-publish verification.
205
228
  - **Name worktree branches meaningfully.** Use descriptive names like \`feat/auth-refactor\` or \`fix/build-123\`.${coordinatorNote}`;
206
229
  }