@aragon-agent/cli 0.5.8 → 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 (123) hide show
  1. package/CHANGELOG.md +279 -0
  2. package/README.md +1429 -1163
  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/headless.d.ts +8 -0
  8. package/dist/agent/headless.js +40 -0
  9. package/dist/agent/reducer.d.ts +245 -3
  10. package/dist/agent/reducer.js +490 -17
  11. package/dist/agent/retry-view.d.ts +68 -0
  12. package/dist/agent/retry-view.js +95 -0
  13. package/dist/agent/system-prompt.d.ts +11 -0
  14. package/dist/agent/system-prompt.js +2 -0
  15. package/dist/cli.js +86 -2
  16. package/dist/commands/builtins.d.ts +1 -1
  17. package/dist/commands/builtins.js +308 -2
  18. package/dist/config/cli-commands.d.ts +53 -2
  19. package/dist/config/cli-commands.js +136 -3
  20. package/dist/config/env.d.ts +6 -3
  21. package/dist/config/env.js +103 -3
  22. package/dist/config/load.d.ts +49 -0
  23. package/dist/config/load.js +83 -1
  24. package/dist/config/schema.d.ts +212 -2
  25. package/dist/config/schema.js +207 -1
  26. package/dist/config/store.js +20 -1
  27. package/dist/fast/critique.d.ts +43 -0
  28. package/dist/fast/critique.js +62 -0
  29. package/dist/fast/digest.d.ts +42 -0
  30. package/dist/fast/digest.js +85 -0
  31. package/dist/fast/frames.d.ts +99 -0
  32. package/dist/fast/frames.js +180 -0
  33. package/dist/fast/limits.d.ts +98 -0
  34. package/dist/fast/limits.js +98 -0
  35. package/dist/fast/prompt.d.ts +72 -0
  36. package/dist/fast/prompt.js +94 -0
  37. package/dist/fast/resolve.d.ts +43 -0
  38. package/dist/fast/resolve.js +105 -0
  39. package/dist/fast/review-call.d.ts +99 -0
  40. package/dist/fast/review-call.js +130 -0
  41. package/dist/fast/reviewer.d.ts +248 -0
  42. package/dist/fast/reviewer.js +664 -0
  43. package/dist/fast/types.d.ts +128 -0
  44. package/dist/fast/types.js +14 -0
  45. package/dist/fast/wiring.d.ts +170 -0
  46. package/dist/fast/wiring.js +316 -0
  47. package/dist/logging/install.d.ts +15 -1
  48. package/dist/logging/install.js +37 -4
  49. package/dist/logging/logger.d.ts +1 -1
  50. package/dist/session/persist.js +57 -1
  51. package/dist/team/normalize.d.ts +26 -1
  52. package/dist/team/normalize.js +23 -3
  53. package/dist/team/prompt.d.ts +11 -0
  54. package/dist/team/prompt.js +10 -0
  55. package/dist/team/report.d.ts +18 -0
  56. package/dist/team/report.js +27 -1
  57. package/dist/team/runtime.d.ts +13 -2
  58. package/dist/team/runtime.js +33 -6
  59. package/dist/team/subagent.d.ts +19 -1
  60. package/dist/team/subagent.js +72 -10
  61. package/dist/team/task-tool.d.ts +26 -0
  62. package/dist/team/task-tool.js +48 -4
  63. package/dist/team/types.d.ts +80 -1
  64. package/dist/tools/bash-tool.js +19 -3
  65. package/dist/tools/diff.d.ts +25 -5
  66. package/dist/tools/diff.js +72 -41
  67. package/dist/tools/file-change-store.d.ts +34 -0
  68. package/dist/tools/file-change-store.js +57 -0
  69. package/dist/tools/fs-tools.d.ts +26 -0
  70. package/dist/tools/fs-tools.js +62 -5
  71. package/dist/tools/index.d.ts +31 -0
  72. package/dist/tools/index.js +7 -1
  73. package/dist/tools/patch.d.ts +115 -0
  74. package/dist/tools/patch.js +453 -0
  75. package/dist/tools/terminal-output.d.ts +107 -0
  76. package/dist/tools/terminal-output.js +198 -0
  77. package/dist/tools/tool-output-store.d.ts +68 -0
  78. package/dist/tools/tool-output-store.js +102 -0
  79. package/dist/ui/ActivityLine.d.ts +48 -0
  80. package/dist/ui/ActivityLine.js +22 -0
  81. package/dist/ui/App.d.ts +1 -4
  82. package/dist/ui/App.js +273 -37
  83. package/dist/ui/BottomStatusRow.d.ts +43 -0
  84. package/dist/ui/BottomStatusRow.js +14 -0
  85. package/dist/ui/Composer.d.ts +1 -6
  86. package/dist/ui/Composer.js +2 -2
  87. package/dist/ui/PromptInput.d.ts +1 -15
  88. package/dist/ui/PromptInput.js +1 -38
  89. package/dist/ui/StatusBar.d.ts +37 -0
  90. package/dist/ui/StatusBar.js +5 -3
  91. package/dist/ui/TeamPanel.d.ts +5 -4
  92. package/dist/ui/TeamPanel.js +1 -1
  93. package/dist/ui/TodoPanel.d.ts +5 -4
  94. package/dist/ui/Transcript.d.ts +26 -3
  95. package/dist/ui/Transcript.js +76 -10
  96. package/dist/ui/activity-phrases.d.ts +33 -0
  97. package/dist/ui/activity-phrases.js +50 -0
  98. package/dist/ui/entries/AssistantEntry.d.ts +20 -0
  99. package/dist/ui/entries/AssistantEntry.js +3 -2
  100. package/dist/ui/entries/DiffView.d.ts +67 -0
  101. package/dist/ui/entries/DiffView.js +103 -0
  102. package/dist/ui/entries/FastCard.d.ts +53 -0
  103. package/dist/ui/entries/FastCard.js +87 -0
  104. package/dist/ui/entries/RetryCard.d.ts +56 -0
  105. package/dist/ui/entries/RetryCard.js +60 -0
  106. package/dist/ui/entries/TeamCard.js +6 -1
  107. package/dist/ui/entries/ToolCard.d.ts +38 -1
  108. package/dist/ui/entries/ToolCard.js +68 -5
  109. package/dist/ui/glyphs.d.ts +9 -0
  110. package/dist/ui/glyphs.js +2 -0
  111. package/dist/ui/layout/AppShell.d.ts +13 -22
  112. package/dist/ui/layout/AppShell.js +3 -38
  113. package/dist/ui/layout/virtual-window.d.ts +1 -1
  114. package/dist/ui/layout/virtual-window.js +109 -5
  115. package/dist/ui/overlays/HelpOverlay.js +16 -2
  116. package/dist/ui/overlays/SettingsScreen.d.ts +82 -1
  117. package/dist/ui/overlays/SettingsScreen.js +147 -1
  118. package/dist/ui/transcript-text.js +27 -0
  119. package/dist/ui/use-wheel-routing.d.ts +16 -32
  120. package/dist/ui/use-wheel-routing.js +21 -38
  121. package/package.json +2 -2
  122. package/dist/ui/layout/regions.d.ts +0 -55
  123. package/dist/ui/layout/regions.js +0 -52
