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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/boot/daemon-lifecycle.d.ts +1 -0
  2. package/dist/commands/cli-manager.d.ts +2 -1
  3. package/dist/commands/mesh-coordinator.d.ts +13 -0
  4. package/dist/commands/router.d.ts +5 -1
  5. package/dist/config/chat-history.d.ts +1 -0
  6. package/dist/git/git-commands.d.ts +2 -0
  7. package/dist/git/git-types.d.ts +2 -0
  8. package/dist/index.d.ts +2 -2
  9. package/dist/index.js +15461 -14552
  10. package/dist/index.js.map +1 -1
  11. package/dist/index.mjs +14411 -13502
  12. package/dist/index.mjs.map +1 -1
  13. package/dist/mesh/beads-db.d.ts +1 -0
  14. package/dist/mesh/mesh-events.d.ts +46 -1
  15. package/dist/mesh/mesh-work-queue.d.ts +1 -0
  16. package/dist/providers/cli-provider-instance.d.ts +4 -0
  17. package/dist/providers/contracts.d.ts +32 -1
  18. package/dist/providers/native-history/dispatcher.d.ts +2 -0
  19. package/dist/providers/sdk/v1/types/common/index.d.ts +35 -1
  20. package/dist/providers/spec/cli-adapter.d.ts +1 -0
  21. package/dist/providers/spec/driver.d.ts +6 -1
  22. package/dist/providers/spec/native-history-executor.d.ts +2 -0
  23. package/dist/providers/spec/schema.gen.d.ts +22 -0
  24. package/dist/providers/spec/types.d.ts +10 -0
  25. package/dist/repo-mesh-types.d.ts +6 -0
  26. package/package.json +1 -1
  27. package/src/boot/daemon-lifecycle.ts +2 -0
  28. package/src/commands/chat-commands.ts +206 -18
  29. package/src/commands/cli-manager.ts +56 -14
  30. package/src/commands/mesh-coordinator.ts +110 -5
  31. package/src/commands/router.ts +146 -21
  32. package/src/config/chat-history.ts +4 -0
  33. package/src/git/git-commands.ts +20 -2
  34. package/src/git/git-status.ts +35 -6
  35. package/src/git/git-types.ts +2 -0
  36. package/src/index.ts +2 -2
  37. package/src/mesh/beads-db.ts +4 -0
  38. package/src/mesh/mesh-events.ts +264 -4
  39. package/src/mesh/mesh-work-queue.ts +4 -0
  40. package/src/providers/cli-provider-instance.ts +122 -13
  41. package/src/providers/contracts.d.ts +55 -0
  42. package/src/providers/contracts.ts +36 -1
  43. package/src/providers/native-history/dispatcher.ts +126 -17
  44. package/src/providers/provider-loader.ts +4 -7
  45. package/src/providers/provider-schema.ts +56 -1
  46. package/src/providers/sdk/v1/schemas/cli/provider.schema.json +46 -0
  47. package/src/providers/sdk/v1/types/common/index.ts +19 -0
  48. package/src/providers/spec/cli-adapter.ts +32 -5
  49. package/src/providers/spec/driver.ts +68 -1
  50. package/src/providers/spec/evaluator.ts +11 -1
  51. package/src/providers/spec/native-history-executor.ts +93 -27
  52. package/src/providers/spec/schema.gen.ts +12 -1
  53. package/src/providers/spec/schema.json +21 -1
  54. package/src/providers/spec/types.ts +10 -0
  55. package/src/repo-mesh-types.ts +6 -0
@@ -390,7 +390,7 @@ export interface ProviderMeshCoordinatorConfig {
390
390
  requiresRestart?: boolean;
391
391
  /** User-facing setup explanation for manual modes. */
392
392
  instructions?: string;
393
- /** Copyable setup template. Supports {{meshId}}, {{adhdevMcpCommand}}, {{workspace}}, {{serverName}}. */
393
+ /** Copyable setup template. Supports {{meshId}}, {{adhdevMcpCommand}}, {{adhdevMcpArgs}}, {{workspace}}, {{serverName}}. */
394
394
  template?: string;
395
395
  };
396
396
  /**
@@ -403,8 +403,43 @@ export interface ProviderMeshCoordinatorConfig {
403
403
  * the CLI doesn't recognize).
404
404
  */
405
405
  systemPromptInjection?: MeshCoordinatorSystemPromptInjection;
406
+ /**
407
+ * How coordinator-launched worker sessions are isolated from coordinator-only
408
+ * MCP/tools/config. Provider-specific CLI quirks belong here, not in daemon
409
+ * launch code.
410
+ */
411
+ delegatedWorkerIsolation?: MeshCoordinatorDelegatedWorkerIsolation;
406
412
  }
