@aragon-agent/cli 0.6.3 → 0.6.7

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 (138) hide show
  1. package/CHANGELOG.md +142 -0
  2. package/README.md +203 -13
  3. package/dist/agent/controller.d.ts +158 -2
  4. package/dist/agent/controller.js +400 -1
  5. package/dist/agent/reducer.d.ts +102 -19
  6. package/dist/agent/reducer.js +162 -71
  7. package/dist/agent/system-prompt.d.ts +14 -0
  8. package/dist/agent/system-prompt.js +2 -0
  9. package/dist/agent/usage.d.ts +34 -0
  10. package/dist/agent/usage.js +43 -0
  11. package/dist/cli.js +132 -23
  12. package/dist/commands/builtins.js +101 -1
  13. package/dist/compaction/child.js +5 -0
  14. package/dist/compaction/command.js +14 -1
  15. package/dist/compaction/compactor.d.ts +94 -24
  16. package/dist/compaction/compactor.js +247 -98
  17. package/dist/compaction/context-command.d.ts +33 -0
  18. package/dist/compaction/context-command.js +166 -0
  19. package/dist/compaction/meter.d.ts +251 -0
  20. package/dist/compaction/meter.js +428 -0
  21. package/dist/compaction/pressure.d.ts +6 -0
  22. package/dist/compaction/pressure.js +4 -0
  23. package/dist/compaction/types.d.ts +77 -0
  24. package/dist/compaction/wiring.d.ts +56 -0
  25. package/dist/compaction/wiring.js +107 -25
  26. package/dist/config/cli-commands.js +5 -0
  27. package/dist/config/env.d.ts +1 -1
  28. package/dist/config/env.js +63 -2
  29. package/dist/config/load.d.ts +28 -0
  30. package/dist/config/load.js +83 -1
  31. package/dist/config/schema.d.ts +129 -0
  32. package/dist/config/schema.js +69 -0
  33. package/dist/config/store.js +47 -1
  34. package/dist/exec/events.d.ts +138 -1
  35. package/dist/exec/events.js +42 -0
  36. package/dist/exec/index.js +43 -6
  37. package/dist/exec/progress-reporter.d.ts +23 -0
  38. package/dist/exec/progress-reporter.js +88 -0
  39. package/dist/exec/runner.d.ts +34 -0
  40. package/dist/exec/runner.js +137 -25
  41. package/dist/fast/reviewer.d.ts +19 -1
  42. package/dist/fast/reviewer.js +22 -0
  43. package/dist/fast/types.d.ts +15 -0
  44. package/dist/fast/wiring.d.ts +10 -1
  45. package/dist/fast/wiring.js +18 -1
  46. package/dist/input/limits.d.ts +72 -0
  47. package/dist/input/limits.js +63 -0
  48. package/dist/input/paste-parse.d.ts +71 -0
  49. package/dist/input/paste-parse.js +95 -0
  50. package/dist/input/stdin-filter.d.ts +81 -0
  51. package/dist/input/stdin-filter.js +379 -0
  52. package/dist/logging/install.d.ts +2 -0
  53. package/dist/logging/install.js +53 -1
  54. package/dist/proc/classify.d.ts +52 -0
  55. package/dist/proc/classify.js +143 -0
  56. package/dist/proc/kill-tree.d.ts +42 -0
  57. package/dist/proc/kill-tree.js +119 -0
  58. package/dist/proc/limits.d.ts +84 -0
  59. package/dist/proc/limits.js +84 -0
  60. package/dist/proc/log-ring.d.ts +68 -0
  61. package/dist/proc/log-ring.js +102 -0
  62. package/dist/proc/prompt.d.ts +24 -0
  63. package/dist/proc/prompt.js +49 -0
  64. package/dist/proc/readiness.d.ts +102 -0
  65. package/dist/proc/readiness.js +219 -0
  66. package/dist/proc/supervisor.d.ts +103 -0
  67. package/dist/proc/supervisor.js +465 -0
  68. package/dist/proc/types.d.ts +175 -0
  69. package/dist/proc/types.js +15 -0
  70. package/dist/session/persist.js +36 -1
  71. package/dist/todo/limits.d.ts +4 -17
  72. package/dist/todo/limits.js +4 -17
  73. package/dist/todo/panel-rows.d.ts +8 -6
  74. package/dist/todo/panel-rows.js +19 -6
  75. package/dist/tools/bash-tool.d.ts +51 -2
  76. package/dist/tools/bash-tool.js +321 -51
  77. package/dist/tools/fs-tools.d.ts +17 -0
  78. package/dist/tools/index.d.ts +55 -2
  79. package/dist/tools/index.js +39 -1
  80. package/dist/tools/proc-tools.d.ts +25 -0
  81. package/dist/tools/proc-tools.js +102 -0
  82. package/dist/ui/App.d.ts +15 -2
  83. package/dist/ui/App.js +330 -69
  84. package/dist/ui/AutocompletePopup.d.ts +3 -1
  85. package/dist/ui/AutocompletePopup.js +9 -11
  86. package/dist/ui/BottomStatusRow.d.ts +1 -1
  87. package/dist/ui/Composer.d.ts +56 -1
  88. package/dist/ui/Composer.js +50 -6
  89. package/dist/ui/PromptInput.d.ts +52 -3
  90. package/dist/ui/PromptInput.js +201 -55
  91. package/dist/ui/StatusBar.d.ts +40 -15
  92. package/dist/ui/StatusBar.js +51 -9
  93. package/dist/ui/TeamPanel.d.ts +3 -1
  94. package/dist/ui/TeamPanel.js +13 -9
  95. package/dist/ui/TodoPanel.d.ts +5 -5
  96. package/dist/ui/TodoPanel.js +56 -24
  97. package/dist/ui/Transcript.js +29 -1
  98. package/dist/ui/UpdateLine.d.ts +1 -1
  99. package/dist/ui/capabilities.js +5 -5
  100. package/dist/ui/composer-limits.d.ts +31 -0
  101. package/dist/ui/composer-limits.js +50 -0
  102. package/dist/ui/composer-rows.d.ts +73 -0
  103. package/dist/ui/composer-rows.js +201 -0
  104. package/dist/ui/editor-reducer.d.ts +37 -0
  105. package/dist/ui/editor-reducer.js +88 -12
  106. package/dist/ui/entries/ServiceCard.d.ts +66 -0
  107. package/dist/ui/entries/ServiceCard.js +116 -0
  108. package/dist/ui/entries/UserEntry.d.ts +17 -3
  109. package/dist/ui/entries/UserEntry.js +34 -5
  110. package/dist/ui/exit-snapshot.d.ts +9 -0
  111. package/dist/ui/layout/ScrollViewport.d.ts +1 -1
  112. package/dist/ui/layout/autocomplete.d.ts +18 -0
  113. package/dist/ui/layout/autocomplete.js +26 -0
  114. package/dist/ui/layout/budget.d.ts +34 -6
  115. package/dist/ui/layout/budget.js +32 -7
  116. package/dist/ui/layout/rail.d.ts +4 -53
  117. package/dist/ui/layout/rail.js +10 -62
  118. package/dist/ui/layout/team-panel.d.ts +18 -0
  119. package/dist/ui/layout/team-panel.js +22 -0
  120. package/dist/ui/layout/todo-layout.d.ts +24 -0
  121. package/dist/ui/layout/todo-layout.js +27 -0
  122. package/dist/ui/layout/virtual-window.js +51 -1
  123. package/dist/ui/overlays/HelpOverlay.js +3 -0
  124. package/dist/ui/overlays/PlanReviewOverlay.js +10 -2
  125. package/dist/ui/overlays/QuestionOverlay.js +5 -1
  126. package/dist/ui/overlays/SettingsScreen.js +14 -1
  127. package/dist/ui/paste-frames.d.ts +87 -0
  128. package/dist/ui/paste-frames.js +112 -0
  129. package/dist/ui/paste-tokens.d.ts +90 -0
  130. package/dist/ui/paste-tokens.js +147 -0
  131. package/dist/ui/screen.d.ts +16 -0
  132. package/dist/ui/screen.js +45 -2
  133. package/dist/ui/selection/selection-controller.d.ts +1 -1
  134. package/dist/ui/transcript-text.js +55 -2
  135. package/dist/ui/use-wheel-routing.d.ts +1 -1
  136. package/package.json +2 -2
  137. package/dist/input/stdin-mouse-filter.d.ts +0 -47
  138. package/dist/input/stdin-mouse-filter.js +0 -153