@@ -1,14 +1,15 @@
1
1
  /**
2
2
  * Built-in slash commands (spec §5.2):
3
3
  * /help /model /settings /thinking /max-tokens /tools /clear /reset /cwd /save
4
- * /resume /copy /exit (/quit).
4
+ * /resume /copy /exit (/quit), plus the /team, /fast and /todo subsystems.
5
5
  */
6
6
  import { spawn } from 'node:child_process';
7
7
  import { existsSync, statSync } from 'node:fs';
8
8
  import { isAbsolute, resolve } from 'node:path';
9
9
  import process from 'node:process';
10
10
  import { resolveOutputTokens } from '@aragon-agent/core';
11
- import { clampThinkingLevel, clampTheme, isThemeName, parseMaxTokensInput, DEFAULT_MAX_TOKENS, LEGACY_DARK_THEME, THEME_NAMES, THINKING_LEVELS, } from '../config/schema.js';
11
+ 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';
12
+ import { computeCost, formatCost, formatTokens } from '../agent/usage.js';
12
13
  import { TODO_FOLLOW_LIMITS } from '../todo/limits.js';
13
14
  import { buildContinuationMessage } from '../todo/follow-through.js';
14
15
  import { MODE_LABEL, nextMode } from '../agent/agent-mode.js';
