@aragon-agent/cli 0.5.6 → 0.5.9

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 (156) hide show
  1. package/CHANGELOG.md +356 -0
  2. package/README.md +1429 -1105
  3. package/dist/agent/coalesce.d.ts +13 -0
  4. package/dist/agent/coalesce.js +50 -11
  5. package/dist/agent/controller.d.ts +141 -2
  6. package/dist/agent/controller.js +396 -4
  7. package/dist/agent/entry-limits.d.ts +65 -0
  8. package/dist/agent/entry-limits.js +133 -0
  9. package/dist/agent/headless.d.ts +8 -0
  10. package/dist/agent/headless.js +40 -0
  11. package/dist/agent/reducer.d.ts +253 -3
  12. package/dist/agent/reducer.js +553 -28
  13. package/dist/agent/retry-view.d.ts +68 -0
  14. package/dist/agent/retry-view.js +95 -0
  15. package/dist/agent/system-prompt.d.ts +11 -0
  16. package/dist/agent/system-prompt.js +2 -0
  17. package/dist/cli.js +122 -4
  18. package/dist/commands/builtins.d.ts +1 -1
  19. package/dist/commands/builtins.js +313 -2
  20. package/dist/commands/perf.d.ts +45 -0
  21. package/dist/commands/perf.js +78 -0
  22. package/dist/config/cli-commands.d.ts +53 -2
  23. package/dist/config/cli-commands.js +136 -3
  24. package/dist/config/env.d.ts +6 -1
  25. package/dist/config/env.js +131 -1
  26. package/dist/config/load.d.ts +63 -0
  27. package/dist/config/load.js +125 -2
  28. package/dist/config/schema.d.ts +273 -10
  29. package/dist/config/schema.js +250 -7
  30. package/dist/config/store.js +20 -1
  31. package/dist/fast/critique.d.ts +43 -0
  32. package/dist/fast/critique.js +62 -0
  33. package/dist/fast/digest.d.ts +42 -0
  34. package/dist/fast/digest.js +85 -0
  35. package/dist/fast/frames.d.ts +99 -0
  36. package/dist/fast/frames.js +180 -0
  37. package/dist/fast/limits.d.ts +98 -0
  38. package/dist/fast/limits.js +98 -0
  39. package/dist/fast/prompt.d.ts +72 -0
  40. package/dist/fast/prompt.js +94 -0
  41. package/dist/fast/resolve.d.ts +43 -0
  42. package/dist/fast/resolve.js +105 -0
  43. package/dist/fast/review-call.d.ts +99 -0
  44. package/dist/fast/review-call.js +130 -0
  45. package/dist/fast/reviewer.d.ts +248 -0
  46. package/dist/fast/reviewer.js +664 -0
  47. package/dist/fast/types.d.ts +128 -0
  48. package/dist/fast/types.js +14 -0
  49. package/dist/fast/wiring.d.ts +170 -0
  50. package/dist/fast/wiring.js +316 -0
  51. package/dist/logging/install.d.ts +15 -1
  52. package/dist/logging/install.js +37 -4
  53. package/dist/logging/logger.d.ts +1 -1
  54. package/dist/session/persist.js +57 -1
  55. package/dist/team/normalize.d.ts +26 -1
  56. package/dist/team/normalize.js +23 -3
  57. package/dist/team/prompt.d.ts +11 -0
  58. package/dist/team/prompt.js +10 -0
  59. package/dist/team/report.d.ts +18 -0
  60. package/dist/team/report.js +27 -1
  61. package/dist/team/runtime.d.ts +13 -2
  62. package/dist/team/runtime.js +33 -6
  63. package/dist/team/subagent.d.ts +19 -1
  64. package/dist/team/subagent.js +72 -10
  65. package/dist/team/task-tool.d.ts +26 -0
  66. package/dist/team/task-tool.js +48 -4
  67. package/dist/team/types.d.ts +80 -1
  68. package/dist/tools/bash-tool.js +19 -3
  69. package/dist/tools/diff.d.ts +25 -5
  70. package/dist/tools/diff.js +72 -41
  71. package/dist/tools/file-change-store.d.ts +34 -0
  72. package/dist/tools/file-change-store.js +57 -0
  73. package/dist/tools/fs-tools.d.ts +26 -0
  74. package/dist/tools/fs-tools.js +62 -5
  75. package/dist/tools/index.d.ts +31 -0
  76. package/dist/tools/index.js +7 -1
  77. package/dist/tools/patch.d.ts +115 -0
  78. package/dist/tools/patch.js +453 -0
  79. package/dist/tools/terminal-output.d.ts +107 -0
  80. package/dist/tools/terminal-output.js +198 -0
  81. package/dist/tools/tool-output-store.d.ts +68 -0
  82. package/dist/tools/tool-output-store.js +102 -0
  83. package/dist/ui/ActivityLine.d.ts +48 -0
  84. package/dist/ui/ActivityLine.js +22 -0
  85. package/dist/ui/App.d.ts +1 -4
  86. package/dist/ui/App.js +357 -44
  87. package/dist/ui/BottomStatusRow.d.ts +43 -0
  88. package/dist/ui/BottomStatusRow.js +14 -0
  89. package/dist/ui/Composer.d.ts +1 -6
  90. package/dist/ui/Composer.js +2 -2
  91. package/dist/ui/Markdown.d.ts +7 -1
  92. package/dist/ui/Markdown.js +15 -48
  93. package/dist/ui/PromptInput.d.ts +1 -15
  94. package/dist/ui/PromptInput.js +1 -38
  95. package/dist/ui/StatusBar.d.ts +50 -0
  96. package/dist/ui/StatusBar.js +7 -3
  97. package/dist/ui/TeamPanel.d.ts +5 -4
  98. package/dist/ui/TeamPanel.js +1 -1
  99. package/dist/ui/TodoPanel.d.ts +5 -4
  100. package/dist/ui/Transcript.d.ts +80 -5
  101. package/dist/ui/Transcript.js +179 -11
  102. package/dist/ui/activity-phrases.d.ts +33 -0
  103. package/dist/ui/activity-phrases.js +50 -0
  104. package/dist/ui/entries/AssistantEntry.d.ts +38 -1
  105. package/dist/ui/entries/AssistantEntry.js +34 -3
  106. package/dist/ui/entries/DiffView.d.ts +67 -0
  107. package/dist/ui/entries/DiffView.js +103 -0
  108. package/dist/ui/entries/FastCard.d.ts +53 -0
  109. package/dist/ui/entries/FastCard.js +87 -0
  110. package/dist/ui/entries/RetryCard.d.ts +56 -0
  111. package/dist/ui/entries/RetryCard.js +60 -0
  112. package/dist/ui/entries/TeamCard.d.ts +9 -1
  113. package/dist/ui/entries/TeamCard.js +28 -2
  114. package/dist/ui/entries/TodoCard.d.ts +9 -1
  115. package/dist/ui/entries/TodoCard.js +23 -1
  116. package/dist/ui/entries/ToolCard.d.ts +50 -1
  117. package/dist/ui/entries/ToolCard.js +104 -7
  118. package/dist/ui/entries/ToolPreview.d.ts +2 -2
  119. package/dist/ui/entries/UserEntry.d.ts +9 -1
  120. package/dist/ui/entries/UserEntry.js +17 -1
  121. package/dist/ui/exit-snapshot.d.ts +7 -0
  122. package/dist/ui/glyphs.d.ts +9 -0
  123. package/dist/ui/glyphs.js +2 -0
  124. package/dist/ui/layout/AppShell.d.ts +13 -22
  125. package/dist/ui/layout/AppShell.js +3 -38
  126. package/dist/ui/layout/MeasuredEntry.d.ts +21 -0
  127. package/dist/ui/layout/MeasuredEntry.js +30 -0
  128. package/dist/ui/layout/ScrollViewport.js +8 -2
  129. package/dist/ui/layout/viewport-geometry.d.ts +28 -0
  130. package/dist/ui/layout/viewport-geometry.js +27 -0
  131. package/dist/ui/layout/virtual-window.d.ts +100 -0
  132. package/dist/ui/layout/virtual-window.js +308 -0
  133. package/dist/ui/live-clamp.d.ts +42 -0
  134. package/dist/ui/live-clamp.js +74 -0
  135. package/dist/ui/markdown-blocks.d.ts +25 -1
  136. package/dist/ui/markdown-blocks.js +43 -0
  137. package/dist/ui/overlays/HelpOverlay.js +16 -2
  138. package/dist/ui/overlays/SettingsScreen.d.ts +82 -1
  139. package/dist/ui/overlays/SettingsScreen.js +147 -1
  140. package/dist/ui/render-cache.d.ts +81 -0
  141. package/dist/ui/render-cache.js +195 -0
  142. package/dist/ui/render-governor.d.ts +54 -0
  143. package/dist/ui/render-governor.js +78 -0
  144. package/dist/ui/transcript-text.d.ts +11 -0
  145. package/dist/ui/transcript-text.js +32 -0
  146. package/dist/ui/use-height-store.d.ts +31 -0
  147. package/dist/ui/use-height-store.js +76 -0
  148. package/dist/ui/use-render-governor.d.ts +39 -0
  149. package/dist/ui/use-render-governor.js +57 -0
  150. package/dist/ui/use-startup-notices.d.ts +9 -1
  151. package/dist/ui/use-startup-notices.js +35 -1
  152. package/dist/ui/use-wheel-routing.d.ts +16 -32
  153. package/dist/ui/use-wheel-routing.js +21 -38
  154. package/package.json +2 -2
  155. package/dist/ui/layout/regions.d.ts +0 -55
  156. package/dist/ui/layout/regions.js +0 -52
