@aragon-agent/cli 0.5.2 → 0.5.4

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 (87) hide show
  1. package/CHANGELOG.md +127 -0
  2. package/README.md +193 -3
  3. package/dist/agent/controller.d.ts +51 -1
  4. package/dist/agent/controller.js +177 -2
  5. package/dist/agent/headless.d.ts +12 -0
  6. package/dist/agent/headless.js +41 -1
  7. package/dist/agent/reducer.d.ts +48 -0
  8. package/dist/agent/reducer.js +91 -2
  9. package/dist/agent/system-prompt.d.ts +17 -0
  10. package/dist/agent/system-prompt.js +4 -0
  11. package/dist/cli.js +98 -7
  12. package/dist/commands/builtins.d.ts +2 -2
  13. package/dist/commands/builtins.js +133 -3
  14. package/dist/config/app-paths.d.ts +17 -0
  15. package/dist/config/app-paths.js +21 -0
  16. package/dist/config/cli-commands.d.ts +26 -2
  17. package/dist/config/cli-commands.js +83 -6
  18. package/dist/config/env.d.ts +3 -2
  19. package/dist/config/env.js +44 -6
  20. package/dist/config/history-commands.d.ts +20 -0
  21. package/dist/config/history-commands.js +71 -0
  22. package/dist/config/load.d.ts +13 -0
  23. package/dist/config/load.js +107 -6
  24. package/dist/config/migrate-state-out-of-config.d.ts +32 -0
  25. package/dist/config/migrate-state-out-of-config.js +85 -0
  26. package/dist/config/prompt-history.d.ts +86 -0
  27. package/dist/config/prompt-history.js +332 -0
  28. package/dist/config/schema.d.ts +176 -18
  29. package/dist/config/schema.js +162 -8
  30. package/dist/config/store.js +40 -4
  31. package/dist/config/ui-state.d.ts +52 -0
  32. package/dist/config/ui-state.js +149 -0
  33. package/dist/logging/install.d.ts +85 -0
  34. package/dist/logging/install.js +94 -0
  35. package/dist/logging/logger.d.ts +1 -1
  36. package/dist/session/persist.d.ts +16 -0
  37. package/dist/session/persist.js +19 -1
  38. package/dist/team/activity.d.ts +67 -0
  39. package/dist/team/activity.js +130 -0
  40. package/dist/team/bus.d.ts +190 -0
  41. package/dist/team/bus.js +366 -0
  42. package/dist/team/comm-tools.d.ts +61 -0
  43. package/dist/team/comm-tools.js +221 -0
  44. package/dist/team/human-queue.d.ts +55 -0
  45. package/dist/team/human-queue.js +72 -0
  46. package/dist/team/limits.d.ts +123 -0
  47. package/dist/team/limits.js +128 -0
  48. package/dist/team/normalize.d.ts +39 -0
  49. package/dist/team/normalize.js +106 -0
  50. package/dist/team/panel-rows.d.ts +48 -0
  51. package/dist/team/panel-rows.js +75 -0
  52. package/dist/team/prompt.d.ts +49 -0
  53. package/dist/team/prompt.js +87 -0
  54. package/dist/team/report.d.ts +58 -0
  55. package/dist/team/report.js +205 -0
  56. package/dist/team/retry.d.ts +48 -0
  57. package/dist/team/retry.js +56 -0
  58. package/dist/team/runtime.d.ts +122 -0
  59. package/dist/team/runtime.js +406 -0
  60. package/dist/team/subagent.d.ts +90 -0
  61. package/dist/team/subagent.js +294 -0
  62. package/dist/team/task-tool.d.ts +47 -0
  63. package/dist/team/task-tool.js +118 -0
  64. package/dist/team/types.d.ts +172 -0
  65. package/dist/team/types.js +13 -0
  66. package/dist/tools/index.d.ts +48 -2
  67. package/dist/tools/index.js +26 -1
  68. package/dist/ui/App.d.ts +22 -1
  69. package/dist/ui/App.js +144 -18
  70. package/dist/ui/StatusBar.d.ts +21 -0
  71. package/dist/ui/StatusBar.js +5 -2
  72. package/dist/ui/TeamPanel.d.ts +57 -0
  73. package/dist/ui/TeamPanel.js +134 -0
  74. package/dist/ui/Transcript.js +10 -0
  75. package/dist/ui/entries/TeamCard.d.ts +31 -0
  76. package/dist/ui/entries/TeamCard.js +39 -0
  77. package/dist/ui/glyphs.d.ts +10 -0
  78. package/dist/ui/glyphs.js +6 -0
  79. package/dist/ui/layout/AppShell.d.ts +11 -1
  80. package/dist/ui/layout/AppShell.js +3 -3
  81. package/dist/ui/overlays/HelpOverlay.js +13 -0
  82. package/dist/ui/overlays/SettingsScreen.d.ts +12 -1
  83. package/dist/ui/overlays/SettingsScreen.js +38 -1
  84. package/dist/ui/transcript-text.js +20 -0
  85. package/dist/ui/use-startup-notices.d.ts +1 -1
  86. package/dist/ui/use-startup-notices.js +2 -1
  87. package/package.json +2 -2
