@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
@@ -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,136 @@ 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
+ export interface BashConfig {
603
+ /**
604
+ * Register `bash_output` / `bash_kill`, honour `background`, splice the prompt
605
+ * block, render the chip. Default TRUE.
606
+ *
607
+ * READ ONCE AT CONSTRUCTION, the `team.enabled` / `todo.enabled` /
608
+ * `fast.enabled` rule.
609
+ *
610
+ * IT GOVERNS *SERVICES*, NEVER *INTERRUPTIBILITY* (I-2 / D-9). `Esc`/`Esc`,
611
+ * the foreground child registry and `forceStop()` are unconditional, because
612
+ * "bash always settles" and "Esc twice always works" are promised
613
+ * unconditionally — a user who turned background services off has not asked
614
+ * for a less interruptible agent.
615
+ */
616
+ background: boolean;
617
+ /**
618
+ * Let the classifier decide when the model did not say. Default TRUE.
619
+ *
620
+ * RESOLVES TO `false` WITHOUT AN INTERACTIVE VIEW (D-15 / P1-8), and that is
621
+ * decided at the tool layer rather than here: the value on disk is the user's
622
+ * preference, and headless has no card, no `/bg` and no user to keep a service
623
+ * alive, so the rescue would be a surprise instead. An explicit
624
+ * `background: true` is still honoured there — that one the model asked for.
625
+ */
626
+ autoBackground: boolean;
627
+ /** How long a background launch may block the turn before returning. */
628
+ startupSettleMs: number;
629
+ /** How long a service may stay `starting` before it is called `running`. */
630
+ readyTimeoutMs: number;
631
+ }
632
+ export declare const DEFAULT_BASH_CONFIG: BashConfig;
633
+ /**
634
+ * THE single gate for every bash-config read AND write, mirroring
635
+ * `clampSkillsConfig` / `clampLogConfig` / `clampTeamConfig` / `clampTodoConfig`
636
+ * for the reason those record: hardening only the read path leaves a bad value
637
+ * on disk that reverts to the default on every launch, which presents to the
638
+ * user as "my setting won't stick".
639
+ *
640
+ * CLAMPED, NEVER REJECTED — the discipline every other key in this file follows.
641
+ * A garbage `startupSettleMs` resolves to 4000 and throws nothing.
642
+ */
643
+ export declare function clampBashConfig(raw: unknown): BashConfig;
644
+ /**
645
+ * Parse `0.9`, `90%` or `90` into a ratio, or `null` when it is neither.
646
+ *
647
+ * `null` RATHER THAN A CLAMPED DEFAULT, because this is a COMMAND argument and
648
+ * not a config value: `/compact threshold banana` must say so rather than
649
+ * silently setting 0.9 and reporting success.
650
+ */
651
+ export declare function parseThresholdInput(raw: string): number | null;
492
652
  export interface PersistedConfig {
493
653
  version: number;
494
654
  provider: string;
@@ -578,6 +738,24 @@ export interface PersistedConfig {
578
738
  * heuristics for an explicit `true` to override (D-10).
579
739
  */
580
740
  mouse: boolean;
741
+ /**
742
+ * Drag-select + copy in full-screen mode (tui-selection-and-scroll-follow
743
+ * §6.1). Ignored when `mouse` is false; also decides whether `?1002h` is
744
+ * written at all, so `false` is byte-identical to a pre-feature session.
745
+ */
746
+ mouseSelect: boolean;
747
+ /**
748
+ * Recognise pasted text and collapse a large paste into a placeholder
749
+ * (tui-paste-handling D-10 / section 7.3).
750
+ *
751
+ * A PLAIN BOOLEAN and the ONLY user-facing key this feature adds: the collapse
752
+ * threshold and every size bound are structural and live in `input/limits.ts`
753
+ * / `ui/composer-limits.ts`. A tunable threshold is a preference nobody has
754
+ * asked for; a kill switch is what a bug report needs.
755
+ */
756
+ paste: boolean;
757
+ /** Idle ms before a paused viewport returns to the newest line; `0` disables. */
758
+ scrollResumeMs: number;
581
759
  /** Start every session in PLAN mode (plan-mode §4.4). */
582
760
  planModeDefault: boolean;
583
761
  /** `ask_user` rounds allowed per user turn; clamped to [1, 10]. */
@@ -598,6 +776,10 @@ export interface PersistedConfig {
598
776
  fast: FastConfig;
599
777
  /** Auto-update. The EIGHTH nested object; scalars only (see UpdateConfig). */
600
778
  update: UpdateConfig;
779
+ /** Context compaction. The NINTH nested object; scalars only (see CompactionConfig). */
780
+ compaction: CompactionConfig;
781
+ /** Background services. The TENTH nested object; scalars only (see BashConfig). */
782
+ bash: BashConfig;
601
783
  }
602
784
  export declare const DEFAULT_CONFIG: PersistedConfig;
603
785
  /**
@@ -727,6 +909,30 @@ export interface CliConfig {
727
909
  * only). Resolved by `resolveMouse` in `load.ts`.
728
910
  */
729
911
  mouse: boolean;
912
+ /**
913
+ * Whether this session may drag-select and copy.
914
+ *
915
+ * IT MUST EXIST HERE AND NOT ONLY IN `PersistedConfig`, for the reason
916
+ * `diffRender` above records at length: `runInteractive` reads
917
+ * `config.mouseSelect` — where `config` is a `CliConfig` — to decide the
918
+ * `?1002h` write, the controller and the `decorate` hook. A key added to the
919
+ * persisted shape alone would round-trip through `config set`, appear in
920
+ * `config list`, and never reach a single one of them.
921
+ */
922
+ mouseSelect: boolean;
923
+ /**
924
+ * Whether this session recognises pastes at all.
925
+ *
926
+ * IT MUST EXIST HERE AND NOT ONLY IN `PersistedConfig`, for the reason
927
+ * `mouseSelect` above records: `runInteractive` reads `config.paste` -- where
928
+ * `config` is a `CliConfig` -- to decide the filter's `paste` feature, the
929
+ * `?2004h` write and the `PasteBridge`. A key added to the persisted shape
930
+ * alone would round-trip through `config set`, appear in `config list`, and
931
+ * reach none of them.
932
+ */
933
+ paste: boolean;
934
+ /** Idle ms before a paused viewport returns to the newest line; `0` disables. */
935
+ scrollResumeMs: number;
730
936
  /**
731
937
  * Whether this session opens in PLAN mode.
732
938
  *
@@ -800,6 +1006,44 @@ export interface CliConfig {
800
1006
  * and be discarded, with no compile error and no runtime error.
801
1007
  */
802
1008
  update: UpdateConfig;
1009
+ /**
1010
+ * Resolved context-compaction settings (context-auto-compaction §4.2).
1011
+ *
1012
+ * `enabled` here decides whether `AgentController` CONSTRUCTS a
1013
+ * `CompactionWiring` at all, which is what makes `false` byte-identical to a
1014
+ * pre-feature build: no manager is passed to `new Agent({...})`, nothing is
1015
+ * subscribed, no registry is allocated, and the loop's checkpoint is one `if`
1016
+ * against an absent field. The other five keys are read LIVE on every use, so
1017
+ * `/compact threshold 0.8` takes effect in the running session.
1018
+ *
1019
+ * IT MUST EXIST HERE AND NOT ONLY IN `PersistedConfig`, for the reason
1020
+ * `showThinking`, `liveToolOutput`, `diffRender`, `mouseSelect` and `update`
1021
+ * each record above: the controller and the compactor read `config.compaction`,
1022
+ * where `config` is a `CliConfig`, so a key added to the persisted shape alone
1023
+ * would round-trip through `config set`, appear in `config list` — and never
1024
+ * reach either of them.
1025
+ */
1026
+ compaction: CompactionConfig;
1027
+ /**
1028
+ * Resolved background-service settings (background-service-supervision §5.4).
1029
+ *
1030
+ * `background` here decides whether `bash`'s schema carries a `background`
1031
+ * property, whether `bash_output` / `bash_kill` are REGISTERED and whether
1032
+ * `<background_services>` is spliced — all three at construction, and none of
1033
+ * them undoable, the `team.enabled` / `todo.enabled` / `fast.enabled` rule.
1034
+ * The other three keys are read LIVE.
1035
+ *
1036
+ * IT DOES NOT GATE THE INTERRUPT LADDER (I-2 / D-9). `ProcSupervisor` is
1037
+ * constructed unconditionally, because `Esc`/`Esc` and the foreground registry
1038
+ * are what make G2 and G3 true and those are promised whatever this says.
1039
+ *
1040
+ * IT MUST EXIST HERE AND NOT ONLY IN `PersistedConfig`, for the reason every
1041
+ * section above records: the controller and `bash` read `config.bash`, where
1042
+ * `config` is a `CliConfig`, so a key added to the persisted shape alone would
1043
+ * round-trip through `config set`, appear in `config list` — and never reach
1044
+ * either of them.
1045
+ */
1046
+ bash: BashConfig;
803
1047
  /**
804
1048
  * Lifetime submit count driving the composer hint fade (`HINT_FADE_AFTER`).
805
1049
  *
@@ -169,6 +169,42 @@ export const DEFAULT_DIFF_RENDER = true;
169
169
  */
170
170
  export const DEFAULT_SYNC_OUTPUT = true;
171
171
  // ---------------------------------------------------------------------------
172
+ // Selection + scroll follow (tui-selection-and-scroll-follow §6.1)
173
+ // ---------------------------------------------------------------------------
174
+ /**
175
+ * Drag-select + copy, and — because §4.4.1 gates `\x1b[?1002h` on exactly this
176
+ * value — whether motion reporting is requested from the terminal at all.
177
+ *
178
+ * That gating is what makes `false` BYTE-IDENTICAL to a pre-feature session
179
+ * rather than merely equivalent (AC-8 / D-15). Ignored when `mouse` is false,
180
+ * since there is no capture to select with.
181
+ */
182
+ export const DEFAULT_MOUSE_SELECT = true;
183
+ /**
184
+ * Idle delay before a paused viewport returns to the newest line, in ms.
185
+ *
186
+ * Five seconds is chosen to be LONGER than a glance and SHORTER than a read.
187
+ * `0` disables auto-resume entirely, and it is a real value rather than a
188
+ * sentinel-shaped default: `shouldArmResume` refuses on it before any timer is
189
+ * created (AC-9), so the revert for Rule B is one config key.
190
+ *
191
+ * NO CLI FLAG (§6.1). It is a comfort preference, set once
192
+ * (`aragon config set scrollResumeMs 8000`) and then forgotten — the same
193
+ * argument this file already makes for `historyEnabled`. Adding a flag for every
194
+ * scalar is how a CLI ends up with sixty of them.
195
+ */
196
+ export const DEFAULT_SCROLL_RESUME_MS = 5000;
197
+ const SCROLL_RESUME_RANGE = { min: 0, max: 120_000 };
198
+ /**
199
+ * `clampIntAllowingZero`, and the choice is load-bearing: `clampInt` delegates to
200
+ * `coercePositiveInt`, which returns the FALLBACK for `n <= 0` — so `0` would
201
+ * silently become 5000 and the documented off switch would not be an off switch.
202
+ * Exactly the trap `retry.maxRetries` needed this helper for.
203
+ */
204
+ export function clampScrollResumeMs(v, fallback) {
205
+ return clampIntAllowingZero(v, fallback, SCROLL_RESUME_RANGE);
206
+ }
207
+ // ---------------------------------------------------------------------------
172
208
  // Plan mode (plan-mode §4.4)
173
209
  //
174
210
  // THREE FLAT KEYS, not a nested `planMode` object. `store.ts` documents that its
@@ -669,6 +705,126 @@ function isRegistryUrl(raw) {
669
705
  return false;
670
706
  }
671
707
  }
708
+ export const DEFAULT_COMPACTION_CONFIG = {
709
+ enabled: true,
710
+ threshold: 0.9,
711
+ warnThreshold: 0.75,
712
+ keepRecentTurns: 4,
713
+ useFastTier: true,
714
+ onFailure: 'truncate',
715
+ subagents: true,
716
+ archive: true,
717
+ };
718
+ const THRESHOLD_RANGE = { min: 0.5, max: 0.95 };
719
+ const WARN_THRESHOLD_FLOOR = 0.4;
720
+ /** The gap the warn mark must stay below the trigger by. */
721
+ const WARN_THRESHOLD_GAP = 0.05;
722
+ const KEEP_RECENT_TURNS_RANGE = { min: 1, max: 20 };
723
+ const COMPACTION_FAILURE_MODES = ['truncate', 'stop'];
724
+ /**
725
+ * Round a ratio to two decimals — the precision a percentage actually has.
726
+ *
727
+ * BINARY FLOATING POINT MAKES `0.95 - 0.05` EQUAL `0.8999999999999999`, and both
728
+ * numbers here are produced by subtraction (`threshold - WARN_THRESHOLD_GAP`) and
729
+ * by division (`parseThresholdInput` turning `90` into `90/100`). Without this,
730
+ * `aragon config list` shows a seventeen-digit value for a setting the user typed
731
+ * as `90%`, and `config.json` records one — which looks like corruption and
732
+ * invites a hand-edit that the clamp then has to defend against.
733
+ *
734
+ * Two decimals is exact for every value the range can hold: the clamp is
735
+ * `[0.5, 0.95]` and the UI reads it as a whole percentage.
736
+ */
737
+ function ratio(value) {
738
+ return Math.round(value * 100) / 100;
739
+ }
740
+ function failureMode(value, fallback) {
741
+ return typeof value === 'string' && COMPACTION_FAILURE_MODES.includes(value)
742
+ ? value
743
+ : fallback;
744
+ }
745
+ /**
746
+ * THE single gate for every compaction-config read AND write, mirroring the six
747
+ * clamps above for the reason all of them record: hardening only the read path
748
+ * leaves a bad value on disk that reverts to the default on every launch, which
749
+ * presents to the user as "my setting won't stick".
750
+ *
751
+ * THE ORDER IS FIXED AND IT MATTERS (§4.2 / P2-5). `threshold` is clamped to
752
+ * `[0.5, 0.95]` FIRST, then `warnThreshold` to
753
+ * `[0.4, clampedThreshold - 0.05]`. The other order lets a file holding
754
+ * `{threshold: 0.6, warnThreshold: 0.9}` produce a warn mark ABOVE the trigger —
755
+ * a gauge that turns amber after it has already gone red. Both bounds of the
756
+ * second range stay reachable at the extremes: `threshold` floored at `0.5`
757
+ * gives `warnThreshold` a range of `[0.4, 0.45]`.
758
+ */
759
+ export function clampCompactionConfig(raw) {
760
+ const src = (raw && typeof raw === 'object' ? raw : {});
761
+ const threshold = ratio(clampNumber(src.threshold, DEFAULT_COMPACTION_CONFIG.threshold, THRESHOLD_RANGE));
762
+ const warnCeiling = ratio(threshold - WARN_THRESHOLD_GAP);
763
+ return {
764
+ enabled: bool(src.enabled, DEFAULT_COMPACTION_CONFIG.enabled),
765
+ threshold,
766
+ warnThreshold: ratio(clampNumber(src.warnThreshold, DEFAULT_COMPACTION_CONFIG.warnThreshold, {
767
+ min: Math.min(WARN_THRESHOLD_FLOOR, warnCeiling),
768
+ max: warnCeiling,
769
+ })),
770
+ keepRecentTurns: clampInt(src.keepRecentTurns, DEFAULT_COMPACTION_CONFIG.keepRecentTurns, KEEP_RECENT_TURNS_RANGE),
771
+ useFastTier: bool(src.useFastTier, DEFAULT_COMPACTION_CONFIG.useFastTier),
772
+ // CLAMPED TO THE DEFAULT, NEVER REJECTED — the discipline every other key in
773
+ // this file follows. `ARAGON_COMPACTION_ON_FAILURE=garbage` resolves to
774
+ // `truncate` and throws nothing.
775
+ onFailure: failureMode(src.onFailure, DEFAULT_COMPACTION_CONFIG.onFailure),
776
+ subagents: bool(src.subagents, DEFAULT_COMPACTION_CONFIG.subagents),
777
+ archive: bool(src.archive, DEFAULT_COMPACTION_CONFIG.archive),
778
+ };
779
+ }
780
+ export const DEFAULT_BASH_CONFIG = {
781
+ background: true,
782
+ autoBackground: true,
783
+ startupSettleMs: 4000,
784
+ readyTimeoutMs: 60_000,
785
+ };
786
+ const STARTUP_SETTLE_RANGE = { min: 500, max: 30_000 };
787
+ const READY_TIMEOUT_RANGE = { min: 5_000, max: 600_000 };
788
+ /**
789
+ * THE single gate for every bash-config read AND write, mirroring
790
+ * `clampSkillsConfig` / `clampLogConfig` / `clampTeamConfig` / `clampTodoConfig`
791
+ * for the reason those record: hardening only the read path leaves a bad value
792
+ * on disk that reverts to the default on every launch, which presents to the
793
+ * user as "my setting won't stick".
794
+ *
795
+ * CLAMPED, NEVER REJECTED — the discipline every other key in this file follows.
796
+ * A garbage `startupSettleMs` resolves to 4000 and throws nothing.
797
+ */
798
+ export function clampBashConfig(raw) {
799
+ const src = (raw && typeof raw === 'object' ? raw : {});
800
+ return {
801
+ background: bool(src.background, DEFAULT_BASH_CONFIG.background),
802
+ autoBackground: bool(src.autoBackground, DEFAULT_BASH_CONFIG.autoBackground),
803
+ startupSettleMs: clampInt(src.startupSettleMs, DEFAULT_BASH_CONFIG.startupSettleMs, STARTUP_SETTLE_RANGE),
804
+ readyTimeoutMs: clampInt(src.readyTimeoutMs, DEFAULT_BASH_CONFIG.readyTimeoutMs, READY_TIMEOUT_RANGE),
805
+ };
806
+ }
807
+ /**
808
+ * Parse `0.9`, `90%` or `90` into a ratio, or `null` when it is neither.
809
+ *
810
+ * `null` RATHER THAN A CLAMPED DEFAULT, because this is a COMMAND argument and
811
+ * not a config value: `/compact threshold banana` must say so rather than
812
+ * silently setting 0.9 and reporting success.
813
+ */
814
+ export function parseThresholdInput(raw) {
815
+ const trimmed = raw.trim().replace(/%$/, '');
816
+ if (trimmed.length === 0)
817
+ return null;
818
+ const n = Number.parseFloat(trimmed);
819
+ if (!Number.isFinite(n))
820
+ return null;
821
+ // `90` means 90 %, `0.9` means 0.9. The ambiguity is only at `1`, which is
822
+ // above the clamp ceiling either way.
823
+ const ratio = n > 1 ? n / 100 : n;
824
+ if (ratio <= 0 || ratio > 1)
825
+ return null;
826
+ return ratio;
827
+ }
672
828
  export const DEFAULT_CONFIG = {
673
829
  version: CONFIG_VERSION,
674
830
  provider: 'anthropic',
@@ -703,6 +859,9 @@ export const DEFAULT_CONFIG = {
703
859
  density: 'comfortable',
704
860
  hints: true,
705
861
  mouse: true,
862
+ mouseSelect: DEFAULT_MOUSE_SELECT,
863
+ paste: true,
864
+ scrollResumeMs: DEFAULT_SCROLL_RESUME_MS,
706
865
  planModeDefault: false,
707
866
  planModeMaxAskRounds: DEFAULT_PLAN_MAX_ASK_ROUNDS,
708
867
  planModeHumanTimeoutMs: DEFAULT_PLAN_HUMAN_TIMEOUT_MS,
@@ -728,6 +887,17 @@ export const DEFAULT_CONFIG = {
728
887
  // (`cfg.update.mode !== 'off'`) would read TRUE and the feature would be on
729
888
  // for a reason nobody could point at.
730
889
  update: DEFAULT_UPDATE_CONFIG,
890
+ // And one section later again, for the fourth time. Without this line a user
891
+ // whose `config.json` predates compaction resolves `config.compaction.enabled`
892
+ // to `undefined`, which is FALSY — so a feature that is `true` by default would
893
+ // arrive OFF for every existing user, which is the one direction this
894
+ // particular default must never fail in.
895
+ compaction: DEFAULT_COMPACTION_CONFIG,
896
+ // And one section later again, for the fifth time. `bash.background` is `true`
897
+ // by default, so a `config.json` predating this feature resolving it to
898
+ // `undefined` would arrive with background services OFF for every existing
899
+ // user — the same direction the compaction line above must never fail in.
900
+ bash: DEFAULT_BASH_CONFIG,
731
901
  };
732
902
  /**
733
903
  * Runtime state that used to be stored in `config.json` by mistake
@@ -14,7 +14,7 @@ import { dirname } from 'node:path';
14
14
  import process from 'node:process';
15
15
  import { getConfigPath, getSessionsDir } from './app-paths.js';
16
16
  import { registerSecretsFrom } from '../logging/secret-registry.js';
17
- import { CONFIG_VERSION, DEFAULT_CONFIG, clampFastConfig, clampLogConfig, clampMaxTokens, clampRetryConfig, clampSkillsConfig, clampTeamConfig, clampTodoConfig, clampUpdateConfig, isAutoToken, stripLegacyStateKeys, } from './schema.js';
17
+ import { clampBashConfig, CONFIG_VERSION, DEFAULT_CONFIG, clampCompactionConfig, clampFastConfig, clampLogConfig, clampMaxTokens, clampRetryConfig, clampSkillsConfig, clampTeamConfig, clampTodoConfig, clampUpdateConfig, isAutoToken, stripLegacyStateKeys, } from './schema.js';
18
18
  /**
19
19
  * Normalize `maxTokens` for both directions of the config file.
20
20
  *
@@ -139,6 +139,29 @@ export function loadPersistedConfig() {
139
139
  // predates auto-update, which is all of them. Without it `config.update.mode`
140
140
  // reads `undefined`, and the §3.8 gate tests `!== 'off'`.
141
141
  update: clampUpdateConfig({ ...DEFAULT_CONFIG.update, ...(partial.update ?? {}) }),
142
+ // `compaction` is the ninth. Same one-level rule, same merge — and the READ
143
+ // half supplies a whole section to every user whose `config.json` predates
144
+ // this feature, which is all of them. Without it `config.compaction.enabled`
145
+ // reads `undefined`, which is FALSY, so a feature that is `true` by default
146
+ // would arrive off for every existing user.
147
+ compaction: clampCompactionConfig({
148
+ ...DEFAULT_CONFIG.compaction,
149
+ ...(partial.compaction ?? {}),
150
+ }),
151
+ // `bash` is the tenth. Same one-level rule, same merge — and the READ half
152
+ // supplies a whole section to every user whose `config.json` predates
153
+ // background services, which is all of them. Without it
154
+ // `config.bash.background` reads `undefined`, which is FALSY, so a feature
155
+ // that is `true` by default would arrive off for every existing user.
156
+ //
157
+ // A SIBLING OF `compaction`, NOT A KEY INSIDE ITS ARGUMENT. Nesting it one
158
+ // level too deep type-checks — `...DEFAULT_CONFIG` above already supplies a
159
+ // `bash`, so nothing is missing from the return type — and hands
160
+ // `clampCompactionConfig` a key it drops on the floor. The section then
161
+ // reaches callers straight off the shallow `...partial` spread: unclamped,
162
+ // and NOT merged over the defaults, which is verbatim the failure the
163
+ // paragraph above says this line prevents.
164
+ bash: clampBashConfig({ ...DEFAULT_CONFIG.bash, ...(partial.bash ?? {}) }),
142
165
  };
143
166
  }
144
167
  /**
@@ -208,6 +231,19 @@ export function updatePersistedConfig(patch) {
208
231
  // enterprise mirror the user configured last month is gone with no error
209
232
  // anywhere. AC-20 is the assertion that proves both halves exist.
210
233
  update: clampUpdateConfig({ ...current.update, ...(patch.update ?? {}) }),
234
+ // The ninth section, and the fourth time this file records the same trap.
235
+ // `/compact threshold 0.8` sends `{ compaction: { threshold: 0.8 } }`;
236
+ // without this line the shallow top-level spread REPLACES the section,
237
+ // `enabled` is written absent, and a user who deliberately turned compaction
238
+ // OFF finds it back on tomorrow — a feature that spends money on their behalf
239
+ // re-enabling itself because they nudged a threshold.
240
+ compaction: clampCompactionConfig({ ...current.compaction, ...(patch.compaction ?? {}) }),
241
+ // The tenth section, and the fifth time this file records the same trap.
242
+ // A patch carrying only `{ bash: { autoBackground: false } }` — turning the
243
+ // CLASSIFIER off — would, without this line, be spread shallowly over the
244
+ // top level and write `background` absent, silently unregistering
245
+ // `bash_output` / `bash_kill` from the next launch onward.
246
+ bash: clampBashConfig({ ...current.bash, ...(patch.bash ?? {}) }),
211
247
  };
212
248
  writeConfigFile(merged);
213
249
  return merged;
@@ -32,8 +32,32 @@ export interface UiState {
32
32
  schema: number;
33
33
  /** Lifetime submit count; drives the composer hint fade (`HINT_FADE_AFTER`). */
34
34
  submitCount: number;
35
- /** Whether the one-shot "hold Shift to select text" notice has been shown. */
35
+ /**
36
+ * Whether the one-shot "hold Shift to select text" notice has been shown.
37
+ *
38
+ * DELIBERATELY NOT READ BY THE LIVE GATE ANY MORE (tui-selection-and-scroll-
39
+ * follow P1-8). It is kept in the shape so `importLegacyUiState` still has
40
+ * somewhere to put a migrated value and so a file written by 0.6.2 round-trips
41
+ * unchanged; `mouseNoticeVersion` below is what the notice now consults.
42
+ */
36
43
  mouseNoticeSeen: boolean;
44
+ /**
45
+ * Which revision of the mouse notice this user has already been shown
46
+ * (`0` = none).
47
+ *
48
+ * A VERSION BESIDE THE BOOLEAN, NOT A BUMPED SCHEMA, and this is the second
49
+ * time this file has had to say so. The audience for the CORRECTED text — "drag
50
+ * to select, releasing copies" — is exactly the set of users who already have
51
+ * `mouseNoticeSeen: true` on disk from the old "hold Shift" text, so a boolean
52
+ * can only say "shown", never "shown WHAT". And bumping `UI_STATE_SCHEMA`, the
53
+ * nearest thing to "bump the key", makes `readFromDisk` discard the WHOLE
54
+ * object: every user's `submitCount` resets and the composer hint un-fades for
55
+ * all of them, to re-show one notice.
56
+ *
57
+ * `vtInputNoticeVersion` below is the precedent, written about this exact
58
+ * situation one feature earlier.
59
+ */
60
+ mouseNoticeVersion: number;
37
61
  /**
38
62
  * Which revision of the "this Windows console cannot deliver Shift+Tab or the
39
63
  * wheel" notice this user has already been shown (`0` = none).
@@ -69,6 +93,9 @@ export declare function bumpSubmitCount(): number;
69
93
  export declare function getMouseNoticeSeen(): boolean;
70
94
  export declare function setMouseNoticeSeen(seen: boolean): void;
71
95
  /** `0` on a file written before the field existed — see the field's comment. */
96
+ export declare function getMouseNoticeVersion(): number;
97
+ export declare function setMouseNoticeVersion(version: number): void;
98
+ /** `0` on a file written before the field existed — see the field's comment. */
72
99
  export declare function getVtInputNoticeVersion(): number;
73
100
  export declare function setVtInputNoticeVersion(version: number): void;
74
101
  /**
@@ -35,6 +35,7 @@ const DEFAULT_UI_STATE = {
35
35
  schema: UI_STATE_SCHEMA,
36
36
  submitCount: 0,
37
37
  mouseNoticeSeen: false,
38
+ mouseNoticeVersion: 0,
38
39
  vtInputNoticeVersion: 0,
39
40
  };
40
41
  let memory = null;
@@ -66,6 +67,7 @@ function readFromDisk() {
66
67
  mouseNoticeSeen: typeof parsed.mouseNoticeSeen === 'boolean'
67
68
  ? parsed.mouseNoticeSeen
68
69
  : DEFAULT_UI_STATE.mouseNoticeSeen,
70
+ mouseNoticeVersion: coerceCount(parsed.mouseNoticeVersion, DEFAULT_UI_STATE.mouseNoticeVersion),
69
71
  vtInputNoticeVersion: coerceCount(parsed.vtInputNoticeVersion, DEFAULT_UI_STATE.vtInputNoticeVersion),
70
72
  };
71
73
  }
@@ -127,6 +129,13 @@ export function setMouseNoticeSeen(seen) {
127
129
  update({ mouseNoticeSeen: seen });
128
130
  }
129
131
  /** `0` on a file written before the field existed — see the field's comment. */
132
+ export function getMouseNoticeVersion() {
133
+ return loadUiState().mouseNoticeVersion;
134
+ }
135
+ export function setMouseNoticeVersion(version) {
136
+ update({ mouseNoticeVersion: version });
137
+ }
138
+ /** `0` on a file written before the field existed — see the field's comment. */
130
139
  export function getVtInputNoticeVersion() {
131
140
  return loadUiState().vtInputNoticeVersion;
132
141
  }