@adhdev/daemon-core 0.9.82-rc.187 → 0.9.82-rc.189

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 (42) 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/router.d.ts +5 -1
  4. package/dist/git/git-commands.d.ts +2 -0
  5. package/dist/git/git-types.d.ts +2 -0
  6. package/dist/index.d.ts +1 -1
  7. package/dist/index.js +459 -38
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +458 -38
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/providers/cli-provider-instance.d.ts +4 -0
  12. package/dist/providers/contracts.d.ts +31 -0
  13. package/dist/providers/sdk/v1/types/common/index.d.ts +35 -1
  14. package/dist/providers/spec/adapter.d.ts +4 -0
  15. package/dist/providers/spec/driver.d.ts +10 -1
  16. package/dist/providers/spec/evaluator.d.ts +9 -1
  17. package/dist/providers/spec/schema.gen.d.ts +38 -0
  18. package/dist/providers/spec/types.d.ts +25 -0
  19. package/dist/repo-mesh-types.d.ts +6 -0
  20. package/package.json +1 -1
  21. package/src/boot/daemon-lifecycle.ts +2 -0
  22. package/src/commands/chat-commands.ts +26 -0
  23. package/src/commands/cli-manager.ts +52 -14
  24. package/src/commands/router.ts +35 -4
  25. package/src/git/git-commands.ts +20 -2
  26. package/src/git/git-status.ts +35 -6
  27. package/src/git/git-types.ts +2 -0
  28. package/src/index.ts +1 -1
  29. package/src/mesh/mesh-events.ts +7 -0
  30. package/src/providers/cli-provider-instance.ts +110 -9
  31. package/src/providers/contracts.d.ts +55 -0
  32. package/src/providers/contracts.ts +35 -0
  33. package/src/providers/provider-schema.ts +56 -1
  34. package/src/providers/sdk/v1/schemas/cli/provider.schema.json +46 -0
  35. package/src/providers/sdk/v1/types/common/index.ts +19 -0
  36. package/src/providers/spec/adapter.ts +8 -0
  37. package/src/providers/spec/driver.ts +74 -2
  38. package/src/providers/spec/evaluator.ts +39 -3
  39. package/src/providers/spec/schema.gen.ts +28 -1
  40. package/src/providers/spec/schema.json +26 -2
  41. package/src/providers/spec/types.ts +25 -0
  42. package/src/repo-mesh-types.ts +6 -0