@@ -1,6 +1,6 @@
1
1
  /**
2
- * `aragon config get | list | edit | home`, plus the `log.*` and `team.*`
3
- * branches of `config set`.
2
+ * `aragon config get | list | edit | home`, plus the `log.*`, `team.*`,
3
+ * `retry.*` and `fast.*` branches of `config set`.
4
4
  *
5
5
  * These live here rather than in `cli.tsx` because `runConfigSet` is already a
6
6
  * 21-case switch — well past the complexity ceiling this package writes down —
@@ -13,7 +13,7 @@ import process from 'node:process';
13
13
  import { getConfigBackupPath, getConfigPath, getHomeRoot, getLogsDir, getPromptHistoryPath, getUiStatePath, isHomeOverridden, } from './app-paths.js';
14
14
  import { getHomeMigrationBreadcrumbPath } from './migrate-home.js';
15
15
  import { loadPersistedConfig, updatePersistedConfig } from './store.js';
16
- import { coercePositiveInt, maskSecret, DEFAULT_TEAM_CONFIG, } from './schema.js';
16
+ import { clampFastConfig, clampRetryConfig, coercePositiveInt, maskSecret, DEFAULT_FAST_CONFIG, DEFAULT_RETRY_CONFIG, DEFAULT_TEAM_CONFIG, } from './schema.js';
17
17
  import { clampLogLevel } from '../logging/levels.js';
18
18
  /** Config keys under `log.` that `aragon config set` accepts. */
