@adhdev/daemon-core 0.8.14 → 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.
@@ -66,6 +66,7 @@ export interface CliScriptInput {
66
66
  screenText: string;
67
67
  messages: CliChatMessage[];
68
68
  partialResponse: string;
69
+ promptText?: string;
69
70
  }
70
71
  export interface CliTraceEntry {
71
72
  id: number;
@@ -172,6 +173,8 @@ export declare class ProviderCliAdapter implements CliAdapter {
172
173
  private submitRetryUsed;
173
174
  private submitRetryPromptSnippet;
174
175
  private idleFinishCandidate;
176
+ private finishRetryTimer;
177
+ private finishRetryCount;
175
178
  private resizeSuppressUntil;
176
179
  private statusHistory;
177
180
  private cliScripts;
@@ -190,6 +193,8 @@ export declare class ProviderCliAdapter implements CliAdapter {
190
193
  private static readonly MAX_TRACE_ENTRIES;
191
194
  private readonly providerResolutionMeta;
192
195
  private static readonly IDLE_FINISH_CONFIRM_MS;
196
+ private static readonly FINISH_RETRY_DELAY_MS;
197
+ private static readonly MAX_FINISH_RETRIES;
193
198
  private syncMessageViews;
194
199
  private normalizeParsedMessages;
195
200
  private sliceFromOffset;
@@ -220,6 +225,10 @@ export declare class ProviderCliAdapter implements CliAdapter {
220
225
  private scheduleSettle;
221
226
  private armApprovalExitTimeout;
222
227
  private looksLikeVisibleIdlePrompt;
228
+ private looksLikeVisibleAssistantCandidate;
229
+ private shouldRetryFinishResponse;
230
+ private getStartupConfirmationModal;
231
+ private shouldResolveModalWithEnter;
223
232
  private waitForInteractivePrompt;
224
233
  private evaluateSettled;
225
234
  private finishResponse;
@@ -19,6 +19,7 @@ type CliExerciseVerification = {
19
19
  lastAssistantMustNotMatchAny?: string[];
20
20
  inspectFields?: string[];
21
21
  description?: string;
22
+ focusAreas?: string[];
22
23
  fixtureName?: string;
23
24
  fixtureNames?: string[];
24
25
  };
package/dist/index.js CHANGED
@@ -705,17 +705,32 @@ var init_terminal_screen = __esm({
705
705
  }
706
706
  });
707
707
 
708
+ // src/cli-adapters/spawn-env.ts
709
+ var import_session_host_core;
710
+ var init_spawn_env = __esm({
711
+ "src/cli-adapters/spawn-env.ts"() {
712
+ "use strict";
713
+ import_session_host_core = require("@adhdev/session-host-core");
714
+ }
715
+ });
716
+
708
717
  // src/cli-adapters/pty-transport.ts
709
- var os7, pty, NodePtyRuntimeTransport, NodePtyTransportFactory;
718
+ function loadNodePty() {
719
+ if (cachedPty !== void 0) return cachedPty;
720
+ try {
721
+ cachedPty = require("node-pty");
722
+ (0, import_session_host_core.ensureNodePtySpawnHelperPermissions)();
723
+ } catch {
724
+ cachedPty = null;
725
+ }
726
+ return cachedPty;
727
+ }
728
+ var os7, cachedPty, NodePtyRuntimeTransport, NodePtyTransportFactory;
710
729
  var init_pty_transport = __esm({
711
730
  "src/cli-adapters/pty-transport.ts"() {
712
731
  "use strict";
713
732
  os7 = __toESM(require("os"));
714
- try {
715
- pty = require("node-pty");
716
- } catch {
717
- pty = null;
718
- }
733
+ init_spawn_env();
719
734
  NodePtyRuntimeTransport = class {
720
735
  constructor(handle) {
721
736
  this.handle = handle;
@@ -746,6 +761,7 @@ var init_pty_transport = __esm({
746
761
  };
747
762
  NodePtyTransportFactory = class {
748
763
  spawn(command, args, options) {
764
+ const pty = loadNodePty();
749
765
  if (!pty) throw new Error("node-pty is not installed");
750
766
  let cwd = options.cwd;
751
767
  if (cwd) {
@@ -770,15 +786,6 @@ var init_pty_transport = __esm({
770
786
  }
771
787
  });
772
788
 
773
- // src/cli-adapters/spawn-env.ts
774
- var import_session_host_core;
775
- var init_spawn_env = __esm({
776
- "src/cli-adapters/spawn-env.ts"() {
777
- "use strict";
778
- import_session_host_core = require("@adhdev/session-host-core");
779
- }
780
- });
781
-
782
789
  // src/cli-adapters/provider-cli-adapter.ts
783
790
  var provider_cli_adapter_exports = {};
784
791
  __export(provider_cli_adapter_exports, {
@@ -898,6 +905,40 @@ function normalizeScreenSnapshot(text) {
898
905
  function normalizeComparableMessageContent(text) {
899
906
  return String(text || "").replace(/\s+/g, " ").trim();
900
907
  }
908
+ function trimPromptEchoPrefix(text, promptText) {
909
+ const prompt = normalizeComparableMessageContent(String(promptText || ""));
910
+ if (!prompt) return String(text || "");
911
+ const lines = String(text || "").split(/\r\n|\n|\r/g);
912
+ let dropCount = 0;
913
+ for (let index = 0; index < Math.min(lines.length, 6); index += 1) {
914
+ const fragment = normalizeComparableMessageContent(lines[index].replace(/^[.…]+\s*/, ""));
915
+ if (!fragment) {
916
+ if (dropCount === index) dropCount = index + 1;
917
+ continue;
918
+ }
919
+ const fragmentWordCount = fragment ? fragment.split(/\s+/).filter(Boolean).length : 0;
920
+ const canBePromptEcho = fragment.length >= 16 || fragmentWordCount >= 4;
921
+ if (canBePromptEcho && prompt.includes(fragment)) {
922
+ dropCount = index + 1;
923
+ continue;
924
+ }
925
+ break;
926
+ }
927
+ return lines.slice(dropCount).join("\n").trim();
928
+ }
929
+ function getLastUserPromptText(messages) {
930
+ const items = Array.isArray(messages) ? messages : [];
931
+ for (let index = items.length - 1; index >= 0; index -= 1) {
932
+ const message = items[index];
933
+ if (message?.role === "user" && typeof message.content === "string" && message.content.trim()) {
934
+ return message.content;
935
+ }
936
+ }
937
+ return "";
938
+ }
939
+ function looksLikeConfirmOnlyLabel(label) {
940
+ return /^(?:continue|confirm|ok|yes|trust|proceed|enter)$/i.test(String(label || "").trim());
941
+ }
901
942
  function parsePatternEntry(x) {
902
943
  if (x instanceof RegExp) return x;
903
944
  if (x && typeof x === "object" && typeof x.source === "string") {
@@ -922,7 +963,7 @@ function normalizeCliProviderForRuntime(raw) {
922
963
  }
923
964
  };
924
965
  }
925
- var os8, path7, import_child_process4, pty2, buildCliSpawnEnv, ProviderCliAdapter;
966
+ var os8, path7, import_child_process4, buildCliSpawnEnv, ProviderCliAdapter;
926
967
  var init_provider_cli_adapter = __esm({
927
968
  "src/cli-adapters/provider-cli-adapter.ts"() {
928
969
  "use strict";
@@ -933,12 +974,6 @@ var init_provider_cli_adapter = __esm({
933
974
  init_terminal_screen();
934
975
  init_pty_transport();
935
976
  init_spawn_env();
936
- try {
937
- pty2 = require("node-pty");
938
- (0, import_session_host_core.ensureNodePtySpawnHelperPermissions)((msg) => LOG.info("CLI", msg));
939
- } catch {
940
- LOG.error("CLI", "[ProviderCliAdapter] node-pty not found. Terminal features disabled.");
941
- }
942
977
  buildCliSpawnEnv = import_session_host_core.sanitizeSpawnEnv;
943
978
  ProviderCliAdapter = class _ProviderCliAdapter {
944
979
  constructor(provider, workingDir, extraArgs = [], transportFactory = new NodePtyTransportFactory()) {
@@ -1040,6 +1075,8 @@ var init_provider_cli_adapter = __esm({
1040
1075
  submitRetryUsed = false;
1041
1076
  submitRetryPromptSnippet = "";
1042
1077
  idleFinishCandidate = null;
1078
+ finishRetryTimer = null;
1079
+ finishRetryCount = 0;
1043
1080
  // Resize redraw suppression
1044
1081
  resizeSuppressUntil = 0;
1045
1082
  // Debug: status transition history
@@ -1061,6 +1098,8 @@ var init_provider_cli_adapter = __esm({
1061
1098
  static MAX_TRACE_ENTRIES = 250;
1062
1099
  providerResolutionMeta;
1063
1100
  static IDLE_FINISH_CONFIRM_MS = 900;
1101
+ static FINISH_RETRY_DELAY_MS = 300;
1102
+ static MAX_FINISH_RETRIES = 2;
1064
1103
  syncMessageViews() {
1065
1104
  this.messages = [...this.committedMessages];
1066
1105
  this.structuredMessages = [...this.committedMessages];
@@ -1120,7 +1159,8 @@ var init_provider_cli_adapter = __esm({
1120
1159
  recentBuffer: buffer.slice(-1e3) || this.recentOutputBuffer,
1121
1160
  screenText: this.terminalScreen.getText(),
1122
1161
  messages: [...baseMessages],
1123
- partialResponse
1162
+ partialResponse,
1163
+ promptText: scope?.prompt || ""
1124
1164
  };
1125
1165
  }
1126
1166
  setStatus(status, trigger) {
@@ -1363,6 +1403,11 @@ var init_provider_cli_adapter = __esm({
1363
1403
  this.terminalScreen.reset(24, 80);
1364
1404
  this.pendingTerminalQueryTail = "";
1365
1405
  this.currentTurnScope = null;
1406
+ this.finishRetryCount = 0;
1407
+ if (this.finishRetryTimer) {
1408
+ clearTimeout(this.finishRetryTimer);
1409
+ this.finishRetryTimer = null;
1410
+ }
1366
1411
  this.ready = false;
1367
1412
  await this.ptyProcess.ready;
1368
1413
  this.recordTrace("ready", {
@@ -1409,11 +1454,12 @@ var init_provider_cli_adapter = __esm({
1409
1454
  if (this.startupParseGate) {
1410
1455
  this.startupBuffer += cleanData;
1411
1456
  const elapsed = Date.now() - this.spawnAt;
1412
- const scriptStatus = this.runDetectStatus(this.startupBuffer);
1413
1457
  const screenText = this.terminalScreen.getText() || "";
1458
+ const startupModal = this.getStartupConfirmationModal(screenText);
1459
+ const scriptStatus = startupModal ? "waiting_approval" : this.runDetectStatus(this.startupBuffer);
1414
1460
  const hasInteractivePrompt = this.looksLikeVisibleIdlePrompt(screenText);
1415
1461
  const startupStableMs = this.lastScreenChangeAt ? now - this.lastScreenChangeAt : 0;
1416
- const isReady = (scriptStatus === "idle" || scriptStatus === "waiting_approval") && hasInteractivePrompt && startupStableMs >= 700 || elapsed > 8e3 || this.startupBuffer.length > 12e3;
1462
+ const isReady = (scriptStatus === "idle" || scriptStatus === "waiting_approval") && hasInteractivePrompt && startupStableMs >= 700 || !!startupModal && startupStableMs >= 700 || elapsed > 8e3 || this.startupBuffer.length > 12e3;
1417
1463
  if (isReady) {
1418
1464
  this.startupParseGate = false;
1419
1465
  this.ready = true;
@@ -1445,7 +1491,9 @@ var init_provider_cli_adapter = __esm({
1445
1491
  this.approvalExitTimeout = setTimeout(() => {
1446
1492
  if (this.currentStatus !== "waiting_approval") return;
1447
1493
  const tail = this.recentOutputBuffer;
1448
- const modal = this.runParseApproval(tail);
1494
+ const screenText = this.terminalScreen.getText() || "";
1495
+ const startupModal = this.getStartupConfirmationModal(screenText);
1496
+ const modal = this.runParseApproval(tail) || startupModal;
1449
1497
  const stillWaiting = this.runDetectStatus(tail) === "waiting_approval" || !!modal;
1450
1498
  if (stillWaiting) {
1451
1499
  this.activeModal = modal || this.activeModal || { message: "Approval required", buttons: ["Allow", "Deny"] };
@@ -1465,6 +1513,63 @@ var init_provider_cli_adapter = __esm({
1465
1513
  if (!text.trim()) return false;
1466
1514
  return /(^|\n)\s*[❯›>]\s*(?:\n|$)/m.test(text) || /⏎\s+send/i.test(text) || /\?\s*for\s*shortcuts/i.test(text) || /Type your message(?:\s+or\s+@path\/to\/file)?/i.test(text) || /workspace\s*\(\/directory\)/i.test(text) || /for\s*shortcuts/i.test(text);
1467
1515
  }
1516
+ looksLikeVisibleAssistantCandidate(screenText) {
1517
+ const lines = sanitizeTerminalText(String(screenText || "")).split(/\r\n|\n|\r/g);
1518
+ for (const line of lines) {
1519
+ const trimmed = String(line || "").trim();
1520
+ if (!trimmed) continue;
1521
+ if (/^➜\s+\S+/.test(trimmed)) continue;
1522
+ if (/^Update available!/i.test(trimmed)) continue;
1523
+ if (/Claude Code v\d/i.test(trimmed)) continue;
1524
+ if (/^⏵⏵\s+accept edits on/i.test(trimmed)) continue;
1525
+ if (/^[◐◑◒◓◴◵◶◷◸◹◺◿].*\/effort/i.test(trimmed)) continue;
1526
+ if (/^[✻✶✳✢✽⠂⠐⠒⠓⠦⠴⠶⠷⠿]+$/.test(trimmed)) continue;
1527
+ if (/esc to (cancel|interrupt|stop)/i.test(trimmed)) continue;
1528
+ const assistantMatch = trimmed.match(/^⏺\s+(.+)$/);
1529
+ if (!assistantMatch) continue;
1530
+ const content = assistantMatch[1].trim();
1531
+ if (!content) continue;
1532
+ if (/^(?:Bash|Read|Write|Edit|MultiEdit|Task|Glob|Grep|LS|NotebookEdit)\(/.test(content)) continue;
1533
+ if (/This command requires approval|Do you want to proceed|Allow once|Always allow/i.test(content)) continue;
1534
+ return true;
1535
+ }
1536
+ return false;
1537
+ }
1538
+ shouldRetryFinishResponse(commitResult) {
1539
+ if (!this.currentTurnScope) return false;
1540
+ if (this.currentStatus === "waiting_approval" || this.activeModal) return false;
1541
+ if (this.finishRetryCount >= _ProviderCliAdapter.MAX_FINISH_RETRIES) return false;
1542
+ if (commitResult.hasAssistant && commitResult.assistantContent.trim()) return false;
1543
+ const screenText = this.terminalScreen.getText() || "";
1544
+ if (!this.looksLikeVisibleAssistantCandidate(screenText)) return false;
1545
+ const now = Date.now();
1546
+ const quietForMs = this.lastNonEmptyOutputAt ? now - this.lastNonEmptyOutputAt : Number.MAX_SAFE_INTEGER;
1547
+ const screenStableMs = this.lastScreenChangeAt ? now - this.lastScreenChangeAt : 0;
1548
+ return quietForMs < 1200 || screenStableMs < 1200 || !commitResult.hasAssistant;
1549
+ }
1550
+ getStartupConfirmationModal(screenText) {
1551
+ const text = sanitizeTerminalText(String(screenText || ""));
1552
+ if (!text.trim()) return null;
1553
+ if (this.cliType === "claude-cli") {
1554
+ const hasTrustPrompt = /Quick safety check/i.test(text) || /Is this a project you trust/i.test(text) || /Do you trust (?:this project|the contents of this directory|the files in this folder)/i.test(text);
1555
+ const hasConfirmFooter = /Press Enter to (?:continue|confirm)/i.test(text) || /Enter to confirm/i.test(text) || /Esc to (?:cancel|exit)/i.test(text);
1556
+ if (hasTrustPrompt || hasConfirmFooter && /trust/i.test(text)) {
1557
+ return {
1558
+ message: "Confirm Claude Code project trust",
1559
+ buttons: ["Continue"]
1560
+ };
1561
+ }
1562
+ }
1563
+ return null;
1564
+ }
1565
+ shouldResolveModalWithEnter(modal, buttonIndex) {
1566
+ if (!modal || buttonIndex !== 0) return false;
1567
+ const buttons = Array.isArray(modal.buttons) ? modal.buttons : [];
1568
+ if (buttons.length !== 1) return false;
1569
+ const buttonLabel = String(buttons[0] || "").trim();
1570
+ const modalText = `${modal.message || ""} ${buttonLabel}`.trim();
1571
+ return looksLikeConfirmOnlyLabel(buttonLabel) || /Quick safety check|project trust|trust (?:this project|the contents of this directory|the files in this folder)|Enter to confirm/i.test(modalText);
1572
+ }
1468
1573
  async waitForInteractivePrompt(maxWaitMs = 5e3) {
1469
1574
  const startedAt = Date.now();
1470
1575
  let loggedWait = false;
@@ -1514,9 +1619,10 @@ var init_provider_cli_adapter = __esm({
1514
1619
  }
1515
1620
  const tail = this.settledBuffer;
1516
1621
  const screenText = this.terminalScreen.getText() || "";
1517
- const modal = this.runParseApproval(tail);
1622
+ const startupModal = this.getStartupConfirmationModal(screenText);
1623
+ const modal = this.runParseApproval(tail) || startupModal;
1518
1624
  const rawScriptStatus = this.runDetectStatus(tail);
1519
- const scriptStatus = rawScriptStatus;
1625
+ const scriptStatus = startupModal ? "waiting_approval" : rawScriptStatus;
1520
1626
  const parsedTranscript = this.parseCurrentTranscript(
1521
1627
  this.committedMessages,
1522
1628
  this.responseBuffer,
@@ -1711,7 +1817,24 @@ var init_provider_cli_adapter = __esm({
1711
1817
  this.recordTrace("finish_response", {
1712
1818
  ...this.buildTraceParseSnapshot(this.currentTurnScope, this.responseBuffer)
1713
1819
  });
1714
- this.commitCurrentTranscript();
1820
+ const commitResult = this.commitCurrentTranscript();
1821
+ if (this.shouldRetryFinishResponse(commitResult)) {
1822
+ this.finishRetryCount += 1;
1823
+ this.recordTrace("finish_response_retry", {
1824
+ retryCount: this.finishRetryCount,
1825
+ retryDelayMs: _ProviderCliAdapter.FINISH_RETRY_DELAY_MS,
1826
+ assistantContent: this.summarizeTraceText(commitResult.assistantContent, 220),
1827
+ ...this.buildTraceParseSnapshot(this.currentTurnScope, this.responseBuffer)
1828
+ });
1829
+ if (this.finishRetryTimer) clearTimeout(this.finishRetryTimer);
1830
+ this.finishRetryTimer = setTimeout(() => {
1831
+ this.finishRetryTimer = null;
1832
+ if (this.isWaitingForResponse && this.currentStatus !== "waiting_approval") {
1833
+ this.finishResponse();
1834
+ }
1835
+ }, _ProviderCliAdapter.FINISH_RETRY_DELAY_MS);
1836
+ return;
1837
+ }
1715
1838
  if (this.responseTimeout) {
1716
1839
  clearTimeout(this.responseTimeout);
1717
1840
  this.responseTimeout = null;
@@ -1728,11 +1851,16 @@ var init_provider_cli_adapter = __esm({
1728
1851
  clearTimeout(this.submitRetryTimer);
1729
1852
  this.submitRetryTimer = null;
1730
1853
  }
1854
+ if (this.finishRetryTimer) {
1855
+ clearTimeout(this.finishRetryTimer);
1856
+ this.finishRetryTimer = null;
1857
+ }
1731
1858
  this.responseBuffer = "";
1732
1859
  this.isWaitingForResponse = false;
1733
1860
  this.responseSettleIgnoreUntil = 0;
1734
1861
  this.submitRetryUsed = false;
1735
1862
  this.submitRetryPromptSnippet = "";
1863
+ this.finishRetryCount = 0;
1736
1864
  this.currentTurnScope = null;
1737
1865
  this.activeModal = null;
1738
1866
  this.setStatus("idle", "response_finished");
@@ -1746,6 +1874,13 @@ var init_provider_cli_adapter = __esm({
1746
1874
  );
1747
1875
  if (parsed && Array.isArray(parsed.messages)) {
1748
1876
  this.committedMessages = this.normalizeParsedMessages(parsed.messages);
1877
+ const promptForTrim = this.currentTurnScope?.prompt || getLastUserPromptText(this.committedMessages);
1878
+ if (promptForTrim) {
1879
+ const lastAssistantForTrim = [...this.committedMessages].reverse().find((message) => message.role === "assistant");
1880
+ if (lastAssistantForTrim) {
1881
+ lastAssistantForTrim.content = trimPromptEchoPrefix(lastAssistantForTrim.content, promptForTrim);
1882
+ }
1883
+ }
1749
1884
  this.syncMessageViews();
1750
1885
  const lastAssistant = [...this.committedMessages].reverse().find((message) => message.role === "assistant");
1751
1886
  this.recordTrace("commit_transcript", {
@@ -1761,7 +1896,15 @@ var init_provider_cli_adapter = __esm({
1761
1896
  `[${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 || "-"}`
1762
1897
  );
1763
1898
  }
1899
+ return {
1900
+ hasAssistant: !!lastAssistant,
1901
+ assistantContent: lastAssistant?.content || ""
1902
+ };
1764
1903
  }
1904
+ return {
1905
+ hasAssistant: false,
1906
+ assistantContent: ""
1907
+ };
1765
1908
  }
1766
1909
  // ─── Script Execution ──────────────────────────
1767
1910
  runDetectStatus(text) {
@@ -1840,7 +1983,15 @@ var init_provider_cli_adapter = __esm({
1840
1983
  if (!this.cliScripts?.parseOutput) return null;
1841
1984
  try {
1842
1985
  const input = this.buildParseInput(baseMessages, partialResponse, scope);
1843
- return this.cliScripts.parseOutput(input);
1986
+ const parsed = this.cliScripts.parseOutput(input);
1987
+ const promptForTrim = scope?.prompt || getLastUserPromptText(baseMessages);
1988
+ if (parsed && Array.isArray(parsed.messages) && promptForTrim) {
1989
+ const lastAssistant = [...parsed.messages].reverse().find((message) => message?.role === "assistant" && typeof message.content === "string");
1990
+ if (lastAssistant) {
1991
+ lastAssistant.content = trimPromptEchoPrefix(lastAssistant.content, promptForTrim);
1992
+ }
1993
+ }
1994
+ return parsed;
1844
1995
  } catch (e) {
1845
1996
  LOG.warn("CLI", `[${this.cliType}] parseOutput error: ${e.message}`);
1846
1997
  return null;
@@ -1885,10 +2036,19 @@ ${data.message || ""}`.trim();
1885
2036
  if (!this.ready) throw new Error(`${this.cliName} not ready (status: ${this.currentStatus})`);
1886
2037
  if (this.isWaitingForResponse) return;
1887
2038
  await this.waitForInteractivePrompt();
2039
+ const blockingModal = this.activeModal || this.getStartupConfirmationModal(this.terminalScreen.getText() || "");
2040
+ if (blockingModal || this.currentStatus === "waiting_approval") {
2041
+ throw new Error(`${this.cliName} is awaiting confirmation before it can accept a prompt`);
2042
+ }
1888
2043
  this.committedMessages.push({ role: "user", content: text, timestamp: Date.now() });
1889
2044
  this.syncMessageViews();
1890
2045
  this.isWaitingForResponse = true;
1891
2046
  this.responseBuffer = "";
2047
+ this.finishRetryCount = 0;
2048
+ if (this.finishRetryTimer) {
2049
+ clearTimeout(this.finishRetryTimer);
2050
+ this.finishRetryTimer = null;
2051
+ }
1892
2052
  this.clearIdleFinishCandidate("send_message");
1893
2053
  this.currentTurnScope = {
1894
2054
  prompt: text,
@@ -2116,6 +2276,10 @@ ${data.message || ""}`.trim();
2116
2276
  clearTimeout(this.submitRetryTimer);
2117
2277
  this.submitRetryTimer = null;
2118
2278
  }
2279
+ if (this.finishRetryTimer) {
2280
+ clearTimeout(this.finishRetryTimer);
2281
+ this.finishRetryTimer = null;
2282
+ }
2119
2283
  if (this.responseTimeout) {
2120
2284
  clearTimeout(this.responseTimeout);
2121
2285
  this.responseTimeout = null;
@@ -2139,6 +2303,7 @@ ${data.message || ""}`.trim();
2139
2303
  this.ptyOutputFlushTimer = null;
2140
2304
  }
2141
2305
  this.ptyOutputBuffer = "";
2306
+ this.finishRetryCount = 0;
2142
2307
  if (this.ptyProcess) {
2143
2308
  this.ptyProcess.write("");
2144
2309
  setTimeout(() => {
@@ -2169,6 +2334,10 @@ ${data.message || ""}`.trim();
2169
2334
  clearTimeout(this.submitRetryTimer);
2170
2335
  this.submitRetryTimer = null;
2171
2336
  }
2337
+ if (this.finishRetryTimer) {
2338
+ clearTimeout(this.finishRetryTimer);
2339
+ this.finishRetryTimer = null;
2340
+ }
2172
2341
  if (this.responseTimeout) {
2173
2342
  clearTimeout(this.responseTimeout);
2174
2343
  this.responseTimeout = null;
@@ -2192,6 +2361,7 @@ ${data.message || ""}`.trim();
2192
2361
  this.ptyOutputFlushTimer = null;
2193
2362
  }
2194
2363
  this.ptyOutputBuffer = "";
2364
+ this.finishRetryCount = 0;
2195
2365
  if (this.ptyProcess) {
2196
2366
  try {
2197
2367
  if (typeof this.ptyProcess.detach === "function") {
@@ -2228,6 +2398,11 @@ ${data.message || ""}`.trim();
2228
2398
  this.ptyOutputFlushTimer = null;
2229
2399
  }
2230
2400
  this.ptyOutputBuffer = "";
2401
+ if (this.finishRetryTimer) {
2402
+ clearTimeout(this.finishRetryTimer);
2403
+ this.finishRetryTimer = null;
2404
+ }
2405
+ this.finishRetryCount = 0;
2231
2406
  this.terminalScreen.reset();
2232
2407
  this.ptyProcess?.clearBuffer?.();
2233
2408
  this.onStatusChange?.();
@@ -2247,10 +2422,11 @@ ${data.message || ""}`.trim();
2247
2422
  }
2248
2423
  resolveModal(buttonIndex) {
2249
2424
  if (!this.ptyProcess || this.currentStatus !== "waiting_approval" && !this.activeModal) return;
2425
+ const modal = this.activeModal;
2250
2426
  this.clearIdleFinishCandidate("resolve_modal");
2251
2427
  this.recordTrace("resolve_modal", {
2252
2428
  buttonIndex,
2253
- activeModal: this.activeModal
2429
+ activeModal: modal
2254
2430
  });
2255
2431
  this.activeModal = null;
2256
2432
  this.lastApprovalResolvedAt = Date.now();
@@ -2261,7 +2437,9 @@ ${data.message || ""}`.trim();
2261
2437
  }
2262
2438
  this.setStatus("generating", "approval_resolved");
2263
2439
  this.onStatusChange?.();
2264
- if (buttonIndex in this.approvalKeys) {
2440
+ if (this.shouldResolveModalWithEnter(modal, buttonIndex)) {
2441
+ this.ptyProcess.write("\r");
2442
+ } else if (buttonIndex in this.approvalKeys) {
2265
2443
  this.ptyProcess.write(this.approvalKeys[buttonIndex]);
2266
2444
  } else {
2267
2445
  const DOWN = "\x1B[B";
@@ -11673,7 +11851,10 @@ function appendUpgradeLog(message) {
11673
11851
  }
11674
11852
  }
11675
11853
  function getNpmExecutable() {
11676
- return process.platform === "win32" ? "npm.cmd" : "npm";
11854
+ return "npm";
11855
+ }
11856
+ function getNpmExecOptions() {
11857
+ return { shell: process.platform === "win32" };
11677
11858
  }
11678
11859
  function killPid(pid) {
11679
11860
  try {
@@ -11735,9 +11916,10 @@ function removeDaemonPidFile() {
11735
11916
  }
11736
11917
  }
11737
11918
  function cleanupStaleGlobalInstallDirs(pkgName) {
11738
- const npmRoot = (0, import_child_process7.execFileSync)(getNpmExecutable(), ["root", "-g"], { encoding: "utf8" }).trim();
11919
+ const npmExecOpts = getNpmExecOptions();
11920
+ const npmRoot = (0, import_child_process7.execFileSync)(getNpmExecutable(), ["root", "-g"], { encoding: "utf8", ...npmExecOpts }).trim();
11739
11921
  if (!npmRoot) return;
11740
- const npmPrefix = (0, import_child_process7.execFileSync)(getNpmExecutable(), ["prefix", "-g"], { encoding: "utf8" }).trim();
11922
+ const npmPrefix = (0, import_child_process7.execFileSync)(getNpmExecutable(), ["prefix", "-g"], { encoding: "utf8", ...npmExecOpts }).trim();
11741
11923
  const binDir = process.platform === "win32" ? npmPrefix : path13.join(npmPrefix, "bin");
11742
11924
  const packageBaseName = pkgName.startsWith("@") ? pkgName.split("/")[1] : pkgName;
11743
11925
  const binNames = /* @__PURE__ */ new Set([packageBaseName]);
@@ -11798,7 +11980,8 @@ async function runDaemonUpgradeHelper(payload) {
11798
11980
  {
11799
11981
  encoding: "utf8",
11800
11982
  stdio: "pipe",
11801
- maxBuffer: 20 * 1024 * 1024
11983
+ maxBuffer: 20 * 1024 * 1024,
11984
+ ...getNpmExecOptions()
11802
11985
  }
11803
11986
  );
11804
11987
  if (installOutput.trim()) {
@@ -15020,6 +15203,53 @@ async function runCliExerciseInternal(ctx, body) {
15020
15203
  let lastModalKey = "";
15021
15204
  let idleSince = 0;
15022
15205
  let sawBusy = false;
15206
+ const noteStatus = (status) => {
15207
+ if (status !== lastStatus) {
15208
+ statusesSeen.push(status);
15209
+ lastStatus = status;
15210
+ }
15211
+ };
15212
+ const resolveActiveModalIfNeeded = (status, modal) => {
15213
+ if (!autoResolveApprovals || status !== "waiting_approval" || !modal || !Array.isArray(modal.buttons) || modal.buttons.length === 0) {
15214
+ return false;
15215
+ }
15216
+ const clampedIndex = Math.max(0, Math.min(Number(approvalButtonIndex) || 0, modal.buttons.length - 1));
15217
+ const modalKey = JSON.stringify({
15218
+ message: modal.message || "",
15219
+ buttons: modal.buttons,
15220
+ index: clampedIndex
15221
+ });
15222
+ if (modalKey === lastModalKey || typeof bundle?.adapter?.resolveModal !== "function") {
15223
+ return false;
15224
+ }
15225
+ lastModalKey = modalKey;
15226
+ approvalsResolved.push({
15227
+ at: Date.now(),
15228
+ buttonIndex: clampedIndex,
15229
+ label: modal.buttons[clampedIndex] || null
15230
+ });
15231
+ bundle.adapter.resolveModal(clampedIndex);
15232
+ return true;
15233
+ };
15234
+ const preflightStartedAt = Date.now();
15235
+ while (Date.now() - preflightStartedAt < Math.max(1e3, readyTimeoutMs)) {
15236
+ bundle = getCliTargetBundle(ctx, type, bundle.target.instanceId);
15237
+ if (!bundle) {
15238
+ throw new Error("CLI instance disappeared before exercise send");
15239
+ }
15240
+ const debug = typeof bundle.adapter.getDebugState === "function" ? bundle.adapter.getDebugState() : null;
15241
+ const trace = typeof bundle.adapter.getTraceState === "function" ? bundle.adapter.getTraceState(traceLimit) : null;
15242
+ const status = String(debug?.status || bundle.target.status || "unknown");
15243
+ const modal = debug?.activeModal || trace?.activeModal || null;
15244
+ noteStatus(status);
15245
+ if (resolveActiveModalIfNeeded(status, modal)) {
15246
+ await sleep(150);
15247
+ continue;
15248
+ }
15249
+ const startupParseGate = !!debug?.startupParseGate;
15250
+ if (status === "idle" && !startupParseGate) break;
15251
+ await sleep(150);
15252
+ }
15023
15253
  ctx.instanceManager.sendEvent(bundle.target.instanceId, "send_message", { text });
15024
15254
  while (Date.now() - startAt < Math.max(1e3, timeoutMs)) {
15025
15255
  await sleep(150);
@@ -15034,32 +15264,14 @@ async function runCliExerciseInternal(ctx, body) {
15034
15264
  const sawSendMessage = traceEntries.some((entry) => entry?.type === "send_message");
15035
15265
  const sawSubmitWrite = traceEntries.some((entry) => entry?.type === "submit_write");
15036
15266
  const hasTurnStarted = sawSendMessage || sawSubmitWrite || !!debug?.currentTurnScope;
15037
- if (status !== lastStatus) {
15038
- statusesSeen.push(status);
15039
- lastStatus = status;
15040
- }
15267
+ noteStatus(status);
15041
15268
  if (status === "generating" || status === "waiting_approval") {
15042
15269
  sawBusy = true;
15043
15270
  idleSince = 0;
15044
15271
  }
15045
15272
  const modal = debug?.activeModal || trace?.activeModal || null;
15046
- if (autoResolveApprovals && status === "waiting_approval" && modal && Array.isArray(modal.buttons) && modal.buttons.length > 0) {
15047
- const clampedIndex = Math.max(0, Math.min(Number(approvalButtonIndex) || 0, modal.buttons.length - 1));
15048
- const modalKey = JSON.stringify({
15049
- message: modal.message || "",
15050
- buttons: modal.buttons,
15051
- index: clampedIndex
15052
- });
15053
- if (modalKey !== lastModalKey && typeof bundle.adapter.resolveModal === "function") {
15054
- lastModalKey = modalKey;
15055
- approvalsResolved.push({
15056
- at: Date.now(),
15057
- buttonIndex: clampedIndex,
15058
- label: modal.buttons[clampedIndex] || null
15059
- });
15060
- bundle.adapter.resolveModal(clampedIndex);
15061
- continue;
15062
- }
15273
+ if (resolveActiveModalIfNeeded(status, modal)) {
15274
+ continue;
15063
15275
  }
15064
15276
  const traceCount = Number(trace?.entryCount || 0);
15065
15277
  const hasProgress = hasTurnStarted && (traceCount > preTraceCount || statusesSeen.length > 1 || approvalsResolved.length > 0);
@@ -16055,10 +16267,10 @@ async function handleAutoImplement(ctx, type, req, res) {
16055
16267
  let isPty = false;
16056
16268
  const { spawn: spawnFn } = await import("child_process");
16057
16269
  try {
16058
- const pty3 = require("node-pty");
16270
+ const pty = require("node-pty");
16059
16271
  ctx.log(`Auto-implement spawn (PTY): ${shellCmd}`);
16060
16272
  const isWin2 = os17.platform() === "win32";
16061
- child = pty3.spawn(isWin2 ? "cmd.exe" : process.env.SHELL || "/bin/zsh", [isWin2 ? "/c" : "-c", shellCmd], {
16273
+ child = pty.spawn(isWin2 ? "cmd.exe" : process.env.SHELL || "/bin/zsh", [isWin2 ? "/c" : "-c", shellCmd], {
16062
16274
  name: "xterm-256color",
16063
16275
  cols: 120,
16064
16276
  rows: 40,
@@ -16714,6 +16926,13 @@ function buildCliAutoImplPrompt(ctx, type, provider, providerDir, functions, ref
16714
16926
  lines.push("19. Literal string checks are allowed only for stable proper nouns or exact product chrome that cannot be expressed safely as a broader pattern. Everything else should generalize.");
16715
16927
  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.");
16716
16928
  lines.push("");
16929
+ if (verification?.focusAreas?.length) {
16930
+ lines.push("## Provider-Specific Focus Areas");
16931
+ for (const area of verification.focusAreas) {
16932
+ lines.push(`- ${area}`);
16933
+ }
16934
+ lines.push("");
16935
+ }
16717
16936
  lines.push("## Task");
16718
16937
  lines.push(`Edit files in \`${providerDir}\` to implement: **${functions.join(", ")}**`);
16719
16938
  lines.push("");