@aragon-agent/cli 0.5.2 → 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 (83) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/README.md +128 -1
  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 +66 -2
  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 +28 -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 +13 -0
  22. package/dist/config/load.js +65 -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 +112 -16
  28. package/dist/config/schema.js +89 -6
  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/logging/install.d.ts +85 -0
  33. package/dist/logging/install.js +94 -0
  34. package/dist/logging/logger.d.ts +1 -1
  35. package/dist/session/persist.d.ts +16 -0
  36. package/dist/session/persist.js +19 -1
  37. package/dist/team/activity.d.ts +67 -0
  38. package/dist/team/activity.js +130 -0
  39. package/dist/team/bus.d.ts +190 -0
  40. package/dist/team/bus.js +366 -0
  41. package/dist/team/comm-tools.d.ts +61 -0
  42. package/dist/team/comm-tools.js +221 -0
  43. package/dist/team/human-queue.d.ts +55 -0
  44. package/dist/team/human-queue.js +72 -0
  45. package/dist/team/limits.d.ts +123 -0
  46. package/dist/team/limits.js +128 -0
  47. package/dist/team/normalize.d.ts +39 -0
  48. package/dist/team/normalize.js +106 -0
  49. package/dist/team/panel-rows.d.ts +48 -0
  50. package/dist/team/panel-rows.js +75 -0
  51. package/dist/team/prompt.d.ts +49 -0
  52. package/dist/team/prompt.js +87 -0
  53. package/dist/team/report.d.ts +58 -0
  54. package/dist/team/report.js +205 -0
  55. package/dist/team/retry.d.ts +48 -0
  56. package/dist/team/retry.js +56 -0
  57. package/dist/team/runtime.d.ts +122 -0
  58. package/dist/team/runtime.js +406 -0
  59. package/dist/team/subagent.d.ts +90 -0
  60. package/dist/team/subagent.js +294 -0
  61. package/dist/team/task-tool.d.ts +47 -0
  62. package/dist/team/task-tool.js +118 -0
  63. package/dist/team/types.d.ts +172 -0
  64. package/dist/team/types.js +13 -0
  65. package/dist/tools/index.d.ts +48 -2
  66. package/dist/tools/index.js +26 -1
  67. package/dist/ui/App.js +97 -13
  68. package/dist/ui/StatusBar.d.ts +21 -0
  69. package/dist/ui/StatusBar.js +5 -2
  70. package/dist/ui/TeamPanel.d.ts +57 -0
  71. package/dist/ui/TeamPanel.js +134 -0
  72. package/dist/ui/Transcript.js +10 -0
  73. package/dist/ui/entries/TeamCard.d.ts +31 -0
  74. package/dist/ui/entries/TeamCard.js +39 -0
  75. package/dist/ui/glyphs.d.ts +10 -0
  76. package/dist/ui/glyphs.js +6 -0
  77. package/dist/ui/layout/AppShell.d.ts +11 -1
  78. package/dist/ui/layout/AppShell.js +3 -3
  79. package/dist/ui/overlays/HelpOverlay.js +12 -0
  80. package/dist/ui/transcript-text.js +20 -0
  81. package/dist/ui/use-startup-notices.d.ts +1 -1
  82. package/dist/ui/use-startup-notices.js +2 -1
  83. package/package.json +2 -2
@@ -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_MOUSE, ARAGON_PLAN,
9
- * ARAGON_LOG_LEVEL, 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_MOUSE, ARAGON_PLAN,
9
- * ARAGON_LOG_LEVEL, 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
@@ -96,6 +96,32 @@ export function readEnvConfig() {
96
96
  enabled: !(skills === '0' || skills === 'false' || skills === 'off' || skills === 'no'),
97
97
  };
98
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
+ }
99
125
  const log = readEnvLogConfig();
100
126
  if (log)
101
127
  partial.log = log;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * `aragon history path | list | clear` — the prompt history's operations
