@trenchwork/erosolar 1.1.61 → 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 +179 -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 +4 -0
  59. package/dist/ui/ink/InkPromptController.d.ts.map +1 -1
  60. package/dist/ui/ink/InkPromptController.js +5 -0
  61. package/dist/ui/ink/InkPromptController.js.map +1 -1
  62. package/dist/ui/ink/Prompt.d.ts +4 -0
  63. package/dist/ui/ink/Prompt.d.ts.map +1 -1
  64. package/dist/ui/ink/Prompt.js +70 -15
  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;
@@ -342,6 +364,10 @@ class InteractiveShell {
342
364
  onToggleHITL: () => this.handleHITLToggle(),
343
365
  onCyclePermissionMode: (mode) => this.handlePermissionModeChange(mode),
344
366
  onExpandToolResult: () => this.handleExpandToolResult(),
367
+ // Esc interrupts a running turn (handleInterrupt no-ops when idle), so
368
+ // the spinner's "esc to interrupt" is real. Ctrl+C still works too.
369
+ onEscape: () => this.handleInterrupt(),
370
+ onShowShortcuts: () => this.showKeyboardShortcuts(),
345
371
  });
346
372
  // Register cleanup callback for graceful shutdown
347
373
  onShutdown(() => {
@@ -489,12 +515,15 @@ class InteractiveShell {
489
515
  // WHICH lines appear; here we draw the same box with brand colour.
490
516
  const flare = chalk.hex('#ff6a1f');
491
517
  const wire = chalk.hex('#3a362e');
518
+ const keyStatus = resolveKeyMode();
492
519
  const body = welcomeBodyLines({
493
520
  hasApiKey,
494
521
  maskedKey: hasApiKey ? maskApiKey(apiKey) : '',
495
522
  model: this.profileConfig.model,
496
523
  provider: this.profileConfig.provider,
497
524
  cwd: this.workingDir,
525
+ keyMode: keyStatus.mode,
526
+ keyModeLine: keyModeLine(keyStatus),
498
527
  });
499
528
  const boxed = roundedBox(body, (cell) => cell.replace('✻', flare('✻')), (s) => wire(s));
500
529
  const welcomeContent = ['', ...updateLines, ...boxed, ''].join('\n');
@@ -788,26 +817,19 @@ class InteractiveShell {
788
817
  const lower = trimmed.toLowerCase();
789
818
  // /model and /secrets were removed: Erosolar is locked to deepseek-v4-pro
790
819
  // on max thought (no model switching), and /key is the one key you set.
791
- // 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.
792
824
  if (lower === '/key' || lower.startsWith('/key ')) {
793
- const parts = trimmed.split(/\s+/);
794
- const keyValue = parts[1];
795
825
  const renderer = this.promptController?.getRenderer();
796
- if (keyValue) {
797
- // Direct file write - most reliable method
826
+ const arg = trimmed.slice('/key'.length).trim();
827
+ const entry = classifyKeyEntry(arg);
828
+ if (entry) {
798
829
  try {
799
- const secretDir = join(homedir(), '.erosolar');
800
- const secretFile = join(secretDir, 'secrets.json');
801
- mkdirSync(secretDir, { recursive: true });
802
- const existing = existsSync(secretFile)
803
- ? JSON.parse(readFileSync(secretFile, 'utf-8'))
804
- : {};
805
- existing['DEEPSEEK_API_KEY'] = keyValue;
806
- writeFileSync(secretFile, JSON.stringify(existing, null, 2) + '\n');
807
- // Also set in process.env for immediate use
808
- process.env['DEEPSEEK_API_KEY'] = keyValue;
809
- // Show confirmation via renderer
810
- 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`));
811
833
  }
812
834
  catch (error) {
813
835
  const msg = error instanceof Error ? error.message : String(error);
@@ -815,11 +837,33 @@ class InteractiveShell {
815
837
  }
816
838
  }
817
839
  else {
818
- // Show usage hint
819
- 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)'));
820
841
  }
821
842
  return true;
822
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
+ }
823
867
  // /update — check npm for a newer version and upgrade in-shell.
824
868
  if (lower === '/update' || lower === '/upgrade') {
825
869
  void this.handleUpdateCommand();
@@ -1486,6 +1530,19 @@ class InteractiveShell {
1486
1530
  revertAllChanges(this.workingDir); // restores/deletes on disk + clears tracking
1487
1531
  renderer?.addEvent('system', chalk.green('✓ ' + rewindResultLine(restored, deleted)));
1488
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
+ }
1489
1546
  showHelp() {
1490
1547
  if (!this.promptController?.supportsInlinePanel()) {
1491
1548
  this.promptController?.setStatusMessage('Help: /key sk-… (everything else is automatic)');
@@ -1500,13 +1557,17 @@ class InteractiveShell {
1500
1557
  const lines = [
1501
1558
  chalk.bold.hex('#ece6da')('Erosolar Coder') + dim(' (press any key to dismiss)'),
1502
1559
  '',
1503
- 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)'),
1504
1563
  cmd('/update') + dim(' Check npm and upgrade to the latest version'),
1505
1564
  cmd('/resume') + dim(' Restore a previous conversation'),
1506
1565
  cmd('/context') + dim(' Show context-window usage'),
1507
1566
  cmd('/diff') + dim(' Review changes made this run'),
1508
1567
  cmd('/rewind') + dim(' Undo this run\'s file changes'),
1509
1568
  '',
1569
+ dim('Prefixes: ') + cmd('@file') + dim(' attach · ') + cmd('!cmd') + dim(' run shell · ') + cmd('#note') + dim(' save to memory'),
1570
+ '',
1510
1571
  dim('Everything else runs automatically for max performance —'),
1511
1572
  dim('deepseek-v4-pro · max thought · ultracode · adversarial verifier, all on.'),
1512
1573
  dim('Shift+Tab cycles permission mode · Ctrl+D exits · ? for shortcuts'),
@@ -1598,6 +1659,27 @@ class InteractiveShell {
1598
1659
  setTimeout(() => this.promptController?.setStatusMessage(null), 2000);
1599
1660
  return;
1600
1661
  }
1662
+ // `!cmd` — bash mode (Claude Code parity): run the rest as a shell command
1663
+ // directly, no model round-trip. Same executor as /bash, via the leading
1664
+ // bang. Runs immediately (like a slash command), not queued behind the agent.
1665
+ if (trimmed.startsWith('!')) {
1666
+ this.dismissInlinePanel();
1667
+ void this.runLocalCommand(trimmed.slice(1).trim());
1668
+ return;
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
+ }
1601
1683
  // Dismiss inline panel for regular user prompts
1602
1684
  this.dismissInlinePanel();
1603
1685
  // Live follow-up queue (Claude Code parity): a prompt typed while the agent
@@ -1634,6 +1716,10 @@ class InteractiveShell {
1634
1716
  // A fresh user prompt clears any prior interrupt state — this is new
1635
1717
  // work the user actually wants done.
1636
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;
1637
1723
  // Pinned-prompt persistence removed per request — no longer
1638
1724
  // displayed above the chat box.
1639
1725
  }
@@ -1646,6 +1732,12 @@ class InteractiveShell {
1646
1732
  let episodeSuccess = false;
1647
1733
  const toolsUsed = [];
1648
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;
1649
1741
  // Track reasoning content for fallback when response is empty
1650
1742
  let reasoningBuffer = '';
1651
1743
  // Track reasoning-only time to prevent models from reasoning forever without action
@@ -1809,6 +1901,11 @@ class InteractiveShell {
1809
1901
  if (isHitlToolName(event.toolName)) {
1810
1902
  hitlDepth = Math.max(0, hitlDepth - 1);
1811
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
+ }
1812
1909
  // Clear the activity label; the agent is thinking again.
1813
1910
  this.promptController?.setStatusMessage('Thinking…');
1814
1911
  // Reset reasoning timer after tool completes
@@ -1872,6 +1969,11 @@ class InteractiveShell {
1872
1969
  elapsedMs: event.elapsedMs,
1873
1970
  })));
1874
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;
1875
1977
  case 'context.compacted': {
1876
1978
  // The conversation was auto-compacted to stay within the window —
1877
1979
  // surface it as a dim note (Claude Code parity) instead of silently.
@@ -2017,6 +2119,10 @@ class InteractiveShell {
2017
2119
  r?.setQueuedPrompts([]);
2018
2120
  // Note: pendingPrompts may still have items if a drain just started
2019
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);
2020
2126
  this.currentResponseBuffer = '';
2021
2127
  // Autosave the conversation so /resume has something to restore. Each
2022
2128
  // turn updates the same snapshot in place (keyed by this.sessionId).
@@ -2049,19 +2155,54 @@ class InteractiveShell {
2049
2155
  // Check if original user prompt is fully completed
2050
2156
  const detector = getTaskCompletionDetector();
2051
2157
  const analysis = detector.analyzeCompletion(this.currentResponseBuffer, toolsUsed);
2052
- // Continue until task is complete
2053
- 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".
2054
2193
  this.promptController?.setStatusMessage('Continuing...');
2055
2194
  await new Promise(resolve => setTimeout(resolve, 500));
2056
- // Generate auto-continue prompt using stored original prompt
2057
- const autoPrompt = this.generateAutoContinuePrompt(this.originalPromptForAutoContinue || '', this.currentResponseBuffer, toolsUsed);
2058
- if (autoPrompt) {
2059
- await this.processPrompt(autoPrompt);
2060
- }
2061
- else {
2062
- // Default continue if no specific auto-prompt generated
2063
- await this.processPrompt('continue');
2064
- }
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);
2065
2206
  }
2066
2207
  else {
2067
2208
  this.promptController?.setStatusMessage('Task complete');