@aragon-agent/cli 0.6.1 → 0.6.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 (127) hide show
  1. package/CHANGELOG.md +216 -0
  2. package/README.md +241 -18
  3. package/dist/agent/agent-mode.d.ts +39 -0
  4. package/dist/agent/agent-mode.js +36 -0
  5. package/dist/agent/controller.d.ts +74 -1
  6. package/dist/agent/controller.js +200 -1
  7. package/dist/agent/headless.d.ts +8 -0
  8. package/dist/agent/headless.js +47 -0
  9. package/dist/agent/reducer.d.ts +120 -0
  10. package/dist/agent/reducer.js +201 -2
  11. package/dist/cli.js +297 -6
  12. package/dist/commands/builtins.js +73 -23
  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 +113 -0
  18. package/dist/compaction/command.d.ts +41 -0
  19. package/dist/compaction/command.js +342 -0
  20. package/dist/compaction/compactor.d.ts +268 -0
  21. package/dist/compaction/compactor.js +689 -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 +179 -0
  35. package/dist/compaction/types.js +22 -0
  36. package/dist/compaction/wiring.d.ts +255 -0
  37. package/dist/compaction/wiring.js +556 -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 +4 -1
  43. package/dist/config/env.js +92 -2
  44. package/dist/config/load.d.ts +31 -0
  45. package/dist/config/load.js +63 -1
  46. package/dist/config/schema.d.ts +159 -0
  47. package/dist/config/schema.js +137 -0
  48. package/dist/config/store.js +17 -1
  49. package/dist/config/ui-state.d.ts +44 -6
  50. package/dist/config/ui-state.js +21 -12
  51. package/dist/diagnostics/doctor.js +41 -1
  52. package/dist/exec/events.d.ts +50 -1
  53. package/dist/exec/index.js +8 -4
  54. package/dist/exec/runner.js +44 -2
  55. package/dist/input/mouse-events.d.ts +31 -5
  56. package/dist/input/mouse-events.js +86 -27
  57. package/dist/input/stdin-mouse-filter.d.ts +8 -3
  58. package/dist/logging/install.d.ts +51 -1
  59. package/dist/logging/install.js +28 -0
  60. package/dist/logging/logger.d.ts +1 -1
  61. package/dist/session/cli-commands.js +23 -1
  62. package/dist/session/persist.js +19 -0
  63. package/dist/session/store.d.ts +18 -0
  64. package/dist/session/store.js +59 -2
  65. package/dist/team/report.js +7 -1
  66. package/dist/team/runtime.d.ts +10 -0
  67. package/dist/team/runtime.js +7 -0
  68. package/dist/team/subagent.d.ts +31 -1
  69. package/dist/team/subagent.js +42 -15
  70. package/dist/team/types.d.ts +8 -0
  71. package/dist/ui/ActivityLine.d.ts +19 -1
  72. package/dist/ui/ActivityLine.js +10 -2
  73. package/dist/ui/App.d.ts +48 -1
  74. package/dist/ui/App.js +317 -17
  75. package/dist/ui/Composer.d.ts +26 -1
  76. package/dist/ui/Composer.js +6 -6
  77. package/dist/ui/PromptInput.d.ts +34 -1
  78. package/dist/ui/PromptInput.js +54 -3
  79. package/dist/ui/StatusBar.d.ts +40 -0
  80. package/dist/ui/StatusBar.js +13 -4
  81. package/dist/ui/TeamPanel.js +7 -1
  82. package/dist/ui/Transcript.d.ts +21 -1
  83. package/dist/ui/Transcript.js +32 -2
  84. package/dist/ui/clipboard.d.ts +52 -0
  85. package/dist/ui/clipboard.js +91 -0
  86. package/dist/ui/entries/CompactionCard.d.ts +75 -0
  87. package/dist/ui/entries/CompactionCard.js +158 -0
  88. package/dist/ui/frame-differ.d.ts +46 -0
  89. package/dist/ui/frame-differ.js +94 -10
  90. package/dist/ui/gauge.d.ts +16 -1
  91. package/dist/ui/gauge.js +25 -6
  92. package/dist/ui/glyphs.d.ts +11 -0
  93. package/dist/ui/glyphs.js +2 -0
  94. package/dist/ui/layout/ScrollViewport.d.ts +43 -6
  95. package/dist/ui/layout/ScrollViewport.js +128 -21
  96. package/dist/ui/layout/budget.d.ts +10 -3
  97. package/dist/ui/layout/budget.js +10 -3
  98. package/dist/ui/layout/follow-state.d.ts +137 -0
  99. package/dist/ui/layout/follow-state.js +150 -0
  100. package/dist/ui/layout/virtual-window.d.ts +11 -0
  101. package/dist/ui/layout/virtual-window.js +34 -0
  102. package/dist/ui/overlays/HelpOverlay.js +18 -2
  103. package/dist/ui/overlays/SettingsScreen.d.ts +47 -1
  104. package/dist/ui/overlays/SettingsScreen.js +60 -1
  105. package/dist/ui/palettes.d.ts +16 -0
  106. package/dist/ui/palettes.js +6 -0
  107. package/dist/ui/screen.d.ts +22 -0
  108. package/dist/ui/screen.js +70 -8
  109. package/dist/ui/selection/highlight.d.ts +39 -0
  110. package/dist/ui/selection/highlight.js +64 -0
  111. package/dist/ui/selection/screen-mirror.d.ts +24 -0
  112. package/dist/ui/selection/screen-mirror.js +35 -0
  113. package/dist/ui/selection/selection-controller.d.ts +88 -0
  114. package/dist/ui/selection/selection-controller.js +282 -0
  115. package/dist/ui/selection/selection.d.ts +90 -0
  116. package/dist/ui/selection/selection.js +223 -0
  117. package/dist/ui/stdout-frame-writer.d.ts +33 -0
  118. package/dist/ui/stdout-frame-writer.js +15 -0
  119. package/dist/ui/theme.d.ts +11 -0
  120. package/dist/ui/theme.js +2 -0
  121. package/dist/ui/transcript-text.js +23 -0
  122. package/dist/ui/use-startup-notices.d.ts +62 -5
  123. package/dist/ui/use-startup-notices.js +81 -17
  124. package/dist/ui/use-wheel-routing.js +8 -0
  125. package/dist/ui/win-vt-force.d.ts +136 -0
  126. package/dist/ui/win-vt-force.js +237 -0
  127. package/package.json +4 -2