3
+ * surface (config-state-separation §5.5).
4
+ *
5
+ * Exit codes follow the existing convention: 0 success · 1 run-time failure ·
6
+ * 2 usage error.
7
+ *
8
+ * EXEMPT from the "every subcommand records one info line" rule, for the same
9
+ * reason `logging/cli-commands.ts` is: recording a line opens today's log file,
10
+ * and a command whose job is to delete the user's data should not be creating
11
+ * files as a side effect of announcing itself.
12
+ */
13
+ export interface HistoryCliOptions {
14
+ /** `-n <N>` — entries printed by `list` (default 20). */
15
+ lines?: string;
16
+ json?: boolean;
17
+ /** Required by `clear`; without it the command refuses. */
18
+ yes?: boolean;
19
+ }
20
+ export declare function runHistoryCommand(subcommand: string, opts: HistoryCliOptions): number;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * `aragon history path | list | clear` — the prompt history's operations
3
+ * surface (config-state-separation §5.5).
4
+ *
5
+ * Exit codes follow the existing convention: 0 success · 1 run-time failure ·
6
+ * 2 usage error.
7
+ *
8
+ * EXEMPT from the "every subcommand records one info line" rule, for the same
9
+ * reason `logging/cli-commands.ts` is: recording a line opens today's log file,
10
+ * and a command whose job is to delete the user's data should not be creating
11
+ * files as a side effect of announcing itself.
12
+ */
13
+ import { existsSync } from 'node:fs';
14
+ import process from 'node:process';
15
+ import { getPromptHistoryPath } from './app-paths.js';
16
+ import { clearPromptHistory, listPromptHistory } from './prompt-history.js';
17
+ const EXIT_OK = 0;
18
+ const EXIT_USAGE = 2;
19
+ const DEFAULT_LIST_ENTRIES = 20;
20
+ export function runHistoryCommand(subcommand, opts) {
21
+ switch (subcommand) {
22
+ case 'path':
23
+ return runPath();
24
+ case 'list':
25
+ return runList(opts);
26
+ case 'clear':
27
+ return runClear(opts);
28
+ default:
29
+ process.stderr.write(`Unknown history subcommand "${subcommand}". Try: path | list | clear.\n`);
30
+ return EXIT_USAGE;
31
+ }
32
+ }
33
+ function runPath() {
34
+ const path = getPromptHistoryPath();
35
+ process.stdout.write(`${path}\n`);
36
+ if (!existsSync(path))
37
+ process.stdout.write('(no history recorded yet)\n');
38
+ // The file holds prompts verbatim — the one thing to say out loud before
39
+ // someone attaches it to a bug report or copies it to another machine.
40
+ process.stdout.write('entries are stored verbatim; clear them with: aragon history clear --yes\n');
41
+ return EXIT_OK;
42
+ }
43
+ function runList(opts) {
44
+ const entries = listPromptHistory();
45
+ if (entries.length === 0) {
46
+ process.stdout.write('No prompt history recorded.\n');
47
+ return EXIT_OK;
48
+ }
49
+ const parsed = Number.parseInt(opts.lines ?? '', 10);
50
+ const wanted = Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_LIST_ENTRIES;
51
+ const shown = entries.slice(0, wanted);
52
+ if (opts.json) {
53
+ process.stdout.write(`${JSON.stringify(shown, null, 2)}\n`);
54
+ return EXIT_OK;
55
+ }
56
+ for (const entry of shown) {
57
+ // Newlines would break the one-entry-per-line reading this output invites.
58
+ const text = entry.text.replace(/\r?\n/g, ' ');
59
+ process.stdout.write(`${new Date(entry.ts).toISOString()} ${text}\n`);
60
+ }
61
+ return EXIT_OK;
62
+ }
63
+ function runClear(opts) {
64
+ if (!opts.yes) {
65
+ process.stderr.write('Refusing to delete the prompt history without --yes.\n');
66
+ return EXIT_USAGE;
67
+ }
68
+ const removed = clearPromptHistory();
69
+ process.stdout.write(`Removed ${removed} entries.\n`);
70
+ return EXIT_OK;
71
+ }
@@ -39,6 +39,19 @@ export interface CliFlags {
39
39
  skillToolPolicy?: string;
40
40
  /** `--plan` ⇒ true, `--no-plan` ⇒ false, absent ⇒ undefined (plan-mode §4.4). */
41
41
  plan?: boolean;
42
+ /**
43
+ * `--team` ⇒ true, `--no-team` ⇒ false, absent ⇒ undefined (team-subagents §4.4).
44
+ *
45
+ * BOTH commander forms must be declared and this field must be carried through
46
+ * `toFlags`, or the flag is inert with no compile error and no runtime error —
47
+ * the failure `cli.tsx` already records in its own words for `--no-mouse`
48
+ * (P1-4). Declaring only the negative form additionally makes commander
49
+ * default `opts.team` to `true`, at which point the flag is indistinguishable
50
+ * from its own default and silently overrides `config.json` on EVERY run.
51
+ */
52
+ team?: boolean;
53
+ /** `--team-max <n>` — override `team.maxSubagents` for this run (still clamped). */
54
+ teamMax?: string | number;
42
55
  /** `--log-level <lv>` — this run only, never persisted. */
43
56
  logLevel?: string;
44
57
  /** `--log-file` ⇒ true, `--no-log-file` ⇒ false, absent ⇒ undefined. */
@@ -6,9 +6,12 @@
6
6
  * legitimately long single tool run can never be watchdog-killed.
7
7
  */
8
8
  import process from 'node:process';
9
- import { DEFAULT_CONFIG, IDLE_TIMEOUT_MARGIN_MS, clampAskRounds, clampDensity, clampHumanTimeout, clampLogConfig, clampSkillsConfig, clampTheme, clampThinkingLevel, clampTranscriptWindow, coerceMaxTokens, coercePositiveInt, isSkillsToolPolicyMode, } from './schema.js';
9
+ import { DEFAULT_CONFIG, IDLE_TIMEOUT_MARGIN_MS, clampAskRounds, clampDensity, clampHumanTimeout, clampLogConfig, clampSkillsConfig, clampTeamConfig, clampTheme, clampThinkingLevel, clampTranscriptWindow, coerceMaxTokens, coercePositiveInt, isSkillsToolPolicyMode, } from './schema.js';
10
10
  import { loadDotenv, readEnvConfig, readEnvDisabledSkills } from './env.js';
11
11
  import { readConfigFile } from './store.js';
12
+ import { getConfigPath } from './app-paths.js';
13
+ import { setHistoryEnabled } from './prompt-history.js';
14
+ import { readSubmitCount } from './ui-state.js';
12
15
  import { getLogger } from '../logging/logger.js';
13
16
  import { registerSecret, registerSecretsFrom } from '../logging/secret-registry.js';
14
17
  import { detectCapabilities } from '../ui/capabilities.js';
@@ -73,6 +76,27 @@ function resolveLogToFile(flags, env, file) {
73
76
  return false;
74
77
  return true;
75
78
  }
79
+ /**
80
+ * Merge the `team` section across all four layers (defaults › file › env ›
81
+ * flags), team-subagents §3.10.
82
+ *
83
+ * `flags.team !== undefined` rather than a truthiness check, and that is the
84
+ * whole point (P1-4): a truthiness check cannot tell `--no-team` from "not
85
+ * passed", so `--no-team` would resolve to the config file's value and the off
86
+ * switch would not be off. `--no-skills` gets away with a one-directional test
87
+ * only because nothing ever needs to distinguish `--skills` from silence.
88
+ */
89
+ function resolveTeamConfig(flags, env, file) {
90
+ return clampTeamConfig({
91
+ ...DEFAULT_CONFIG.team,
92
+ ...(file.team ?? {}),
93
+ ...(env.team ?? {}),
94
+ ...(flags.team !== undefined ? { enabled: flags.team } : {}),
95
+ ...(flags.teamMax !== undefined && flags.teamMax !== ''
96
+ ? { maxSubagents: flags.teamMax }
97
+ : {}),
98
+ });
99
+ }
76
100
  /** Merge the `log` section across all four layers (defaults › file › env › flags). */
77
101
  function resolveLogConfig(flags, env, file) {
78
102
  const merged = clampLogConfig({
@@ -178,7 +202,12 @@ export function loadConfig(flags = {}) {
178
202
  // Detect terminal capabilities once; force monochrome when color is disabled.
179
203
  const caps = detectCapabilities(process.env, process.stdout);
180
204
  const colorLevel = color ? caps.colorLevel : 0;
181
- return {
205
+ // No flag and no env var: a privacy switch you set once, not a per-run knob.
206
+ const historyEnabled = file.historyEnabled ?? DEFAULT_CONFIG.historyEnabled;
207
+ // The prompt-history store is a module-level singleton, so the resolved value
208
+ // has to be pushed into it — this is the ONLY place that happens.
209
+ setHistoryEnabled(historyEnabled);
210
+ const config = {
182
211
  provider,
183
212
  model,
184
213
  baseUrl,
@@ -195,12 +224,12 @@ export function loadConfig(flags = {}) {
195
224
  toolTimeoutMs,
196
225
  idleTimeoutMs,
197
226
  apiKeys,
198
- recentModels: file.recentModels ?? [],
199
- promptHistory: file.promptHistory ?? [],
227
+ historyEnabled,
200
228
  density,
201
229
  hints,
202
230
  mouse: resolveMouse(flags, env.partial, file),
203
- submitCount: coercePositiveInt(file.submitCount, 0),
231
+ // From `<home>/state.json`, not the config file — see `ui-state.ts`.
232
+ submitCount: readSubmitCount(),
204
233
  startInPlanMode,
205
234
  planModeMaxAskRounds,
206
235
  planModeHumanTimeoutMs,
@@ -217,12 +246,43 @@ export function loadConfig(flags = {}) {
217
246
  : {}),
218
247
  },
219
248
  log: resolveLogConfig(flags, env.partial, file),
249
+ team: resolveTeamConfig(flags, env.partial, file),
220
250
  cwd,
221
251
  color,
222
252
  colorLevel,
223
253
  unicode: caps.unicode,
224
254
  apiKeyOverride: flags.apiKey && flags.apiKey.trim().length > 0 ? flags.apiKey.trim() : undefined,
225
255
  };
256
+ recordResolvedConfig(config);
257
+ return config;
258
+ }
259
+ /**
260
+ * One record naming the values this run actually resolved to.
261
+ *
262
+ * "I changed the model in config.json and it still uses the old one" was, until
263
+ * this line existed, unanswerable after the fact: four layers feed this
264
+ * function and none of them left a trace. Now the answer is in the log next to
265
+ * the path the values came from.
266
+ *
267
+ * NO SECRET VALUES — only whether a key was found. `logging/redact.ts` would
268
+ * catch one anyway, but a record that never carries the secret does not depend
269
+ * on that backstop being configured.
270
+ */
271
+ function recordResolvedConfig(config) {
272
+ getLogger().info('config', 'config_loaded', {
273
+ provider: config.provider,
274
+ model: config.model,
275
+ baseUrl: config.baseUrl,
276
+ thinkingLevel: config.thinkingLevel,
277
+ theme: config.theme,
278
+ fullscreen: config.fullscreen,
279
+ toolTimeoutMs: config.toolTimeoutMs,
280
+ idleTimeoutMs: config.idleTimeoutMs,
281
+ historyEnabled: config.historyEnabled,
282
+ logLevel: config.log.level,
283
+ hasApiKey: Boolean(config.apiKeys[config.provider]),
284
+ configPath: getConfigPath(),
285
+ });
226
286
  }
227
287
  /**
228
288
  * Build the `getApiKey(providerId)` resolver the core `Agent` consumes.
@@ -0,0 +1,32 @@
1
+ /**
2
+ * One-shot move of the four runtime-state keys out of `config.json`
3
+ * (config-state-separation §4.6).
4
+ *
5
+ * ══════════════════════════════════════════════════════════════════════════
6
+ * WHY THIS EXISTS: the schema change alone would strip `promptHistory`,
7
+ * `submitCount` and `mouseNoticeSeen` from an existing user's config file on
8
+ * the first write after upgrading, with nowhere for the values to go. That is
9
+ * the only path in this feature that can actually LOSE data, which is why the
10
+ * strip and this migration must ship together.
11
+ * ══════════════════════════════════════════════════════════════════════════
12
+ *
13
+ * THE IDEMPOTENCE KEY IS THE SOURCE DATA — "are those keys still in
14
+ * config.json" — not a breadcrumb and not "does prompt-history.jsonl exist".
15
+ * Same reasoning as `migrate-home.ts`: a breadcrumb written before a failed
16
+ * step makes a half-finished migration skip itself forever. Keyed on the source,
17
+ * a failure simply retries next launch, and the two import functions are
18
+ * written so that retrying imports nothing twice.
19
+ *
20
+ * NEVER THROWS (C3) and never prints (D-8). The user sees no difference —
21
+ * history still recalls, the counter still counts — so a line on stderr would
22
+ * be noise, and `aragon config home` names the new files for anyone curious.
23
+ */
24
+ export interface StateMigrationResult {
25
+ /** The keys actually carried out of the file, e.g. `['promptHistory']`. */
26
+ moved: string[];
27
+ /** History entries imported, for the log record. */
28
+ promptEntries: number;
29
+ /** Returned rather than thrown (C3). */
30
+ error?: string;
31
+ }
32
+ export declare function migrateStateOutOfConfig(): StateMigrationResult;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * One-shot move of the four runtime-state keys out of `config.json`
3
+ * (config-state-separation §4.6).
4
+ *
5
+ * ══════════════════════════════════════════════════════════════════════════
6
+ * WHY THIS EXISTS: the schema change alone would strip `promptHistory`,
7
+ * `submitCount` and `mouseNoticeSeen` from an existing user's config file on
8
+ * the first write after upgrading, with nowhere for the values to go. That is
9
+ * the only path in this feature that can actually LOSE data, which is why the
10
+ * strip and this migration must ship together.
11
+ * ══════════════════════════════════════════════════════════════════════════
12
+ *
13
+ * THE IDEMPOTENCE KEY IS THE SOURCE DATA — "are those keys still in
14
+ * config.json" — not a breadcrumb and not "does prompt-history.jsonl exist".
15
+ * Same reasoning as `migrate-home.ts`: a breadcrumb written before a failed
16
+ * step makes a half-finished migration skip itself forever. Keyed on the source,
17
+ * a failure simply retries next launch, and the two import functions are
18
+ * written so that retrying imports nothing twice.
19
+ *
20
+ * NEVER THROWS (C3) and never prints (D-8). The user sees no difference —
21
+ * history still recalls, the counter still counts — so a line on stderr would
22
+ * be noise, and `aragon config home` names the new files for anyone curious.
23
+ */
24
+ import { readConfigFile, updatePersistedConfig } from './store.js';
25
+ import { importLegacyPromptHistory } from './prompt-history.js';
26
+ import { importLegacyUiState } from './ui-state.js';
27
+ import { LEGACY_STATE_KEYS } from './schema.js';
28
+ import { getLogger } from '../logging/logger.js';
29
+ export function migrateStateOutOfConfig() {
30
+ const result = { moved: [], promptEntries: 0 };
31
+ try {
32
+ const { config } = readConfigFile();
33
+ // A missing file has nothing to move; an UNPARSEABLE one must be left
34
+ // exactly as it is, or the user's chance to fix their typo is gone.
35
+ if (!config)
36
+ return result;
37
+ const legacy = config;
38
+ const present = LEGACY_STATE_KEYS.filter((key) => legacy[key] !== undefined);
39
+ if (present.length === 0)
40
+ return result;
41
+ result.promptEntries = importHistory(legacy);
42
+ importCounters(legacy);
43
+ // An empty patch still goes read-strip-merge-write, so this is what makes
44
+ // the legacy keys leave the file — `stripLegacyStateKeys` does the work.
45
+ updatePersistedConfig({});
46
+ result.moved = [...present];
47
+ getLogger().info('migrate', 'migrate_state_split', {
48
+ moved: result.moved,
49
+ promptEntries: result.promptEntries,
50
+ });
51
+ return result;
52
+ }
53
+ catch (err) {
54
+ return { moved: [], promptEntries: 0, error: err instanceof Error ? err.message : String(err) };
55
+ }
56
+ }
57
+ /**
58
+ * Import the recall list, unless the user turned recording off.
59
+ *
60
+ * THE `historyEnabled` CHECK IS NOT REDUNDANT with the one inside
61
+ * `appendPrompt`. This runs before any `loadConfig()`, so the module-level
62
+ * switch is still at its `true` default; the only place the user's real answer
63
+ * exists at this moment is the config object read above. The path that makes it
64
+ * matter: upgrade, switch recording off, downgrade to 0.5.x (which writes
65
+ * `promptHistory` back into config.json), upgrade again. Without this, prompts
66
+ * the user explicitly refused to have recorded get copied into the new file.
67
+ */
68
+ function importHistory(legacy) {
69
+ if (!Array.isArray(legacy.promptHistory) || legacy.promptHistory.length === 0)
70
+ return 0;
71
+ if (legacy.historyEnabled === false)
72
+ return 0;
73
+ const entries = legacy.promptHistory.filter((item) => typeof item === 'string');
74
+ return importLegacyPromptHistory(entries);
75
+ }
76
+ function importCounters(legacy) {
77
+ const patch = {};
78
+ if (typeof legacy.submitCount === 'number')
79
+ patch.submitCount = legacy.submitCount;
80
+ if (typeof legacy.mouseNoticeSeen === 'boolean')
81
+ patch.mouseNoticeSeen = legacy.mouseNoticeSeen;
82
+ if (Object.keys(patch).length === 0)
83
+ return;
84
+ importLegacyUiState(patch);
85
+ }
@@ -0,0 +1,86 @@
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
+ export declare const PROMPT_HISTORY_FILENAME = "prompt-history.jsonl";
27
+ export declare const PROMPT_HISTORY_LINE_VERSION = 1;
28
+ /**
29
+ * Longer submissions are not recorded at all (D-6).
30
+ *
31
+ * Storing a TRUNCATED copy would be worse than storing nothing: the user
32
+ * presses `↑`, gets back something that looks complete, and sends half a
33
+ * prompt. 16 KiB is far past anything typed by hand.
34
+ */
35
+ export declare const PROMPT_ENTRY_MAX_CHARS = 16384;
36
+ /** Physical lines tolerated before the file is rewritten down to the cap. */
37
+ export declare const HISTORY_COMPACT_AT_LINES = 400;
38
+ export interface PromptHistoryEntry {
39
+ /** Line schema version. Lines with any other value are skipped, not fatal. */
40
+ v: number;
41
+ /** `Date.now()` at submit time. */
42
+ ts: number;
43
+ /** The prompt verbatim — never redacted (D-5). */
44
+ text: string;
45
+ }
46
+ /**
47
+ * The recall list: oldest first, de-duplicated, capped. Cached for the life of
48
+ * the process so `/reload` — which calls `loadConfig()` again — cannot discard
49
+ * entries added during this session.
50
+ *
51
+ * Reads even when `historyEnabled` is false (RV-12): turning recording off
52
+ * stops new writes, but someone who wants to see what is already stored before
53
+ * deleting it must not be told "nothing here" while the file still exists.
54
+ */
55
+ export declare function loadPromptHistory(): string[];
56
+ /** Every entry on disk with its timestamp, newest first. For `history list`. */
57
+ export declare function listPromptHistory(): PromptHistoryEntry[];
58
+ /**
59
+ * Record one submitted prompt and return THE UPDATED LIST.
60
+ *
61
+ * The return value is the whole point of the signature (RV-1). `App.tsx` writes
62
+ * `setPromptHistory(appendPrompt(text))`: the recall list rendered by the
63
+ * composer is React state, and a module-level array updated in here re-renders
64
+ * nothing. Drop the return value and `↑` stops recalling what was just
65
+ * submitted — but only until the next restart, which makes it about as hard to
66
+ * diagnose as a bug in this area gets.
67
+ *
68
+ * Returns the current list unchanged when recording is off, the text is blank,
69
+ * or the text is oversized, so callers never need a branch.
70
+ */
71
+ export declare function appendPrompt(text: string, now?: number): string[];
72
+ /**
73
+ * Migration-only: append the texts that are not in the file yet, returning how
74
+ * many were written.
75
+ *
76
+ * Ignores `historyEnabled` on purpose — the caller decides, because it is the
77
+ * only code that can see an explicit `historyEnabled: false` in the config file
78
+ * before `loadConfig()` has run (§4.6 step 3).
79
+ */
80
+ export declare function importLegacyPromptHistory(entries: string[]): number;
81
+ /** Delete the file and empty the cache. Returns how many entries were removed. */
82
+ export declare function clearPromptHistory(): number;
83
+ /** Called once by `loadConfig()` after `historyEnabled` has been resolved. */
84
+ export declare function setHistoryEnabled(enabled: boolean): void;
85
+ /** Test-only: forget the cache, the line counters and the switch. */
86
+ export declare function resetPromptHistoryForTests(): void;