19
19
  export const LOG_CONFIG_SET_KEYS = [
@@ -43,6 +43,50 @@ export const TEAM_CONFIG_SET_KEYS = [
43
43
  'team.dispatchTimeoutMs',
44
44
  'team.maxTurnsPerSubagent',
45
45
  ];
46
+ /**
47
+ * Config keys under `retry.` that `aragon config set` accepts
48
+ * (llm-api-retry-backoff §8.2).
49
+ *
50
+ * Handled here rather than in `cli.tsx`'s switch for the reason this module's
51
+ * header gives about `log.*` and `team.*`: that switch is past this package's
52
+ * complexity ceiling, and a key added to the membership set but forgotten in the
53
+ * switch falls through every case, writes nothing, and still prints
54
+ * `Set retry.maxRetries = 0`.
55
+ */
56
+ export const RETRY_CONFIG_SET_KEYS = [
57
+ 'retry.enabled',
58
+ 'retry.maxRetries',
59
+ 'retry.initialDelayMs',
60
+ 'retry.maxDelayMs',
61
+ 'retry.multiplier',
62
+ 'retry.jitter',
63
+ 'retry.respectRetryAfter',
64
+ 'retry.maxElapsedMs',
65
+ 'retry.onPartialStream',
66
+ ];
67
+ /**
68
+ * Config keys under `fast.` that `aragon config set` accepts (fast-model-tier
69
+ * §4.2).
70
+ *
71
+ * A DEDICATED SETTER RATHER THAN A GENERIC DOTTED WRITER, for the reason the
72
+ * three sets above each record: a generic setter would write `fast` as a FLAT
73
+ * string key (`"fast.model": "haiku"` at the top level of the file), the section
74
+ * would silently never take effect, and `config set` would still print
75
+ * `Set fast.model = haiku`.
76
+ */
77
+ export const FAST_CONFIG_SET_KEYS = [
78
+ 'fast.enabled',
79
+ 'fast.provider',
80
+ 'fast.model',
81
+ 'fast.baseUrl',
82
+ 'fast.thinkingLevel',
83
+ 'fast.delegate',
84
+ 'fast.review',
85
+ 'fast.reviewEveryTurns',
86
+ 'fast.reviewContextTurns',
87
+ 'fast.reviewMaxChars',
88
+ 'fast.reviewMaxPerSession',
89
+ ];
46
90
  function isTrue(value) {
47
91
  return value === 'true' || value === '1';
48
92
  }
@@ -112,6 +156,95 @@ export function applyTeamConfigSet(key, value) {
112
156
  return null;
113
157
  }
114
158
  }