@@ -16,6 +17,59 @@ import { loadConfig } from '../config/load.js';
16
17
  import { loadSession, resolveSessionPath, saveSession } from '../session/persist.js';
17
18
  import { getLogger } from '../logging/logger.js';
18
19
  import { perfCommand } from './perf.js';
20
+ /**
21
+ * `/fast status` — THE GUARANTEED REPORTING SURFACE for the tier (§4.4 / R-8).
22
+ *
23
+ * The chip degrades on a narrow terminal and the card only appears when a review
24
+ * happens; this is the one place that always answers "is it on, what is it, and
25
+ * has it done anything". The session totals are here rather than on the status
26
+ * bar because the aggregate there cannot answer "is the cheap tier actually
27
+ * saving me anything?".
28
+ *
29
+ * `unknown (no price table for <model>)` RATHER THAN A CURRENCY AMOUNT when the
30
+ * static table has never seen the model (C-11 / RV-4 / AC-34): `$0.00` on a
31
+ * feature that is spending money is the same class of lie as pricing a Haiku
32
+ * child at Sonnet rates.
33
+ */
34
+ function formatFastStatus(controller) {
35
+ const status = controller.getFastStatus();
36
+ const cfg = controller.getFastConfig();
37
+ const state = !status.registered
38
+ ? 'off for this session (not configured at launch)'
39
+ : status.enabled
40
+ ? 'on'
41
+ : 'off';
42
+ const lines = [];
43
+ if (status.tier.ok) {
44
+ const same = status.tier.sameAsMain ? ' (same as main)' : '';
45
+ lines.push(`Fast tier: ${state}. Model: ${status.tier.ref.providerId}:${status.tier.ref.modelId}${same}.`);
46
+ }
47
+ else {
48
+ lines.push(`Fast tier: ${state}. Model: ${cfg.model || '(not set)'}.`);
49
+ }
50
+ lines.push(`Review: ${cfg.review ? `every ${cfg.reviewEveryTurns} turns` : 'off'}. ` +
51
+ `Delegation: ${cfg.delegate ? 'on' : 'off'}.`);
52
+ const snapshot = status.snapshot;
53
+ const cost = snapshot.pricingUnknown
54
+ ? `unknown (no price table for ${snapshot.model})`
55
+ : formatCost(computeCost(snapshot.usage, status.tier.ok ? controller.getModelInfoFor(status.tier.ref).cost : undefined));
56
+ // `n/N`, NEVER A BARE `n` (fast-model-tier-hardening §4.3). A bare count
57
+ // answers "what have I spent" only for a reader who already knows the
58
+ // ceiling, which is precisely what the user does not know; `12/40` answers
59
+ // that and "how much is left" in the same three characters, and makes the
60
+ // budget discoverable to someone who never read the config schema. The
61
+ // exhausted form names the remedy inline, because a status line that reports
62
+ // a stop without its escape hatch just relocates the question.
63
+ //
64
+ // `budgetReached` is READ, not re-derived (RV-H10): with the tier off both
65
+ // numbers are `0` placeholders and `reviews >= reviewBudget` would be true.
66
+ const budget = snapshot.budgetReached ? ' (budget reached - raise with /fast budget <n>)' : '';
67
+ lines.push(`This session: ${snapshot.reviews}/${snapshot.reviewBudget} reviews${budget}, ` +
68
+ `${snapshot.delegated} delegated subagents, ` +
69
+ `in ${formatTokens(snapshot.usage.inputTokens)} / out ` +
70
+ `${formatTokens(snapshot.usage.outputTokens)}, cost ${cost}.`);
71
+ return lines.join('\n');
72
+ }
19
73
  const COMMANDS = [
20
74
  {
21
75
  name: 'help',
@@ -223,6 +277,187 @@ const COMMANDS = [
223
277
  ctx.notify('warn', `Unknown argument "${verb}" - use /team [status|on|off|max <n>].`);
224
278
  },
225
279
  },
280
+ {
281
+ name: 'fast',
282
+ description: 'Fast model tier: status | on | off | model <id> | provider <id> | same | ' +
283
+ 'review <n|off> | budget <n> | delegate on|off',
284
+ /**
285
+ * `/fast on` CANNOT turn the tier on in a session that started without it,
286
+ * for the reason `/team on` above records at length: the tool array is built
287
+ * ONCE and rebuilding it is forbidden (C-2), so a session that grew a schema
288
+ * field mid-flight would be a session whose request payloads change shape
289
+ * between turns.
290
+ *
291
+ * EVERY MUTATING BRANCH DOES THE TWO CALLS PLUS PERSIST that `/todo`
292
+ * documents: `controller.setFastConfig(patch)` updates the object `App`
293
+ * reads at render time AND re-resolves the tier AND rebuilds the prompt;
294
+ * `ctx.persistConfig({ fast: patch })` survives the session. Persist alone
295
+ * reports success and changes nothing until relaunch; the setter alone
296
+ * forgets by morning.
297
+ */
298
+ run: (ctx) => {
299
+ const arg = ctx.args.trim();
300
+ const [rawVerb, value] = arg.split(/\s+/, 2);
301
+ const verb = (rawVerb ?? '').toLowerCase();
302
+ const controller = ctx.controller;
303
+ // Refused mid-dispatch for the reason `/team` records: half a dispatch
304
+ // under one tier and half under another produces a report nothing can
305
+ // afterwards explain. THAT ARGUMENT IS ABOUT WRITES, so the two READS are
306
+ // grouped and answerable (RV-H3): `/fast status` always was, and a bare
307
+ // `/fast budget` reports the same numbers by another name. `/fast budget
308
+ // 80` is a write and is still refused.
309
+ const isRead = verb === 'status' || (verb === 'budget' && !value);
310
+ if (verb && !isRead && controller.isTeamBusy()) {
311
+ ctx.notify('warn', 'Cannot change fast-tier settings mid-dispatch.');
312
+ return;
313
+ }
314
+ const persist = (patch) => {
315
+ controller.setFastConfig(patch);
316
+ ctx.persistConfig({ fast: patch });
317
+ };
318
+ if (!verb || verb === 'status') {
319
+ ctx.notify('info', formatFastStatus(controller));
320
+ return;
321
+ }
322
+ if (verb === 'on' || verb === 'off') {
323
+ const enabled = verb === 'on';
324
+ persist({ enabled });
325
+ if (!controller.isFastRegistered()) {
326
+ // The honest branch, and the whole point of splitting by what was
327
+ // decided at construction (§3.3). It is one sentence, and it keeps
328
+ // AC-2 / AC-3 intact: a session launched without the tier never grows
329
+ // a schema field, whatever the user types afterwards.
330
+ ctx.notify('info', enabled
331
+ ? 'The fast tier is off for this session (not configured at launch). ' +
332
+ 'Saved for next launch.'
333
+ : 'The fast tier is already off for this session. Saved for next launch.');
334
+ return;
335
+ }
336
+ controller.setFastEnabled(enabled);
337
+ ctx.toast('success', `Fast tier ${enabled ? 'on' : 'off'}.`);
338
+ return;
339
+ }
340
+ if (verb === 'model') {
341
+ const raw = (value ?? '').trim();
342
+ if (!raw) {
343
+ ctx.notify('warn', 'Usage: /fast model <id> (or provider:model)');
344
+ return;
345
+ }
346
+ // `provider:model` is accepted because that is how a user thinks about a
347
+ // tier on another vendor, and an unknown provider is rejected with THE
348
+ // MESSAGE `cli.tsx` ALREADY PRINTS rather than a second wording for the
349
+ // same fact (RV-16).
350
+ const colon = raw.indexOf(':');
351
+ if (colon > 0) {
352
+ const provider = raw.slice(0, colon);
353
+ const model = raw.slice(colon + 1);
354
+ if (!isAdapterProvider(provider)) {
355
+ ctx.notify('warn', `Unknown provider "${provider}". Choose: ${ADAPTER_PROVIDERS.join(', ')}.`);
356
+ return;
357
+ }
358
+ persist({ provider, model });
359
+ ctx.toast('success', `Fast model set to ${provider}:${model}.`);
360
+ return;
361
+ }
362
+ persist({ model: raw });
363
+ ctx.toast('success', `Fast model set to ${raw}.`);
364
+ return;
365
+ }
366
+ if (verb === 'provider') {
367
+ const raw = (value ?? '').trim();
368
+ if (!raw) {
369
+ ctx.notify('warn', 'Usage: /fast provider <id>');
370
+ return;
371
+ }
372
+ if (!isAdapterProvider(raw)) {
373
+ ctx.notify('warn', `Unknown provider "${raw}". Choose: ${ADAPTER_PROVIDERS.join(', ')}.`);
374
+ return;
375
+ }
376
+ persist({ provider: raw });
377
+ ctx.toast('success', `Fast provider set to ${raw}.`);
378
+ return;
379
+ }
380
+ if (verb === 'same') {
381
+ // R-e IN ONE WORD. `sameAsMain` is display-only and no branch reads it
382
+ // (R-13): the user is allowed to run both tiers on one model, and the
383
+ // review still costs what it costs.
384
+ const cfg = controller.getConfig();
385
+ persist({
386
+ provider: cfg.provider,
387
+ model: cfg.model,
388
+ baseUrl: cfg.baseUrl ?? '',
389
+ });
390
+ ctx.toast('success', `Fast tier set to ${cfg.provider}:${cfg.model} (same as main).`);
391
+ return;
392
+ }
393
+ if (verb === 'review') {
394
+ const raw = (value ?? '').trim().toLowerCase();
395
+ if (raw === 'off') {
396
+ persist({ review: false });
397
+ ctx.toast('success', 'Fast review off.');
398
+ return;
399
+ }
400
+ const n = Number.parseInt(raw, 10);
401
+ if (!Number.isFinite(n) || n <= 0) {
402
+ ctx.notify('warn', 'Usage: /fast review <n> | off');
403
+ return;
404
+ }
405
+ // Clamped by `setFastConfig` and again on the persist path, so a value
406
+ // outside the range can reach neither the runtime nor the file.
407
+ const applied = controller.setFastConfig({ review: true, reviewEveryTurns: n });
408
+ ctx.persistConfig({
409
+ fast: { review: true, reviewEveryTurns: applied.reviewEveryTurns },
410
+ });
411
+ ctx.toast('success', `Fast review every ${applied.reviewEveryTurns} turns.`);
412
+ return;
413
+ }
414
+ if (verb === 'budget') {
415
+ const snapshot = controller.getFastStatus().snapshot;
416
+ const raw = (value ?? '').trim();
417
+ if (!raw) {
418
+ // The READ. Answerable mid-dispatch (see the guard above), and it
419
+ // reports the same pair `/fast status` renders as `n/N`.
420
+ ctx.notify('info', `Fast review budget: ${snapshot.reviews} of ${snapshot.reviewBudget} used ` +
421
+ 'this session. Use /fast budget <1-500> to raise it.');
422
+ return;
423
+ }
424
+ const n = Number.parseInt(raw, 10);
425
+ if (!Number.isFinite(n) || n <= 0) {
426
+ // Change NOTHING on a typo, the discipline `/max-tokens` sets: a
427
+ // silent resolve to the default discards the number the user meant.
428
+ ctx.notify('warn', 'Usage: /fast budget <1-500>');
429
+ return;
430
+ }
431
+ // BOTH CALLS, IN THIS ORDER (RV-H4 / AC-H17). `setFastConfig` is what
432
+ // makes a raised budget take effect LIVE - the reviewer's `budgetLimit()`
433
+ // reads the controller's in-memory config, never disk - and it also
434
+ // re-emits `tier_changed`, which is how the new denominator reaches the
435
+ // status chip without a restart. See the comment on this command: a
436
+ // persist alone reports success and changes nothing until relaunch.
437
+ const applied = controller.setFastConfig({ reviewMaxPerSession: n });
438
+ ctx.persistConfig({
439
+ fast: { reviewMaxPerSession: applied.reviewMaxPerSession },
440
+ });
441
+ // The APPLIED value, not the requested one, so a clamp is visible.
442
+ ctx.toast('success', `Fast review budget: ${applied.reviewMaxPerSession} per session. ` +
443
+ `${snapshot.reviews} used.`);
444
+ return;
445
+ }
446
+ if (verb === 'delegate') {
447
+ const raw = (value ?? '').trim().toLowerCase();
448
+ if (raw !== 'on' && raw !== 'off') {
449
+ ctx.notify('warn', 'Usage: /fast delegate <on|off>');
450
+ return;
451
+ }
452
+ persist({ delegate: raw === 'on' });
453
+ ctx.toast('success', `Fast delegation ${raw}.`);
454
+ return;
455
+ }
456
+ ctx.notify('warn', `Unknown argument "${verb}" - use /fast ` +
457
+ '[status|on|off|model <id>|provider <id>|same|review <n|off>|budget <n>|' +
458
+ 'delegate on|off].');
459
+ },
460
+ },
226
461
  {
227
462
  name: 'todo',
228
463
  description: 'Todo planning: status | on | off | panel on|off | follow <mode> | clear | continue',
@@ -377,6 +612,77 @@ const COMMANDS = [
377
612
  '/todo [status|on|off|panel on|off|follow <mode>|clear|continue].');
378
613
  },
379
614
  },
