@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.
Files changed (125) hide show
  1. package/README.md +29 -52
  2. package/agents/architect.json +18 -0
  3. package/agents/explorer.json +18 -0
  4. package/agents/reviewer.json +18 -0
  5. package/agents/verifier.json +18 -0
  6. package/agents/worker.json +18 -0
  7. package/bin/cli.cjs +71 -0
  8. package/dist/agent/agent.d.ts +224 -3
  9. package/dist/agent/agent.js +561 -55
  10. package/dist/cli/banner.d.ts +80 -0
  11. package/dist/cli/banner.js +232 -0
  12. package/dist/cli/cliPrompt.d.ts +106 -0
  13. package/dist/cli/cliPrompt.js +314 -0
  14. package/dist/cli/commands/_context.d.ts +3 -1
  15. package/dist/cli/commands/_helpers.d.ts +1 -1
  16. package/dist/cli/commands/_helpers.js +6 -6
  17. package/dist/cli/commands/config.d.ts +46 -0
  18. package/dist/cli/commands/config.js +1042 -0
  19. package/dist/cli/commands/guard.js +75 -10
  20. package/dist/cli/commands/init.d.ts +20 -0
  21. package/dist/cli/commands/init.js +64 -0
  22. package/dist/cli/commands/login.d.ts +13 -0
  23. package/dist/cli/commands/login.js +179 -0
  24. package/dist/cli/commands/mcp.d.ts +19 -0
  25. package/dist/cli/commands/mcp.js +286 -0
  26. package/dist/cli/commands/memory.js +2 -2
  27. package/dist/cli/commands/obs.js +22 -22
  28. package/dist/cli/commands/orchestration.js +18 -0
  29. package/dist/cli/commands/session.js +13 -5
  30. package/dist/cli/commands/ui.js +202 -91
  31. package/dist/cli/commands/workflow.d.ts +20 -0
  32. package/dist/cli/commands/workflow.js +368 -51
  33. package/dist/cli/ink/ChatApp.d.ts +206 -0
  34. package/dist/cli/ink/ChatApp.js +493 -0
  35. package/dist/cli/ink/Frame.d.ts +26 -0
  36. package/dist/cli/ink/Frame.js +5 -0
  37. package/dist/cli/ink/Picker.d.ts +65 -0
  38. package/dist/cli/ink/Picker.js +133 -0
  39. package/dist/cli/ink/SlashPalette.d.ts +51 -0
  40. package/dist/cli/ink/SlashPalette.js +136 -0
  41. package/dist/cli/ink/TextField.d.ts +34 -0
  42. package/dist/cli/ink/TextField.js +47 -0
  43. package/dist/cli/ink/WizardApp.d.ts +7 -0
  44. package/dist/cli/ink/WizardApp.js +422 -0
  45. package/dist/cli/ink/ambientChat.d.ts +34 -0
  46. package/dist/cli/ink/ambientChat.js +7 -0
  47. package/dist/cli/ink/consoleCapture.d.ts +11 -0
  48. package/dist/cli/ink/consoleCapture.js +33 -0
  49. package/dist/cli/ink/markdownRender.d.ts +41 -0
  50. package/dist/cli/ink/markdownRender.js +278 -0
  51. package/dist/cli/ink/renderWithResizeClear.d.ts +14 -0
  52. package/dist/cli/ink/renderWithResizeClear.js +33 -0
  53. package/dist/cli/ink/runChat.d.ts +34 -0
  54. package/dist/cli/ink/runChat.js +571 -0
  55. package/dist/cli/ink/runPicker.d.ts +31 -0
  56. package/dist/cli/ink/runPicker.js +139 -0
  57. package/dist/cli/ink/runSlashPalette.d.ts +23 -0
  58. package/dist/cli/ink/runSlashPalette.js +33 -0
  59. package/dist/cli/ink/runWizard.d.ts +22 -0
  60. package/dist/cli/ink/runWizard.js +133 -0
  61. package/dist/cli/ink/stdinHandoff.d.ts +51 -0
  62. package/dist/cli/ink/stdinHandoff.js +78 -0
  63. package/dist/cli/ink/toolFormat.d.ts +73 -0
  64. package/dist/cli/ink/toolFormat.js +180 -0
  65. package/dist/cli/ink/useTerminalSize.d.ts +35 -0
  66. package/dist/cli/ink/useTerminalSize.js +26 -0
  67. package/dist/cli/repl.d.ts +25 -3
  68. package/dist/cli/repl.js +64 -646
  69. package/dist/cli/slashSuggest.d.ts +32 -0
  70. package/dist/cli/slashSuggest.js +146 -0
  71. package/dist/cli/spinner.d.ts +34 -0
  72. package/dist/cli/spinner.js +36 -0
  73. package/dist/cli/statusline.d.ts +67 -0
  74. package/dist/cli/statusline.js +204 -0
  75. package/dist/cli/theme.d.ts +79 -0
  76. package/dist/cli/theme.js +106 -0
  77. package/dist/cli/whereView.d.ts +81 -0
  78. package/dist/cli/whereView.js +245 -0
  79. package/dist/cli/wizard/modelsApi.d.ts +72 -0
  80. package/dist/cli/wizard/modelsApi.js +166 -0
  81. package/dist/cli/wizard/picker.d.ts +202 -0
  82. package/dist/cli/wizard/picker.js +547 -0
  83. package/dist/cli/wizard/providers.d.ts +86 -0
  84. package/dist/cli/wizard/providers.js +190 -0
  85. package/dist/cli/wizard/runner.d.ts +13 -0
  86. package/dist/cli/wizard/runner.js +488 -0
  87. package/dist/cli/wizard/types.d.ts +122 -0
  88. package/dist/cli/wizard/types.js +109 -0
  89. package/dist/config/config.d.ts +52 -0
  90. package/dist/config/config.js +89 -75
  91. package/dist/index.js +215 -206
  92. package/dist/memory/briefing.d.ts +11 -1
  93. package/dist/memory/briefing.js +69 -1
  94. package/dist/memory/consolidation.d.ts +1 -1
  95. package/dist/orchestration/agentRegistry.d.ts +36 -0
  96. package/dist/orchestration/agentRegistry.js +64 -0
  97. package/dist/orchestration/orchestrator.d.ts +7 -0
  98. package/dist/orchestration/orchestrator.js +2 -0
  99. package/dist/orchestration/tools.d.ts +10 -1
  100. package/dist/orchestration/tools.js +48 -4
  101. package/dist/prompt/breadthHint.d.ts +5 -0
  102. package/dist/prompt/breadthHint.js +44 -0
  103. package/dist/prompt/skillCatalog.d.ts +11 -0
  104. package/dist/prompt/skillCatalog.js +134 -0
  105. package/dist/prompt/skillRunner.d.ts +2 -2
  106. package/dist/prompt/skillRunner.js +2 -31
  107. package/dist/prompt/systemPrompt.d.ts +34 -0
  108. package/dist/prompt/systemPrompt.js +128 -108
  109. package/dist/runtime/dangerousCommand.d.ts +53 -0
  110. package/dist/runtime/dangerousCommand.js +105 -0
  111. package/dist/runtime/mcpClient.d.ts +38 -1
  112. package/dist/runtime/mcpClient.js +104 -13
  113. package/dist/runtime/mcpPool.d.ts +162 -0
  114. package/dist/runtime/mcpPool.js +423 -0
  115. package/dist/runtime/mcpUtils.d.ts +3 -1
  116. package/dist/state/goalStore.d.ts +98 -17
  117. package/dist/state/goalStore.js +132 -42
  118. package/dist/state/preferencesStore.d.ts +67 -3
  119. package/dist/state/preferencesStore.js +84 -1
  120. package/dist/state/workflowArtifacts.d.ts +63 -2
  121. package/dist/state/workflowArtifacts.js +120 -8
  122. package/dist/tests/_helpers.d.ts +31 -0
  123. package/dist/tests/_helpers.js +91 -0
  124. package/package.json +12 -5
  125. package/.env.example +0 -109