159
+ /**
160
+ * Translate a `retry.*` dotted key into a patch, or `null` for anything else.
161
+ *
162
+ * The patch is a PARTIAL `retry` section, safe only because
163
+ * `updatePersistedConfig` deep-merges that section — with a shallow merge,
164
+ * setting the retry count would silently reset `respectRetryAfter` and `jitter`.
165
+ *
166
+ * `maxRetries` GOES THROUGH `clampRetryConfig`, NOT `coercePositiveInt`, and that
167
+ * is not a style choice (R-16): `coercePositiveInt` returns its fallback for
168
+ * `n <= 0`, so `config set retry.maxRetries 0` would write **10** — the maximum
169
+ * instead of the kill switch, silently. The whole section is routed through the
170
+ * one gate so no key here can pick up the wrong coercion later; the caller echoes
171
+ * the STORED value, which is what makes an out-of-range input visible.
172
+ */
173
+ export function applyRetryConfigSet(key, value) {
174
+ const patch = (retry) => ({ retry });
175
+ const clampOne = (field) => {
176
+ const merged = clampRetryConfig({ ...DEFAULT_RETRY_CONFIG, [field]: value });
177
+ return { [field]: merged[field] };
178
+ };
179
+ switch (key) {
180
+ case 'retry.enabled':
181
+ return patch({ enabled: isTrue(value) });
182
+ case 'retry.maxRetries':
183
+ return patch(clampOne('maxRetries'));
184
+ case 'retry.initialDelayMs':
185
+ return patch(clampOne('initialDelayMs'));
186
+ case 'retry.maxDelayMs':
187
+ return patch(clampOne('maxDelayMs'));
188
+ case 'retry.multiplier':
189
+ return patch(clampOne('multiplier'));
190
+ case 'retry.jitter':
191
+ return patch({ jitter: isTrue(value) });
192
+ case 'retry.respectRetryAfter':
193
+ return patch({ respectRetryAfter: isTrue(value) });
194
+ case 'retry.maxElapsedMs':
195
+ return patch(clampOne('maxElapsedMs'));
196
+ case 'retry.onPartialStream':
197
+ return patch({ onPartialStream: isTrue(value) });
198
+ default:
199
+ return null;
200
+ }
201
+ }
202
+ /**
203
+ * Translate a `fast.*` dotted key into a patch, or `null` for anything else.
204
+ *
205
+ * The patch is a PARTIAL `fast` section, safe only because
206
+ * `updatePersistedConfig` deep-merges that section — with a shallow merge,
207
+ * setting the cadence would silently switch the whole tier off (R-10).
208
+ *
209
+ * EVERY NUMERIC AND ENUM KEY ROUTES THROUGH `clampFastConfig`, not through
210
+ * `coercePositiveInt` or a local parse, for the reason `applyRetryConfigSet`
211
+ * records: one gate for both directions means no key here can pick up a
212
+ * different coercion later, and `config set fast.reviewEveryTurns 400` writes
213
+ * the clamped 50 while the caller echoes the STORED value (AC-13).
214
+ */
215
+ export function applyFastConfigSet(key, value) {
216
+ const patch = (fast) => ({ fast });
217
+ const clampOne = (field) => {
218
+ const merged = clampFastConfig({ ...DEFAULT_FAST_CONFIG, [field]: value });
219
+ return { [field]: merged[field] };
220
+ };
221
+ switch (key) {
222
+ case 'fast.enabled':
223
+ return patch({ enabled: isTrue(value) });
224
+ case 'fast.provider':
225
+ return patch(clampOne('provider'));
226
+ case 'fast.model':
227
+ return patch(clampOne('model'));
228
+ case 'fast.baseUrl':
229
+ return patch(clampOne('baseUrl'));
230
+ case 'fast.thinkingLevel':
231
+ return patch(clampOne('thinkingLevel'));
232
+ case 'fast.delegate':
233
+ return patch({ delegate: isTrue(value) });
234
+ case 'fast.review':
235
+ return patch({ review: isTrue(value) });
236
+ case 'fast.reviewEveryTurns':
237
+ return patch(clampOne('reviewEveryTurns'));
238
+ case 'fast.reviewContextTurns':
239
+ return patch(clampOne('reviewContextTurns'));
240
+ case 'fast.reviewMaxChars':
241
+ return patch(clampOne('reviewMaxChars'));
242
+ case 'fast.reviewMaxPerSession':
243
+ return patch(clampOne('reviewMaxPerSession'));
244
+ default:
245
+ return null;
246
+ }
247
+ }
115
248
  /**
116
249
  * `log.redactSecrets false` is the one setting that lets credentials reach the
117
250
  * disk. It is command-line-only (there is no settings-screen entry) and it says
@@ -5,9 +5,14 @@
5
5
  * - Provider secrets: ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_API_KEY /
6
6
  * GEMINI_API_KEY.
7
7
  * - Overrides: ARAGON_PROVIDER, ARAGON_MODEL, ARAGON_BASE_URL, ARAGON_THINKING,
8
+ * ARAGON_SHOW_THINKING,
8
9
  * ARAGON_MAX_TOKENS (a number, or `auto` / `0` for the per-model ceiling),
9
10
  * ARAGON_THEME, ARAGON_MOUSE, ARAGON_PLAN, ARAGON_TEAM,
10
- * ARAGON_TEAM_MAX, ARAGON_LOG_LEVEL, ARAGON_LOG_FILE, ARAGON_LOG_DIR.
11
+ * ARAGON_TEAM_MAX, ARAGON_RETRY, ARAGON_RETRY_MAX, ARAGON_FAST,
12
+ * ARAGON_FAST_PROVIDER, ARAGON_FAST_MODEL, ARAGON_FAST_BASE_URL,
13
+ * ARAGON_LOG_LEVEL,
14
+ * ARAGON_LOG_FILE, ARAGON_LOG_DIR, ARAGON_TRANSCRIPT_RETAIN,
15
+ * ARAGON_RENDER_GOVERNOR, ARAGON_MAX_RENDER_INTERVAL_MS.
11
16
  *
12
17
  * NOT here: `ARAGON_HOME`. It is read when `config/app-paths.ts` is first
13
18
  * evaluated, which is long before `loadDotenv()` runs, so a `.env` can never
@@ -5,9 +5,14 @@
5
5
  * - Provider secrets: ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_API_KEY /
6
6
  * GEMINI_API_KEY.
7
7
  * - Overrides: ARAGON_PROVIDER, ARAGON_MODEL, ARAGON_BASE_URL, ARAGON_THINKING,
8
+ * ARAGON_SHOW_THINKING,
8
9
  * ARAGON_MAX_TOKENS (a number, or `auto` / `0` for the per-model ceiling),
9
10
  * ARAGON_THEME, ARAGON_MOUSE, ARAGON_PLAN, ARAGON_TEAM,
10
- * ARAGON_TEAM_MAX, ARAGON_LOG_LEVEL, ARAGON_LOG_FILE, ARAGON_LOG_DIR.
11
+ * ARAGON_TEAM_MAX, ARAGON_RETRY, ARAGON_RETRY_MAX, ARAGON_FAST,
12
+ * ARAGON_FAST_PROVIDER, ARAGON_FAST_MODEL, ARAGON_FAST_BASE_URL,
13
+ * ARAGON_LOG_LEVEL,
14
+ * ARAGON_LOG_FILE, ARAGON_LOG_DIR, ARAGON_TRANSCRIPT_RETAIN,
15
+ * ARAGON_RENDER_GOVERNOR, ARAGON_MAX_RENDER_INTERVAL_MS.
11
16
  *
12
17
  * NOT here: `ARAGON_HOME`. It is read when `config/app-paths.ts` is first
13
18
  * evaluated, which is long before `loadDotenv()` runs, so a `.env` can never
@@ -59,6 +64,27 @@ export function readEnvConfig() {
59
64
  if (process.env.ARAGON_THINKING) {
60
65
  partial.thinkingLevel = clampThinkingLevel(process.env.ARAGON_THINKING.trim(), 'off');
61
66
  }
67
+ // Parsed by the POSITIVE list, like ARAGON_FULLSCREEN: anything else is left
68
+ // ABSENT so the config file underneath still wins.
69
+ const showThinking = process.env.ARAGON_SHOW_THINKING?.trim().toLowerCase();
70
+ if (showThinking !== undefined && showThinking.length > 0) {
71
+ partial.showThinking =
72
+ showThinking === '1' ||
73
+ showThinking === 'true' ||
74
+ showThinking === 'on' ||
75
+ showThinking === 'yes';
76
+ }
77
+ // The same positive-list parse as its neighbour, and the same "a non-empty
78
+ // value is an ANSWER" rule -- which is what lets `ARAGON_LIVE_TOOL_OUTPUT=0`
79
+ // turn a default-ON feature off (agent-activity-presentation-live §4.1).
80
+ const liveToolOutput = process.env.ARAGON_LIVE_TOOL_OUTPUT?.trim().toLowerCase();
81
+ if (liveToolOutput !== undefined && liveToolOutput.length > 0) {
82
+ partial.liveToolOutput =
83
+ liveToolOutput === '1' ||
84
+ liveToolOutput === 'true' ||
85
+ liveToolOutput === 'on' ||
86
+ liveToolOutput === 'yes';
87
+ }
62
88
  // `auto` / `0` mean AUTO (`null`); a number is clamped to the accepted range.
63
89
  // Anything else is LEFT ABSENT rather than written as `null`: a typo here used
64
90
  // to become AUTO and silently discard the number in the config file under it,
@@ -177,6 +203,110 @@ export function readEnvConfig() {
177
203
  if (Object.keys(section).length > 0)
178
204
  partial.todo = section;
179
205
  }
206
+ // ARAGON_RETRY=0 is the kill switch for API retry; ARAGON_RETRY_MAX=<n>
207
+ // overrides the count. Parsed by the POSITIVE list, exactly as
208
+ // ARAGON_FULLSCREEN / ARAGON_MOUSE / ARAGON_PLAN / ARAGON_TEAM / ARAGON_TODO
209
+ // are — NOT by `envBool` below, which uses a negative list.
210
+ //
211
+ // TWO VARS ONLY, and no third for the delays: every env var in this file exists
212
+ // because a flag and a config file are BOTH unreachable in the channel it
213
+ // serves, and nobody tunes a backoff multiplier from a container entrypoint.
214
+ //
215
+ // `>= 0`, NOT `> 0`, and the difference is the whole point (R-16).
216
+ // `ARAGON_TEAM_MAX` above guards `parsed > 0`, which is right for a fan-out
217
+ // width and wrong here: `0` is this key's documented floor AND its kill switch,
218
+ // so a `> 0` guard would drop `ARAGON_RETRY_MAX=0` on the floor and leave the
219
+ // user with the default 10. "Left ABSENT when unparseable" still holds and
220
+ // still matters — an unparseable value must not look permanently supplied, or
221
+ // the config file can never win again.
222
+ const retry = process.env.ARAGON_RETRY?.trim().toLowerCase();
223
+ const retryMax = process.env.ARAGON_RETRY_MAX?.trim();
224
+ if ((retry !== undefined && retry.length > 0) || (retryMax !== undefined && retryMax.length > 0)) {
225
+ const section = {};
226
+ if (retry !== undefined && retry.length > 0) {
227
+ section.enabled = retry === '1' || retry === 'true' || retry === 'on' || retry === 'yes';
228
+ }
229
+ if (retryMax !== undefined && retryMax.length > 0) {
230
+ const parsed = Number.parseInt(retryMax, 10);
231
+ if (Number.isFinite(parsed) && parsed >= 0)
232
+ section.maxRetries = parsed;
233
+ }
234
+ // A PARTIAL section, safe only because every consumer deep-merges it — the
235
+ // same contract `partial.skills` / `partial.log` / `partial.team` rely on.
236
+ if (Object.keys(section).length > 0)
237
+ partial.retry = section;
238
+ }
239
+ // ARAGON_FAST=1 turns the fast model tier on; the other three name it
240
+ // (fast-model-tier §4.3). Parsed by the POSITIVE list, exactly as
241
+ // ARAGON_FULLSCREEN / ARAGON_MOUSE / ARAGON_PLAN / ARAGON_TEAM / ARAGON_TODO /
242
+ // ARAGON_RETRY are — NOT by `envBool` below, whose NEGATIVE list disagrees with
243
+ // it on values like `ARAGON_FAST=disable`.
244
+ //
245
+ // FOUR VARS, AND DELIBERATELY NO `ARAGON_FAST_REVIEW_EVERY` / `_MAX_CHARS` /
246
+ // `_DELEGATE` (D-14). The rule this file states is that an environment variable
247
+ // exists only where a flag AND a config file are BOTH unreachable — a
248
+ // container, an ssh session, being spawned by another tool. "Which model, and
249
+ // is it on" passes that test; tuning a review cadence does not, and every key
250
+ // that fails it is documentation debt plus a fifth place for the value to
251
+ // disagree with itself.
252
+ //
253
+ // ONE ACCUMULATED SECTION, ASSIGNED ONCE — the shape the `team`, `todo` and
254
+ // `retry` branches above all use, and NOT four `if` blocks each assigning
255
+ // `partial.fast`. THE CAST IS WHAT WOULD MAKE THAT MISTAKE SILENT: a second
256
+ // assignment compiles clean and drops `enabled`, so
257
+ // `ARAGON_FAST=1 ARAGON_FAST_MODEL=x` would quietly resolve with the tier off
258
+ // (the P1-4 defect this file records for `todo`).
259
+ const fast = process.env.ARAGON_FAST?.trim().toLowerCase();
260
+ const fastProvider = process.env.ARAGON_FAST_PROVIDER?.trim();
261
+ const fastModel = process.env.ARAGON_FAST_MODEL?.trim();
262
+ const fastBaseUrl = process.env.ARAGON_FAST_BASE_URL?.trim();
263
+ if ((fast !== undefined && fast.length > 0) ||
264
+ (fastProvider !== undefined && fastProvider.length > 0) ||
265
+ (fastModel !== undefined && fastModel.length > 0) ||
266
+ (fastBaseUrl !== undefined && fastBaseUrl.length > 0)) {
267
+ const section = {};
268
+ if (fast !== undefined && fast.length > 0) {
269
+ section.enabled = fast === '1' || fast === 'true' || fast === 'on' || fast === 'yes';
270
+ }
271
+ if (fastProvider !== undefined && fastProvider.length > 0)
272
+ section.provider = fastProvider;
273
+ if (fastModel !== undefined && fastModel.length > 0)
274
+ section.model = fastModel;
275
+ if (fastBaseUrl !== undefined && fastBaseUrl.length > 0)
276
+ section.baseUrl = fastBaseUrl;
277
+ // A PARTIAL section, safe only because every consumer deep-merges it — the
278
+ // same contract the four sections above rely on.
279
+ if (Object.keys(section).length > 0)
280
+ partial.fast = section;
281
+ }
282
+ // Render bounds (tui-render-performance §5.3). Same channel story as
283
+ // ARAGON_FULLSCREEN: a container or a wrapper that owns argv can reach neither
284
+ // a flag nor a config file, and a session that freezes is exactly the case
285
+ // where the user needs a knob they can turn from the outside.
286
+ //
287
+ // ARAGON_TRANSCRIPT_RETAIN / ARAGON_MAX_RENDER_INTERVAL_MS are left ABSENT
288
+ // when unparseable rather than written as a default, so the config file can
289
+ // still win — the rule `ARAGON_TEAM_MAX` above records.
290
+ const retain = process.env.ARAGON_TRANSCRIPT_RETAIN?.trim();
291
+ if (retain !== undefined && retain.length > 0) {
292
+ const parsed = Number.parseInt(retain, 10);
293
+ if (Number.isFinite(parsed) && parsed > 0)
294
+ partial.transcriptRetain = parsed;
295
+ }
296
+ // Parsed by the POSITIVE list, exactly as ARAGON_FULLSCREEN / ARAGON_MOUSE /
297
+ // ARAGON_PLAN / ARAGON_TEAM / ARAGON_TODO are — NOT by `envBool` below, which
298
+ // uses a negative list. The two readers disagree on `ARAGON_RENDER_GOVERNOR=disable`.
299
+ const governor = process.env.ARAGON_RENDER_GOVERNOR?.trim().toLowerCase();
300
+ if (governor !== undefined && governor.length > 0) {
301
+ partial.renderGovernor =
302
+ governor === '1' || governor === 'true' || governor === 'on' || governor === 'yes';
303
+ }
304
+ const renderInterval = process.env.ARAGON_MAX_RENDER_INTERVAL_MS?.trim();
305
+ if (renderInterval !== undefined && renderInterval.length > 0) {
306
+ const parsed = Number.parseInt(renderInterval, 10);
307
+ if (Number.isFinite(parsed) && parsed > 0)
308
+ partial.maxRenderIntervalMs = parsed;
309
+ }
180
310
  const log = readEnvLogConfig();
181
311
  if (log)
182
312
  partial.log = log;
@@ -27,6 +27,13 @@ export interface CliFlags {
27
27
  compact?: boolean;
28
28
  /** `--hints` ⇒ true, `--no-hints` ⇒ false, absent ⇒ undefined. */
