@aragon-agent/cli 0.6.2 → 0.6.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 (167) hide show
  1. package/CHANGELOG.md +321 -0
  2. package/README.md +297 -2
  3. package/dist/agent/controller.d.ts +193 -1
  4. package/dist/agent/controller.js +475 -2
  5. package/dist/agent/headless.d.ts +8 -0
  6. package/dist/agent/headless.js +47 -0
  7. package/dist/agent/reducer.d.ts +189 -0
  8. package/dist/agent/reducer.js +301 -2
  9. package/dist/agent/system-prompt.d.ts +14 -0
  10. package/dist/agent/system-prompt.js +2 -0
  11. package/dist/cli.js +297 -16
  12. package/dist/commands/builtins.js +165 -24
  13. package/dist/commands/registry.d.ts +27 -0
  14. package/dist/compaction/archive.d.ts +175 -0
  15. package/dist/compaction/archive.js +267 -0
  16. package/dist/compaction/child.d.ts +97 -0
  17. package/dist/compaction/child.js +118 -0
  18. package/dist/compaction/command.d.ts +41 -0
  19. package/dist/compaction/command.js +355 -0
  20. package/dist/compaction/compactor.d.ts +302 -0
  21. package/dist/compaction/compactor.js +810 -0
  22. package/dist/compaction/digest.d.ts +62 -0
  23. package/dist/compaction/digest.js +164 -0
  24. package/dist/compaction/limits.d.ts +165 -0
  25. package/dist/compaction/limits.js +172 -0
  26. package/dist/compaction/pressure.d.ts +135 -0
  27. package/dist/compaction/pressure.js +167 -0
  28. package/dist/compaction/summarize-call.d.ts +70 -0
  29. package/dist/compaction/summarize-call.js +86 -0
  30. package/dist/compaction/summary-prompt.d.ts +85 -0
  31. package/dist/compaction/summary-prompt.js +197 -0
  32. package/dist/compaction/tail-budget.d.ts +52 -0
  33. package/dist/compaction/tail-budget.js +67 -0
  34. package/dist/compaction/types.d.ts +215 -0
  35. package/dist/compaction/types.js +22 -0
  36. package/dist/compaction/wiring.d.ts +264 -0
  37. package/dist/compaction/wiring.js +577 -0
  38. package/dist/config/app-paths.d.ts +14 -0
  39. package/dist/config/app-paths.js +16 -0
  40. package/dist/config/cli-commands.d.ts +41 -0
  41. package/dist/config/cli-commands.js +77 -1
  42. package/dist/config/env.d.ts +5 -2
  43. package/dist/config/env.js +140 -3
  44. package/dist/config/load.d.ts +42 -0
  45. package/dist/config/load.js +101 -1
  46. package/dist/config/schema.d.ts +244 -0
  47. package/dist/config/schema.js +170 -0
  48. package/dist/config/store.js +37 -1
  49. package/dist/config/ui-state.d.ts +28 -1
  50. package/dist/config/ui-state.js +9 -0
  51. package/dist/exec/events.d.ts +50 -1
  52. package/dist/exec/runner.js +42 -0
  53. package/dist/input/limits.d.ts +72 -0
  54. package/dist/input/limits.js +63 -0
  55. package/dist/input/mouse-events.d.ts +31 -5
  56. package/dist/input/mouse-events.js +86 -27
  57. package/dist/input/paste-parse.d.ts +71 -0
  58. package/dist/input/paste-parse.js +95 -0
  59. package/dist/input/stdin-filter.d.ts +81 -0
  60. package/dist/input/stdin-filter.js +379 -0
  61. package/dist/logging/install.d.ts +53 -1
  62. package/dist/logging/install.js +81 -1
  63. package/dist/logging/logger.d.ts +1 -1
  64. package/dist/proc/classify.d.ts +52 -0
  65. package/dist/proc/classify.js +143 -0
  66. package/dist/proc/kill-tree.d.ts +42 -0
  67. package/dist/proc/kill-tree.js +119 -0
  68. package/dist/proc/limits.d.ts +84 -0
  69. package/dist/proc/limits.js +84 -0
  70. package/dist/proc/log-ring.d.ts +68 -0
  71. package/dist/proc/log-ring.js +102 -0
  72. package/dist/proc/prompt.d.ts +24 -0
  73. package/dist/proc/prompt.js +49 -0
  74. package/dist/proc/readiness.d.ts +102 -0
  75. package/dist/proc/readiness.js +219 -0
  76. package/dist/proc/supervisor.d.ts +103 -0
  77. package/dist/proc/supervisor.js +465 -0
  78. package/dist/proc/types.d.ts +175 -0
  79. package/dist/proc/types.js +15 -0
  80. package/dist/session/persist.js +55 -1
  81. package/dist/team/report.js +7 -1
  82. package/dist/team/runtime.d.ts +10 -0
  83. package/dist/team/runtime.js +7 -0
  84. package/dist/team/subagent.d.ts +31 -1
  85. package/dist/team/subagent.js +42 -15
  86. package/dist/team/types.d.ts +8 -0
  87. package/dist/tools/bash-tool.d.ts +51 -2
  88. package/dist/tools/bash-tool.js +321 -51
  89. package/dist/tools/fs-tools.d.ts +17 -0
  90. package/dist/tools/index.d.ts +55 -2
  91. package/dist/tools/index.js +39 -1
  92. package/dist/tools/proc-tools.d.ts +25 -0
  93. package/dist/tools/proc-tools.js +102 -0
  94. package/dist/ui/ActivityLine.d.ts +19 -1
  95. package/dist/ui/ActivityLine.js +10 -2
  96. package/dist/ui/App.d.ts +62 -2
  97. package/dist/ui/App.js +546 -16
  98. package/dist/ui/Composer.d.ts +66 -1
  99. package/dist/ui/Composer.js +50 -6
  100. package/dist/ui/PromptInput.d.ts +82 -3
  101. package/dist/ui/PromptInput.js +232 -49
  102. package/dist/ui/StatusBar.d.ts +59 -0
  103. package/dist/ui/StatusBar.js +16 -4
  104. package/dist/ui/TeamPanel.js +7 -1
  105. package/dist/ui/Transcript.d.ts +21 -1
  106. package/dist/ui/Transcript.js +61 -3
  107. package/dist/ui/clipboard.d.ts +52 -0
  108. package/dist/ui/clipboard.js +91 -0
  109. package/dist/ui/composer-limits.d.ts +31 -0
  110. package/dist/ui/composer-limits.js +50 -0
  111. package/dist/ui/composer-rows.d.ts +73 -0
  112. package/dist/ui/composer-rows.js +201 -0
  113. package/dist/ui/editor-reducer.d.ts +37 -0
  114. package/dist/ui/editor-reducer.js +88 -12
  115. package/dist/ui/entries/CompactionCard.d.ts +75 -0
  116. package/dist/ui/entries/CompactionCard.js +158 -0
  117. package/dist/ui/entries/ServiceCard.d.ts +66 -0
  118. package/dist/ui/entries/ServiceCard.js +116 -0
  119. package/dist/ui/entries/UserEntry.d.ts +17 -3
  120. package/dist/ui/entries/UserEntry.js +34 -5
  121. package/dist/ui/frame-differ.d.ts +46 -0
  122. package/dist/ui/frame-differ.js +94 -10
  123. package/dist/ui/gauge.d.ts +16 -1
  124. package/dist/ui/gauge.js +25 -6
  125. package/dist/ui/glyphs.d.ts +11 -0
  126. package/dist/ui/glyphs.js +2 -0
  127. package/dist/ui/layout/ScrollViewport.d.ts +43 -6
  128. package/dist/ui/layout/ScrollViewport.js +128 -21
  129. package/dist/ui/layout/budget.d.ts +44 -9
  130. package/dist/ui/layout/budget.js +42 -10
  131. package/dist/ui/layout/follow-state.d.ts +137 -0
  132. package/dist/ui/layout/follow-state.js +150 -0
  133. package/dist/ui/layout/virtual-window.d.ts +11 -0
  134. package/dist/ui/layout/virtual-window.js +85 -1
  135. package/dist/ui/overlays/HelpOverlay.js +9 -1
  136. package/dist/ui/overlays/PlanReviewOverlay.js +10 -2
  137. package/dist/ui/overlays/QuestionOverlay.js +5 -1
  138. package/dist/ui/overlays/SettingsScreen.d.ts +47 -1
  139. package/dist/ui/overlays/SettingsScreen.js +74 -2
  140. package/dist/ui/palettes.d.ts +16 -0
  141. package/dist/ui/palettes.js +6 -0
  142. package/dist/ui/paste-frames.d.ts +87 -0
  143. package/dist/ui/paste-frames.js +112 -0
  144. package/dist/ui/paste-tokens.d.ts +90 -0
  145. package/dist/ui/paste-tokens.js +147 -0
  146. package/dist/ui/screen.d.ts +38 -0
  147. package/dist/ui/screen.js +113 -8
  148. package/dist/ui/selection/highlight.d.ts +39 -0
  149. package/dist/ui/selection/highlight.js +64 -0
  150. package/dist/ui/selection/screen-mirror.d.ts +24 -0
  151. package/dist/ui/selection/screen-mirror.js +35 -0
  152. package/dist/ui/selection/selection-controller.d.ts +88 -0
  153. package/dist/ui/selection/selection-controller.js +282 -0
  154. package/dist/ui/selection/selection.d.ts +90 -0
  155. package/dist/ui/selection/selection.js +223 -0
  156. package/dist/ui/stdout-frame-writer.d.ts +33 -0
  157. package/dist/ui/stdout-frame-writer.js +15 -0
  158. package/dist/ui/theme.d.ts +11 -0
  159. package/dist/ui/theme.js +2 -0
  160. package/dist/ui/transcript-text.js +72 -0
  161. package/dist/ui/use-startup-notices.d.ts +27 -1
  162. package/dist/ui/use-startup-notices.js +43 -11
  163. package/dist/ui/use-wheel-routing.d.ts +1 -1
  164. package/dist/ui/use-wheel-routing.js +8 -0
  165. package/package.json +4 -2
  166. package/dist/input/stdin-mouse-filter.d.ts +0 -42
  167. package/dist/input/stdin-mouse-filter.js +0 -153
