@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
@@ -25,12 +25,22 @@
25
25
  * methods delegate to a compactor that reads config LIVE - which is what lets
26
26
  * `/compact off`, `/compact threshold 0.8` and a settings-screen save take effect
27
27
  * mid-session without touching the `Agent`.
28
+ *
29
+ * DECLINED COMPACTIONS PRODUCE NO CLI EVENTS (quiet-noop §3.6). A `pressure`
30
+ * compaction that changed nothing, called nothing and spent nothing never emits
31
+ * `compaction_start`, so `onCompactorEvent` never opens a card and `settlePending`
32
+ * finds neither a held record nor a skeleton to synthesize. CORE still emits its
33
+ * own pair for every attempt, which is why `settlePending` still runs - it just
34
+ * refreshes the snapshot and returns. `onCompactorEvent`'s `if (!this.open)`
35
+ * guard is now unreachable for declines and stays for the abort race it was
36
+ * written for.
28
37
  */
29
38
  import { DEFAULT_RETRY_POLICY, estimatePromptTokens, initProviders, validateHistory, } from '@aragon-agent/core';
30
39
  import { getLogger } from '../logging/logger.js';
31
40
  import { mintRunId, writeArchive } from './archive.js';
32
41
  import { createChildContextManager } from './child.js';
33
42
  import { Compactor } from './compactor.js';
43
+ import { ContextMeter } from './meter.js';
34
44
  /**
35
45
  * One retry, not ten and not zero - the `fast/wiring.ts` argument unchanged
36
46
  * (D-10).
@@ -59,6 +69,7 @@ export function offCompactionSnapshot() {
59
69
  headroom: 0,
60
70
  source: 'estimate',
61
71
  windowKnown: false,
72
+ windowOverridden: false,
62
73
  deltaTokens: 0,
63
74
  },
64
75
  };
@@ -68,6 +79,8 @@ export class CompactionWiring {
68
79
  log = getLogger().child('compaction');
69
80
  listeners = new Set();
70
81
  compactor;
82
+ /** The occupancy meter, injected or private (R-1). Forwarded to the compactor. */
83
+ meter;
71
84
  /** The compactor's OWN transport (D-10). Lazy - see `getRegistry()`. */
72
85
  registry = null;
73
86
  unsubscribe = null;