@@ -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
@@ -14,7 +14,10 @@
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
@@ -14,7 +14,10 @@
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,29 @@ 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_SCROLL_RESUME_MS=<n> — idle delay before a paused viewport returns to
145
+ // the newest line; `0` disables auto-resume. THE ONLY CHANNEL BESIDES THE
146
+ // CONFIG FILE, because §6.1 gives this key no CLI flag on purpose. `load.ts`
147
+ // clamps it to [0, 120000]; a non-numeric value is LEFT ABSENT rather than
148
+ // written, so a typo falls through to the config file underneath instead of
149
+ // silently replacing it (the rule `ARAGON_MAX_TOKENS` above already follows).
150
+ const scrollResume = process.env.ARAGON_SCROLL_RESUME_MS?.trim();
151
+ if (scrollResume !== undefined && scrollResume.length > 0) {
152
+ const n = Number.parseInt(scrollResume, 10);
153
+ if (Number.isFinite(n) && n >= 0)
154
+ partial.scrollResumeMs = n;
155
+ }
130
156
  // ARAGON_PLAN=1 starts the session in PLAN mode — the same channel story as
131
157
  // ARAGON_FULLSCREEN, for terminals reached through a wrapper that owns argv.
132
158
  const plan = process.env.ARAGON_PLAN?.trim().toLowerCase();
@@ -289,6 +315,70 @@ export function readEnvConfig() {
289
315
  if (Object.keys(section).length > 0)
290
316
  partial.fast = section;
291
317
  }