29
29
  hints?: boolean;
30
+ /** `--show-thinking` ⇒ true, `--no-show-thinking` ⇒ false, absent ⇒ undefined. */
31
+ showThinking?: boolean;
32
+ /**
33
+ * `--live-tool-output` ⇒ true, `--no-live-tool-output` ⇒ false, absent ⇒ undefined
34
+ * (agent-activity-presentation-live §4.1).
35
+ */
36
+ liveToolOutput?: boolean;
30
37
  /** `--mouse` ⇒ true, `--no-mouse` ⇒ false, absent ⇒ undefined. */
31
38
  mouse?: boolean;
32
39
  /** `--no-skills` ⇒ false. Turns the whole skill subsystem off (§7.5). */
@@ -85,6 +92,62 @@ export interface CliFlags {
85
92
  logFile?: boolean;
86
93
  /** `--log-dir <dir>` — this run only. */
87
94
  logDir?: string;
95
+ /** `--transcript-retain <n>` — the `ViewState` ring cap for this run (L1). */
96
+ transcriptRetain?: string | number;
97
+ /**
98
+ * `--render-governor` ⇒ true, `--no-render-governor` ⇒ false, absent ⇒
99
+ * undefined (L4).
100
+ *
101
+ * DECLARED AS A PAIR for the reason `--todo-panel` records: `renderGovernor`
102
+ * is PERSISTED, so a lone `--no-render-governor` would make commander default
103
+ * `opts.renderGovernor` to `true` and silently overwrite a stored `false` on
104
+ * every run that passed no flag at all.
105
+ */
106
+ renderGovernor?: boolean;
107
+ /** `--max-render-interval <ms>` — the governor ceiling for this run (L4). */
108
+ maxRenderInterval?: string | number;
109
+ /**
110
+ * `--retry` ⇒ true, `--no-retry` ⇒ false, absent ⇒ undefined
111
+ * (llm-api-retry-backoff §6.2).
112
+ *
113
+ * DECLARED AS A PAIR, for the reason `--todo-panel` and `--render-governor`
114
+ * each record: `retry.enabled` is PERSISTED, so a lone `--no-retry` would make
115
+ * commander default `opts.retry` to `true` and silently overwrite a stored
116
+ * `enabled: false` on every run that passed no flag at all.
117
+ */
118
+ retry?: boolean;
119
+ /**
120
+ * `--retry-max <n>` — override `retry.maxRetries` for this run (still clamped).
121
+ *
122
+ * `0` IS A LEGITIMATE VALUE HERE and is the flag's kill switch, which is why
123
+ * `resolveRetryConfig` tests `!== undefined` rather than truthiness and why the
124
+ * clamp is `clampIntAllowingZero`.
125
+ */
126
+ retryMax?: string | number;
127
+ /**
128
+ * `--fast` ⇒ true, `--no-fast` ⇒ false, absent ⇒ undefined
129
+ * (fast-model-tier §4.3).
130
+ *
131
+ * DECLARED AS A PAIR — MANDATORY (C-9), for the reason `--team`,
132
+ * `--todo-panel`, `--render-governor` and `--retry` each record in turn.
133
+ * `fast.enabled` is PERSISTED and defaults to `false`, so a lone `--fast`
134
+ * would be indistinguishable from silence and the DEFAULT-OFF guarantee (R-d)
135
+ * would depend on a commander detail.
136
+ */
137
+ fast?: boolean;
138
+ /** `--fast-model <id>` — implies nothing about `enabled` on its own. */
139
+ fastModel?: string;
140
+ /** `--fast-provider <id>` — `''` (absent) inherits the main provider. */
141
+ fastProvider?: string;
142
+ /**
143
+ * `--fast-review <n|off>` — `off` sets `fast.review: false`, a number sets
144
+ * `reviewEveryTurns`.
145
+ *
146
+ * ONE FLAG FOR TWO KEYS, and deliberately: "how often" and "at all" are one
147
+ * decision to a user, and a separate `--no-fast-review` would be a fifth pair
148
+ * to keep in step with `toFlags`.
149
+ */
150
+ fastReview?: string;
88
151
  }
