@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
package/dist/cli.js CHANGED
@@ -21,11 +21,13 @@ 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
- 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';
30
+ import { ADAPTER_PROVIDERS, clampDensity, clampTheme, clampThinkingLevel, clampAskRounds, clampHumanTimeout, clampSkillsIntegrity, clampSkillsToolPolicy, clampTranscriptWindow, coercePositiveInt, isAdapterProvider, parseMaxTokensInput, DEFAULT_MAX_TOKENS, DEFAULT_PLAN_HUMAN_TIMEOUT_MS, DEFAULT_PLAN_MAX_ASK_ROUNDS, DEFAULT_SKILLS_CONFIG, } from './config/schema.js';
29
31
  import { makeGetApiKey } from './config/load.js';
30
32
  import { AgentController, DENY_ALL_APPROVAL } from './agent/controller.js';
31
33
  import { runSkillsCommand } from './skills/cli-commands.js';
@@ -79,6 +81,11 @@ function toFlags(opts) {
79
81
  skillsYes: opts.skillsYes,
80
82
  skillToolPolicy: opts.skillToolPolicy,
81
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,
82
89
  // `--verbose` is just a shorthand for `--log-level debug`; an explicit
83
90
  // `--log-level` still wins, so the two can be combined without surprise.
84
91
  logLevel: opts.logLevel ?? (opts.verbose ? 'debug' : undefined),
@@ -166,6 +173,11 @@ function runInteractive(flags, extras = {}) {
166
173
  const logger = getLogger();
167
174
  logger.info('cli', 'run_interactive', { mode, provider: config.provider, model: config.model });
168
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);
169
181
  let screen = null;
170
182
  let replayed = false;
171
183
  // --- Mouse wheel region routing (mouse-wheel-region-routing §4.2). -------
@@ -264,11 +276,16 @@ function runInteractive(flags, extras = {}) {
264
276
  .then(() => {
265
277
  screen?.restore();
266
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();
267
283
  replayTranscript();
268
284
  })
269
285
  .catch(() => {
270
286
  screen?.restore();
271
287
  disposeMouse();
288
+ controller.dispose();
272
289
  });
273
290
  }
274
291
  async function runOneShot(flags, prompt, quiet) {
@@ -282,6 +299,11 @@ async function runOneShot(flags, prompt, quiet) {
282
299
  // toast channel `App.tsx` uses does not exist here.
283
300
  logger.onFailure((reason) => process.stderr.write(`[log] ${reason}\n`));
284
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);
285
307
  const untrusted = controller.getSkillService().untrustedDirs();
286
308
  for (const dir of untrusted) {
287
309
  process.stderr.write(`[skills] project skills in ${dir} skipped (untrusted). Run: aragon skills trust ${dir}\n`);
@@ -354,6 +376,7 @@ const CONFIG_SET_KEYS = new Set([
354
376
  // falls through every case, writes nothing, and still prints
355
377
  // `Set mouse = false` (P1-2) -- exactly what happened to `density`/`hints`.
356
378
  'mouse',
379
+ 'historyEnabled',
357
380
  'planModeDefault',
358
381
  'planModeMaxAskRounds',
359
382
  'planModeHumanTimeoutMs',
@@ -368,7 +391,35 @@ const CONFIG_SET_KEYS = new Set([
368
391
  // `log.*` keys are handled by `applyLogConfigSet` below, NOT by the switch:
369
392
  // that switch is already past this package's complexity ceiling.
370
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,
371
399
  ]);
400
+ /**
401
+ * `aragon config set maxTokens <n|auto|default>`.
402
+ *
403
+ * REJECTS nonsense rather than clamping it, unlike `theme` and `thinkingLevel`.
404
+ * Those two clamp because their bad values are out-of-VOCABULARY; here a
405
+ * non-numeric argument is almost always a typo for a number the user meant, and
406
+ * storing 64000 for `3200O` would quietly discard their intent. An out-of-RANGE
407
+ * number is still clamped, in both directions, by the store's single gate.
408
+ */
409
+ function runConfigSetMaxTokens(value) {
410
+ const raw = value.trim().toLowerCase();
411
+ const parsed = raw === 'default'
412
+ ? { kind: 'value', value: DEFAULT_MAX_TOKENS, clamped: false }
413
+ : parseMaxTokensInput(value);
414
+ if (parsed.kind === 'invalid') {
415
+ process.stderr.write(`Invalid maxTokens "${value}". Use a number, "auto", or "default".\n`);
416
+ process.exitCode = 2;
417
+ return;
418
+ }
419
+ const stored = parsed.kind === 'auto' ? null : parsed.value;
420
+ const merged = updatePersistedConfig({ maxTokens: stored });
421
+ process.stdout.write(`Set maxTokens = ${merged.maxTokens ?? 'auto'}\n`);
422
+ }
372
423
  function runConfigSet(key, value) {
373
424
  if (!CONFIG_SET_KEYS.has(key)) {
374
425
  process.stderr.write(`Unknown config key "${key}". Known keys: ${[...CONFIG_SET_KEYS].join(', ')}.\n`);
@@ -382,6 +433,23 @@ function runConfigSet(key, value) {
382
433
  process.stdout.write(`Set ${key} = ${value}\n`);
383
434
  return;
384
435
  }
436
+ // Handled ahead of the switch because it needs to echo the STORED value, not
437
+ // the typed one: `config set maxTokens 900000` must not report a number that
438
+ // was never written. Same reason `team.*` has its own branch.
439
+ if (key === 'maxTokens') {
440
+ runConfigSetMaxTokens(value);
441
+ return;
442
+ }
443
+ const teamPatch = applyTeamConfigSet(key, value);
444
+ if (teamPatch) {
445
+ // The written value is echoed from the CLAMPED result rather than from the
446
+ // input, so `config set team.maxSubagents 40` says 10 instead of silently
447
+ // reporting a number that was never stored (R-g).
448
+ const merged = updatePersistedConfig(teamPatch);
449
+ const field = key.slice('team.'.length);
450
+ process.stdout.write(`Set ${key} = ${String(merged.team[field])}\n`);
451
+ return;
452
+ }
385
453
  const patch = {};
386
454
  switch (key) {
387
455
  case 'provider':
@@ -396,9 +464,6 @@ function runConfigSet(key, value) {
396
464
  case 'thinkingLevel':
397
465
  patch.thinkingLevel = clampThinkingLevel(value, 'off');
398
466
  break;
399
- case 'maxTokens':
400
- patch.maxTokens = coerceMaxTokens(value) ?? null;
401
- break;
402
467
  case 'theme':
403
468
  patch.theme = clampTheme(value, 'auto');
404
469
  break;
@@ -429,6 +494,11 @@ function runConfigSet(key, value) {
429
494
  case 'mouse':
430
495
  patch.mouse = value === 'true' || value === '1';
431
496
  break;
497
+ // Turning this off stops new prompts being recorded; it does NOT delete
498
+ // what is already in `prompt-history.jsonl` (that is `aragon history clear`).
499
+ case 'historyEnabled':
500
+ patch.historyEnabled = value === 'true' || value === '1';
501
+ break;
432
502
  // Clamped rather than rejected, for the same reason as `theme` and
433
503
  // `skills.integrity`: hardening only the READ path leaves a bad value on
434
504
  // disk that reverts to the default on every launch, which presents as
@@ -497,7 +567,7 @@ function buildProgram() {
497
567
  .option('--base-url <url>', 'Override the provider base URL')
498
568
  .option('--api-key <key>', 'One-shot API key override (not persisted)')
499
569
  .option('--thinking <level>', 'off|minimal|low|medium|high|xhigh')
500
- .option('--max-tokens <n>', 'Output token cap')
570
+ .option('--max-tokens <n|auto>', 'Output token cap: a number, or "auto" for the model ceiling (default 64000)')
501
571
  .option('--cwd <dir>', 'Working directory for tools')
502
572
  .option('--confirm', 'Confirm each mutating tool call')
503
573
  .option('--tool-timeout <ms>', 'Per-tool executor ceiling (default 180000)')
@@ -525,6 +595,13 @@ function buildProgram() {
525
595
  // config layer win when neither flag was given.
526
596
  .option('--plan', 'Start the session in PLAN mode (read-only research + review)')
527
597
  .option('--no-plan', 'Start the session in BUILD mode (overrides planModeDefault)')
598
+ // BOTH FORMS, positive first, and the pair is mandatory (team-subagents
599
+ // §4.4 / P1-4). Declaring only `--no-team` would make commander default
600
+ // `opts.team` to `true`, at which point the flag is indistinguishable from
601
+ // its own default and silently overrides `config.json` on every run.
602
+ .option('--team', 'Enable team subagents for this session (the default)')
603
+ .option('--no-team', 'Disable team subagents (the task tool is not registered at all)')
604
+ .option('--team-max <n>', 'Max subagents per dispatch for this run (1-10)')
528
605
  .option('--no-skills', 'Disable the skill system entirely (no catalog, no skill tools)')
529
606
  .option('--skill <name>', 'Force-load a skill at Level 2 for this run (repeatable)', (value, previous = []) => [...previous, value])
530
607
  .option('--skills-yes', 'Approve skill installs for this run (CI / headless)')
@@ -618,6 +695,16 @@ function buildProgram() {
618
695
  .action(async (subcommand, opts) => {
619
696
  process.exitCode = await runLogsCommand(subcommand ?? 'path', opts, toFlags(program.opts()));
620
697
  });
698
+ // aragon history <sub> [-n N] [--json] [--yes]
699
+ program
700
+ .command('history [subcommand]')
701
+ .description('Inspect the prompt history: path | list | clear')
702
+ .option('-n, --lines <n>', 'Entries for `history list` (default 20)')
703
+ .option('--json', 'Print raw entries instead of rendered lines')
704
+ .option('--yes', 'Confirm destructive actions (required by `history clear`)')
705
+ .action((subcommand, opts) => {
706
+ process.exitCode = runHistoryCommand(subcommand ?? 'path', opts);
707
+ });
621
708
  // aragon skills <sub> [arg] [--scope] [--name] [--description] [--yes] [--json]
622
709
  program
623
710
  .command('skills [subcommand] [argument]')
@@ -666,6 +753,10 @@ async function main() {
666
753
  // may just have created. What ① and ② did is replayed from `pending` rather
667
754
  // than lost.
668
755
  installLogging({ pending: migrationNotes(legacy, home) });
756
+ // ③.5 — after ③ because it writes a log record, and before anything that
757
+ // resolves config because every `loadConfig()` from here on must already see
758
+ // the post-migration world. Never throws, never prints (D-8).
759
+ migrateStateOutOfConfig();
669
760
  // An unparseable config.json still falls back to defaults — the CLI has to
670
761
  // start — but it must not do so in silence, or a stray comma presents as
671
762
  // "every setting I ever chose is gone". stderr, never stdout, so
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Built-in slash commands (spec §5.2):
3
- * /help /model /settings /thinking /tools /clear /reset /cwd /save /resume
4
- * /copy /exit (/quit).
3
+ * /help /model /settings /thinking /max-tokens /tools /clear /reset /cwd /save
4
+ * /resume /copy /exit (/quit).
5
5
  */
6
6
  import { CommandRegistry } from './registry.js';
7
7
  export declare function registerBuiltinCommands(registry: CommandRegistry): void;
@@ -1,13 +1,14 @@
1
1
  /**
2
2
  * Built-in slash commands (spec §5.2):
3
- * /help /model /settings /thinking /tools /clear /reset /cwd /save /resume
4
- * /copy /exit (/quit).
3
+ * /help /model /settings /thinking /max-tokens /tools /clear /reset /cwd /save
4
+ * /resume /copy /exit (/quit).
5
5
  */
6
6
  import { spawn } from 'node:child_process';
7
7
  import { existsSync, statSync } from 'node:fs';
8
8
  import { isAbsolute, resolve } from 'node:path';
9
9
  import process from 'node:process';
10
- import { clampThinkingLevel, clampTheme, isThemeName, LEGACY_DARK_THEME, THEME_NAMES, THINKING_LEVELS, } from '../config/schema.js';
10
+ import { resolveOutputTokens } from '@aragon-agent/core';
11
+ import { clampThinkingLevel, clampTheme, isThemeName, parseMaxTokensInput, DEFAULT_MAX_TOKENS, LEGACY_DARK_THEME, THEME_NAMES, THINKING_LEVELS, } from '../config/schema.js';
11
12
  import { MODE_LABEL, nextMode } from '../agent/agent-mode.js';
12
13
  import { loadConfig } from '../config/load.js';
13
14
  import { loadSession, resolveSessionPath, saveSession } from '../session/persist.js';
@@ -43,6 +44,53 @@ const COMMANDS = [
43
44
  ctx.toast('info', `Thinking level set to "${clamped}".`);
44
45
  },
45
46
  },
47
+ {
48
+ name: 'max-tokens',
49
+ description: 'Show or set the output token cap (<n> | auto | default)',
50
+ /**
51
+ * Exists next to the settings screen for the reason `/theme` and `/plan` do:
52
+ * the slash palette is the discoverable surface, and this is one keystroke
53
+ * instead of five arrow presses.
54
+ *
55
+ * The no-argument branch reports the EFFECTIVE cap, not just the setting.
56
+ * "I set 64000" and "this model will produce 8192" are different facts, and
57
+ * only the second one explains a truncated answer.
58
+ */
59
+ run: (ctx) => {
60
+ const arg = ctx.args.trim();
61
+ const cfg = ctx.controller.getConfig();
62
+ if (!arg) {
63
+ const resolution = resolveOutputTokens({
64
+ providerId: cfg.provider,
65
+ modelId: cfg.model,
66
+ ...(cfg.maxTokens !== undefined ? { requested: cfg.maxTokens } : {}),
67
+ });
68
+ const setting = cfg.maxTokens === undefined ? 'auto' : String(cfg.maxTokens);
69
+ const why = resolution.clampedBy === 'ceiling' ? ` (${cfg.model} ceiling)` : '';
70
+ ctx.notify('info', `Max tokens: ${setting}. Effective for ${cfg.provider}:${cfg.model}: ` +
71
+ `${resolution.value}${why}.\nUse /max-tokens <n|auto|default>.`);
72
+ return;
73
+ }
74
+ const parsed = arg.toLowerCase() === 'default'
75
+ ? { kind: 'value', value: DEFAULT_MAX_TOKENS, clamped: false }
76
+ : parseMaxTokensInput(arg);
77
+ if (parsed.kind === 'invalid') {
78
+ // Change NOTHING on a typo: silently resolving it to a default would
79
+ // discard the number the user meant to type.
80
+ ctx.notify('warn', `Invalid value "${arg}" - use /max-tokens <n|auto|default>.`);
81
+ return;
82
+ }
83
+ if (parsed.kind === 'auto') {
84
+ ctx.controller.setMaxTokens(undefined);
85
+ ctx.persistConfig({ maxTokens: null });
86
+ ctx.toast('success', 'Max tokens: auto (per-model ceiling).');
87
+ return;
88
+ }
89
+ ctx.controller.setMaxTokens(parsed.value);
90
+ ctx.persistConfig({ maxTokens: parsed.value });
91
+ ctx.toast('success', `Max tokens set to ${parsed.value}.`);
92
+ },
93
+ },
46
94
  {
47
95
  name: 'theme',
48
96
  description: 'Switch the color theme (auto|warm|cool|light)',
@@ -90,6 +138,88 @@ const COMMANDS = [
90
138
  ctx.applyAgentMode(target);
91
139
  },
92
140
  },
141
+ {
142
+ name: 'team',
143
+ description: 'Team subagents: status | on | off | max <n>',
144
+ /**
145
+ * `/team on` CANNOT turn team mode on in a session that started without it,
146
+ * and this command must not pretend otherwise (§4.5 / D-17 / P0-2).
147
+ *
148
+ * `controller.ts` builds the tool array ONCE and its own comment forbids
149
+ * rebuilding it: `Agent.setTools()` mutates the live `ToolRegistry`, and
150
+ * `submit_plan` flips the session mode from INSIDE a tool execution, so a
151
+ * rebuild at that moment would mutate the registry mid-iteration. But
152
+ * `rebuildSystemPrompt()` IS a live path — so the naive reading of `/team on`
153
+ * (flip a flag, re-splice `<team_mode>`) produces a session where the model
154
+ * is told it has a `task` tool that was never registered. Every call comes
155
+ * back "unknown tool", and the model has no way to discover why.
156
+ *
157
+ * So the answer splits by what was decided at construction, and the honest
158
+ * branch — persist it, say the session is unchanged — is the whole point.
159
+ */
160
+ run: (ctx) => {
161
+ const arg = ctx.args.trim().toLowerCase();
162
+ const [verb, value] = arg.split(/\s+/, 2);
163
+ const controller = ctx.controller;
164
+ // Refused mid-dispatch for the reason `/reload` already records: half a
165
+ // dispatch under one ceiling and half under another produces a result
166
+ // nothing can afterwards explain.
167
+ if (verb && verb !== 'status' && controller.isTeamBusy()) {
168
+ ctx.notify('warn', 'Cannot change team settings mid-dispatch.');
169
+ return;
170
+ }
171
+ if (!verb || verb === 'status') {
172
+ const cfg = controller.getTeamConfig();
173
+ const state = !controller.isTeamRegistered()
174
+ ? 'off for this session (started with --no-team)'
175
+ : controller.isTeamEnabled()
176
+ ? 'on'
177
+ : 'off';
178
+ const snapshot = controller.getTeamSnapshot();
179
+ const roster = snapshot
180
+ ? `\nRunning: ${snapshot.runs.map((r) => `${r.label} (${r.phase})`).join(', ')}`
181
+ : '';
182
+ ctx.notify('info', `Team mode: ${state}. Max subagents: ${cfg.maxSubagents}, ` +
183
+ `concurrent: ${cfg.maxConcurrent}.${roster}`);
184
+ return;
185
+ }
186
+ if (verb === 'on' || verb === 'off') {
187
+ const enabled = verb === 'on';
188
+ // A PARTIAL `team` section, safe only because `updatePersistedConfig`
189
+ // deep-merges it — the same contract `/skills disable` relies on.
190
+ ctx.persistConfig({ team: { enabled } });
191
+ if (!controller.isTeamRegistered()) {
192
+ // The bottom-right cell of §4.5's table. It is honest, it is one
193
+ // sentence, and it keeps AC-11 intact: a `--no-team` session never
194
+ // grows a wrapper or an entry, whatever the user types afterwards.
195
+ ctx.notify('info', enabled
196
+ ? 'Team mode is off for this session (started with --no-team). Saved for next launch.'
197
+ : 'Team mode is already off for this session. Saved for next launch.');
198
+ return;
199
+ }
200
+ controller.setTeamEnabled(enabled);
201
+ ctx.toast('success', `Team mode ${enabled ? 'on' : 'off'}.`);
202
+ return;
203
+ }
204
+ if (verb === 'max') {
205
+ const n = Number.parseInt(value ?? '', 10);
206
+ if (!Number.isFinite(n) || n <= 0) {
207
+ ctx.notify('warn', 'Usage: /team max <n>');
208
+ return;
209
+ }
210
+ // Clamped by `setTeamConfig` and again on the persist path, so a value
211
+ // above the requirement's ceiling can reach neither the runtime nor the
212
+ // file (R-g).
213
+ const applied = controller.setTeamConfig({ maxSubagents: n });
214
+ ctx.persistConfig({
215
+ team: { maxSubagents: applied.maxSubagents },
216
+ });
217
+ ctx.toast('success', `Max subagents set to ${applied.maxSubagents}.`);
218
+ return;
219
+ }
220
+ ctx.notify('warn', `Unknown argument "${verb}" - use /team [status|on|off|max <n>].`);
221
+ },
222
+ },
93
223
  {
94
224
  name: 'expand',
95
225
  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
@@ -80,6 +135,20 @@ function maskConfig(config) {
80
135
  }
81
136
  return { ...config, apiKeys };
82
137
  }
138
+ /**
139
+ * The human-readable label for a value whose raw form would mislead, or
140
+ * `undefined` to leave the caller's own formatting alone.
141
+ *
142
+ * `maxTokens: null` is AUTO, not "unset". Printing a bare `null` reads as a
143
+ * missing setting and sends the user looking for a bug that is not there.
144
+ * `get` and `list` format everything else differently (unquoted vs. JSON), so
145
+ * this deliberately overrides one case rather than replacing both renderers.
146
+ */
147
+ function displayOverride(key, value) {
148
+ if (key === 'maxTokens' && value === null)
149
+ return 'auto';
150
+ return undefined;
151
+ }
83
152
  /** Resolve a flat or one-level-dotted key against the persisted config. */
84
153
  function readKey(config, key) {
85
154
  const [head, tail] = key.split('.', 2);
@@ -98,10 +167,14 @@ export function runConfigGet(key) {
98
167
  process.exitCode = 2;
99
168
  return;
100
169
  }
101
- process.stdout.write(`${typeof value === 'string' ? value : JSON.stringify(value)}\n`);
170
+ const shown = displayOverride(key, value)
171
+ ?? (typeof value === 'string' ? value : JSON.stringify(value));
172
+ process.stdout.write(`${shown}\n`);
102
173
  }
103
174
  export function runConfigList(opts = {}) {
104
175
  const masked = maskConfig(loadPersistedConfig());
176
+ // `--json` stays RAW on purpose: it mirrors the file, and a consumer parsing
177
+ // it must see the `null` that is actually on disk.
105
178
  if (opts.json) {
106
179
  process.stdout.write(`${JSON.stringify(masked, null, 2)}\n`);
107
180
  return;
@@ -113,7 +186,7 @@ export function runConfigList(opts = {}) {
113
186
  }
114
187
  continue;
115
188
  }
116
- process.stdout.write(`${key} = ${JSON.stringify(value)}\n`);
189
+ process.stdout.write(`${key} = ${displayOverride(key, value) ?? JSON.stringify(value)}\n`);
117
190
  }
118
191
  }
119
192
  // ---------------------------------------------------------------------------
@@ -177,6 +250,10 @@ export function runConfigHome() {
177
250
  process.stdout.write(`${getHomeRoot()}\n`);
178
251
  if (isHomeOverridden())
179
252
  process.stdout.write(' (overridden by ARAGON_HOME)\n');
253
+ // Named here because config.json no longer holds either of them, and a user
254
+ // looking for "where did my prompt history go" has to land somewhere.
255
+ process.stdout.write(` prompt history: ${getPromptHistoryPath()}\n`);
256
+ process.stdout.write(` ui state: ${getUiStatePath()}\n`);
180
257
  const breadcrumb = getHomeMigrationBreadcrumbPath();
181
258
  if (!existsSync(breadcrumb))
182
259
  return;
@@ -5,8 +5,9 @@
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_MOUSE, ARAGON_PLAN,
9
- * ARAGON_LOG_LEVEL, ARAGON_LOG_FILE, ARAGON_LOG_DIR.
8
+ * ARAGON_MAX_TOKENS (a number, or `auto` / `0` for the per-model ceiling),
9
+ * ARAGON_THEME, ARAGON_MOUSE, ARAGON_PLAN, ARAGON_TEAM,
10
+ * ARAGON_TEAM_MAX, ARAGON_LOG_LEVEL, ARAGON_LOG_FILE, ARAGON_LOG_DIR.
10
11
  *
11
12
  * NOT here: `ARAGON_HOME`. It is read when `config/app-paths.ts` is first
12
13
  * evaluated, which is long before `loadDotenv()` runs, so a `.env` can never