615
+ {
616
+ name: 'retry',
617
+ description: 'API retry: show | on | off | max <n>',
618
+ /**
619
+ * TWO CALLS FOR EVERY MUTATION, IN THIS ORDER, and neither is optional
620
+ * (llm-api-retry-backoff §6.9 / §6.3):
621
+ *
622
+ * `setRetryConfig` replaces the policy on the live `ProviderRegistry`, so
623
+ * the change takes effect in THIS session — and, because
624
+ * `TeamRuntime` holds the same registry instance, for every
625
+ * child too. Without it `/retry off` reports success and
626
+ * changes nothing until the next launch.
627
+ * `persistConfig` is what survives the session.
628
+ *
629
+ * UNLIKE `/team on` AND `/todo on` there is no "off for this session" branch:
630
+ * retry is not a tool, nothing is registered at construction, and there is
631
+ * therefore no state a flag could have made unreachable. `--no-retry` is a
632
+ * starting value, not a permanent verdict.
633
+ */
634
+ run: (ctx) => {
635
+ const arg = ctx.args.trim().toLowerCase();
636
+ const [verb, value] = arg.split(/\s+/, 2);
637
+ const controller = ctx.controller;
638
+ if (!verb || verb === 'show' || verb === 'status') {
639
+ const cfg = controller.getRetryConfig();
640
+ if (!cfg.enabled || cfg.maxRetries <= 0) {
641
+ ctx.notify('info', `API retry: off (maxRetries ${cfg.maxRetries}). Use /retry on or /retry max <n>.`);
642
+ return;
643
+ }
644
+ ctx.notify('info', `API retry: on. Up to ${cfg.maxRetries} retries, ${cfg.initialDelayMs}ms doubling to ` +
645
+ `${cfg.maxDelayMs}ms${cfg.jitter ? ' with jitter' : ''}, ` +
646
+ `giving up after ${Math.round(cfg.maxElapsedMs / 1000)}s. ` +
647
+ `Retry-After ${cfg.respectRetryAfter ? 'honoured' : 'ignored'}.`);
648
+ return;
649
+ }
650
+ if (verb === 'on' || verb === 'off') {
651
+ const enabled = verb === 'on';
652
+ const applied = controller.setRetryConfig({ enabled });
653
+ // A PARTIAL `retry` section, safe only because `updatePersistedConfig`
654
+ // deep-merges it — the same contract `/team on` relies on.
655
+ ctx.persistConfig({ retry: { enabled } });
656
+ if (enabled && applied.maxRetries <= 0) {
657
+ // Honest rather than cheerful: `enabled` is on but the count is the
658
+ // kill switch, so nothing will actually retry.
659
+ ctx.notify('warn', 'API retry enabled, but maxRetries is 0 - use /retry max <n>.');
660
+ return;
661
+ }
662
+ ctx.toast('success', `API retry ${enabled ? 'on' : 'off'}.`);
663
+ return;
664
+ }
665
+ if (verb === 'max') {
666
+ // `Number.isFinite` and `>= 0`, NOT `> 0`: `0` is this key's documented
667
+ // floor and its kill switch, and rejecting it here would make `/retry max 0`
668
+ // the one documented control that does not work (R-16).
669
+ const n = Number.parseInt(value ?? '', 10);
670
+ if (!Number.isFinite(n) || n < 0) {
671
+ ctx.notify('warn', `Usage: /retry max <0-${HARD_MAX_RETRIES}> (0 turns retry off)`);
672
+ return;
673
+ }
674
+ const applied = controller.setRetryConfig({ maxRetries: n });
675
+ ctx.persistConfig({
676
+ retry: { maxRetries: applied.maxRetries },
677
+ });
678
+ ctx.toast('success', applied.maxRetries === 0
679
+ ? 'API retry off (maxRetries 0).'
680
+ : `API retry: up to ${applied.maxRetries} retries.`);
681
+ return;
682
+ }
683
+ ctx.notify('warn', `Unknown argument "${verb}" - use /retry [show|on|off|max <n>].`);
684
+ },
685
+ },
380
686
  {
381
687
  name: 'expand',
382
688
  description: 'Expand / collapse a recent tool card (default: last)',
@@ -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 —
@@ -21,6 +21,28 @@ export declare const LOG_CONFIG_SET_KEYS: readonly ["log.level", "log.toFile", "
21
21
  * `Set team.enabled = false`.
22
22
  */
23
23
  export declare const TEAM_CONFIG_SET_KEYS: readonly ["team.enabled", "team.maxSubagents", "team.maxConcurrent", "team.subagentTimeoutMs", "team.dispatchTimeoutMs", "team.maxTurnsPerSubagent"];
24
+ /**
25
+ * Config keys under `retry.` that `aragon config set` accepts
26
+ * (llm-api-retry-backoff §8.2).
27
+ *
28
+ * Handled here rather than in `cli.tsx`'s switch for the reason this module's
29
+ * header gives about `log.*` and `team.*`: that switch is past this package's
30
+ * complexity ceiling, and a key added to the membership set but forgotten in the
31
+ * switch falls through every case, writes nothing, and still prints
32
+ * `Set retry.maxRetries = 0`.
33
+ */
34
+ export declare const RETRY_CONFIG_SET_KEYS: readonly ["retry.enabled", "retry.maxRetries", "retry.initialDelayMs", "retry.maxDelayMs", "retry.multiplier", "retry.jitter", "retry.respectRetryAfter", "retry.maxElapsedMs", "retry.onPartialStream"];
35
+ /**
36
+ * Config keys under `fast.` that `aragon config set` accepts (fast-model-tier
37
+ * §4.2).
38
+ *
39
+ * A DEDICATED SETTER RATHER THAN A GENERIC DOTTED WRITER, for the reason the
40
+ * three sets above each record: a generic setter would write `fast` as a FLAT
41
+ * string key (`"fast.model": "haiku"` at the top level of the file), the section
42
+ * would silently never take effect, and `config set` would still print
43
+ * `Set fast.model = haiku`.
44
+ */
45
+ export declare const FAST_CONFIG_SET_KEYS: readonly ["fast.enabled", "fast.provider", "fast.model", "fast.baseUrl", "fast.thinkingLevel", "fast.delegate", "fast.review", "fast.reviewEveryTurns", "fast.reviewContextTurns", "fast.reviewMaxChars", "fast.reviewMaxPerSession"];
24
46
  /**
25
47
  * Translate a `log.*` dotted key into a patch, or `null` for anything else.
26
48
  *
@@ -43,6 +65,35 @@ export declare function applyLogConfigSet(key: string, value: string): Partial<P
43
65
  * won't stick". `clampTeamConfig` is the one gate both directions pass through.
44
66
  */
45
67
  export declare function applyTeamConfigSet(key: string, value: string): Partial<PersistedConfig> | null;
68
+ /**
69
+ * Translate a `retry.*` dotted key into a patch, or `null` for anything else.
70
+ *
71
+ * The patch is a PARTIAL `retry` section, safe only because
72
+ * `updatePersistedConfig` deep-merges that section — with a shallow merge,
73
+ * setting the retry count would silently reset `respectRetryAfter` and `jitter`.
74
+ *
75
+ * `maxRetries` GOES THROUGH `clampRetryConfig`, NOT `coercePositiveInt`, and that
76
+ * is not a style choice (R-16): `coercePositiveInt` returns its fallback for
77
+ * `n <= 0`, so `config set retry.maxRetries 0` would write **10** — the maximum
78
+ * instead of the kill switch, silently. The whole section is routed through the
79
+ * one gate so no key here can pick up the wrong coercion later; the caller echoes
80
+ * the STORED value, which is what makes an out-of-range input visible.
81
+ */
82
+ export declare function applyRetryConfigSet(key: string, value: string): Partial<PersistedConfig> | null;
83
+ /**
84
+ * Translate a `fast.*` dotted key into a patch, or `null` for anything else.
85
+ *
86
+ * The patch is a PARTIAL `fast` section, safe only because
87
+ * `updatePersistedConfig` deep-merges that section — with a shallow merge,
88
+ * setting the cadence would silently switch the whole tier off (R-10).
89
+ *
90
+ * EVERY NUMERIC AND ENUM KEY ROUTES THROUGH `clampFastConfig`, not through
91
+ * `coercePositiveInt` or a local parse, for the reason `applyRetryConfigSet`
92
+ * records: one gate for both directions means no key here can pick up a
93
+ * different coercion later, and `config set fast.reviewEveryTurns 400` writes
94
+ * the clamped 50 while the caller echoes the STORED value (AC-13).
95
+ */
96
+ export declare function applyFastConfigSet(key: string, value: string): Partial<PersistedConfig> | null;
46
97
  /**
47
98
  * `log.redactSecrets false` is the one setting that lets credentials reach the
48
99
  * disk. It is command-line-only (there is no settings-screen entry) and it says
@@ -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,11 +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_TRANSCRIPT_RETAIN, ARAGON_RENDER_GOVERNOR,
12
- * ARAGON_MAX_RENDER_INTERVAL_MS.
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.
13
16
  *
14
17
  * NOT here: `ARAGON_HOME`. It is read when `config/app-paths.ts` is first
15
18
  * evaluated, which is long before `loadDotenv()` runs, so a `.env` can never