@@ -0,0 +1,332 @@
1
+ /**
2
+ * The prompts the user submitted, as an append-only JSON Lines file at
3
+ * `<home>/prompt-history.jsonl` (config-state-separation §4.3).
4
+ *
5
+ * ══════════════════════════════════════════════════════════════════════════
6
+ * WHY THIS IS NOT IN config.json ANY MORE: it used to be a `promptHistory`
7
+ * array inside the config file, which meant every single submit rewrote the
8
+ * whole file — API key section included — and left a user who opened
9
+ * `config.json` to change their model scrolling past their own typing. A
10
+ * config file holds what the user DECLARED; this holds what they DID.
11
+ * ══════════════════════════════════════════════════════════════════════════
12
+ *
13
+ * APPEND, never read-modify-write. Two `aragon` windows are an ordinary setup,
14
+ * and `O_APPEND` + one `writeSync` per entry means they interleave instead of
15
+ * overwriting each other's snapshots. It also means a process killed mid-write
16
+ * costs one broken line rather than a truncated array that reads back as "your
17
+ * history is gone" — hence the per-line tolerance in `parseLine`.
18
+ *
19
+ * FAILURE POLICY (C2), the same one `skills/usage.ts` documents: every function
20
+ * here swallows its errors. This is recall convenience; a read-only home
21
+ * directory must degrade to "no history", never to a CLI that will not start.
22
+ * The one thing that policy makes dangerous is a MISSING WRITE — it would be
23
+ * invisible — which is why the write path creates `<home>` first and why a
24
+ * failed append is logged rather than merely ignored.
25
+ */
26
+ import { chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, writeFileSync, writeSync, } from 'node:fs';
27
+ import process from 'node:process';
28
+ import { getHomeRoot, getPromptHistoryPath } from './app-paths.js';
29
+ import { PROMPT_HISTORY_CAP } from './schema.js';
30
+ import { getLogger } from '../logging/logger.js';
31
+ export const PROMPT_HISTORY_FILENAME = 'prompt-history.jsonl';
32
+ export const PROMPT_HISTORY_LINE_VERSION = 1;
33
+ /**
34
+ * Longer submissions are not recorded at all (D-6).
35
+ *
36
+ * Storing a TRUNCATED copy would be worse than storing nothing: the user
37
+ * presses `↑`, gets back something that looks complete, and sends half a
38
+ * prompt. 16 KiB is far past anything typed by hand.
39
+ */
40
+ export const PROMPT_ENTRY_MAX_CHARS = 16_384;
41
+ /** Physical lines tolerated before the file is rewritten down to the cap. */
42
+ export const HISTORY_COMPACT_AT_LINES = 400;
43
+ // ---------------------------------------------------------------------------
44
+ // In-memory state
45
+ // ---------------------------------------------------------------------------
46
+ let memory = null;
47
+ let loadedLines = 0;
48
+ let appendedLines = 0;
49
+ /**
50
+ * Starts `true`, and that initial value is load-bearing (RV-5).
51
+ *
52
+ * `loadConfig()` calls `setHistoryEnabled()`, but the startup migration runs
53
+ * BEFORE any `loadConfig()`. Something has to be true in that window, and
54
+ * `true` is what the config key itself defaults to. The one case in that window
55
+ * that must honour an explicit opt-out — importing a legacy `promptHistory` —
56
+ * reads the value straight off the config file instead of trusting this flag
57
+ * (see `migrate-state-out-of-config.ts`).
58
+ */
59
+ let historyEnabled = true;
60
+ // ---------------------------------------------------------------------------
61
+ // Parsing
62
+ // ---------------------------------------------------------------------------
63
+ function parseLine(line) {
64
+ try {
65
+ const parsed = JSON.parse(line);
66
+ if (!parsed || typeof parsed !== 'object')
67
+ return null;
68
+ // Unknown versions are skipped rather than coerced: that is what lets a
69
+ // future field be added without an older CLI reading the file as garbage.
70
+ if (parsed.v !== PROMPT_HISTORY_LINE_VERSION)
71
+ return null;
72
+ if (typeof parsed.text !== 'string' || parsed.text.length === 0)
73
+ return null;
74
+ const ts = typeof parsed.ts === 'number' && Number.isFinite(parsed.ts) ? parsed.ts : 0;
75
+ return { v: PROMPT_HISTORY_LINE_VERSION, ts, text: parsed.text };
76
+ }
77
+ catch {
78
+ return null;
79
+ }
80
+ }
81
+ /**
82
+ * Read and parse the file. `null` means the read itself failed — distinct from
83
+ * "no file yet", because compaction must not rewrite a file it could not read.
84
+ */
85
+ function readFromDisk() {
86
+ const path = getPromptHistoryPath();
87
+ if (!existsSync(path))
88
+ return { entries: [], lines: 0 };
89
+ let raw;
90
+ try {
91
+ raw = readFileSync(path, 'utf-8');
92
+ }
93
+ catch {
94
+ return null;
95
+ }
96
+ const entries = [];
97
+ let lines = 0;
98
+ for (const line of raw.split('\n')) {
99
+ if (line.trim().length === 0)
100
+ continue;
101
+ lines += 1;
102
+ const entry = parseLine(line);
103
+ if (entry)
104
+ entries.push(entry);
105
+ }
106
+ return { entries, lines };
107
+ }
108
+ /**
109
+ * Drop repeats, keeping the LAST occurrence, then keep the newest `cap`.
110
+ *
111
+ * Oldest-first ordering is the contract `PromptInput` walks with `↑`/`↓`, and
112
+ * "last occurrence wins" reproduces what the old in-config implementation did
113
+ * with `filter(p => p !== text)` followed by an append.
114
+ */
115
+ function dedupeEntries(entries) {
116
+ const seen = new Set();
117
+ const reversed = [];
118
+ for (let i = entries.length - 1; i >= 0; i -= 1) {
119
+ const entry = entries[i];
120
+ if (seen.has(entry.text))
121
+ continue;
122
+ seen.add(entry.text);
123
+ reversed.push(entry);
124
+ }
125
+ reversed.reverse();
126
+ return reversed.slice(-PROMPT_HISTORY_CAP);
127
+ }
128
+ // ---------------------------------------------------------------------------
129
+ // Read
130
+ // ---------------------------------------------------------------------------
131
+ /**
132
+ * The recall list: oldest first, de-duplicated, capped. Cached for the life of
133
+ * the process so `/reload` — which calls `loadConfig()` again — cannot discard
134
+ * entries added during this session.
135
+ *
136
+ * Reads even when `historyEnabled` is false (RV-12): turning recording off
137
+ * stops new writes, but someone who wants to see what is already stored before
138
+ * deleting it must not be told "nothing here" while the file still exists.
139
+ */
140
+ export function loadPromptHistory() {
141
+ if (memory !== null)
142
+ return memory;
143
+ const read = readFromDisk() ?? { entries: [], lines: 0 };
144
+ memory = dedupeEntries(read.entries).map((entry) => entry.text);
145
+ loadedLines = read.lines;
146
+ appendedLines = 0;
147
+ return memory;
148
+ }
149
+ /** Every entry on disk with its timestamp, newest first. For `history list`. */
150
+ export function listPromptHistory() {
151
+ const read = readFromDisk();
152
+ if (!read)
153
+ return [];
154
+ return [...read.entries].reverse();
155
+ }
156
+ // ---------------------------------------------------------------------------
157
+ // Write
158
+ // ---------------------------------------------------------------------------
159
+ /**
160
+ * Append one line. Returns whether it reached the disk.
161
+ *
162
+ * `mkdirSync` IS NOT DEFENSIVE NOISE — deleting it breaks a real user. `<home>`
163
+ * is created by the logger (only when `log.toFile` is on) or by the first
164
+ * config write (only when the user changes a setting); someone on a fresh
165
+ * install who did neither has no directory here, `openSync` throws `ENOENT`,
166
+ * the swallow below hides it, and their history silently never persists.
167
+ *
168
+ * A short-lived descriptor rather than a kept-open one: this is a once-per-
169
+ * submit write, and a descriptor held for the life of the process would make
170
+ * `aragon history clear` unable to delete the file on Windows.
171
+ */
172
+ function appendLine(entry) {
173
+ try {
174
+ mkdirSync(getHomeRoot(), { recursive: true });
175
+ const fd = openSync(getPromptHistoryPath(), 'a', 0o600);
176
+ try {
177
+ writeSync(fd, `${JSON.stringify(entry)}\n`);
178
+ }
179
+ finally {
180
+ closeSync(fd);
181
+ }
182
+ return true;
183
+ }
184
+ catch (err) {
185
+ getLogger().warn('history', 'history_append_failed', {
186
+ reason: err instanceof Error ? err.message : String(err),
187
+ });
188
+ return false;
189
+ }
190
+ }
191
+ /**
192
+ * Record one submitted prompt and return THE UPDATED LIST.
193
+ *
194
+ * The return value is the whole point of the signature (RV-1). `App.tsx` writes
195
+ * `setPromptHistory(appendPrompt(text))`: the recall list rendered by the
196
+ * composer is React state, and a module-level array updated in here re-renders
197
+ * nothing. Drop the return value and `↑` stops recalling what was just
198
+ * submitted — but only until the next restart, which makes it about as hard to
199
+ * diagnose as a bug in this area gets.
200
+ *
201
+ * Returns the current list unchanged when recording is off, the text is blank,
202
+ * or the text is oversized, so callers never need a branch.
203
+ */
204
+ export function appendPrompt(text, now = Date.now()) {
205
+ const current = loadPromptHistory();
206
+ if (!historyEnabled)
207
+ return current;
208
+ if (text.trim().length === 0)
209
+ return current;
210
+ if (text.length > PROMPT_ENTRY_MAX_CHARS) {
211
+ getLogger().debug('history', 'prompt_history_entry_too_large', {
212
+ chars: text.length,
213
+ max: PROMPT_ENTRY_MAX_CHARS,
214
+ });
215
+ return current;
216
+ }
217
+ memory = [...current.filter((prompt) => prompt !== text), text].slice(-PROMPT_HISTORY_CAP);
218
+ if (appendLine({ v: PROMPT_HISTORY_LINE_VERSION, ts: now, text })) {
219
+ appendedLines += 1;
220
+ if (loadedLines + appendedLines > HISTORY_COMPACT_AT_LINES)
221
+ compact();
222
+ }
223
+ return memory;
224
+ }
225
+ /**
226
+ * Rewrite the file down to the cap.
227
+ *
228
+ * THE RE-READ ON THE FIRST LINE IS THE WHOLE POINT (RV-3). Writing this
229
+ * process's in-memory array over the file would reintroduce exactly the
230
+ * multi-instance clobbering that append-only exists to avoid — and not "a few
231
+ * trailing entries" either, but everything every other instance wrote since
232
+ * this one last read. A failed read therefore skips compaction entirely: the
233
+ * file keeps growing and the next append tries again, which is strictly better
234
+ * than replacing it with a stale snapshot.
235
+ */
236
+ function compact() {
237
+ const disk = readFromDisk();
238
+ if (!disk)
239
+ return;
240
+ // Entries this session has but the file does not — normally none, since the
241
+ // appends above are already on disk. It only matters if a read raced a write.
242
+ const onDisk = new Set(disk.entries.map((entry) => entry.text));
243
+ const carried = (memory ?? [])
244
+ .filter((text) => !onDisk.has(text))
245
+ .map((text) => ({ v: PROMPT_HISTORY_LINE_VERSION, ts: Date.now(), text }));
246
+ const kept = dedupeEntries([...disk.entries, ...carried]);
247
+ const path = getPromptHistoryPath();
248
+ const tmp = `${path}.tmp-${process.pid}`;
249
+ try {
250
+ mkdirSync(getHomeRoot(), { recursive: true });
251
+ const payload = kept.map((entry) => `${JSON.stringify(entry)}\n`).join('');
252
+ writeFileSync(tmp, payload, { encoding: 'utf-8', mode: 0o600 });
253
+ renameSync(tmp, path);
254
+ // rename does not preserve mode everywhere, and this file holds the user's
255
+ // prompts verbatim — re-apply it rather than hope (RV-6, as `store.ts` does).
256
+ if (process.platform !== 'win32')
257
+ chmodSync(path, 0o600);
258
+ memory = kept.map((entry) => entry.text);
259
+ loadedLines = kept.length;
260
+ appendedLines = 0;
261
+ }
262
+ catch (err) {
263
+ try {
264
+ rmSync(tmp, { force: true });
265
+ }
266
+ catch {
267
+ // Nothing further to try; the original file is untouched either way.
268
+ }
269
+ getLogger().warn('history', 'history_compact_failed', {
270
+ reason: err instanceof Error ? err.message : String(err),
271
+ });
272
+ }
273
+ }
274
+ /**
275
+ * Migration-only: append the texts that are not in the file yet, returning how
276
+ * many were written.
277
+ *
278
+ * Ignores `historyEnabled` on purpose — the caller decides, because it is the
279
+ * only code that can see an explicit `historyEnabled: false` in the config file
280
+ * before `loadConfig()` has run (§4.6 step 3).
281
+ */
282
+ export function importLegacyPromptHistory(entries) {
283
+ const read = readFromDisk();
284
+ if (!read)
285
+ return 0;
286
+ const known = new Set(read.entries.map((entry) => entry.text));
287
+ let written = 0;
288
+ for (const text of entries) {
289
+ if (typeof text !== 'string')
290
+ continue;
291
+ if (text.trim().length === 0)
292
+ continue;
293
+ if (text.length > PROMPT_ENTRY_MAX_CHARS)
294
+ continue;
295
+ if (known.has(text))
296
+ continue;
297
+ if (!appendLine({ v: PROMPT_HISTORY_LINE_VERSION, ts: Date.now(), text }))
298
+ break;
299
+ known.add(text);
300
+ written += 1;
301
+ }
302
+ // The cache, if anything already built one, no longer matches the file.
303
+ if (written > 0)
304
+ memory = null;
305
+ return written;
306
+ }
307
+ /** Delete the file and empty the cache. Returns how many entries were removed. */
308
+ export function clearPromptHistory() {
309
+ const removed = readFromDisk()?.entries.length ?? 0;
310
+ try {
311
+ rmSync(getPromptHistoryPath(), { force: true });
312
+ }
313
+ catch {
314
+ // Same policy as every other failure here: a file we could not delete is
315
+ // not worth an error the user can do nothing about.
316
+ }
317
+ memory = [];
318
+ loadedLines = 0;
319
+ appendedLines = 0;
320
+ return removed;
321
+ }
322
+ /** Called once by `loadConfig()` after `historyEnabled` has been resolved. */
323
+ export function setHistoryEnabled(enabled) {
324
+ historyEnabled = enabled;
325
+ }
326
+ /** Test-only: forget the cache, the line counters and the switch. */
327
+ export function resetPromptHistoryForTests() {
328
+ memory = null;
329
+ loadedLines = 0;
330
+ appendedLines = 0;
331
+ historyEnabled = true;
332
+ }
@@ -5,7 +5,7 @@
5
5
  * See spec §3.5 (resolution pipeline), §6.1 (config file), and §3.8 (timeout
6
6
  * invariant).