89
152
  /**
90
153
  * Resolve the effective config by merging all layers, then derive the runtime
@@ -6,11 +6,12 @@
6
6
  * legitimately long single tool run can never be watchdog-killed.
7
7
  */
8
8
  import process from 'node:process';
9
- import { DEFAULT_CONFIG, IDLE_TIMEOUT_MARGIN_MS, clampAskRounds, clampDensity, clampHumanTimeout, clampLogConfig, clampSkillsConfig, clampTeamConfig, clampTodoConfig, clampTheme, clampThinkingLevel, clampMaxTokens, clampTranscriptWindow, coercePositiveInt, isAutoToken, isSkillsToolPolicyMode, } from './schema.js';
9
+ import { DEFAULT_CONFIG, IDLE_TIMEOUT_MARGIN_MS, clampAskRounds, clampDensity, clampHumanTimeout, clampLogConfig, clampFastConfig, clampRetryConfig, clampSkillsConfig, clampTeamConfig, clampTodoConfig, clampTheme, clampThinkingLevel, clampMaxTokens, clampMaxRenderInterval, clampTranscriptRetain, clampTranscriptWindow, coercePositiveInt, isAutoToken, isSkillsToolPolicyMode, } 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';
13
13
  import { setHistoryEnabled } from './prompt-history.js';
