@kinqs/brainrouter-cli 0.3.5 → 0.3.7
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/README.md +29 -52
- package/agents/architect.json +18 -0
- package/agents/explorer.json +18 -0
- package/agents/reviewer.json +18 -0
- package/agents/verifier.json +18 -0
- package/agents/worker.json +18 -0
- package/bin/cli.cjs +71 -0
- package/dist/agent/agent.d.ts +224 -3
- package/dist/agent/agent.js +561 -55
- package/dist/cli/banner.d.ts +80 -0
- package/dist/cli/banner.js +232 -0
- package/dist/cli/cliPrompt.d.ts +106 -0
- package/dist/cli/cliPrompt.js +314 -0
- package/dist/cli/commands/_context.d.ts +3 -1
- package/dist/cli/commands/_helpers.d.ts +1 -1
- package/dist/cli/commands/_helpers.js +6 -6
- package/dist/cli/commands/config.d.ts +46 -0
- package/dist/cli/commands/config.js +1042 -0
- package/dist/cli/commands/guard.js +75 -10
- package/dist/cli/commands/init.d.ts +20 -0
- package/dist/cli/commands/init.js +64 -0
- package/dist/cli/commands/login.d.ts +13 -0
- package/dist/cli/commands/login.js +179 -0
- package/dist/cli/commands/mcp.d.ts +19 -0
- package/dist/cli/commands/mcp.js +286 -0
- package/dist/cli/commands/memory.js +2 -2
- package/dist/cli/commands/obs.js +22 -22
- package/dist/cli/commands/orchestration.js +18 -0
- package/dist/cli/commands/session.js +13 -5
- package/dist/cli/commands/ui.js +202 -91
- package/dist/cli/commands/workflow.d.ts +20 -0
- package/dist/cli/commands/workflow.js +368 -51
- package/dist/cli/ink/ChatApp.d.ts +206 -0
- package/dist/cli/ink/ChatApp.js +493 -0
- package/dist/cli/ink/Frame.d.ts +26 -0
- package/dist/cli/ink/Frame.js +5 -0
- package/dist/cli/ink/Picker.d.ts +65 -0
- package/dist/cli/ink/Picker.js +133 -0
- package/dist/cli/ink/SlashPalette.d.ts +51 -0
- package/dist/cli/ink/SlashPalette.js +136 -0
- package/dist/cli/ink/TextField.d.ts +34 -0
- package/dist/cli/ink/TextField.js +47 -0
- package/dist/cli/ink/WizardApp.d.ts +7 -0
- package/dist/cli/ink/WizardApp.js +422 -0
- package/dist/cli/ink/ambientChat.d.ts +34 -0
- package/dist/cli/ink/ambientChat.js +7 -0
- package/dist/cli/ink/consoleCapture.d.ts +11 -0
- package/dist/cli/ink/consoleCapture.js +33 -0
- package/dist/cli/ink/markdownRender.d.ts +41 -0
- package/dist/cli/ink/markdownRender.js +278 -0
- package/dist/cli/ink/renderWithResizeClear.d.ts +14 -0
- package/dist/cli/ink/renderWithResizeClear.js +33 -0
- package/dist/cli/ink/runChat.d.ts +34 -0
- package/dist/cli/ink/runChat.js +571 -0
- package/dist/cli/ink/runPicker.d.ts +31 -0
- package/dist/cli/ink/runPicker.js +139 -0
- package/dist/cli/ink/runSlashPalette.d.ts +23 -0
- package/dist/cli/ink/runSlashPalette.js +33 -0
- package/dist/cli/ink/runWizard.d.ts +22 -0
- package/dist/cli/ink/runWizard.js +133 -0
- package/dist/cli/ink/stdinHandoff.d.ts +51 -0
- package/dist/cli/ink/stdinHandoff.js +78 -0
- package/dist/cli/ink/toolFormat.d.ts +73 -0
- package/dist/cli/ink/toolFormat.js +180 -0
- package/dist/cli/ink/useTerminalSize.d.ts +35 -0
- package/dist/cli/ink/useTerminalSize.js +26 -0
- package/dist/cli/repl.d.ts +25 -3
- package/dist/cli/repl.js +64 -646
- package/dist/cli/slashSuggest.d.ts +32 -0
- package/dist/cli/slashSuggest.js +146 -0
- package/dist/cli/spinner.d.ts +34 -0
- package/dist/cli/spinner.js +36 -0
- package/dist/cli/statusline.d.ts +67 -0
- package/dist/cli/statusline.js +204 -0
- package/dist/cli/theme.d.ts +79 -0
- package/dist/cli/theme.js +106 -0
- package/dist/cli/whereView.d.ts +81 -0
- package/dist/cli/whereView.js +245 -0
- package/dist/cli/wizard/modelsApi.d.ts +72 -0
- package/dist/cli/wizard/modelsApi.js +166 -0
- package/dist/cli/wizard/picker.d.ts +202 -0
- package/dist/cli/wizard/picker.js +547 -0
- package/dist/cli/wizard/providers.d.ts +86 -0
- package/dist/cli/wizard/providers.js +190 -0
- package/dist/cli/wizard/runner.d.ts +13 -0
- package/dist/cli/wizard/runner.js +488 -0
- package/dist/cli/wizard/types.d.ts +122 -0
- package/dist/cli/wizard/types.js +109 -0
- package/dist/config/config.d.ts +52 -0
- package/dist/config/config.js +89 -75
- package/dist/index.js +215 -206
- package/dist/memory/briefing.d.ts +11 -1
- package/dist/memory/briefing.js +69 -1
- package/dist/memory/consolidation.d.ts +1 -1
- package/dist/orchestration/agentRegistry.d.ts +36 -0
- package/dist/orchestration/agentRegistry.js +64 -0
- package/dist/orchestration/orchestrator.d.ts +7 -0
- package/dist/orchestration/orchestrator.js +2 -0
- package/dist/orchestration/tools.d.ts +10 -1
- package/dist/orchestration/tools.js +48 -4
- package/dist/prompt/breadthHint.d.ts +5 -0
- package/dist/prompt/breadthHint.js +44 -0
- package/dist/prompt/skillCatalog.d.ts +11 -0
- package/dist/prompt/skillCatalog.js +134 -0
- package/dist/prompt/skillRunner.d.ts +2 -2
- package/dist/prompt/skillRunner.js +2 -31
- package/dist/prompt/systemPrompt.d.ts +34 -0
- package/dist/prompt/systemPrompt.js +128 -108
- package/dist/runtime/dangerousCommand.d.ts +53 -0
- package/dist/runtime/dangerousCommand.js +105 -0
- package/dist/runtime/mcpClient.d.ts +38 -1
- package/dist/runtime/mcpClient.js +104 -13
- package/dist/runtime/mcpPool.d.ts +162 -0
- package/dist/runtime/mcpPool.js +423 -0
- package/dist/runtime/mcpUtils.d.ts +3 -1
- package/dist/state/goalStore.d.ts +98 -17
- package/dist/state/goalStore.js +132 -42
- package/dist/state/preferencesStore.d.ts +67 -3
- package/dist/state/preferencesStore.js +84 -1
- package/dist/state/workflowArtifacts.d.ts +63 -2
- package/dist/state/workflowArtifacts.js +120 -8
- package/dist/tests/_helpers.d.ts +31 -0
- package/dist/tests/_helpers.js +91 -0
- package/package.json +12 -5
- package/.env.example +0 -109
package/dist/cli/repl.js
CHANGED
|
@@ -1,18 +1,6 @@
|
|
|
1
|
-
import readline from 'node:readline';
|
|
2
1
|
import fs from 'node:fs';
|
|
3
2
|
import path from 'node:path';
|
|
4
3
|
import chalk from 'chalk';
|
|
5
|
-
import ora from 'ora';
|
|
6
|
-
import { exec } from 'node:child_process';
|
|
7
|
-
import { promisify } from 'node:util';
|
|
8
|
-
import { marked } from 'marked';
|
|
9
|
-
import { markedTerminal } from 'marked-terminal';
|
|
10
|
-
import { expandMentions } from '../memory/mentions.js';
|
|
11
|
-
import { addGoalTokens, buildBudgetSteeringMessage, goalHasBudgetLeft, goalIsOnFinalBudgetTurn, readGoal, tickGoalIteration, usageLimitGoal } from '../state/goalStore.js';
|
|
12
|
-
import { readPreferences } from '../state/preferencesStore.js';
|
|
13
|
-
import { execSync } from 'node:child_process';
|
|
14
|
-
import { listSessions } from '../orchestration/orchestrator.js';
|
|
15
|
-
import { setActiveReadline } from './cliPrompt.js';
|
|
16
4
|
// Category dispatch — extracted slash-command handlers. Each module exports
|
|
17
5
|
// a tryHandleX(ctx) that returns true iff it matched the command. Walked
|
|
18
6
|
// in order; first match wins, no match falls through to the legacy switch.
|
|
@@ -23,640 +11,34 @@ import { tryHandleObsCommand } from './commands/obs.js';
|
|
|
23
11
|
import { tryHandleOrchestrationCommand } from './commands/orchestration.js';
|
|
24
12
|
import { tryHandleSessionCommand } from './commands/session.js';
|
|
25
13
|
import { tryHandleGuardCommand } from './commands/guard.js';
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}));
|
|
14
|
+
import { tryHandleMcpCommand } from './commands/mcp.js';
|
|
15
|
+
import { tryHandleInitCommand } from './commands/init.js';
|
|
16
|
+
import { tryHandleConfigCommand } from './commands/config.js';
|
|
17
|
+
import { tryHandleLoginCommand } from './commands/login.js';
|
|
31
18
|
/**
|
|
32
19
|
* All slash commands the REPL recognizes. Used for tab autocomplete and for
|
|
33
20
|
* the readline completer. Keep alphabetically grouped roughly by surface area.
|
|
21
|
+
*
|
|
22
|
+
* The Ink chat REPL (cli/ink/runChat.tsx) consumes this same list for its
|
|
23
|
+
* inline slash palette so both surfaces stay in lockstep as new commands land.
|
|
34
24
|
*/
|
|
35
|
-
const SLASH_COMMANDS = [
|
|
36
|
-
'/help', '/status', '/workspace', '/tools', '/skills', '/plan', '/transcript',
|
|
25
|
+
export const SLASH_COMMANDS = [
|
|
26
|
+
'/help', '/status', '/workspace', '/where', '/tools', '/skills', '/plan', '/transcript',
|
|
37
27
|
'/doctor', '/config', '/diff', '/commit', '/clear', '/compact', '/exit', '/quit',
|
|
38
28
|
'/roles', '/agents', '/agent', '/spawn', '/wait',
|
|
39
|
-
'/spec', '/feature-dev', '/review', '/implement-plan', '/skill', '/workflows', '/approve',
|
|
29
|
+
'/spec', '/feature-dev', '/grill-me', '/review', '/implement-plan', '/skill', '/workflow', '/workflows', '/approve',
|
|
40
30
|
'/memory', '/recall', '/briefing', '/scenes', '/working', '/forget',
|
|
41
|
-
'/init', '/sessions', '/resume', '/model', '/mcp',
|
|
31
|
+
'/init', '/login', '/sessions', '/resume', '/model', '/mcp',
|
|
42
32
|
'/goal', '/copy', '/fork', '/rename', '/permissions', '/hooks', '/hookify', '/loop',
|
|
43
|
-
'/continue', '/auto-review', '/vim', '/statusline',
|
|
33
|
+
'/continue', '/auto-review', '/vim', '/statusline', '/quiet',
|
|
44
34
|
'/handover', '/explain', '/trace', '/failed', '/verify', '/audit',
|
|
45
35
|
'/export', '/import', '/persona', '/skill-hints', '/diagnostics',
|
|
46
|
-
'/tokens', '/watch', '/yolo', '/sandbox', '/kill',
|
|
36
|
+
'/tokens', '/watch', '/yolo', '/mode', '/review-policy', '/sandbox', '/kill',
|
|
47
37
|
// workflow & ergonomics commands
|
|
48
|
-
'/theme', '/title', '/personality', '/new', '/side', '/btw', '/raw',
|
|
38
|
+
'/theme', '/title', '/personality', '/effort', '/new', '/side', '/btw', '/raw',
|
|
49
39
|
'/feedback', '/rollout', '/ps', '/stop', '/logout', '/apps', '/plugins',
|
|
50
40
|
'/experimental', '/memories', '/debug-config', '/mention', '/keymap', '/ide',
|
|
51
41
|
];
|
|
52
|
-
export function startREPL(agent, mcpClient, config, workspace) {
|
|
53
|
-
console.log(chalk.bold.hex('#CC9166')('\n🧠 BRAINROUTER TERMINAL AGENT CLIENT v0.3.5'));
|
|
54
|
-
console.log(chalk.gray('Midnight Ledger / Obsidian Surface theme active.'));
|
|
55
|
-
console.log(chalk.gray(`Workspace root: ${workspace?.workspaceRoot || process.cwd()}`));
|
|
56
|
-
// Surface offline mode prominently — easy to miss the warning that scrolled
|
|
57
|
-
// by during startup, and the user needs to know memory tools won't fire.
|
|
58
|
-
if (!mcpClient.isConnected()) {
|
|
59
|
-
console.log(chalk.yellow('⚠️ OFFLINE MODE — MCP server unreachable. Local tools only; memory recall / skills disabled.'));
|
|
60
|
-
}
|
|
61
|
-
console.log(chalk.gray('Type ') + chalk.cyan('/help') + chalk.gray(' for commands, or start typing your prompt.\n'));
|
|
62
|
-
const rl = readline.createInterface({
|
|
63
|
-
input: process.stdin,
|
|
64
|
-
output: process.stdout,
|
|
65
|
-
prompt: chalk.hex('#CC9166')('brainrouter> '),
|
|
66
|
-
// Tab-completion: complete slash commands when the line begins with "/"
|
|
67
|
-
// and complete workspace file paths when the user is mid-`@mention`.
|
|
68
|
-
completer: (line) => {
|
|
69
|
-
const atMatch = line.match(/@([^\s]*)$/);
|
|
70
|
-
if (atMatch) {
|
|
71
|
-
const partial = atMatch[1];
|
|
72
|
-
const candidates = completeWorkspacePath(agent.workspaceRoot, partial);
|
|
73
|
-
return [candidates.map((c) => `@${c}`), `@${partial}`];
|
|
74
|
-
}
|
|
75
|
-
if (line.startsWith('/')) {
|
|
76
|
-
const hits = SLASH_COMMANDS.filter((cmd) => cmd.startsWith(line));
|
|
77
|
-
return [hits.length ? hits : SLASH_COMMANDS.slice(), line];
|
|
78
|
-
}
|
|
79
|
-
return [[], line];
|
|
80
|
-
},
|
|
81
|
-
});
|
|
82
|
-
// GitHub PR detection cache. `gh pr view` takes ~300ms and prompts often
|
|
83
|
-
// refresh many times per turn; cache the result for 30s. Returns either
|
|
84
|
-
// a string like "#42" or null when there's no PR / gh not installed.
|
|
85
|
-
let prCache = null;
|
|
86
|
-
const PR_CACHE_TTL_MS = 30_000;
|
|
87
|
-
const detectGitHubPR = (cwd) => {
|
|
88
|
-
const now = Date.now();
|
|
89
|
-
if (prCache && now - prCache.cachedAt < PR_CACHE_TTL_MS)
|
|
90
|
-
return prCache.value;
|
|
91
|
-
let value = null;
|
|
92
|
-
try {
|
|
93
|
-
const out = execSync('gh pr view --json number,title 2>/dev/null', {
|
|
94
|
-
cwd,
|
|
95
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
96
|
-
timeout: 1500,
|
|
97
|
-
}).toString().trim();
|
|
98
|
-
if (out) {
|
|
99
|
-
const parsed = JSON.parse(out);
|
|
100
|
-
if (typeof parsed.number === 'number')
|
|
101
|
-
value = `#${parsed.number}`;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
catch {
|
|
105
|
-
// gh missing, not a PR branch, or not a github repo — fine.
|
|
106
|
-
}
|
|
107
|
-
prCache = { value, cachedAt: now };
|
|
108
|
-
return value;
|
|
109
|
-
};
|
|
110
|
-
// Reflect the current access mode and any configured statusline segments
|
|
111
|
-
// in the prompt. Configurable via /statusline; default just shows the mode.
|
|
112
|
-
const renderStatusline = () => {
|
|
113
|
-
const prefs = readPreferences(agent.workspaceRoot);
|
|
114
|
-
const segments = prefs.statusline.split(',').map((s) => s.trim()).filter(Boolean);
|
|
115
|
-
const out = [];
|
|
116
|
-
for (const seg of segments) {
|
|
117
|
-
if (seg === 'mode')
|
|
118
|
-
out.push(agent.getAccessMode());
|
|
119
|
-
else if (seg === 'model')
|
|
120
|
-
out.push(agent.getModel());
|
|
121
|
-
else if (seg === 'tokens') {
|
|
122
|
-
const u = agent.lastTurnUsage;
|
|
123
|
-
if (u.calls > 0)
|
|
124
|
-
out.push(`${u.promptTokens}↑${u.completionTokens}↓`);
|
|
125
|
-
}
|
|
126
|
-
else if (seg === 'session') {
|
|
127
|
-
const k = agent.sessionKey;
|
|
128
|
-
out.push(k.length > 22 ? `${k.slice(0, 22)}…` : k);
|
|
129
|
-
}
|
|
130
|
-
else if (seg === 'branch' || seg === 'dirty') {
|
|
131
|
-
try {
|
|
132
|
-
const branch = execSync('git rev-parse --abbrev-ref HEAD', { cwd: agent.workspaceRoot, stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
|
|
133
|
-
const dirty = execSync('git status --porcelain', { cwd: agent.workspaceRoot, stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim() !== '';
|
|
134
|
-
if (seg === 'branch')
|
|
135
|
-
out.push(branch);
|
|
136
|
-
else if (seg === 'dirty' && dirty)
|
|
137
|
-
out.push('*');
|
|
138
|
-
}
|
|
139
|
-
catch { /* not a git repo */ }
|
|
140
|
-
}
|
|
141
|
-
else if (seg === 'pr') {
|
|
142
|
-
// Detect open GitHub PR for the current branch — 30s cache so the
|
|
143
|
-
// prompt refresh is cheap (re-shells out only on a stale window).
|
|
144
|
-
const pr = detectGitHubPR(agent.workspaceRoot);
|
|
145
|
-
if (pr)
|
|
146
|
-
out.push(pr);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
return out.filter(Boolean).join(' · ');
|
|
150
|
-
};
|
|
151
|
-
const refreshPromptForMode = () => {
|
|
152
|
-
const mode = agent.getAccessMode();
|
|
153
|
-
const accent = mode === 'shell' ? chalk.red : mode === 'write' ? chalk.hex('#CC9166') : chalk.green;
|
|
154
|
-
const line = renderStatusline();
|
|
155
|
-
rl.setPrompt(accent(`brainrouter[${line}]> `));
|
|
156
|
-
// The terminal title shares the same trigger conditions as the prompt:
|
|
157
|
-
// any time the prompt redraws (mode change, post-turn, post-spawn), the
|
|
158
|
-
// awaiting-input count or active model may have shifted. Cheap call.
|
|
159
|
-
refreshTerminalTitle();
|
|
160
|
-
};
|
|
161
|
-
// When a /goal is active, after each turn we schedule the NEXT turn
|
|
162
|
-
// automatically. The flag tracks whether such a continuation is pending so
|
|
163
|
-
// user input (next line typed) cancels it before it fires. Declared early
|
|
164
|
-
// so refreshTerminalTitle() (below) can read it for the awaiting-count.
|
|
165
|
-
let pendingContinuation = false;
|
|
166
|
-
// Returns the number of child agents currently in `pending` or `running`
|
|
167
|
-
// status — used by the tab title to surface "needs attention" counts.
|
|
168
|
-
const getRunningChildCount = () => {
|
|
169
|
-
try {
|
|
170
|
-
const sessions = listSessions(agent.workspaceRoot);
|
|
171
|
-
return sessions.filter((s) => s.status === 'pending' || s.status === 'running').length;
|
|
172
|
-
}
|
|
173
|
-
catch {
|
|
174
|
-
return 0;
|
|
175
|
-
}
|
|
176
|
-
};
|
|
177
|
-
// Dynamic terminal tab title. Refreshed at startup AND whenever the agent
|
|
178
|
-
// count / awaiting state changes (after each turn, post-spawn). Prefixes
|
|
179
|
-
// a "(N) " count when there's work requiring attention — pending
|
|
180
|
-
// continuation OR a running child — so background tabs surface attention
|
|
181
|
-
// without focus.
|
|
182
|
-
const refreshTerminalTitle = () => {
|
|
183
|
-
try {
|
|
184
|
-
const prefs = readPreferences(agent.workspaceRoot);
|
|
185
|
-
const cfg = prefs.terminalTitle ?? 'model,session';
|
|
186
|
-
if (cfg.toLowerCase() === 'off')
|
|
187
|
-
return;
|
|
188
|
-
const segs = cfg.split(',').map((s) => s.trim()).filter(Boolean);
|
|
189
|
-
const parts = [];
|
|
190
|
-
for (const seg of segs) {
|
|
191
|
-
if (seg === 'model')
|
|
192
|
-
parts.push(agent.getModel());
|
|
193
|
-
else if (seg === 'session')
|
|
194
|
-
parts.push(agent.sessionKey.slice(0, 24));
|
|
195
|
-
else if (seg === 'mode')
|
|
196
|
-
parts.push(agent.getAccessMode());
|
|
197
|
-
else if (seg === 'branch') {
|
|
198
|
-
try {
|
|
199
|
-
parts.push(execSync('git rev-parse --abbrev-ref HEAD', {
|
|
200
|
-
cwd: agent.workspaceRoot,
|
|
201
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
202
|
-
}).toString().trim());
|
|
203
|
-
}
|
|
204
|
-
catch { /* not a git repo */ }
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
if (parts.length === 0)
|
|
208
|
-
return;
|
|
209
|
-
// Awaiting-input prefix: pendingContinuation OR any running children.
|
|
210
|
-
const awaitingCount = (pendingContinuation ? 1 : 0) + getRunningChildCount();
|
|
211
|
-
const prefix = awaitingCount > 0 ? `(${awaitingCount}) ` : '';
|
|
212
|
-
process.stdout.write(`\x1b]0;${prefix}brainrouter · ${parts.join(' · ')}\x07`);
|
|
213
|
-
}
|
|
214
|
-
catch { /* terminal doesn't support OSC titles */ }
|
|
215
|
-
};
|
|
216
|
-
refreshPromptForMode();
|
|
217
|
-
refreshTerminalTitle();
|
|
218
|
-
// Vim mode: readline supports editorMode 'vi' via setRawMode + tty.
|
|
219
|
-
// We honor the persisted preference at startup so users don't have to
|
|
220
|
-
// re-toggle it each session.
|
|
221
|
-
const initialPrefs = readPreferences(agent.workspaceRoot);
|
|
222
|
-
if (initialPrefs.editorMode === 'vi') {
|
|
223
|
-
process.stdout.write(chalk.gray('Vim mode enabled (composer uses vi keybindings). Toggle with /vim.\n'));
|
|
224
|
-
// Node's readline doesn't natively expose vi mode; we approximate by
|
|
225
|
-
// emitting a hint and trusting the user's terminal/inputrc. A future
|
|
226
|
-
// pass can swap in a custom keypress handler for full Vim semantics.
|
|
227
|
-
}
|
|
228
|
-
// Shift+Tab cycles the access mode.
|
|
229
|
-
// Order: read → write → shell → read …
|
|
230
|
-
if (process.stdin.isTTY) {
|
|
231
|
-
try {
|
|
232
|
-
process.stdin.setRawMode?.(false);
|
|
233
|
-
}
|
|
234
|
-
catch { /* noop */ }
|
|
235
|
-
}
|
|
236
|
-
process.stdin.on('keypress', (_str, key) => {
|
|
237
|
-
if (key && key.name === 'tab' && key.shift) {
|
|
238
|
-
const cycle = ['read', 'write', 'shell'];
|
|
239
|
-
const current = agent.getAccessMode();
|
|
240
|
-
const next = cycle[(cycle.indexOf(current) + 1) % cycle.length];
|
|
241
|
-
agent.setAccessMode(next);
|
|
242
|
-
refreshPromptForMode();
|
|
243
|
-
process.stdout.write(`\n${chalk.gray(`Access mode → ${next}`)}\n`);
|
|
244
|
-
rl.prompt();
|
|
245
|
-
}
|
|
246
|
-
});
|
|
247
|
-
// Publish the rl interface to cliPrompt.ts so out-of-scope helpers
|
|
248
|
-
// (askYesNo, safePrintAbovePrompt) can talk to the same stdin/stdout
|
|
249
|
-
// pair the REPL owns. Cleared on close.
|
|
250
|
-
setActiveReadline(rl);
|
|
251
|
-
rl.on('close', () => { setActiveReadline(undefined); });
|
|
252
|
-
rl.prompt();
|
|
253
|
-
let isProcessing = false;
|
|
254
|
-
// (pendingContinuation declared earlier alongside the title refresh helpers.)
|
|
255
|
-
/**
|
|
256
|
-
* Prompt the agent receives for iterations 2..N of an active goal —
|
|
257
|
-
* fired automatically by the post-turn loop after each completed turn.
|
|
258
|
-
* Orients the model around the active objective, forces an evidence
|
|
259
|
-
* audit, and refuses prose-only "I will continue" answers.
|
|
260
|
-
*
|
|
261
|
-
* Distinct from `buildGoalKickoffPrompt` (in `commands/_helpers.ts`),
|
|
262
|
-
* which is the FIRST-turn prompt fired by `/goal <text>` and `/goal resume`.
|
|
263
|
-
*/
|
|
264
|
-
const buildGoalContinuationPrompt = (goal, lastPrompt, lastAnswer) => {
|
|
265
|
-
const iter = goal.budget.iterationsUsed + 1;
|
|
266
|
-
const remaining = Math.max(0, goal.budget.maxIterations - iter);
|
|
267
|
-
return [
|
|
268
|
-
`[GOAL CONTINUATION — iteration ${iter}/${goal.budget.maxIterations}, ${remaining} remaining]`,
|
|
269
|
-
'',
|
|
270
|
-
`Your active goal is: ${goal.text}`,
|
|
271
|
-
'',
|
|
272
|
-
`Last user message: ${lastPrompt || '(none)'}`,
|
|
273
|
-
`Your previous response (truncated): ${lastAnswer.slice(0, 600)}${lastAnswer.length > 600 ? '…' : ''}`,
|
|
274
|
-
'',
|
|
275
|
-
'## What to do this turn',
|
|
276
|
-
'1. **Audit the evidence in this thread** against the goal\'s outcome. Look at files you wrote, tests you ran, tools that returned ok=true.',
|
|
277
|
-
'2. **Decide one of three:**',
|
|
278
|
-
' - If the outcome is met with concrete evidence (file paths, test names, command outputs), **write the user-visible answer / analysis / summary as prose AND THEN call `goal_complete` with a short 1–2 sentence proof — in the SAME response.** The proof is audit metadata; the prose is what the user reads. Skipping the prose means the user sees a placeholder.',
|
|
279
|
-
' - If no defensible path forward remains without user input or missing materials, **write the user-visible explanation as prose AND THEN call `goal_blocked` with a reason + needed input.**',
|
|
280
|
-
' - Otherwise (mid-goal), take the **next concrete tool action** (read a file, write code, spawn a worker child, run a verifier). Do NOT respond with prose like "I will now do X" — that\'s a no-op and the CLI will stop the continuation. Anti-spin applies to mid-goal turns; the final goal-completing turn requires prose.',
|
|
281
|
-
'3. Use update_plan to track progress if you haven\'t already.',
|
|
282
|
-
'',
|
|
283
|
-
'Reminder: budget is finite. Pick the highest-leverage action that moves the goal forward.',
|
|
284
|
-
].join('\n');
|
|
285
|
-
};
|
|
286
|
-
/**
|
|
287
|
-
* Print a line of output while the readline prompt is showing without
|
|
288
|
-
* clobbering whatever the user is mid-typing. Used by child-agent callbacks
|
|
289
|
-
* that fire AFTER the parent's runTurn returned — the agent's tool events
|
|
290
|
-
* keep streaming for a while because children run detached, and naive
|
|
291
|
-
* console.log + spinner.start() would steal the input row.
|
|
292
|
-
*/
|
|
293
|
-
const safePrintAbovePrompt = (msg) => {
|
|
294
|
-
if (!process.stdout.isTTY) {
|
|
295
|
-
console.log(msg);
|
|
296
|
-
return;
|
|
297
|
-
}
|
|
298
|
-
// \r → column 0, \x1b[2K → clear the whole line, including any prompt + typed text.
|
|
299
|
-
process.stdout.write('\r\x1b[2K');
|
|
300
|
-
console.log(msg);
|
|
301
|
-
// Redraw the prompt and re-render the in-progress input buffer.
|
|
302
|
-
try {
|
|
303
|
-
rl._refreshLine?.();
|
|
304
|
-
}
|
|
305
|
-
catch {
|
|
306
|
-
rl.prompt(true);
|
|
307
|
-
}
|
|
308
|
-
};
|
|
309
|
-
/** Run a turn programmatically (used by `/continue` and the line handler). */
|
|
310
|
-
const runAgentTurn = async (rawInput) => {
|
|
311
|
-
if (isProcessing) {
|
|
312
|
-
console.log(chalk.yellow('\nA previous turn is still running.\n'));
|
|
313
|
-
return;
|
|
314
|
-
}
|
|
315
|
-
isProcessing = true;
|
|
316
|
-
rl.pause();
|
|
317
|
-
const { expanded, mentions } = expandMentions(rawInput, agent.workspaceRoot);
|
|
318
|
-
if (mentions.length > 0) {
|
|
319
|
-
console.log(chalk.gray(`📎 Attached ${mentions.length} file${mentions.length === 1 ? '' : 's'}: ${mentions.map((m) => m.token).join(', ')}`));
|
|
320
|
-
}
|
|
321
|
-
const startedAt = Date.now();
|
|
322
|
-
const spinner = ora(chalk.gray('Agent starting...')).start();
|
|
323
|
-
// Once the parent's runTurn returns, child agents may still emit tool
|
|
324
|
-
// events asynchronously. After this flag flips, we MUST NOT touch the
|
|
325
|
-
// spinner (which is already .succeeded) — restarting it would steal the
|
|
326
|
-
// readline row and the user would feel like they can't type.
|
|
327
|
-
let parentDone = false;
|
|
328
|
-
const tickStatus = (status) => {
|
|
329
|
-
if (parentDone)
|
|
330
|
-
return;
|
|
331
|
-
const elapsed = Math.floor((Date.now() - startedAt) / 1000);
|
|
332
|
-
const u = agent.lastTurnUsage;
|
|
333
|
-
const tokens = u.calls > 0 ? ` ${u.promptTokens.toLocaleString()}↑ ${u.completionTokens.toLocaleString()}↓` : '';
|
|
334
|
-
spinner.text = chalk.gray(`${status} ${elapsed}s${tokens}`);
|
|
335
|
-
};
|
|
336
|
-
try {
|
|
337
|
-
const answer = await agent.runTurn(expanded, {
|
|
338
|
-
onStatusUpdate: tickStatus,
|
|
339
|
-
onToolStart: (name, args) => {
|
|
340
|
-
// Render spawn_agent / spawn_agents specially — a one-liner
|
|
341
|
-
// ("Ran agent <role> — <one-line task>") so a fan-out of 5
|
|
342
|
-
// children produces 5 clean lines instead of 5 JSON dumps. The
|
|
343
|
-
// raw JSON is still in the transcript for debugging.
|
|
344
|
-
let line;
|
|
345
|
-
if (name === 'spawn_agent') {
|
|
346
|
-
const role = chalk.magenta(String(args?.role ?? 'agent'));
|
|
347
|
-
const label = args?.label ? chalk.gray(` [${args.label}]`) : '';
|
|
348
|
-
const task = String(args?.prompt ?? '').replace(/\s+/g, ' ').trim();
|
|
349
|
-
const preview = chalk.gray(task.length > 100 ? task.slice(0, 99) + '…' : task);
|
|
350
|
-
line = chalk.gray('🤖 Spawning agent: ') + role + label + chalk.gray(' — ') + preview;
|
|
351
|
-
}
|
|
352
|
-
else if (name === 'spawn_agents') {
|
|
353
|
-
const agents = Array.isArray(args?.agents) ? args.agents : [];
|
|
354
|
-
const summary = agents
|
|
355
|
-
.map((a) => chalk.magenta(String(a?.role ?? 'agent')))
|
|
356
|
-
.join(chalk.gray(', '));
|
|
357
|
-
line = chalk.gray(`🤖 Spawning ${agents.length} agent${agents.length === 1 ? '' : 's'} in parallel: `) + summary;
|
|
358
|
-
}
|
|
359
|
-
else {
|
|
360
|
-
line = chalk.gray('🛞 Calling tool: ') + chalk.cyan(name) + chalk.gray(`(${JSON.stringify(args).slice(0, 240)})`);
|
|
361
|
-
}
|
|
362
|
-
if (parentDone) {
|
|
363
|
-
safePrintAbovePrompt(line);
|
|
364
|
-
return;
|
|
365
|
-
}
|
|
366
|
-
spinner.stop();
|
|
367
|
-
console.log(line);
|
|
368
|
-
},
|
|
369
|
-
onToolEnd: (name, result) => {
|
|
370
|
-
const line = result.success
|
|
371
|
-
? chalk.green('✓ Tool ') + chalk.cyan(name) + chalk.green(' completed: ') + chalk.gray(result.summary)
|
|
372
|
-
: chalk.red('❌ Tool ') + chalk.cyan(name) + chalk.red(' failed: ') + chalk.yellow(result.summary);
|
|
373
|
-
// Inspection-tool preview: indented under the summary so the user
|
|
374
|
-
// sees the actual result (directory listing, grep matches, glob
|
|
375
|
-
// paths) even when the LLM later replies with only a stub like
|
|
376
|
-
// "I have listed the directory." Capped to a handful of lines in
|
|
377
|
-
// getToolPreview itself.
|
|
378
|
-
const previewBlock = result.preview
|
|
379
|
-
? '\n' + result.preview.split('\n').map((l) => chalk.gray(' ' + l)).join('\n')
|
|
380
|
-
: '';
|
|
381
|
-
const composed = line + previewBlock;
|
|
382
|
-
if (parentDone) {
|
|
383
|
-
safePrintAbovePrompt(composed);
|
|
384
|
-
return;
|
|
385
|
-
}
|
|
386
|
-
console.log(composed);
|
|
387
|
-
tickStatus('Thinking');
|
|
388
|
-
spinner.start();
|
|
389
|
-
},
|
|
390
|
-
onPlanUpdate: (items, explanation) => {
|
|
391
|
-
if (parentDone) {
|
|
392
|
-
safePrintAbovePrompt(chalk.gray(`📋 Plan updated (${items.length} item${items.length === 1 ? '' : 's'})`));
|
|
393
|
-
return;
|
|
394
|
-
}
|
|
395
|
-
spinner.stop();
|
|
396
|
-
console.log(chalk.gray('📋 Plan updated:'));
|
|
397
|
-
if (explanation)
|
|
398
|
-
console.log(chalk.gray(` ${explanation}`));
|
|
399
|
-
for (const item of items) {
|
|
400
|
-
const mark = item.status === 'completed' ? chalk.green('✓')
|
|
401
|
-
: item.status === 'in_progress' ? chalk.yellow('⏳')
|
|
402
|
-
: chalk.gray('☐');
|
|
403
|
-
const text = item.status === 'completed' ? chalk.gray(item.step) : item.step;
|
|
404
|
-
console.log(` ${mark} ${text}`);
|
|
405
|
-
}
|
|
406
|
-
tickStatus('Thinking');
|
|
407
|
-
spinner.start();
|
|
408
|
-
},
|
|
409
|
-
onChildComplete: (event) => {
|
|
410
|
-
const head = event.status === 'completed'
|
|
411
|
-
? chalk.green(`🏁 Agent ${event.childId} (${event.role}) completed`)
|
|
412
|
-
: chalk.red(`💥 Agent ${event.childId} (${event.role}) failed`);
|
|
413
|
-
const tail = event.status === 'completed' && event.preview
|
|
414
|
-
? chalk.gray(` — ${event.preview}`)
|
|
415
|
-
: event.error ? chalk.yellow(` — ${event.error}`) : '';
|
|
416
|
-
const line = head + tail;
|
|
417
|
-
if (parentDone) {
|
|
418
|
-
safePrintAbovePrompt(line);
|
|
419
|
-
return;
|
|
420
|
-
}
|
|
421
|
-
spinner.stop();
|
|
422
|
-
console.log(line);
|
|
423
|
-
tickStatus('Thinking');
|
|
424
|
-
spinner.start();
|
|
425
|
-
},
|
|
426
|
-
onMemoryEvent: (event) => {
|
|
427
|
-
let line;
|
|
428
|
-
if (event.kind === 'briefing') {
|
|
429
|
-
const src = event.sources.length > 0 ? event.sources.join(', ') : '(none)';
|
|
430
|
-
line = chalk.gray(`🧠 Briefing: ${event.recordCount} record${event.recordCount === 1 ? '' : 's'} from ${src}`);
|
|
431
|
-
}
|
|
432
|
-
else if (event.kind === 'capture') {
|
|
433
|
-
// Truthful capture line: show sensory rows actually written, and
|
|
434
|
-
// — critically — flag when extraction silently failed. The old
|
|
435
|
-
// line said "Captured turn → memory" even when 0 cognitive
|
|
436
|
-
// records came out the other end, which made the user think
|
|
437
|
-
// their conversation was searchable when nothing of the sort
|
|
438
|
-
// was happening.
|
|
439
|
-
const sensory = event.sensoryRecorded ?? event.messageCount;
|
|
440
|
-
const extracted = event.extractedCount;
|
|
441
|
-
const triggered = event.extractionTriggered;
|
|
442
|
-
const sk = event.sessionKey.slice(0, 12);
|
|
443
|
-
if (event.extractionWarning) {
|
|
444
|
-
line = chalk.yellow(`💾 Captured ${sensory} sensory msg(s) in ${sk}… — ⚠️ ${event.extractionWarning}`);
|
|
445
|
-
}
|
|
446
|
-
else if (triggered && typeof extracted === 'number') {
|
|
447
|
-
if (extracted > 0) {
|
|
448
|
-
line = chalk.gray(`💾 Captured ${sensory} msg(s) → ${extracted} cognitive record(s) extracted (${sk}…)`);
|
|
449
|
-
}
|
|
450
|
-
else {
|
|
451
|
-
// LLM ran successfully but found nothing notable to promote
|
|
452
|
-
// (greeting, trivial exchange, all-duplicates). NOT an error.
|
|
453
|
-
line = chalk.gray(`💾 Captured ${sensory} msg(s) → no new memories worth promoting (${sk}…)`);
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
else if (triggered === false) {
|
|
457
|
-
// Sensory landed; extractor below the every-N-turn threshold.
|
|
458
|
-
line = chalk.gray(`💾 Captured ${sensory} msg(s) → sensory buffer (${sk}…)`);
|
|
459
|
-
}
|
|
460
|
-
else {
|
|
461
|
-
line = chalk.gray(`💾 Captured ${sensory} msg(s) → memory (${sk}…)`);
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
else if (event.kind === 'citation' && event.recordIds.length > 0) {
|
|
465
|
-
line = chalk.gray(`📌 Reinforced ${event.recordIds.length} record${event.recordIds.length === 1 ? '' : 's'}: ${event.recordIds.slice(0, 3).join(', ')}${event.recordIds.length > 3 ? '…' : ''}`);
|
|
466
|
-
}
|
|
467
|
-
else if (event.kind === 'contradiction') {
|
|
468
|
-
line = chalk.yellow(`⚠️ Memory contradiction: ${event.warning.slice(0, 140)}`);
|
|
469
|
-
}
|
|
470
|
-
if (!line)
|
|
471
|
-
return;
|
|
472
|
-
if (parentDone) {
|
|
473
|
-
safePrintAbovePrompt(line);
|
|
474
|
-
return;
|
|
475
|
-
}
|
|
476
|
-
spinner.stop();
|
|
477
|
-
console.log(line);
|
|
478
|
-
tickStatus('Thinking');
|
|
479
|
-
spinner.start();
|
|
480
|
-
},
|
|
481
|
-
});
|
|
482
|
-
const elapsed = Math.floor((Date.now() - startedAt) / 1000);
|
|
483
|
-
const u = agent.lastTurnUsage;
|
|
484
|
-
const tokenSummary = u.calls > 0
|
|
485
|
-
? chalk.gray(` · ${u.promptTokens.toLocaleString()} in / ${u.completionTokens.toLocaleString()} out across ${u.calls} call${u.calls === 1 ? '' : 's'}`)
|
|
486
|
-
: '';
|
|
487
|
-
parentDone = true;
|
|
488
|
-
spinner.succeed(chalk.green(`Done!${chalk.gray(` ${elapsed}s`)}${tokenSummary}`));
|
|
489
|
-
const prefsForRender = readPreferences(agent.workspaceRoot);
|
|
490
|
-
const rendered = prefsForRender.rawScrollback ? answer : marked.parse(answer);
|
|
491
|
-
console.log('\n' + rendered + '\n');
|
|
492
|
-
const warning = agent.takeContradictionWarning();
|
|
493
|
-
if (warning) {
|
|
494
|
-
console.log(chalk.yellow(`⚠️ Memory: ${warning}`));
|
|
495
|
-
console.log(chalk.gray(` Use /memory or /briefing to investigate, /forget <id> to archive obsolete records.\n`));
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
catch (err) {
|
|
499
|
-
parentDone = true;
|
|
500
|
-
spinner.fail(chalk.red('Execution failed'));
|
|
501
|
-
console.error(chalk.red(`\nError: ${err.message}\n`));
|
|
502
|
-
}
|
|
503
|
-
finally {
|
|
504
|
-
isProcessing = false;
|
|
505
|
-
// Clear any active skill latched by /skill / /feature-dev / /spec /
|
|
506
|
-
// /review / /implement-plan so subsequent plain prompts don't keep
|
|
507
|
-
// spiking the same skill. The skill memetic potential still decays
|
|
508
|
-
// server-side on its own half-life; this just stops attribution.
|
|
509
|
-
agent.activeSkill = undefined;
|
|
510
|
-
// Auto-continuation logic. Rules:
|
|
511
|
-
// - the goal must be active (not paused / complete / blocked / usage_limited)
|
|
512
|
-
// - the turn made at least one tool call (prose-only turns are anti-spin)
|
|
513
|
-
// - we still have iteration AND token budget left
|
|
514
|
-
// - the agent didn't call goal_complete / goal_blocked this turn
|
|
515
|
-
//
|
|
516
|
-
// BEFORE checking, accumulate this turn's tokens into the goal's
|
|
517
|
-
// running tally. If that tips us over a token cap, transition to
|
|
518
|
-
// `usage_limited` instead of continuing — same effect as exhausting
|
|
519
|
-
// the iteration cap, but distinguishable in status.
|
|
520
|
-
let goalAfter = readGoal(agent.workspaceRoot, agent.sessionKey);
|
|
521
|
-
if (goalAfter && goalAfter.budget.maxTokens) {
|
|
522
|
-
const delta = (agent.lastTurnUsage?.promptTokens ?? 0) + (agent.lastTurnUsage?.completionTokens ?? 0);
|
|
523
|
-
if (delta > 0) {
|
|
524
|
-
const updated = addGoalTokens(agent.workspaceRoot, agent.sessionKey, delta);
|
|
525
|
-
if (updated)
|
|
526
|
-
goalAfter = updated;
|
|
527
|
-
}
|
|
528
|
-
if (goalAfter &&
|
|
529
|
-
goalAfter.status === 'active' &&
|
|
530
|
-
typeof goalAfter.budget.maxTokens === 'number' &&
|
|
531
|
-
(goalAfter.budget.tokensUsed ?? 0) >= goalAfter.budget.maxTokens) {
|
|
532
|
-
const limited = usageLimitGoal(agent.workspaceRoot, agent.sessionKey, `Token budget reached: ${(goalAfter.budget.tokensUsed ?? 0).toLocaleString()} of ${goalAfter.budget.maxTokens.toLocaleString()} used.`);
|
|
533
|
-
if (limited)
|
|
534
|
-
goalAfter = limited;
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
const shouldContinue = !!goalAfter &&
|
|
538
|
-
goalAfter.status === 'active' &&
|
|
539
|
-
goalHasBudgetLeft(goalAfter) &&
|
|
540
|
-
agent.lastTurnToolCalls > 0 &&
|
|
541
|
-
agent.lastGoalTransition === undefined;
|
|
542
|
-
if (goalAfter && goalAfter.status === 'complete') {
|
|
543
|
-
console.log(chalk.green(`\n🎯 Goal achieved — ${goalAfter.blockedReason ?? 'evidence on record.'}\n`));
|
|
544
|
-
}
|
|
545
|
-
else if (goalAfter && goalAfter.status === 'blocked') {
|
|
546
|
-
console.log(chalk.yellow(`\n🚧 Goal blocked: ${goalAfter.blockedReason ?? '(no reason)'}\n`));
|
|
547
|
-
console.log(chalk.gray(` Resolve the blocker, then /goal resume to continue.\n`));
|
|
548
|
-
}
|
|
549
|
-
else if (goalAfter && goalAfter.status === 'usage_limited') {
|
|
550
|
-
console.log(chalk.yellow(`\n⏸ Goal hit usage limit: ${goalAfter.blockedReason ?? 'budget exhausted'}.`));
|
|
551
|
-
console.log(chalk.gray(` Raise the cap with /goal budget <n> or /goal tokens <n>, then /goal resume.\n`));
|
|
552
|
-
}
|
|
553
|
-
else if (goalAfter && goalAfter.status === 'active' && !goalHasBudgetLeft(goalAfter)) {
|
|
554
|
-
// Iteration cap reached — transition to usage_limited so the user
|
|
555
|
-
// gets a consistent resumable state regardless of which cap tripped.
|
|
556
|
-
const reason = `Iteration budget exhausted (${goalAfter.budget.iterationsUsed}/${goalAfter.budget.maxIterations}).`;
|
|
557
|
-
const limited = usageLimitGoal(agent.workspaceRoot, agent.sessionKey, reason);
|
|
558
|
-
console.log(chalk.yellow(`\n⏸ ${reason} Extend with /goal budget <n> and /goal resume, mark /goal complete, or /goal clear.\n`));
|
|
559
|
-
if (limited)
|
|
560
|
-
goalAfter = limited;
|
|
561
|
-
}
|
|
562
|
-
else if (goalAfter && goalAfter.status === 'active' && agent.lastTurnToolCalls === 0) {
|
|
563
|
-
console.log(chalk.gray(`(goal continuation suppressed: last turn made no tool calls — anti-spin)\n`));
|
|
564
|
-
}
|
|
565
|
-
rl.resume();
|
|
566
|
-
refreshPromptForMode(); // pick up token-meter / branch updates
|
|
567
|
-
rl.prompt();
|
|
568
|
-
if (shouldContinue && goalAfter) {
|
|
569
|
-
pendingContinuation = true;
|
|
570
|
-
const next = goalAfter.budget.iterationsUsed + 1;
|
|
571
|
-
// Pre-tick steering: if the NEXT turn would be the final one inside
|
|
572
|
-
// the budget, inject a wrap-up directive so the model lands soft
|
|
573
|
-
// instead of being cut off mid-thought.
|
|
574
|
-
//
|
|
575
|
-
// CRITICAL: also drop any stale steering when the next turn is NOT
|
|
576
|
-
// final. Without this, a previously-injected "wrap up gracefully"
|
|
577
|
-
// message would persist after the user extended the budget via
|
|
578
|
-
// /goal budget or /goal tokens, telling the model "this is your
|
|
579
|
-
// last turn" for every subsequent turn. The removal is idempotent
|
|
580
|
-
// — if no steering was set, this is a no-op.
|
|
581
|
-
const finalBudgetTurn = goalIsOnFinalBudgetTurn(goalAfter);
|
|
582
|
-
if (finalBudgetTurn) {
|
|
583
|
-
agent.replaceTaggedSystemMessage('goal-budget-steering', buildBudgetSteeringMessage(goalAfter));
|
|
584
|
-
console.log(chalk.gray(`(final budget turn — wrap-up steering injected)`));
|
|
585
|
-
}
|
|
586
|
-
else {
|
|
587
|
-
agent.removeTaggedSystemMessage('goal-budget-steering');
|
|
588
|
-
}
|
|
589
|
-
console.log(chalk.gray(`(goal continuation queued — iteration ${next}/${goalAfter.budget.maxIterations}; type anything to cancel)`));
|
|
590
|
-
const followUp = buildGoalContinuationPrompt(goalAfter, agent.lastUserPrompt, agent.lastAnswer);
|
|
591
|
-
setImmediate(() => {
|
|
592
|
-
if (!pendingContinuation || isProcessing)
|
|
593
|
-
return; // user cancelled or busy
|
|
594
|
-
pendingContinuation = false;
|
|
595
|
-
tickGoalIteration(agent.workspaceRoot, agent.sessionKey);
|
|
596
|
-
void runAgentTurn(followUp);
|
|
597
|
-
});
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
};
|
|
601
|
-
rl.on('line', async (line) => {
|
|
602
|
-
// User typed: any pending goal continuation is cancelled.
|
|
603
|
-
if (pendingContinuation) {
|
|
604
|
-
pendingContinuation = false;
|
|
605
|
-
console.log(chalk.gray('(goal continuation cancelled by user input)'));
|
|
606
|
-
}
|
|
607
|
-
const input = line.trim();
|
|
608
|
-
if (!input) {
|
|
609
|
-
rl.prompt();
|
|
610
|
-
return;
|
|
611
|
-
}
|
|
612
|
-
if (input.startsWith('/')) {
|
|
613
|
-
// Split on any whitespace, not a literal space. Without this, a slash
|
|
614
|
-
// command followed by a tab (autocomplete completion that wasn't
|
|
615
|
-
// consumed) or a trailing newline ends up as command="/help\t" which
|
|
616
|
-
// would fall through to "Unknown slash command".
|
|
617
|
-
const parts = input.trim().split(/\s+/);
|
|
618
|
-
const command = parts[0].toLowerCase();
|
|
619
|
-
const args = parts.slice(1);
|
|
620
|
-
// Wrap the slash-command dispatcher so a thrown error or rejected
|
|
621
|
-
// promise can never leave the REPL without a prompt. Without this, a
|
|
622
|
-
// bug inside any /command (file write, MCP call, etc.) bricks the
|
|
623
|
-
// session because the user never sees the prompt come back.
|
|
624
|
-
try {
|
|
625
|
-
await handleSlashCommand(command, args, agent, mcpClient, config, rl, {
|
|
626
|
-
refreshPromptForMode,
|
|
627
|
-
isProcessing: () => isProcessing,
|
|
628
|
-
runAgentTurn: (prompt) => { void runAgentTurn(prompt); },
|
|
629
|
-
runAgentTurnAsync: (prompt) => runAgentTurn(prompt),
|
|
630
|
-
});
|
|
631
|
-
}
|
|
632
|
-
catch (err) {
|
|
633
|
-
console.error(chalk.red(`\nSlash command "${command}" failed: ${err?.message ?? err}\n`));
|
|
634
|
-
}
|
|
635
|
-
finally {
|
|
636
|
-
// The /continue and /side/btw cases own their own prompt cycle via
|
|
637
|
-
// runAgentTurn — only re-prompt if no turn is in flight.
|
|
638
|
-
if (!isProcessing)
|
|
639
|
-
rl.prompt();
|
|
640
|
-
}
|
|
641
|
-
return;
|
|
642
|
-
}
|
|
643
|
-
if (isProcessing) {
|
|
644
|
-
console.log(chalk.yellow('\nA previous turn is still running. Wait for the prompt before sending another message.\n'));
|
|
645
|
-
rl.prompt();
|
|
646
|
-
return;
|
|
647
|
-
}
|
|
648
|
-
await runAgentTurn(input);
|
|
649
|
-
});
|
|
650
|
-
rl.on('SIGINT', async () => {
|
|
651
|
-
console.log(chalk.yellow('\nExiting session...'));
|
|
652
|
-
rl.close();
|
|
653
|
-
});
|
|
654
|
-
rl.on('close', async () => {
|
|
655
|
-
await mcpClient.close();
|
|
656
|
-
console.log(chalk.bold.hex('#CC9166')('Goodbye!\n'));
|
|
657
|
-
process.exit(0);
|
|
658
|
-
});
|
|
659
|
-
}
|
|
660
42
|
const HELP_CATEGORIES = [
|
|
661
43
|
{
|
|
662
44
|
key: 'session',
|
|
@@ -664,8 +46,10 @@ const HELP_CATEGORIES = [
|
|
|
664
46
|
entries: [
|
|
665
47
|
{ cmd: '/status', desc: 'Connection status, LLM config, DB stats' },
|
|
666
48
|
{ cmd: '/workspace', desc: 'Active workspace and session identity' },
|
|
49
|
+
{ cmd: '/where', desc: 'Single-screen view of workspace, workflow, goal, plan, recall, children' },
|
|
667
50
|
{ cmd: '/doctor', desc: 'Config, connection, memory extraction health' },
|
|
668
|
-
{ cmd: '/config', desc: '
|
|
51
|
+
{ cmd: '/config [key] [value]', desc: 'Settings panel; `/config theme dark` to set; `/config raw` for JSON dump' },
|
|
52
|
+
{ cmd: '/login', desc: 'In-REPL MCP profile editor (transport → fields → probe → save)' },
|
|
669
53
|
{ cmd: '/clear', desc: 'Clear chat history for the active session' },
|
|
670
54
|
{ cmd: '/compact', desc: 'LLM-driven compaction of the active session' },
|
|
671
55
|
{ cmd: '/new [label]', desc: 'Start a new chat with a fresh session key' },
|
|
@@ -674,7 +58,7 @@ const HELP_CATEGORIES = [
|
|
|
674
58
|
{ cmd: '/resume <id>', desc: 'Resume a previous session by sessionKey' },
|
|
675
59
|
{ cmd: '/sessions', desc: 'List persisted sessions for this workspace' },
|
|
676
60
|
{ cmd: '/side <q> /btw <q>', desc: 'Ephemeral side conversation in a forked session' },
|
|
677
|
-
{ cmd: '/init', desc: '
|
|
61
|
+
{ cmd: '/init', desc: 'Re-run the onboarding wizard (Theme → Provider → API key → Model → MCP → AGENT.md)' },
|
|
678
62
|
{ cmd: '/exit /quit', desc: 'Close MCP connection and exit' },
|
|
679
63
|
],
|
|
680
64
|
},
|
|
@@ -708,13 +92,17 @@ const HELP_CATEGORIES = [
|
|
|
708
92
|
entries: [
|
|
709
93
|
{ cmd: '/spec <title>', desc: 'Produce spec.md (spec-driven-skill)' },
|
|
710
94
|
{ cmd: '/feature-dev <feat>', desc: 'Multi-agent feature dev with spec + tasks' },
|
|
95
|
+
{ cmd: '/grill-me [--force] <task>', desc: 'Clarify 2–5 questions before implementing (CLARIFY mode)' },
|
|
711
96
|
{ cmd: '/review [scope]', desc: 'Multi-agent code review → review.md' },
|
|
712
97
|
{ cmd: '/implement-plan', desc: 'Execute next plan item; append walkthrough' },
|
|
713
98
|
{ cmd: '/approve [slug]', desc: 'Approve workflow + kick off implementation' },
|
|
714
99
|
{ cmd: '/workflows', desc: 'List durable workflow folders' },
|
|
100
|
+
{ cmd: '/workflow switch <slug>', desc: 'Refocus on an existing workflow (migrates any session goal into the target)' },
|
|
101
|
+
{ cmd: '/workflow pause', desc: 'Pause the current workflow\'s goal' },
|
|
102
|
+
{ cmd: '/workflow resume <slug>', desc: 'Switch to <slug> AND resume its goal in one shot' },
|
|
715
103
|
{ cmd: '/skill <name> [input]', desc: 'Run any catalogued skill' },
|
|
716
104
|
{ cmd: '/skills', desc: 'List installed BrainRouter skills' },
|
|
717
|
-
{ cmd: '/plan', desc: 'Show the durable CLI task plan' },
|
|
105
|
+
{ cmd: '/plan /plan clear', desc: 'Show the durable CLI task plan; clear it (drops stale items)' },
|
|
718
106
|
{ cmd: '/tools', desc: 'List local + MCP tools available to the agent' },
|
|
719
107
|
{ cmd: '/goal [text|clear|complete|pause|resume|budget <n>]', desc: 'Sticky goal' },
|
|
720
108
|
{ cmd: '/continue', desc: 'Resume after a loop-limit abort' },
|
|
@@ -743,7 +131,9 @@ const HELP_CATEGORIES = [
|
|
|
743
131
|
title: 'Guardrails & Permissions',
|
|
744
132
|
entries: [
|
|
745
133
|
{ cmd: '/permissions [read|write|shell]', desc: 'View or set agent access mode' },
|
|
746
|
-
{ cmd: '/
|
|
134
|
+
{ cmd: '/mode [planning|fast]', desc: 'Session execution stance (planning asks, fast skips per-call y/N for safe commands)' },
|
|
135
|
+
{ cmd: '/review-policy [request|proceed]', desc: 'How the agent treats multi-file approval gates' },
|
|
136
|
+
{ cmd: '/yolo [on|off]', desc: 'Alias for `/mode fast` + `/review-policy proceed`' },
|
|
747
137
|
{ cmd: '/sandbox [status|add-read|add-write|remove|clear]', desc: 'Sandbox grants' },
|
|
748
138
|
{ cmd: '/hooks [list|add|remove|enable|disable]', desc: 'Lifecycle shell hooks' },
|
|
749
139
|
{ cmd: '/hookify [list|create|enable|disable|remove]', desc: 'Markdown rule guards' },
|
|
@@ -768,15 +158,17 @@ const HELP_CATEGORIES = [
|
|
|
768
158
|
entries: [
|
|
769
159
|
{ cmd: '/theme [auto|light|dark|mono]', desc: 'Markdown output theme' },
|
|
770
160
|
{ cmd: '/title <segments>', desc: 'Terminal title (model,session,branch,mode)' },
|
|
771
|
-
{ cmd: '/statusline <segments>', desc: 'Prompt (mode,branch,dirty,model,tokens,session,pr)' },
|
|
161
|
+
{ cmd: '/statusline <segments>', desc: 'Prompt (mode,exec,effort,branch,dirty,model,tokens,session,pr,workflow,goal,plan)' },
|
|
772
162
|
{ cmd: '/personality <style>', desc: 'concise | standard | detailed | pair-programmer' },
|
|
163
|
+
{ cmd: '/effort [low|medium|high]', desc: 'Reasoning depth: low=terse, medium=default, high=step-by-step (env: BRAINROUTER_EFFORT)' },
|
|
773
164
|
{ cmd: '/raw [on|off]', desc: 'Toggle raw scrollback' },
|
|
165
|
+
{ cmd: '/quiet [on|off]', desc: 'Hide recall tables, previews, briefings (model prose only)' },
|
|
774
166
|
{ cmd: '/vim', desc: 'Toggle vi-mode for the composer' },
|
|
775
167
|
{ cmd: '/keymap [json]', desc: 'Show built-in bindings and set overrides' },
|
|
776
168
|
{ cmd: '/copy', desc: 'Copy last assistant response to clipboard' },
|
|
777
169
|
{ cmd: '/mention [partial]', desc: 'Suggest files for @ mentions' },
|
|
778
170
|
{ cmd: '/model <name>', desc: 'Switch the LLM model in-session' },
|
|
779
|
-
{ cmd: '/mcp', desc: '
|
|
171
|
+
{ cmd: '/mcp [list|reconnect|tools]', desc: 'MCP profiles, identity tags, online/offline status, reconnect, tool namespaces' },
|
|
780
172
|
{ cmd: '/ide', desc: 'Show detected IDE host' },
|
|
781
173
|
{ cmd: '/apps /plugins', desc: 'List workspace skills and plugin folders' },
|
|
782
174
|
{ cmd: '/feedback [message]', desc: 'Append feedback entry' },
|
|
@@ -820,6 +212,28 @@ export function renderHelp(category) {
|
|
|
820
212
|
}
|
|
821
213
|
console.log(chalk.gray('\nYour terminal is short — run /help <category> to drill in. Resize and re-run /help to see all at once.\n'));
|
|
822
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* Look up a one-line description for a slash command by walking the
|
|
217
|
+
* help registry. Used to populate the slash-suggest popup. Falls back
|
|
218
|
+
* to a generic placeholder if the command isn't documented (those
|
|
219
|
+
* commands still work — they just won't get a custom description
|
|
220
|
+
* inside the popup until someone adds them to `HELP_CATEGORIES`).
|
|
221
|
+
*
|
|
222
|
+
* Description text in `HELP_CATEGORIES` sometimes carries a parenthesised
|
|
223
|
+
* argument hint (e.g. "/config [key] [value]"); we strip everything
|
|
224
|
+
* after the first space when matching by cmd token so e.g. `/config`
|
|
225
|
+
* matches `cmd: "/config [key] [value]"`.
|
|
226
|
+
*/
|
|
227
|
+
export function lookupSlashDescription(cmd) {
|
|
228
|
+
for (const cat of HELP_CATEGORIES) {
|
|
229
|
+
for (const entry of cat.entries) {
|
|
230
|
+
const token = entry.cmd.split(/\s+/)[0];
|
|
231
|
+
if (token === cmd)
|
|
232
|
+
return entry.desc;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return '(no description)';
|
|
236
|
+
}
|
|
823
237
|
function printHelpCategory(c) {
|
|
824
238
|
console.log(chalk.bold(`\n${c.title}:`));
|
|
825
239
|
// Find max command-column width for alignment.
|
|
@@ -828,12 +242,21 @@ function printHelpCategory(c) {
|
|
|
828
242
|
console.log(` ${chalk.cyan(e.cmd.padEnd(colWidth))} ${chalk.gray(e.desc)}`);
|
|
829
243
|
}
|
|
830
244
|
}
|
|
831
|
-
async function handleSlashCommand(command, args, agent, mcpClient, config, rl, ctx) {
|
|
245
|
+
export async function handleSlashCommand(command, args, agent, mcpClient, config, rl, ctx) {
|
|
832
246
|
// Category dispatch — each extracted module returns true iff it matched
|
|
833
247
|
// the command. New categories should be added here as they're extracted
|
|
834
248
|
// from the giant switch below. Long-term goal: shrink the switch to
|
|
835
249
|
// nothing so this dispatch is the only entrypoint.
|
|
836
250
|
const cmdCtx = { command, args, agent, mcpClient, config, rl, repl: ctx };
|
|
251
|
+
// 0.3.7 wizard / config / login dispatchers run first so they shadow
|
|
252
|
+
// the legacy /init + /config handlers in ui.ts (which still ship
|
|
253
|
+
// their old behaviour as fallbacks but are now superseded).
|
|
254
|
+
if (await tryHandleInitCommand(cmdCtx))
|
|
255
|
+
return;
|
|
256
|
+
if (await tryHandleConfigCommand(cmdCtx))
|
|
257
|
+
return;
|
|
258
|
+
if (await tryHandleLoginCommand(cmdCtx))
|
|
259
|
+
return;
|
|
837
260
|
if (await tryHandleMemoryCommand(cmdCtx))
|
|
838
261
|
return;
|
|
839
262
|
if (await tryHandleUiCommand(cmdCtx))
|
|
@@ -848,17 +271,12 @@ async function handleSlashCommand(command, args, agent, mcpClient, config, rl, c
|
|
|
848
271
|
return;
|
|
849
272
|
if (await tryHandleGuardCommand(cmdCtx))
|
|
850
273
|
return;
|
|
274
|
+
if (await tryHandleMcpCommand(cmdCtx))
|
|
275
|
+
return;
|
|
851
276
|
// All commands extracted to category files above. Anything that reaches
|
|
852
277
|
// here didn't match any handler.
|
|
853
278
|
console.log(chalk.red(`\nUnknown slash command: ${command}. Type /help for assistance.\n`));
|
|
854
279
|
}
|
|
855
|
-
// runOrchestrationPrompt was the second-class turn pipeline used by /spawn,
|
|
856
|
-
// /wait, /kill, /commit, /approve, /spec, /feature-dev, /review,
|
|
857
|
-
// /implement-plan, /skill. It lacked goal continuation, the isProcessing
|
|
858
|
-
// lock, /raw honoring, contradiction surfacing, and token summary — so any
|
|
859
|
-
// command that took the second-class path felt visibly weaker than a plain
|
|
860
|
-
// prompt. Removed in favor of routing every command through the closure's
|
|
861
|
-
// runAgentTurn (which has all of the above).
|
|
862
280
|
/**
|
|
863
281
|
* Tab-completion source for `@path/to/file` mentions. Given a partial workspace
|
|
864
282
|
* path, return the matching files and directories one level deep. Stays inside
|