@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.
- 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 +179 -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 +4 -0
- package/dist/ui/ink/InkPromptController.d.ts.map +1 -1
- package/dist/ui/ink/InkPromptController.js +5 -0
- package/dist/ui/ink/InkPromptController.js.map +1 -1
- package/dist/ui/ink/Prompt.d.ts +4 -0
- package/dist/ui/ink/Prompt.d.ts.map +1 -1
- package/dist/ui/ink/Prompt.js +70 -15
- 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;
|
|
@@ -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
|
|
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
|
-
|
|
797
|
-
|
|
826
|
+
const arg = trimmed.slice('/key'.length).trim();
|
|
827
|
+
const entry = classifyKeyEntry(arg);
|
|
828
|
+
if (entry) {
|
|
798
829
|
try {
|
|
799
|
-
|
|
800
|
-
const
|
|
801
|
-
|
|
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
|
-
|
|
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('
|
|
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
|
-
//
|
|
2053
|
-
|
|
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
|
-
//
|
|
2057
|
-
const
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
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');
|