14
+ import { setEntryRetain } from '../agent/entry-limits.js';
14
15
  import { readSubmitCount } from './ui-state.js';
15
16
  import { getLogger } from '../logging/logger.js';
16
17
  import { registerSecret, registerSecretsFrom } from '../logging/secret-registry.js';
@@ -120,6 +121,89 @@ function resolveTodoConfig(flags, env, file) {
120
121
  ...(flags.todoFollow !== undefined ? { followThrough: flags.todoFollow } : {}),
121
122
  });
122
123
  }
124
+ /**
125
+ * Merge the `retry` section across all four layers (defaults › file › env ›
126
+ * flags), llm-api-retry-backoff §6.2.
127
+ *
128
+ * `!== undefined` ON BOTH FLAGS, and that is the whole point — the rule
129
+ * `resolveTeamConfig` and `resolveTodoConfig` each state once. Commander
130
+ * materialises a lone `--no-retry` as `opts.retry = true` when the flag is
131
+ * ABSENT, so a truthiness check cannot tell `--no-retry` from "not passed"; and
132
+ * `flags.retryMax` may legitimately be `0`, which a truthiness check would drop
133
+ * on the floor and replace with the default 10 (R-16).
134
+ */
135
+ function resolveRetryConfig(flags, env, file) {
136
+ return clampRetryConfig({
137
+ ...DEFAULT_CONFIG.retry,
138
+ ...(file.retry ?? {}),
139
+ ...(env.retry ?? {}),
140
+ ...(flags.retry !== undefined ? { enabled: flags.retry } : {}),
141
+ ...(flags.retryMax !== undefined && flags.retryMax !== ''
142
+ ? { maxRetries: flags.retryMax }
143
+ : {}),
144
+ });
145
+ }
146
+ /**
147
+ * Merge the `fast` section across all four layers (defaults › file › env ›
148
+ * flags), fast-model-tier §3.7.
149
+ *
150
+ * `!== undefined` ON `flags.fast`, which is the rule `resolveTeamConfig`,
151
+ * `resolveTodoConfig` and `resolveRetryConfig` each state once (C-9): commander
152
+ * materialises a lone `--no-fast` as `opts.fast = true` when the flag is absent,
153
+ * so a truthiness check cannot tell `--no-fast` from "not passed" — and here the
154
+ * damage would run the other way from usual, silently turning a default-OFF
155
+ * feature ON for every run that passed no flag at all.
156
+ *
157
+ * `--fast-review` maps onto TWO keys, so it is parsed rather than spread: `off`
158
+ * is `review: false`, a number is `reviewEveryTurns`, and anything else is
159
+ * ignored (the clamp discipline every other key in this file follows).
160
+ */
161
+ function resolveFastConfig(flags, env, file) {
162
+ const reviewFlag = flags.fastReview?.trim().toLowerCase();
163
+ const reviewPatch = {};
164
+ if (reviewFlag !== undefined && reviewFlag.length > 0) {
165
+ if (reviewFlag === 'off') {
166
+ reviewPatch.review = false;
167
+ }
168
+ else {
169
+ const n = Number.parseInt(reviewFlag, 10);
170
+ if (Number.isFinite(n) && n > 0)
171
+ reviewPatch.reviewEveryTurns = n;
172
+ }
173
+ }
174
+ return clampFastConfig({
175
+ ...DEFAULT_CONFIG.fast,
176
+ ...(file.fast ?? {}),
177
+ ...(env.fast ?? {}),
178
+ ...(flags.fast !== undefined ? { enabled: flags.fast } : {}),
179
+ ...(flags.fastModel !== undefined && flags.fastModel !== ''
180
+ ? { model: flags.fastModel }
181
+ : {}),
182
+ ...(flags.fastProvider !== undefined && flags.fastProvider !== ''
183
+ ? { provider: flags.fastProvider }
184
+ : {}),
185
+ ...reviewPatch,
186
+ });
187
+ }
188
+ /**
189
+ * Resolve the adaptive render governor: flag › env › file › default `true`
190
+ * (tui-render-performance L4).
191
+ *
192
+ * Shaped like `resolveMouse` rather than like `resolveFullscreen`: there are no
193
+ * heuristics for an explicit `true` to override, so a persisted `true` is just a
194
+ * value and not an opinion that has to be told apart from silence. `!==
195
+ * undefined` on the flag is what makes `--no-render-governor` beat a config file
196
+ * that turned it on (P1-4's rule, one feature later).
197
+ */
198
+ function resolveRenderGovernor(flags, env, file) {
199
+ if (flags.renderGovernor !== undefined)
200
+ return flags.renderGovernor;
201
+ if (env.renderGovernor !== undefined)
202
+ return env.renderGovernor;
203
+ if (file.renderGovernor !== undefined)
204
+ return file.renderGovernor;
205
+ return DEFAULT_CONFIG.renderGovernor;
206
+ }
123
207
  /** Merge the `log` section across all four layers (defaults › file › env › flags). */