7
7
  */
8
- import type { ThinkingLevel } from '@aragon-agent/core';
8
+ import { type ThinkingLevel } from '@aragon-agent/core';
9
9
  import { type LogLevelName } from '../logging/levels.js';
10
10
  /**
11
11
  * `ModelRegistry` seeds builtin lists for five providers, but `initProviders()`
@@ -55,9 +55,13 @@ export declare const DEFAULT_TOOL_TIMEOUT_MS = 180000;
55
55
  export declare const IDLE_TIMEOUT_MARGIN_MS = 30000;
56
56
  /** Watchdog idle timeout (§3.8) — must stay ≥ toolTimeout so long tools survive. */
57
57
  export declare const DEFAULT_IDLE_TIMEOUT_MS: number;
58
- /** Cap on persisted prompt history / recent-model MRU lists. */
58
+ /**
59
+ * Cap on the recall list.
60
+ *
61
+ * The list itself now lives in `<home>/prompt-history.jsonl`; this constant is
62
+ * consumed by `config/prompt-history.ts`, not by the UI.
63
+ */
59
64
  export declare const PROMPT_HISTORY_CAP = 100;
60
- export declare const RECENT_MODELS_CAP = 10;
61
65
  /**
62
66
  * How many trailing entries the full-screen viewport renders (invariant I-3).
63
67
  * Yoga lays out every child even when `overflow: hidden` clips it, so the cost
@@ -190,12 +194,69 @@ export declare const DEFAULT_LOG_CONFIG: LogConfig;
190
194
  * but none of it".
191
195
  */
