@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
package/dist/cli.js CHANGED
@@ -21,8 +21,10 @@ import { loadConfig } from './config/load.js';
21
21
  import { getConfigPath, readConfigFile, updatePersistedConfig } from './config/store.js';
22
22
  import { formatMigrationNotice, migrateLegacyState } from './config/migrate-legacy-state.js';
23
23
  import { formatHomeMigrationNotice, migrateToHome } from './config/migrate-home.js';
24
- import { LOG_CONFIG_SET_KEYS, applyLogConfigSet, runConfigEdit, runConfigGet, runConfigHome, runConfigList, warnIfRedactionDisabled, } from './config/cli-commands.js';
25
- import { attachAgentEvents, installLogging, setScreenRestore, setSignalTerminator, } from './logging/install.js';
24
+ import { migrateStateOutOfConfig } from './config/migrate-state-out-of-config.js';
25
+ import { runHistoryCommand } from './config/history-commands.js';
26
+ import { LOG_CONFIG_SET_KEYS, TEAM_CONFIG_SET_KEYS, applyLogConfigSet, applyTeamConfigSet, runConfigEdit, runConfigGet, runConfigHome, runConfigList, warnIfRedactionDisabled, } from './config/cli-commands.js';
27
+ import { attachAgentEvents, attachTeamEvents, installLogging, setScreenRestore, setSignalTerminator, } from './logging/install.js';
26
28
  import { getLogger } from './logging/logger.js';
27
29
  import { runLogsCommand } from './logging/cli-commands.js';
28
30
  import { ADAPTER_PROVIDERS, clampDensity, clampTheme, clampThinkingLevel, clampAskRounds, clampHumanTimeout, clampSkillsIntegrity, clampSkillsToolPolicy, clampTranscriptWindow, coerceMaxTokens, coercePositiveInt, isAdapterProvider, DEFAULT_PLAN_HUMAN_TIMEOUT_MS, DEFAULT_PLAN_MAX_ASK_ROUNDS, DEFAULT_SKILLS_CONFIG, } from './config/schema.js';
@@ -32,6 +34,7 @@ import { runSkillsCommand } from './skills/cli-commands.js';
32
34
  import { runHeadless } from './agent/headless.js';
33
35
  import { App } from './ui/App.js';
34
36
  import { decideRenderMode } from './ui/layout/frame.js';
37
+ import { tryCreateMouseFilter } from './input/stdin-mouse-filter.js';
35
38
  import { enterAltScreen, writeExitTranscript } from './ui/screen.js';
36
39
  import { readExitSnapshot } from './ui/exit-snapshot.js';
37
40
  import { renderTranscriptText } from './ui/transcript-text.js';
@@ -70,11 +73,19 @@ function toFlags(opts) {
70
73
  exitTranscript: opts.exitTranscript,
71
74
  compact: opts.compact,
72
75
  hints: opts.hints,
76
+ // Without this line the flag never reaches `CliFlags` and `--no-mouse` is
77
+ // inert — a failure with no compile error and no runtime error (P1-2).
78
+ mouse: opts.mouse,
73
79
  skills: opts.skills,
74
80
  skill: opts.skill,
75
81
  skillsYes: opts.skillsYes,
76
82
  skillToolPolicy: opts.skillToolPolicy,
77
83
  plan: opts.plan,
84
+ // These two lines are the same failure the `mouse` note above records, one
85
+ // feature later (P1-4): without them `--no-team` and `--team-max` never
86
+ // reach `CliFlags` and are inert, with no compile error and no runtime error.
87
+ team: opts.team,
88
+ teamMax: opts.teamMax,
78
89
  // `--verbose` is just a shorthand for `--log-level debug`; an explicit
79
90
  // `--log-level` still wins, so the two can be combined without surprise.
80
91
  logLevel: opts.logLevel ?? (opts.verbose ? 'debug' : undefined),
@@ -162,8 +173,38 @@ function runInteractive(flags, extras = {}) {
162
173
  const logger = getLogger();
163
174
  logger.info('cli', 'run_interactive', { mode, provider: config.provider, model: config.model });
164
175
  attachAgentEvents(logger, controller);
176
+ // The team stream is CLI-local (D-10), so `attachAgentEvents` cannot see it.
177
+ // Without this a dispatch leaves NO trace in the log file at all — the wrong
178
+ // outcome for the one feature here that runs five agents the user cannot
179
+ // watch (§3.13 / P1-2 / R-19).
180
+ attachTeamEvents(logger, controller);
165
181
  let screen = null;
166
182
  let replayed = false;
183
+ // --- Mouse wheel region routing (mouse-wheel-region-routing §4.2). -------
184
+ //
185
+ // BUILD THE FILTER FIRST, THEN ENABLE REPORTING. The order is invariant I-8
186
+ // and it is not negotiable: `\x1b[?1000h` tells the terminal to send SGR
187
+ // reports, and Ink 5.2.1 has no mouse branch at all, so any report that
188
+ // reaches its key parser is INSERTED INTO THE USER'S MESSAGE (`[` is 0x5B, so
189
+ // `PromptInput`'s `isControlSeq` guard passes it). A window in which
190
+ // reporting is on and Ink holds the raw stream is not a degraded feature — it
191
+ // is every click typing `[<0;12;5M` into the composer, strictly worse than
192
+ // the bug being fixed.
193
+ //
194
+ // `tryCreateMouseFilter` returns `null` rather than throwing, so a
195
+ // construction failure degrades to "the wheel is inert" (rung 3 of the
196
+ // fail-safe ladder), never to "reporting on, no filter".
197
+ // BOTH streams have to be terminals. `stdout.isTTY` is what decides whether
198
+ // `enterAltScreen` writes anything at all, but the reports come back on
199
+ // STDIN, so a TTY stdout with a piped stdin (`aragon config < file`, which
200
+ // reaches `runInteractive` without the stdin check the default action makes)
201
+ // would wrap a stream no report can ever arrive on and ask the terminal to
202
+ // start reporting anyway. §4.2's "non-TTY ⇒ no filter" covers both ends.
203
+ const wantMouse = mode === 'fullscreen' && config.mouse && !!process.stdout.isTTY && !!process.stdin.isTTY;
204
+ const mouseFilter = wantMouse
205
+ ? tryCreateMouseFilter(process.stdin, (reason) => logger.warn('cli', 'mouse_filter_failed', { reason }))
206
+ : null;
207
+ const disposeMouse = () => mouseFilter?.dispose();
167
208
  const replayTranscript = () => {
168
209
  if (replayed || mode !== 'fullscreen' || !config.exitTranscript)
169
210
  return;
@@ -181,7 +222,10 @@ function runInteractive(flags, extras = {}) {
181
222
  }));
182
223
  };
