@aragon-agent/cli 0.5.1 → 0.5.3

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 (106) hide show
  1. package/CHANGELOG.md +117 -0
  2. package/README.md +155 -5
  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 +131 -6
  12. package/dist/commands/builtins.js +82 -0
  13. package/dist/config/app-paths.d.ts +17 -0
  14. package/dist/config/app-paths.js +21 -0
  15. package/dist/config/cli-commands.d.ts +26 -2
  16. package/dist/config/cli-commands.js +63 -4
  17. package/dist/config/env.d.ts +2 -2
  18. package/dist/config/env.js +36 -2
  19. package/dist/config/history-commands.d.ts +20 -0
  20. package/dist/config/history-commands.js +71 -0
  21. package/dist/config/load.d.ts +15 -0
  22. package/dist/config/load.js +88 -5
  23. package/dist/config/migrate-state-out-of-config.d.ts +32 -0
  24. package/dist/config/migrate-state-out-of-config.js +85 -0
  25. package/dist/config/prompt-history.d.ts +86 -0
  26. package/dist/config/prompt-history.js +332 -0
  27. package/dist/config/schema.d.ts +125 -9
  28. package/dist/config/schema.js +90 -5
  29. package/dist/config/store.js +13 -4
  30. package/dist/config/ui-state.d.ts +52 -0
  31. package/dist/config/ui-state.js +149 -0
  32. package/dist/input/mouse-events.d.ts +61 -0
  33. package/dist/input/mouse-events.js +153 -0
  34. package/dist/input/stdin-mouse-filter.d.ts +42 -0
  35. package/dist/input/stdin-mouse-filter.js +153 -0
  36. package/dist/logging/install.d.ts +85 -0
  37. package/dist/logging/install.js +94 -0
  38. package/dist/logging/logger.d.ts +1 -1
  39. package/dist/session/persist.d.ts +16 -0
  40. package/dist/session/persist.js +19 -1
  41. package/dist/team/activity.d.ts +67 -0
  42. package/dist/team/activity.js +130 -0
  43. package/dist/team/bus.d.ts +190 -0
  44. package/dist/team/bus.js +366 -0
  45. package/dist/team/comm-tools.d.ts +61 -0
  46. package/dist/team/comm-tools.js +221 -0
  47. package/dist/team/human-queue.d.ts +55 -0
  48. package/dist/team/human-queue.js +72 -0
  49. package/dist/team/limits.d.ts +123 -0
  50. package/dist/team/limits.js +128 -0
  51. package/dist/team/normalize.d.ts +39 -0
  52. package/dist/team/normalize.js +106 -0
  53. package/dist/team/panel-rows.d.ts +48 -0
  54. package/dist/team/panel-rows.js +75 -0
  55. package/dist/team/prompt.d.ts +49 -0
  56. package/dist/team/prompt.js +87 -0
  57. package/dist/team/report.d.ts +58 -0
  58. package/dist/team/report.js +205 -0
  59. package/dist/team/retry.d.ts +48 -0
  60. package/dist/team/retry.js +56 -0
  61. package/dist/team/runtime.d.ts +122 -0
  62. package/dist/team/runtime.js +406 -0
  63. package/dist/team/subagent.d.ts +90 -0
  64. package/dist/team/subagent.js +294 -0
  65. package/dist/team/task-tool.d.ts +47 -0
  66. package/dist/team/task-tool.js +118 -0
  67. package/dist/team/types.d.ts +172 -0
  68. package/dist/team/types.js +13 -0
  69. package/dist/tools/index.d.ts +48 -2
  70. package/dist/tools/index.js +26 -1
  71. package/dist/ui/App.d.ts +9 -1
  72. package/dist/ui/App.js +140 -21
  73. package/dist/ui/Composer.d.ts +6 -1
  74. package/dist/ui/Composer.js +2 -2
  75. package/dist/ui/PromptInput.d.ts +15 -1
  76. package/dist/ui/PromptInput.js +38 -1
  77. package/dist/ui/StatusBar.d.ts +21 -0
  78. package/dist/ui/StatusBar.js +5 -2
  79. package/dist/ui/TeamPanel.d.ts +57 -0
  80. package/dist/ui/TeamPanel.js +134 -0
  81. package/dist/ui/Transcript.js +10 -0
  82. package/dist/ui/entries/TeamCard.d.ts +31 -0
  83. package/dist/ui/entries/TeamCard.js +39 -0
  84. package/dist/ui/glyphs.d.ts +22 -0
  85. package/dist/ui/glyphs.js +10 -0
  86. package/dist/ui/layout/AppShell.d.ts +20 -1
  87. package/dist/ui/layout/AppShell.js +39 -4
  88. package/dist/ui/layout/ScrollIndicator.d.ts +31 -0
  89. package/dist/ui/layout/ScrollIndicator.js +15 -0
  90. package/dist/ui/layout/ScrollViewport.d.ts +8 -2
  91. package/dist/ui/layout/ScrollViewport.js +24 -4
  92. package/dist/ui/layout/regions.d.ts +55 -0
  93. package/dist/ui/layout/regions.js +52 -0
  94. package/dist/ui/layout/scroll-indicator.d.ts +27 -0
  95. package/dist/ui/layout/scroll-indicator.js +35 -0
  96. package/dist/ui/layout/scroll.d.ts +9 -0
  97. package/dist/ui/layout/scroll.js +23 -0
  98. package/dist/ui/overlays/HelpOverlay.js +14 -0
  99. package/dist/ui/screen.d.ts +14 -1
  100. package/dist/ui/screen.js +37 -3
  101. package/dist/ui/transcript-text.js +20 -0
  102. package/dist/ui/use-startup-notices.d.ts +11 -1
  103. package/dist/ui/use-startup-notices.js +29 -2
  104. package/dist/ui/use-wheel-routing.d.ts +68 -0
  105. package/dist/ui/use-wheel-routing.js +139 -0
  106. 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