407
413
 
414
+ export interface MeshCoordinatorDelegatedWorkerIsolation {
415
+ /** Environment variables to unset for delegated worker sessions. */
416
+ env?: {
417
+ unset?: string[];
418
+ };
419
+ /** Spawn-argument rules applied before launching a delegated worker. */
420
+ args?: MeshCoordinatorDelegatedWorkerArgRule[];
421
+ }
422
+
423
+ export type MeshCoordinatorDelegatedWorkerArgRule =
424
+ | {
425
+ mode: 'empty_mcp_config';
426
+ /** CLI flag that points at an MCP config file, e.g. '--mcp-config'. */
427
+ flag: string;
428
+ /** Optional CLI flag that forces only the provided MCP config to be used. */
429
+ strictFlag?: string;
430
+ }
431
+ | {
432
+ mode: 'config_override';
433
+ /** CLI config flag, e.g. '-c' or '--config'. */
434
+ flag: string;
435
+ /** Config key to set for worker isolation. */
436
+ key: string;
437
+ /** Config value to set. */
438
+ value: string;
439
+ /** Optional broader key prefix used for duplicate detection. */
440
+ dedupeKey?: string;
441
+ };
442
+
408
443
  /**
409
444
  * Declarative description of how a CLI accepts a session-scoped system prompt.
410
445
  *
@@ -27,6 +27,7 @@ export interface NativeHistoryInput {
27
27
  providerSessionId?: string;
28
28
  historySessionId?: string;
29
29
  workspace?: string;
30
+ sessionStartedAtMs?: number;
30
31
  format?: string;
31
32
  watchPath?: string;
32
33
  forceRefresh?: boolean;
@@ -34,7 +35,7 @@ export interface NativeHistoryInput {
34
35
  }
35
36
 
36
37
  export interface NativeHistoryResult {
37
- messages: Array<{ role: string; content: string; receivedAt?: number; kind?: string }>;
38
+ messages: Array<{ role: string; content: string; receivedAt?: number; kind?: string; workspace?: string }>;
38
39
  providerSessionId?: string;
39
40
  sourcePath: string;
40
41
  sourceMtimeMs: number;
@@ -53,7 +54,12 @@ export function createNativeHistoryDispatcher(reader: ReaderId): (input: NativeH
53
54
  // shows up before I type anything" on every provider).
54
55
  const requestedProviderSid = input.providerSessionId || '';
55
56
 
56
- const sourcePath = resolveSourcePath(reader, workspace, sessionId);
57
+ const sessionStartedAtMs = typeof input.sessionStartedAtMs === 'number'
58
+ ? input.sessionStartedAtMs
59
+ : typeof input.args?.sessionStartedAtMs === 'number'
60
+ ? input.args.sessionStartedAtMs
61
+ : 0;
62
+ const sourcePath = resolveSourcePath(reader, workspace, sessionId, sessionStartedAtMs);
57
63
  if (!sourcePath) return null;
58
64
  if (input.forceRefresh === true || input.args?.forceRefresh === true) {
59
65
  try { fs.statSync(sourcePath); } catch { /* best-effort metadata refresh */ }
@@ -72,6 +78,7 @@ export function createNativeHistoryDispatcher(reader: ReaderId): (input: NativeH
72
78
  content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content),
73
79
  receivedAt: typeof m.receivedAt === 'number' ? m.receivedAt : Date.parse(m.timestamp || '') || Date.now(),
74
80
  kind: typeof m.kind === 'string' ? m.kind : 'standard',
81
+ workspace: typeof m.workspace === 'string' ? m.workspace : workspace || undefined,
75
82
  })),
76
83
  providerSessionId: session.providerSessionId,
77
84
  sourcePath: session.sourcePath,