@@ -116,6 +129,15 @@ export class CompactionWiring {
116
129
  constructor(deps) {
117
130
  this.deps = deps;
118
131
  this.enabled = deps.getConfig().compaction.enabled;
132
+ this.meter =
133
+ deps.meter ??
134
+ new ContextMeter({
135
+ getMessages: deps.getMessages,
136
+ getSystemPrompt: deps.getSystemPrompt,
137
+ getModelInfo: () => deps.getModelInfoFor(this.mainRef()),
138
+ isWindowKnown: () => deps.isPricedModel(this.mainRef()),
139
+ getWindowOverride: () => deps.getConfig().contextWindow,
140
+ });
119
141
  this.compactor = new Compactor({
120
142
  getConfig: deps.getConfig,
121
143
  hasKey: deps.hasKey,
@@ -130,6 +152,9 @@ export class CompactionWiring {
130
152
  complete: (providerId, request) => this.getRegistry().complete(providerId, request),
131
153
  emit: (event) => this.onCompactorEvent(event),
132
154
  notify: deps.notify,
155
+ // ONE METER PER PROCESS. The compactor must not build its own fallback
156
+ // here, or the trigger would read a different number from the gauge.
157
+ meter: this.meter,
133
158
  // ONE NUMBER ON SCREEN AND IN THE TRIGGER (R-11 / DH-15). The checkpoint
134
159
  // has already measured; publishing THAT pressure rather than re-measuring
135
160
  // is what keeps the two from disagreeing by one turn's tool results.
@@ -196,9 +221,23 @@ export class CompactionWiring {
196
221
  * `/clear`, `/resume`, and any future host-side rewrite. A recorded prefix
197
222
  * length indexes an array that no longer exists, so the delta must be
198
223
  * abandoned rather than applied to a different history.
224
+ *
225
+ * THE **DEEP** RESET (I-8). `/resume` can change the model in the same breath,
226
+ * so `estimateOffset` - a calibration for one toolset on one model - goes too.
227
+ * The SHALLOW reset is `settlePending`'s splice branch, and the two are not
228
+ * interchangeable: swapping them produces silent errors in OPPOSITE
229
+ * directions (a splice that drops the offset reads too low and climbs back; a
230
+ * resume that keeps it reads too high and never self-corrects).
231
+ *
232
+ * DUPLICATED BY `AgentController` ON PURPOSE. The controller calls
233
+ * `contextMeter.onHistoryReplaced()` directly at the same two sites, because
234
+ * this class does not exist at all when `compaction.enabled` is false - and
235
+ * that unconditional call is the only thing that keeps `/resume` re-measuring
236
+ * for such a session. A deep reset is idempotent, so both firing costs
237
+ * nothing; deleting the controller's copy "to de-duplicate" reintroduces P0-2.
199
238
  */
200
239
  onHistoryReplaced() {
201
- this.compactor.invalidateMeasurement();
240
+ this.meter.onHistoryReplaced();
202
241
  }
203
242
  /**
204
243
  * The per-child manager factory, or `null` when children get none
@@ -305,13 +344,19 @@ export class CompactionWiring {
305
344
  // -----------------------------------------------------------------------
306
345
  // Status and events
307
346
  // -----------------------------------------------------------------------
347
+ /**
348
+ * The status a reader gets right now.
349
+ *
350
+ * IT READS `meter.current()`, WHICH RE-MEASURES WHEN DIRTY - and that is the
351
+ * fix for P0-1 (§3.3). This method is called one statement after
352
+ * `settlePending` marks the meter spliced, so the `snapshot` event emitted
353
+ * immediately after `compaction_end` carries the POST-compaction occupancy.
354
+ * The old body preferred `compactor.lastMeasured()`, i.e. the measurement that
355
+ * TRIGGERED this compaction, so the gauge fell and was bounced straight back
356
+ * to the pre-compaction figure inside the same synchronous emit loop.
357
+ */
308
358
  snapshot() {
309
- const pressure = this.compactor.lastMeasured() ??
310
- this.compactor.measure({
311
- messages: this.deps.getMessages(),
312
- systemPrompt: this.deps.getSystemPrompt(),
313
- });
314
- return this.snapshotWith(pressure);
359
+ return this.snapshotWith(this.meter.current());
315
360
  }
316
361
  /**
317
362
  * `snapshot()` with the pressure argument substituted for the re-measurement.
@@ -328,6 +373,10 @@ export class CompactionWiring {
328
373
  live: this.enabled && summarizer !== null,
329
374
  model: summarizer?.ref.modelId ?? '',
330
375
  compactions: totals.compactions,
376
+ // OMITTED WHEN ZERO (quiet-noop D-9), so a healthy session's snapshot is
377
+ // shaped exactly as it was before this feature and `/compact status` has
378
+ // nothing extra to render.
379
+ ...(totals.declined > 0 ? { declined: totals.declined } : {}),
331
380
  tokensReclaimed: totals.tokensReclaimed,
332
381
  usage: totals.usage,
333
382
  pricingUnknown: totals.pricingUnknown,
@@ -349,6 +398,17 @@ export class CompactionWiring {
349
398
  // -----------------------------------------------------------------------
350
399
  // Internals
351
400
  // -----------------------------------------------------------------------
401
+ /**
402
+ * The main model, for the FALLBACK meter's deps only (R-1).
403
+ *
404
+ * Production injects a meter built by `AgentController`, which resolves the
405
+ * same ref from the same live config; this exists so a test that constructs a
406
+ * bare wiring still gets a working denominator.
407
+ */
408
+ mainRef() {
409
+ const config = this.deps.getConfig();
410
+ return { providerId: config.provider, modelId: config.model };
411
+ }
352
412
  /**
353
413
  * The transport one summarization call goes through - separate from the lead's,
354
414
  * and fail-fast (D-10).
@@ -395,19 +455,16 @@ export class CompactionWiring {
395
455
  return;
396
456
  }
397
457
  if (event.type === 'turn_end') {
398
- // The calibration offset, re-measured every turn (D-23). Read the history
399
- // AFTER the turn's assistant message is pushed? No: `turn_end` is emitted
400
- // BEFORE the push, so this is exactly the history the reported `usage`
401
- // was measured against, which is what makes the difference meaningful.
402
- this.compactor.onTurnEnd(event.usage, this.deps.getMessages(), this.deps.getSystemPrompt());
403
- // The gauge and the trigger read one function (R-11 / AC-3), so the
404
- // snapshot's pressure is refreshed from the same measurement the status bar
405
- // is about to render.
406
- this.compactor.measure({
407
- lastUsage: event.usage,
408
- messages: this.deps.getMessages(),
409
- systemPrompt: this.deps.getSystemPrompt(),
410
- });
458
+ // THE MEASUREMENT IS THE METER'S (context-usage-gauge-accuracy §3.3.4). It
459
+ // subscribes to `turn_end` on the same stream and recomputes the offset,
460
+ // the prefix length and the pressure there; the two calls that used to
461
+ // live here - `compactor.onTurnEnd(...)` and `compactor.measure(...)` -
462
+ // were both forwarders to it and doing the same work twice.
463
+ //
464
+ // THE SNAPSHOT EMIT STAYS. The other fields on it - `inFlight`, the
465
+ // session counts, the summarizer - are this class's own and still need
466
+ // refreshing on a turn boundary, and `snapshot()` picks up the meter's
467
+ // freshly published pressure for free.
411
468
  this.emit({ type: 'snapshot', snapshot: this.snapshot() });
412
469
  return;
413
470
  }
@@ -445,8 +502,20 @@ export class CompactionWiring {
445
502
  // compaction no matter which branch the verdict took - including the early
446
503
  // `return` below.
447
504
  const dropped = this.compactor.takeLastDropped();
448
- if (!record)
505
+ if (!record) {
506
+ // A SILENT DECLINE (quiet-noop §3.6). No card - but the gauge and the chip
507
+ // are still refreshed: `inFlight` has just gone false and `/compact status`
508
+ // reads this same snapshot, which is where the `declined` count surfaces.
509
+ //
510
+ // `snapshot()` NOW GOES THROUGH `meter.current()`, so this path DOES pay a
511
+ // re-measurement when the history has moved since the last one (RV-11 -
512
+ // the previous sentence here claimed the opposite and was left stale by
513
+ // context-usage-gauge-accuracy). The cost is bounded the same way it always
514
+ // was: guard 4 caps declines per session at `stuckLimit`, and a re-measure
515
+ // on the measured branch walks one turn's worth of appended messages.
516
+ this.emit({ type: 'snapshot', snapshot: this.snapshot() });
449
517
  return;
518
+ }
450
519
  const settled = {
451
520
  ...record,
452
521
  applied: verdict.applied,
@@ -457,10 +526,23 @@ export class CompactionWiring {
457
526
  };
458
527
  if (verdict.applied) {
459
528
  this.compactor.recordApplied(settled.tokensBefore, settled.tokensAfter);
460
- // SITE 1 OF THE FOUR INVALIDATION SITES (hardening §3.2.3). The in-loop
461
- // splice replaced the history, so every recorded index means nothing; the
462
- // manual path settles through here too, so one call covers both.
463
- this.compactor.invalidateMeasurement();
529
+ // SITE 1 OF THE FOUR INVALIDATION SITES (hardening §3.2.3), and THE LOAD-
530
+ // BEARING LINE OF THE P0-1 FIX (context-usage-gauge-accuracy I-9).
531
+ //
532
+ // ITS POSITION IS THE WHOLE ARGUMENT: it runs in this class's own
533
+ // synchronous code, BEFORE both `emit` calls below, so the `snapshot()`
534
+ // two statements down is guaranteed to see a dirty meter and re-measure -
535
+ // no matter which of the two agent-stream listeners the emitter reached
536
+ // first. Deleting it and relying on the meter's own `compaction_end`
537
+ // subscription instead moves correctness onto subscription ORDER, which is
538
+ // true in every unit test and false in production import order.
539
+ //
540
+ // THE **SHALLOW** RESET (I-8). A splice changes the history and nothing
541
+ // else, so `estimateOffset` - a calibration for this toolset on this model
542
+ // - is still valid and is the only thing keeping the post-compaction
543
+ // estimate honest (I-5). `onHistoryReplaced()` is the other depth and is
544
+ // NOT interchangeable with this one.
545
+ this.meter.onHistorySpliced();
464
546
  const path = this.archive(settled, dropped);
465
547
  if (path)
466
548
  settled.archivePath = path;
@@ -405,6 +405,11 @@ function maskConfig(config) {
405
405
  function displayOverride(key, value) {
406
406
  if (key === 'maxTokens' && value === null)
407
407
  return 'auto';
408
+ // The SECOND tri-state key, and it needs this for the same reason (§4.5):
409
+ // `contextWindow: null` is AUTO ("use the model table"), not "unset", and a
410
+ // bare `null` sends the user looking for a bug that is not there.
411
+ if (key === 'contextWindow' && value === null)
412
+ return 'auto';
408
413
  return undefined;
409
414
  }
410
415
  /** Resolve a flat or one-level-dotted key against the persisted config. */
@@ -7,7 +7,7 @@
7
7
  * - Overrides: ARAGON_PROVIDER, ARAGON_MODEL, ARAGON_BASE_URL, ARAGON_THINKING,
8
8
  * ARAGON_SHOW_THINKING,
9
9
  * ARAGON_MAX_TOKENS (a number, or `auto` / `0` for the per-model ceiling),
10
- * ARAGON_THEME, ARAGON_MOUSE, ARAGON_PLAN, ARAGON_TEAM,
10
+ * ARAGON_THEME, ARAGON_MOUSE, ARAGON_PASTE, ARAGON_PLAN, ARAGON_TEAM,
11
11
  * ARAGON_TEAM_MAX, ARAGON_RETRY, ARAGON_RETRY_MAX, ARAGON_FAST,
12
12
  * ARAGON_FAST_PROVIDER, ARAGON_FAST_MODEL, ARAGON_FAST_BASE_URL,
13
13
  * ARAGON_LOG_LEVEL,
@@ -7,7 +7,7 @@
7
7
  * - Overrides: ARAGON_PROVIDER, ARAGON_MODEL, ARAGON_BASE_URL, ARAGON_THINKING,
8
8
  * ARAGON_SHOW_THINKING,
9
9
  * ARAGON_MAX_TOKENS (a number, or `auto` / `0` for the per-model ceiling),
10
- * ARAGON_THEME, ARAGON_MOUSE, ARAGON_PLAN, ARAGON_TEAM,
10
+ * ARAGON_THEME, ARAGON_MOUSE, ARAGON_PASTE, ARAGON_PLAN, ARAGON_TEAM,
11
11
  * ARAGON_TEAM_MAX, ARAGON_RETRY, ARAGON_RETRY_MAX, ARAGON_FAST,
12
12
  * ARAGON_FAST_PROVIDER, ARAGON_FAST_MODEL, ARAGON_FAST_BASE_URL,
13
13
  * ARAGON_LOG_LEVEL,
@@ -37,7 +37,7 @@ import process from 'node:process';
37
37
  import dotenv from 'dotenv';
38
38
  import { registerSecretsFrom } from '../logging/secret-registry.js';
39
39
  import { clampLogLevel } from '../logging/levels.js';
40
- import { clampMaxTokens, clampTheme, clampThinkingLevel, isAutoToken, parseThresholdInput, } from './schema.js';
40
+ import { clampContextWindow, clampMaxTokens, clampTheme, clampThinkingLevel, isAutoToken, parseThresholdInput, } from './schema.js';
41
41
  /**
42
42
  * Load a project `.env` from the given directory into `process.env` (existing
43
43
  * process env wins — dotenv never overrides already-set variables).
@@ -113,6 +113,20 @@ export function readEnvConfig() {
113
113
  partial.maxTokens = clamped;
114
114
  }
115
115
  }
116
+ // `auto` / `0` mean AUTO (`null`); a number is clamped. Anything else is LEFT
117
+ // ABSENT rather than written as `null`, the rule `ARAGON_MAX_TOKENS` states one
118
+ // block up: a typo must not silently discard the number in the config file.
119
+ const contextWindow = process.env.ARAGON_CONTEXT_WINDOW?.trim();
120
+ if (contextWindow !== undefined && contextWindow.length > 0) {
121
+ if (isAutoToken(contextWindow)) {
122
+ partial.contextWindow = null;
123
+ }
124
+ else {
125
+ const clamped = clampContextWindow(contextWindow, null);
126
+ if (clamped !== null)
127
+ partial.contextWindow = clamped;
128
+ }
129
+ }
116
130
  if (process.env.ARAGON_THEME) {
117
131
  partial.theme = clampTheme(process.env.ARAGON_THEME.trim(), 'auto');
118
132
  }
@@ -141,6 +155,13 @@ export function readEnvConfig() {
141
155
  mouseSelect === 'on' ||
142
156
  mouseSelect === 'yes';
143
157
  }
158
+ // ARAGON_PASTE=0 turns paste recognition off, which is the documented one-env
159
+ // revert to v0.6.3 input handling (tui-paste-handling section 7.2). Parsed by
160
+ // the POSITIVE list, exactly as its two neighbours above are.
161
+ const paste = process.env.ARAGON_PASTE?.trim().toLowerCase();
162
+ if (paste !== undefined && paste.length > 0) {
163
+ partial.paste = paste === '1' || paste === 'true' || paste === 'on' || paste === 'yes';
164
+ }
144
165
  // ARAGON_SCROLL_RESUME_MS=<n> — idle delay before a paused viewport returns to
145
166
  // the newest line; `0` disables auto-resume. THE ONLY CHANNEL BESIDES THE
146
167
  // CONFIG FILE, because §6.1 gives this key no CLI flag on purpose. `load.ts`
@@ -239,6 +260,46 @@ export function readEnvConfig() {
239
260
  if (Object.keys(section).length > 0)
240
261
  partial.todo = section;
241
262
  }
263
+ // ARAGON_BASH_BACKGROUND=0 turns background services off entirely (no
264
+ // `background` param, no `bash_output`/`bash_kill`, no prompt block, no chip);
265
+ // ARAGON_BASH_AUTO_BACKGROUND=0 keeps them but stops the classifier firing on
266
+ // an unset `background`. Parsed by the POSITIVE list, exactly as
267
+ // ARAGON_FULLSCREEN / ARAGON_MOUSE / ARAGON_PLAN / ARAGON_TEAM / ARAGON_TODO
268
+ // are - NOT by `envBool` below, which uses a negative list.
269
+ //
270
+ // Both pass the test every env var in this file has to pass: an unattended run
271
+ // in a container with a fixed entrypoint can reach neither a flag nor a config
272
+ // file, and "do not leave a dev server running in my CI job" is exactly the
273
+ // kind of decision that channel needs to make.
274
+ //
275
+ // ONE ACCUMULATED SECTION, ASSIGNED ONCE - the shape the `team` and `todo`
276
+ // branches above already use, and NOT two `if` blocks each assigning
277
+ // `partial.bash` (the `todo` P1-4 trap). THE CAST IS WHAT WOULD MAKE THAT
278
+ // MISTAKE SILENT: a second assignment compiles clean and drops the first key,
279
+ // so `ARAGON_BASH_BACKGROUND=0 ARAGON_BASH_AUTO_BACKGROUND=0` would quietly
280
+ // re-register the tools the user turned off.
281
+ const bashBackground = process.env.ARAGON_BASH_BACKGROUND?.trim().toLowerCase();
282
+ const bashAuto = process.env.ARAGON_BASH_AUTO_BACKGROUND?.trim().toLowerCase();
283
+ if ((bashBackground !== undefined && bashBackground.length > 0) ||
284
+ (bashAuto !== undefined && bashAuto.length > 0)) {
285
+ const section = {};
286
+ if (bashBackground !== undefined && bashBackground.length > 0) {
287
+ section.background =
288
+ bashBackground === '1' ||
289
+ bashBackground === 'true' ||
290
+ bashBackground === 'on' ||
291
+ bashBackground === 'yes';
292
+ }
293
+ if (bashAuto !== undefined && bashAuto.length > 0) {
294
+ section.autoBackground =
295
+ bashAuto === '1' || bashAuto === 'true' || bashAuto === 'on' || bashAuto === 'yes';
296
+ }
297
+ // A PARTIAL section, safe only because every consumer deep-merges it - the
298
+ // same contract `partial.skills`, `partial.log`, `partial.team` and
299
+ // `partial.todo` rely on.
300
+ if (Object.keys(section).length > 0)
301
+ partial.bash = section;
302
+ }
242
303
  // ARAGON_RETRY=0 is the kill switch for API retry; ARAGON_RETRY_MAX=<n>
243
304
  // overrides the count. Parsed by the POSITIVE list, exactly as
244
305
  // ARAGON_FULLSCREEN / ARAGON_MOUSE / ARAGON_PLAN / ARAGON_TEAM / ARAGON_TODO
@@ -49,6 +49,17 @@ export interface CliFlags {
49
49
  * `false` on every run (the `--todo-panel` trap).
50
50
  */
51
51
  mouseSelect?: boolean;
52
+ /**
53
+ * `--paste` ⇒ true, `--no-paste` ⇒ false, absent ⇒ undefined
54
+ * (tui-paste-handling section 7.1).
55
+ *
56
+ * DECLARED AS A PAIR, and carried through `toFlags`, for the reason the two
57
+ * notes above record. `paste` is additionally PERSISTED and defaults to
58
+ * `true`, so a lone `--no-paste` would make commander default `opts.paste` to
59
+ * `true` -- indistinguishable from silence, and silently overriding a stored
60
+ * `false` on every run that passed no flag at all.
61
+ */
62
+ paste?: boolean;
52
63
  /** `--no-skills` ⇒ false. Turns the whole skill subsystem off (§7.5). */
53
64
  skills?: boolean;
54
65
  /** `--skill <name>` ×N — force Level 2 injection for this run. */
@@ -174,6 +185,23 @@ export interface CliFlags {
174
185
  * to keep in step with `toFlags`.
175
186
  */
176
187
  fastReview?: string;
188
+ /**
189
+ * `--fast-delegate` ⇒ true, `--no-fast-delegate` ⇒ false, absent ⇒ undefined
190
+ * (web-use-tier-cooperation-and-control-closure §4.1.5).
191
+ *
192
+ * DECLARED AS A PAIR — MANDATORY, for the reason `--team`, `--todo-panel`,
193
+ * `--render-governor`, `--retry` and `--fast` each record in turn, and here
194
+ * with the SAME polarity as `--update`: `fast.delegate` is PERSISTED and
195
+ * defaults to `true`, so a lone `--no-fast-delegate` would make commander
196
+ * default `opts.fastDelegate` to `true` and silently overwrite a stored
197
+ * `false` on every run that passed no flag at all.
198
+ *
199
+ * It exists because it is the ONLY `fast.*` sub-switch with no channel a
200
+ * wrapper can reach: `--fast-review <n|off>` already covers `review` and
201
+ * `reviewEveryTurns`, and `enabled` / `model` / `provider` have both a flag
202
+ * and an environment variable.
203
+ */
204
+ fastDelegate?: boolean;
177
205
  /**
178
206
  * `--update` ⇒ true, `--no-update` ⇒ false, absent ⇒ undefined
179
207
  * (cli-auto-update §4.2).
@@ -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, clampCompactionConfig, clampLogConfig, clampFastConfig, clampRetryConfig, clampSkillsConfig, clampTeamConfig, clampTodoConfig, clampUpdateConfig, clampTheme, clampThinkingLevel, clampMaxTokens, clampMaxRenderInterval, clampScrollResumeMs, clampTranscriptRetain, clampTranscriptWindow, coercePositiveInt, isAutoToken, isSkillsToolPolicyMode, parseThresholdInput, } from './schema.js';
9
+ import { DEFAULT_CONFIG, IDLE_TIMEOUT_MARGIN_MS, clampAskRounds, clampDensity, clampHumanTimeout, clampCompactionConfig, clampLogConfig, clampFastConfig, clampRetryConfig, clampSkillsConfig, clampTeamConfig, clampTodoConfig, clampBashConfig, clampUpdateConfig, clampTheme, clampThinkingLevel, clampContextWindow, 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';
@@ -80,6 +80,24 @@ function resolveMouseSelect(flags, env, file) {
80
80
  return file.mouseSelect;
81
81
  return DEFAULT_CONFIG.mouseSelect;
82
82
  }
83
+ /**
84
+ * Paste handling: flag > env > file > default `true` (tui-paste-handling
85
+ * section 7.4).
86
+ *
87
+ * SHAPED EXACTLY LIKE `resolveMouse` above, for the reason its own note gives:
88
+ * reaching for the `hints` shape -- `flags.x !== undefined ? ... : file.x ??
89
+ * DEFAULT`, which reads NO ENV AT ALL -- would ship `ARAGON_PASTE` as
90
+ * documented-but-dead.
91
+ */
92
+ function resolvePaste(flags, env, file) {
93
+ if (flags.paste !== undefined)
94
+ return flags.paste;
95
+ if (env.paste !== undefined)
96
+ return env.paste;
97
+ if (file.paste !== undefined)
98
+ return file.paste;
99
+ return DEFAULT_CONFIG.paste;
100
+ }
83
101
  /**
84
102
  * Idle scroll-resume delay: env › file › default 5000, clamped to [0, 120000].
85
103
  *
@@ -153,6 +171,24 @@ function resolveTodoConfig(flags, env, file) {
153
171
  ...(flags.todoFollow !== undefined ? { followThrough: flags.todoFollow } : {}),
154
172
  });
155
173
  }
174
+ /**
175
+ * Merge the `bash` section across the three layers that supply it
176
+ * (defaults > file > env), background-service-supervision §5.4.
177
+ *
178
+ * THREE LAYERS, NOT FOUR, AND THAT IS NOT AN OVERSIGHT. There is deliberately no
179
+ * `--no-background` flag: the two switches this section carries are already
180
+ * reachable from `config.json` (a per-machine preference) and from the
181
+ * environment (the channel a container entrypoint has), which is the two-part
182
+ * test every knob in this file is held to. A third spelling would be a third
183
+ * place for the answer to differ.
184
+ */
185
+ function resolveBashConfig(env, file) {
186
+ return clampBashConfig({
187
+ ...DEFAULT_CONFIG.bash,
188
+ ...(file.bash ?? {}),
189
+ ...(env.bash ?? {}),
190
+ });
191
+ }
156
192
  /**
157
193
  * Merge the `retry` section across all four layers (defaults › file › env ›
158
194
  * flags), llm-api-retry-backoff §6.2.
@@ -214,6 +250,12 @@ function resolveFastConfig(flags, env, file) {
214
250
  ...(flags.fastProvider !== undefined && flags.fastProvider !== ''
215
251
  ? { provider: flags.fastProvider }
216
252
  : {}),
253
+ // `!== undefined`, for the reason stated on `flags.fast` above: commander
254
+ // materialises a lone `--no-fast-delegate` as `opts.fastDelegate = true`
255
+ // when the flag is absent, and `delegate` defaults to TRUE — so a truthiness
256
+ // check would force it on for every run that passed no flag at all,
257
+ // overwriting a stored `false` with nothing to show for it.
258
+ ...(flags.fastDelegate !== undefined ? { delegate: flags.fastDelegate } : {}),
217
259
  ...reviewPatch,
218
260
  });
219
261
  }
@@ -374,6 +416,42 @@ function resolveMaxTokens(flags, env, file) {
374
416
  }
375
417
  return DEFAULT_CONFIG.maxTokens ?? undefined;
376
418
  }
419
+ /**
420
+ * Resolve the context-window override: env > file > default, where an EXPLICIT
421
+ * `null` at either layer means AUTO (context-usage-gauge-accuracy §3.6).
422
+ *
423
+ * IT CANNOT USE `pick()` FOR THE REASON `resolveMaxTokens` GIVES: `pick` skips
424
+ * `null`, and `null` is exactly the value being looked for. NO CLI FLAG feeds
425
+ * this - it is a per-installation correction for a model table entry, not a
426
+ * per-invocation choice, so `config set` and the environment variable are the
427
+ * two surfaces the design names.
428
+ */
429
+ function resolveContextWindow(env, file) {
430
+ const layers = [
431
+ {
432
+ name: 'ARAGON_CONTEXT_WINDOW',
433
+ value: hasOwn(env, 'contextWindow') ? env.contextWindow : undefined,
434
+ },
435
+ {
436
+ name: 'config.json contextWindow',
437
+ value: hasOwn(file, 'contextWindow') ? file.contextWindow : undefined,
438
+ },
439
+ ];
440
+ for (const layer of layers) {
441
+ if (layer.value === undefined)
442
+ continue; // this layer said nothing
443
+ if (layer.value === null || isAutoToken(layer.value))
444
+ return null; // AUTO
445
+ const n = clampContextWindow(layer.value, null);
446
+ if (n !== null)
447
+ return n;
448
+ getLogger().warn('config', 'context_window_unusable', {
449
+ source: layer.name,
450
+ value: String(layer.value),
451
+ });
452
+ }
453
+ return DEFAULT_CONFIG.contextWindow;
454
+ }
377
455
  /**
378
456
  * Resolve the effective config by merging all layers, then derive the runtime
379
457
  * (non-persisted) fields and enforce the timeout invariant.
@@ -409,6 +487,7 @@ export function loadConfig(flags = {}) {
409
487
  const liveToolOutput = pick(flags.liveToolOutput, env.partial.liveToolOutput, file.liveToolOutput) ??
410
488
  DEFAULT_CONFIG.liveToolOutput;
411
489
  const maxTokens = resolveMaxTokens(flags, env.partial, file);
490
+ const contextWindow = resolveContextWindow(env.partial, file);
412
491
  // `clampTheme` is also where the v0.3.0 `dark` name is migrated to `cool`,
413
492
  // so reading a legacy config file needs no extra step here (§4.5 / R-4).
414
493
  const theme = clampTheme(pick(flags.theme, env.partial.theme, file.theme), DEFAULT_CONFIG.theme);
@@ -498,6 +577,7 @@ export function loadConfig(flags = {}) {
498
577
  showThinking,
499
578
  liveToolOutput,
500
579
  maxTokens,
580
+ contextWindow,
501
581
  theme,
502
582
  reducedMotion,
503
583
  fullscreen: resolveFullscreen(flags, env.partial, file),
@@ -520,6 +600,7 @@ export function loadConfig(flags = {}) {
520
600
  hints,
521
601
  mouse: resolveMouse(flags, env.partial, file),
522
602
  mouseSelect: resolveMouseSelect(flags, env.partial, file),
603
+ paste: resolvePaste(flags, env.partial, file),
523
604
  scrollResumeMs: resolveScrollResumeMs(env.partial, file),
524
605
  // From `<home>/state.json`, not the config file — see `ui-state.ts`.
525
606
  submitCount: readSubmitCount(),
@@ -545,6 +626,7 @@ export function loadConfig(flags = {}) {
545
626
  fast: resolveFastConfig(flags, env.partial, file),
546
627
  update: resolveUpdateConfig(flags, env.partial, file),
547
628
  compaction: resolveCompactionConfig(flags, env.partial, file),
629
+ bash: resolveBashConfig(env.partial, file),
548
630
  cwd,
549
631
  color,
550
632
  colorLevel,