@adhdev/daemon-core 0.8.15 → 0.8.16
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.
- package/dist/cli-adapters/provider-cli-adapter.d.ts +9 -0
- package/dist/daemon/dev-auto-implement.d.ts +1 -0
- package/dist/index.js +253 -35
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +253 -35
- package/dist/index.mjs.map +1 -1
- package/node_modules/@adhdev/session-host-core/package.json +1 -1
- package/package.json +1 -1
- package/src/cli-adapters/provider-cli-adapter.ts +188 -9
- package/src/commands/upgrade-helper.ts +9 -3
- package/src/daemon/dev-auto-implement.ts +9 -0
- package/src/daemon/dev-cli-debug.ts +58 -21
package/package.json
CHANGED
|
@@ -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
|
|
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
|
|
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():
|
|
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
|
-
|
|
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:
|
|
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 (
|
|
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
|
|
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
|
|
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
|
-
|
|
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 (
|
|
415
|
-
|
|
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);
|