@@ -0,0 +1,286 @@
1
+ /**
2
+ * `/mcp` slash-command surface. 0.3.7 multi-MCP rewrite — third-party
3
+ * MCPs connect concurrently while BrainRouter MCP profiles are mutually
4
+ * exclusive so exactly one brain is active at a time.
5
+ *
6
+ * /mcp — alias for /mcp list
7
+ * /mcp list — every configured profile + per-server status
8
+ * /mcp tools [server] — MCP tools grouped by `mcp__<server>__*`
9
+ * namespace; pass a server id to scope
10
+ * /mcp connect <name> — connect a configured server that's idle/offline
11
+ * /mcp disconnect <name> — close one server's transport (config preserved)
12
+ * /mcp reconnect [name] — reconnect ONE (when name given) or the selected pool
13
+ *
14
+ * Backwards-compat: `/mcp reconnect` with no arg reconnects the same
15
+ * selected set used at boot: all third-party MCPs plus the active
16
+ * BrainRouter MCP. Pass an explicit name to target one.
17
+ */
18
+ import chalk from 'chalk';
19
+ import { spinner as makeSpinner } from '../spinner.js';
20
+ import { saveConfig } from '../../config/config.js';
21
+ import { resolveIdentityFromConfig } from '../../runtime/mcpClient.js';
22
+ import { selectMcpServerIds } from '../../runtime/mcpPool.js';
23
+ import { buildBannerInputs, renderBanner } from '../banner.js';
24
+ import { resolveTheme } from '../theme.js';
25
+ export async function tryHandleMcpCommand(ctx) {
26
+ const { command, args, mcpClient, config } = ctx;
27
+ if (command !== '/mcp')
28
+ return false;
29
+ const sub = (args[0] ?? 'list').toLowerCase();
30
+ const targetName = args[1]?.trim();
31
+ if (sub === 'tools') {
32
+ const onlyServer = targetName;
33
+ console.log(chalk.bold('\nMCP tools (pooled)'));
34
+ const statuses = mcpClient.getStatuses();
35
+ const connected = statuses.filter((s) => s.status === 'connected');
36
+ if (connected.length === 0) {
37
+ console.log(chalk.yellow(' No MCP servers connected. Try /mcp reconnect.\n'));
38
+ return true;
39
+ }
40
+ const spinner = makeSpinner(chalk.gray('Fetching pooled tool surface...')).start();
41
+ try {
42
+ const res = await mcpClient.listTools();
43
+ const allTools = res.tools || [];
44
+ spinner.succeed(chalk.green(`${allTools.length} tools across ${connected.length} server${connected.length === 1 ? '' : 's'}`));
45
+ // Pool tools are exposed as `mcp__<serverId>__<rawTool>`. Group by serverId.
46
+ const byServer = {};
47
+ for (const t of allTools) {
48
+ const m = /^mcp__([^_]+(?:_[^_]+)*?)__(.+)$/.exec(t.name);
49
+ const serverId = m?.[1] ?? '__unknown__';
50
+ const raw = m?.[2] ?? t.name;
51
+ if (onlyServer && serverId !== onlyServer)
52
+ continue;
53
+ (byServer[serverId] ||= []).push(raw);
54
+ }
55
+ const serverIds = Object.keys(byServer).sort();
56
+ if (serverIds.length === 0) {
57
+ console.log(chalk.gray(`\n No tools for server "${onlyServer}".\n`));
58
+ return true;
59
+ }
60
+ for (const section of groupServerIdsByEcosystem(serverIds, (id) => mcpClient.getStatus(id)?.identity ?? 'unknown')) {
61
+ console.log(`\n${section.title}`);
62
+ for (const id of section.ids) {
63
+ const ident = mcpClient.getStatus(id)?.identity ?? 'unknown';
64
+ const identTag = formatIdentityTag(ident);
65
+ console.log(` ${chalk.bold.green(id)} ${identTag} (${byServer[id].length})`);
66
+ for (const name of byServer[id].sort()) {
67
+ console.log(` ${chalk.gray('•')} ${name} ${chalk.gray(`mcp__${id}__${name}`)}`);
68
+ }
69
+ }
70
+ }
71
+ }
72
+ catch (err) {
73
+ spinner.fail(chalk.red(`Failed: ${err.message}`));
74
+ }
75
+ console.log();
76
+ return true;
77
+ }
78
+ if (sub === 'list') {
79
+ const profiles = Object.keys(config.servers ?? {});
80
+ if (profiles.length === 0) {
81
+ console.log(chalk.yellow('\nNo MCP profiles configured. Run `/login` or `brainrouter config` to set one up.\n'));
82
+ return true;
83
+ }
84
+ console.log(chalk.bold('\nMCP servers'));
85
+ const statuses = mcpClient.getStatuses();
86
+ const statusById = new Map(statuses.map((s) => [s.serverId, s]));
87
+ const activeName = config.activeServer;
88
+ for (const section of groupServerIdsByEcosystem(profiles, (name) => {
89
+ const profile = config.servers[name];
90
+ return statusById.get(name)?.identity ?? profile?.identity ?? 'unknown';
91
+ })) {
92
+ console.log(`\n${section.title}`);
93
+ for (const name of section.ids) {
94
+ const profile = config.servers[name];
95
+ const poolStatus = statusById.get(name);
96
+ // Identity: pool live > config metadata > 'unknown'.
97
+ const identity = poolStatus?.identity ?? profile.identity ?? 'unknown';
98
+ // Status: pool live > 'not in pool' (configured but never tried).
99
+ const liveStatus = poolStatus?.status;
100
+ const statusLabel = liveStatus === 'connected' ? chalk.green('online') :
101
+ liveStatus === 'failed' ? chalk.red('failed') :
102
+ liveStatus === 'connecting' ? chalk.yellow('connecting') :
103
+ liveStatus === 'offline' ? chalk.gray('disconnected') :
104
+ chalk.gray('idle');
105
+ const idLabel = formatIdentityTag(identity);
106
+ const marker = name === activeName ? chalk.bold('★ ') : ' ';
107
+ const transport = profile.type;
108
+ const target = profile.type === 'http' ? profile.url ?? '<no url>' : profile.command ?? '<no command>';
109
+ const toolTag = poolStatus?.toolCount != null ? chalk.gray(`${poolStatus.toolCount} tools`) : '';
110
+ const errTag = liveStatus === 'failed' && poolStatus?.error ? chalk.red(` · ${poolStatus.error}`) : '';
111
+ console.log(`${marker}${chalk.bold(name)} ${idLabel} ${transport} ${statusLabel} ${chalk.gray(target)} ${toolTag}${errTag}`);
112
+ }
113
+ }
114
+ console.log(chalk.gray('\n★ = highlighted profile in the banner.'));
115
+ console.log(chalk.gray(' Multi-MCP: third-party MCPs connect together; only one BrainRouter MCP is active. Use /mcp connect|disconnect|reconnect <name> to manage.\n'));
116
+ return true;
117
+ }
118
+ if (sub === 'reconnect') {
119
+ if (!targetName) {
120
+ // No name → reconnect every configured server in the pool.
121
+ const ids = selectMcpServerIds(config.servers ?? {}, config.activeServer);
122
+ if (ids.length === 0) {
123
+ console.log(chalk.red(`\nNo MCP profiles configured.\n`));
124
+ return true;
125
+ }
126
+ console.log(chalk.gray(`Reconnecting all servers (${ids.length})…`));
127
+ await Promise.allSettled(ids.map(async (id) => {
128
+ try {
129
+ await mcpClient.reconnectOne(id);
130
+ const s = mcpClient.getStatus(id);
131
+ if (s?.status === 'connected') {
132
+ console.log(chalk.green(` ✓ ${id}`));
133
+ }
134
+ else {
135
+ console.log(chalk.red(` ✗ ${id} — ${s?.error ?? 'failed'}`));
136
+ }
137
+ }
138
+ catch (err) {
139
+ console.log(chalk.red(` ✗ ${id} — ${err?.message ?? err}`));
140
+ }
141
+ }));
142
+ console.log();
143
+ return true;
144
+ }
145
+ if (!config.servers?.[targetName]) {
146
+ console.log(chalk.red(`\nNo profile named "${targetName}".\n`));
147
+ return true;
148
+ }
149
+ console.log(chalk.gray(`Reconnecting "${targetName}"…`));
150
+ try {
151
+ await disconnectOtherBrainrouterServers(ctx, targetName);
152
+ await mcpClient.reconnectOne(targetName);
153
+ const s = mcpClient.getStatus(targetName);
154
+ if (s?.status === 'connected') {
155
+ const activated = activateBrainrouterProfile(ctx, targetName);
156
+ console.log(chalk.green(`✓ Reconnected to "${targetName}".\n`));
157
+ if (activated) {
158
+ console.log(chalk.gray(` Active BrainRouter profile saved as "${targetName}" for this and future sessions.\n`));
159
+ printRefreshedBanner(ctx);
160
+ }
161
+ }
162
+ else {
163
+ console.log(chalk.red(`✗ "${targetName}" remained ${s?.status ?? 'offline'} — ${s?.error ?? 'unknown'}\n`));
164
+ }
165
+ }
166
+ catch (err) {
167
+ console.log(chalk.red(`✗ Reconnect failed: ${err?.message ?? err}\n`));
168
+ }
169
+ return true;
170
+ }
171
+ if (sub === 'connect') {
172
+ if (!targetName) {
173
+ console.log(chalk.red('\nUsage: /mcp connect <name>\n'));
174
+ return true;
175
+ }
176
+ const profile = config.servers?.[targetName];
177
+ if (!profile) {
178
+ console.log(chalk.red(`\nNo profile named "${targetName}". Available: ${Object.keys(config.servers ?? {}).join(', ') || '(none)'}.\n`));
179
+ return true;
180
+ }
181
+ console.log(chalk.gray(`Connecting "${targetName}"…`));
182
+ try {
183
+ await disconnectOtherBrainrouterServers(ctx, targetName);
184
+ await mcpClient.connectOne(targetName, profile, config.llm, 5_000);
185
+ const s = mcpClient.getStatus(targetName);
186
+ if (s?.status === 'connected') {
187
+ const activated = activateBrainrouterProfile(ctx, targetName);
188
+ console.log(chalk.green(`✓ "${targetName}" online (${s.toolCount ?? 0} tools).\n`));
189
+ if (activated) {
190
+ console.log(chalk.gray(` Active BrainRouter profile saved as "${targetName}" for this and future sessions.\n`));
191
+ printRefreshedBanner(ctx);
192
+ }
193
+ }
194
+ else {
195
+ console.log(chalk.red(`✗ "${targetName}" failed — ${s?.error ?? 'unknown'}\n`));
196
+ }
197
+ }
198
+ catch (err) {
199
+ console.log(chalk.red(`✗ Connect failed: ${err?.message ?? err}\n`));
200
+ }
201
+ return true;
202
+ }
203
+ if (sub === 'disconnect') {
204
+ if (!targetName) {
205
+ console.log(chalk.red('\nUsage: /mcp disconnect <name>\n'));
206
+ return true;
207
+ }
208
+ if (!mcpClient.getStatus(targetName)) {
209
+ console.log(chalk.yellow(`\n"${targetName}" is not in the pool.\n`));
210
+ return true;
211
+ }
212
+ try {
213
+ await mcpClient.disconnectOne(targetName);
214
+ console.log(chalk.green(`✓ "${targetName}" disconnected. Config preserved — /mcp connect ${targetName} to bring it back.\n`));
215
+ }
216
+ catch (err) {
217
+ console.log(chalk.red(`✗ Disconnect failed: ${err?.message ?? err}\n`));
218
+ }
219
+ return true;
220
+ }
221
+ console.log(chalk.red(`\nUnknown /mcp subcommand "${sub}". Usage: /mcp list | /mcp tools [server] | /mcp connect <name> | /mcp disconnect <name> | /mcp reconnect [name]\n`));
222
+ return true;
223
+ }
224
+ function formatIdentityTag(identity) {
225
+ return identity === 'brainrouter' ? chalk.cyan('brainrouter') :
226
+ identity === 'third-party' ? chalk.yellow('third-party') :
227
+ chalk.gray('unknown');
228
+ }
229
+ function groupServerIdsByEcosystem(ids, identityFor) {
230
+ const brainrouter = [];
231
+ const other = [];
232
+ for (const id of ids.slice().sort()) {
233
+ if (identityFor(id) === 'brainrouter')
234
+ brainrouter.push(id);
235
+ else
236
+ other.push(id);
237
+ }
238
+ const sections = [];
239
+ if (brainrouter.length > 0) {
240
+ sections.push({ title: chalk.bold.cyan('BrainRouter MCP (Our Ecosystem)'), ids: brainrouter });
241
+ }
242
+ if (other.length > 0) {
243
+ sections.push({ title: chalk.bold.yellow('Third-party MCPs (Other)'), ids: other });
244
+ }
245
+ return sections;
246
+ }
247
+ async function disconnectOtherBrainrouterServers(ctx, targetName) {
248
+ const targetProfile = ctx.config.servers?.[targetName];
249
+ if (!targetProfile)
250
+ return;
251
+ if (resolveIdentityFromConfig(targetProfile, targetName) !== 'brainrouter')
252
+ return;
253
+ for (const [id, profile] of Object.entries(ctx.config.servers ?? {})) {
254
+ if (id === targetName)
255
+ continue;
256
+ if (resolveIdentityFromConfig(profile, id) !== 'brainrouter')
257
+ continue;
258
+ const status = ctx.mcpClient.getStatus(id);
259
+ if (status?.status === 'connected' || status?.status === 'connecting') {
260
+ await ctx.mcpClient.disconnectOne(id);
261
+ }
262
+ }
263
+ }
264
+ function activateBrainrouterProfile(ctx, targetName) {
265
+ const status = ctx.mcpClient.getStatus(targetName);
266
+ const profile = ctx.config.servers?.[targetName];
267
+ const isBrainrouter = status?.identity === 'brainrouter' ||
268
+ (profile ? resolveIdentityFromConfig(profile, targetName) === 'brainrouter' : false);
269
+ if (!isBrainrouter)
270
+ return false;
271
+ ctx.config.activeServer = targetName;
272
+ saveConfig(ctx.config);
273
+ return true;
274
+ }
275
+ function printRefreshedBanner(ctx) {
276
+ const theme = resolveTheme(ctx.agent.workspaceRoot);
277
+ const banner = renderBanner(buildBannerInputs(ctx.config, ctx.agent, ctx.mcpClient), theme);
278
+ if (ctx.repl.replaceBanner) {
279
+ ctx.repl.replaceBanner('\n' + banner);
280
+ }
281
+ else {
282
+ console.log(chalk.gray('Updated active BrainRouter banner:'));
283
+ console.log(banner);
284
+ console.log();
285
+ }
286
+ }
@@ -11,7 +11,7 @@
11
11
  import fs from 'node:fs';