318
+ // ARAGON_COMPACTION=0 turns context compaction off; the other two tune it
319
+ // (context-auto-compaction §4.3). Parsed by the POSITIVE list, exactly as
320
+ // ARAGON_FULLSCREEN / ARAGON_MOUSE / ARAGON_PLAN / ARAGON_TEAM / ARAGON_TODO /
321
+ // ARAGON_RETRY / ARAGON_FAST are — NOT by `envBool` below, whose NEGATIVE list
322
+ // disagrees with it on values like `ARAGON_COMPACTION=disable`.
323
+ //
324
+ // THREE VARS, AND DELIBERATELY NO `ARAGON_COMPACTION_ON_FAILURE` /
325
+ // `_USE_FAST_TIER` / `_WARN_THRESHOLD`. The rule this file states is that an
326
+ // environment variable exists only where a flag AND a config file are BOTH
327
+ // unreachable — a container, an ssh session, being spawned by another tool.
328
+ // "Is it on, when does it fire, and how much does it keep" passes that test;
329
+ // "what should it do if the summarizer 500s" does not, and every key that fails
330
+ // it is documentation debt plus a fifth place for the value to disagree with
331
+ // itself.
332
+ //
333
+ // ONE ACCUMULATED SECTION, ASSIGNED ONCE — the shape the four branches above
334
+ // all use, and NOT three `if` blocks each assigning `partial.compaction`. THE
335
+ // CAST IS WHAT WOULD MAKE THAT MISTAKE SILENT: a second assignment compiles
336
+ // clean and drops `enabled`.
337
+ const compaction = process.env.ARAGON_COMPACTION?.trim().toLowerCase();
338
+ const compactionThreshold = process.env.ARAGON_COMPACTION_THRESHOLD?.trim();
339
+ const compactionKeepTurns = process.env.ARAGON_COMPACTION_KEEP_TURNS?.trim();
340
+ // The two hardening kill switches (context-auto-compaction-hardening §4.3).
341
+ // They pass the same test the three above do: both name a behaviour that a
342
+ // container or an automated harness may need to switch off with no flag and no
343
+ // config file in reach - sub-agent compaction because it changes what a
344
+ // dispatch costs, and the archive because it writes conversation content to
345
+ // disk in an environment that may not permit it.
346
+ const compactionSubagents = process.env.ARAGON_COMPACTION_SUBAGENTS?.trim().toLowerCase();
347
+ const compactionArchive = process.env.ARAGON_COMPACTION_ARCHIVE?.trim().toLowerCase();
348
+ const positive = (v) => v === '1' || v === 'true' || v === 'on' || v === 'yes';
349
+ if ((compaction !== undefined && compaction.length > 0) ||
350
+ (compactionThreshold !== undefined && compactionThreshold.length > 0) ||
351
+ (compactionKeepTurns !== undefined && compactionKeepTurns.length > 0) ||
352
+ (compactionSubagents !== undefined && compactionSubagents.length > 0) ||
353
+ (compactionArchive !== undefined && compactionArchive.length > 0)) {
354
+ const section = {};
355
+ if (compaction !== undefined && compaction.length > 0) {
356
+ section.enabled = positive(compaction);
357
+ }
358
+ if (compactionSubagents !== undefined && compactionSubagents.length > 0) {
359
+ section.subagents = positive(compactionSubagents);
360
+ }
361
+ if (compactionArchive !== undefined && compactionArchive.length > 0) {
362
+ section.archive = positive(compactionArchive);
363
+ }
364
+ if (compactionThreshold !== undefined && compactionThreshold.length > 0) {
365
+ // Left ABSENT on a bad value rather than clamped to a default, so a typo
366
+ // falls through to the config layer instead of masking it for the session.
367
+ const parsed = parseThresholdInput(compactionThreshold);
368
+ if (parsed !== null)
369
+ section.threshold = parsed;
370
+ }
371
+ if (compactionKeepTurns !== undefined && compactionKeepTurns.length > 0) {
372
+ const n = Number.parseInt(compactionKeepTurns, 10);
373
+ if (Number.isFinite(n) && n > 0)
374
+ section.keepRecentTurns = n;
375
+ }
376
+ // A PARTIAL section, safe only because every consumer deep-merges it — the
377
+ // same contract the five sections above rely on.
378
+ if (Object.keys(section).length > 0) {
379
+ partial.compaction = section;
380
+ }
381
+ }
292
382
  // ARAGON_UPDATE=0|off|false turns auto-update off; `notify` reports without
293
383
  // installing; 1|on|true|yes turns it on (cli-auto-update §4.2).
294
384
  //
@@ -36,6 +36,19 @@ 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;
39
52
  /** `--no-skills` ⇒ false. Turns the whole skill subsystem off (§7.5). */
40
53
  skills?: boolean;
41
54
  /** `--skill <name>` ×N — force Level 2 injection for this run. */
