@trenchwork/erosolar 1.1.62 → 1.1.63

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 (70) hide show
  1. package/dist/capabilities/todoCapability.js +2 -2
  2. package/dist/capabilities/todoCapability.js.map +1 -1
  3. package/dist/config.js +1 -1
  4. package/dist/contracts/v1/agent.d.ts +12 -2
  5. package/dist/contracts/v1/agent.d.ts.map +1 -1
  6. package/dist/core/adversarialCorrection.d.ts +22 -0
  7. package/dist/core/adversarialCorrection.d.ts.map +1 -0
  8. package/dist/core/adversarialCorrection.js +25 -0
  9. package/dist/core/adversarialCorrection.js.map +1 -0
  10. package/dist/core/agent.d.ts +2 -0
  11. package/dist/core/agent.d.ts.map +1 -1
  12. package/dist/core/agent.js +11 -1
  13. package/dist/core/agent.js.map +1 -1
  14. package/dist/core/failureRegistry.d.ts +30 -0
  15. package/dist/core/failureRegistry.d.ts.map +1 -0
  16. package/dist/core/failureRegistry.js +74 -0
  17. package/dist/core/failureRegistry.js.map +1 -0
  18. package/dist/core/hostedAuth.d.ts +55 -0
  19. package/dist/core/hostedAuth.d.ts.map +1 -0
  20. package/dist/core/hostedAuth.js +134 -0
  21. package/dist/core/hostedAuth.js.map +1 -0
  22. package/dist/core/secretStore.d.ts +11 -0
  23. package/dist/core/secretStore.d.ts.map +1 -1
  24. package/dist/core/secretStore.js +25 -0
  25. package/dist/core/secretStore.js.map +1 -1
  26. package/dist/core/slashCommands.d.ts.map +1 -1
  27. package/dist/core/slashCommands.js +1 -0
  28. package/dist/core/slashCommands.js.map +1 -1
  29. package/dist/core/thinkingVerbs.d.ts +31 -0
  30. package/dist/core/thinkingVerbs.d.ts.map +1 -0
  31. package/dist/core/thinkingVerbs.js +58 -0
  32. package/dist/core/thinkingVerbs.js.map +1 -0
  33. package/dist/core/turnGovernor.d.ts +63 -0
  34. package/dist/core/turnGovernor.d.ts.map +1 -0
  35. package/dist/core/turnGovernor.js +94 -0
  36. package/dist/core/turnGovernor.js.map +1 -0
  37. package/dist/headless/interactiveShell.d.ts +4 -0
  38. package/dist/headless/interactiveShell.d.ts.map +1 -1
  39. package/dist/headless/interactiveShell.js +168 -38
  40. package/dist/headless/interactiveShell.js.map +1 -1
  41. package/dist/plugins/tools/todo/todoPlugin.js +1 -1
  42. package/dist/plugins/tools/todo/todoPlugin.js.map +1 -1
  43. package/dist/runtime/agentController.d.ts.map +1 -1
  44. package/dist/runtime/agentController.js +7 -0
  45. package/dist/runtime/agentController.js.map +1 -1
  46. package/dist/shell/toolPresentation.d.ts +7 -0
  47. package/dist/shell/toolPresentation.d.ts.map +1 -1
  48. package/dist/shell/toolPresentation.js +51 -1
  49. package/dist/shell/toolPresentation.js.map +1 -1
  50. package/dist/tools/memoryTools.d.ts +7 -0
  51. package/dist/tools/memoryTools.d.ts.map +1 -1
  52. package/dist/tools/memoryTools.js +17 -0
  53. package/dist/tools/memoryTools.js.map +1 -1
  54. package/dist/tools/todoTools.d.ts +3 -4
  55. package/dist/tools/todoTools.d.ts.map +1 -1
  56. package/dist/tools/todoTools.js +23 -4
  57. package/dist/tools/todoTools.js.map +1 -1
  58. package/dist/ui/ink/InkPromptController.d.ts +2 -0
  59. package/dist/ui/ink/InkPromptController.d.ts.map +1 -1
  60. package/dist/ui/ink/InkPromptController.js +4 -0
  61. package/dist/ui/ink/InkPromptController.js.map +1 -1
  62. package/dist/ui/ink/Prompt.d.ts +2 -0
  63. package/dist/ui/ink/Prompt.d.ts.map +1 -1
  64. package/dist/ui/ink/Prompt.js +8 -1
  65. package/dist/ui/ink/Prompt.js.map +1 -1
  66. package/dist/ui/ink/StatusLine.d.ts +6 -0
  67. package/dist/ui/ink/StatusLine.d.ts.map +1 -1
  68. package/dist/ui/ink/StatusLine.js +17 -3
  69. package/dist/ui/ink/StatusLine.js.map +1 -1
  70. package/package.json +1 -1
