@adhdev/daemon-core 0.9.77-rc.4 → 0.9.77-rc.40

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.
@@ -8,17 +8,12 @@ export interface PendingMeshCoordinatorEvent {
8
8
  }
9
9
  /** Drain and return all pending coordinator events, clearing the queue. */
10
10
  export declare function drainPendingMeshCoordinatorEvents(): PendingMeshCoordinatorEvent[];
11
- export declare function tryAssignQueueTask(components: {
12
- cliManager: any;
13
- }, meshId: string, nodeId: string, sessionId: string, providerType: string): boolean;
11
+ export declare function tryAssignQueueTask(components: DaemonComponents, meshId: string, nodeId: string, sessionId: string, providerType: string): boolean;
14
12
  /**
15
13
  * Triggers a queue check for all nodes in the mesh.
16
14
  * Called when a new task is enqueued, in case nodes are already idle.
17
15
  */
18
- export declare function triggerMeshQueue(components: {
19
- instanceManager: any;
20
- cliManager: any;
21
- }, meshId: string): void;
16
+ export declare function triggerMeshQueue(components: DaemonComponents, meshId: string): void;
22
17
  export declare function handleMeshForwardEvent(components: DaemonComponents, payload: Record<string, unknown>): {
23
18
  success: boolean;
24
19
  forwarded: number;
@@ -1,4 +1,4 @@
1
- export type MeshTaskStatus = 'pending' | 'assigned' | 'completed' | 'failed';
1
+ export type MeshTaskStatus = 'pending' | 'assigned' | 'completed' | 'failed' | 'cancelled';
2
2
  export interface MeshWorkQueueEntry {
3
3
  id: string;
4
4
  meshId: string;
@@ -6,10 +6,19 @@ export interface MeshWorkQueueEntry {
6
6
  status: MeshTaskStatus;
7
7
  /** If specified, only this node can claim the task (used by legacy mesh_send_task) */
8
8
  targetNodeId?: string;
9
+ /** If specified, only this runtime session can claim the task */
10
+ targetSessionId?: string;
9
11
  /** The node that actually claimed and is executing the task */
10
12
  assignedNodeId?: string;
11
13
  /** The session currently executing the task */
12
14
  assignedSessionId?: string;
15
+ /** Human/operator reason for terminal cancellation. */
16
+ cancelReason?: string;
17
+ cancelledAt?: string;
18
+ /** Human/operator reason for manually requeueing a task. */
19
+ requeueReason?: string;
20
+ requeuedAt?: string;
21
+ requeueCount?: number;
13
22
  createdAt: string;
14
23
  updatedAt: string;
15
24
  }
@@ -18,6 +27,7 @@ export interface MeshWorkQueueEntry {
18
27
  */
19
28
  export declare function enqueueTask(meshId: string, message: string, opts?: {
20
29
  targetNodeId?: string;
30
+ targetSessionId?: string;
21
31
  }): MeshWorkQueueEntry;
22
32
  /**
23
33
  * Get all tasks in the queue, optionally filtered by status.
@@ -34,6 +44,23 @@ export declare function claimNextTask(meshId: string, nodeId: string, sessionId:
34
44
  * Used when a session completes, fails, or stalls.
35
45
  */
36
46
  export declare function updateTaskStatus(meshId: string, taskId: string, status: MeshTaskStatus): MeshWorkQueueEntry | null;
47
+ /**
48
+ * Mark a queue task as manually cancelled without deleting audit history.
49
+ */
50
+ export declare function cancelTask(meshId: string, taskId: string, opts?: {
51
+ reason?: string;
52
+ }): MeshWorkQueueEntry | null;
53
+ /**
54
+ * Return a queue task to pending for retry. By default, dead session targeting
55
+ * and assigned ownership are cleared so stale assignments do not strand again.
56
+ */
57
+ export declare function requeueTask(meshId: string, taskId: string, opts?: {
58
+ reason?: string;
59
+ targetNodeId?: string;
60
+ targetSessionId?: string;
61
+ clearTargetNode?: boolean;
62
+ clearTargetSession?: boolean;
63
+ }): MeshWorkQueueEntry | null;
37
64
  /**
38
65
  * Update the status of the task currently assigned to a specific session.
39
66
  */
@@ -43,6 +70,13 @@ export interface MeshWorkQueueStats {
43
70
  assigned: number;
44
71
  completed: number;
45
72
  failed: number;
73
+ cancelled: number;
74
+ activeAssignments: Array<{
75
+ id: string;
76
+ nodeId?: string;
77
+ sessionId?: string;
78
+ message: string;
79
+ }>;
46
80
  }
47
81
  /**
48
82
  * Return aggregate queue statistics for the given mesh.
@@ -297,6 +297,13 @@ export interface SessionEntry {
297
297
  assigned: number;
298
298
  completed: number;
299
299
  failed: number;
300
+ cancelled?: number;
301
+ activeAssignments?: Array<{
302
+ id: string;
303
+ nodeId?: string;
304
+ sessionId?: string;
305
+ message: string;
306
+ }>;
300
307
  };
301
308
  }
302
309
  /**
@@ -341,6 +348,13 @@ export interface CompactSessionEntry {
341
348
  assigned: number;
342
349
  completed: number;
343
350
  failed: number;
351
+ cancelled?: number;
352
+ activeAssignments?: Array<{
353
+ id: string;
354
+ nodeId?: string;
355
+ sessionId?: string;
356
+ message: string;
357
+ }>;
344
358
  };
345
359
  }
346
360
  export type VersionUpdateReason = 'force_update_below' | 'major_minor_mismatch' | 'patch_mismatch' | 'daemon_ahead';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.77-rc.4",
3
+ "version": "0.9.77-rc.40",
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",
@@ -83,6 +83,9 @@ export interface DaemonInitConfig {
83
83
 
84
84
  /** Fired before send_chat is dispatched — used for turn snapshot hooks */
85
85
  onBeforeSendChat?: (params: { workspace: string; sessionId: string }) => void;
86
+
87
+ /** Relays a command to a remote mesh node daemon */
88
+ dispatchMeshCommand?: (daemonId: string, command: string, args: Record<string, unknown>) => Promise<any>;
86
89
  }
87
90
 
88
91
  // ─── Result ───
@@ -100,6 +103,7 @@ export interface DaemonComponents {
100
103
  sessionRegistry: SessionRegistry;
101
104
  detectedIdes: { value: IDEInfo[] };
102
105
  refreshProviderAvailability: (providerType?: string) => Promise<void>;
106
+ dispatchMeshCommand?: (daemonId: string, command: string, args: Record<string, unknown>) => Promise<any>;
103
107
  }
104
108
 
105
109
  export interface DaemonDevSupportOptions {
@@ -331,6 +335,7 @@ export async function initDaemonComponents(config: DaemonInitConfig): Promise<Da
331
335
  sessionRegistry,
332
336
  detectedIdes: detectedIdesRef,
333
337
  refreshProviderAvailability,
338
+ dispatchMeshCommand: config.dispatchMeshCommand,
334
339
  };
335
340
 
336
341
  // 11. Setup Mesh Event Forwarding
@@ -195,6 +195,8 @@ export class ProviderCliAdapter implements CliAdapter {
195
195
 
196
196
  // ─── CLI Scripts (script-based parsing) ───
197
197
  private cliScripts: CliScripts;
198
+ /** Per-session opaque state object created by cliScripts.createState(), reset on stop. */
199
+ private scriptState: unknown = null;
198
200
  private runtimeSettings: Record<string, any> = {};
199
201
  /** Full accumulated rendered PTY transcript for parser/readback use */
200
202
  private accumulatedBuffer: string = '';
@@ -223,6 +225,7 @@ export class ProviderCliAdapter implements CliAdapter {
223
225
  currentTurnScope: TurnParseScope | null;
224
226
  recentOutputBuffer: string;
225
227
  accumulatedBuffer: string;
228
+ accumulatedRawBuffer: string;
226
229
  screenText: string;
227
230
  currentStatus: CliSessionStatus['status'];
228
231
  activeModal: { message: string; buttons: string[] } | null;
@@ -305,6 +308,7 @@ export class ProviderCliAdapter implements CliAdapter {
305
308
  && cached.currentTurnScope === this.currentTurnScope
306
309
  && cached.recentOutputBuffer === this.recentOutputBuffer
307
310
  && cached.accumulatedBuffer === this.accumulatedBuffer
311
+ && cached.accumulatedRawBuffer === this.accumulatedRawBuffer
308
312
  && cached.screenText === this.lastScreenText
309
313
  && cached.currentStatus === this.currentStatus
310
314
  && cached.activeModal === this.activeModal
@@ -477,6 +481,9 @@ export class ProviderCliAdapter implements CliAdapter {
477
481
  this.cliScripts = scripts;
478
482
  this.parsedStatusCache = null;
479
483
  this.parseErrorMessage = null;
484
+ // Initialize per-session state: createState() is called once here and on script reload.
485
+ // The returned object lives until the PTY exits (scriptState = null on exit).
486
+ this.scriptState = typeof scripts.createState === 'function' ? scripts.createState() : null;
480
487
  const scriptNames = listCliScriptNames(scripts);
481
488
  LOG.info('CLI', `[${this.cliType}] CLI scripts injected: [${scriptNames.join(', ')}]`);
482
489
  }
@@ -610,6 +617,7 @@ export class ProviderCliAdapter implements CliAdapter {
610
617
  this.ready = false;
611
618
  this.startupParseGate = false;
612
619
  this.spawnAt = 0;
620
+ this.scriptState = null;
613
621
  this.onStatusChange?.();
614
622
  });
615
623
 
@@ -1450,6 +1458,14 @@ export class ProviderCliAdapter implements CliAdapter {
1450
1458
 
1451
1459
  // ─── Script Execution ──────────────────────────
1452
1460
 
1461
+ private invokeCliScript<T>(script: Function, input: any): T {
1462
+ const hasStateFactory = typeof this.cliScripts?.createState === 'function';
1463
+ const expectsStateArgument = hasStateFactory || this.scriptState !== null || script.length >= 2;
1464
+ return expectsStateArgument
1465
+ ? script(this.scriptState, input)
1466
+ : script(input);
1467
+ }
1468
+
1453
1469
  private runParseSession(): ParsedSession | null {
1454
1470
  if (typeof this.cliScripts?.parseSession !== 'function') {
1455
1471
  this.parseErrorMessage = `${this.cliType} parseSession unavailable`;
@@ -1470,7 +1486,10 @@ export class ProviderCliAdapter implements CliAdapter {
1470
1486
  scope: this.currentTurnScope,
1471
1487
  runtimeSettings: this.runtimeSettings,
1472
1488
  });
1473
- const session = this.cliScripts.parseSession({ ...input, tail, tailScreen: buildCliScreenSnapshot(tail) });
1489
+ const session = this.invokeCliScript<ParsedSession | null>(
1490
+ this.cliScripts.parseSession,
1491
+ { ...input, tail, tailScreen: buildCliScreenSnapshot(tail) },
1492
+ );
1474
1493
  this.parseErrorMessage = null;
1475
1494
  return session && typeof session === 'object' ? session : null;
1476
1495
  } catch (e: any) {
@@ -1485,7 +1504,7 @@ export class ProviderCliAdapter implements CliAdapter {
1485
1504
  if (!this.cliScripts?.detectStatus) return null;
1486
1505
  try {
1487
1506
  const screenText = this.terminalScreen.getText();
1488
- const status = this.cliScripts.detectStatus({
1507
+ const status = this.invokeCliScript<string | null>(this.cliScripts.detectStatus, {
1489
1508
  tail: text.slice(-500),
1490
1509
  screenText,
1491
1510
  rawBuffer: this.accumulatedRawBuffer,
@@ -1505,7 +1524,7 @@ export class ProviderCliAdapter implements CliAdapter {
1505
1524
  try {
1506
1525
  const screenText = this.terminalScreen.getText();
1507
1526
  const buffer = screenText || this.accumulatedBuffer;
1508
- return this.cliScripts.parseApproval({
1527
+ return this.invokeCliScript<{ message: string; buttons: string[] } | null>(this.cliScripts.parseApproval, {
1509
1528
  buffer,
1510
1529
  screenText,
1511
1530
  rawBuffer: this.accumulatedRawBuffer,
@@ -1576,6 +1595,7 @@ export class ProviderCliAdapter implements CliAdapter {
1576
1595
  && cached.currentTurnScope === this.currentTurnScope
1577
1596
  && cached.recentOutputBuffer === this.recentOutputBuffer
1578
1597
  && cached.accumulatedBuffer === this.accumulatedBuffer
1598
+ && cached.accumulatedRawBuffer === this.accumulatedRawBuffer
1579
1599
  && cached.screenText === parseScreenText
1580
1600
  && cached.currentStatus === this.currentStatus
1581
1601
  && cached.activeModal === this.activeModal
@@ -1615,6 +1635,7 @@ export class ProviderCliAdapter implements CliAdapter {
1615
1635
  currentTurnScope: this.currentTurnScope,
1616
1636
  recentOutputBuffer: this.recentOutputBuffer,
1617
1637
  accumulatedBuffer: this.accumulatedBuffer,
1638
+ accumulatedRawBuffer: this.accumulatedRawBuffer,
1618
1639
  screenText: parseScreenText,
1619
1640
  currentStatus: this.currentStatus,
1620
1641
  activeModal: this.activeModal,
@@ -1640,7 +1661,7 @@ export class ProviderCliAdapter implements CliAdapter {
1640
1661
  scope: this.currentTurnScope,
1641
1662
  runtimeSettings: this.runtimeSettings,
1642
1663
  });
1643
- return await Promise.resolve(fn({
1664
+ return await Promise.resolve(fn(this.scriptState, {
1644
1665
  ...input,
1645
1666
  args: args && typeof args === 'object' ? { ...args } : {},
1646
1667
  }));
@@ -48,11 +48,21 @@ export interface ParsedSession {
48
48
  }
49
49
 
50
50
  export interface CliScripts {
51
- parseSession?: (input: CliScriptInput & { tail?: string; tailScreen?: CliScreenSnapshot }) => ParsedSession | null;
52
- detectStatus?: (input: CliStatusInput) => string | null;
53
- parseApproval?: (input: CliApprovalInput) => { message: string; buttons: string[] } | null;
51
+ /**
52
+ * Optional state factory. Called once per CLI session start (or script reload).
53
+ * The returned object is passed as the first argument to detectStatus, parseApproval,
54
+ * and parseSession on every invocation, allowing scripts to maintain per-session state
55
+ * (e.g. last-seen status, approval fingerprints, stability counters).
56
+ *
57
+ * Scripts that don't define createState() receive null as the state argument,
58
+ * making this change fully backward compatible.
59
+ */
60
+ createState?: () => unknown;
61
+ parseSession?: (state: unknown, input: CliScriptInput & { tail?: string; tailScreen?: CliScreenSnapshot }) => ParsedSession | null;
62
+ detectStatus?: (state: unknown, input: CliStatusInput) => string | null;
63
+ parseApproval?: (state: unknown, input: CliApprovalInput) => { message: string; buttons: string[] } | null;
54
64
  resolveAction?: (data: any) => string;
55
- [name: string]: ((input: any) => any) | undefined;
65
+ [name: string]: ((state: unknown, input: any) => any) | ((data: any) => any) | (() => unknown) | undefined;
56
66
  }
57
67
 
58
68
  export interface CliScreenLine {
@@ -177,10 +177,6 @@ export function buildCoordinatorDelegatedCliLaunchOptions(
177
177
  const cliArgs = Array.isArray(input.cliArgs) ? [...input.cliArgs] : [];
178
178
  const env: Record<string, string> = { ...(input.env || {}), ...COORDINATOR_DELEGATED_ENV_UNSETS };
179
179
 
180
- if (cliType === 'hermes-cli' && !hasCliArg(cliArgs, '--ignore-user-config')) {
181
- cliArgs.unshift('--ignore-user-config');
182
- }
183
-
184
180
  if (cliType === 'claude-cli' && !hasCliArg(cliArgs, '--mcp-config')) {
185
181
  cliArgs.unshift('--mcp-config', ensureEmptyDelegatedMcpConfig(input.workspace));
186
182
  }
@@ -28,6 +28,15 @@ export type MeshCoordinatorSetup =
28
28
  instructions: string
29
29
  template: string
30
30
  }
31
+ | {
32
+ /** Provider registers MCP via its own CLI command (e.g. `codex mcp add` / `gemini mcp add`). */
33
+ kind: 'cli_command'
34
+ serverName: string
35
+ /** The rendered shell command to execute before launching the coordinator session. */
36
+ command: string
37
+ requiresRestart: boolean
38
+ instructions: string
39
+ }
31
40
  | {
32
41
  kind: 'unsupported'
33
42
  reason: string
@@ -41,6 +50,8 @@ export interface ResolveMeshCoordinatorSetupOptions {
41
50
  adhdevMcpCommand?: string
42
51
  adhdevMcpEntryPath?: string
43
52
  nodeExecutable?: string
53
+ adhdevMcpTransport?: 'local' | 'ipc'
54
+ adhdevMcpPort?: number
44
55
  }
45
56
 
46
57
  const DEFAULT_SERVER_NAME = 'adhdev-mesh'
@@ -58,6 +69,8 @@ function resolveHermesMeshCoordinatorSetup(options: ResolveMeshCoordinatorSetupO
58
69
  meshId: options.meshId,
59
70
  nodeExecutable: options.nodeExecutable,
60
71
  adhdevMcpEntryPath: options.adhdevMcpEntryPath,
72
+ adhdevMcpTransport: options.adhdevMcpTransport,
73
+ adhdevMcpPort: options.adhdevMcpPort,
61
74
  })
62
75
  if (!mcpServer) {
63
76
  return {
@@ -130,6 +143,8 @@ export function resolveMeshCoordinatorSetup(options: ResolveMeshCoordinatorSetup
130
143
  meshId,
131
144
  nodeExecutable: options.nodeExecutable,
132
145
  adhdevMcpEntryPath: options.adhdevMcpEntryPath,
146
+ adhdevMcpTransport: options.adhdevMcpTransport,
147
+ adhdevMcpPort: options.adhdevMcpPort,
133
148
  })
134
149
  if (!mcpServer) {
135
150
  return {
@@ -152,6 +167,24 @@ export function resolveMeshCoordinatorSetup(options: ResolveMeshCoordinatorSetup
152
167
  if (!instructions || !template?.trim()) {
153
168
  return { kind: 'unsupported', reason: 'Provider manual MCP setup is missing instructions or template' }
154
169
  }
170
+ const renderedTemplate = renderMeshCoordinatorTemplate(template, {
171
+ meshId,
172
+ workspace,
173
+ serverName,
174
+ adhdevMcpCommand: options.adhdevMcpCommand || DEFAULT_ADHDEV_MCP_COMMAND,
175
+ })
176
+ // Detect if the template is a runnable CLI command (single line, no YAML/JSON structure).
177
+ // If so, use cli_command kind so the daemon can execute it automatically.
178
+ const isCliCommand = !renderedTemplate.trim().includes('\n') && !renderedTemplate.trim().startsWith('{')
179
+ if (isCliCommand) {
180
+ return {
181
+ kind: 'cli_command',
182
+ serverName,
183
+ command: renderedTemplate.trim(),
184
+ requiresRestart: mcpConfig.requiresRestart === true,
185
+ instructions: instructions,
186
+ }
187
+ }
155
188
  return {
156
189
  kind: 'manual',
157
190
  serverName,
@@ -159,12 +192,7 @@ export function resolveMeshCoordinatorSetup(options: ResolveMeshCoordinatorSetup
159
192
  configPathCommand: mcpConfig.configPathCommand,
160
193
  requiresRestart: mcpConfig.requiresRestart === true,
161
194
  instructions,
162
- template: renderMeshCoordinatorTemplate(template, {
163
- meshId,
164
- workspace,
165
- serverName,
166
- adhdevMcpCommand: options.adhdevMcpCommand || DEFAULT_ADHDEV_MCP_COMMAND,
167
- }),
195
+ template: renderedTemplate,
168
196
  }
169
197
  }
170
198
 
@@ -196,17 +224,37 @@ function resolveAdhdevMcpServerLaunch(options: {
196
224
  meshId: string
197
225
  nodeExecutable?: string
198
226
  adhdevMcpEntryPath?: string
227
+ adhdevMcpTransport?: 'local' | 'ipc'
228
+ adhdevMcpPort?: number
199
229
  }): MeshCoordinatorMcpServerLaunch | null {
200
230
  const entryPath = resolveAdhdevMcpEntryPath(options.adhdevMcpEntryPath)
201
231
  if (!entryPath) return null
202
232
  const nodeExecutable = resolveMcpNodeExecutable(options.nodeExecutable)
203
233
  if (!nodeExecutable) return null
234
+ const transport = resolveMcpTransport(options.adhdevMcpTransport)
235
+ const args = [entryPath, '--mode', transport, '--repo-mesh', options.meshId]
236
+ const port = resolveMcpPort(options.adhdevMcpPort)
237
+ if (port !== undefined) args.push('--port', String(port))
204
238
  return {
205
239
  command: nodeExecutable,
206
- args: [entryPath, '--mode', 'ipc', '--repo-mesh', options.meshId],
240
+ args,
207
241
  }
208
242
  }
209
243
 
244
+ function resolveMcpTransport(explicitTransport?: 'local' | 'ipc'): 'local' | 'ipc' {
245
+ if (explicitTransport === 'local' || explicitTransport === 'ipc') return explicitTransport
246
+ const envTransport = process.env.ADHDEV_COORDINATOR_MCP_TRANSPORT?.trim()
247
+ return envTransport === 'local' ? 'local' : 'ipc'
248
+ }
249
+
250
+ function resolveMcpPort(explicitPort?: number): number | undefined {
251
+ if (typeof explicitPort === 'number' && Number.isInteger(explicitPort) && explicitPort > 0) return explicitPort
252
+ const raw = process.env.ADHDEV_COORDINATOR_MCP_PORT?.trim()
253
+ if (!raw) return undefined
254
+ const parsed = Number(raw)
255
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined
256
+ }
257
+
210
258
  function resolveMcpNodeExecutable(explicitExecutable?: string): string | null {
211
259
  const explicit = explicitExecutable?.trim()
212
260
  if (explicit) return explicit
@@ -332,7 +332,7 @@ export class DaemonCommandRouter {
332
332
  this.deps = deps;
333
333
  }
334
334
 
335
- private getCachedInlineMesh(meshId: string, inlineMesh?: unknown): any | undefined {
335
+ public getCachedInlineMesh(meshId: string, inlineMesh?: unknown): any | undefined {
336
336
  if (inlineMesh && typeof inlineMesh === 'object') {
337
337
  this.inlineMeshCache.set(meshId, inlineMesh as any);
338
338
  return inlineMesh as any;
@@ -1331,6 +1331,56 @@ export class DaemonCommandRouter {
1331
1331
  }
1332
1332
  }
1333
1333
 
1334
+ case 'get_mesh_queue': {
1335
+ const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
1336
+ if (!meshId) return { success: false, error: 'meshId required' };
1337
+ try {
1338
+ const { getQueue } = await import('../mesh/mesh-work-queue.js');
1339
+ const status = Array.isArray(args?.status)
1340
+ ? args.status.map((s: any) => typeof s === 'string' ? s.trim() : '').filter(Boolean)
1341
+ : undefined;
1342
+ const queue = getQueue(meshId, { status: status as any });
1343
+ return { success: true, queue };
1344
+ } catch (e: any) {
1345
+ return { success: false, error: e.message };
1346
+ }
1347
+ }
1348
+
1349
+ case 'cancel_mesh_queue_task': {
1350
+ const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
1351
+ const taskId = typeof args?.taskId === 'string' ? args.taskId.trim() : '';
1352
+ if (!meshId || !taskId) return { success: false, error: 'meshId and taskId required' };
1353
+ try {
1354
+ const { cancelTask } = await import('../mesh/mesh-work-queue.js');
1355
+ const reason = typeof args?.reason === 'string' ? args.reason : undefined;
1356
+ const task = cancelTask(meshId, taskId, { reason });
1357
+ if (!task) return { success: false, error: `Queue task '${taskId}' not found` };
1358
+ return { success: true, task };
1359
+ } catch (e: any) {
1360
+ return { success: false, error: e.message };
1361
+ }
1362
+ }
1363
+
1364
+ case 'requeue_mesh_queue_task': {
1365
+ const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
1366
+ const taskId = typeof args?.taskId === 'string' ? args.taskId.trim() : '';
1367
+ if (!meshId || !taskId) return { success: false, error: 'meshId and taskId required' };
1368
+ try {
1369
+ const { requeueTask } = await import('../mesh/mesh-work-queue.js');
1370
+ const task = requeueTask(meshId, taskId, {
1371
+ reason: typeof args?.reason === 'string' ? args.reason : undefined,
1372
+ targetNodeId: typeof args?.targetNodeId === 'string' ? args.targetNodeId.trim() : undefined,
1373
+ targetSessionId: typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : undefined,
1374
+ clearTargetNode: args?.clearTargetNode === true,
1375
+ clearTargetSession: args?.clearTargetSession !== false,
1376
+ });
1377
+ if (!task) return { success: false, error: `Queue task '${taskId}' not found` };
1378
+ return { success: true, task };
1379
+ } catch (e: any) {
1380
+ return { success: false, error: e.message };
1381
+ }
1382
+ }
1383
+
1334
1384
  case 'add_mesh_node': {
1335
1385
  const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
1336
1386
  const workspace = typeof args?.workspace === 'string' ? args.workspace.trim() : '';
@@ -1519,7 +1569,13 @@ export class DaemonCommandRouter {
1519
1569
  appendLedgerEntry(meshId, {
1520
1570
  kind: 'node_removed',
1521
1571
  nodeId,
1522
- payload: { worktree: !!node?.isLocalWorktree, sessionCleanupMode },
1572
+ payload: {
1573
+ worktree: !!node?.isLocalWorktree,
1574
+ sessionCleanupMode,
1575
+ workspace: typeof node?.workspace === 'string' ? node.workspace : undefined,
1576
+ daemonId: typeof node?.daemonId === 'string' ? node.daemonId : undefined,
1577
+ worktreeBranch: typeof node?.worktreeBranch === 'string' ? node.worktreeBranch : undefined,
1578
+ },
1523
1579
  });
1524
1580
  } catch { /* ledger append is best-effort */ }
1525
1581
  }
@@ -1711,6 +1767,105 @@ export class DaemonCommandRouter {
1711
1767
  };
1712
1768
  }
1713
1769
 
1770
+ // ─── CLI-command MCP registration (Codex, Gemini CLI) ───────────
1771
+ if (coordinatorSetup.kind === 'cli_command') {
1772
+ // Build coordinator prompt first — fail closed on errors.
1773
+ let cliCmdSystemPrompt = '';
1774
+ try {
1775
+ cliCmdSystemPrompt = buildCoordinatorSystemPrompt({ mesh, coordinatorCliType: cliType });
1776
+ } catch (error: any) {
1777
+ const message = error?.message || String(error);
1778
+ LOG.error('MeshCoordinator', `Failed to build coordinator prompt: ${message}`);
1779
+ return {
1780
+ success: false,
1781
+ code: 'mesh_coordinator_prompt_failed',
1782
+ error: `Failed to build Repo Mesh coordinator prompt: ${message}`,
1783
+ meshId, cliType, workspace,
1784
+ };
1785
+ }
1786
+
1787
+ // Run the provider's MCP registration command.
1788
+ try {
1789
+ const { execFileSync: execCmdSync } = await import('node:child_process');
1790
+ const cmdParts = coordinatorSetup.command.trim().split(/\s+/);
1791
+ const [regCmd, ...regArgs] = cmdParts;
1792
+ LOG.info('MeshCoordinator', `Running MCP registration: ${coordinatorSetup.command}`);
1793
+ execCmdSync(regCmd, regArgs, { stdio: 'pipe', timeout: 15_000 });
1794
+ } catch (error: any) {
1795
+ // Non-fatal — server may already be registered (providers return exit 1 on duplicate).
1796
+ LOG.warn('MeshCoordinator', `MCP registration command failed (may be pre-registered): ${error?.message || error}`);
1797
+ }
1798
+
1799
+ // Inject system prompt using provider-native methods.
1800
+ // Codex: -c 'instructions="..."' CLI config override
1801
+ // Gemini: write GEMINI.md to workspace (auto-loaded as context)
1802
+ const cliCmdArgs: string[] = [];
1803
+ const cliCmdEnv: Record<string, string> = {};
1804
+ if (cliCmdSystemPrompt) {
1805
+ if (cliType === 'codex-cli') {
1806
+ // Codex reads `developer_instructions` from config.toml as system instructions.
1807
+ // The -c flag overrides a config key for this session only.
1808
+ cliCmdArgs.push('-c', `developer_instructions=${JSON.stringify(cliCmdSystemPrompt)}`);
1809
+ } else if (cliType === 'gemini-cli') {
1810
+ // Gemini CLI auto-loads GEMINI.md from CWD as project context.
1811
+ // Write a temporary GEMINI.md to the workspace before launch.
1812
+ try {
1813
+ const { writeFileSync: wfs, existsSync: efs, readFileSync: rfs } = await import('node:fs');
1814
+ const geminiMdPath = `${workspace}/GEMINI.md`;
1815
+ const marker = '<!-- adhdev-mesh-coordinator-prompt -->';
1816
+ const markerEnd = '<!-- /adhdev-mesh-coordinator-prompt -->';
1817
+ const block = `${marker}\n${cliCmdSystemPrompt}\n${markerEnd}`;
1818
+ if (efs(geminiMdPath)) {
1819
+ const existing = rfs(geminiMdPath, 'utf-8');
1820
+ // Replace existing block or append
1821
+ const replaced = existing.replace(
1822
+ new RegExp(`${marker}[\\s\\S]*?${markerEnd}`, 'g'),
1823
+ block,
1824
+ );
1825
+ wfs(geminiMdPath, replaced.includes(marker) ? replaced : `${existing}\n\n${block}`);
1826
+ } else {
1827
+ wfs(geminiMdPath, block);
1828
+ }
1829
+ LOG.info('MeshCoordinator', `Wrote coordinator prompt to ${workspace}/GEMINI.md`);
1830
+ } catch (e: any) {
1831
+ LOG.warn('MeshCoordinator', `Could not write GEMINI.md: ${e?.message || e}`);
1832
+ }
1833
+ }
1834
+ }
1835
+
1836
+ const cliCmdLaunch: any = await this.deps.cliManager.handleCliCommand('launch_cli', {
1837
+ cliType,
1838
+ dir: workspace,
1839
+ cliArgs: cliCmdArgs.length > 0 ? cliCmdArgs : undefined,
1840
+ env: Object.keys(cliCmdEnv).length > 0 ? cliCmdEnv : undefined,
1841
+ settings: { meshCoordinatorFor: meshId },
1842
+ });
1843
+
1844
+ if (!cliCmdLaunch?.success) {
1845
+ return { success: false, error: cliCmdLaunch?.error || 'Failed to launch CLI session' };
1846
+ }
1847
+
1848
+ LOG.info('MeshCoordinator', `Launched ${cliType} coordinator (cli_command) for mesh ${meshId}`);
1849
+ try {
1850
+ const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
1851
+ appendLedgerEntry(meshId, {
1852
+ kind: 'coordinator_started',
1853
+ sessionId: cliCmdLaunch.sessionId || cliCmdLaunch.id,
1854
+ providerType: cliType,
1855
+ payload: { workspace },
1856
+ });
1857
+ } catch { /* best-effort */ }
1858
+
1859
+ return {
1860
+ success: true,
1861
+ meshId,
1862
+ cliType,
1863
+ workspace,
1864
+ sessionId: cliCmdLaunch.sessionId || cliCmdLaunch.id,
1865
+ mcpRegistered: true,
1866
+ };
1867
+ }
1868
+
1714
1869
  const configFormat = coordinatorSetup.configFormat as MeshCoordinatorConfigFormat;
1715
1870
  if (configFormat !== 'claude_mcp_json' && configFormat !== 'hermes_config_yaml') {
1716
1871
  return {
@@ -1777,9 +1932,11 @@ export class DaemonCommandRouter {
1777
1932
  args: coordinatorSetup.mcpServer.args,
1778
1933
  };
1779
1934
  if (args?.inlineMesh) {
1935
+ const modeArgIndex = coordinatorSetup.mcpServer.args.findIndex((value: string) => value === '--mode');
1936
+ const mcpTransport = modeArgIndex >= 0 ? coordinatorSetup.mcpServer.args[modeArgIndex + 1] : 'ipc';
1780
1937
  mcpServerEntry.env = {
1781
1938
  ADHDEV_INLINE_MESH: JSON.stringify(mesh),
1782
- ADHDEV_MCP_TRANSPORT: 'ipc',
1939
+ ADHDEV_MCP_TRANSPORT: mcpTransport === 'local' ? 'local' : 'ipc',
1783
1940
  };
1784
1941
  }
1785
1942
 
@@ -113,11 +113,18 @@ export async function handleOpenPanel(h: CommandHelpers, args: any): Promise<Com
113
113
  export async function handlePtyInput(h: CommandHelpers, args: any): Promise<CommandResult> {
114
114
  const { cliType, data, targetSessionId } = args || {};
115
115
  if (!data) return { success: false, error: 'data required' };
116
+
117
+ // Filter out VT100/VT420 Device Attributes responses (e.g. \x1b[?1;2c or \x1b[>0;276;0c)
118
+ // These are echoed by xterm.js in the dashboard in response to \x1b[c queries
119
+ // and pollute the CLI input buffer.
120
+ const cleanData = typeof data === 'string' ? data.replace(/\x1b\[[?>][0-9;]*c/g, '') : data;
121
+ if (!cleanData) return { success: true };
122
+
116
123
  const adapter = h.getCliAdapter(targetSessionId || cliType);
117
124
  if (!adapter || typeof adapter.writeRaw !== 'function') {
118
125
  return { success: false, error: `CLI adapter not found: ${targetSessionId || cliType || 'unknown'}` };
119
126
  }
120
- await adapter.writeRaw(data);
127
+ await adapter.writeRaw(cleanData);
121
128
  return { success: true };
122
129
  }
123
130
 
package/src/index.ts CHANGED
@@ -154,8 +154,8 @@ export { appendLedgerEntry, readLedgerEntries, getLedgerSummary, getLedgerDir, g
154
154
  export type { MeshLedgerEntry, MeshLedgerKind, MeshLedgerSummary, ReadLedgerOptions, SessionRecoveryContext } from './mesh/mesh-ledger.js';
155
155
 
156
156
  // ── Mesh Work Queue (GUPP) ──
157
- export { enqueueTask, getQueue, claimNextTask, updateTaskStatus, updateSessionTaskStatus } from './mesh/mesh-work-queue.js';
158
- export type { MeshWorkQueueEntry, MeshTaskStatus } from './mesh/mesh-work-queue.js';
157
+ export { enqueueTask, getQueue, claimNextTask, updateTaskStatus, updateSessionTaskStatus, cancelTask, requeueTask, getMeshQueueStats } from './mesh/mesh-work-queue.js';
158
+ export type { MeshWorkQueueEntry, MeshTaskStatus, MeshWorkQueueStats } from './mesh/mesh-work-queue.js';
159
159
 
160
160
  // ── Mesh Events ──
161
161
  export { triggerMeshQueue } from './mesh/mesh-events.js';