@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.
- package/dist/capabilities/todoCapability.js +2 -2
- package/dist/capabilities/todoCapability.js.map +1 -1
- package/dist/config.js +1 -1
- package/dist/contracts/v1/agent.d.ts +12 -2
- package/dist/contracts/v1/agent.d.ts.map +1 -1
- package/dist/core/adversarialCorrection.d.ts +22 -0
- package/dist/core/adversarialCorrection.d.ts.map +1 -0
- package/dist/core/adversarialCorrection.js +25 -0
- package/dist/core/adversarialCorrection.js.map +1 -0
- package/dist/core/agent.d.ts +2 -0
- package/dist/core/agent.d.ts.map +1 -1
- package/dist/core/agent.js +11 -1
- package/dist/core/agent.js.map +1 -1
- package/dist/core/failureRegistry.d.ts +30 -0
- package/dist/core/failureRegistry.d.ts.map +1 -0
- package/dist/core/failureRegistry.js +74 -0
- package/dist/core/failureRegistry.js.map +1 -0
- package/dist/core/hostedAuth.d.ts +55 -0
- package/dist/core/hostedAuth.d.ts.map +1 -0
- package/dist/core/hostedAuth.js +134 -0
- package/dist/core/hostedAuth.js.map +1 -0
- package/dist/core/secretStore.d.ts +11 -0
- package/dist/core/secretStore.d.ts.map +1 -1
- package/dist/core/secretStore.js +25 -0
- package/dist/core/secretStore.js.map +1 -1
- package/dist/core/slashCommands.d.ts.map +1 -1
- package/dist/core/slashCommands.js +1 -0
- package/dist/core/slashCommands.js.map +1 -1
- package/dist/core/thinkingVerbs.d.ts +31 -0
- package/dist/core/thinkingVerbs.d.ts.map +1 -0
- package/dist/core/thinkingVerbs.js +58 -0
- package/dist/core/thinkingVerbs.js.map +1 -0
- package/dist/core/turnGovernor.d.ts +63 -0
- package/dist/core/turnGovernor.d.ts.map +1 -0
- package/dist/core/turnGovernor.js +94 -0
- package/dist/core/turnGovernor.js.map +1 -0
- package/dist/headless/interactiveShell.d.ts +4 -0
- package/dist/headless/interactiveShell.d.ts.map +1 -1
- package/dist/headless/interactiveShell.js +168 -38
- package/dist/headless/interactiveShell.js.map +1 -1
- package/dist/plugins/tools/todo/todoPlugin.js +1 -1
- package/dist/plugins/tools/todo/todoPlugin.js.map +1 -1
- package/dist/runtime/agentController.d.ts.map +1 -1
- package/dist/runtime/agentController.js +7 -0
- package/dist/runtime/agentController.js.map +1 -1
- package/dist/shell/toolPresentation.d.ts +7 -0
- package/dist/shell/toolPresentation.d.ts.map +1 -1
- package/dist/shell/toolPresentation.js +51 -1
- package/dist/shell/toolPresentation.js.map +1 -1
- package/dist/tools/memoryTools.d.ts +7 -0
- package/dist/tools/memoryTools.d.ts.map +1 -1
- package/dist/tools/memoryTools.js +17 -0
- package/dist/tools/memoryTools.js.map +1 -1
- package/dist/tools/todoTools.d.ts +3 -4
- package/dist/tools/todoTools.d.ts.map +1 -1
- package/dist/tools/todoTools.js +23 -4
- package/dist/tools/todoTools.js.map +1 -1
- package/dist/ui/ink/InkPromptController.d.ts +2 -0
- package/dist/ui/ink/InkPromptController.d.ts.map +1 -1
- package/dist/ui/ink/InkPromptController.js +4 -0
- package/dist/ui/ink/InkPromptController.js.map +1 -1
- package/dist/ui/ink/Prompt.d.ts +2 -0
- package/dist/ui/ink/Prompt.d.ts.map +1 -1
- package/dist/ui/ink/Prompt.js +8 -1
- package/dist/ui/ink/Prompt.js.map +1 -1
- package/dist/ui/ink/StatusLine.d.ts +6 -0
- package/dist/ui/ink/StatusLine.d.ts.map +1 -1
- package/dist/ui/ink/StatusLine.js +17 -3
- package/dist/ui/ink/StatusLine.js.map +1 -1
- 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
|
|
17
|
-
import { resolve, dirname,
|
|
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
|
-
|
|
148
|
-
|
|
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
|
|
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
|
-
|
|
800
|
-
|
|
826
|
+
const arg = trimmed.slice('/key'.length).trim();
|
|
827
|
+
const entry = classifyKeyEntry(arg);
|
|
828
|
+
if (entry) {
|
|
801
829
|
try {
|
|
802
|
-
|
|
803
|
-
const
|
|
804
|
-
|
|
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
|
-
|
|
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('
|
|
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
|
-
//
|
|
2064
|
-
|
|
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
|
-
//
|
|
2068
|
-
const
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
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');
|