@@ -13,9 +13,8 @@
13
13
  * - Ctrl+C to interrupt
14
14
  */
15
15
  import { stdin, stdout, exit } from 'node:process';
16
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
17
- import { resolve, dirname, join, relative } from 'node:path';
18
- import { homedir } from 'node:os';
16
+ import { readFileSync } from 'node:fs';
17
+ import { resolve, dirname, relative } from 'node:path';
19
18
  import { fileURLToPath } from 'node:url';
20
19
  import { exec as childExec } from 'node:child_process';
21
20
  import { promisify } from 'node:util';
@@ -32,7 +31,9 @@ import { resolveProfileConfig } from '../config.js';
32
31
  import { createAgentController } from '../runtime/agentController.js';
33
32
  import { expandFileMentions, listWorkspaceFiles } from '../core/fileMentions.js';
34
33
  import { resolveWorkspaceCaptureOptions, buildWorkspaceContext } from '../workspace.js';
35
- import { loadAllSecrets, listSecretDefinitions, setSecretValue, getSecretValue } from '../core/secretStore.js';
34
+ import { loadAllSecrets, listSecretDefinitions, setSecretValue, getSecretValue, getSecretDefinition, classifyKeyEntry } from '../core/secretStore.js';
35
+ import { resolveKeyMode, keyModeLine, setPreferOwnKeys, clearHostedSession } from '../core/hostedAuth.js';
36
+ import { appendMemoryNote } from '../tools/memoryTools.js';
36
37
  import { listSessions, loadSessionById, saveSessionSnapshot } from '../core/sessionStore.js';
37
38
  import { relativeTime } from '../core/relativeTime.js';
38
39
  import { getModelContextInfo } from '../core/contextWindow.js';
@@ -48,6 +49,10 @@ import { setDebugMode, debugSnippet } from '../utils/debugLogger.js';
48
49
  const exec = promisify(childExec);
49
50
  import { ensureNextSteps } from '../core/finalResponseFormatter.js';
50
51
  import { getTaskCompletionDetector, detectFailingTestOrBuild } from '../core/taskCompletionDetector.js';
52
+ import { TurnGovernor, pendingTodos, nextTodoPrompt } from '../core/turnGovernor.js';
53
+ import { FailureRegistry } from '../core/failureRegistry.js';
54
+ import { buildAdversarialCorrectionPrompt, MAX_ADVERSARIAL_CORRECTIONS } from '../core/adversarialCorrection.js';
55
+ import { getCurrentTodos } from '../tools/todoTools.js';
51
56
  import { checkForUpdates, performBackgroundUpdate } from '../core/updateChecker.js';
52
57
  import { startNewRun } from '../tools/fileChangeTracker.js';
53
58
  import { onSudoPasswordNeeded, offSudoPasswordNeeded, provideSudoPassword } from '../core/sudoPasswordManager.js';