124
208
  function resolveLogConfig(flags, env, file) {
125
209
  const merged = clampLogConfig({
@@ -203,6 +287,16 @@ export function loadConfig(flags = {}) {
203
287
  const model = pick(flags.model, env.partial.model, file.model) ?? DEFAULT_CONFIG.model;
204
288
  const baseUrl = pick(flags.baseUrl, env.partial.baseUrl ?? undefined, file.baseUrl ?? undefined);
205
289
  const thinkingLevel = clampThinkingLevel(pick(flags.thinking, env.partial.thinkingLevel, file.thinkingLevel), DEFAULT_CONFIG.thinkingLevel);
290
+ // BESIDE ITS SIBLING, and through the same three layers (§4.1 / P1-2). This is
291
+ // the ONLY place flags, environment and file are joined, so a key resolved
292
+ // anywhere else reaches the UI from at most one of them.
293
+ const showThinking = pick(flags.showThinking, env.partial.showThinking, file.showThinking) ??
294
+ DEFAULT_CONFIG.showThinking;
295
+ // THE SAME THREE LAYERS, in the same one place (§4.1). Round 1's P1-2 and the
296
+ // fast tier's round-2 IF both record the identical failure: a key resolved
297
+ // anywhere else reaches the UI from at most one of flags, environment and file.
298
+ const liveToolOutput = pick(flags.liveToolOutput, env.partial.liveToolOutput, file.liveToolOutput) ??
299
+ DEFAULT_CONFIG.liveToolOutput;
206
300
  const maxTokens = resolveMaxTokens(flags, env.partial, file);
207
301
  // `clampTheme` is also where the v0.3.0 `dark` name is migrated to `cool`,
208
302
  // so reading a legacy config file needs no extra step here (§4.5 / R-4).
@@ -268,11 +362,30 @@ export function loadConfig(flags = {}) {
268
362
  // The prompt-history store is a module-level singleton, so the resolved value
269
363
  // has to be pushed into it — this is the ONLY place that happens.
270
364
  setHistoryEnabled(historyEnabled);
365
+ // Transcript bounds (tui-render-performance L1 / L3).
366
+ //
367
+ // `transcriptRetain` is RAISED TO `transcriptWindow` when the two conflict:
368
+ // retaining fewer entries than the window can scroll to would silently make
369
+ // part of the window unreachable, and the user would see a horizon they can
370
+ // never actually reach.
371
+ //
372
+ // The raise is REPORTED, not just performed (§5.1). Overriding a value the
373
+ // user typed and saying nothing is the one thing this feature is not allowed
374
+ // to add — every other adjustment it makes is announced somewhere, and a
375
+ // config key is the last place to start making exceptions.
376
+ const transcriptWindow = clampTranscriptWindow(pick(env.partial.transcriptWindow, file.transcriptWindow), DEFAULT_CONFIG.transcriptWindow);
377
+ const requestedRetain = clampTranscriptRetain(pick(flags.transcriptRetain, env.partial.transcriptRetain, file.transcriptRetain), DEFAULT_CONFIG.transcriptRetain);
378
+ const transcriptRetain = Math.max(transcriptWindow, requestedRetain);
379
+ // `viewReducer` is pure and takes no config, so the resolved ring cap reaches
380
+ // it through the same module-singleton channel `setHistoryEnabled` uses.
381
+ setEntryRetain(transcriptRetain);
271
382
  const config = {
272
383
  provider,
273
384
  model,
274
385
  baseUrl,
275
386
  thinkingLevel,
387
+ showThinking,
388
+ liveToolOutput,
276
389
  maxTokens,
277
390
  theme,
278
391
  reducedMotion,
@@ -280,7 +393,11 @@ export function loadConfig(flags = {}) {
280
393
  exitTranscript: flags.exitTranscript !== undefined
281
394
  ? flags.exitTranscript
282
395
  : file.exitTranscript ?? DEFAULT_CONFIG.exitTranscript,
283
- transcriptWindow: clampTranscriptWindow(file.transcriptWindow, DEFAULT_CONFIG.transcriptWindow),
396
+ transcriptWindow,
397
+ transcriptRetain,
398
+ ...(requestedRetain < transcriptRetain ? { transcriptRetainRequested: requestedRetain } : {}),
399
+ renderGovernor: resolveRenderGovernor(flags, env.partial, file),
400
+ maxRenderIntervalMs: clampMaxRenderInterval(pick(flags.maxRenderInterval, env.partial.maxRenderIntervalMs, file.maxRenderIntervalMs), DEFAULT_CONFIG.maxRenderIntervalMs),
284
401
  confirmTools,
285
402
  toolTimeoutMs,
286
403
  idleTimeoutMs,
@@ -309,6 +426,8 @@ export function loadConfig(flags = {}) {
309
426
  log: resolveLogConfig(flags, env.partial, file),
310
427
  team: resolveTeamConfig(flags, env.partial, file),
311
428
  todo: resolveTodoConfig(flags, env.partial, file),
429
+ retry: resolveRetryConfig(flags, env.partial, file),
430
+ fast: resolveFastConfig(flags, env.partial, file),
312
431
  cwd,
313
432
  color,
314
433
  colorLevel,
@@ -345,6 +464,10 @@ function recordResolvedConfig(config) {
345
464
  idleTimeoutMs: config.idleTimeoutMs,
346
465
  historyEnabled: config.historyEnabled,
347
466
  logLevel: config.log.level,
467
+ // Four layers feed this one too, and none of them would otherwise leave a
468
+ // trace — so "why did my failing call sit there for three minutes" (or
469
+ // "why did it not retry at all") has an answer after the fact.
470
+ retry: config.retry.enabled ? config.retry.maxRetries : 'off',
348
471
  hasApiKey: Boolean(config.apiKeys[config.provider]),
349
472
  configPath: getConfigPath(),
350
473
  });