@@ -178,6 +191,24 @@ export interface CliFlags {
178
191
  * config file and `ARAGON_UPDATE=notify`.
179
192
  */
180
193
  update?: boolean;
194
+ /**
195
+ * `--compaction` ⇒ true, `--no-compaction` ⇒ false, absent ⇒ undefined
196
+ * (context-auto-compaction §4.3).
197
+ *
198
+ * DECLARED AS A PAIR — MANDATORY (C-13 / P1-5), for the reason `--team`,
199
+ * `--todo-panel`, `--render-governor`, `--retry`, `--fast` and `--update` each
200
+ * record in turn, and this one shares the WORST polarity with `--update`:
201
+ * `compaction.enabled` is PERSISTED and defaults to `true`, so a lone
202
+ * `--no-compaction` would make commander default `opts.compaction` to `true`,
203
+ * at which point the flag is indistinguishable from its own default and
204
+ * silently overwrites a stored `false` on EVERY run that passed no flag at all.
205
+ *
206
+ * That would make R-12's whole mitigation ("one config key restores the old
207
+ * behaviour exactly") FALSE — for a feature that spends the user's money.
208
+ */
209
+ compaction?: boolean;
210
+ /** `--compaction-threshold <0..1|N%>` — session override for `threshold`. */
211
+ compactionThreshold?: string;
181
212
  }
182
213
  /**
183
214
  * 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, 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,38 @@ 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
+ * Idle scroll-resume delay: env › file › default 5000, clamped to [0, 120000].
85
+ *
86
+ * NO FLAG LAYER, and the absence is the design rather than an omission (§6.1):
87
+ * this key has no `--scroll-resume-ms`, so `CliFlags` carries no field for one
88
+ * and a resolver that read it would be reading a value nothing can ever set.
89
+ */
90
+ function resolveScrollResumeMs(env, file) {
91
+ const raw = env.scrollResumeMs ?? file.scrollResumeMs;
92
+ return clampScrollResumeMs(raw, DEFAULT_CONFIG.scrollResumeMs);
93
+ }
62
94
  /**
63
95
  * Resolve whether logs reach a file: flag › env › an explicit `false` in the
64
96
  * file › default `true`.
@@ -208,6 +240,33 @@ function resolveUpdateConfig(flags, env, file) {
208
240
  ...(flags.update !== undefined ? { mode: flags.update ? 'auto' : 'off' } : {}),
209
241
  });
210
242
  }
243
+ /**
244
+ * Merge the `compaction` section across all four layers (defaults › file › env ›
245
+ * flags), context-auto-compaction §4.3.
246
+ *
247
+ * `!== undefined` ON `flags.compaction`, WHICH IS THE WHOLE POINT (C-13 / P1-5).
248
+ * The rule `resolveTeamConfig`, `resolveTodoConfig`, `resolveRetryConfig`,
249
+ * `resolveFastConfig` and `resolveUpdateConfig` each state once: commander
250
+ * materialises a lone `--no-compaction` as `opts.compaction = true` when the flag
251
+ * is absent, so a truthiness check cannot tell `--no-compaction` from "not
252
+ * passed" — and here the damage runs in the worst direction, silently re-enabling
253
+ * a subsystem the user turned off, on every flagless run, for a feature that
254
+ * spends money.
255
+ *
256
+ * `--compaction-threshold` is PARSED rather than spread, because `90%` and `0.9`
257
+ * are both legitimate spellings and a typo must change NOTHING rather than
258
+ * resolve to a default (`parseThresholdInput` returns `null`).
259
+ */
260
+ function resolveCompactionConfig(flags, env, file) {
261
+ const thresholdFlag = flags.compactionThreshold !== undefined ? parseThresholdInput(flags.compactionThreshold) : null;
262
+ return clampCompactionConfig({
263
+ ...DEFAULT_CONFIG.compaction,
264
+ ...(file.compaction ?? {}),
265
+ ...(env.compaction ?? {}),
266
+ ...(flags.compaction !== undefined ? { enabled: flags.compaction } : {}),
267
+ ...(thresholdFlag !== null ? { threshold: thresholdFlag } : {}),
268
+ });
269
+ }
211
270
  /**
212
271
  * Resolve the adaptive render governor: flag › env › file › default `true`
213
272
  * (tui-render-performance L4).
@@ -460,6 +519,8 @@ export function loadConfig(flags = {}) {
460
519
  density,
461
520
  hints,
462
521
  mouse: resolveMouse(flags, env.partial, file),
522
+ mouseSelect: resolveMouseSelect(flags, env.partial, file),
523
+ scrollResumeMs: resolveScrollResumeMs(env.partial, file),
463
524
  // From `<home>/state.json`, not the config file — see `ui-state.ts`.
464
525
  submitCount: readSubmitCount(),
465
526
  startInPlanMode,
@@ -483,6 +544,7 @@ export function loadConfig(flags = {}) {
483
544
  retry: resolveRetryConfig(flags, env.partial, file),
484
545
  fast: resolveFastConfig(flags, env.partial, file),
485
546
  update: resolveUpdateConfig(flags, env.partial, file),
547
+ compaction: resolveCompactionConfig(flags, env.partial, file),
486
548
  cwd,
487
549
  color,
488
550
  colorLevel,
@@ -115,6 +115,36 @@ export declare const DEFAULT_DIFF_RENDER = true;
115
115
  * because the frame differ already removes the flicker on its own.
116
116
  */