192
196
  export declare function clampLogConfig(raw: unknown): LogConfig;
197
+ export interface TeamConfig {
198
+ /** Register the `task` tool at all. Defaults to TRUE (R-b). */
199
+ enabled: boolean;
200
+ /** Children per dispatch. Clamped to [1, HARD_MAX_SUBAGENTS] (R-g). */
201
+ maxSubagents: number;
202
+ /**
203
+ * Children in flight at once.
204
+ *
205
+ * A SEPARATE KNOB FROM `maxSubagents` on purpose (D-6): provider rate limits
206
+ * are a different constraint from context economics, and folding them into one
207
+ * number makes both wrong. Five simultaneous streams against one API key is a
208
+ * reliable way to collect HTTP 429s, each of which lands in the report as a
209
+ * failed subagent.
210
+ */
211
+ maxConcurrent: number;
212
+ /** One child's wall clock before its own abort. */
213
+ subagentTimeoutMs: number;
214
+ /** The whole dispatch's wall clock. Also the `task` tool-timeout override. */
215
+ dispatchTimeoutMs: number;
216
+ /** Runaway-loop cap: turns one child may take before it is stopped. */
217
+ maxTurnsPerSubagent: number;
218
+ }
219
+ /**
220
+ * The requirement's ceiling on the fan-out (R-g).
221
+ *
222
+ * Re-exported from `TEAM_LIMITS` rather than re-spelled, because it is enforced
223
+ * in TWO places — here on the config value, and again in
224
+ * `normalizeSubagentSpecs` on what the model asked for — and two copies of the
225
+ * same 10 is exactly how a hand-edited `config.json` ends up being able to raise
226
+ * a ceiling the requirement fixed.
227
+ */
228
+ export declare const HARD_MAX_SUBAGENTS: 10;
229
+ export declare const DEFAULT_TEAM_CONFIG: TeamConfig;
230
+ /**
231
+ * THE single gate for every team-config read AND write, mirroring
232
+ * `clampSkillsConfig` / `clampLogConfig` for the reason those two record:
233
+ * hardening only the read path leaves a bad value on disk that reverts to the
234
+ * default on every launch, which presents to the user as "my setting won't
235
+ * stick".
236
+ *
237
+ * `maxConcurrent` is additionally capped at `maxSubagents`: a pool wider than
238
+ * the population it draws from is not wrong so much as meaningless, and letting
239
+ * the two disagree would make the panel's `n/m` readout confusing for no gain.
240
+ */
241
+ export declare function clampTeamConfig(raw: unknown): TeamConfig;
193
242
  export interface PersistedConfig {
194
243
  version: number;
195
244
  provider: string;
196
245
  model: string;
197
246
  baseUrl: string | null;
198
247
  thinkingLevel: ThinkingLevel;
248
+ /**
249
+ * Output token cap.
250
+ *
251
+ * a number — an explicit cap, clamped to `[256, 200000]` on the way in AND
252
+ * on the way out, then clamped again at request time down to the
253
+ * target model's real ceiling when that ceiling is lower.
254
+ * `null` — AUTO: use each model's own ceiling, never above 64000.
255
+ * absent — the product default, 64000.
256
+ *
257
+ * `null` MEANS AUTO. It used to resolve to 64000 by accident (`pick()` skips
258
+ * `null`), which left the user no way to express "let the model decide".
259
+ */
199
260
  maxTokens: number | null;
200
261
  theme: ThemeName;
201
262
  /** Replace spinners with a static glyph for calmer, low-motion output. */
@@ -211,8 +272,15 @@ export interface PersistedConfig {
211
272
  idleTimeoutMs: number;
212
273
  /** provider -> key. Masked in the UI; the file is written 0600 on POSIX. */
213
274
  apiKeys: Record<string, string | null>;
214
- recentModels: string[];
215
- promptHistory: string[];
275
+ /**
276
+ * Record submitted prompts to `<home>/prompt-history.jsonl` at all (D-9).
277
+ *
278
+ * A real preference, and the reason it belongs here rather than in
279
+ * `state.json`: the user declares it, and switching it off is a decision they
280
+ * expect to survive. Command-line only, like `log.redactSecrets` — there is
281
+ * no settings-screen entry, because it is set once and then forgotten.
282
+ */
283
+ historyEnabled: boolean;
216
284
  /** Entry spacing: `comfortable` keeps a blank line at turn boundaries (§4.3). */
217
285
  density: DensityMode;
218
286
  /** Show the composer hint row at all. */
@@ -225,15 +293,6 @@ export interface PersistedConfig {
225
293
  * heuristics for an explicit `true` to override (D-10).
226
294
  */
227
295
  mouse: boolean;
228
- /**
229
- * Whether the one-time "hold Shift to select text" notice has been shown
230
- * (§6.1). Bookkeeping for a one-shot notice, NOT a preference — hence no
231
- * flag, no env var and no `config set` key. Giving it a user-facing channel
232
- * would invite it to be treated as one.
233
- */
234
- mouseNoticeSeen: boolean;
235
- /** Lifetime submit count driving the progressive-disclosure fade (§4.6). */
236
- submitCount: number;
237
296
  /** Start every session in PLAN mode (plan-mode §4.4). */
238
297
  planModeDefault: boolean;
239
298
  /** `ask_user` rounds allowed per user turn; clamped to [1, 10]. */
@@ -244,13 +303,40 @@ export interface PersistedConfig {
244
303
  skills: SkillsConfig;
245
304
  /** Logging settings. The THIRD nested object; scalars only (see LogConfig). */
246
305
  log: LogConfig;
306
+ /** Team mode. The FOURTH nested object; scalars only (see TeamConfig). */
307
+ team: TeamConfig;
247
308
  }
248
309
  export declare const DEFAULT_CONFIG: PersistedConfig;
310
+ /**
311
+ * Runtime state that used to be stored in `config.json` by mistake
312
+ * (config-state-separation §4.5). Each one now lives in its own file:
313
+ * `promptHistory` → `prompt-history.jsonl`, `submitCount` / `mouseNoticeSeen`
314
+ * → `state.json`, and `recentModels` was dead — nothing ever wrote it.
315
+ */
316
+ export declare const LEGACY_STATE_KEYS: readonly ["promptHistory", "submitCount", "mouseNoticeSeen", "recentModels"];
317
+ /**
318
+ * THE single gate that keeps those keys out of `config.json`, and the one thing
319
+ * in this change that cannot be skipped.
320
+ *
321
+ * `loadPersistedConfig()` merges `{ ...DEFAULT_CONFIG, ...partial }` where
322
+ * `partial` is the raw parse of whatever is on disk. Removing the fields from
323
+ * the interface therefore removes NOTHING from the file: the keys ride through
324
+ * the spread and `updatePersistedConfig` writes them straight back. The user
325
+ * deletes `promptHistory` by hand and watches it grow back.
326
+ *
327
+ * Applied on the READ side on purpose, which is what makes it self-healing:
328
+ * every write goes read-strip-merge-write, so a hand-edited file, a downgrade
329
+ * to 0.5.x and back, or a half-finished migration all converge on a clean file
330
+ * at the next write. Same shape as `clampSkillsConfig` / `clampLogConfig` —
331
+ * one door for both directions.
332
+ */
333
+ export declare function stripLegacyStateKeys<T extends object>(raw: T): T;
249
334
  export interface CliConfig {
250
335
  provider: string;
251
336
  model: string;
252
337
  baseUrl?: string;
253
338
  thinkingLevel: ThinkingLevel;
339
+ /** The resolved cap, or `undefined` for AUTO (per-model ceiling, ≤ 64000). */
254
340
  maxTokens?: number;
255
341
  theme: ThemeName;
256
342
  reducedMotion: boolean;
@@ -274,8 +360,15 @@ export interface CliConfig {
274
360
  idleTimeoutMs: number;
275
361
  /** provider -> resolved key (config-file key ?? provider env var). */
276
362
  apiKeys: Record<string, string | undefined>;
277
- recentModels: string[];
278
- promptHistory: string[];
363
+ /**
364
+ * Whether submitted prompts are recorded (D-9).
365
+ *
366
+ * The recall LIST is deliberately not here: its only consumer is one
367
+ * `useState` seed in `App.tsx`, and carrying it on `CliConfig` would make
368
+ * every `loadConfig()` — `aragon logs tail` and `aragon skills list`
369
+ * included — read a file it has no use for (D-4).
370
+ */
371
+ historyEnabled: boolean;
279
372
  density: DensityMode;
280
373
  hints: boolean;
281
374
  /**
@@ -283,7 +376,6 @@ export interface CliConfig {
283
376
  * only). Resolved by `resolveMouse` in `load.ts`.
284
377
  */
285
378
  mouse: boolean;
286
- submitCount: number;
287
379
  /**
288
380
  * Whether this session opens in PLAN mode.
289
381
  *
@@ -299,6 +391,23 @@ export interface CliConfig {
299
391
  skillsRuntime: SkillsRuntimeOptions;
300
392
  /** Resolved logging settings; the authoritative input to `Logger.reconfigure`. */
301
393
  log: LogConfig;
394
+ /**
395
+ * Resolved team-mode settings (team-subagents §3.10).
396
+ *
397
+ * `enabled` here decides whether `task` is REGISTERED, which happens once at
398
+ * construction and can never be undone (§4.5 / D-17). The live on/off switch
399
+ * `/team` flips is a separate flag on the controller.
400
+ */
401
+ team: TeamConfig;
402
+ /**
403
+ * Lifetime submit count driving the composer hint fade (`HINT_FADE_AFTER`).
404
+ *
405
+ * Read from `<home>/state.json` rather than the config file. It stays on
406
+ * `CliConfig` — unlike the recall list — because it is one scalar that is
407
+ * already plumbed through `controller.getConfig()` / `setSubmitCount()` and
408
+ * read during render (D-4).
409
+ */
410
+ submitCount: number;
302
411
  /** Working directory for tools (default `process.cwd()`, overridable). */
303
412
  cwd: string;
304
413
  /** Whether ANSI color is enabled. */
@@ -310,8 +419,57 @@ export interface CliConfig {
310
419
  /** One-shot `--api-key` override; applies to the active provider only. */
311
420
  apiKeyOverride?: string;
312
421
  }
313
- /** Coerce an arbitrary value into a positive integer, or `undefined`. */
422
+ /**
423
+ * Coerce an arbitrary value into a positive integer, or `undefined`.
424
+ *
425
+ * Kept because callers outside this feature still use it, and deliberately
426
+ * WITHOUT the range clamp so its behaviour for in-range values is byte-identical
427
+ * to what it always was. New code wants `clampMaxTokens`.
428
+ */
314
429
  export declare function coerceMaxTokens(v: unknown): number | undefined;
430
+ /**
431
+ * The accepted range for an explicit output cap.
432
+ *
433
+ * The floor is the engine's: below it a turn cannot produce a usable tool call.
434
+ * The ceiling is a sanity bound on a hand-edited file, not a model limit — the
435
+ * real per-model ceiling is applied later, by `resolveOutputTokens`.
436
+ */
437
+ export declare const MAX_TOKENS_RANGE: {
438
+ min: number;
439
+ max: number;
440
+ };
441
+ /** `auto`, an empty string and `0` all mean AUTO — "let the model decide". */
442
+ export declare function isAutoToken(v: unknown): boolean;
443
+ /**
444
+ * THE single gate for every `maxTokens` read AND write, in the shape
445
+ * `clampSkillsConfig` / `clampLogConfig` / `clampTeamConfig` already use.
446
+ *
447
+ * Both directions on purpose: hardening only the read path leaves a bad value on
448
+ * disk that reverts to a default on every launch, which presents to the user as
449
+ * "my setting won't stick".
450
+ *
451
+ * Returns `undefined` for AUTO and for anything unusable — the caller decides
452
+ * which of the two it was, because they mean different things.
453
+ */
454
+ export declare function clampMaxTokens(v: unknown): number | undefined;
455
+ /**
456
+ * Parse text typed into the settings screen, a slash command, or `config set`.
457
+ *
458
+ * Distinguishes AUTO from "the user typed nonsense", which `clampMaxTokens`
459
+ * cannot: both come back as `undefined` there, and treating a typo as AUTO would
460
+ * silently discard the number the user meant to set.
461
+ */
462
+ export declare function parseMaxTokensInput(raw: string): {
463
+ kind: 'auto';
464
+ } | {
465
+ kind: 'value';
466
+ value: number;
467
+ clamped: boolean;
468
+ } | {
469
+ kind: 'invalid';
470
+ };
471
+ /** The product default, re-exported so the CLI never spells the number itself. */
472
+ export declare const DEFAULT_MAX_TOKENS = 64000;
315
473
  /** Coerce a value into a positive integer with a fallback (for timeouts). */
316
474
  export declare function coercePositiveInt(v: unknown, fallback: number): number;
317
475
  /**