package/dist/cli.js CHANGED
@@ -27,7 +27,7 @@ import { COMPACTION_CONFIG_SET_KEYS, FAST_CONFIG_SET_KEYS, LOG_CONFIG_SET_KEYS,
27
27
  import { attachAgentEvents, attachTeamEvents, installLogging, setScreenRestore, setSignalTerminator, } from './logging/install.js';
28
28
  import { getLogger } from './logging/logger.js';
29
29
  import { runLogsCommand } from './logging/cli-commands.js';
30
- import { ADAPTER_PROVIDERS, clampDensity, clampTheme, clampThinkingLevel, clampAskRounds, clampHumanTimeout, clampSkillsIntegrity, clampSkillsToolPolicy, clampMaxRenderInterval, clampScrollResumeMs, clampTranscriptRetain, clampTranscriptWindow, coercePositiveInt, isAdapterProvider, parseMaxTokensInput, DEFAULT_MAX_RENDER_INTERVAL_MS, DEFAULT_MAX_TOKENS, DEFAULT_PLAN_HUMAN_TIMEOUT_MS, DEFAULT_PLAN_MAX_ASK_ROUNDS, DEFAULT_SCROLL_RESUME_MS, DEFAULT_SKILLS_CONFIG, DEFAULT_TRANSCRIPT_RETAIN, DEFAULT_TRANSCRIPT_WINDOW, } from './config/schema.js';
30
+ import { ADAPTER_PROVIDERS, clampDensity, clampTheme, clampThinkingLevel, clampAskRounds, clampHumanTimeout, clampSkillsIntegrity, clampSkillsToolPolicy, clampContextWindow, clampMaxRenderInterval, clampScrollResumeMs, clampTranscriptRetain, clampTranscriptWindow, coercePositiveInt, isAdapterProvider, isAutoToken, parseMaxTokensInput, DEFAULT_MAX_RENDER_INTERVAL_MS, DEFAULT_MAX_TOKENS, DEFAULT_PLAN_HUMAN_TIMEOUT_MS, DEFAULT_PLAN_MAX_ASK_ROUNDS, DEFAULT_SCROLL_RESUME_MS, DEFAULT_SKILLS_CONFIG, DEFAULT_TRANSCRIPT_RETAIN, DEFAULT_TRANSCRIPT_WINDOW, } from './config/schema.js';
31
31
  import { makeGetApiKey } from './config/load.js';
32
32
  import { AgentController, DENY_ALL_APPROVAL } from './agent/controller.js';
33
33
  import { runSkillsCommand } from './skills/cli-commands.js';
@@ -44,7 +44,7 @@ import { decideRenderMode } from './ui/layout/frame.js';
44
44
  import { createFrameDiffer } from './ui/frame-differ.js';
45
45
  import { wrapStdoutForFrames } from './ui/stdout-frame-writer.js';
46
46
  import { setFrameStatsProvider } from './commands/perf.js';
47
- import { tryCreateMouseFilter } from './input/stdin-mouse-filter.js';
47
+ import { tryCreateStdinFilter } from './input/stdin-filter.js';
48
48
  import { enterAltScreen, writeExitTranscript } from './ui/screen.js';
49
49
  import { supportsWindowsVtInput } from './ui/win-vt-input.js';
50
50
  import { forceWindowsVtInput } from './ui/win-vt-force.js';
@@ -113,6 +113,11 @@ function toFlags(opts) {
113
113
  // on `CliFlags`, AND a line here; `config.test.ts` is the regression pin that
114
114
  // exists because `--no-mouse` shipped inert once already.
115
115
  mouseSelect: opts.mouseSelect,
116
+ // Site three of three for `--paste` / `--no-paste` -- TEN features, one
117
+ // trap. A flag needs the `.option()` declaration below, a field on
118
+ // `CliFlags`, AND a line here; `config.test.ts` is the regression pin that
119
+ // exists because `--no-mouse` shipped inert once already.
120
+ paste: opts.paste,
116
121
  skills: opts.skills,
117
122
  skill: opts.skill,
118
123
  skillsYes: opts.skillsYes,
@@ -148,6 +153,11 @@ function toFlags(opts) {
148
153
  fastModel: opts.fastModel,
149
154
  fastProvider: opts.fastProvider,
150
155
  fastReview: opts.fastReview,
156
+ // …and again for `--fast-delegate` / `--no-fast-delegate`, one feature
157
+ // later — NINE features, one trap. It is `fast.delegate`'s only channel:
158
+ // there is no `ARAGON_FAST_DELEGATE`, so missing this line leaves a wrapper
159
+ // with no way at all to say "do not delegate on this run".
160
+ fastDelegate: opts.fastDelegate,
151
161
  // `--verbose` is just a shorthand for `--log-level debug`; an explicit
152
162
  // `--log-level` still wins, so the two can be combined without surprise.
153
163
  logLevel: opts.logLevel ?? (opts.verbose ? 'debug' : undefined),
@@ -416,7 +426,7 @@ function runInteractive(flags, extras = {}) {
416
426
  // before it reaches this process, so `\x1b[?1000h` is a pure no-op and the
417
427
  // filter has nothing to filter. Gating `wantMouse` — rather than the three
418
428
  // things downstream of it — is what keeps the three in agreement: the
419
- // `?1000h` write (`enterAltScreen(..., { mouse: filter !== null })`), the
429
+ // `?1000h` write (`enterAltScreen(..., { mouse: mouseOn })`), the
420
430
  // filter itself, and the `mouseSource` prop that decides whether the user is
421
431
  // advised about a mode that is not running. Wiring any of them separately
422
432
  // re-opens the gap where the app claims a capability it does not have.
@@ -434,10 +444,36 @@ function runInteractive(flags, extras = {}) {
434
444
  !!process.stdout.isTTY &&
435
445
  !!process.stdin.isTTY &&
436
446
  vtInputSupported;
437
- const mouseFilter = wantMouse
438
- ? tryCreateMouseFilter(process.stdin, (reason) => logger.warn('cli', 'mouse_filter_failed', { reason }))
447
+ // --- Paste handling (tui-paste-handling section 5.1.1 / D-13 / D-17). ----
448
+ //
449
+ // THE GATE IS WIDER THAN THE MOUSE'S, AND `paste` DEFAULTS TO TRUE. Paste
450
+ // needs no `vtInputSupported` and no full-screen: Tier 2 is a heuristic on
451
+ // chunk shape and needs no terminal cooperation at all, which is exactly why
452
+ // it exists for the consoles that never deliver `\x1b[200~` (R-11).
453
+ //
454
+ // WHAT THIS WIDENING BREAKS IF IT IS NOT PAIRED WITH `mouseOn` BELOW (P0-1).
455
+ // `filter !== null` used to be the answer to FIVE questions, only one of which
456
+ // is really about the stream. With paste able to build a filter on its own,
457
+ // `aragon --no-mouse` would get a non-null handle and every one of those five
458
+ // would flip true: `?1000h`/`?1006h`/`?1002h` written, terminal selection taken
459
+ // away (the ONLY reason anyone passes `--no-mouse`), and a startup notice
460
+ // advising the user about a mode they just disabled.
461
+ const wantPaste = config.paste && !!process.stdin.isTTY;
462
+ const pasteBridge = { notify: null };
463
+ const filter = wantMouse || wantPaste
464
+ ? tryCreateStdinFilter(process.stdin, { mouse: wantMouse, paste: wantPaste }, (reason) => logger.warn('cli', 'stdin_filter_failed', { reason }), pasteBridge)
439
465
  : null;
440
- const disposeMouse = () => mouseFilter?.dispose();
466
+ /**
467
+ * THE ONE EXPRESSION EVERY MOUSE SITE READS (I-11 / D-17).
468
+ *
469
+ * `filter !== null` now answers exactly one question -- "is a stream wrapped?"
470
+ * -- and the only site allowed to keep reading it is the `stdin` handed to
471
+ * `render()`.
472
+ */
473
+ const mouseOn = wantMouse && filter !== null;
474
+ /** Same discipline for DEC 2004: already decided here, never re-derived. */
475
+ const pasteOn = wantPaste && filter !== null;
476
+ const disposeStdinFilter = () => filter?.dispose();
441
477
  // --- Drag-select (tui-selection-and-scroll-follow §4.4). ------------------
442
478
  //
443
479
  // ONE EXPRESSION, READ BY FOUR THINGS, and that is the same discipline
@@ -446,7 +482,7 @@ function runInteractive(flags, extras = {}) {
446
482
  // disagree about whether drag-select is running. With it false the session is
447
483
  // byte-identical to a pre-feature build — no extra escape on the wire (AC-8),
448
484
  // no controller, no `decorate`, no mirror.
449
- const wantSelect = mouseFilter !== null && config.mouseSelect;
485
+ const wantSelect = mouseOn && config.mouseSelect;
450
486
  const selectionBridge = {
451
487
  controller: null,
452
488
  onCopied: null,
@@ -526,11 +562,21 @@ function runInteractive(flags, extras = {}) {
526
562
  }));
527
563
  };
528
564
  if (mode === 'fullscreen') {
529
- // `mouse` here means "a filter is installed", NOT "the user wants mouse
530
- // support" — see `AltScreenOptions`. `screen.ts` must never derive it for
531
- // itself, or a later refactor re-opens the gap I-8 closes. `motion` carries
532
- // the same discipline for `?1002h`: already decided, never re-derived.
533
- screen = enterAltScreen(process.stdout, { mouse: mouseFilter !== null, motion: wantSelect });
565
+ // `mouse` here means "a mouse-parsing filter is installed", NOT "the user
566
+ // wants mouse support" — see `AltScreenOptions`. `screen.ts` must never
567
+ // derive it for itself, or a later refactor re-opens the gap I-8 closes.
568
+ // `motion` carries the same discipline for `?1002h`, and `bracketedPaste`
569
+ // for `?2004h`: all three are already decided, none is re-derived.
570
+ //
571
+ // IT IS `mouseOn`, NOT `filter !== null` (P0-1). A `--no-mouse` session with
572
+ // paste at its default `true` still builds a filter, and passing the handle
573
+ // here would write `?1000h` at a user who disabled the mouse precisely to
574
+ // keep their terminal's own selection.
575
+ screen = enterAltScreen(process.stdout, {
576
+ mouse: mouseOn,
577
+ motion: wantSelect,
578
+ bracketedPaste: pasteOn && mode === 'fullscreen',
579
+ });
534
580
  const restore = () => screen?.restore();
535
581
  // Four idempotent restore paths (§4.4). The signal hook is NOT redundant:
536
582
  // - `waitUntilExit().then()` is a microtask and may never be reached when
@@ -613,10 +659,10 @@ function runInteractive(flags, extras = {}) {
613
659
  // than inside `screen.ts`'s closure because `App` needs to READ it and the
614
660
  // handle's own flag is private (it has to be — `restore()` reads it, and
615
661
  // exposing a setter would be a second way to get the two out of step).
616
- let mouseCaptured = mouseFilter !== null;
617
- if (wantSelect && mouseFilter) {
662
+ let mouseCaptured = mouseOn;
663
+ if (wantSelect && filter) {
618
664
  selectionBridge.controller = createSelectionController({
619
- source: mouseFilter.source,
665
+ source: filter.source,
620
666
  // The frame writer is absent under `--no-diff-render`, where there is no
621
667
  // cache to repaint from at all; the controller then falls back to asking
622
668
  // `App` for a React redraw on every drag flush, which is slower and
@@ -656,7 +702,7 @@ function runInteractive(flags, extras = {}) {
656
702
  const terminalBridge = mode === 'fullscreen' || writeForeign
657
703
  ? {
658
704
  mouseSelect: wantSelect,
659
- ...(mouseFilter
705
+ ...(mouseOn
660
706
  ? {
661
707
  setMouseCapture: (on) => {
662
708
  screen?.setMouseCapture(on);
@@ -682,13 +728,21 @@ function runInteractive(flags, extras = {}) {
682
728
  !!process.stdin.isTTY &&
683
729
  !process.env.CI &&
684
730
  config.update.mode !== 'off';
685
- const instance = render(_jsx(App, { controller: controller, version: VERSION, mode: mode, confirmBridge: confirmBridge, humanInputBridge: humanInputBridge, initialPrompt: extras.initialPrompt, initialOverlay: extras.initialOverlay, mouseSource: mouseFilter?.source,
731
+ const instance = render(_jsx(App, { controller: controller, version: VERSION, mode: mode, confirmBridge: confirmBridge, humanInputBridge: humanInputBridge, initialPrompt: extras.initialPrompt, initialOverlay: extras.initialOverlay,
732
+ // `mouseOn`, NOT the handle (I-11). `App.tsx:186-188` documents this
733
+ // prop's contract as "absent under `--no-mouse`, inline, or non-TTY", and
734
+ // it feeds `enabled: !!mouseSource` on a wheel subscription that would
735
+ // otherwise be a channel no event can ever arrive on.
736
+ mouseSource: mouseOn && filter ? filter.source : undefined,
686
737
  // Present ONLY when this console swallows `CSI Z` and mouse reports.
687
738
  // Passed in rather than probed inside `App` so that every test renders
688
739
  // the same tree on every machine — reading `process.versions.node` from
689
740
  // a component would make the notice appear or vanish according to which
690
741
  // Node the developer happened to have installed.
691
- vtInputWarning: vtInputSupported ? undefined : { nodeVersion: process.versions.node }, updateBridge: updateEligible ? updateBridge : undefined, terminal: terminalBridge }),
742
+ vtInputWarning: vtInputSupported ? undefined : { nodeVersion: process.versions.node }, updateBridge: updateEligible ? updateBridge : undefined,
743
+ // Absent when nothing can refuse a paste, so a `--no-paste` tree is
744
+ // byte-identical to a build without this feature.
745
+ pasteBridge: pasteOn ? pasteBridge : undefined, terminal: terminalBridge }),
692
746
  // Full-screen owns console.* itself (I-4): Ink's patchConsole writes
693
747
  // straight to stdout and permanently shifts the fixed frame's accounting.
694
748
  {
@@ -696,7 +750,7 @@ function runInteractive(flags, extras = {}) {
696
750
  patchConsole: mode === 'inline',
697
751
  // I-5: with mouse support off the REAL stdin is handed over unwrapped, so
698
752
  // every existing path is provably unchanged.
699
- stdin: mouseFilter?.stdin ?? process.stdin,
753
+ stdin: filter?.stdin ?? process.stdin,
700
754
  // SPREAD, not `frameWriter?.stdout ?? process.stdout`. This call passed no
701
755
  // `stdout` at all before, and Ink defaults it to `process.stdout`; omitting
702
756
  // the key entirely when the writer is off keeps the options object the same
@@ -757,12 +811,12 @@ function runInteractive(flags, extras = {}) {
757
811
  .waitUntilExit()
758
812
  .then(() => {
759
813
  screen?.restore();
760
- // BEFORE `disposeMouse()`, which clears the listener set it subscribed to,
814
+ // BEFORE `disposeStdinFilter()`, which clears the listener set it subscribed to,
761
815
  // and after `restore()` for the reason that call records: the terminal must
762
816
  // stop reporting first, and anything already in flight is still consumed by
763
817
  // a live filter.
764
818
  disposeSelection();
765
- disposeMouse();
819
+ disposeStdinFilter();
766
820
  // BOTH BRANCHES, the same discipline `controller.dispose()` and
767
821
  // `updateService?.dispose()` already follow. It drops the `'resize'`
768
822
  // listener of I-8 and detaches the `/perf` provider so a later `/perf` in a
@@ -786,12 +840,12 @@ function runInteractive(flags, extras = {}) {
786
840
  })
787
841
  .catch(() => {
788
842
  screen?.restore();
789
- // BEFORE `disposeMouse()`, which clears the listener set it subscribed to,
843
+ // BEFORE `disposeStdinFilter()`, which clears the listener set it subscribed to,
790
844
  // and after `restore()` for the reason that call records: the terminal must
791
845
  // stop reporting first, and anything already in flight is still consumed by
792
846
  // a live filter.
793
847
  disposeSelection();
794
- disposeMouse();
848
+ disposeStdinFilter();
795
849
  disposeFrameWriter();
796
850
  controller.dispose();
797
851
  updateService?.dispose();
@@ -875,6 +929,11 @@ const CONFIG_SET_KEYS = new Set([
875
929
  'baseUrl',
876
930
  'thinkingLevel',
877
931
  'maxTokens',
932
+ // context-usage-gauge-accuracy §3.6. Listed here AND given its own branch in
933
+ // `runConfigSet` below - a key present here but absent there falls through
934
+ // every case, writes nothing, and still prints `Set contextWindow = 1000000`
935
+ // (P1-2).
936
+ 'contextWindow',
878
937
  'theme',
879
938
  'confirmTools',
880
939
  'toolTimeoutMs',
@@ -911,6 +970,10 @@ const CONFIG_SET_KEYS = new Set([
911
970
  // records: a key listed here but absent there writes nothing and still prints
912
971
  // `Set mouseSelect = false`.
913
972
  'mouseSelect',
973
+ // Listed here AND cased in the switch below, for the reason the note above
974
+ // `mouse` records: a key present here but absent there falls through every
975
+ // case, writes nothing, and still prints `Set paste = false`.
976
+ 'paste',
914
977
  'scrollResumeMs',
915
978
  'historyEnabled',
916
979
  'planModeDefault',
@@ -967,6 +1030,30 @@ function runConfigSetMaxTokens(value) {
967
1030
  const merged = updatePersistedConfig({ maxTokens: stored });
968
1031
  process.stdout.write(`Set maxTokens = ${merged.maxTokens ?? 'auto'}\n`);
969
1032
  }
1033
+ /**
1034
+ * `aragon config set contextWindow <n|auto>`.
1035
+ *
1036
+ * ITS OWN BRANCH, AHEAD OF THE SWITCH, for the reason `maxTokens` has one: it
1037
+ * must echo the STORED value rather than the typed one, so
1038
+ * `config set contextWindow 99999999` reports the clamped 5000000 instead of a
1039
+ * number that was never written. It also REJECTS nonsense rather than clamping
1040
+ * it - a non-numeric argument here is almost always a typo for a number, and
1041
+ * storing AUTO for `20O000` would quietly discard the user's intent.
1042
+ */
1043
+ function runConfigSetContextWindow(value) {
1044
+ const trimmed = value.trim();
1045
+ const auto = isAutoToken(trimmed);
1046
+ if (!auto && !/^\d+$/.test(trimmed)) {
1047
+ process.stderr.write(`Invalid contextWindow "${value}". Use a number of tokens or "auto".\n`);
1048
+ process.exitCode = 2;
1049
+ return;
1050
+ }
1051
+ // `null` GOES IN UNCLAMPED (RV-12). Running AUTO through the clamp yields
1052
+ // 8000 and removes "auto" from the config permanently.
1053
+ const stored = auto ? null : clampContextWindow(trimmed, null);
1054
+ const merged = updatePersistedConfig({ contextWindow: stored });
1055
+ process.stdout.write(`Set contextWindow = ${merged.contextWindow ?? 'auto'}\n`);
1056
+ }
970
1057
  function runConfigSet(key, value) {
971
1058
  if (!CONFIG_SET_KEYS.has(key)) {
972
1059
  process.stderr.write(`Unknown config key "${key}". Known keys: ${[...CONFIG_SET_KEYS].join(', ')}.\n`);
@@ -987,6 +1074,11 @@ function runConfigSet(key, value) {
987
1074
  runConfigSetMaxTokens(value);
988
1075
  return;
989
1076
  }
1077
+ // The second tri-state key, and it needs its own branch for the same reason.
1078
+ if (key === 'contextWindow') {
1079
+ runConfigSetContextWindow(value);
1080
+ return;
1081
+ }
990
1082
  const teamPatch = applyTeamConfigSet(key, value);
991
1083
  if (teamPatch) {
992
1084
  // The written value is echoed from the CLAMPED result rather than from the
@@ -1109,6 +1201,9 @@ function runConfigSet(key, value) {
1109
1201
  case 'mouseSelect':
1110
1202
  patch.mouseSelect = value === 'true' || value === '1';
1111
1203
  break;
1204
+ case 'paste':
1205
+ patch.paste = value === 'true' || value === '1';
1206
+ break;
1112
1207
  // Clamped rather than rejected, for the same reason `planModeDefault` below
1113
1208
  // records: hardening only the READ path leaves a bad value on disk that
1114
1209
  // reverts to the default on every launch, which presents as "my setting
@@ -1381,6 +1476,12 @@ function buildProgram() {
1381
1476
  // `false` on every run that passes no flag at all.
1382
1477
  .option('--mouse-select', 'Drag with the mouse to select text; releasing copies it')
1383
1478
  .option('--no-mouse-select', 'Keep wheel scrolling, but leave drag-select off')
1479
+ // Positive form FIRST, same tri-state reason as `--mouse-select` above:
1480
+ // `paste` is persisted and defaults to `true`, so a lone `--no-paste` would
1481
+ // make commander default `opts.paste` to `true` -- indistinguishable from
1482
+ // silence, and silently overriding a stored `false` on every run.
1483
+ .option('--paste', 'Collapse large pastes into a placeholder (the default)')
1484
+ .option('--no-paste', 'Treat pasted bytes as keystrokes (pre-0.6.3 behavior)')
1384
1485
  // Declaring `--fullscreen` BEFORE `--no-fullscreen` keeps the default
1385
1486
  // `undefined` instead of `true`, which is what makes the tri-state work:
1386
1487
  // "no opinion" must stay distinguishable from "force it".
@@ -1429,6 +1530,14 @@ function buildProgram() {
1429
1530
  .option('--fast-model <id>', 'The fast model id (implies nothing about --fast)')
1430
1531
  .option('--fast-provider <id>', 'Provider for the fast model (default: the main provider)')
1431
1532
  .option('--fast-review <n|off>', 'Turns between automatic fast reviews, or "off"')
1533
+ // BOTH FORMS, POSITIVE FIRST, and here it matters more than anywhere above:
1534
+ // `fast.delegate` is PERSISTED and defaults to TRUE, so declaring only
1535
+ // `--no-fast-delegate` would make commander default `opts.fastDelegate` to
1536
+ // `true` and force `delegate: true` on EVERY run that passed no flag at all
1537
+ // — silently overwriting a user's stored `false`. Same polarity, same trap
1538
+ // and same fix as `--update` / `--compaction`.
1539
+ .option('--fast-delegate', 'Let the fast tier take mechanical sub-steps (the default)')
1540
+ .option('--no-fast-delegate', 'Run every sub-step on the main model')
1432
1541
  // BOTH FORMS, positive first, for the reason recorded seven times above —
1433
1542
  // and this is the pair whose omission would be worst. `update.mode` is
1434
1543
  // PERSISTED and defaults to `'auto'`, so a lone `--no-update` would make
@@ -7,7 +7,7 @@ import { existsSync, statSync } from 'node:fs';
7
7
  import { isAbsolute, resolve } from 'node:path';
8
8
  import { resolveOutputTokens } from '@aragon-agent/core';
9
9
  import { clampThinkingLevel, clampTheme, isAdapterProvider, isThemeName, parseMaxTokensInput, ADAPTER_PROVIDERS, DEFAULT_MAX_TOKENS, HARD_MAX_RETRIES, LEGACY_DARK_THEME, THEME_NAMES, THINKING_LEVELS, } from '../config/schema.js';
10
- import { computeCost, formatCost, formatTokens } from '../agent/usage.js';
10
+ import { computeCost, formatCost, formatDuration, formatTokens } from '../agent/usage.js';
11
11
  import { TODO_FOLLOW_LIMITS } from '../todo/limits.js';
12
12
  import { buildContinuationMessage } from '../todo/follow-through.js';
13
13
  import { MODE_LABEL, nextMode } from '../agent/agent-mode.js';
@@ -17,6 +17,7 @@ import { getLogger } from '../logging/logger.js';
17
17
  import { copyText } from '../ui/clipboard.js';
18
18
  import { perfCommand } from './perf.js';
19
19
  import { runCompactCommand } from '../compaction/command.js';
20
+ import { runContextCommand } from '../compaction/context-command.js';
20
21
  /**
21
22
  * `/fast status` — THE GUARANTEED REPORTING SURFACE for the tier (§4.4 / R-8).
22
23
  *
@@ -809,6 +810,14 @@ const COMMANDS = [
809
810
  description: 'Context compaction: status | on | off | threshold <n> | keep <n> | <instructions>',
810
811
  run: (ctx) => runCompactCommand(ctx),
811
812
  },
813
+ {
814
+ // REGISTRATION ONLY, for the reason `/compact` records one entry up: this
815
+ // file is past the 1000-line guideline and the report has five sections.
816
+ // The body is in `compaction/context-command.ts`.
817
+ name: 'context',
818
+ description: 'Show context occupancy, the window it is measured against, and session spend',
819
+ run: (ctx) => runContextCommand(ctx),
820
+ },
812
821
  {
813
822
  name: 'expand',
814
823
  // DELIBERATELY LEFT TOOL-ONLY, and the asymmetry with Ctrl+O is a decision
@@ -1048,6 +1057,95 @@ const COMMANDS = [
1048
1057
  // render layer, and threading `ui/render-cache.js` into this file would make
1049
1058
  // every other command's import graph pull the renderer in with it.
1050
1059
  perfCommand,
1060
+ {
1061
+ name: 'bg',
1062
+ description: 'Background services: list | logs <id> [n] | stop <id|all> | status',
1063
+ /**
1064
+ * THE USER'S HANDLE ON WHAT THE AGENT LEFT RUNNING (§5.3).
1065
+ *
1066
+ * It is the LIVE view, and that is why it exists at all: the transcript
1067
+ * records EVENTS about a service (D-11) — a card printed into `<Static>`
1068
+ * cannot be rewritten — so "what is running right now" lives here, in the
1069
+ * status chip, and in `bash_output`. Without this command a user whose
1070
+ * server card scrolled away an hour ago has no way to name it.
1071
+ */
1072
+ run: async (ctx) => {
1073
+ const arg = ctx.args.trim();
1074
+ const [verb = 'list', value, count] = arg.split(/\s+/);
1075
+ const controller = ctx.controller;
1076
+ if (!controller.isBackgroundRegistered()) {
1077
+ ctx.notify('info', 'Background services are off for this session (bash.background).');
1078
+ return;
1079
+ }
1080
+ const services = controller.listServices();
1081
+ if (verb === 'status') {
1082
+ const live = controller.liveServiceCount();
1083
+ ctx.notify('info', live === 0
1084
+ ? 'No background services running.'
1085
+ : `${live} background service${live === 1 ? '' : 's'} running.`);
1086
+ return;
1087
+ }
1088
+ if (verb === 'logs') {
1089
+ if (!value) {
1090
+ ctx.notify('warn', 'Usage: /bg logs <id> [n]');
1091
+ return;
1092
+ }
1093
+ const service = controller.getServiceSnapshot(value);
1094
+ if (!service) {
1095
+ ctx.notify('warn', `No service "${value}".`);
1096
+ return;
1097
+ }
1098
+ const page = controller.readServiceLog(value);
1099
+ const n = count ? Number.parseInt(count, 10) : DEFAULT_BG_LOG_ROWS;
1100
+ const rows = page?.rows ?? [];
1101
+ const take = Number.isFinite(n) && n > 0 ? n : DEFAULT_BG_LOG_ROWS;
1102
+ const shown = rows.slice(-take);
1103
+ ctx.notify('info', shown.length === 0
1104
+ ? `service ${service.id}: no output yet.`
1105
+ : `service ${service.id} (last ${shown.length} rows)\n${shown.join('\n')}`);
1106
+ return;
1107
+ }
1108
+ if (verb === 'stop') {
1109
+ if (!value) {
1110
+ ctx.notify('warn', 'Usage: /bg stop <id|all>');
1111
+ return;
1112
+ }
1113
+ // AWAITED, unlike `doExit`'s force path: a slash command runs on a live
1114
+ // event loop, so the graceful SIGTERM -> SIGKILL ladder can run and a
1115
+ // dev server gets its chance to release its port cleanly.
1116
+ const stopped = await controller.stopService(value);
1117
+ if (stopped.length === 0) {
1118
+ ctx.notify('info', 'Nothing to stop.');
1119
+ return;
1120
+ }
1121
+ const leaked = stopped.filter((svc) => svc.killIncomplete);
1122
+ ctx.toast('success', `Stopped ${stopped.length} service${stopped.length === 1 ? '' : 's'}.`);
1123
+ if (leaked.length > 0) {
1124
+ // AN HONEST WARNING BEATS A SILENT LEAK (R-2 / P2-6). `taskkill /t`
1125
+ // cannot reach a grandchild that re-parented itself, and saying so is
1126
+ // the difference between a puzzle and a `netstat` the user knows to run.
1127
+ ctx.notify('warn', `${leaked.map((svc) => svc.id).join(', ')} may have left a detached child - ` +
1128
+ 'check with netstat/lsof if a port is still held.');
1129
+ }
1130
+ return;
1131
+ }
1132
+ if (verb !== 'list' && verb !== '') {
1133
+ ctx.notify('warn', `Unknown argument "${verb}" - use /bg [list|logs|stop|status].`);
1134
+ return;
1135
+ }
1136
+ if (services.length === 0) {
1137
+ ctx.notify('info', 'No background services this session.');
1138
+ return;
1139
+ }
1140
+ const now = Date.now();
1141
+ const lines = services.map((svc) => {
1142
+ const uptime = formatDuration((svc.endedAt ?? now) - svc.startedAt);
1143
+ const where = svc.url ? ` ${svc.url}` : '';
1144
+ return `${svc.id} ${svc.status} ${uptime}${where} $ ${svc.command}`;
1145
+ });
1146
+ ctx.notify('info', `Background services:\n${lines.join('\n')}`);
1147
+ },
1148
+ },
1051
1149
  {
1052
1150
  name: 'exit',
1053
1151
  aliases: ['quit'],
@@ -1055,6 +1153,8 @@ const COMMANDS = [
1055
1153
  run: (ctx) => ctx.exit(),
1056
1154
  },
1057
1155
  ];
1156
+ /** Rows `/bg logs <id>` shows when the user names no count. */
1157
+ const DEFAULT_BG_LOG_ROWS = 40;
1058
1158
  export function registerBuiltinCommands(registry) {
1059
1159
  for (const command of COMMANDS)
1060
1160
  registry.register(command);
@@ -85,6 +85,11 @@ export function createChildContextManager(req, deps) {
85
85
  }
86
86
  if (event.type !== 'compaction_end')
87
87
  return;
88
+ // DEFENCE IN DEPTH SINCE quiet-noop D-8: a child compaction that declines
89
+ // before committing to work no longer emits this event at all, so this
90
+ // filter is now unreachable on that path. It is retained deliberately -
91
+ // it is this tree's own record that a no-op compaction is a non-event, and
92
+ // deleting it would make the child depend on that rule holding forever.
88
93
  if (!event.record.applied && event.record.mode === 'none')
89
94
  return;
90
95
  req.onCompacted();
@@ -66,9 +66,22 @@ export function formatCompactionStatus(ctx) {
66
66
  lines.push(` Summarizer: ${snapshot.model || '(none)'}` +
67
67
  (config.useFastTier ? ' (fast tier preferred)' : ' (fast tier not used)'));
68
68
  lines.push(` On summarize failure: ${config.onFailure}`);
69
+ // THE PARENTHETICAL IS BUILT FROM WHATEVER APPLIES, AND OMITTED WHEN NOTHING
70
+ // DOES (quiet-noop D-9). `declined` counts checkpoints that fired and chose to
71
+ // do nothing - the only human-readable trace of a decline once the transcript
72
+ // stopped carrying a card for one. It renders ONLY when non-zero: a counter
73
+ // that says `(0 checkpoints declined)` in every healthy session would have
74
+ // replaced one piece of noise with another.
75
+ const sessionNotes = [];
76
+ if (snapshot.generation > 0)
77
+ sessionNotes.push(`generation ${snapshot.generation}`);
78
+ const declined = snapshot.declined ?? 0;
79
+ if (declined > 0) {
80
+ sessionNotes.push(`${declined} ${declined === 1 ? 'checkpoint' : 'checkpoints'} declined`);
81
+ }
69
82
  lines.push(` This session: ${snapshot.compactions} ` +
70
83
  `${snapshot.compactions === 1 ? 'compaction' : 'compactions'}` +
71
- (snapshot.generation > 0 ? ` (generation ${snapshot.generation})` : '') +
84
+ (sessionNotes.length > 0 ? ` (${sessionNotes.join(', ')})` : '') +
72
85
  `, ${formatTokens(snapshot.tokensReclaimed)} tokens reclaimed`);
73
86
  const cost = snapshot.pricingUnknown
74
87
  ? `unknown (no price table for ${snapshot.model || 'the summarizer'})`