117
117
  export declare const DEFAULT_SYNC_OUTPUT = true;
118
+ /**
119
+ * Drag-select + copy, and — because §4.4.1 gates `\x1b[?1002h` on exactly this
120
+ * value — whether motion reporting is requested from the terminal at all.
121
+ *
122
+ * That gating is what makes `false` BYTE-IDENTICAL to a pre-feature session
123
+ * rather than merely equivalent (AC-8 / D-15). Ignored when `mouse` is false,
124
+ * since there is no capture to select with.
125
+ */
126
+ export declare const DEFAULT_MOUSE_SELECT = true;
127
+ /**
128
+ * Idle delay before a paused viewport returns to the newest line, in ms.
129
+ *
130
+ * Five seconds is chosen to be LONGER than a glance and SHORTER than a read.
131
+ * `0` disables auto-resume entirely, and it is a real value rather than a
132
+ * sentinel-shaped default: `shouldArmResume` refuses on it before any timer is
133
+ * created (AC-9), so the revert for Rule B is one config key.
134
+ *
135
+ * NO CLI FLAG (§6.1). It is a comfort preference, set once
136
+ * (`aragon config set scrollResumeMs 8000`) and then forgotten — the same
137
+ * argument this file already makes for `historyEnabled`. Adding a flag for every
138
+ * scalar is how a CLI ends up with sixty of them.
139
+ */
140
+ export declare const DEFAULT_SCROLL_RESUME_MS = 5000;
141
+ /**
142
+ * `clampIntAllowingZero`, and the choice is load-bearing: `clampInt` delegates to
143
+ * `coercePositiveInt`, which returns the FALLBACK for `n <= 0` — so `0` would
144
+ * silently become 5000 and the documented off switch would not be an off switch.
145
+ * Exactly the trap `retry.maxRetries` needed this helper for.
146
+ */
147
+ export declare function clampScrollResumeMs(v: unknown, fallback: number): number;
118
148
  /** `ask_user` calls allowed per user turn. */
119
149
  export declare const DEFAULT_PLAN_MAX_ASK_ROUNDS = 4;
120
150
  /**
@@ -489,6 +519,94 @@ export declare const DEFAULT_UPDATE_CONFIG: UpdateConfig;
489
519
  * cannot see it reading.
490
520
  */
491
521
  export declare function clampUpdateConfig(raw: unknown): UpdateConfig;