12
12
  import path from 'node:path';
13
13
  import chalk from 'chalk';
14
- import ora from 'ora';
14
+ import { spinner as makeSpinner } from '../spinner.js';
15
15
  import { callMcpTool } from '../../runtime/mcpUtils.js';
16
16
  import { extractMemories, renderMemoryCards } from '../../memory/formatters.js';
17
17
  import { consolidateMemories } from '../../memory/consolidation.js';
@@ -237,7 +237,7 @@ export async function tryHandleMemoryCommand(ctx) {
237
237
  return true;
238
238
  }
239
239
  if (sub === 'consolidate') {
240
- const spinner = ora(chalk.gray('Consolidating memories from MCP into filesystem artifacts...')).start();
240
+ const spinner = makeSpinner(chalk.gray('Consolidating memories from MCP into filesystem artifacts...')).start();
241
241
  try {
242
242
  const result = await consolidateMemories(mcpClient, agent.workspaceRoot, { sessionKey: agent.sessionKey });
243
243
  spinner.succeed(chalk.green(`Consolidated ${result.totalRecords} records.`));
@@ -98,21 +98,23 @@ export async function tryHandleObsCommand(ctx) {
98
98
  {
99
99
  const session = agent.sessionUsage;
100
100
  const metrics = agent.memoryMetrics;
101
- const children = listSessions(agent.workspaceRoot).filter((s) => s.usage);
101
+ // Scope to the live parent: sessions.json is workspace-wide and
102
+ // persists across CLI restarts, so an unfiltered list mixes in every
103
+ // child spawned by every prior CLI process. Filtering by
104
+ // parentSessionKey limits the row to children spawned by THIS parent.
105
+ const children = listSessions(agent.workspaceRoot).filter((s) => s.usage && s.parentSessionKey === agent.sessionKey);
102
106
  const childPrompt = children.reduce((acc, c) => acc + (c.usage?.promptTokens ?? 0), 0);
103
107
  const childCompletion = children.reduce((acc, c) => acc + (c.usage?.completionTokens ?? 0), 0);
104
108
  const childCalls = children.reduce((acc, c) => acc + (c.usage?.calls ?? 0), 0);
105
- // Memory savings estimate:
106
- // - Each recalled record (avg ~200 chars 50 tokens) supplies cross-
107
- // session context that would otherwise require either a manual
108
- // user explanation, a re-read of files, or skill re-discovery.
109
- // Conservative multiplier of to account for the "without memory
110
- // you would have read 3-5 files" replacement cost.
111
- // - Offloaded child output bytes are subtracted from what the parent
112
- // would otherwise have had to carry in context.
113
- const recallSavings = metrics.briefingTokensInjected * 5;
114
- const offloadSavings = Math.round(metrics.offloadCharsAvoided / 4);
115
- const totalSaved = recallSavings + offloadSavings;
109
+ // What we can actually measure:
110
+ // - offload: bytes of child output that did NOT land in the parent's
111
+ // context. These are real tokens not spent on the parent. We
112
+ // subtract the preview that DID land (OFFLOAD_PREVIEW_CHARS is
113
+ // already netted out in tools.ts before recordOffload fires).
114
+ // - briefing tokens: cost, not savings. They're already counted in
115
+ // session.promptTokens. We report them so the user can see how
116
+ // much of the prompt budget memory is consuming.
117
+ const offloadSavedTokens = Math.round(metrics.offloadCharsAvoided / 4);
116
118
  const totalSpent = session.promptTokens + session.completionTokens + childPrompt + childCompletion;
117
119
  console.log(chalk.bold('\nToken usage — this session'));
118
120
  console.log(` Parent: ${chalk.cyan(session.promptTokens.toLocaleString())}↑ ${chalk.cyan(session.completionTokens.toLocaleString())}↓ ${chalk.gray(`(${session.turns} turn${session.turns === 1 ? '' : 's'}, ${session.calls} LLM call${session.calls === 1 ? '' : 's'})`)}`);
@@ -126,17 +128,15 @@ export async function tryHandleObsCommand(ctx) {
126
128
  console.log(chalk.gray(` …and ${children.length - 5} more (see /agents)`));
127
129
  }
128
130
  console.log(` Total this session: ${chalk.bold.cyan(totalSpent.toLocaleString())} tokens`);
129
- console.log(chalk.bold('\nMemory savings (estimated)'));
130
- console.log(` Briefing tokens injected: ${chalk.gray(metrics.briefingTokensInjected.toLocaleString())} (${metrics.recallRecordsConsulted} records consulted)`);
131
- console.log(` Cross-session recall value: ~${chalk.green(recallSavings.toLocaleString())} tokens you'd otherwise spend re-reading files / re-explaining context`);
132
- console.log(` Offload bytes avoided: ${chalk.gray(metrics.offloadCharsAvoided.toLocaleString())} chars (large child outputs that stayed out of parent context)`);
133
- console.log(` → Offload value: ~${chalk.green(offloadSavings.toLocaleString())} tokens`);
134
- console.log(` ${chalk.bold('Total estimated savings:')} ${chalk.bold.green('~' + totalSaved.toLocaleString())} tokens`);
135
- if (totalSpent > 0) {
136
- const ratio = totalSaved / totalSpent;
137
- console.log(chalk.gray(` Ratio: for every 1 token spent, memory saved ~${ratio.toFixed(2)} tokens of context.`));
131
+ console.log(chalk.bold('\nMemory'));
132
+ console.log(` Briefing tokens injected: ${chalk.gray(metrics.briefingTokensInjected.toLocaleString())} ${chalk.gray(`(${metrics.recallRecordsConsulted} records consulted — already included in parent ↑)`)}`);
133
+ console.log(` Child output offloaded: ${chalk.gray(metrics.offloadCharsAvoided.toLocaleString())} chars ${chalk.gray(`(≈${offloadSavedTokens.toLocaleString()} parent tokens not spent)`)}`);
134
+ if (offloadSavedTokens > 0 && totalSpent > 0) {
135
+ const ratio = offloadSavedTokens / totalSpent;
136
+ const display = ratio >= 0.01 ? ratio.toFixed(2) : '<0.01';
137
+ console.log(chalk.gray(` Offload ratio: ~${display} saved per token spent.`));
138
138
  }
139
- console.log(chalk.gray('\n (Estimates use a 5× multiplier on briefing tokens a heuristic for "you would have needed to re-derive this from files/prompts otherwise". Treat as directional, not exact.)\n'));
139
+ console.log(chalk.gray('\n (Offload is measured; briefing tokens are an information-gain stat, not a savings number.)\n'));
140
140
  return true;
141
141
  }
142
142
  case '/feedback':
@@ -5,6 +5,7 @@
5
5
  import chalk from 'chalk';
6
6
  import { childSessionKey } from '../../runtime/mcpUtils.js';
7
7
  import { listRoles } from '../../orchestration/roles.js';
8
+ import { listAll as listAgentDefs } from '../../orchestration/agentRegistry.js';
8
9
  import { formatSessionSummary, getSession, listSessions, reconcileStale } from '../../orchestration/orchestrator.js';
9
10
  import { readPreferences, writePreferences } from '../../state/preferencesStore.js';
10
11
  import { readTranscriptEntries } from '../../state/sessionStore.js';
@@ -26,6 +27,23 @@ export async function tryHandleOrchestrationCommand(ctx) {
26
27
  }
27
28
  case '/agents':
28
29
  {
30
+ if (args[0] === 'defs') {
31
+ const defs = listAgentDefs(agent.workspaceRoot);
32
+ console.log(chalk.bold('\nAgent Definitions:'));
33
+ const ID_W = Math.max(...defs.map((l) => l.def.id.length), 4) + 2;
34
+ const TIER_W = 12;
35
+ const SRC_W = 10;
36
+ console.log(chalk.gray(` ${'ID'.padEnd(ID_W)}${'TIER'.padEnd(TIER_W)}${'SOURCE'.padEnd(SRC_W)}PATH`));
37
+ for (const loaded of defs) {
38
+ const idStr = chalk.cyan(loaded.def.id.padEnd(ID_W));
39
+ const tierColor = loaded.def.tier === 'reasoning' ? chalk.blue : chalk.yellow;
40
+ const tierStr = tierColor(loaded.def.tier.padEnd(TIER_W));
41
+ const srcStr = chalk.gray(loaded.source.padEnd(SRC_W));
42
+ console.log(` ${idStr}${tierStr}${srcStr}${chalk.gray(loaded.filePath)}`);
43
+ }
44
+ console.log();
45
+ return true;
46
+ }
29
47
  reconcileStale(agent.workspaceRoot);
30
48
  const sessions = listSessions(agent.workspaceRoot);
31
49
  // `--json` for scripting. Emits a single JSON line on stdout so
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { randomUUID } from 'node:crypto';
6
6
  import chalk from 'chalk';
7
- import ora from 'ora';
7
+ import { spinner as makeSpinner } from '../spinner.js';
8
8
  import { marked } from 'marked';
9
9
  import { listTranscripts, loadTranscript } from '../../state/sessionStore.js';
10
10
  import { readGoal, resumeGoal } from '../../state/goalStore.js';
@@ -51,6 +51,11 @@ export async function tryHandleSessionCommand(ctx) {
51
51
  return true;
52
52
  }
53
53
  agent.sessionKey = sessionKey;
54
+ // The persisted transcript doesn't record per-call token usage, so
55
+ // we can't reconstruct counters for the resumed session — start
56
+ // counting from this point forward instead of carrying over the
57
+ // pre-resume parent counts (which were for a different session).
58
+ agent.resetSessionCounters();
54
59
  const loaded = agent.loadHistory(entries);
55
60
  console.log(chalk.green(`\n✓ Resumed session ${chalk.cyan(sessionKey)} with ${loaded} prior messages.`));
56
61
  // If the resumed session has a goal that was suspended (paused,
@@ -73,9 +78,12 @@ export async function tryHandleSessionCommand(ctx) {
73
78
  // before/after. Just unpause and kick off the next iteration.
74
79
  const reactivated = resumeGoal(agent.workspaceRoot, sessionKey);
75
80
  if (reactivated) {
76
- // Same rationale as /goal resume drop any stale wrap-up
77
- // steering left over from the budget-trigger that paused us.
78
- agent.removeTaggedSystemMessage('goal-budget-steering');
81
+ // 9d: pre-9d this branch had to drop a `goal-budget-steering`
82
+ // tagged system message left over from a budget-trigger pause.
83
+ // That message no longer exists — the wrap-up directive is
84
+ // folded into the goal-anchor and the anchor is re-rendered
85
+ // by the next runTurn. `refreshSystemPrompt` is still useful
86
+ // here to rebuild any overlays that depend on the active goal.
79
87
  agent.refreshSystemPrompt();
80
88
  console.log(chalk.green(`\n▶ Goal resumed (${reactivated.budget.iterationsUsed}/${reactivated.budget.maxIterations} used). Starting next iteration…\n`));
81
89
  ctx.repl.runAgentTurn(buildGoalKickoffPrompt(reactivated, 'resume'));
@@ -123,7 +131,7 @@ export async function tryHandleSessionCommand(ctx) {
123
131
  }
124
132
  case '/compact':
125
133
  {
126
- const spinner = ora(chalk.gray('Summarizing conversation for compaction...')).start();
134
+ const spinner = makeSpinner(chalk.gray('Summarizing conversation for compaction...')).start();
127
135
  try {
128
136
  const result = await agent.compactHistory();
129
137
  if (!result) {