183
224
  if (mode === 'fullscreen') {
184
- screen = enterAltScreen(process.stdout);
225
+ // `mouse` here means "a filter is installed", NOT "the user wants mouse
226
+ // support" — see `AltScreenOptions`. `screen.ts` must never derive it for
227
+ // itself, or a later refactor re-opens the gap I-8 closes.
228
+ screen = enterAltScreen(process.stdout, { mouse: mouseFilter !== null });
185
229
  const restore = () => screen?.restore();
186
230
  // Four idempotent restore paths (§4.4). The signal hook is NOT redundant:
187
231
  // - `waitUntilExit().then()` is a microtask and may never be reached when
@@ -205,20 +249,43 @@ function runInteractive(flags, extras = {}) {
205
249
  process.exit(128 + signo);
206
250
  });
207
251
  }
208
- const instance = render(_jsx(App, { controller: controller, version: VERSION, mode: mode, confirmBridge: confirmBridge, humanInputBridge: humanInputBridge, initialPrompt: extras.initialPrompt, initialOverlay: extras.initialOverlay }),
252
+ const instance = render(_jsx(App, { controller: controller, version: VERSION, mode: mode, confirmBridge: confirmBridge, humanInputBridge: humanInputBridge, initialPrompt: extras.initialPrompt, initialOverlay: extras.initialOverlay, mouseSource: mouseFilter?.source }),
209
253
  // Full-screen owns console.* itself (I-4): Ink's patchConsole writes
210
254
  // straight to stdout and permanently shifts the fixed frame's accounting.
211
- { exitOnCtrlC: false, patchConsole: mode === 'inline' });
255
+ {
256
+ exitOnCtrlC: false,
257
+ patchConsole: mode === 'inline',
258
+ // I-5: with mouse support off the REAL stdin is handed over unwrapped, so
259
+ // every existing path is provably unchanged.
260
+ stdin: mouseFilter?.stdin ?? process.stdin,
261
+ });
212
262
  // The return value MUST be captured — `waitUntilExit()` is the normal-exit
213
- // restore path and it cannot be registered otherwise.
263
+ // restore path and it cannot be registered otherwise. `dispose()` runs on
264
+ // BOTH branches (I-7): a live `'data'` listener on stdin keeps the event loop
265
+ // alive and `aragon` would never return to the shell. The signal path does
266
+ // not need it — it calls `process.exit()`.
267
+ //
268
+ // RESTORE BEFORE DISPOSE. `restore()` writes `?1000l`, so the terminal stops
269
+ // reporting first and anything already in flight is still consumed by a live
270
+ // filter. Disposing first leaves a window — narrow, but real — in which the
271
+ // terminal is still reporting and nothing is draining stdin, so a notch taken
272
+ // in that instant survives in the OS buffer and is handed to the shell as
273
+ // `[<64;12;5M` on exit. That is the I-2 failure mode, just smaller.
214
274
  void instance
215
275
  .waitUntilExit()
216
276
  .then(() => {
217
277
  screen?.restore();
278
+ disposeMouse();
279
+ // Orphaned subagents survive `Ctrl+C` otherwise, and a live child holds
280
+ // the event loop open so `aragon` never returns to the shell (R-15). The
281
+ // App's unmount cleanup also calls this; `dispose()` is idempotent.
282
+ controller.dispose();
218
283
  replayTranscript();
219
284
  })
220
285
  .catch(() => {
221
286
  screen?.restore();
287
+ disposeMouse();
288
+ controller.dispose();
222
289
  });
223
290
  }
