@adhdev/daemon-core 0.8.15 → 0.8.17

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/session-host-core",
3
- "version": "0.8.15",
3
+ "version": "0.8.17",
4
4
  "description": "ADHDev local session host core — session registry, protocol, buffers",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.8.15",
3
+ "version": "0.8.17",
4
4
  "description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -69,6 +69,7 @@ export interface CliScriptInput {
69
69
  screenText: string; // Current visible screen snapshot
70
70
  messages: CliChatMessage[]; // Previously parsed messages
71
71
  partialResponse: string; // Current partial response being generated
72
+ promptText?: string; // Current turn prompt when available
72
73
  }
73
74
 
74
75
  interface TurnParseScope {
@@ -312,6 +313,45 @@ function normalizeComparableMessageContent(text: string): string {
312
313
  .trim();
313
314
  }
314
315
 
316
+ function trimPromptEchoPrefix(text: string, promptText?: string | null): string {
317
+ const prompt = normalizeComparableMessageContent(String(promptText || ''));
318
+ if (!prompt) return String(text || '');
319
+
320
+ const lines = String(text || '').split(/\r\n|\n|\r/g);
321
+ let dropCount = 0;
322
+ for (let index = 0; index < Math.min(lines.length, 6); index += 1) {
323
+ const fragment = normalizeComparableMessageContent(lines[index].replace(/^[.…]+\s*/, ''));
324
+ if (!fragment) {
325
+ if (dropCount === index) dropCount = index + 1;
326
+ continue;
327
+ }
328
+ const fragmentWordCount = fragment ? fragment.split(/\s+/).filter(Boolean).length : 0;
329
+ const canBePromptEcho = fragment.length >= 16 || fragmentWordCount >= 4;
330
+ if (canBePromptEcho && prompt.includes(fragment)) {
331
+ dropCount = index + 1;
332
+ continue;
333
+ }
334
+ break;
335
+ }
336
+
337
+ return lines.slice(dropCount).join('\n').trim();
338
+ }
339
+
340
+ function getLastUserPromptText(messages: Array<{ role?: string; content?: string }> | null | undefined): string {
341
+ const items = Array.isArray(messages) ? messages : [];
342
+ for (let index = items.length - 1; index >= 0; index -= 1) {
343
+ const message = items[index];
344
+ if (message?.role === 'user' && typeof message.content === 'string' && message.content.trim()) {
345
+ return message.content;
346
+ }
347
+ }
348
+ return '';
349
+ }
350
+
351
+ function looksLikeConfirmOnlyLabel(label: string): boolean {
352
+ return /^(?:continue|confirm|ok|yes|trust|proceed|enter)$/i.test(String(label || '').trim());
353
+ }
354
+
315
355
  /**
316
356
  * Normalize provider.json for auto-implement approval detection.
317
357
  * Kept for backward compat with dev-server auto-impl pipeline only.
@@ -410,6 +450,8 @@ export class ProviderCliAdapter implements CliAdapter {
410
450
  private submitRetryUsed = false;
411
451
  private submitRetryPromptSnippet = '';
412
452
  private idleFinishCandidate: IdleFinishCandidate | null = null;
453
+ private finishRetryTimer: NodeJS.Timeout | null = null;
454
+ private finishRetryCount = 0;
413
455
 
414
456
  // Resize redraw suppression
415
457
  private resizeSuppressUntil: number = 0;
@@ -434,6 +476,8 @@ export class ProviderCliAdapter implements CliAdapter {
434
476
  private static readonly MAX_TRACE_ENTRIES = 250;
435
477
  private readonly providerResolutionMeta: Record<string, any>;
436
478
  private static readonly IDLE_FINISH_CONFIRM_MS = 900;
479
+ private static readonly FINISH_RETRY_DELAY_MS = 300;
480
+ private static readonly MAX_FINISH_RETRIES = 2;
437
481
 
438
482
  private syncMessageViews(): void {
439
483
  this.messages = [...this.committedMessages];
@@ -519,6 +563,7 @@ export class ProviderCliAdapter implements CliAdapter {
519
563
  screenText: this.terminalScreen.getText(),
520
564
  messages: [...baseMessages],
521
565
  partialResponse,
566
+ promptText: scope?.prompt || '',
522
567
  };
523
568
  }
524
569
 
@@ -874,6 +919,8 @@ export class ProviderCliAdapter implements CliAdapter {
874
919
  this.terminalScreen.reset(24, 80);
875
920
  this.pendingTerminalQueryTail = '';
876
921
  this.currentTurnScope = null;
922
+ this.finishRetryCount = 0;
923
+ if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
877
924
  this.ready = false;
878
925
  await this.ptyProcess.ready;
879
926
  this.recordTrace('ready', {
@@ -929,11 +976,13 @@ export class ProviderCliAdapter implements CliAdapter {
929
976
  if (this.startupParseGate) {
930
977
  this.startupBuffer += cleanData;
931
978
  const elapsed = Date.now() - this.spawnAt;
932
- const scriptStatus = this.runDetectStatus(this.startupBuffer);
933
979
  const screenText = this.terminalScreen.getText() || '';
980
+ const startupModal = this.getStartupConfirmationModal(screenText);
981
+ const scriptStatus = startupModal ? 'waiting_approval' : this.runDetectStatus(this.startupBuffer);
934
982
  const hasInteractivePrompt = this.looksLikeVisibleIdlePrompt(screenText);
935
983
  const startupStableMs = this.lastScreenChangeAt ? (now - this.lastScreenChangeAt) : 0;
936
984
  const isReady = ((scriptStatus === 'idle' || scriptStatus === 'waiting_approval') && hasInteractivePrompt && startupStableMs >= 700)
985
+ || (!!startupModal && startupStableMs >= 700)
937
986
  || elapsed > 8000
938
987
  || this.startupBuffer.length > 12000;
939
988
 
@@ -975,7 +1024,9 @@ export class ProviderCliAdapter implements CliAdapter {
975
1024
  this.approvalExitTimeout = setTimeout(() => {
976
1025
  if (this.currentStatus !== 'waiting_approval') return;
977
1026
  const tail = this.recentOutputBuffer;
978
- const modal = this.runParseApproval(tail);
1027
+ const screenText = this.terminalScreen.getText() || '';
1028
+ const startupModal = this.getStartupConfirmationModal(screenText);
1029
+ const modal = this.runParseApproval(tail) || startupModal;
979
1030
  const stillWaiting = this.runDetectStatus(tail) === 'waiting_approval' || !!modal;
980
1031
  if (stillWaiting) {
981
1032
  this.activeModal = modal || this.activeModal || { message: 'Approval required', buttons: ['Allow', 'Deny'] };
@@ -1002,6 +1053,76 @@ export class ProviderCliAdapter implements CliAdapter {
1002
1053
  || /for\s*shortcuts/i.test(text);
1003
1054
  }
1004
1055
 
1056
+ private looksLikeVisibleAssistantCandidate(screenText: string): boolean {
1057
+ const lines = sanitizeTerminalText(String(screenText || '')).split(/\r\n|\n|\r/g);
1058
+ for (const line of lines) {
1059
+ const trimmed = String(line || '').trim();
1060
+ if (!trimmed) continue;
1061
+ if (/^➜\s+\S+/.test(trimmed)) continue;
1062
+ if (/^Update available!/i.test(trimmed)) continue;
1063
+ if (/Claude Code v\d/i.test(trimmed)) continue;
1064
+ if (/^⏵⏵\s+accept edits on/i.test(trimmed)) continue;
1065
+ if (/^[◐◑◒◓◴◵◶◷◸◹◺◿].*\/effort/i.test(trimmed)) continue;
1066
+ if (/^[✻✶✳✢✽⠂⠐⠒⠓⠦⠴⠶⠷⠿]+$/.test(trimmed)) continue;
1067
+ if (/esc to (cancel|interrupt|stop)/i.test(trimmed)) continue;
1068
+ const assistantMatch = trimmed.match(/^⏺\s+(.+)$/);
1069
+ if (!assistantMatch) continue;
1070
+ const content = assistantMatch[1].trim();
1071
+ if (!content) continue;
1072
+ if (/^(?:Bash|Read|Write|Edit|MultiEdit|Task|Glob|Grep|LS|NotebookEdit)\(/.test(content)) continue;
1073
+ if (/This command requires approval|Do you want to proceed|Allow once|Always allow/i.test(content)) continue;
1074
+ return true;
1075
+ }
1076
+ return false;
1077
+ }
1078
+
1079
+ private shouldRetryFinishResponse(commitResult: { hasAssistant: boolean; assistantContent: string }): boolean {
1080
+ if (!this.currentTurnScope) return false;
1081
+ if (this.currentStatus === 'waiting_approval' || this.activeModal) return false;
1082
+ if (this.finishRetryCount >= ProviderCliAdapter.MAX_FINISH_RETRIES) return false;
1083
+ if (commitResult.hasAssistant && commitResult.assistantContent.trim()) return false;
1084
+
1085
+ const screenText = this.terminalScreen.getText() || '';
1086
+ if (!this.looksLikeVisibleAssistantCandidate(screenText)) return false;
1087
+
1088
+ const now = Date.now();
1089
+ const quietForMs = this.lastNonEmptyOutputAt ? (now - this.lastNonEmptyOutputAt) : Number.MAX_SAFE_INTEGER;
1090
+ const screenStableMs = this.lastScreenChangeAt ? (now - this.lastScreenChangeAt) : 0;
1091
+ return quietForMs < 1200 || screenStableMs < 1200 || !commitResult.hasAssistant;
1092
+ }
1093
+
1094
+ private getStartupConfirmationModal(screenText: string): { message: string; buttons: string[] } | null {
1095
+ const text = sanitizeTerminalText(String(screenText || ''));
1096
+ if (!text.trim()) return null;
1097
+
1098
+ if (this.cliType === 'claude-cli') {
1099
+ const hasTrustPrompt = /Quick safety check/i.test(text)
1100
+ || /Is this a project you trust/i.test(text)
1101
+ || /Do you trust (?:this project|the contents of this directory|the files in this folder)/i.test(text);
1102
+ const hasConfirmFooter = /Press Enter to (?:continue|confirm)/i.test(text)
1103
+ || /Enter to confirm/i.test(text)
1104
+ || /Esc to (?:cancel|exit)/i.test(text);
1105
+ if (hasTrustPrompt || (hasConfirmFooter && /trust/i.test(text))) {
1106
+ return {
1107
+ message: 'Confirm Claude Code project trust',
1108
+ buttons: ['Continue'],
1109
+ };
1110
+ }
1111
+ }
1112
+
1113
+ return null;
1114
+ }
1115
+
1116
+ private shouldResolveModalWithEnter(modal: { message: string; buttons: string[] } | null, buttonIndex: number): boolean {
1117
+ if (!modal || buttonIndex !== 0) return false;
1118
+ const buttons = Array.isArray(modal.buttons) ? modal.buttons : [];
1119
+ if (buttons.length !== 1) return false;
1120
+ const buttonLabel = String(buttons[0] || '').trim();
1121
+ const modalText = `${modal.message || ''} ${buttonLabel}`.trim();
1122
+ return looksLikeConfirmOnlyLabel(buttonLabel)
1123
+ || /Quick safety check|project trust|trust (?:this project|the contents of this directory|the files in this folder)|Enter to confirm/i.test(modalText);
1124
+ }
1125
+
1005
1126
  private async waitForInteractivePrompt(maxWaitMs = 5000): Promise<void> {
1006
1127
  const startedAt = Date.now();
1007
1128
  let loggedWait = false;
@@ -1060,10 +1181,11 @@ export class ProviderCliAdapter implements CliAdapter {
1060
1181
  }
1061
1182
  const tail = this.settledBuffer;
1062
1183
  const screenText = this.terminalScreen.getText() || '';
1063
- const modal = this.runParseApproval(tail);
1184
+ const startupModal = this.getStartupConfirmationModal(screenText);
1185
+ const modal = this.runParseApproval(tail) || startupModal;
1064
1186
  const rawScriptStatus = this.runDetectStatus(tail);
1065
1187
  // detectStatus is the sole authority for status. parseApproval only enriches modal info.
1066
- const scriptStatus = rawScriptStatus;
1188
+ const scriptStatus = startupModal ? 'waiting_approval' : rawScriptStatus;
1067
1189
  const parsedTranscript = this.parseCurrentTranscript(
1068
1190
  this.committedMessages,
1069
1191
  this.responseBuffer,
@@ -1284,24 +1406,43 @@ export class ProviderCliAdapter implements CliAdapter {
1284
1406
  this.recordTrace('finish_response', {
1285
1407
  ...this.buildTraceParseSnapshot(this.currentTurnScope, this.responseBuffer),
1286
1408
  });
1287
- this.commitCurrentTranscript();
1409
+ const commitResult = this.commitCurrentTranscript();
1410
+ if (this.shouldRetryFinishResponse(commitResult)) {
1411
+ this.finishRetryCount += 1;
1412
+ this.recordTrace('finish_response_retry', {
1413
+ retryCount: this.finishRetryCount,
1414
+ retryDelayMs: ProviderCliAdapter.FINISH_RETRY_DELAY_MS,
1415
+ assistantContent: this.summarizeTraceText(commitResult.assistantContent, 220),
1416
+ ...this.buildTraceParseSnapshot(this.currentTurnScope, this.responseBuffer),
1417
+ });
1418
+ if (this.finishRetryTimer) clearTimeout(this.finishRetryTimer);
1419
+ this.finishRetryTimer = setTimeout(() => {
1420
+ this.finishRetryTimer = null;
1421
+ if (this.isWaitingForResponse && this.currentStatus !== 'waiting_approval') {
1422
+ this.finishResponse();
1423
+ }
1424
+ }, ProviderCliAdapter.FINISH_RETRY_DELAY_MS);
1425
+ return;
1426
+ }
1288
1427
  if (this.responseTimeout) { clearTimeout(this.responseTimeout); this.responseTimeout = null; }
1289
1428
  if (this.idleTimeout) { clearTimeout(this.idleTimeout); this.idleTimeout = null; }
1290
1429
  if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
1291
1430
  if (this.submitRetryTimer) { clearTimeout(this.submitRetryTimer); this.submitRetryTimer = null; }
1431
+ if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
1292
1432
 
1293
1433
  this.responseBuffer = '';
1294
1434
  this.isWaitingForResponse = false;
1295
1435
  this.responseSettleIgnoreUntil = 0;
1296
1436
  this.submitRetryUsed = false;
1297
1437
  this.submitRetryPromptSnippet = '';
1438
+ this.finishRetryCount = 0;
1298
1439
  this.currentTurnScope = null;
1299
1440
  this.activeModal = null;
1300
1441
  this.setStatus('idle', 'response_finished');
1301
1442
  this.onStatusChange?.();
1302
1443
  }
1303
1444
 
1304
- private commitCurrentTranscript(): void {
1445
+ private commitCurrentTranscript(): { hasAssistant: boolean; assistantContent: string } {
1305
1446
  const parsed = this.parseCurrentTranscript(
1306
1447
  this.committedMessages,
1307
1448
  this.responseBuffer,
@@ -1309,6 +1450,13 @@ export class ProviderCliAdapter implements CliAdapter {
1309
1450
  );
1310
1451
  if (parsed && Array.isArray(parsed.messages)) {
1311
1452
  this.committedMessages = this.normalizeParsedMessages(parsed.messages);
1453
+ const promptForTrim = this.currentTurnScope?.prompt || getLastUserPromptText(this.committedMessages);
1454
+ if (promptForTrim) {
1455
+ const lastAssistantForTrim = [...this.committedMessages].reverse().find((message) => message.role === 'assistant');
1456
+ if (lastAssistantForTrim) {
1457
+ lastAssistantForTrim.content = trimPromptEchoPrefix(lastAssistantForTrim.content, promptForTrim);
1458
+ }
1459
+ }
1312
1460
  this.syncMessageViews();
1313
1461
  const lastAssistant = [...this.committedMessages].reverse().find((message) => message.role === 'assistant');
1314
1462
  this.recordTrace('commit_transcript', {
@@ -1324,7 +1472,15 @@ export class ProviderCliAdapter implements CliAdapter {
1324
1472
  `[${this.cliType}] Commit without assistant turn: prompt=${JSON.stringify(this.currentTurnScope.prompt).slice(0, 140)} responseBuffer=${JSON.stringify(this.summarizeTraceText(this.responseBuffer, 220)).slice(0, 260)} providerDir=${this.providerResolutionMeta.providerDir || '-'} scriptDir=${this.providerResolutionMeta.scriptDir || '-'} scriptsPath=${this.providerResolutionMeta.scriptsPath || '-'}`
1325
1473
  );
1326
1474
  }
1475
+ return {
1476
+ hasAssistant: !!lastAssistant,
1477
+ assistantContent: lastAssistant?.content || '',
1478
+ };
1327
1479
  }
1480
+ return {
1481
+ hasAssistant: false,
1482
+ assistantContent: '',
1483
+ };
1328
1484
  }
1329
1485
 
1330
1486
  // ─── Script Execution ──────────────────────────
@@ -1411,7 +1567,15 @@ export class ProviderCliAdapter implements CliAdapter {
1411
1567
  if (!this.cliScripts?.parseOutput) return null;
1412
1568
  try {
1413
1569
  const input = this.buildParseInput(baseMessages, partialResponse, scope);
1414
- return this.cliScripts.parseOutput(input);
1570
+ const parsed = this.cliScripts.parseOutput(input);
1571
+ const promptForTrim = scope?.prompt || getLastUserPromptText(baseMessages);
1572
+ if (parsed && Array.isArray(parsed.messages) && promptForTrim) {
1573
+ const lastAssistant = [...parsed.messages].reverse().find((message: any) => message?.role === 'assistant' && typeof message.content === 'string');
1574
+ if (lastAssistant) {
1575
+ lastAssistant.content = trimPromptEchoPrefix(lastAssistant.content, promptForTrim);
1576
+ }
1577
+ }
1578
+ return parsed;
1415
1579
  } catch (e: any) {
1416
1580
  LOG.warn('CLI', `[${this.cliType}] parseOutput error: ${e.message}`);
1417
1581
  return null;
@@ -1456,11 +1620,17 @@ export class ProviderCliAdapter implements CliAdapter {
1456
1620
  if (!this.ready) throw new Error(`${this.cliName} not ready (status: ${this.currentStatus})`);
1457
1621
  if (this.isWaitingForResponse) return;
1458
1622
  await this.waitForInteractivePrompt();
1623
+ const blockingModal = this.activeModal || this.getStartupConfirmationModal(this.terminalScreen.getText() || '');
1624
+ if (blockingModal || this.currentStatus === 'waiting_approval') {
1625
+ throw new Error(`${this.cliName} is awaiting confirmation before it can accept a prompt`);
1626
+ }
1459
1627
 
1460
1628
  this.committedMessages.push({ role: 'user', content: text, timestamp: Date.now() });
1461
1629
  this.syncMessageViews();
1462
1630
  this.isWaitingForResponse = true;
1463
1631
  this.responseBuffer = '';
1632
+ this.finishRetryCount = 0;
1633
+ if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
1464
1634
  this.clearIdleFinishCandidate('send_message');
1465
1635
  this.currentTurnScope = {
1466
1636
  prompt: text,
@@ -1697,6 +1867,7 @@ export class ProviderCliAdapter implements CliAdapter {
1697
1867
  if (this.settleTimer) { clearTimeout(this.settleTimer); this.settleTimer = null; }
1698
1868
  if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
1699
1869
  if (this.submitRetryTimer) { clearTimeout(this.submitRetryTimer); this.submitRetryTimer = null; }
1870
+ if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
1700
1871
  if (this.responseTimeout) { clearTimeout(this.responseTimeout); this.responseTimeout = null; }
1701
1872
  if (this.idleTimeout) { clearTimeout(this.idleTimeout); this.idleTimeout = null; }
1702
1873
  if (this.pendingScriptStatusTimer) { clearTimeout(this.pendingScriptStatusTimer); this.pendingScriptStatusTimer = null; }
@@ -1705,6 +1876,7 @@ export class ProviderCliAdapter implements CliAdapter {
1705
1876
  this.pendingTerminalQueryTail = '';
1706
1877
  if (this.ptyOutputFlushTimer) { clearTimeout(this.ptyOutputFlushTimer); this.ptyOutputFlushTimer = null; }
1707
1878
  this.ptyOutputBuffer = '';
1879
+ this.finishRetryCount = 0;
1708
1880
  if (this.ptyProcess) {
1709
1881
  this.ptyProcess.write('\x03');
1710
1882
  setTimeout(() => {
@@ -1724,6 +1896,7 @@ export class ProviderCliAdapter implements CliAdapter {
1724
1896
  if (this.settleTimer) { clearTimeout(this.settleTimer); this.settleTimer = null; }
1725
1897
  if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
1726
1898
  if (this.submitRetryTimer) { clearTimeout(this.submitRetryTimer); this.submitRetryTimer = null; }
1899
+ if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
1727
1900
  if (this.responseTimeout) { clearTimeout(this.responseTimeout); this.responseTimeout = null; }
1728
1901
  if (this.idleTimeout) { clearTimeout(this.idleTimeout); this.idleTimeout = null; }
1729
1902
  if (this.pendingScriptStatusTimer) { clearTimeout(this.pendingScriptStatusTimer); this.pendingScriptStatusTimer = null; }
@@ -1732,6 +1905,7 @@ export class ProviderCliAdapter implements CliAdapter {
1732
1905
  this.pendingTerminalQueryTail = '';
1733
1906
  if (this.ptyOutputFlushTimer) { clearTimeout(this.ptyOutputFlushTimer); this.ptyOutputFlushTimer = null; }
1734
1907
  this.ptyOutputBuffer = '';
1908
+ this.finishRetryCount = 0;
1735
1909
  if (this.ptyProcess) {
1736
1910
  try {
1737
1911
  if (typeof this.ptyProcess.detach === 'function') {
@@ -1762,6 +1936,8 @@ export class ProviderCliAdapter implements CliAdapter {
1762
1936
  this.pendingTerminalQueryTail = '';
1763
1937
  if (this.ptyOutputFlushTimer) { clearTimeout(this.ptyOutputFlushTimer); this.ptyOutputFlushTimer = null; }
1764
1938
  this.ptyOutputBuffer = '';
1939
+ if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
1940
+ this.finishRetryCount = 0;
1765
1941
  this.terminalScreen.reset();
1766
1942
  this.ptyProcess?.clearBuffer?.();
1767
1943
  this.onStatusChange?.();
@@ -1780,10 +1956,11 @@ export class ProviderCliAdapter implements CliAdapter {
1780
1956
 
1781
1957
  resolveModal(buttonIndex: number): void {
1782
1958
  if (!this.ptyProcess || (this.currentStatus !== 'waiting_approval' && !this.activeModal)) return;
1959
+ const modal = this.activeModal;
1783
1960
  this.clearIdleFinishCandidate('resolve_modal');
1784
1961
  this.recordTrace('resolve_modal', {
1785
1962
  buttonIndex,
1786
- activeModal: this.activeModal,
1963
+ activeModal: modal,
1787
1964
  });
1788
1965
  this.activeModal = null;
1789
1966
  this.lastApprovalResolvedAt = Date.now();
@@ -1794,7 +1971,9 @@ export class ProviderCliAdapter implements CliAdapter {
1794
1971
  }
1795
1972
  this.setStatus('generating', 'approval_resolved');
1796
1973
  this.onStatusChange?.();
1797
- if (buttonIndex in this.approvalKeys) {
1974
+ if (this.shouldResolveModalWithEnter(modal, buttonIndex)) {
1975
+ this.ptyProcess.write('\r');
1976
+ } else if (buttonIndex in this.approvalKeys) {
1798
1977
  this.ptyProcess.write(this.approvalKeys[buttonIndex]);
1799
1978
  } else {
1800
1979
  const DOWN = '\x1B[B';
@@ -32,7 +32,11 @@ function appendUpgradeLog(message: string): void {
32
32
  }
33
33
 
34
34
  function getNpmExecutable(): string {
35
- return process.platform === 'win32' ? 'npm.cmd' : 'npm';
35
+ return 'npm';
36
+ }
37
+
38
+ function getNpmExecOptions(): { shell: boolean } {
39
+ return { shell: process.platform === 'win32' };
36
40
  }
37
41
 
38
42
  function killPid(pid: number): boolean {
@@ -104,9 +108,10 @@ function removeDaemonPidFile(): void {
104
108
  }
105
109
 
106
110
  function cleanupStaleGlobalInstallDirs(pkgName: string): void {
107
- const npmRoot = execFileSync(getNpmExecutable(), ['root', '-g'], { encoding: 'utf8' }).trim();
111
+ const npmExecOpts = getNpmExecOptions();
112
+ const npmRoot = execFileSync(getNpmExecutable(), ['root', '-g'], { encoding: 'utf8', ...npmExecOpts }).trim();
108
113
  if (!npmRoot) return;
109
- const npmPrefix = execFileSync(getNpmExecutable(), ['prefix', '-g'], { encoding: 'utf8' }).trim();
114
+ const npmPrefix = execFileSync(getNpmExecutable(), ['prefix', '-g'], { encoding: 'utf8', ...npmExecOpts }).trim();
110
115
  const binDir = process.platform === 'win32' ? npmPrefix : path.join(npmPrefix, 'bin');
111
116
  const packageBaseName = pkgName.startsWith('@') ? pkgName.split('/')[1] : pkgName;
112
117
  const binNames = new Set<string>([packageBaseName]);
@@ -175,6 +180,7 @@ async function runDaemonUpgradeHelper(payload: DaemonUpgradeHelperPayload): Prom
175
180
  encoding: 'utf8',
176
181
  stdio: 'pipe',
177
182
  maxBuffer: 20 * 1024 * 1024,
183
+ ...getNpmExecOptions(),
178
184
  },
179
185
  );
180
186
  if (installOutput.trim()) {
@@ -27,6 +27,7 @@ type CliExerciseVerification = {
27
27
  lastAssistantMustNotMatchAny?: string[];
28
28
  inspectFields?: string[];
29
29
  description?: string;
30
+ focusAreas?: string[];
30
31
  fixtureName?: string;
31
32
  fixtureNames?: string[];
32
33
  };
@@ -1226,6 +1227,14 @@ export function buildCliAutoImplPrompt(ctx: DevServerContext,
1226
1227
  lines.push('20. When a bug comes from noisy PTY text, first normalize and classify the line family; do NOT just append another special-case substring to the parser.');
1227
1228
  lines.push('');
1228
1229
 
1230
+ if (verification?.focusAreas?.length) {
1231
+ lines.push('## Provider-Specific Focus Areas');
1232
+ for (const area of verification.focusAreas) {
1233
+ lines.push(`- ${area}`);
1234
+ }
1235
+ lines.push('');
1236
+ }
1237
+
1229
1238
  lines.push('## Task');
1230
1239
  lines.push(`Edit files in \`${providerDir}\` to implement: **${functions.join(', ')}**`);
1231
1240
  lines.push('');
@@ -383,6 +383,61 @@ export async function runCliExerciseInternal(ctx: DevServerContext, body: CliExe
383
383
  let idleSince = 0;
384
384
  let sawBusy = false;
385
385
 
386
+ const noteStatus = (status: string) => {
387
+ if (status !== lastStatus) {
388
+ statusesSeen.push(status);
389
+ lastStatus = status;
390
+ }
391
+ };
392
+
393
+ const resolveActiveModalIfNeeded = (status: string, modal: any): boolean => {
394
+ if (!autoResolveApprovals || status !== 'waiting_approval' || !modal || !Array.isArray(modal.buttons) || modal.buttons.length === 0) {
395
+ return false;
396
+ }
397
+ const clampedIndex = Math.max(0, Math.min(Number(approvalButtonIndex) || 0, modal.buttons.length - 1));
398
+ const modalKey = JSON.stringify({
399
+ message: modal.message || '',
400
+ buttons: modal.buttons,
401
+ index: clampedIndex,
402
+ });
403
+ if (modalKey === lastModalKey || typeof bundle?.adapter?.resolveModal !== 'function') {
404
+ return false;
405
+ }
406
+ lastModalKey = modalKey;
407
+ approvalsResolved.push({
408
+ at: Date.now(),
409
+ buttonIndex: clampedIndex,
410
+ label: modal.buttons[clampedIndex] || null,
411
+ });
412
+ bundle.adapter.resolveModal(clampedIndex);
413
+ return true;
414
+ };
415
+
416
+ // Exercise runs are easiest to reason about when startup/trust confirmations are
417
+ // cleared before we attempt to type the repro prompt into the CLI.
418
+ const preflightStartedAt = Date.now();
419
+ while (Date.now() - preflightStartedAt < Math.max(1_000, readyTimeoutMs)) {
420
+ bundle = getCliTargetBundle(ctx, type, bundle.target.instanceId);
421
+ if (!bundle) {
422
+ throw new Error('CLI instance disappeared before exercise send');
423
+ }
424
+
425
+ const debug = typeof bundle.adapter.getDebugState === 'function' ? bundle.adapter.getDebugState() : null;
426
+ const trace = typeof bundle.adapter.getTraceState === 'function' ? bundle.adapter.getTraceState(traceLimit) : null;
427
+ const status = String(debug?.status || bundle.target.status || 'unknown');
428
+ const modal = debug?.activeModal || trace?.activeModal || null;
429
+ noteStatus(status);
430
+
431
+ if (resolveActiveModalIfNeeded(status, modal)) {
432
+ await sleep(150);
433
+ continue;
434
+ }
435
+
436
+ const startupParseGate = !!debug?.startupParseGate;
437
+ if (status === 'idle' && !startupParseGate) break;
438
+ await sleep(150);
439
+ }
440
+
386
441
  ctx.instanceManager.sendEvent(bundle.target.instanceId, 'send_message', { text });
387
442
 
388
443
  while (Date.now() - startAt < Math.max(1_000, timeoutMs)) {
@@ -400,10 +455,7 @@ export async function runCliExerciseInternal(ctx: DevServerContext, body: CliExe
400
455
  const sawSubmitWrite = traceEntries.some((entry: any) => entry?.type === 'submit_write');
401
456
  const hasTurnStarted = sawSendMessage || sawSubmitWrite || !!debug?.currentTurnScope;
402
457
 
403
- if (status !== lastStatus) {
404
- statusesSeen.push(status);
405
- lastStatus = status;
406
- }
458
+ noteStatus(status);
407
459
 
408
460
  if (status === 'generating' || status === 'waiting_approval') {
409
461
  sawBusy = true;
@@ -411,23 +463,8 @@ export async function runCliExerciseInternal(ctx: DevServerContext, body: CliExe
411
463
  }
412
464
 
413
465
  const modal = debug?.activeModal || trace?.activeModal || null;
414
- if (autoResolveApprovals && status === 'waiting_approval' && modal && Array.isArray(modal.buttons) && modal.buttons.length > 0) {
415
- const clampedIndex = Math.max(0, Math.min(Number(approvalButtonIndex) || 0, modal.buttons.length - 1));
416
- const modalKey = JSON.stringify({
417
- message: modal.message || '',
418
- buttons: modal.buttons,
419
- index: clampedIndex,
420
- });
421
- if (modalKey !== lastModalKey && typeof bundle.adapter.resolveModal === 'function') {
422
- lastModalKey = modalKey;
423
- approvalsResolved.push({
424
- at: Date.now(),
425
- buttonIndex: clampedIndex,
426
- label: modal.buttons[clampedIndex] || null,
427
- });
428
- bundle.adapter.resolveModal(clampedIndex);
429
- continue;
430
- }
466
+ if (resolveActiveModalIfNeeded(status, modal)) {
467
+ continue;
431
468
  }
432
469
 
433
470
  const traceCount = Number(trace?.entryCount || 0);
package/src/index.ts CHANGED
@@ -91,7 +91,8 @@ export { DaemonCommandHandler } from './commands/handler.js';
91
91
  export type { CommandResult, CommandContext } from './commands/handler.js';
92
92
  export { DaemonCommandRouter } from './commands/router.js';
93
93
  export type { CommandRouterDeps, CommandRouterResult } from './commands/router.js';
94
- export { maybeRunDaemonUpgradeHelperFromEnv } from './commands/upgrade-helper.js';
94
+ export { maybeRunDaemonUpgradeHelperFromEnv, spawnDetachedDaemonUpgradeHelper } from './commands/upgrade-helper.js';
95
+ export type { DaemonUpgradeHelperPayload } from './commands/upgrade-helper.js';
95
96
 
96
97
  // ── Status ──
97
98
  export { DaemonStatusReporter } from './status/reporter.js';