@@ -48,6 +48,11 @@ export async function getGitRepoStatus(
48
48
  if (includeSubmodules) {
49
49
  submodules = await getSubmoduleStatuses(repo, options);
50
50
  }
51
+ const submoduleDirty = (submodules || []).some(submodule => submodule.dirty || submodule.outOfSync || !!submodule.error);
52
+ const dirty = parsed.staged + parsed.modified + parsed.untracked + parsed.deleted + parsed.renamed > 0
53
+ || parsed.conflictFiles.length > 0
54
+ || stashCount > 0
55
+ || submoduleDirty;
51
56
 
52
57
  return {
53
58
  workspace: repo.workspace,
@@ -67,6 +72,7 @@ export async function getGitRepoStatus(
67
72
  untracked: parsed.untracked,
68
73
  deleted: parsed.deleted,
69
74
  renamed: parsed.renamed,
75
+ dirty,
70
76
  hasConflicts: parsed.conflictFiles.length > 0,
71
77
  conflictFiles: parsed.conflictFiles,
72
78
  stashCount,
@@ -285,6 +291,7 @@ function emptyStatus(workspace: string, lastCheckedAt: number, error: GitCommand
285
291
  untracked: 0,
286
292
  deleted: 0,
287
293
  renamed: 0,
294
+ dirty: false,
288
295
  hasConflicts: false,
289
296
  conflictFiles: [],
290
297
  stashCount: 0,
@@ -304,12 +311,34 @@ async function getSubmoduleStatuses(
304
311
 
305
312
  try {
306
313
  const result = await runGit(repo, ['submodule', 'status', '--recursive'], options);
307
- return parseSubmoduleStatusOutput(result.stdout, repo.repoRoot, options.submoduleIgnorePaths);
314
+ const submodules = parseSubmoduleStatusOutput(result.stdout, repo.repoRoot, options.submoduleIgnorePaths);
315
+ await Promise.all(submodules.map(submodule => enrichSubmoduleWorktreeStatus(repo, submodule, options)));
316
+ return submodules;
308
317
  } catch {
309
318
  return [];
310
319
  }
311
320
  }
312
321
 
322
+ async function enrichSubmoduleWorktreeStatus(
323
+ repo: ResolvedGitRepo,
324
+ submodule: GitSubmoduleStatus,
325
+ options: GitStatusOptions,
326
+ ): Promise<void> {
327
+ try {
328
+ const result = await runGit(repo, ['status', '--porcelain=v2', '--branch'], {
329
+ ...options,
330
+ cwd: submodule.repoPath,
331
+ });
332
+ const parsed = parsePorcelainV2Status(result.stdout);
333
+ const dirty = parsed.staged + parsed.modified + parsed.untracked + parsed.deleted + parsed.renamed > 0
334
+ || parsed.conflictFiles.length > 0;
335
+ submodule.dirty = submodule.dirty || dirty;
336
+ } catch (error) {
337
+ submodule.dirty = true;
338
+ submodule.error = formatGitError(error);
339
+ }
340
+ }
341
+
313
342
  function parseSubmoduleStatusOutput(
314
343
  output: string,
315
344
  repoRoot: string,
@@ -321,9 +350,9 @@ function parseSubmoduleStatusOutput(
321
350
  for (const line of output.split('\n')) {
322
351
  if (!line.trim()) continue;
323
352
 
324
- // Format: [+- ]<commit> <path> (<branch>)
325
- // - = out of sync, + = dirty, ' ' = clean
326
- const match = line.match(/^([\-+\s])([0-9a-f]{40})\s+(\S+)(?:\s+\(([^)]+)\))?/);
353
+ // Format: [+-U ]<commit> <path> (<branch>)
354
+ // - = not initialized, + = gitlink out of sync, U = conflict, ' ' = aligned.
355
+ const match = line.match(/^([\-+U\s])([0-9a-f]{40})\s+(\S+)(?:\s+\(([^)]+)\))?/);
327
356
  if (!match) continue;
328
357
 
329
358
  const prefix = match[1];
@@ -336,8 +365,8 @@ function parseSubmoduleStatusOutput(
336
365
  path,
337
366
  commit,
338
367
  repoPath: repoRoot + '/' + path,
339
- dirty: prefix === '+',
340
- outOfSync: prefix === '-',
368
+ dirty: prefix === 'U',
369
+ outOfSync: prefix === '-' || prefix === '+',
341
370
  lastCheckedAt: Date.now(),
342
371
  });
343
372
  }
@@ -60,6 +60,8 @@ export interface GitRepoStatus extends GitRepoIdentity {
60
60
  untracked: number;
61
61
  deleted: number;
62
62
  renamed: number;
63
+ /** Aggregate dirty flag including root worktree changes, conflicts, stash, and submodule drift. */
64
+ dirty: boolean;
63
65
  hasConflicts: boolean;
64
66
  conflictFiles: string[];
65
67
  stashCount: number;
package/src/index.ts CHANGED
@@ -275,7 +275,7 @@ export type { CdpInitializerConfig } from './cdp/initializer.js';
275
275
  // ── Commands ──
276
276
  export { DaemonCommandHandler } from './commands/handler.js';
277
277
  export type { CommandResult, CommandContext } from './commands/handler.js';
278
- export { DaemonCommandRouter } from './commands/router.js';
278
+ export { DaemonCommandRouter, readCachedInlineMeshActiveSessionDetails } from './commands/router.js';
279
279
  export type { CommandRouterDeps, CommandRouterResult } from './commands/router.js';
280
280
  export {
281
281
  maybeRunDaemonUpgradeHelperFromEnv,
@@ -1962,6 +1962,11 @@ export function handleMeshForwardEvent(components: DaemonComponents, payload: Re
1962
1962
  const nodeId = readNonEmptyString(payload.nodeId);
1963
1963
  const workspace = readNonEmptyString(payload.workspace);
1964
1964
  const nodeLabel = nodeId ? `Node '${nodeId}'` : workspace ? `Agent at ${workspace}` : 'Remote agent';
1965
+ const relayModalMessage = readNonEmptyString(payload.modalMessage);
1966
+ const relayModalButtons = Array.isArray(payload.modalButtons)
1967
+ ? (payload.modalButtons as unknown[]).filter((b): b is string => typeof b === 'string' && b.trim().length > 0)
1968
+ : null;
1969
+
1965
1970
  return injectMeshSystemMessage(components, {
1966
1971
  meshId,
1967
1972
  nodeId,
@@ -1979,6 +1984,8 @@ export function handleMeshForwardEvent(components: DaemonComponents, payload: Re
1979
1984
  startedAt: readNonEmptyString(payload.startedAt),
1980
1985
  completedAt: readNonEmptyString(payload.completedAt),
1981
1986
  retryOfJobId: readNonEmptyString(payload.retryOfJobId),
1987
+ ...(relayModalMessage ? { modalMessage: relayModalMessage } : {}),
1988
+ ...(relayModalButtons && relayModalButtons.length > 0 ? { modalButtons: relayModalButtons } : {}),
1982
1989
  ...(payload.result && typeof payload.result === 'object' && !Array.isArray(payload.result) ? { result: payload.result } : {}),
1983
1990
  ...(payload.completionDiagnostic && typeof payload.completionDiagnostic === 'object' && !Array.isArray(payload.completionDiagnostic) ? { completionDiagnostic: payload.completionDiagnostic } : {}),
1984
1991
  ...(payload.workerResult && typeof payload.workerResult === 'object' && !Array.isArray(payload.workerResult) ? { workerResult: payload.workerResult } : {}),
@@ -73,6 +73,12 @@ type CompletionFinalAssistantEvidence = {
73
73
  source: 'parsed' | 'external-native' | 'unavailable';
74
74
  };
75
75
 
76
+ type ExternalNativeFinalReconciliation = {
77
+ fingerprint: string;
78
+ finalSummary: string;
79
+ evidence: CompletionFinalAssistantEvidence;
80
+ };
81
+
76
82
  type ExternalTranscriptProbe = {
77
83
  readAt: number;
78
84
  msgCount: number;
@@ -369,6 +375,8 @@ export class CliProviderInstance implements ProviderInstance {
369
375
  private historyWriter: ChatHistoryWriter;
370
376
  private runtimeMessages: Array<{ key: string; message: ChatMessage }> = [];
371
377
  private lastPersistedHistoryMessages: PersistableCliHistoryMessage[] = [];
378
+ private lastAcknowledgedUserInputAt = 0;
379
+ private externalBusyIdleFingerprint = '';
372
380
  private lastNativeSourceCanonicalCheckAt = 0;
373
381
  private lastNativeSourceCanonicalCacheKey: string | undefined = undefined;
374
382
  private cachedSqliteDb: {
@@ -587,9 +595,13 @@ export class CliProviderInstance implements ProviderInstance {
587
595
  typeof adapterStatus?.providerSessionId === 'string' ? adapterStatus.providerSessionId : '',
588
596
  );
589
597
  const autoApproveActive = this.maybeAutoApproveStatus(adapterStatus, Date.now());
590
- const visibleStatus = parseErrorMessage || parsedStatus?.status === 'error'
598
+ let visibleStatus = parseErrorMessage || parsedStatus?.status === 'error'
591
599
  ? 'error'
592
600
  : (autoApproveActive ? 'generating' : adapterStatus.status);
601
+ const externalNativeFinal = this.getExternalNativeFinalReconciliation(parsedStatus?.messages, adapterStatus);
602
+ if (externalNativeFinal && isCliGeneratingLikeStatus(visibleStatus)) {
603
+ visibleStatus = 'idle';
604
+ }
593
605
  const runtime = this.adapter.getRuntimeMetadata();
594
606
  this.maybeAppendRuntimeRecoveryMessage(runtime);
595
607
  let parsedMessages = Array.isArray(parsedStatus?.messages)
@@ -788,7 +800,22 @@ export class CliProviderInstance implements ProviderInstance {
788
800
  }
789
801
 
790
802
  updateSettings(newSettings: Record<string, any>): void {
791
- this.settings = { ...newSettings };
803
+ const runtimeMeshSettings: Record<string, any> = {};
804
+ for (const key of [
805
+ 'meshNodeFor',
806
+ 'meshNodeId',
807
+ 'meshActiveTaskId',
808
+ 'meshCoordinatorFor',
809
+ 'meshCoordinatorDaemonId',
810
+ 'meshCoordinatorNodeId',
811
+ 'spawnedSessionVisibility',
812
+ 'launchedByCoordinator',
813
+ ]) {
814
+ if (this.settings[key] !== undefined && newSettings[key] === undefined) {
815
+ runtimeMeshSettings[key] = this.settings[key];
816
+ }
817
+ }
818
+ this.settings = { ...newSettings, ...runtimeMeshSettings };
792
819
  this.adapter.updateRuntimeSettings?.(this.settings);
793
820
  this.monitor.updateConfig({
794
821
  approvalAlert: this.settings.approvalAlert !== false,
@@ -884,6 +911,8 @@ export class CliProviderInstance implements ProviderInstance {
884
911
  if (!content) return;
885
912
 
886
913
  const receivedAt = Date.now();
914
+ this.lastAcknowledgedUserInputAt = receivedAt;
915
+ this.externalBusyIdleFingerprint = '';
887
916
  const dedupKey = `user_input_ack:${crypto
888
917
  .createHash('sha256')
889
918
  .update(`${this.instanceId}:${content}:${receivedAt}`)
@@ -1049,6 +1078,59 @@ export class CliProviderInstance implements ProviderInstance {
1049
1078
  return extractFinalSummaryFromMessages(evidence.messages as any);
1050
1079
  }
1051
1080
 
1081
+ private externalNativeFinalFingerprint(evidence: CompletionFinalAssistantEvidence): string {
1082
+ const messages = Array.isArray(evidence.messages) ? evidence.messages : [];
1083
+ const visibleMessages = messages.filter((message: any) => isUserFacingChatMessage(message as ChatMessage));
1084
+ const lastVisible = visibleMessages[visibleMessages.length - 1] as ChatMessage | undefined;
1085
+ const content = lastVisible ? flattenContent(lastVisible.content).trim() : '';
1086
+ const receivedAt = lastVisible ? getMessageTime(lastVisible) : 0;
1087
+ const probe = this.lastExternalCompletionProbe;
1088
+ return crypto
1089
+ .createHash('sha256')
1090
+ .update([
1091
+ this.type,
1092
+ this.providerSessionId || '',
1093
+ probe?.sourcePath || '',
1094
+ String(probe?.sourceMtimeMs || 0),
1095
+ String(receivedAt || 0),
1096
+ content.slice(-500),
1097
+ ].join('\0'))
1098
+ .digest('hex')
1099
+ .slice(0, 24);
1100
+ }
1101
+
1102
+ private getExternalNativeFinalReconciliation(parsedMessages: unknown, adapterStatus: any): ExternalNativeFinalReconciliation | null {
1103
+ const rawStatus = typeof adapterStatus?.status === 'string' ? adapterStatus.status.trim() : '';
1104
+ if (!isCliGeneratingLikeStatus(rawStatus)) return null;
1105
+ if (hasNonEmptyCliModalButtons(adapterStatus?.activeModal ?? adapterStatus?.modal)) return null;
1106
+
1107
+ const evidence = this.completionFinalAssistantEvidence(parsedMessages);
1108
+ if (evidence.source !== 'external-native' || !evidence.present) return null;
1109
+
1110
+ const messages = Array.isArray(evidence.messages) ? evidence.messages : [];
1111
+ const visibleMessages = messages.filter((message: any) => isUserFacingChatMessage(message as ChatMessage));
1112
+ const lastVisible = visibleMessages[visibleMessages.length - 1] as ChatMessage | undefined;
1113
+ const lastMessageAt = lastVisible ? getMessageTime(lastVisible) : 0;
1114
+ const sourceMtimeMs = Number(this.lastExternalCompletionProbe?.sourceMtimeMs || 0);
1115
+ const minEvidenceAt = Math.max(
1116
+ this.startedAt > 0 ? this.startedAt - 5_000 : 0,
1117
+ this.generatingStartedAt > 0 ? this.generatingStartedAt - 5_000 : 0,
1118
+ this.lastAcknowledgedUserInputAt > 0 ? this.lastAcknowledgedUserInputAt - 1_000 : 0,
1119
+ );
1120
+ if (minEvidenceAt > 0 && lastMessageAt > 0 && lastMessageAt < minEvidenceAt && sourceMtimeMs < minEvidenceAt) {
1121
+ return null;
1122
+ }
1123
+
1124
+ const finalSummary = extractFinalSummaryFromMessages(evidence.messages as any);
1125
+ if (!finalSummary) return null;
1126
+ const fingerprint = this.externalNativeFinalFingerprint(evidence);
1127
+ if (fingerprint === this.externalBusyIdleFingerprint) {
1128
+ return { fingerprint, finalSummary, evidence };
1129
+ }
1130
+ this.externalBusyIdleFingerprint = fingerprint;
1131
+ return { fingerprint, finalSummary, evidence };
1132
+ }
1133
+
1052
1134
  private buildCompletedFinalizationDiagnostic(args: {
1053
1135
  blockReason: string;
1054
1136
  latestStatus?: any;
@@ -1138,12 +1220,13 @@ export class CliProviderInstance implements ProviderInstance {
1138
1220
  return true;
1139
1221
  }
1140
1222
 
1141
- private getCompletedFinalizationBlock(latestVisibleStatus: string, pending: CompletedDebouncePending): CompletedFinalizationBlock | null {
1223
+ private getCompletedFinalizationBlock(latestVisibleStatus: string, pending: CompletedDebouncePending, opts?: { externalNativeFinal?: ExternalNativeFinalReconciliation | null }): CompletedFinalizationBlock | null {
1142
1224
  if (latestVisibleStatus !== 'idle') return { reason: `status:${latestVisibleStatus}`, terminal: true };
1143
1225
 
1144
1226
  const adapterAny = this.adapter as any;
1145
1227
  const approvalResolvedIdle = pending.previousStatus === 'waiting_approval';
1146
- if (!approvalResolvedIdle) {
1228
+ const externalNativeFinal = opts?.externalNativeFinal || null;
1229
+ if (!approvalResolvedIdle && !externalNativeFinal) {
1147
1230
  if (adapterAny?.isWaitingForResponse === true) return { reason: 'adapter_waiting_for_response', terminal: true };
1148
1231
  if (adapterAny?.currentTurnScope) return { reason: 'adapter_turn_scope_active', terminal: true };
1149
1232
  if (this.hasAdapterPendingResponse()) return { reason: 'adapter_pending_response', terminal: true };
@@ -1152,7 +1235,7 @@ export class CliProviderInstance implements ProviderInstance {
1152
1235
  const partial = typeof this.adapter.getPartialResponse === 'function'
1153
1236
  ? this.adapter.getPartialResponse()
1154
1237
  : '';
1155
- if (typeof partial === 'string' && partial.trim()) return { reason: 'partial_response_pending', terminal: true };
1238
+ if (!externalNativeFinal && typeof partial === 'string' && partial.trim()) return { reason: 'partial_response_pending', terminal: true };
1156
1239
 
1157
1240
  let parsed: any;
1158
1241
  try {
@@ -1164,6 +1247,7 @@ export class CliProviderInstance implements ProviderInstance {
1164
1247
  const parsedStatus = typeof parsed?.status === 'string' ? parsed.status : 'unknown';
1165
1248
  if (parsedStatus !== 'idle') {
1166
1249
  const adapterStatus = this.adapter.getStatus({ allowParse: false });
1250
+ if (externalNativeFinal && isCliGeneratingLikeStatus(parsedStatus)) return null;
1167
1251
  if (this.shouldSuppressStaleParsedBusyStatus(parsed, adapterStatus)) return null;
1168
1252
  return { reason: `parsed_status:${parsedStatus}`, terminal: isCliGeneratingLikeStatus(parsedStatus) };
1169
1253
  }
@@ -1230,7 +1314,10 @@ export class CliProviderInstance implements ProviderInstance {
1230
1314
 
1231
1315
  const latestStatus = this.adapter.getStatus({ allowParse: false });
1232
1316
  const latestAutoApproveActive = latestStatus.status === 'waiting_approval' && this.shouldAutoApprove();
1233
- const latestVisibleStatus = latestAutoApproveActive ? 'generating' : latestStatus.status;
1317
+ const externalNativeFinal = this.getExternalNativeFinalReconciliation(undefined, latestStatus);
1318
+ const latestVisibleStatus = externalNativeFinal && isCliGeneratingLikeStatus(latestStatus.status)
1319
+ ? 'idle'
1320
+ : (latestAutoApproveActive ? 'generating' : latestStatus.status);
1234
1321
  if (latestVisibleStatus !== 'idle') {
1235
1322
  LOG.info('CLI', `[${this.type}] cancelled pending completed (resumed ${latestVisibleStatus})`);
1236
1323
  this.completedDebouncePending = null;
@@ -1238,7 +1325,7 @@ export class CliProviderInstance implements ProviderInstance {
1238
1325
  return;
1239
1326
  }
1240
1327
 
1241
- const block = this.getCompletedFinalizationBlock(latestVisibleStatus, pending);
1328
+ const block = this.getCompletedFinalizationBlock(latestVisibleStatus, pending, { externalNativeFinal });
1242
1329
  if (block) {
1243
1330
  const blockReason = block.reason;
1244
1331
  const waitedMs = Date.now() - pending.firstObservedAt;
@@ -1282,7 +1369,18 @@ export class CliProviderInstance implements ProviderInstance {
1282
1369
  chatTitle: pending.chatTitle,
1283
1370
  duration: pending.duration,
1284
1371
  timestamp: pending.timestamp,
1285
- finalSummary: this.completionFinalSummary(this.adapter?.getScriptParsedStatus()?.messages),
1372
+ finalSummary: externalNativeFinal?.finalSummary || this.completionFinalSummary(this.adapter?.getScriptParsedStatus()?.messages),
1373
+ ...(externalNativeFinal ? {
1374
+ completionDiagnostic: {
1375
+ providerType: this.type,
1376
+ sessionId: this.instanceId,
1377
+ providerSessionId: this.providerSessionId || null,
1378
+ reconciliationReason: 'external_native_final_assistant_while_adapter_busy',
1379
+ finalAssistantPresent: true,
1380
+ finalAssistantEvidenceSource: externalNativeFinal.evidence.source,
1381
+ externalFinalFingerprint: externalNativeFinal.fingerprint,
1382
+ },
1383
+ } : {}),
1286
1384
  });
1287
1385
  this.completedDebouncePending = null;
1288
1386
  this.completedDebounceTimer = null;
@@ -1359,7 +1457,10 @@ export class CliProviderInstance implements ProviderInstance {
1359
1457
  const parsedStatus = null;
1360
1458
  const rawStatus = adapterStatus.status;
1361
1459
  const autoApproveActive = this.maybeAutoApproveStatus(adapterStatus, now);
1362
- const newStatus = autoApproveActive ? 'generating' : rawStatus;
1460
+ const externalNativeFinal = this.getExternalNativeFinalReconciliation(undefined, adapterStatus);
1461
+ const newStatus = externalNativeFinal && isCliGeneratingLikeStatus(rawStatus)
1462
+ ? 'idle'
1463
+ : (autoApproveActive ? 'generating' : rawStatus);
1363
1464
  const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
1364
1465
  const chatTitle = `${this.provider.name} · ${dirName}`;
1365
1466
  const partial = this.adapter.getPartialResponse();
@@ -401,7 +401,62 @@ export interface ProviderModule {
401
401
  spawnArgBuilder?: (config: Record<string, string>) => string[];
402
402
  /** ACP agent auth methods (multiple supported — in priority order) */
403
403
  auth?: AcpAuthMethod[];
404
+ /**
405
+ * Repo Mesh coordinator capability and MCP ingestion behavior.
406
+ * Providers must declare this rather than relying on daemon hardcoded CLI quirks.
407
+ */
408
+ meshCoordinator?: ProviderMeshCoordinatorConfig;
409
+ }
410
+ export type MeshCoordinatorMcpConfigMode = 'auto_import' | 'manual' | 'none';
411
+ export type MeshCoordinatorMcpConfigFormat = 'claude_mcp_json' | 'hermes_config_yaml';
412
+ export interface ProviderMeshCoordinatorConfig {
413
+ supported: boolean;
414
+ reason?: string;
415
+ mcpConfig?: {
416
+ mode: MeshCoordinatorMcpConfigMode;
417
+ format?: MeshCoordinatorMcpConfigFormat;
418
+ path?: string;
419
+ serverName?: string;
420
+ configPathCommand?: string;
421
+ requiresRestart?: boolean;
422
+ instructions?: string;
423
+ template?: string;
424
+ };
425
+ systemPromptInjection?: MeshCoordinatorSystemPromptInjection;
426
+ delegatedWorkerIsolation?: MeshCoordinatorDelegatedWorkerIsolation;
404
427
  }
428
+ export type MeshCoordinatorSystemPromptInjection = {
429
+ mode: 'cli_arg';
430
+ flag: string;
431
+ } | {
432
+ mode: 'config_override';
433
+ flag: string;
434
+ template: string;
435
+ } | {
436
+ mode: 'context_file';
437
+ path: string;
438
+ wrapper?: string;
439
+ } | {
440
+ mode: 'env_var';
441
+ name: string;
442
+ };
443
+ export interface MeshCoordinatorDelegatedWorkerIsolation {
444
+ env?: {
445
+ unset?: string[];
446
+ };
447
+ args?: MeshCoordinatorDelegatedWorkerArgRule[];
448
+ }
449
+ export type MeshCoordinatorDelegatedWorkerArgRule = {
450
+ mode: 'empty_mcp_config';
451
+ flag: string;
452
+ strictFlag?: string;
453
+ } | {
454
+ mode: 'config_override';
455
+ flag: string;
456
+ key: string;
457
+ value: string;
458
+ dedupeKey?: string;
459
+ };
405
460
  export interface ProviderResumeCapability {
406
461
  supported: boolean;
407
462
  stopStrategy?: 'command' | 'ctrl_c';
@@ -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
  *
@@ -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 {
@@ -128,6 +128,14 @@ export class TerminalAdapter {
128
128
  return this.lastScreen || this.computeScreen();
129
129
  }
130
130
 
131
+ getCursorPosition(): { row: number; col: number } {
132
+ const buf = this.term.buffer.active;
133
+ return {
134
+ row: Math.max(0, (buf as any).cursorY ?? 0),
135
+ col: Math.max(0, (buf as any).cursorX ?? 0),
136
+ };
137
+ }
138
+
131
139
  kill(): void {
132
140
  this.stopTimers();
133
141
  try { this.pty?.kill(); } catch { /* ignore */ }