@adhdev/daemon-core 0.9.82-rc.187 → 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 (37) 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 +401 -33
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +400 -33
  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/driver.d.ts +6 -1
  15. package/dist/providers/spec/schema.gen.d.ts +22 -0
  16. package/dist/providers/spec/types.d.ts +10 -0
  17. package/dist/repo-mesh-types.d.ts +6 -0
  18. package/package.json +1 -1
  19. package/src/boot/daemon-lifecycle.ts +2 -0
  20. package/src/commands/chat-commands.ts +26 -0
  21. package/src/commands/cli-manager.ts +52 -14
  22. package/src/commands/router.ts +35 -4
  23. package/src/git/git-commands.ts +20 -2
  24. package/src/git/git-status.ts +35 -6
  25. package/src/git/git-types.ts +2 -0
  26. package/src/index.ts +1 -1
  27. package/src/providers/cli-provider-instance.ts +110 -9
  28. package/src/providers/contracts.d.ts +55 -0
  29. package/src/providers/contracts.ts +35 -0
  30. package/src/providers/provider-schema.ts +56 -1
  31. package/src/providers/sdk/v1/schemas/cli/provider.schema.json +46 -0
  32. package/src/providers/sdk/v1/types/common/index.ts +19 -0
  33. package/src/providers/spec/driver.ts +68 -1
  34. package/src/providers/spec/schema.gen.ts +12 -1
  35. package/src/providers/spec/schema.json +21 -1
  36. package/src/providers/spec/types.ts +10 -0
  37. package/src/repo-mesh-types.ts +6 -0
@@ -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 {
@@ -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
@@ -130,7 +130,18 @@ export const SCHEMA = {
130
130
  "additionalProperties": false,
131
131
  "properties": {
132
132
  "busy_hold_ms": { "type": "integer", "minimum": 0 },
133
- "startup_grace_ms": { "type": "integer", "minimum": 0 }
133
+ "startup_grace_ms": { "type": "integer", "minimum": 0 },
134
+ "completion_idle_after": {
135
+ "type": "object",
136
+ "additionalProperties": false,
137
+ "required": ["regex", "hold_ms"],
138
+ "properties": {
139
+ "section": { "type": "string", "minLength": 1 },
140
+ "regex": { "type": "string", "minLength": 1 },
141
+ "flags": { "type": "string" },
142
+ "hold_ms": { "type": "integer", "minimum": 0 }
143
+ }
144
+ }
134
145
  }
135
146
  }
136
147
  },
@@ -59,7 +59,27 @@
59
59
  "default": [],
60
60
  "items": { "$ref": "#/definitions/delegateTrigger" }
61
61
  },
62
- "native_history": { "$ref": "#/definitions/nativeHistory" }
62
+ "native_history": { "$ref": "#/definitions/nativeHistory" },
63
+ "cli_version_range": { "type": "string", "minLength": 1 },
64
+ "debounce": {
65
+ "type": "object",
66
+ "additionalProperties": false,
67
+ "properties": {
68
+ "busy_hold_ms": { "type": "integer", "minimum": 0 },
69
+ "startup_grace_ms": { "type": "integer", "minimum": 0 },
70
+ "completion_idle_after": {
71
+ "type": "object",
72
+ "additionalProperties": false,
73
+ "required": ["regex", "hold_ms"],
74
+ "properties": {
75
+ "section": { "type": "string", "minLength": 1 },
76
+ "regex": { "type": "string", "minLength": 1 },
77
+ "flags": { "type": "string" },
78
+ "hold_ms": { "type": "integer", "minimum": 0 }
79
+ }
80
+ }
81
+ }
82
+ }
63
83
  },
64
84
  "definitions": {
65
85
  "size": {
@@ -227,5 +227,15 @@ export interface CliSpec {
227
227
  * once the window passes and an idle state has actually been
228
228
  * observed. */
229
229
  startup_grace_ms?: number;
230
+ /** Treat a provider-specific completion marker as idle after it has
231
+ * remained visible for hold_ms. This handles TUIs that leave their
232
+ * last spinner glyph next to a completed timer, causing the normal
233
+ * busy regex to keep matching after the turn is done. */
234
+ completion_idle_after?: {
235
+ section?: string;
236
+ regex: string;
237
+ flags?: string;
238
+ hold_ms: number;
239
+ };
230
240
  };
231
241
  }
@@ -359,11 +359,17 @@ export interface RepoMeshSessionStatus {
359
359
  sessionId: string;
360
360
  providerType?: string;
361
361
  state?: string;
362
+ chatStatus?: string;
362
363
  lifecycle?: 'starting' | 'running' | 'stopping' | 'stopped' | 'failed' | 'interrupted';
363
364
  surfaceKind?: 'live_runtime' | 'recovery_snapshot' | 'inactive_record';
364
365
  recoveryState?: string | null;
365
366
  workspace?: string | null;
366
367
  title?: string | null;
368
+ role?: string | null;
369
+ isSelfCoordinator?: boolean;
370
+ statusNote?: string | null;
371
+ createdAt?: string | null;
372
+ startedAt?: string | null;
367
373
  lastActivityAt?: string | null;
368
374
  isCached?: boolean;
369
375
  }