@@ -13,7 +13,7 @@ import process from 'node:process';
13
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 { clampFastConfig, clampRetryConfig, clampUpdateConfig, coercePositiveInt, maskSecret, DEFAULT_FAST_CONFIG, DEFAULT_RETRY_CONFIG, DEFAULT_TEAM_CONFIG, DEFAULT_UPDATE_CONFIG, } from './schema.js';
16
+ import { clampFastConfig, clampRetryConfig, clampCompactionConfig, parseThresholdInput, clampUpdateConfig, coercePositiveInt, maskSecret, DEFAULT_FAST_CONFIG, DEFAULT_RETRY_CONFIG, DEFAULT_TEAM_CONFIG, DEFAULT_COMPACTION_CONFIG, DEFAULT_UPDATE_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 = [
@@ -103,6 +103,33 @@ export const UPDATE_CONFIG_SET_KEYS = [
103
103
  'update.registry',
104
104
  'update.distTag',
105
105
  ];
106
+ /**
107
+ * Config keys under `compaction.` that `aragon config set` accepts
108
+ * (context-auto-compaction §4.2).
109
+ *
110
+ * A DEDICATED SETTER RATHER THAN A GENERIC DOTTED WRITER, for the reason the
111
+ * five sets above each record: a generic setter would write `compaction` as a
112
+ * FLAT string key (`"compaction.enabled": "false"` at the top level of the
113
+ * file), the section would silently never take effect, and `config set` would
114
+ * still print `Set compaction.enabled = false`.
115
+ *
116
+ * THIS SET IS WHAT MAKES THE KILL SWITCH REACHABLE FROM A SCRIPT. `config list`
117
+ * enumerates the section dynamically and so shows these keys whether they are
118
+ * here or not; `config set` does not, so without this block
119
+ * `aragon config set compaction.enabled false` fails with "Unknown config key"
120
+ * while the key is visibly listed one command over. Manual-test row 9 — the
121
+ * persisted-off-survives-a-flagless-run check — runs exactly that command.
122
+ */
123
+ export const COMPACTION_CONFIG_SET_KEYS = [
124
+ 'compaction.enabled',
125
+ 'compaction.threshold',
126
+ 'compaction.warnThreshold',
127
+ 'compaction.keepRecentTurns',
128
+ 'compaction.useFastTier',
129
+ 'compaction.onFailure',
130
+ 'compaction.subagents',
131
+ 'compaction.archive',
132
+ ];
106
133
  function isTrue(value) {
107
134
  return value === 'true' || value === '1';
108
135
  }
@@ -294,6 +321,55 @@ export function applyUpdateConfigSet(key, value) {
294
321
  return null;
295
322
  }
296
323
  }
324
+ /**
325
+ * Translate a `compaction.*` dotted key into a patch, or `null` for anything
326
+ * else (context-auto-compaction §4.2).
327
+ *
328
+ * EVERY KEY ROUTES THROUGH `clampCompactionConfig`, not through a local parse,
329
+ * for the reason `applyRetryConfigSet`, `applyFastConfigSet` and
330
+ * `applyUpdateConfigSet` all record: one gate for both directions means no key
331
+ * here can pick up a different coercion later, and `config set
332
+ * compaction.threshold 0.2` writes the clamped `0.5` while the caller echoes the
333
+ * STORED value.
334
+ *
335
+ * `threshold` and `warnThreshold` additionally accept a PERCENTAGE, because that
336
+ * is the unit the status bar shows and `/compact threshold` accepts.
337
+ *
338
+ * A VALUE THE PARSER REJECTS RESOLVES TO THE DEFAULT, NOT TO THE CURRENT SETTING
339
+ * — the same thing `config set retry.maxRetries banana` does, because `clampOne`
340
+ * merges the field onto `DEFAULT_COMPACTION_CONFIG` before clamping it. That is
341
+ * the house behaviour and it is not silent: `cli.tsx` echoes the STORED value, so
342
+ * a typo prints the number that was actually written rather than the one that was
343
+ * typed. Unlike the settings screen and `/compact threshold`, both of which
344
+ * genuinely ignore an unparseable value, this surface has no third state.
345
+ */
346
+ export function applyCompactionConfigSet(key, value) {
347
+ const patch = (compaction) => ({ compaction });
348
+ const clampOne = (field, raw = value) => {
349
+ const merged = clampCompactionConfig({ ...DEFAULT_COMPACTION_CONFIG, [field]: raw });
350
+ return { [field]: merged[field] };
351
+ };
352
+ switch (key) {
353
+ case 'compaction.enabled':
354
+ return patch(clampOne('enabled', isTrue(value)));
355
+ case 'compaction.threshold':
356
+ return patch(clampOne('threshold', parseThresholdInput(value) ?? undefined));
357
+ case 'compaction.warnThreshold':
358
+ return patch(clampOne('warnThreshold', parseThresholdInput(value) ?? undefined));
359
+ case 'compaction.keepRecentTurns':
360
+ return patch(clampOne('keepRecentTurns'));
361
+ case 'compaction.useFastTier':
362
+ return patch(clampOne('useFastTier', isTrue(value)));
363
+ case 'compaction.onFailure':
364
+ return patch(clampOne('onFailure'));
365
+ case 'compaction.subagents':
366
+ return patch(clampOne('subagents', isTrue(value)));
367
+ case 'compaction.archive':
368
+ return patch(clampOne('archive', isTrue(value)));
369
+ default:
370
+ return null;
371
+ }
372
+ }
297
373
  /**
298
374
  * `log.redactSecrets false` is the one setting that lets credentials reach the
299
375
  * disk. It is command-line-only (there is no settings-screen entry) and it says
@@ -7,14 +7,17 @@
7
7
  * - Overrides: ARAGON_PROVIDER, ARAGON_MODEL, ARAGON_BASE_URL, ARAGON_THINKING,
8
8
  * ARAGON_SHOW_THINKING,
9
9
  * ARAGON_MAX_TOKENS (a number, or `auto` / `0` for the per-model ceiling),
10
- * ARAGON_THEME, ARAGON_MOUSE, ARAGON_PLAN, ARAGON_TEAM,
10
+ * ARAGON_THEME, ARAGON_MOUSE, ARAGON_PASTE, ARAGON_PLAN, ARAGON_TEAM,
11
11
  * ARAGON_TEAM_MAX, ARAGON_RETRY, ARAGON_RETRY_MAX, ARAGON_FAST,
12
12
  * ARAGON_FAST_PROVIDER, ARAGON_FAST_MODEL, ARAGON_FAST_BASE_URL,
13
13
  * ARAGON_LOG_LEVEL,
14
14
  * ARAGON_LOG_FILE, ARAGON_LOG_DIR, ARAGON_TRANSCRIPT_RETAIN,
15
15
  * ARAGON_RENDER_GOVERNOR, ARAGON_MAX_RENDER_INTERVAL_MS,
16
16
  * ARAGON_DIFF_RENDER, ARAGON_SYNC_OUTPUT,
17
- * ARAGON_UPDATE.
17
+ * ARAGON_UPDATE,
18
+ * ARAGON_COMPACTION, ARAGON_COMPACTION_THRESHOLD,
19
+ * ARAGON_COMPACTION_KEEP_TURNS, ARAGON_COMPACTION_SUBAGENTS,
20
+ * ARAGON_COMPACTION_ARCHIVE.
18
21
  *
19
22
  * READ ELSEWHERE, ON PURPOSE: `ARAGON_UPDATE_REGISTRY` and `npm_config_registry`
20
23
  * are resolved by `update/registry.ts::resolveRegistryUrl`, NOT here, because
@@ -7,14 +7,17 @@
7
7
  * - Overrides: ARAGON_PROVIDER, ARAGON_MODEL, ARAGON_BASE_URL, ARAGON_THINKING,
8
8
  * ARAGON_SHOW_THINKING,
9
9
  * ARAGON_MAX_TOKENS (a number, or `auto` / `0` for the per-model ceiling),
10
- * ARAGON_THEME, ARAGON_MOUSE, ARAGON_PLAN, ARAGON_TEAM,
10
+ * ARAGON_THEME, ARAGON_MOUSE, ARAGON_PASTE, ARAGON_PLAN, ARAGON_TEAM,
11
11
  * ARAGON_TEAM_MAX, ARAGON_RETRY, ARAGON_RETRY_MAX, ARAGON_FAST,
12
12
  * ARAGON_FAST_PROVIDER, ARAGON_FAST_MODEL, ARAGON_FAST_BASE_URL,
13
13
  * ARAGON_LOG_LEVEL,
14
14
  * ARAGON_LOG_FILE, ARAGON_LOG_DIR, ARAGON_TRANSCRIPT_RETAIN,
15
15
  * ARAGON_RENDER_GOVERNOR, ARAGON_MAX_RENDER_INTERVAL_MS,
16
16
  * ARAGON_DIFF_RENDER, ARAGON_SYNC_OUTPUT,
17
- * ARAGON_UPDATE.
17
+ * ARAGON_UPDATE,
18
+ * ARAGON_COMPACTION, ARAGON_COMPACTION_THRESHOLD,
19
+ * ARAGON_COMPACTION_KEEP_TURNS, ARAGON_COMPACTION_SUBAGENTS,
20
+ * ARAGON_COMPACTION_ARCHIVE.
18
21
  *
19
22
  * READ ELSEWHERE, ON PURPOSE: `ARAGON_UPDATE_REGISTRY` and `npm_config_registry`
20
23
  * are resolved by `update/registry.ts::resolveRegistryUrl`, NOT here, because
@@ -34,7 +37,7 @@ import process from 'node:process';
34
37
  import dotenv from 'dotenv';
35
38
  import { registerSecretsFrom } from '../logging/secret-registry.js';
36
39
  import { clampLogLevel } from '../logging/levels.js';
37
- import { clampMaxTokens, clampTheme, clampThinkingLevel, isAutoToken, } from './schema.js';
40
+ import { clampMaxTokens, clampTheme, clampThinkingLevel, isAutoToken, parseThresholdInput, } from './schema.js';
38
41
  /**
39
42
  * Load a project `.env` from the given directory into `process.env` (existing
40
43
  * process env wins — dotenv never overrides already-set variables).
@@ -127,6 +130,36 @@ export function readEnvConfig() {
127
130
  if (mouse !== undefined && mouse.length > 0) {
128
131
  partial.mouse = mouse === '1' || mouse === 'true' || mouse === 'on' || mouse === 'yes';
129
132
  }
133
+ // ARAGON_MOUSE_SELECT=0 turns drag-select off while leaving the wheel alone —
134
+ // the third rung of the mouse ladder (`--no-mouse` is the first two). Parsed by
135
+ // the POSITIVE list, exactly as its neighbour above is.
136
+ const mouseSelect = process.env.ARAGON_MOUSE_SELECT?.trim().toLowerCase();
137
+ if (mouseSelect !== undefined && mouseSelect.length > 0) {
138
+ partial.mouseSelect =
139
+ mouseSelect === '1' ||
140
+ mouseSelect === 'true' ||
141
+ mouseSelect === 'on' ||
142
+ mouseSelect === 'yes';
143
+ }
144
+ // ARAGON_PASTE=0 turns paste recognition off, which is the documented one-env
145
+ // revert to v0.6.3 input handling (tui-paste-handling section 7.2). Parsed by
146
+ // the POSITIVE list, exactly as its two neighbours above are.
147
+ const paste = process.env.ARAGON_PASTE?.trim().toLowerCase();
148
+ if (paste !== undefined && paste.length > 0) {
149
+ partial.paste = paste === '1' || paste === 'true' || paste === 'on' || paste === 'yes';
150
+ }
151
+ // ARAGON_SCROLL_RESUME_MS=<n> — idle delay before a paused viewport returns to
152
+ // the newest line; `0` disables auto-resume. THE ONLY CHANNEL BESIDES THE
153
+ // CONFIG FILE, because §6.1 gives this key no CLI flag on purpose. `load.ts`
154
+ // clamps it to [0, 120000]; a non-numeric value is LEFT ABSENT rather than
155
+ // written, so a typo falls through to the config file underneath instead of
156
+ // silently replacing it (the rule `ARAGON_MAX_TOKENS` above already follows).
157
+ const scrollResume = process.env.ARAGON_SCROLL_RESUME_MS?.trim();
158
+ if (scrollResume !== undefined && scrollResume.length > 0) {
159
+ const n = Number.parseInt(scrollResume, 10);
160
+ if (Number.isFinite(n) && n >= 0)
161
+ partial.scrollResumeMs = n;
162
+ }
130
163
  // ARAGON_PLAN=1 starts the session in PLAN mode — the same channel story as
131
164
  // ARAGON_FULLSCREEN, for terminals reached through a wrapper that owns argv.
132
165
  const plan = process.env.ARAGON_PLAN?.trim().toLowerCase();
@@ -213,6 +246,46 @@ export function readEnvConfig() {
213
246
  if (Object.keys(section).length > 0)
214
247
  partial.todo = section;
215
248
  }
249
+ // ARAGON_BASH_BACKGROUND=0 turns background services off entirely (no
250
+ // `background` param, no `bash_output`/`bash_kill`, no prompt block, no chip);
251
+ // ARAGON_BASH_AUTO_BACKGROUND=0 keeps them but stops the classifier firing on
252
+ // an unset `background`. Parsed by the POSITIVE list, exactly as
253
+ // ARAGON_FULLSCREEN / ARAGON_MOUSE / ARAGON_PLAN / ARAGON_TEAM / ARAGON_TODO
254
+ // are - NOT by `envBool` below, which uses a negative list.
255
+ //
256
+ // Both pass the test every env var in this file has to pass: an unattended run
257
+ // in a container with a fixed entrypoint can reach neither a flag nor a config
258
+ // file, and "do not leave a dev server running in my CI job" is exactly the
259
+ // kind of decision that channel needs to make.
260
+ //
261
+ // ONE ACCUMULATED SECTION, ASSIGNED ONCE - the shape the `team` and `todo`
262
+ // branches above already use, and NOT two `if` blocks each assigning
263
+ // `partial.bash` (the `todo` P1-4 trap). THE CAST IS WHAT WOULD MAKE THAT
264
+ // MISTAKE SILENT: a second assignment compiles clean and drops the first key,
265
+ // so `ARAGON_BASH_BACKGROUND=0 ARAGON_BASH_AUTO_BACKGROUND=0` would quietly
266
+ // re-register the tools the user turned off.
267
+ const bashBackground = process.env.ARAGON_BASH_BACKGROUND?.trim().toLowerCase();
268
+ const bashAuto = process.env.ARAGON_BASH_AUTO_BACKGROUND?.trim().toLowerCase();
269
+ if ((bashBackground !== undefined && bashBackground.length > 0) ||
270
+ (bashAuto !== undefined && bashAuto.length > 0)) {
271
+ const section = {};
272
+ if (bashBackground !== undefined && bashBackground.length > 0) {
273
+ section.background =
274
+ bashBackground === '1' ||
275
+ bashBackground === 'true' ||
276
+ bashBackground === 'on' ||
277
+ bashBackground === 'yes';
278
+ }
279
+ if (bashAuto !== undefined && bashAuto.length > 0) {
280
+ section.autoBackground =
281
+ bashAuto === '1' || bashAuto === 'true' || bashAuto === 'on' || bashAuto === 'yes';
282
+ }
283
+ // A PARTIAL section, safe only because every consumer deep-merges it - the
284
+ // same contract `partial.skills`, `partial.log`, `partial.team` and
285
+ // `partial.todo` rely on.
286
+ if (Object.keys(section).length > 0)
287
+ partial.bash = section;
288
+ }
216
289
  // ARAGON_RETRY=0 is the kill switch for API retry; ARAGON_RETRY_MAX=<n>
217
290
  // overrides the count. Parsed by the POSITIVE list, exactly as
218
291
  // ARAGON_FULLSCREEN / ARAGON_MOUSE / ARAGON_PLAN / ARAGON_TEAM / ARAGON_TODO
@@ -289,6 +362,70 @@ export function readEnvConfig() {
289
362
  if (Object.keys(section).length > 0)
290
363
  partial.fast = section;
291
364
  }
365
+ // ARAGON_COMPACTION=0 turns context compaction off; the other two tune it
366
+ // (context-auto-compaction §4.3). Parsed by the POSITIVE list, exactly as
367
+ // ARAGON_FULLSCREEN / ARAGON_MOUSE / ARAGON_PLAN / ARAGON_TEAM / ARAGON_TODO /
368
+ // ARAGON_RETRY / ARAGON_FAST are — NOT by `envBool` below, whose NEGATIVE list
369
+ // disagrees with it on values like `ARAGON_COMPACTION=disable`.
370
+ //
371
+ // THREE VARS, AND DELIBERATELY NO `ARAGON_COMPACTION_ON_FAILURE` /
372
+ // `_USE_FAST_TIER` / `_WARN_THRESHOLD`. The rule this file states is that an
373
+ // environment variable exists only where a flag AND a config file are BOTH
374
+ // unreachable — a container, an ssh session, being spawned by another tool.
375
+ // "Is it on, when does it fire, and how much does it keep" passes that test;
376
+ // "what should it do if the summarizer 500s" does not, and every key that fails
377
+ // it is documentation debt plus a fifth place for the value to disagree with
378
+ // itself.
379
+ //
380
+ // ONE ACCUMULATED SECTION, ASSIGNED ONCE — the shape the four branches above
381
+ // all use, and NOT three `if` blocks each assigning `partial.compaction`. THE
382
+ // CAST IS WHAT WOULD MAKE THAT MISTAKE SILENT: a second assignment compiles
383
+ // clean and drops `enabled`.
384
+ const compaction = process.env.ARAGON_COMPACTION?.trim().toLowerCase();
385
+ const compactionThreshold = process.env.ARAGON_COMPACTION_THRESHOLD?.trim();
386
+ const compactionKeepTurns = process.env.ARAGON_COMPACTION_KEEP_TURNS?.trim();
387
+ // The two hardening kill switches (context-auto-compaction-hardening §4.3).
388
+ // They pass the same test the three above do: both name a behaviour that a
389
+ // container or an automated harness may need to switch off with no flag and no
390
+ // config file in reach - sub-agent compaction because it changes what a
391
+ // dispatch costs, and the archive because it writes conversation content to
392
+ // disk in an environment that may not permit it.
393
+ const compactionSubagents = process.env.ARAGON_COMPACTION_SUBAGENTS?.trim().toLowerCase();
394
+ const compactionArchive = process.env.ARAGON_COMPACTION_ARCHIVE?.trim().toLowerCase();
395
+ const positive = (v) => v === '1' || v === 'true' || v === 'on' || v === 'yes';
396
+ if ((compaction !== undefined && compaction.length > 0) ||
397
+ (compactionThreshold !== undefined && compactionThreshold.length > 0) ||
398
+ (compactionKeepTurns !== undefined && compactionKeepTurns.length > 0) ||
399
+ (compactionSubagents !== undefined && compactionSubagents.length > 0) ||
400
+ (compactionArchive !== undefined && compactionArchive.length > 0)) {
401
+ const section = {};
402
+ if (compaction !== undefined && compaction.length > 0) {
403
+ section.enabled = positive(compaction);
404
+ }
405
+ if (compactionSubagents !== undefined && compactionSubagents.length > 0) {
406
+ section.subagents = positive(compactionSubagents);
407
+ }
408
+ if (compactionArchive !== undefined && compactionArchive.length > 0) {
409
+ section.archive = positive(compactionArchive);
410
+ }
411
+ if (compactionThreshold !== undefined && compactionThreshold.length > 0) {
412
+ // Left ABSENT on a bad value rather than clamped to a default, so a typo
413
+ // falls through to the config layer instead of masking it for the session.
414
+ const parsed = parseThresholdInput(compactionThreshold);
415
+ if (parsed !== null)
416
+ section.threshold = parsed;
417
+ }
418
+ if (compactionKeepTurns !== undefined && compactionKeepTurns.length > 0) {
419
+ const n = Number.parseInt(compactionKeepTurns, 10);
420
+ if (Number.isFinite(n) && n > 0)
421
+ section.keepRecentTurns = n;
422
+ }
423
+ // A PARTIAL section, safe only because every consumer deep-merges it — the
424
+ // same contract the five sections above rely on.
425
+ if (Object.keys(section).length > 0) {
426
+ partial.compaction = section;
427
+ }
428
+ }
292
429
  // ARAGON_UPDATE=0|off|false turns auto-update off; `notify` reports without
293
430
  // installing; 1|on|true|yes turns it on (cli-auto-update §4.2).
294
431
  //
@@ -36,6 +36,30 @@ export interface CliFlags {
36
36
  liveToolOutput?: boolean;
37
37
  /** `--mouse` ⇒ true, `--no-mouse` ⇒ false, absent ⇒ undefined. */
38
38
  mouse?: boolean;
39
+ /**
40
+ * `--mouse-select` ⇒ true, `--no-mouse-select` ⇒ false, absent ⇒ undefined
41
+ * (tui-selection-and-scroll-follow §6.1).
42
+ *
43
+ * DECLARED AS A PAIR, and carried through `toFlags`, for the reason the
44
+ * `mouse` note above it records: a flag that reaches neither `CliFlags` nor
45
+ * `toFlags` is inert with no compile error and no runtime error. `mouseSelect`
46
+ * is additionally PERSISTED and defaults to `true`, so a lone
47
+ * `--no-mouse-select` would make commander default `opts.mouseSelect` to
48
+ * `true` — indistinguishable from silence, and silently overriding a stored
49
+ * `false` on every run (the `--todo-panel` trap).
50
+ */
51
+ mouseSelect?: boolean;
52
+ /**
53
+ * `--paste` ⇒ true, `--no-paste` ⇒ false, absent ⇒ undefined
54
+ * (tui-paste-handling section 7.1).
55
+ *
56
+ * DECLARED AS A PAIR, and carried through `toFlags`, for the reason the two
57
+ * notes above record. `paste` is additionally PERSISTED and defaults to
58
+ * `true`, so a lone `--no-paste` would make commander default `opts.paste` to
59
+ * `true` -- indistinguishable from silence, and silently overriding a stored
60
+ * `false` on every run that passed no flag at all.
61
+ */
62
+ paste?: boolean;
39
63
  /** `--no-skills` ⇒ false. Turns the whole skill subsystem off (§7.5). */
40
64
  skills?: boolean;
41
65
  /** `--skill <name>` ×N — force Level 2 injection for this run. */
@@ -178,6 +202,24 @@ export interface CliFlags {
178
202
  * config file and `ARAGON_UPDATE=notify`.
179
203
  */
180
204
  update?: boolean;
205
+ /**
206
+ * `--compaction` ⇒ true, `--no-compaction` ⇒ false, absent ⇒ undefined
207
+ * (context-auto-compaction §4.3).
208
+ *
209
+ * DECLARED AS A PAIR — MANDATORY (C-13 / P1-5), for the reason `--team`,
210
+ * `--todo-panel`, `--render-governor`, `--retry`, `--fast` and `--update` each
211
+ * record in turn, and this one shares the WORST polarity with `--update`:
212
+ * `compaction.enabled` is PERSISTED and defaults to `true`, so a lone
213
+ * `--no-compaction` would make commander default `opts.compaction` to `true`,
214
+ * at which point the flag is indistinguishable from its own default and
215
+ * silently overwrites a stored `false` on EVERY run that passed no flag at all.
216
+ *
217
+ * That would make R-12's whole mitigation ("one config key restores the old
218
+ * behaviour exactly") FALSE — for a feature that spends the user's money.
219
+ */
220
+ compaction?: boolean;
221
+ /** `--compaction-threshold <0..1|N%>` — session override for `threshold`. */
222
+ compactionThreshold?: string;
181
223
  }
182
224
  /**
183
225
  * Resolve the effective config by merging all layers, then derive the runtime
@@ -6,7 +6,7 @@
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, clampFastConfig, clampRetryConfig, clampSkillsConfig, clampTeamConfig, clampTodoConfig, clampUpdateConfig, clampTheme, clampThinkingLevel, clampMaxTokens, clampMaxRenderInterval, clampTranscriptRetain, clampTranscriptWindow, coercePositiveInt, isAutoToken, isSkillsToolPolicyMode, } from './schema.js';
9
+ import { DEFAULT_CONFIG, IDLE_TIMEOUT_MARGIN_MS, clampAskRounds, clampDensity, clampHumanTimeout, clampCompactionConfig, clampLogConfig, clampFastConfig, clampRetryConfig, clampSkillsConfig, clampTeamConfig, clampTodoConfig, clampBashConfig, clampUpdateConfig, clampTheme, clampThinkingLevel, clampMaxTokens, clampMaxRenderInterval, clampScrollResumeMs, clampTranscriptRetain, clampTranscriptWindow, coercePositiveInt, isAutoToken, isSkillsToolPolicyMode, parseThresholdInput, } from './schema.js';
10
10
  import { loadDotenv, readEnvConfig, readEnvDisabledSkills } from './env.js';
11
11
  import { readConfigFile } from './store.js';
12
12
  import { getConfigPath } from './app-paths.js';
@@ -59,6 +59,56 @@ function resolveMouse(flags, env, file) {
59
59
  return file.mouse;
60
60
  return DEFAULT_CONFIG.mouse;
61
61
  }
62
+ /**
63
+ * Drag-select: flag › env › file › default `true`
64
+ * (tui-selection-and-scroll-follow §6.1).
65
+ *
66
+ * SHAPED EXACTLY LIKE `resolveMouse` above, deliberately and not by copy-paste
67
+ * habit: it has the same four layers, the same "a persisted `true` is a value
68
+ * and not an opinion" property, and the same env var story. Reaching for the
69
+ * `hints` shape instead — `flags.x !== undefined ? … : file.x ?? DEFAULT`, which
70
+ * reads NO ENV AT ALL — would ship `ARAGON_MOUSE_SELECT` as documented-but-dead,
71
+ * which is precisely the failure the note above `resolveMouse` records for
72
+ * `ARAGON_MOUSE`.
73
+ */
74
+ function resolveMouseSelect(flags, env, file) {
75
+ if (flags.mouseSelect !== undefined)
76
+ return flags.mouseSelect;
77
+ if (env.mouseSelect !== undefined)
78
+ return env.mouseSelect;
79
+ if (file.mouseSelect !== undefined)
80
+ return file.mouseSelect;
81
+ return DEFAULT_CONFIG.mouseSelect;
82
+ }
83
+ /**
84
+ * Paste handling: flag > env > file > default `true` (tui-paste-handling
85
+ * section 7.4).
86
+ *
87
+ * SHAPED EXACTLY LIKE `resolveMouse` above, for the reason its own note gives:
88
+ * reaching for the `hints` shape -- `flags.x !== undefined ? ... : file.x ??
89
+ * DEFAULT`, which reads NO ENV AT ALL -- would ship `ARAGON_PASTE` as
90
+ * documented-but-dead.
91
+ */
92
+ function resolvePaste(flags, env, file) {
93
+ if (flags.paste !== undefined)
94
+ return flags.paste;
95
+ if (env.paste !== undefined)
96
+ return env.paste;
97
+ if (file.paste !== undefined)
98
+ return file.paste;
99
+ return DEFAULT_CONFIG.paste;
100
+ }
101
+ /**
102
+ * Idle scroll-resume delay: env › file › default 5000, clamped to [0, 120000].
103
+ *
104
+ * NO FLAG LAYER, and the absence is the design rather than an omission (§6.1):
105
+ * this key has no `--scroll-resume-ms`, so `CliFlags` carries no field for one
106
+ * and a resolver that read it would be reading a value nothing can ever set.
107
+ */
108
+ function resolveScrollResumeMs(env, file) {
109
+ const raw = env.scrollResumeMs ?? file.scrollResumeMs;
110
+ return clampScrollResumeMs(raw, DEFAULT_CONFIG.scrollResumeMs);
111
+ }
62
112
  /**
63
113
  * Resolve whether logs reach a file: flag › env › an explicit `false` in the
64
114
  * file › default `true`.
@@ -121,6 +171,24 @@ function resolveTodoConfig(flags, env, file) {
121
171
  ...(flags.todoFollow !== undefined ? { followThrough: flags.todoFollow } : {}),
122
172
  });
123
173
  }
174
+ /**
175
+ * Merge the `bash` section across the three layers that supply it
176
+ * (defaults > file > env), background-service-supervision §5.4.
177
+ *
178
+ * THREE LAYERS, NOT FOUR, AND THAT IS NOT AN OVERSIGHT. There is deliberately no
179
+ * `--no-background` flag: the two switches this section carries are already
180
+ * reachable from `config.json` (a per-machine preference) and from the
181
+ * environment (the channel a container entrypoint has), which is the two-part
182
+ * test every knob in this file is held to. A third spelling would be a third
183
+ * place for the answer to differ.
184
+ */
185
+ function resolveBashConfig(env, file) {
186
+ return clampBashConfig({
187
+ ...DEFAULT_CONFIG.bash,
188
+ ...(file.bash ?? {}),
189
+ ...(env.bash ?? {}),
190
+ });
191
+ }
124
192
  /**
125
193
  * Merge the `retry` section across all four layers (defaults › file › env ›
126
194
  * flags), llm-api-retry-backoff §6.2.
@@ -208,6 +276,33 @@ function resolveUpdateConfig(flags, env, file) {
208
276
  ...(flags.update !== undefined ? { mode: flags.update ? 'auto' : 'off' } : {}),
209
277
  });
210
278
  }
279
+ /**
280
+ * Merge the `compaction` section across all four layers (defaults › file › env ›
281
+ * flags), context-auto-compaction §4.3.
282
+ *
283
+ * `!== undefined` ON `flags.compaction`, WHICH IS THE WHOLE POINT (C-13 / P1-5).
284
+ * The rule `resolveTeamConfig`, `resolveTodoConfig`, `resolveRetryConfig`,
285
+ * `resolveFastConfig` and `resolveUpdateConfig` each state once: commander
286
+ * materialises a lone `--no-compaction` as `opts.compaction = true` when the flag
287
+ * is absent, so a truthiness check cannot tell `--no-compaction` from "not
288
+ * passed" — and here the damage runs in the worst direction, silently re-enabling
289
+ * a subsystem the user turned off, on every flagless run, for a feature that
290
+ * spends money.
291
+ *
292
+ * `--compaction-threshold` is PARSED rather than spread, because `90%` and `0.9`
293
+ * are both legitimate spellings and a typo must change NOTHING rather than
294
+ * resolve to a default (`parseThresholdInput` returns `null`).
295
+ */
296
+ function resolveCompactionConfig(flags, env, file) {
297
+ const thresholdFlag = flags.compactionThreshold !== undefined ? parseThresholdInput(flags.compactionThreshold) : null;
298
+ return clampCompactionConfig({
299
+ ...DEFAULT_CONFIG.compaction,
300
+ ...(file.compaction ?? {}),
301
+ ...(env.compaction ?? {}),
302
+ ...(flags.compaction !== undefined ? { enabled: flags.compaction } : {}),
303
+ ...(thresholdFlag !== null ? { threshold: thresholdFlag } : {}),
304
+ });
305
+ }
211
306
  /**
212
307
  * Resolve the adaptive render governor: flag › env › file › default `true`
213
308
  * (tui-render-performance L4).
@@ -460,6 +555,9 @@ export function loadConfig(flags = {}) {
460
555
  density,
461
556
  hints,
462
557
  mouse: resolveMouse(flags, env.partial, file),
558
+ mouseSelect: resolveMouseSelect(flags, env.partial, file),
559
+ paste: resolvePaste(flags, env.partial, file),
560
+ scrollResumeMs: resolveScrollResumeMs(env.partial, file),
463
561
  // From `<home>/state.json`, not the config file — see `ui-state.ts`.
464
562
  submitCount: readSubmitCount(),
465
563
  startInPlanMode,
@@ -483,6 +581,8 @@ export function loadConfig(flags = {}) {
483
581
  retry: resolveRetryConfig(flags, env.partial, file),
484
582
  fast: resolveFastConfig(flags, env.partial, file),
485
583
  update: resolveUpdateConfig(flags, env.partial, file),
584
+ compaction: resolveCompactionConfig(flags, env.partial, file),
585
+ bash: resolveBashConfig(env.partial, file),
486
586
  cwd,
487
587
  color,
488
588
  colorLevel,