224
291
  async function runOneShot(flags, prompt, quiet) {
@@ -232,6 +299,11 @@ async function runOneShot(flags, prompt, quiet) {
232
299
  // toast channel `App.tsx` uses does not exist here.
233
300
  logger.onFailure((reason) => process.stderr.write(`[log] ${reason}\n`));
234
301
  attachAgentEvents(logger, controller);
302
+ // The team stream is CLI-local (D-10), so `attachAgentEvents` cannot see it.
303
+ // Without this a dispatch leaves NO trace in the log file at all — the wrong
304
+ // outcome for the one feature here that runs five agents the user cannot
305
+ // watch (§3.13 / P1-2 / R-19).
306
+ attachTeamEvents(logger, controller);
235
307
  const untrusted = controller.getSkillService().untrustedDirs();
236
308
  for (const dir of untrusted) {
237
309
  process.stderr.write(`[skills] project skills in ${dir} skipped (untrusted). Run: aragon skills trust ${dir}\n`);
@@ -299,6 +371,12 @@ const CONFIG_SET_KEYS = new Set([
299
371
  // flags and never persist -- inconsistent with every other config field.
300
372
  'density',
301
373
  'hints',
374
+ // MEMBERSHIP HERE ONLY DECIDES THAT THE KEY IS NOT REJECTED. The `switch`
375
+ // below is what builds the patch, and a key present here but absent there
376
+ // falls through every case, writes nothing, and still prints
377
+ // `Set mouse = false` (P1-2) -- exactly what happened to `density`/`hints`.
378
+ 'mouse',
379
+ 'historyEnabled',
302
380
  'planModeDefault',
303
381
  'planModeMaxAskRounds',
304
382
  'planModeHumanTimeoutMs',
@@ -313,6 +391,11 @@ const CONFIG_SET_KEYS = new Set([
313
391
  // `log.*` keys are handled by `applyLogConfigSet` below, NOT by the switch:
314
392
  // that switch is already past this package's complexity ceiling.
315
393
  ...LOG_CONFIG_SET_KEYS,
394
+ // `team.*` keys are handled by `applyTeamConfigSet`, for the same reason
395
+ // `log.*` are: the switch below is already past this package's complexity
396
+ // ceiling, and a key listed here but forgotten there writes nothing while
397
+ // still printing `Set team.enabled = false`.
398
+ ...TEAM_CONFIG_SET_KEYS,
316
399
  ]);
317
400
  function runConfigSet(key, value) {
318
401
  if (!CONFIG_SET_KEYS.has(key)) {
@@ -327,6 +410,16 @@ function runConfigSet(key, value) {
327
410
  process.stdout.write(`Set ${key} = ${value}\n`);
328
411
  return;
329
412
  }
413
+ const teamPatch = applyTeamConfigSet(key, value);
414
+ if (teamPatch) {
415
+ // The written value is echoed from the CLAMPED result rather than from the
416
+ // input, so `config set team.maxSubagents 40` says 10 instead of silently
417
+ // reporting a number that was never stored (R-g).
418
+ const merged = updatePersistedConfig(teamPatch);
419
+ const field = key.slice('team.'.length);
420
+ process.stdout.write(`Set ${key} = ${String(merged.team[field])}\n`);
421
+ return;
422
+ }
330
423
  const patch = {};
331
424
  switch (key) {
332
425
  case 'provider':
@@ -371,6 +464,14 @@ function runConfigSet(key, value) {
371
464
  case 'hints':
372
465
  patch.hints = value === 'true' || value === '1';
373
466
  break;
467
+ case 'mouse':
468
+ patch.mouse = value === 'true' || value === '1';
469
+ break;
470
+ // Turning this off stops new prompts being recorded; it does NOT delete
471
+ // what is already in `prompt-history.jsonl` (that is `aragon history clear`).
472
+ case 'historyEnabled':
473
+ patch.historyEnabled = value === 'true' || value === '1';
474
+ break;
374
475
  // Clamped rather than rejected, for the same reason as `theme` and
375
476
  // `skills.integrity`: hardening only the READ path leaves a bad value on
376
477
  // disk that reverts to the default on every launch, which presents as
@@ -452,6 +553,9 @@ function buildProgram() {
452
553
  .option('--no-compact', 'Comfortable transcript density')
453
554
  .option('--hints', 'Always show the composer hint row')
454
555
  .option('--no-hints', 'Hide the composer hint row')
556
+ // Positive form first, same tri-state reason as `--fullscreen` above.
557
+ .option('--mouse', 'Wheel scrolls the region under the pointer (full-screen mode)')
558
+ .option('--no-mouse', 'Leave the mouse to the terminal (the wheel does nothing)')
455
559
  // Declaring `--fullscreen` BEFORE `--no-fullscreen` keeps the default
456
560
  // `undefined` instead of `true`, which is what makes the tri-state work:
457
561
  // "no opinion" must stay distinguishable from "force it".
@@ -464,6 +568,13 @@ function buildProgram() {
464
568
  // config layer win when neither flag was given.
465
569
  .option('--plan', 'Start the session in PLAN mode (read-only research + review)')
466
570
  .option('--no-plan', 'Start the session in BUILD mode (overrides planModeDefault)')
571
+ // BOTH FORMS, positive first, and the pair is mandatory (team-subagents
572
+ // §4.4 / P1-4). Declaring only `--no-team` would make commander default
573
+ // `opts.team` to `true`, at which point the flag is indistinguishable from
574
+ // its own default and silently overrides `config.json` on every run.
575
+ .option('--team', 'Enable team subagents for this session (the default)')
576
+ .option('--no-team', 'Disable team subagents (the task tool is not registered at all)')
577
+ .option('--team-max <n>', 'Max subagents per dispatch for this run (1-10)')
467
578
  .option('--no-skills', 'Disable the skill system entirely (no catalog, no skill tools)')
468
579
  .option('--skill <name>', 'Force-load a skill at Level 2 for this run (repeatable)', (value, previous = []) => [...previous, value])
469
580
  .option('--skills-yes', 'Approve skill installs for this run (CI / headless)')
@@ -557,6 +668,16 @@ function buildProgram() {
557
668
  .action(async (subcommand, opts) => {
558
669
  process.exitCode = await runLogsCommand(subcommand ?? 'path', opts, toFlags(program.opts()));
559
670
  });
671
+ // aragon history <sub> [-n N] [--json] [--yes]
672
+ program
673
+ .command('history [subcommand]')
674
+ .description('Inspect the prompt history: path | list | clear')
675
+ .option('-n, --lines <n>', 'Entries for `history list` (default 20)')
676
+ .option('--json', 'Print raw entries instead of rendered lines')
677
+ .option('--yes', 'Confirm destructive actions (required by `history clear`)')
678
+ .action((subcommand, opts) => {
679
+ process.exitCode = runHistoryCommand(subcommand ?? 'path', opts);
680
+ });
560
681
  // aragon skills <sub> [arg] [--scope] [--name] [--description] [--yes] [--json]
561
682
  program
562
683
  .command('skills [subcommand] [argument]')
@@ -605,6 +726,10 @@ async function main() {
605
726
  // may just have created. What ① and ② did is replayed from `pending` rather
606
727
  // than lost.
607
728
  installLogging({ pending: migrationNotes(legacy, home) });
729
+ // ③.5 — after ③ because it writes a log record, and before anything that
730
+ // resolves config because every `loadConfig()` from here on must already see
731
+ // the post-migration world. Never throws, never prints (D-8).
732
+ migrateStateOutOfConfig();
608
733
  // An unparseable config.json still falls back to defaults — the CLI has to
609
734
  // start — but it must not do so in silence, or a stray comma presents as
610
735
  // "every setting I ever chose is gone". stderr, never stdout, so
@@ -90,6 +90,88 @@ const COMMANDS = [
90
90
  ctx.applyAgentMode(target);
91
91
  },
92
92
  },
93
+ {
94
+ name: 'team',
95
+ description: 'Team subagents: status | on | off | max <n>',
96
+ /**
97
+ * `/team on` CANNOT turn team mode on in a session that started without it,
98
+ * and this command must not pretend otherwise (§4.5 / D-17 / P0-2).
99
+ *
100
+ * `controller.ts` builds the tool array ONCE and its own comment forbids
101
+ * rebuilding it: `Agent.setTools()` mutates the live `ToolRegistry`, and
102
+ * `submit_plan` flips the session mode from INSIDE a tool execution, so a
103
+ * rebuild at that moment would mutate the registry mid-iteration. But
104
+ * `rebuildSystemPrompt()` IS a live path — so the naive reading of `/team on`
105
+ * (flip a flag, re-splice `<team_mode>`) produces a session where the model
106
+ * is told it has a `task` tool that was never registered. Every call comes
107
+ * back "unknown tool", and the model has no way to discover why.
108
+ *
109
+ * So the answer splits by what was decided at construction, and the honest
110
+ * branch — persist it, say the session is unchanged — is the whole point.
111
+ */
112
+ run: (ctx) => {
113
+ const arg = ctx.args.trim().toLowerCase();
114
+ const [verb, value] = arg.split(/\s+/, 2);
115
+ const controller = ctx.controller;
116
+ // Refused mid-dispatch for the reason `/reload` already records: half a
117
+ // dispatch under one ceiling and half under another produces a result
118
+ // nothing can afterwards explain.
119
+ if (verb && verb !== 'status' && controller.isTeamBusy()) {
120
+ ctx.notify('warn', 'Cannot change team settings mid-dispatch.');
121
+ return;
122
+ }
123
+ if (!verb || verb === 'status') {
124
+ const cfg = controller.getTeamConfig();
125
+ const state = !controller.isTeamRegistered()
126
+ ? 'off for this session (started with --no-team)'
127
+ : controller.isTeamEnabled()
128
+ ? 'on'
129
+ : 'off';
130
+ const snapshot = controller.getTeamSnapshot();
131
+ const roster = snapshot
132
+ ? `\nRunning: ${snapshot.runs.map((r) => `${r.label} (${r.phase})`).join(', ')}`
133
+ : '';
134
+ ctx.notify('info', `Team mode: ${state}. Max subagents: ${cfg.maxSubagents}, ` +
135
+ `concurrent: ${cfg.maxConcurrent}.${roster}`);
136
+ return;
137
+ }
138
+ if (verb === 'on' || verb === 'off') {
139
+ const enabled = verb === 'on';
140
+ // A PARTIAL `team` section, safe only because `updatePersistedConfig`
141
+ // deep-merges it — the same contract `/skills disable` relies on.
142
+ ctx.persistConfig({ team: { enabled } });
143
+ if (!controller.isTeamRegistered()) {
144
+ // The bottom-right cell of §4.5's table. It is honest, it is one
145
+ // sentence, and it keeps AC-11 intact: a `--no-team` session never
146
+ // grows a wrapper or an entry, whatever the user types afterwards.
147
+ ctx.notify('info', enabled
148
+ ? 'Team mode is off for this session (started with --no-team). Saved for next launch.'
149
+ : 'Team mode is already off for this session. Saved for next launch.');
150
+ return;
151
+ }
152
+ controller.setTeamEnabled(enabled);
153
+ ctx.toast('success', `Team mode ${enabled ? 'on' : 'off'}.`);
154
+ return;
155
+ }
156
+ if (verb === 'max') {
157
+ const n = Number.parseInt(value ?? '', 10);
158
+ if (!Number.isFinite(n) || n <= 0) {
159
+ ctx.notify('warn', 'Usage: /team max <n>');
160
+ return;
161
+ }
162
+ // Clamped by `setTeamConfig` and again on the persist path, so a value
163
+ // above the requirement's ceiling can reach neither the runtime nor the
164
+ // file (R-g).
165
+ const applied = controller.setTeamConfig({ maxSubagents: n });
166
+ ctx.persistConfig({
167
+ team: { maxSubagents: applied.maxSubagents },
168
+ });
169
+ ctx.toast('success', `Max subagents set to ${applied.maxSubagents}.`);
170
+ return;
171
+ }
172
+ ctx.notify('warn', `Unknown argument "${verb}" - use /team [status|on|off|max <n>].`);
173
+ },
174
+ },
93
175
  {
94
176
  name: 'expand',
95
177
  description: 'Expand / collapse a recent tool card (default: last)',
@@ -12,11 +12,18 @@
12
12
  * ~/.aragon-agent/
13
13
  * ├── config.json persisted config (0600 on POSIX)
14
14
  * ├── config.json.bak `aragon config edit` pre-edit backup
15
+ * ├── prompt-history.jsonl submitted prompts, append-only (0600 on POSIX)
16
+ * ├── state.json UI bookkeeping scalars (0600 on POSIX)
15
17
  * ├── logs/ JSONL logs (0700 on POSIX)
16
18
  * ├── sessions/ /save · /resume
17
19
  * ├── skills/ `aragon skills install` target
18
20
  * └── skill-usage.json
19
21
  *
22
+ * The two files added by config-state-separation are DIRECT CHILDREN of the
23
+ * root, siblings of `skill-usage.json` rather than children of any directory
24
+ * here: `logs/`, `sessions/` and `skills/` are all scan roots, and every extra
25
+ * file inside one is another thing each walk has to recognise and skip.
26
+ *
20
27
  * The same path on all three platforms, deliberately. Claude Code, git and npm
21
28
  * all put their dot-directory in the user's home; one path means documentation,
22
29
  * support scripts and community answers have exactly one thing to say. A user
@@ -62,6 +69,16 @@ export declare function getHomeRoot(): string;
62
69
  export declare function getConfigPath(): string;
63
70
  /** Written by `aragon config edit` before spawning the editor. */
64
71
  export declare function getConfigBackupPath(): string;
72
+ /**
73
+ * `<home>/prompt-history.jsonl` — the prompts the user submitted, one JSON
74
+ * object per line. Deliberately NOT under `logs/`: that directory is governed
75
+ * by `log.toFile`, `log.maxFiles` and `aragon logs clear`, and turning
76
+ * diagnostics off or reclaiming disk must not also delete the user's `↑`
77
+ * recall (config-state-separation D-1).
78
+ */
79
+ export declare function getPromptHistoryPath(): string;
80
+ /** `<home>/state.json` — UI bookkeeping scalars only; see `ui-state.ts`. */
81
+ export declare function getUiStatePath(): string;
65
82
  export declare function getLogsDir(): string;
66
83
  export declare function getSessionsDir(): string;
67
84
  /** `<home>` — parent of `skills/`, and where sidecar state such as usage lives. */
@@ -12,11 +12,18 @@
12
12
  * ~/.aragon-agent/
13
13
  * ├── config.json persisted config (0600 on POSIX)
14
14
  * ├── config.json.bak `aragon config edit` pre-edit backup
15
+ * ├── prompt-history.jsonl submitted prompts, append-only (0600 on POSIX)
16
+ * ├── state.json UI bookkeeping scalars (0600 on POSIX)
15
17
  * ├── logs/ JSONL logs (0700 on POSIX)
16
18
  * ├── sessions/ /save · /resume
17
19
  * ├── skills/ `aragon skills install` target
18
20
  * └── skill-usage.json
19
21
  *
22
+ * The two files added by config-state-separation are DIRECT CHILDREN of the
23
+ * root, siblings of `skill-usage.json` rather than children of any directory
24
+ * here: `logs/`, `sessions/` and `skills/` are all scan roots, and every extra
25
+ * file inside one is another thing each walk has to recognise and skip.
26
+ *
20
27
  * The same path on all three platforms, deliberately. Claude Code, git and npm
21
28
  * all put their dot-directory in the user's home; one path means documentation,
22
29
  * support scripts and community answers have exactly one thing to say. A user
@@ -119,6 +126,20 @@ export function getConfigPath() {
119
126
  export function getConfigBackupPath() {
120
127
  return join(resolution.root, 'config.json.bak');
121
128
  }
129
+ /**
130
+ * `<home>/prompt-history.jsonl` — the prompts the user submitted, one JSON
131
+ * object per line. Deliberately NOT under `logs/`: that directory is governed
132
+ * by `log.toFile`, `log.maxFiles` and `aragon logs clear`, and turning
133
+ * diagnostics off or reclaiming disk must not also delete the user's `↑`
134
+ * recall (config-state-separation D-1).
135
+ */
136
+ export function getPromptHistoryPath() {
137
+ return join(resolution.root, 'prompt-history.jsonl');
138
+ }
139
+ /** `<home>/state.json` — UI bookkeeping scalars only; see `ui-state.ts`. */
140
+ export function getUiStatePath() {
141
+ return join(resolution.root, 'state.json');
142
+ }
122
143
  export function getLogsDir() {
123
144
  return join(resolution.root, 'logs');
124
145
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
- * `aragon config get | list | edit | home`, plus the `log.*` branch of
3
- * `config set`.
2
+ * `aragon config get | list | edit | home`, plus the `log.*` and `team.*`
3
+ * branches of `config set`.
4
4
  *
5
5
  * These live here rather than in `cli.tsx` because `runConfigSet` is already a
6
6
  * 21-case switch — well past the complexity ceiling this package writes down —
@@ -10,6 +10,17 @@
10
10
  import { type PersistedConfig } from './schema.js';
11
11
  /** Config keys under `log.` that `aragon config set` accepts. */
12
12
  export declare const LOG_CONFIG_SET_KEYS: readonly ["log.level", "log.toFile", "log.dir", "log.maxFileBytes", "log.maxFiles", "log.redactSecrets", "log.previewChars"];
13
+ /**
14
+ * Config keys under `team.` that `aragon config set` accepts (team-subagents
15
+ * §4.3).
16
+ *
17
+ * Handled here rather than in `cli.tsx`'s switch for the reason this module's
18
+ * header already gives about `log.*`: that switch is past this package's
19
+ * complexity ceiling, and a key added to the membership set but forgotten in the
20
+ * switch falls through every case, writes nothing, and still prints
21
+ * `Set team.enabled = false`.
22
+ */
23
+ export declare const TEAM_CONFIG_SET_KEYS: readonly ["team.enabled", "team.maxSubagents", "team.maxConcurrent", "team.subagentTimeoutMs", "team.dispatchTimeoutMs", "team.maxTurnsPerSubagent"];
13
24
  /**
14
25
  * Translate a `log.*` dotted key into a patch, or `null` for anything else.
15
26
  *
@@ -19,6 +30,19 @@ export declare const LOG_CONFIG_SET_KEYS: readonly ["log.level", "log.toFile", "
19
30
  * included.
20
31
  */
21
32
  export declare function applyLogConfigSet(key: string, value: string): Partial<PersistedConfig> | null;
33
+ /**
34
+ * Translate a `team.*` dotted key into a patch, or `null` for anything else.
35
+ *
36
+ * The patch is a PARTIAL `team` section, safe only because
37
+ * `updatePersistedConfig` deep-merges that section — with a shallow merge,
38
+ * setting the fan-out width would silently reset every timeout in the section.
39
+ *
40
+ * Values are CLAMPED rather than rejected, exactly like `theme` and
41
+ * `skills.integrity`: hardening only the read path leaves a bad value on disk
42
+ * that reverts to the default on every launch, which presents as "my setting
43
+ * won't stick". `clampTeamConfig` is the one gate both directions pass through.
44
+ */
45
+ export declare function applyTeamConfigSet(key: string, value: string): Partial<PersistedConfig> | null;
22
46
  /**
23
47
  * `log.redactSecrets false` is the one setting that lets credentials reach the
24
48
  * disk. It is command-line-only (there is no settings-screen entry) and it says
@@ -1,6 +1,6 @@
1
1
  /**
2
- * `aragon config get | list | edit | home`, plus the `log.*` branch of
3
- * `config set`.
2
+ * `aragon config get | list | edit | home`, plus the `log.*` and `team.*`
3
+ * branches of `config set`.
4
4
  *
5
5
  * These live here rather than in `cli.tsx` because `runConfigSet` is already a
6
6
  * 21-case switch — well past the complexity ceiling this package writes down —
@@ -10,10 +10,10 @@
10
10
  import { spawnSync } from 'node:child_process';
11
11
  import { copyFileSync, existsSync, readFileSync } from 'node:fs';
12
12
  import process from 'node:process';
13
- import { getConfigBackupPath, getConfigPath, getHomeRoot, getLogsDir, isHomeOverridden, } from './app-paths.js';
13
+ import { getConfigBackupPath, getConfigPath, getHomeRoot, getLogsDir, getPromptHistoryPath, getUiStatePath, isHomeOverridden, } from './app-paths.js';
14
14
  import { getHomeMigrationBreadcrumbPath } from './migrate-home.js';
15
15
  import { loadPersistedConfig, updatePersistedConfig } from './store.js';
16
- import { coercePositiveInt, maskSecret, } from './schema.js';
16
+ import { coercePositiveInt, maskSecret, DEFAULT_TEAM_CONFIG, } from './schema.js';
17
17
  import { clampLogLevel } from '../logging/levels.js';
18
18
  /** Config keys under `log.` that `aragon config set` accepts. */
19
19
  export const LOG_CONFIG_SET_KEYS = [
@@ -25,6 +25,24 @@ export const LOG_CONFIG_SET_KEYS = [
25
25
  'log.redactSecrets',
26
26
  'log.previewChars',
27
27
  ];
28
+ /**
29
+ * Config keys under `team.` that `aragon config set` accepts (team-subagents
30
+ * §4.3).
31
+ *
32
+ * Handled here rather than in `cli.tsx`'s switch for the reason this module's
33
+ * header already gives about `log.*`: that switch is past this package's
34
+ * complexity ceiling, and a key added to the membership set but forgotten in the
35
+ * switch falls through every case, writes nothing, and still prints
36
+ * `Set team.enabled = false`.
37
+ */
38
+ export const TEAM_CONFIG_SET_KEYS = [
39
+ 'team.enabled',
40
+ 'team.maxSubagents',
41
+ 'team.maxConcurrent',
42
+ 'team.subagentTimeoutMs',
43
+ 'team.dispatchTimeoutMs',
44
+ 'team.maxTurnsPerSubagent',
45
+ ];
28
46
  function isTrue(value) {
29
47
  return value === 'true' || value === '1';
30
48
  }
@@ -57,6 +75,43 @@ export function applyLogConfigSet(key, value) {
57
75
  return null;
58
76
  }
59
77
  }
78
+ /**
79
+ * Translate a `team.*` dotted key into a patch, or `null` for anything else.
80
+ *
81
+ * The patch is a PARTIAL `team` section, safe only because
82
+ * `updatePersistedConfig` deep-merges that section — with a shallow merge,
83
+ * setting the fan-out width would silently reset every timeout in the section.
84
+ *
85
+ * Values are CLAMPED rather than rejected, exactly like `theme` and
86
+ * `skills.integrity`: hardening only the read path leaves a bad value on disk
87
+ * that reverts to the default on every launch, which presents as "my setting
88
+ * won't stick". `clampTeamConfig` is the one gate both directions pass through.
89
+ */
90
+ export function applyTeamConfigSet(key, value) {
91
+ const patch = (team) => ({ team });
92
+ switch (key) {
93
+ case 'team.enabled':
94
+ return patch({ enabled: isTrue(value) });
95
+ case 'team.maxSubagents':
96
+ return patch({ maxSubagents: coercePositiveInt(value, DEFAULT_TEAM_CONFIG.maxSubagents) });
97
+ case 'team.maxConcurrent':
98
+ return patch({ maxConcurrent: coercePositiveInt(value, DEFAULT_TEAM_CONFIG.maxConcurrent) });
99
+ case 'team.subagentTimeoutMs':
100
+ return patch({
101
+ subagentTimeoutMs: coercePositiveInt(value, DEFAULT_TEAM_CONFIG.subagentTimeoutMs),
102
+ });
103
+ case 'team.dispatchTimeoutMs':
104
+ return patch({
105
+ dispatchTimeoutMs: coercePositiveInt(value, DEFAULT_TEAM_CONFIG.dispatchTimeoutMs),
106
+ });
107
+ case 'team.maxTurnsPerSubagent':
108
+ return patch({
109
+ maxTurnsPerSubagent: coercePositiveInt(value, DEFAULT_TEAM_CONFIG.maxTurnsPerSubagent),
110
+ });
111
+ default:
112
+ return null;
113
+ }
114
+ }
60
115
  /**
61
116
  * `log.redactSecrets false` is the one setting that lets credentials reach the
62
117
  * disk. It is command-line-only (there is no settings-screen entry) and it says
@@ -177,6 +232,10 @@ export function runConfigHome() {
177
232
  process.stdout.write(`${getHomeRoot()}\n`);
178
233
  if (isHomeOverridden())
179
234
  process.stdout.write(' (overridden by ARAGON_HOME)\n');
235
+ // Named here because config.json no longer holds either of them, and a user
236
+ // looking for "where did my prompt history go" has to land somewhere.
237
+ process.stdout.write(` prompt history: ${getPromptHistoryPath()}\n`);
238
+ process.stdout.write(` ui state: ${getUiStatePath()}\n`);
180
239
  const breadcrumb = getHomeMigrationBreadcrumbPath();
181
240
  if (!existsSync(breadcrumb))
182
241
  return;
@@ -5,8 +5,8 @@
5
5
  * - Provider secrets: ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_API_KEY /
6
6
  * GEMINI_API_KEY.
7
7
  * - Overrides: ARAGON_PROVIDER, ARAGON_MODEL, ARAGON_BASE_URL, ARAGON_THINKING,
8
- * ARAGON_MAX_TOKENS, ARAGON_THEME, ARAGON_PLAN, ARAGON_LOG_LEVEL,
9
- * ARAGON_LOG_FILE, ARAGON_LOG_DIR.
8
+ * ARAGON_MAX_TOKENS, ARAGON_THEME, ARAGON_MOUSE, ARAGON_PLAN, ARAGON_TEAM,
9
+ * ARAGON_TEAM_MAX, ARAGON_LOG_LEVEL, ARAGON_LOG_FILE, ARAGON_LOG_DIR.
10
10
  *
11
11
  * NOT here: `ARAGON_HOME`. It is read when `config/app-paths.ts` is first
12
12
  * evaluated, which is long before `loadDotenv()` runs, so a `.env` can never
@@ -5,8 +5,8 @@
5
5
  * - Provider secrets: ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_API_KEY /
6
6
  * GEMINI_API_KEY.
7
7
  * - Overrides: ARAGON_PROVIDER, ARAGON_MODEL, ARAGON_BASE_URL, ARAGON_THINKING,
8
- * ARAGON_MAX_TOKENS, ARAGON_THEME, ARAGON_PLAN, ARAGON_LOG_LEVEL,
9
- * ARAGON_LOG_FILE, ARAGON_LOG_DIR.
8
+ * ARAGON_MAX_TOKENS, ARAGON_THEME, ARAGON_MOUSE, ARAGON_PLAN, ARAGON_TEAM,
9
+ * ARAGON_TEAM_MAX, ARAGON_LOG_LEVEL, ARAGON_LOG_FILE, ARAGON_LOG_DIR.
10
10
  *
11
11
  * NOT here: `ARAGON_HOME`. It is read when `config/app-paths.ts` is first
12
12
  * evaluated, which is long before `loadDotenv()` runs, so a `.env` can never
@@ -71,6 +71,14 @@ export function readEnvConfig() {
71
71
  if (fullscreen !== undefined && fullscreen.length > 0) {
72
72
  partial.fullscreen = fullscreen === '1' || fullscreen === 'true' || fullscreen === 'on' || fullscreen === 'yes';
73
73
  }
74
+ // ARAGON_MOUSE=0 turns off wheel region routing. Parsed by the POSITIVE list
75
+ // above, exactly as ARAGON_FULLSCREEN is — NOT by `envBool` below, which uses
76
+ // a negative list. The two disagree on `ARAGON_MOUSE=disable`, and `mouse` is
77
+ // modelled on `--fullscreen`, so it uses the reader `--fullscreen` uses.
78
+ const mouse = process.env.ARAGON_MOUSE?.trim().toLowerCase();
79
+ if (mouse !== undefined && mouse.length > 0) {
80
+ partial.mouse = mouse === '1' || mouse === 'true' || mouse === 'on' || mouse === 'yes';
81
+ }
74
82
  // ARAGON_PLAN=1 starts the session in PLAN mode — the same channel story as
75
83
  // ARAGON_FULLSCREEN, for terminals reached through a wrapper that owns argv.
76
84
  const plan = process.env.ARAGON_PLAN?.trim().toLowerCase();
@@ -88,6 +96,32 @@ export function readEnvConfig() {
88
96
  enabled: !(skills === '0' || skills === 'false' || skills === 'off' || skills === 'no'),
89
97
  };
90
98
  }
99
+ // ARAGON_TEAM=0 turns team mode off; ARAGON_TEAM_MAX=<n> overrides the fan-out
100
+ // width. Parsed by the POSITIVE list, exactly as ARAGON_FULLSCREEN /
101
+ // ARAGON_MOUSE / ARAGON_PLAN are — NOT by `envBool` below, which uses a
102
+ // negative list. The two readers disagree on `ARAGON_TEAM=disable`, and the
103
+ // documented-but-dead outcome of copying the wrong one is what `load.ts`
104
+ // records for `ARAGON_MOUSE`.
105
+ const team = process.env.ARAGON_TEAM?.trim().toLowerCase();
106
+ const teamMax = process.env.ARAGON_TEAM_MAX?.trim();
107
+ if ((team !== undefined && team.length > 0) || (teamMax !== undefined && teamMax.length > 0)) {
108
+ const section = {};
109
+ if (team !== undefined && team.length > 0) {
110
+ section.enabled = team === '1' || team === 'true' || team === 'on' || team === 'yes';
111
+ }
112
+ if (teamMax !== undefined && teamMax.length > 0) {
113
+ const parsed = Number.parseInt(teamMax, 10);
114
+ // Left ABSENT when unparseable rather than written as a default, so the
115
+ // config file can still win. A resolved default here would make the env
116
+ // var look permanently supplied.
117
+ if (Number.isFinite(parsed) && parsed > 0)
118
+ section.maxSubagents = parsed;
119
+ }
120
+ // A PARTIAL section, safe only because every consumer deep-merges it — the
121
+ // same contract `partial.skills` and `partial.log` rely on.
122
+ if (Object.keys(section).length > 0)
123
+ partial.team = section;
124
+ }
91
125
  const log = readEnvLogConfig();
92
126
  if (log)
93
127
  partial.log = log;