+ }
@@ -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,6 +194,51 @@ 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;
@@ -211,14 +260,27 @@ export interface PersistedConfig {
211
260
  idleTimeoutMs: number;
212
261
  /** provider -> key. Masked in the UI; the file is written 0600 on POSIX. */
213
262
  apiKeys: Record<string, string | null>;
214
- recentModels: string[];
215
- promptHistory: string[];
263
+ /**
264
+ * Record submitted prompts to `<home>/prompt-history.jsonl` at all (D-9).
265
+ *
266
+ * A real preference, and the reason it belongs here rather than in
267
+ * `state.json`: the user declares it, and switching it off is a decision they
268
+ * expect to survive. Command-line only, like `log.redactSecrets` — there is
269
+ * no settings-screen entry, because it is set once and then forgotten.
270
+ */
271
+ historyEnabled: boolean;
216
272
  /** Entry spacing: `comfortable` keeps a blank line at turn boundaries (§4.3). */
217
273
  density: DensityMode;
218
274
  /** Show the composer hint row at all. */
219
275
  hints: boolean;
220
- /** Lifetime submit count driving the progressive-disclosure fade (§4.6). */
221
- submitCount: number;
276
+ /**
277
+ * Enable SGR mouse reporting in full-screen mode so the wheel scrolls the
278
+ * region under the pointer (mouse-wheel-region-routing §5.7).
279
+ *
280
+ * A PLAIN BOOLEAN, not a `fullscreen`-style tri-state: there are no
281
+ * heuristics for an explicit `true` to override (D-10).
282
+ */
283
+ mouse: boolean;
222
284
  /** Start every session in PLAN mode (plan-mode §4.4). */
223
285
  planModeDefault: boolean;
224
286
  /** `ask_user` rounds allowed per user turn; clamped to [1, 10]. */
@@ -229,8 +291,34 @@ export interface PersistedConfig {
229
291
  skills: SkillsConfig;
230
292
  /** Logging settings. The THIRD nested object; scalars only (see LogConfig). */
231
293
  log: LogConfig;
294
+ /** Team mode. The FOURTH nested object; scalars only (see TeamConfig). */
295
+ team: TeamConfig;
232
296
  }
233
297
  export declare const DEFAULT_CONFIG: PersistedConfig;
298
+ /**
299
+ * Runtime state that used to be stored in `config.json` by mistake
300
+ * (config-state-separation §4.5). Each one now lives in its own file:
301
+ * `promptHistory` → `prompt-history.jsonl`, `submitCount` / `mouseNoticeSeen`
302
+ * → `state.json`, and `recentModels` was dead — nothing ever wrote it.
303
+ */
304
+ export declare const LEGACY_STATE_KEYS: readonly ["promptHistory", "submitCount", "mouseNoticeSeen", "recentModels"];
305
+ /**
306
+ * THE single gate that keeps those keys out of `config.json`, and the one thing
307
+ * in this change that cannot be skipped.
308
+ *
309
+ * `loadPersistedConfig()` merges `{ ...DEFAULT_CONFIG, ...partial }` where
310
+ * `partial` is the raw parse of whatever is on disk. Removing the fields from
311
+ * the interface therefore removes NOTHING from the file: the keys ride through
312
+ * the spread and `updatePersistedConfig` writes them straight back. The user
313
+ * deletes `promptHistory` by hand and watches it grow back.
314
+ *
315
+ * Applied on the READ side on purpose, which is what makes it self-healing:
316
+ * every write goes read-strip-merge-write, so a hand-edited file, a downgrade
317
+ * to 0.5.x and back, or a half-finished migration all converge on a clean file
318
+ * at the next write. Same shape as `clampSkillsConfig` / `clampLogConfig` —
319
+ * one door for both directions.
320
+ */
321
+ export declare function stripLegacyStateKeys<T extends object>(raw: T): T;
234
322
  export interface CliConfig {
235
323
  provider: string;
236
324
  model: string;
@@ -259,11 +347,22 @@ export interface CliConfig {
259
347
  idleTimeoutMs: number;
260
348
  /** provider -> resolved key (config-file key ?? provider env var). */
261
349
  apiKeys: Record<string, string | undefined>;
262
- recentModels: string[];
263
- promptHistory: string[];
350
+ /**
351
+ * Whether submitted prompts are recorded (D-9).
352
+ *
353
+ * The recall LIST is deliberately not here: its only consumer is one
354
+ * `useState` seed in `App.tsx`, and carrying it on `CliConfig` would make
355
+ * every `loadConfig()` — `aragon logs tail` and `aragon skills list`
356
+ * included — read a file it has no use for (D-4).
357
+ */
358
+ historyEnabled: boolean;
264
359
  density: DensityMode;
265
360
  hints: boolean;
266
- submitCount: number;
361
+ /**
362
+ * Whether this session may enable SGR mouse reporting (full-screen + TTY
363
+ * only). Resolved by `resolveMouse` in `load.ts`.
364
+ */
365
+ mouse: boolean;
267
366
  /**
268
367
  * Whether this session opens in PLAN mode.
269
368
  *
@@ -279,6 +378,23 @@ export interface CliConfig {
279
378
  skillsRuntime: SkillsRuntimeOptions;
280
379
  /** Resolved logging settings; the authoritative input to `Logger.reconfigure`. */
281
380
  log: LogConfig;
381
+ /**
382
+ * Resolved team-mode settings (team-subagents §3.10).
383
+ *
384
+ * `enabled` here decides whether `task` is REGISTERED, which happens once at
385
+ * construction and can never be undone (§4.5 / D-17). The live on/off switch
386
+ * `/team` flips is a separate flag on the controller.
387
+ */
388
+ team: TeamConfig;
389
+ /**
390
+ * Lifetime submit count driving the composer hint fade (`HINT_FADE_AFTER`).
391
+ *
392
+ * Read from `<home>/state.json` rather than the config file. It stays on
393
+ * `CliConfig` — unlike the recall list — because it is one scalar that is
394
+ * already plumbed through `controller.getConfig()` / `setSubmitCount()` and
395
+ * read during render (D-4).
396
+ */
397
+ submitCount: number;
282
398
  /** Working directory for tools (default `process.cwd()`, overridable). */
283
399
  cwd: string;
284
400
  /** Whether ANSI color is enabled. */
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import { isAbsolute } from 'node:path';
9
9
  import { clampLogLevel } from '../logging/levels.js';
10
+ import { TEAM_LIMITS } from '../team/limits.js';
10
11
  // ---------------------------------------------------------------------------
11
12
  // Provider set — only the adapter-backed providers are usable at run time (R5).
12
13
  // ---------------------------------------------------------------------------
@@ -88,9 +89,13 @@ export const DEFAULT_TOOL_TIMEOUT_MS = 180_000;
88
89
  export const IDLE_TIMEOUT_MARGIN_MS = 30_000;
89
90
  /** Watchdog idle timeout (§3.8) — must stay ≥ toolTimeout so long tools survive. */
90
91
  export const DEFAULT_IDLE_TIMEOUT_MS = DEFAULT_TOOL_TIMEOUT_MS + IDLE_TIMEOUT_MARGIN_MS;
91
- /** Cap on persisted prompt history / recent-model MRU lists. */
92
+ /**
93
+ * Cap on the recall list.
94
+ *
95
+ * The list itself now lives in `<home>/prompt-history.jsonl`; this constant is
96
+ * consumed by `config/prompt-history.ts`, not by the UI.
97
+ */
92
98
  export const PROMPT_HISTORY_CAP = 100;
93
- export const RECENT_MODELS_CAP = 10;
94
99
  /**
95
100
  * How many trailing entries the full-screen viewport renders (invariant I-3).
96
101
  * Yoga lays out every child even when `overflow: hidden` clips it, so the cost
@@ -260,6 +265,52 @@ export function clampLogConfig(raw) {
260
265
  : DEFAULT_LOG_CONFIG.previewChars,
261
266
  };
262
267
  }
268
+ /**
269
+ * The requirement's ceiling on the fan-out (R-g).
270
+ *
271
+ * Re-exported from `TEAM_LIMITS` rather than re-spelled, because it is enforced
272
+ * in TWO places — here on the config value, and again in
273
+ * `normalizeSubagentSpecs` on what the model asked for — and two copies of the
274
+ * same 10 is exactly how a hand-edited `config.json` ends up being able to raise
275
+ * a ceiling the requirement fixed.
276
+ */
277
+ export const HARD_MAX_SUBAGENTS = TEAM_LIMITS.hardMaxSubagents;
278
+ export const DEFAULT_TEAM_CONFIG = {
279
+ enabled: true,
280
+ maxSubagents: 5,
281
+ maxConcurrent: 3,
282
+ subagentTimeoutMs: 300_000,
283
+ dispatchTimeoutMs: 900_000,
284
+ maxTurnsPerSubagent: 24,
285
+ };
286
+ const SUBAGENTS_RANGE = { min: 1, max: HARD_MAX_SUBAGENTS };
287
+ const CONCURRENT_RANGE = { min: 1, max: HARD_MAX_SUBAGENTS };
288
+ const SUBAGENT_TIMEOUT_RANGE = { min: 30_000, max: 1_800_000 };
289
+ const DISPATCH_TIMEOUT_RANGE = { min: 60_000, max: 3_600_000 };
290
+ const TURNS_RANGE = { min: 4, max: 100 };
291
+ /**
292
+ * THE single gate for every team-config read AND write, mirroring
293
+ * `clampSkillsConfig` / `clampLogConfig` for the reason those two record:
294
+ * hardening only the read path leaves a bad value on disk that reverts to the
295
+ * default on every launch, which presents to the user as "my setting won't
296
+ * stick".
297
+ *
298
+ * `maxConcurrent` is additionally capped at `maxSubagents`: a pool wider than
299
+ * the population it draws from is not wrong so much as meaningless, and letting
300
+ * the two disagree would make the panel's `n/m` readout confusing for no gain.
301
+ */
302
+ export function clampTeamConfig(raw) {
303
+ const src = (raw && typeof raw === 'object' ? raw : {});
304
+ const maxSubagents = clampInt(src.maxSubagents, DEFAULT_TEAM_CONFIG.maxSubagents, SUBAGENTS_RANGE);
305
+ return {
306
+ enabled: bool(src.enabled, DEFAULT_TEAM_CONFIG.enabled),
307
+ maxSubagents,
308
+ maxConcurrent: Math.min(maxSubagents, clampInt(src.maxConcurrent, DEFAULT_TEAM_CONFIG.maxConcurrent, CONCURRENT_RANGE)),
309
+ subagentTimeoutMs: clampInt(src.subagentTimeoutMs, DEFAULT_TEAM_CONFIG.subagentTimeoutMs, SUBAGENT_TIMEOUT_RANGE),
310
+ dispatchTimeoutMs: clampInt(src.dispatchTimeoutMs, DEFAULT_TEAM_CONFIG.dispatchTimeoutMs, DISPATCH_TIMEOUT_RANGE),
311
+ maxTurnsPerSubagent: clampInt(src.maxTurnsPerSubagent, DEFAULT_TEAM_CONFIG.maxTurnsPerSubagent, TURNS_RANGE),
312
+ };
313
+ }
263
314
  export const DEFAULT_CONFIG = {
264
315
  version: CONFIG_VERSION,
265
316
  provider: 'anthropic',
@@ -276,17 +327,51 @@ export const DEFAULT_CONFIG = {
276
327
  toolTimeoutMs: DEFAULT_TOOL_TIMEOUT_MS,
277
328
  idleTimeoutMs: DEFAULT_IDLE_TIMEOUT_MS,
278
329
  apiKeys: {},
279
- recentModels: [],
280
- promptHistory: [],
330
+ historyEnabled: true,
281
331
  density: 'comfortable',
282
332
  hints: true,
283
- submitCount: 0,
333
+ mouse: true,
284
334
  planModeDefault: false,
285
335
  planModeMaxAskRounds: DEFAULT_PLAN_MAX_ASK_ROUNDS,
286
336
  planModeHumanTimeoutMs: DEFAULT_PLAN_HUMAN_TIMEOUT_MS,
287
337
  skills: DEFAULT_SKILLS_CONFIG,
288
338
  log: DEFAULT_LOG_CONFIG,
339
+ team: DEFAULT_TEAM_CONFIG,
289
340
  };
341
+ /**
342
+ * Runtime state that used to be stored in `config.json` by mistake
343
+ * (config-state-separation §4.5). Each one now lives in its own file:
344
+ * `promptHistory` → `prompt-history.jsonl`, `submitCount` / `mouseNoticeSeen`
345
+ * → `state.json`, and `recentModels` was dead — nothing ever wrote it.
346
+ */
347
+ export const LEGACY_STATE_KEYS = [
348
+ 'promptHistory',
349
+ 'submitCount',
350
+ 'mouseNoticeSeen',
351
+ 'recentModels',
352
+ ];
353
+ /**
354
+ * THE single gate that keeps those keys out of `config.json`, and the one thing
355
+ * in this change that cannot be skipped.
356
+ *
357
+ * `loadPersistedConfig()` merges `{ ...DEFAULT_CONFIG, ...partial }` where
358
+ * `partial` is the raw parse of whatever is on disk. Removing the fields from
359
+ * the interface therefore removes NOTHING from the file: the keys ride through
360
+ * the spread and `updatePersistedConfig` writes them straight back. The user
361
+ * deletes `promptHistory` by hand and watches it grow back.
362
+ *
363
+ * Applied on the READ side on purpose, which is what makes it self-healing:
364
+ * every write goes read-strip-merge-write, so a hand-edited file, a downgrade
365
+ * to 0.5.x and back, or a half-finished migration all converge on a clean file
366
+ * at the next write. Same shape as `clampSkillsConfig` / `clampLogConfig` —
367
+ * one door for both directions.
368
+ */
369
+ export function stripLegacyStateKeys(raw) {
370
+ const out = { ...raw };
371
+ for (const key of LEGACY_STATE_KEYS)
372
+ delete out[key];
373
+ return out;
374
+ }
290
375
  // ---------------------------------------------------------------------------
291
376
  // Coercion / validation helpers
292
377
  // ---------------------------------------------------------------------------