@@ -144,12 +149,18 @@ function getVersion() {
144
149
  /** Inner content of the welcome box (plain, no border/colour). */
145
150
  function welcomeBodyLines(input) {
146
151
  const body = ['✻ Welcome to Erosolar Coder', ''];
147
- if (!input.hasApiKey) {
148
- body.push('⚠ No API key configured', '', 'Get your key: https://platform.deepseek.com/', 'Set your key: /key YOUR_API_KEY');
152
+ const mode = input.keyMode ?? (input.hasApiKey ? 'own' : 'none');
153
+ if (mode === 'hosted') {
154
+ // Signed in — running on hosted keys. The mode line names the account so
155
+ // it's unmistakable this is NOT the user's own key.
156
+ body.push(input.keyModeLine ?? 'Signed in · using hosted keys');
149
157
  }
150
- else {
158
+ else if (mode === 'own') {
151
159
  body.push(`${input.model} · ${input.provider}`, `Key: ${input.maskedKey} · /help for commands`);
152
160
  }
161
+ else {
162
+ body.push('⚠ No DeepSeek API key configured', '', 'Bring your own keys:', ' /key sk-… DeepSeek (required) · platform.deepseek.com', ' /key tvly-… Tavily web search (optional) · tavily.com');
163
+ }
153
164
  if (input.cwd)
154
165
  body.push(`cwd: ${input.cwd}`);
155
166
  return body;
@@ -275,6 +286,17 @@ class InteractiveShell {
275
286
  // Store original prompt for auto-continuation
276
287
  originalPromptForAutoContinue = null;
277
288
  // (Pinned prompt removed per request — field intentionally absent.)
289
+ // Bounds + stall-detects the auto-continue loop per user request, and drives
290
+ // continuation from the live TODO plan (see src/core/turnGovernor.ts). Reset
291
+ // when a fresh user prompt arrives.
292
+ autoGovernor = new TurnGovernor();
293
+ // Remembers recurring error signatures across auto-continue turns so the
294
+ // agent stops re-trying the same dead end (see src/core/failureRegistry.ts).
295
+ failureRegistry = new FailureRegistry();
296
+ // Adversarial auto-correction: how many bounded re-fixes the reviewer has
297
+ // triggered for the CURRENT user request (capped). Reset on a fresh prompt;
298
+ // the findings themselves are a per-turn local in processPrompt.
299
+ adversarialCorrectionCount = 0;
278
300
  constructor(controller, profile, profileConfig, workingDir) {
279
301
  this.controller = controller;
280
302
  this.profile = profile;
@@ -345,6 +367,7 @@ class InteractiveShell {
345
367
  // Esc interrupts a running turn (handleInterrupt no-ops when idle), so
346
368
  // the spinner's "esc to interrupt" is real. Ctrl+C still works too.
347
369
  onEscape: () => this.handleInterrupt(),
370
+ onShowShortcuts: () => this.showKeyboardShortcuts(),
348
371
  });
349
372
  // Register cleanup callback for graceful shutdown
350
373
  onShutdown(() => {
@@ -492,12 +515,15 @@ class InteractiveShell {
492
515
  // WHICH lines appear; here we draw the same box with brand colour.
493
516
  const flare = chalk.hex('#ff6a1f');
494
517
  const wire = chalk.hex('#3a362e');
518
+ const keyStatus = resolveKeyMode();
495
519
  const body = welcomeBodyLines({
496
520
  hasApiKey,
497
521
  maskedKey: hasApiKey ? maskApiKey(apiKey) : '',
498
522
  model: this.profileConfig.model,
499
523
  provider: this.profileConfig.provider,
500
524
  cwd: this.workingDir,
525
+ keyMode: keyStatus.mode,
526
+ keyModeLine: keyModeLine(keyStatus),
501
527
  });
502
528
  const boxed = roundedBox(body, (cell) => cell.replace('✻', flare('✻')), (s) => wire(s));
503
529
  const welcomeContent = ['', ...updateLines, ...boxed, ''].join('\n');
@@ -791,26 +817,19 @@ class InteractiveShell {
791
817
  const lower = trimmed.toLowerCase();
792
818
  // /model and /secrets were removed: Erosolar is locked to deepseek-v4-pro
793
819
  // on max thought (no model switching), and /key is the one key you set.
794
- // Handle /key - shortcut to set DEEPSEEK_API_KEY
820
+ // Handle /key set your own DeepSeek OR Tavily API key. Routed by prefix:
821
+ // `sk-…` → DeepSeek (the model), `tvly-…` → Tavily (web search). Explicit
822
+ // `/key tavily <k>` / `/key deepseek <k>` also work. Bring-your-own-key is
823
+ // the model; both are stored in the OS-permission secret store.
795
824
  if (lower === '/key' || lower.startsWith('/key ')) {
796
- const parts = trimmed.split(/\s+/);
797
- const keyValue = parts[1];
798
825
  const renderer = this.promptController?.getRenderer();
799
- if (keyValue) {
800
- // Direct file write - most reliable method
826
+ const arg = trimmed.slice('/key'.length).trim();
827
+ const entry = classifyKeyEntry(arg);
828
+ if (entry) {
801
829
  try {
802
- const secretDir = join(homedir(), '.erosolar');
803
- const secretFile = join(secretDir, 'secrets.json');
804
- mkdirSync(secretDir, { recursive: true });
805
- const existing = existsSync(secretFile)
806
- ? JSON.parse(readFileSync(secretFile, 'utf-8'))
807
- : {};
808
- existing['DEEPSEEK_API_KEY'] = keyValue;
809
- writeFileSync(secretFile, JSON.stringify(existing, null, 2) + '\n');
810
- // Also set in process.env for immediate use
811
- process.env['DEEPSEEK_API_KEY'] = keyValue;
812
- // Show confirmation via renderer
813
- renderer?.addEvent('system', chalk.green('✓ DEEPSEEK_API_KEY saved'));
830
+ setSecretValue(entry.id, entry.value);
831
+ const label = getSecretDefinition(entry.id)?.label ?? entry.id;
832
+ renderer?.addEvent('system', chalk.green(`✓ ${label} saved`));
814
833
  }
815
834
  catch (error) {
816
835
  const msg = error instanceof Error ? error.message : String(error);
@@ -818,11 +837,33 @@ class InteractiveShell {
818
837
  }
819
838
  }
820
839
  else {
821
- // Show usage hint
822
- renderer?.addEvent('system', chalk.yellow('Usage: /key YOUR_API_KEY'));
840
+ renderer?.addEvent('system', chalk.yellow('Usage: /key sk-… (DeepSeek) or /key tvly-… (Tavily web search)'));
823
841
  }
824
842
  return true;
825
843
  }
844
+ // /account — show the active key source (hosted vs your own) and switch
845
+ // between them. `/account own` forces your own keys even while signed in;
846
+ // `/account hosted` returns to hosted. Hosted keys come from sign-in
847
+ // (server-side, never baked into this client) — see core/hostedAuth.ts.
848
+ if (lower === '/account' || lower.startsWith('/account ')) {
849
+ const r = this.promptController?.getRenderer();
850
+ const arg = trimmed.slice('/account'.length).trim().toLowerCase();
851
+ if (arg === 'own')
852
+ setPreferOwnKeys(true);
853
+ else if (arg === 'hosted')
854
+ setPreferOwnKeys(false);
855
+ if (arg === 'own' || arg === 'hosted')
856
+ void this.showWelcome(); // banner reflects the switch
857
+ r?.addEvent('system', this.accountStatusText(resolveKeyMode()));
858
+ return true;
859
+ }
860
+ // /logout — drop the hosted session (back to your own keys, or none).
861
+ if (lower === '/logout' || lower === '/signout') {
862
+ clearHostedSession();
863
+ this.promptController?.getRenderer()?.addEvent('system', chalk.green('✓ Signed out — using your own keys.'));
864
+ void this.showWelcome();
865
+ return true;
866
+ }
826
867
  // /update — check npm for a newer version and upgrade in-shell.
827
868
  if (lower === '/update' || lower === '/upgrade') {
828
869
  void this.handleUpdateCommand();
@@ -1489,6 +1530,19 @@ class InteractiveShell {
1489
1530
  revertAllChanges(this.workingDir); // restores/deletes on disk + clears tracking
1490
1531
  renderer?.addEvent('system', chalk.green('✓ ' + rewindResultLine(restored, deleted)));
1491
1532
  }
1533
+ /** One-line summary of the active key source for /account. */
1534
+ accountStatusText(s) {
1535
+ if (s.mode === 'hosted') {
1536
+ return chalk.green(`Hosted keys · signed in as ${s.email}.`) +
1537
+ chalk.dim(` /account own to use your own · /logout to sign out.`);
1538
+ }
1539
+ if (s.mode === 'own') {
1540
+ return chalk.green(`Your own keys · DeepSeek${s.ownTavily ? ' + Tavily' : ''}.`) +
1541
+ chalk.dim(s.signedIn ? ` /account hosted to use hosted keys.` : ` Sign-in for hosted keys is coming.`);
1542
+ }
1543
+ return chalk.yellow('No keys configured.') +
1544
+ chalk.dim(' Set your own: /key sk-… (and /key tvly-…). Hosted sign-in is coming.');
1545
+ }
1492
1546
  showHelp() {
1493
1547
  if (!this.promptController?.supportsInlinePanel()) {
1494
1548
  this.promptController?.setStatusMessage('Help: /key sk-… (everything else is automatic)');
@@ -1503,13 +1557,17 @@ class InteractiveShell {
1503
1557
  const lines = [
1504
1558
  chalk.bold.hex('#ece6da')('Erosolar Coder') + dim(' (press any key to dismiss)'),
1505
1559
  '',
1506
- cmd('/key sk-…') + dim(' Set your DeepSeek API key'),
1560
+ cmd('/key sk-…') + dim(' Set your DeepSeek API key (required)'),
1561
+ cmd('/key tvly-…') + dim(' Set your Tavily key for web search (optional)'),
1562
+ cmd('/account') + dim(' Show / switch key source (hosted vs your own)'),
1507
1563
  cmd('/update') + dim(' Check npm and upgrade to the latest version'),
1508
1564
  cmd('/resume') + dim(' Restore a previous conversation'),
1509
1565
  cmd('/context') + dim(' Show context-window usage'),
1510
1566
  cmd('/diff') + dim(' Review changes made this run'),
1511
1567
  cmd('/rewind') + dim(' Undo this run\'s file changes'),
1512
1568
  '',
1569
+ dim('Prefixes: ') + cmd('@file') + dim(' attach · ') + cmd('!cmd') + dim(' run shell · ') + cmd('#note') + dim(' save to memory'),
1570
+ '',
1513
1571
  dim('Everything else runs automatically for max performance —'),
1514
1572
  dim('deepseek-v4-pro · max thought · ultracode · adversarial verifier, all on.'),
1515
1573
  dim('Shift+Tab cycles permission mode · Ctrl+D exits · ? for shortcuts'),
@@ -1609,6 +1667,19 @@ class InteractiveShell {
1609
1667
  void this.runLocalCommand(trimmed.slice(1).trim());
1610
1668
  return;
1611
1669
  }
1670
+ // `#note` — quick-capture a note to persistent project memory (Claude Code
1671
+ // parity), no model round-trip. Lands in .erosolar/memory/ where the agent
1672
+ // reads it on later sessions.
1673
+ if (trimmed.startsWith('#')) {
1674
+ this.dismissInlinePanel();
1675
+ const note = trimmed.slice(1).trim();
1676
+ const r = this.promptController?.getRenderer();
1677
+ if (appendMemoryNote(this.workingDir, note))
1678
+ r?.addEvent('system', chalk.green('✓ Saved to memory'));
1679
+ else
1680
+ r?.addEvent('system', chalk.yellow('Usage: #<note to remember>'));
1681
+ return;
1682
+ }
1612
1683
  // Dismiss inline panel for regular user prompts
1613
1684
  this.dismissInlinePanel();
1614
1685
  // Live follow-up queue (Claude Code parity): a prompt typed while the agent
@@ -1645,6 +1716,10 @@ class InteractiveShell {
1645
1716
  // A fresh user prompt clears any prior interrupt state — this is new
1646
1717
  // work the user actually wants done.
1647
1718
  this.userInterruptedRun = false;
1719
+ // Fresh user request → start a new auto-continue turn budget + failure log.
1720
+ this.autoGovernor.reset();
1721
+ this.failureRegistry.reset();
1722
+ this.adversarialCorrectionCount = 0;
1648
1723
  // Pinned-prompt persistence removed per request — no longer
1649
1724
  // displayed above the chat box.
1650
1725
  }
@@ -1657,6 +1732,12 @@ class InteractiveShell {
1657
1732
  let episodeSuccess = false;
1658
1733
  const toolsUsed = [];
1659
1734
  const filesModified = [];
1735
+ // Tail of this turn's tool outputs (where TS/test/build errors land), so the
1736
+ // failure registry + governor see real error text, not just the narration.
1737
+ let turnToolOutput = '';
1738
+ // Reviewer findings from THIS turn (set by the adversarial.findings event),
1739
+ // used in the finally to drive a bounded auto-correction.
1740
+ let turnAdversarialFindings = null;
1660
1741
  // Track reasoning content for fallback when response is empty
1661
1742
  let reasoningBuffer = '';
1662
1743
  // Track reasoning-only time to prevent models from reasoning forever without action
@@ -1820,6 +1901,11 @@ class InteractiveShell {
1820
1901
  if (isHitlToolName(event.toolName)) {
1821
1902
  hitlDepth = Math.max(0, hitlDepth - 1);
1822
1903
  }
1904
+ // Keep the tail of tool output for the failure registry / governor
1905
+ // (errors land here, not in the assistant narration).
1906
+ if (typeof event.result === 'string' && event.result) {
1907
+ turnToolOutput = (turnToolOutput + '\n' + event.result).slice(-16000);
1908
+ }
1823
1909
  // Clear the activity label; the agent is thinking again.
1824
1910
  this.promptController?.setStatusMessage('Thinking…');
1825
1911
  // Reset reasoning timer after tool completes
@@ -1883,6 +1969,11 @@ class InteractiveShell {
1883
1969
  elapsedMs: event.elapsedMs,
1884
1970
  })));
1885
1971
  break;
1972
+ case 'adversarial.findings':
1973
+ // The reviewer refuted this turn's draft — remember it so the
1974
+ // auto-continue loop can run a bounded re-fix (handled in finally).
1975
+ turnAdversarialFindings = event.findings;
1976
+ break;
1886
1977
  case 'context.compacted': {
1887
1978
  // The conversation was auto-compacted to stay within the window —
1888
1979
  // surface it as a dim note (Claude Code parity) instead of silently.
@@ -2028,6 +2119,10 @@ class InteractiveShell {
2028
2119
  r?.setQueuedPrompts([]);
2029
2120
  // Note: pendingPrompts may still have items if a drain just started
2030
2121
  // a new processPrompt; the new run will manage the list.
2122
+ // Snapshot this turn's full output (tool results + narration) BEFORE the
2123
+ // buffer is cleared — the auto-continue governor + failure registry need
2124
+ // the real error text, which the reset below would otherwise wipe.
2125
+ const combinedTurnOutput = (turnToolOutput + '\n' + this.currentResponseBuffer).slice(-16000);
2031
2126
  this.currentResponseBuffer = '';
2032
2127
  // Autosave the conversation so /resume has something to restore. Each
2033
2128
  // turn updates the same snapshot in place (keyed by this.sessionId).
@@ -2060,19 +2155,54 @@ class InteractiveShell {
2060
2155
  // Check if original user prompt is fully completed
2061
2156
  const detector = getTaskCompletionDetector();
2062
2157
  const analysis = detector.analyzeCompletion(this.currentResponseBuffer, toolsUsed);
2063
- // Continue until task is complete
2064
- if (!analysis.isComplete) {
2158
+ // Record this turn with the governor (bounds the loop + detects a
2159
+ // stall: the same tools/files/failure repeating with no new progress)
2160
+ // and the failure registry (catches the same error recurring across
2161
+ // NON-consecutive turns — a thrash the stall check would miss).
2162
+ this.autoGovernor.recordTurn({
2163
+ toolsUsed,
2164
+ filesModified,
2165
+ failingSignal: detectFailingTestOrBuild(combinedTurnOutput),
2166
+ });
2167
+ this.failureRegistry.trackTurn(combinedTurnOutput);
2168
+ const gov = this.autoGovernor.check();
2169
+ const failureNudge = this.failureRegistry.nudge();
2170
+ const todos = getCurrentTodos();
2171
+ const pending = pendingTodos(todos);
2172
+ if (gov.stop) {
2173
+ // Yield to the user WITH state instead of thrashing forever.
2174
+ const note = gov.reason === 'limit'
2175
+ ? `Paused after ${gov.turn} auto-continue turns (turn limit).${pending.length ? ` ${pending.length} task${pending.length === 1 ? '' : 's'} still pending` : ''} — say "continue" to keep going.`
2176
+ : `Paused: no new progress over the last few turns (same actions repeating).${pending.length ? ` ${pending.length} task${pending.length === 1 ? '' : 's'} pending` : ''} — tell me how to proceed.`;
2177
+ this.promptController?.getRenderer()?.addEvent('system', chalk.dim(note));
2178
+ this.promptController?.setStatusMessage(null);
2179
+ this.originalPromptForAutoContinue = null;
2180
+ }
2181
+ else if (turnAdversarialFindings && this.adversarialCorrectionCount < MAX_ADVERSARIAL_CORRECTIONS) {
2182
+ // The reviewer refuted this turn's draft — re-run the FULL tool loop
2183
+ // to actually fix the findings (not just show the caveat), bounded
2184
+ // by the governor + this per-request cap.
2185
+ this.adversarialCorrectionCount += 1;
2186
+ this.promptController?.setStatusMessage('Addressing reviewer findings…');
2187
+ await new Promise(resolve => setTimeout(resolve, 300));
2188
+ await this.processPrompt(buildAdversarialCorrectionPrompt(turnAdversarialFindings));
2189
+ }
2190
+ else if (!analysis.isComplete || pending.length > 0) {
2191
+ // Continue — but only stop when the LIVE PLAN is also clear: pending
2192
+ // todos force a continue even if the response sounded "done".
2065
2193
  this.promptController?.setStatusMessage('Continuing...');
2066
2194
  await new Promise(resolve => setTimeout(resolve, 500));
2067
- // Generate auto-continue prompt using stored original prompt
2068
- const autoPrompt = this.generateAutoContinuePrompt(this.originalPromptForAutoContinue || '', this.currentResponseBuffer, toolsUsed);
2069
- if (autoPrompt) {
2070
- await this.processPrompt(autoPrompt);
2071
- }
2072
- else {
2073
- // Default continue if no specific auto-prompt generated
2074
- await this.processPrompt('continue');
2075
- }
2195
+ // Prefer the plan's next task; fall back to the response heuristic.
2196
+ const base = nextTodoPrompt(todos)
2197
+ ?? this.generateAutoContinuePrompt(this.originalPromptForAutoContinue || '', combinedTurnOutput, toolsUsed)
2198
+ ?? 'continue';
2199
+ // When a failure keeps recurring, lead with the change-approach nudge.
2200
+ // Keep an IMPORTANT: prefix so this counts as an auto-continue (not a
2201
+ // fresh user prompt, which would reset the governor).
2202
+ const autoPrompt = failureNudge
2203
+ ? `IMPORTANT: ${failureNudge}\n\n${base.replace(/^IMPORTANT:\s*/, '')}`
2204
+ : base;
2205
+ await this.processPrompt(autoPrompt);
2076
2206
  }
2077
2207
  else {
2078
2208
  this.promptController?.setStatusMessage('Task complete');