@@ -85,10 +92,10 @@ export function createNativeHistoryDispatcher(reader: ReaderId): (input: NativeH
85
92
  // Per-provider path resolution
86
93
  // ────────────────────────────────────────────────────────────────────────────
87
94
 
88
- function resolveSourcePath(reader: ReaderId, workspace: string, sessionId: string): string | null {
95
+ function resolveSourcePath(reader: ReaderId, workspace: string, sessionId: string, sessionStartedAtMs: number): string | null {
89
96
  switch (reader) {
90
97
  case 'claude-cli': return resolveClaudePath(workspace, sessionId);
91
- case 'codex-cli': return resolveCodexPath(workspace);
98
+ case 'codex-cli': return resolveCodexPath(workspace, sessionId, sessionStartedAtMs);
92
99
  case 'antigravity-cli': return resolveAntigravityPath(workspace);
93
100
  case 'hermes-cli': return resolveHermesPath(workspace, sessionId);
94
101
  }
@@ -112,21 +119,111 @@ function resolveClaudePath(workspace: string, sessionId: string): string | null
112
119
  return null;
113
120
  }
114
121
 
115
- function resolveCodexPath(workspace: string): string | null {
116
- void workspace;
122
+ function resolveCodexPath(workspace: string, sessionId: string, sessionStartedAtMs: number): string | null {
117
123
  // codex stores by UTC date: ~/.codex/sessions/<year>/<month>/<day>/<file>.jsonl
118
- const now = new Date();
119
- const dir = path.join(
120
- os.homedir(), '.codex', 'sessions',
121
- String(now.getUTCFullYear()),
122
- String(now.getUTCMonth() + 1).padStart(2, '0'),
123
- String(now.getUTCDate()).padStart(2, '0'),
124
- );
125
- if (fs.existsSync(dir)) {
126
- const f = newestRecentFile(dir, /\.jsonl$/);
127
- if (f) return f;
124
+ const root = codexSessionsRoot();
125
+ if (sessionId && isUuidLikeSessionId(sessionId)) {
126
+ return findCodexPathBySessionId(root, sessionId);
128
127
  }
129
- return null;
128
+ return findCodexPathByRuntime(root, workspace, sessionStartedAtMs);
129
+ }
130
+
131
+ function findCodexPathBySessionId(root: string, sessionId: string): string | null {
132
+ if (!fs.existsSync(root)) return null;
133
+ const needle = sessionId.toLowerCase();
134
+ const matches: Array<{ p: string; mtime: number }> = [];
135
+ const stack: string[] = [root];
136
+ while (stack.length > 0) {
137
+ const current = stack.pop()!;
138
+ let entries: fs.Dirent[] = [];
139
+ try { entries = fs.readdirSync(current, { withFileTypes: true }); } catch { continue; }
140
+ for (const entry of entries) {
141
+ const entryPath = path.join(current, entry.name);
142
+ if (entry.isDirectory()) {
143
+ stack.push(entryPath);
144
+ continue;
145
+ }
146
+ if (!entry.isFile() || !entry.name.endsWith('.jsonl')) continue;
147
+ if (!entry.name.toLowerCase().includes(needle)) continue;
148
+ if (!isSafeFilename(entry.name.replace('.jsonl', ''))) continue;
149
+ matches.push({ p: entryPath, mtime: safeMtime(entryPath) });
150
+ }
151
+ }
152
+ matches.sort((a, b) => b.mtime - a.mtime);
153
+ return matches[0]?.p ?? null;
154
+ }
155
+
156
+ const CODEX_SPAWN_BIND_GRACE_MS = 10_000;
157
+
158
+ function findCodexPathByRuntime(root: string, workspace: string, sessionStartedAtMs: number): string | null {
159
+ if (!fs.existsSync(root) || !workspace) return null;
160
+ const workspaceResolved = resolveRealPath(workspace);
161
+ const cutoff = Date.now() - RECENT_WINDOW_MS;
162
+ const matches: Array<{ p: string; mtime: number; diff: number }> = [];
163
+ const stack: string[] = [root];
164
+
165
+ while (stack.length > 0) {
166
+ const current = stack.pop()!;
167
+ let entries: fs.Dirent[] = [];
168
+ try { entries = fs.readdirSync(current, { withFileTypes: true }); } catch { continue; }
169
+ for (const entry of entries) {
170
+ const entryPath = path.join(current, entry.name);
171
+ if (entry.isDirectory()) {
172
+ stack.push(entryPath);
173
+ continue;
174
+ }
175
+ if (!entry.isFile() || !entry.name.endsWith('.jsonl')) continue;
176
+ const mtime = safeMtime(entryPath);
177
+ if (mtime < cutoff) continue;
178
+ const meta = readCodexSessionMeta(entryPath);
179
+ if (!meta?.cwd || resolveRealPath(meta.cwd) !== workspaceResolved) continue;
180
+ const diff = sessionStartedAtMs > 0 && meta.timestampMs != null
181
+ ? Math.abs(meta.timestampMs - sessionStartedAtMs)
182
+ : 0;
183
+ if (sessionStartedAtMs > 0 && (meta.timestampMs == null || diff > CODEX_SPAWN_BIND_GRACE_MS)) continue;
184
+ matches.push({ p: entryPath, mtime, diff });
185
+ }
186
+ }
187
+
188
+ matches.sort((a, b) => sessionStartedAtMs > 0
189
+ ? a.diff - b.diff || b.mtime - a.mtime
190
+ : b.mtime - a.mtime);
191
+ return matches[0]?.p ?? null;
192
+ }
193
+
194
+ function readCodexSessionMeta(filePath: string): { cwd?: string; timestampMs?: number } | null {
195
+ try {
196
+ const fd = fs.openSync(filePath, 'r');
197
+ try {
198
+ const buffer = Buffer.alloc(8192);
199
+ const bytes = fs.readSync(fd, buffer, 0, buffer.length, 0);
200
+ if (bytes <= 0) return null;
201
+ const text = buffer.subarray(0, bytes).toString('utf8');
202
+ const firstLine = text.slice(0, text.indexOf('\n') >= 0 ? text.indexOf('\n') : text.length).trim();
203
+ if (!firstLine) return null;
204
+ const record = JSON.parse(firstLine) as Record<string, unknown>;
205
+ if (record.type !== 'session_meta' || !record.payload || typeof record.payload !== 'object') return null;
206
+ const payload = record.payload as Record<string, unknown>;
207
+ const timestampRaw = payload.timestamp;
208
+ const timestampMs = typeof timestampRaw === 'string'
209
+ ? Date.parse(timestampRaw)
210
+ : typeof timestampRaw === 'number'
211
+ ? (timestampRaw < 1e12 ? timestampRaw * 1000 : timestampRaw)
212
+ : NaN;
213
+ return {
214
+ cwd: typeof payload.cwd === 'string' ? payload.cwd : undefined,
215
+ timestampMs: Number.isFinite(timestampMs) ? timestampMs : undefined,
216
+ };
217
+ } finally {
218
+ fs.closeSync(fd);
219
+ }
220
+ } catch {
221
+ return null;
222
+ }
223
+ }
224
+
225
+ function resolveRealPath(value: string): string {
226
+ try { return fs.realpathSync(value); } catch { return value; }
130
227
  }
131
228
 
132
229
  function resolveAntigravityPath(workspace: string): string | null {
@@ -187,6 +284,18 @@ function cwdAsDashes(cwd: string): string {
187
284
  return cwd.replace(/\//g, '-');
188
285
  }
189
286
 
287
+ function codexSessionsRoot(): string {
288
+ return path.join(os.homedir(), '.codex', 'sessions');
289
+ }
290
+
291
+ function isUuidLikeSessionId(sessionId: string): boolean {
292
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(sessionId);
293
+ }
294
+
295
+ function isSafeFilename(name: string): boolean {
296
+ return /^[A-Za-z0-9._:-]+$/.test(name) && !name.includes('..');
297
+ }
298
+
190
299
  function newestFile(dir: string, pattern: RegExp): string | null {
191
300
  try {
192
301
  const entries = fs.readdirSync(dir, { withFileTypes: true })
@@ -36,6 +36,9 @@ import {
36
36
  } from './external-sources.js';
37
37
  import type { ProviderSourceMode } from '../config/config.js';
38
38
  import type { ProviderSourceConfigSnapshot, ProviderUserDirSource } from '../config/provider-source-config.js';
39
+ import { loadSpec } from './spec/loader.js';
40
+ import { executeNativeHistory } from './spec/native-history-executor.js';
41
+ import { createNativeHistoryDispatcher, type ReaderId } from './native-history/dispatcher.js';
39
42
 
40
43
  /**
41
44
  * Adds a provider-script root to the require whitelist. Wrapped in a
@@ -1244,8 +1247,6 @@ export class ProviderLoader {
1244
1247
  // Hand the resolved spec path off to route.ts via a hidden field
1245
1248
  // so the routing layer doesn't have to repeat the candidate walk.
1246
1249
  (resolved as any)._resolvedSpecPath = specPath;
1247
- // eslint-disable-next-line @typescript-eslint/no-var-requires
1248
- const { loadSpec } = require('./spec/loader.js');
1249
1250
  const r = loadSpec(specPath);
1250
1251
  // Stub each control_bar entry as a provider.scripts.<id>. The
1251
1252
  // upstream invoke_provider_script gate checks that the script
@@ -1274,8 +1275,6 @@ export class ProviderLoader {
1274
1275
  let format = 'spec';
1275
1276
 
1276
1277
  if (nh.source) {
1277
- // eslint-disable-next-line @typescript-eslint/no-var-requires
1278
- const { executeNativeHistory } = require('./spec/native-history-executor.js');
1279
1278
  format = `spec-${nh.source.kind}`;
1280
1279
  reader = (input: any) => executeNativeHistory(nh, input);
1281
1280
  } else if (nh.override_path) {
@@ -1294,9 +1293,7 @@ export class ProviderLoader {
1294
1293
  } catch { /* fall through — leave native unavailable */ }
1295
1294
  }
1296
1295
  } else if (nh.reader) {
1297
- // eslint-disable-next-line @typescript-eslint/no-var-requires
1298
- const { createNativeHistoryDispatcher } = require('./native-history/dispatcher.js');
1299
- const dispatch = createNativeHistoryDispatcher(nh.reader);
1296
+ const dispatch = createNativeHistoryDispatcher(nh.reader as ReaderId);
1300
1297
  format = nh.reader;
1301
1298
  reader = (input: any) => dispatch(input);
1302
1299
  }
@@ -300,7 +300,11 @@ function validateMeshCoordinator(raw: unknown, errors: string[]): void {
300
300
  errors.push('meshCoordinator.reason must be a non-empty string when provided')
301
301
  }
302
302
 
303
- const mcpConfig = meshCoordinator.mcpConfig
303
+ validateMeshCoordinatorMcpConfig(meshCoordinator.mcpConfig, errors)
304
+ validateMeshCoordinatorDelegatedWorkerIsolation(meshCoordinator.delegatedWorkerIsolation, errors)
305
+ }
306
+
307
+ function validateMeshCoordinatorMcpConfig(mcpConfig: unknown, errors: string[]): void {
304
308
  if (mcpConfig === undefined) return
305
309
  if (!mcpConfig || typeof mcpConfig !== 'object' || Array.isArray(mcpConfig)) {
306
310
  errors.push('meshCoordinator.mcpConfig must be an object')
@@ -348,6 +352,57 @@ function validateMeshCoordinator(raw: unknown, errors: string[]): void {
348
352
  }
349
353
  }
350
354
 
355
+ function validateMeshCoordinatorDelegatedWorkerIsolation(raw: unknown, errors: string[]): void {
356
+ if (raw === undefined) return
357
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
358
+ errors.push('meshCoordinator.delegatedWorkerIsolation must be an object')
359
+ return
360
+ }
361
+ const isolation = raw as Record<string, unknown>
362
+ const env = isolation.env
363
+ if (env !== undefined) {
364
+ if (!env || typeof env !== 'object' || Array.isArray(env)) {
365
+ errors.push('meshCoordinator.delegatedWorkerIsolation.env must be an object')
366
+ } else {
367
+ const unset = (env as Record<string, unknown>).unset
368
+ if (unset !== undefined && (!Array.isArray(unset) || unset.some((key) => typeof key !== 'string' || !key.trim()))) {
369
+ errors.push('meshCoordinator.delegatedWorkerIsolation.env.unset must be an array of non-empty strings')
370
+ }
371
+ }
372
+ }
373
+ const args = isolation.args
374
+ if (args === undefined) return
375
+ if (!Array.isArray(args)) {
376
+ errors.push('meshCoordinator.delegatedWorkerIsolation.args must be an array')
377
+ return
378
+ }
379
+ for (const [index, rule] of args.entries()) {
380
+ const prefix = `meshCoordinator.delegatedWorkerIsolation.args[${index}]`
381
+ if (!rule || typeof rule !== 'object' || Array.isArray(rule)) {
382
+ errors.push(`${prefix} must be an object`)
383
+ continue
384
+ }
385
+ const item = rule as Record<string, unknown>
386
+ const mode = item.mode
387
+ if (mode !== 'empty_mcp_config' && mode !== 'config_override') {
388
+ errors.push(`${prefix}.mode must be one of: empty_mcp_config, config_override`)
389
+ continue
390
+ }
391
+ for (const key of mode === 'empty_mcp_config' ? ['flag'] : ['flag', 'key', 'value']) {
392
+ const value = item[key]
393
+ if (typeof value !== 'string' || !value.trim()) {
394
+ errors.push(`${prefix}.${key} must be a non-empty string`)
395
+ }
396
+ }
397
+ for (const key of ['strictFlag', 'dedupeKey']) {
398
+ const value = item[key]
399
+ if (value !== undefined && (typeof value !== 'string' || !value.trim())) {
400
+ errors.push(`${prefix}.${key} must be a non-empty string when provided`)
401
+ }
402
+ }
403
+ }
404
+ }
405
+
351
406
  function validateControl(control: ProviderControlDef, errors: string[]): void {
352
407
  if (!control || typeof control !== 'object') {
353
408
  errors.push('controls: each control must be an object')
@@ -320,6 +320,52 @@
320
320
  { "type": "object", "additionalProperties": false, "required": ["mode", "name"],
321
321
  "properties": { "mode": { "const": "env_var" }, "name": { "type": "string" } } }
322
322
  ]
323
+ },
324
+ "delegatedWorkerIsolation": {
325
+ "description": "Provider-declared launch isolation for coordinator-spawned worker sessions. Keeps worker-only sessions from inheriting coordinator MCP/tools/config.",
326
+ "type": "object",
327
+ "additionalProperties": false,
328
+ "properties": {
329
+ "env": {
330
+ "type": "object",
331
+ "additionalProperties": false,
332
+ "properties": {
333
+ "unset": {
334
+ "type": "array",
335
+ "items": { "type": "string", "minLength": 1 }
336
+ }
337
+ }
338
+ },
339
+ "args": {
340
+ "type": "array",
341
+ "items": {
342
+ "oneOf": [
343
+ {
344
+ "type": "object",
345
+ "additionalProperties": false,
346
+ "required": ["mode", "flag"],
347
+ "properties": {
348
+ "mode": { "const": "empty_mcp_config" },
349
+ "flag": { "type": "string", "minLength": 1 },
350
+ "strictFlag": { "type": "string", "minLength": 1 }
351
+ }
352
+ },
353
+ {
354
+ "type": "object",
355
+ "additionalProperties": false,
356
+ "required": ["mode", "flag", "key", "value"],
357
+ "properties": {
358
+ "mode": { "const": "config_override" },
359
+ "flag": { "type": "string", "minLength": 1 },
360
+ "key": { "type": "string", "minLength": 1 },
361
+ "value": { "type": "string", "minLength": 1 },
362
+ "dedupeKey": { "type": "string", "minLength": 1 }
363
+ }
364
+ }
365
+ ]
366
+ }
367
+ }
368
+ }
323
369
  }
324
370
  }
325
371
  },
@@ -169,8 +169,27 @@ export interface McpConfigDef {
169
169
  export interface MeshCoordinatorDef {
170
170
  supported: boolean;
171
171
  mcpConfig?: McpConfigDef;
172
+ systemPromptInjection?: MeshCoordinatorSystemPromptInjectionDef;
173
+ delegatedWorkerIsolation?: MeshCoordinatorDelegatedWorkerIsolationDef;
172
174
  }
173
175
 
176
+ export type MeshCoordinatorSystemPromptInjectionDef =
177
+ | { mode: 'cli_arg'; flag: string }
178
+ | { mode: 'config_override'; flag: string; template: string }
179
+ | { mode: 'context_file'; path: string; wrapper?: string }
180
+ | { mode: 'env_var'; name: string };
181
+
182
+ export interface MeshCoordinatorDelegatedWorkerIsolationDef {
183
+ env?: {
184
+ unset?: ReadonlyArray<string>;
185
+ };
186
+ args?: ReadonlyArray<MeshCoordinatorDelegatedWorkerArgRuleDef>;
187
+ }
188
+
189
+ export type MeshCoordinatorDelegatedWorkerArgRuleDef =
190
+ | { mode: 'empty_mcp_config'; flag: string; strictFlag?: string }
191
+ | { mode: 'config_override'; flag: string; key: string; value: string; dedupeKey?: string };
192
+
174
193
  // ─── Compatibility ──────────────────────────────────────────────────────
175
194
 
176
195
  export interface CompatibilityEntryDef {
@@ -36,6 +36,14 @@ import {
36
36
  type InteractivePromptResponse,
37
37
  } from '../types/interactive-prompt.js';
38
38
 
39
+ function stripAnsi(text: string): string {
40
+ // eslint-disable-next-line no-control-regex
41
+ return String(text || '')
42
+ .replace(/\x1B\][^\x07]*(?:\x07|\x1B\\)/g, '')
43
+ .replace(/\x1B[P^_X][\s\S]*?(?:\x07|\x1B\\)/g, '')
44
+ .replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, '');
45
+ }
46
+
39
47
  export class SpecCliAdapter implements CliAdapter {
40
48
  readonly cliType: string;
41
49
  readonly cliName: string;
@@ -129,15 +137,16 @@ export class SpecCliAdapter implements CliAdapter {
129
137
  }
130
138
 
131
139
  getStatus(): CliAdapterStatus {
132
- if (this.exited) return { status: 'stopped', messages: [], activeModal: null, activeInteractivePrompt: this.activeInteractivePrompt };
133
- if (!this.spawned) return { status: 'starting', messages: [], activeModal: null, activeInteractivePrompt: this.activeInteractivePrompt };
140
+ const sessionFields = this.providerSessionId ? { providerSessionId: this.providerSessionId } : {};
141
+ if (this.exited) return { status: 'stopped', messages: [], activeModal: null, activeInteractivePrompt: this.activeInteractivePrompt, ...sessionFields };
142
+ if (!this.spawned) return { status: 'starting', messages: [], activeModal: null, activeInteractivePrompt: this.activeInteractivePrompt, ...sessionFields };
134
143
 
135
144
  // Refresh native history lazily — the watch_path is cheap to stat,
136
145
  // but parsing a full session.jsonl every call would be wasteful.
137
146
  this.maybeRefreshNativeHistory();
138
147
 
139
148
  const state = this.latestState;
140
- if (!state) return { status: 'starting', messages: [], activeModal: null, activeInteractivePrompt: this.activeInteractivePrompt };
149
+ if (!state) return { status: 'starting', messages: [], activeModal: null, activeInteractivePrompt: this.activeInteractivePrompt, ...sessionFields };
141
150
 
142
151
  const modal = this.latestModal;
143
152
  const lc = state.id.toLowerCase();
@@ -150,12 +159,13 @@ export class SpecCliAdapter implements CliAdapter {
150
159
  buttons: modal.buttons.map(b => b.label),
151
160
  },
152
161
  activeInteractivePrompt: this.activeInteractivePrompt,
162
+ ...sessionFields,
153
163
  };
154
164
  }
155
165
  if (lc === 'busy' || lc === 'generating') {
156
- return { status: 'generating', messages: [], activeModal: null, activeInteractivePrompt: this.activeInteractivePrompt };
166
+ return { status: 'generating', messages: [], activeModal: null, activeInteractivePrompt: this.activeInteractivePrompt, ...sessionFields };
157
167
  }
158
- return { status: 'idle', messages: [], activeModal: null, activeInteractivePrompt: this.activeInteractivePrompt };
168
+ return { status: 'idle', messages: [], activeModal: null, activeInteractivePrompt: this.activeInteractivePrompt, ...sessionFields };
159
169
  }
160
170
 
161
171
  private maybeRefreshNativeHistory(): void {
@@ -165,10 +175,13 @@ export class SpecCliAdapter implements CliAdapter {
165
175
  }
166
176
 
167
177
  getScriptParsedStatus(): unknown {
178
+ const providerSessionId = this.extractProviderSessionIdFromScreen();
179
+ if (providerSessionId) this.providerSessionId = providerSessionId;
168
180
  const status = this.getStatus();
169
181
  return {
170
182
  ...status,
171
183
  messages: this.readClaudeScreenAssistantMessages(),
184
+ ...(this.providerSessionId ? { providerSessionId: this.providerSessionId } : {}),
172
185
  };
173
186
  }
174
187
 
@@ -345,6 +358,7 @@ export class SpecCliAdapter implements CliAdapter {
345
358
  displayName: this.spec.name,
346
359
  spawnedAtMs: this.spawnedAtMs,
347
360
  spawnedEnv: this.spawnedEnv,
361
+ ...(this.providerSessionId ? { providerSessionId: this.providerSessionId } : {}),
348
362
  };
349
363
  }
350
364
  updateRuntimeMeta(meta?: Record<string, unknown>): void {
@@ -419,6 +433,19 @@ export class SpecCliAdapter implements CliAdapter {
419
433
  }
420
434
  }
421
435
 
436
+ private extractProviderSessionIdFromScreen(): string | undefined {
437
+ if (this.cliType !== 'codex-cli') return this.providerSessionId;
438
+ let screenText = '';
439
+ try {
440
+ screenText = this.driver.snapshot();
441
+ } catch {
442
+ return this.providerSessionId;
443
+ }
444
+ const clean = stripAnsi(screenText);
445
+ const match = clean.match(/(?:gpt-|o\d|codex-)[^\n·]*·[^\n·]*·\s*([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i);
446
+ return match?.[1] || this.providerSessionId;
447
+ }
448
+
422
449
  private readClaudeScreenAssistantMessages(): ChatMessage[] {
423
450
  if (this.cliType !== 'claude-cli') return [];
424
451
  let screenText = '';
@@ -132,6 +132,37 @@ export function resolveSubmitDelayMs(specBeforeSubmit: number | undefined, text:
132
132
  return Math.max(spec, SUBMIT_DELAY_FLOOR_MS + linesBonus);
133
133
  }
134
134
 
135
+ export function matchesCompletionIdleRule(spec: CliSpec, ev: SpecEvaluation, screen: string): string | null {
136
+ const rule = spec.debounce?.completion_idle_after;
137
+ if (!rule?.regex) return null;
138
+ const haystack = rule.section
139
+ ? ev.sections.find(section => section.id === rule.section)?.text ?? ''
140
+ : screen;
141
+ if (!haystack) return null;
142
+ try {
143
+ const regex = new RegExp(rule.regex, rule.flags || '');
144
+ const match = haystack.match(regex);
145
+ return match?.[0] || null;
146
+ } catch {
147
+ return null;
148
+ }
149
+ }
150
+
151
+ export function matchesCompletionIdleTargetState(spec: CliSpec, ev: SpecEvaluation, screen: string): boolean {
152
+ const target = spec.states.find(state => state.id === spec.default_state)
153
+ ?? spec.states.find(state => state.id === 'idle');
154
+ if (!target?.when?.regex) return false;
155
+ const haystack = target.when.section
156
+ ? ev.sections.find(section => section.id === target.when.section)?.text ?? ''
157
+ : screen;
158
+ if (!haystack) return false;
159
+ try {
160
+ return new RegExp(target.when.regex, target.when.flags || 'i').test(haystack);
161
+ } catch {
162
+ return false;
163
+ }
164
+ }
165
+
135
166
  export class SpecDriver {
136
167
  private spec!: CliSpec;
137
168
  private adapter!: TerminalAdapter;
@@ -157,6 +188,8 @@ export class SpecDriver {
157
188
  * because the evaluator already moved past busy by the time the hold
158
189
  * kicks in. */
159
190
  private lastBusyState: SpecEvaluation['state'] | null = null;
191
+ private completionIdleFirstSeenAt = 0;
192
+ private completionIdleKey = '';
160
193
  /** Timer that re-runs evaluate() once the hold window expires. Needed
161
194
  * because the PTY stops emitting once the agent finishes; without an
162
195
  * explicit wake-up there's nothing to trigger the busy → idle
@@ -305,6 +338,40 @@ export class SpecDriver {
305
338
  evState = this.lastBusyState ?? evState;
306
339
  }
307
340
  }
341
+ const completionIdleRule = this.spec.debounce?.completion_idle_after;
342
+ let busyWakeMs = busyHoldMs;
343
+ if (evState.id === 'busy' && completionIdleRule) {
344
+ const completionKey = matchesCompletionIdleRule(this.spec, ev, screen);
345
+ if (completionKey) {
346
+ const now = Date.now();
347
+ if (completionKey !== this.completionIdleKey) {
348
+ this.completionIdleKey = completionKey;
349
+ this.completionIdleFirstSeenAt = now;
350
+ }
351
+ const holdMs = Math.max(0, completionIdleRule.hold_ms || 0);
352
+ const ageMs = now - this.completionIdleFirstSeenAt;
353
+ if (ageMs >= holdMs) {
354
+ if (matchesCompletionIdleTargetState(this.spec, ev, screen)) {
355
+ const idle = this.spec.states.find(state => state.id === this.spec.default_state)
356
+ ?? this.spec.states.find(state => state.id === 'idle');
357
+ evState = idle
358
+ ? { id: idle.id, label: idle.label, title: null }
359
+ : { id: 'idle', label: 'Ready', title: null };
360
+ } else {
361
+ busyWakeMs = Math.min(busyWakeMs, 1000);
362
+ }
363
+ } else {
364
+ busyWakeMs = Math.min(busyWakeMs, Math.max(holdMs - ageMs, 0));
365
+ }
366
+ } else {
367
+ this.completionIdleKey = '';
368
+ this.completionIdleFirstSeenAt = 0;
369
+ }
370
+ } else if (evState.id !== 'busy') {
371
+ this.completionIdleKey = '';
372
+ this.completionIdleFirstSeenAt = 0;
373
+ }
374
+
308
375
  if (evState.id === 'busy') {
309
376
  this.lastBusyAt = Date.now();
310
377
  this.lastBusyState = evState;
@@ -313,7 +380,7 @@ export class SpecDriver {
313
380
  // footer settles), so without an explicit timer the driver
314
381
  // never wakes up to downshift to idle and the dashboard sees
315
382
  // status stuck at generating long after the turn ended.
316
- this.scheduleBusyExpiry(busyHoldMs);
383
+ this.scheduleBusyExpiry(busyWakeMs);
317
384
  }
318
385
 
319
386
  const changed = forceEmit
@@ -231,8 +231,18 @@ export function evaluate(spec: CliSpec, screenText: string): SpecEvaluation {
231
231
  for (const st of spec.states) {
232
232
  const { matched, title } = matchState(st, sections, screenText, trace);
233
233
  if (!matched) continue;
234
+ const extractedModal = extractModal(st, sections, screenText, title, trace);
235
+ // If the state declares modal_buttons but extraction failed (button
236
+ // count below min_count, or text-was-mistaken-for-modal), do not
237
+ // promote the state. Otherwise we would surface a phantom approval
238
+ // built from arbitrary screen text — see claude-cli numbered-list
239
+ // false-positive on 2026-06-07.
240
+ if (st.modal_buttons && !extractedModal) {
241
+ trace.push({ kind: 'state_skip', text: `state[${st.id}] matched but modal_buttons extraction failed — not promoting` });
242
+ continue;
243
+ }
234
244
  activeState = { id: st.id, label: st.label, title };
235
- modal = extractModal(st, sections, screenText, title, trace);
245
+ modal = extractedModal;
236
246
  break;
237
247
  }
238
248