522
+ export type CompactionFailureMode = 'truncate' | 'stop';
523
+ export interface CompactionConfig {
524
+ /**
525
+ * Whether a `ContextManager` is constructed at all. Default TRUE.
526
+ *
527
+ * THE ONLY KEY IN THIS SECTION THAT DEFAULTS ON, AND THE ONLY ONE READ ONCE AT
528
+ * CONSTRUCTION — the `team.enabled` / `todo.enabled` / `fast.enabled` rule.
529
+ *
530
+ * IT DEFAULTS TO `true` UNLIKE EVERY OTHER OPTIONAL SUBSYSTEM HERE, and that is
531
+ * deliberate (D-16): `fast.enabled` and `team.enabled` default off because they
532
+ * ADD behaviour the user did not ask for. This one REMOVES a failure the user
533
+ * did not ask for — without it, the answer to a full context window is a dead
534
+ * run. Byte-identity is still honoured: `--no-compaction` constructs no
535
+ * manager, subscribes nothing, allocates no registry, and leaves the engine
536
+ * loop with `ctx.contextManager === undefined`.
537
+ */
538
+ enabled: boolean;
539
+ /** Occupancy ratio that triggers compaction. Read LIVE. */
540
+ threshold: number;
541
+ /**
542
+ * Where the status-bar gauge turns amber.
543
+ *
544
+ * CLAMPED JOINTLY WITH `threshold`, AND THE ORDER IS FIXED — see
545
+ * `clampCompactionConfig`. "Jointly" is not an instruction.
546
+ */
547
+ warnThreshold: number;
548
+ /** Complete turns kept verbatim in the tail. Read LIVE. */
549
+ keepRecentTurns: number;
550
+ /**
551
+ * Summarize with the fast tier when it resolves. Default TRUE.
552
+ *
553
+ * A PREFERENCE, NOT THE COMMON PATH (P2-8). `resolveFastTier` returns
554
+ * `{ok:false, reason:'disabled'}` on `fast.enabled !== true` alone, and
555
+ * `fast.enabled` defaults to `false` — so out of the box the summarizer is the
556
+ * SESSION'S OWN MODEL, and the first compaction is a ~30 k-token call on it.
557
+ */
558
+ useFastTier: boolean;
559
+ /** What happens when summarization fails outright. See §3.7 rungs 3 / 4. */
560
+ onFailure: CompactionFailureMode;
561
+ /**
562
+ * Give `task` children their own compaction
563
+ * (context-auto-compaction-hardening §3.4 / W3). Default TRUE.
564
+ *
565
+ * IT REVERSES D-15, WHOSE PREMISE IS MEASURABLY FALSE at the configured bounds:
566
+ * `team.maxTurnsPerSubagent` is 24 and `team.dispatchTimeoutMs` is 900 000, and
567
+ * 24 turns over fifteen minutes is the same profile as the lead run this
568
+ * feature exists for. When a child overflows today its history is discarded and
569
+ * the lead receives one partial sentence for the whole dispatch.
570
+ *
571
+ * `false` REPRODUCES ROUND 1 EXACTLY: `subagent.ts` spreads no `contextManager`
572
+ * key at all, so the child's loop gate tests an absent field.
573
+ */
574
+ subagents: boolean;
575
+ /**
576
+ * Write the dropped messages to `<home>/compaction/` before adopting a splice
577
+ * (context-auto-compaction-hardening §3.5 / W4). Default TRUE.
578
+ *
579
+ * NOT UNDO (§12): restoring an over-full history restores the condition that
580
+ * triggered the compaction. This is fidelity on disk, so "the summary is wrong
581
+ * and the agent proceeded on a false record" becomes something a human can
582
+ * check rather than something they must accept.
583
+ */
584
+ archive: boolean;
585
+ }
586
+ export declare const DEFAULT_COMPACTION_CONFIG: CompactionConfig;
587
+ /**
588
+ * THE single gate for every compaction-config read AND write, mirroring the six
589
+ * clamps above for the reason all of them record: hardening only the read path
590
+ * leaves a bad value on disk that reverts to the default on every launch, which
591
+ * presents to the user as "my setting won't stick".
592
+ *
593
+ * THE ORDER IS FIXED AND IT MATTERS (§4.2 / P2-5). `threshold` is clamped to
594
+ * `[0.5, 0.95]` FIRST, then `warnThreshold` to
595
+ * `[0.4, clampedThreshold - 0.05]`. The other order lets a file holding
596
+ * `{threshold: 0.6, warnThreshold: 0.9}` produce a warn mark ABOVE the trigger —
597
+ * a gauge that turns amber after it has already gone red. Both bounds of the
598
+ * second range stay reachable at the extremes: `threshold` floored at `0.5`
599
+ * gives `warnThreshold` a range of `[0.4, 0.45]`.
600
+ */
601
+ export declare function clampCompactionConfig(raw: unknown): CompactionConfig;
602
+ /**
603
+ * Parse `0.9`, `90%` or `90` into a ratio, or `null` when it is neither.
604
+ *
605
+ * `null` RATHER THAN A CLAMPED DEFAULT, because this is a COMMAND argument and
606
+ * not a config value: `/compact threshold banana` must say so rather than
607
+ * silently setting 0.9 and reporting success.
608
+ */
609
+ export declare function parseThresholdInput(raw: string): number | null;
492
610
  export interface PersistedConfig {
493
611
  version: number;
494
612
  provider: string;
@@ -578,6 +696,14 @@ export interface PersistedConfig {
578
696
  * heuristics for an explicit `true` to override (D-10).
579
697
  */
580
698
  mouse: boolean;
699
+ /**
700
+ * Drag-select + copy in full-screen mode (tui-selection-and-scroll-follow
701
+ * §6.1). Ignored when `mouse` is false; also decides whether `?1002h` is
702
+ * written at all, so `false` is byte-identical to a pre-feature session.
703
+ */
704
+ mouseSelect: boolean;
705
+ /** Idle ms before a paused viewport returns to the newest line; `0` disables. */
706
+ scrollResumeMs: number;
581
707
  /** Start every session in PLAN mode (plan-mode §4.4). */
582
708
  planModeDefault: boolean;
583
709
  /** `ask_user` rounds allowed per user turn; clamped to [1, 10]. */
@@ -598,6 +724,8 @@ export interface PersistedConfig {
598
724
  fast: FastConfig;
599
725
  /** Auto-update. The EIGHTH nested object; scalars only (see UpdateConfig). */
600
726
  update: UpdateConfig;
727
+ /** Context compaction. The NINTH nested object; scalars only (see CompactionConfig). */
728
+ compaction: CompactionConfig;
601
729
  }
602
730
  export declare const DEFAULT_CONFIG: PersistedConfig;
603
731
  /**
@@ -727,6 +855,19 @@ export interface CliConfig {
727
855
  * only). Resolved by `resolveMouse` in `load.ts`.
728
856
  */
729
857
  mouse: boolean;
858
+ /**
859
+ * Whether this session may drag-select and copy.
860
+ *
861
+ * IT MUST EXIST HERE AND NOT ONLY IN `PersistedConfig`, for the reason
862
+ * `diffRender` above records at length: `runInteractive` reads
863
+ * `config.mouseSelect` — where `config` is a `CliConfig` — to decide the
864
+ * `?1002h` write, the controller and the `decorate` hook. A key added to the
865
+ * persisted shape alone would round-trip through `config set`, appear in
866
+ * `config list`, and never reach a single one of them.
867
+ */
868
+ mouseSelect: boolean;
869
+ /** Idle ms before a paused viewport returns to the newest line; `0` disables. */
870
+ scrollResumeMs: number;
730
871
  /**
731
872
  * Whether this session opens in PLAN mode.
732
873
  *
@@ -800,6 +941,24 @@ export interface CliConfig {
800
941
  * and be discarded, with no compile error and no runtime error.
801
942
  */
802
943
  update: UpdateConfig;
944
+ /**
945
+ * Resolved context-compaction settings (context-auto-compaction §4.2).
946
+ *
947
+ * `enabled` here decides whether `AgentController` CONSTRUCTS a
948
+ * `CompactionWiring` at all, which is what makes `false` byte-identical to a
949
+ * pre-feature build: no manager is passed to `new Agent({...})`, nothing is
950
+ * subscribed, no registry is allocated, and the loop's checkpoint is one `if`
951
+ * against an absent field. The other five keys are read LIVE on every use, so
952
+ * `/compact threshold 0.8` takes effect in the running session.
953
+ *
954
+ * IT MUST EXIST HERE AND NOT ONLY IN `PersistedConfig`, for the reason
955
+ * `showThinking`, `liveToolOutput`, `diffRender`, `mouseSelect` and `update`
956
+ * each record above: the controller and the compactor read `config.compaction`,
957
+ * where `config` is a `CliConfig`, so a key added to the persisted shape alone
958
+ * would round-trip through `config set`, appear in `config list` — and never
959
+ * reach either of them.
960
+ */
961
+ compaction: CompactionConfig;
803
962
  /**
804
963
  * Lifetime submit count driving the composer hint fade (`HINT_FADE_AFTER`).
805
964
  *