@aragon-agent/cli 0.5.8 → 0.5.10

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 +1157 -747
  2. package/README.md +1552 -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/boot/guard.d.ts +135 -0
  16. package/dist/boot/guard.js +218 -0
  17. package/dist/boot/rollback.d.ts +86 -0
  18. package/dist/boot/rollback.js +185 -0
  19. package/dist/cli.js +368 -3
  20. package/dist/commands/builtins.d.ts +1 -1
  21. package/dist/commands/builtins.js +426 -2
  22. package/dist/commands/registry.d.ts +17 -0
  23. package/dist/config/app-paths.d.ts +31 -0
  24. package/dist/config/app-paths.js +37 -0
  25. package/dist/config/cli-commands.d.ts +79 -2
  26. package/dist/config/cli-commands.js +185 -3
  27. package/dist/config/env.d.ts +15 -3
  28. package/dist/config/env.js +143 -3
  29. package/dist/config/load.d.ts +66 -0
  30. package/dist/config/load.js +107 -1
  31. package/dist/config/schema.d.ts +268 -2
  32. package/dist/config/schema.js +288 -1
  33. package/dist/config/store.js +32 -1
  34. package/dist/fast/critique.d.ts +43 -0
  35. package/dist/fast/critique.js +62 -0
  36. package/dist/fast/digest.d.ts +42 -0
  37. package/dist/fast/digest.js +85 -0
  38. package/dist/fast/frames.d.ts +99 -0
  39. package/dist/fast/frames.js +180 -0
  40. package/dist/fast/limits.d.ts +98 -0
  41. package/dist/fast/limits.js +98 -0
  42. package/dist/fast/prompt.d.ts +72 -0
  43. package/dist/fast/prompt.js +94 -0
  44. package/dist/fast/resolve.d.ts +43 -0
  45. package/dist/fast/resolve.js +105 -0
  46. package/dist/fast/review-call.d.ts +99 -0
  47. package/dist/fast/review-call.js +130 -0
  48. package/dist/fast/reviewer.d.ts +248 -0
  49. package/dist/fast/reviewer.js +664 -0
  50. package/dist/fast/types.d.ts +128 -0
  51. package/dist/fast/types.js +14 -0
  52. package/dist/fast/wiring.d.ts +170 -0
  53. package/dist/fast/wiring.js +316 -0
  54. package/dist/launcher.d.ts +14 -0
  55. package/dist/launcher.js +43 -0
  56. package/dist/logging/install.d.ts +15 -1
  57. package/dist/logging/install.js +37 -4
  58. package/dist/logging/logger.d.ts +1 -1
  59. package/dist/session/persist.js +57 -1
  60. package/dist/team/normalize.d.ts +26 -1
  61. package/dist/team/normalize.js +23 -3
  62. package/dist/team/prompt.d.ts +11 -0
  63. package/dist/team/prompt.js +10 -0
  64. package/dist/team/report.d.ts +18 -0
  65. package/dist/team/report.js +27 -1
  66. package/dist/team/runtime.d.ts +13 -2
  67. package/dist/team/runtime.js +33 -6
  68. package/dist/team/subagent.d.ts +19 -1
  69. package/dist/team/subagent.js +72 -10
  70. package/dist/team/task-tool.d.ts +26 -0
  71. package/dist/team/task-tool.js +48 -4
  72. package/dist/team/types.d.ts +80 -1
  73. package/dist/tools/bash-tool.js +19 -3
  74. package/dist/tools/diff.d.ts +25 -5
  75. package/dist/tools/diff.js +72 -41
  76. package/dist/tools/file-change-store.d.ts +34 -0
  77. package/dist/tools/file-change-store.js +57 -0
  78. package/dist/tools/fs-tools.d.ts +26 -0
  79. package/dist/tools/fs-tools.js +62 -5
  80. package/dist/tools/index.d.ts +31 -0
  81. package/dist/tools/index.js +7 -1
  82. package/dist/tools/patch.d.ts +115 -0
  83. package/dist/tools/patch.js +453 -0
  84. package/dist/tools/terminal-output.d.ts +107 -0
  85. package/dist/tools/terminal-output.js +198 -0
  86. package/dist/tools/tool-output-store.d.ts +68 -0
  87. package/dist/tools/tool-output-store.js +102 -0
  88. package/dist/ui/ActivityLine.d.ts +48 -0
  89. package/dist/ui/ActivityLine.js +22 -0
  90. package/dist/ui/App.d.ts +11 -4
  91. package/dist/ui/App.js +359 -37
  92. package/dist/ui/BottomStatusRow.d.ts +65 -0
  93. package/dist/ui/BottomStatusRow.js +17 -0
  94. package/dist/ui/Composer.d.ts +1 -6
  95. package/dist/ui/Composer.js +2 -2
  96. package/dist/ui/PromptInput.d.ts +1 -15
  97. package/dist/ui/PromptInput.js +1 -38
  98. package/dist/ui/StatusBar.d.ts +37 -0
  99. package/dist/ui/StatusBar.js +5 -3
  100. package/dist/ui/TeamPanel.d.ts +5 -4
  101. package/dist/ui/TeamPanel.js +1 -1
  102. package/dist/ui/TodoPanel.d.ts +5 -4
  103. package/dist/ui/Transcript.d.ts +26 -3
  104. package/dist/ui/Transcript.js +76 -10
  105. package/dist/ui/UpdateLine.d.ts +44 -0
  106. package/dist/ui/UpdateLine.js +129 -0
  107. package/dist/ui/activity-phrases.d.ts +33 -0
  108. package/dist/ui/activity-phrases.js +50 -0
  109. package/dist/ui/entries/AssistantEntry.d.ts +20 -0
  110. package/dist/ui/entries/AssistantEntry.js +3 -2
  111. package/dist/ui/entries/DiffView.d.ts +67 -0
  112. package/dist/ui/entries/DiffView.js +103 -0
  113. package/dist/ui/entries/FastCard.d.ts +53 -0
  114. package/dist/ui/entries/FastCard.js +87 -0
  115. package/dist/ui/entries/RetryCard.d.ts +56 -0
  116. package/dist/ui/entries/RetryCard.js +60 -0
  117. package/dist/ui/entries/TeamCard.js +6 -1
  118. package/dist/ui/entries/ToolCard.d.ts +38 -1
  119. package/dist/ui/entries/ToolCard.js +68 -5
  120. package/dist/ui/glyphs.d.ts +9 -0
  121. package/dist/ui/glyphs.js +2 -0
  122. package/dist/ui/layout/AppShell.d.ts +13 -22
  123. package/dist/ui/layout/AppShell.js +3 -38
  124. package/dist/ui/layout/virtual-window.d.ts +1 -1
  125. package/dist/ui/layout/virtual-window.js +109 -5
  126. package/dist/ui/overlays/HelpOverlay.js +16 -2
  127. package/dist/ui/overlays/SettingsScreen.d.ts +82 -1
  128. package/dist/ui/overlays/SettingsScreen.js +147 -1
  129. package/dist/ui/transcript-text.js +27 -0
  130. package/dist/ui/use-wheel-routing.d.ts +16 -32
  131. package/dist/ui/use-wheel-routing.js +21 -38
  132. package/dist/update/classify-failure.d.ts +32 -0
  133. package/dist/update/classify-failure.js +60 -0
  134. package/dist/update/install-lock.d.ts +55 -0
  135. package/dist/update/install-lock.js +184 -0
  136. package/dist/update/install-source.d.ts +97 -0
  137. package/dist/update/install-source.js +218 -0
  138. package/dist/update/installer.d.ts +108 -0
  139. package/dist/update/installer.js +242 -0
  140. package/dist/update/limits.d.ts +111 -0
  141. package/dist/update/limits.js +111 -0
  142. package/dist/update/npm-view.d.ts +62 -0
  143. package/dist/update/npm-view.js +133 -0
  144. package/dist/update/registry.d.ts +52 -0
  145. package/dist/update/registry.js +161 -0
  146. package/dist/update/semver.d.ts +89 -0
  147. package/dist/update/semver.js +295 -0
  148. package/dist/update/service.d.ts +193 -0
  149. package/dist/update/service.js +636 -0
  150. package/dist/update/state.d.ts +103 -0
  151. package/dist/update/state.js +142 -0
  152. package/dist/update/types.d.ts +169 -0
  153. package/dist/update/types.js +68 -0
  154. package/package.json +3 -3
  155. package/dist/ui/layout/regions.d.ts +0 -55
  156. package/dist/ui/layout/regions.js +0 -52
@@ -1,7 +1,7 @@
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 { CommandRegistry } from './registry.js';
7
7
  export declare function registerBuiltinCommands(registry: CommandRegistry): void;
@@ -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,115 @@ 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
+ /**
35
+ * `/update status` - the human-readable readout (cli-auto-update §4.4).
36
+ *
37
+ * ASCII ONLY and NO GLYPHS: this string goes into a transcript `notice`, which
38
+ * `Transcript.tsx` renders with its own marker; the glyph tier belongs to the
39
+ * bottom-row line, not here. Everything a user could want to act on is named:
40
+ * what is running, what is available, how it got installed, and when we look
41
+ * again.
42
+ */
43
+ function formatUpdateStatus(port, override) {
44
+ const snapshot = override ?? port.snapshot();
45
+ const lines = [
46
+ `Auto-update: ${snapshot.phase}`,
47
+ ` running ${snapshot.currentVersion}`,
48
+ ` latest ${snapshot.latestVersion ?? 'unknown'}`,
49
+ ` source ${snapshot.source}`,
50
+ ];
51
+ if (snapshot.reason)
52
+ lines.push(` reason ${snapshot.reason}`);
53
+ if (snapshot.advice)
54
+ lines.push(` install ${snapshot.advice}`);
55
+ // The three hardening lines, each only when it has something to say
56
+ // (cli-auto-update-hardening section 6.3).
57
+ //
58
+ // THE KEY COLUMN IS TEN WIDE, WHICH IS WHY THE FIRST KEY IS `rollback` AND NOT
59
+ // `rolledback`. Section 6.3 asks for "a padded key, matching the six lines
60
+ // `formatUpdateStatus` already emits", and every one of those pads the key to
61
+ // ten so the values start in the same column - the entire reason a readout is
62
+ // padded at all. `rolledback` is ten characters on its own, so it cannot take
63
+ // a separating space without pushing its value one column past the other six,
64
+ // which is the misalignment the padding exists to prevent. The spec's sample
65
+ // block is internally consistent (all three new lines at eleven) and
66
+ // inconsistent with the six it says it matches; the prose is the requirement.
67
+ //
68
+ // THE ROLLBACK LINE CARRIES NO FAILURE COUNT on purpose: `bootFailures` is
69
+ // zeroed by the rollback itself, so any number printed here would be `0`. The
70
+ // count that mattered is in the `update_rolled_back` log record.
71
+ if (snapshot.rolledBackFrom) {
72
+ const to = snapshot.rolledBackTo ?? snapshot.currentVersion;
73
+ lines.push(` rollback ${snapshot.rolledBackFrom} -> ${to}`);
74
+ }
75
+ // What `aragon update --rollback` would reinstall. This is the only place a
76
+ // user can find that out BEFORE running the command.
77
+ if (snapshot.lastGoodVersion)
78
+ lines.push(` lastgood ${snapshot.lastGoodVersion}`);
79
+ // The only place a user can learn their machine updates through the fallback.
80
+ if (snapshot.probe) {
81
+ lines.push(` probe ${snapshot.probe === 'npm' ? 'npm (proxy fallback)' : 'http'}`);
82
+ }
83
+ if (snapshot.consecutiveFailures > 0) {
84
+ lines.push(` failures ${snapshot.consecutiveFailures} in a row`);
85
+ }
86
+ const next = override ? port.nextCheckAt() : snapshot.nextCheckAt;
87
+ lines.push(` next ${next ? new Date(next).toLocaleTimeString() : 'not scheduled'}`);
88
+ return lines.join('\n');
89
+ }
90
+ function formatFastStatus(controller) {
91
+ const status = controller.getFastStatus();
92
+ const cfg = controller.getFastConfig();
93
+ const state = !status.registered
94
+ ? 'off for this session (not configured at launch)'
95
+ : status.enabled
96
+ ? 'on'
97
+ : 'off';
98
+ const lines = [];
99
+ if (status.tier.ok) {
100
+ const same = status.tier.sameAsMain ? ' (same as main)' : '';
101
+ lines.push(`Fast tier: ${state}. Model: ${status.tier.ref.providerId}:${status.tier.ref.modelId}${same}.`);
102
+ }
103
+ else {
104
+ lines.push(`Fast tier: ${state}. Model: ${cfg.model || '(not set)'}.`);
105
+ }
106
+ lines.push(`Review: ${cfg.review ? `every ${cfg.reviewEveryTurns} turns` : 'off'}. ` +
107
+ `Delegation: ${cfg.delegate ? 'on' : 'off'}.`);
108
+ const snapshot = status.snapshot;
109
+ const cost = snapshot.pricingUnknown
110
+ ? `unknown (no price table for ${snapshot.model})`
111
+ : formatCost(computeCost(snapshot.usage, status.tier.ok ? controller.getModelInfoFor(status.tier.ref).cost : undefined));
112
+ // `n/N`, NEVER A BARE `n` (fast-model-tier-hardening §4.3). A bare count
113
+ // answers "what have I spent" only for a reader who already knows the
114
+ // ceiling, which is precisely what the user does not know; `12/40` answers
115
+ // that and "how much is left" in the same three characters, and makes the
116
+ // budget discoverable to someone who never read the config schema. The
117
+ // exhausted form names the remedy inline, because a status line that reports
118
+ // a stop without its escape hatch just relocates the question.
119
+ //
120
+ // `budgetReached` is READ, not re-derived (RV-H10): with the tier off both
121
+ // numbers are `0` placeholders and `reviews >= reviewBudget` would be true.
122
+ const budget = snapshot.budgetReached ? ' (budget reached - raise with /fast budget <n>)' : '';
123
+ lines.push(`This session: ${snapshot.reviews}/${snapshot.reviewBudget} reviews${budget}, ` +
124
+ `${snapshot.delegated} delegated subagents, ` +
125
+ `in ${formatTokens(snapshot.usage.inputTokens)} / out ` +
126
+ `${formatTokens(snapshot.usage.outputTokens)}, cost ${cost}.`);
127
+ return lines.join('\n');
128
+ }
19
129
  const COMMANDS = [
20
130
  {
21
131
  name: 'help',
@@ -223,6 +333,249 @@ const COMMANDS = [
223
333
  ctx.notify('warn', `Unknown argument "${verb}" - use /team [status|on|off|max <n>].`);
224
334
  },
225
335
  },
336
+ {
337
+ name: 'fast',
338
+ description: 'Fast model tier: status | on | off | model <id> | provider <id> | same | ' +
339
+ 'review <n|off> | budget <n> | delegate on|off',
340
+ /**
341
+ * `/fast on` CANNOT turn the tier on in a session that started without it,
342
+ * for the reason `/team on` above records at length: the tool array is built
343
+ * ONCE and rebuilding it is forbidden (C-2), so a session that grew a schema
344
+ * field mid-flight would be a session whose request payloads change shape
345
+ * between turns.
346
+ *
347
+ * EVERY MUTATING BRANCH DOES THE TWO CALLS PLUS PERSIST that `/todo`
348
+ * documents: `controller.setFastConfig(patch)` updates the object `App`
349
+ * reads at render time AND re-resolves the tier AND rebuilds the prompt;
350
+ * `ctx.persistConfig({ fast: patch })` survives the session. Persist alone
351
+ * reports success and changes nothing until relaunch; the setter alone
352
+ * forgets by morning.
353
+ */
354
+ run: (ctx) => {
355
+ const arg = ctx.args.trim();
356
+ const [rawVerb, value] = arg.split(/\s+/, 2);
357
+ const verb = (rawVerb ?? '').toLowerCase();
358
+ const controller = ctx.controller;
359
+ // Refused mid-dispatch for the reason `/team` records: half a dispatch
360
+ // under one tier and half under another produces a report nothing can
361
+ // afterwards explain. THAT ARGUMENT IS ABOUT WRITES, so the two READS are
362
+ // grouped and answerable (RV-H3): `/fast status` always was, and a bare
363
+ // `/fast budget` reports the same numbers by another name. `/fast budget
364
+ // 80` is a write and is still refused.
365
+ const isRead = verb === 'status' || (verb === 'budget' && !value);
366
+ if (verb && !isRead && controller.isTeamBusy()) {
367
+ ctx.notify('warn', 'Cannot change fast-tier settings mid-dispatch.');
368
+ return;
369
+ }
370
+ const persist = (patch) => {
371
+ controller.setFastConfig(patch);
372
+ ctx.persistConfig({ fast: patch });
373
+ };
374
+ if (!verb || verb === 'status') {
375
+ ctx.notify('info', formatFastStatus(controller));
376
+ return;
377
+ }
378
+ if (verb === 'on' || verb === 'off') {
379
+ const enabled = verb === 'on';
380
+ persist({ enabled });
381
+ if (!controller.isFastRegistered()) {
382
+ // The honest branch, and the whole point of splitting by what was
383
+ // decided at construction (§3.3). It is one sentence, and it keeps
384
+ // AC-2 / AC-3 intact: a session launched without the tier never grows
385
+ // a schema field, whatever the user types afterwards.
386
+ ctx.notify('info', enabled
387
+ ? 'The fast tier is off for this session (not configured at launch). ' +
388
+ 'Saved for next launch.'
389
+ : 'The fast tier is already off for this session. Saved for next launch.');
390
+ return;
391
+ }
392
+ controller.setFastEnabled(enabled);
393
+ ctx.toast('success', `Fast tier ${enabled ? 'on' : 'off'}.`);
394
+ return;
395
+ }
396
+ if (verb === 'model') {
397
+ const raw = (value ?? '').trim();
398
+ if (!raw) {
399
+ ctx.notify('warn', 'Usage: /fast model <id> (or provider:model)');
400
+ return;
401
+ }
402
+ // `provider:model` is accepted because that is how a user thinks about a
403
+ // tier on another vendor, and an unknown provider is rejected with THE
404
+ // MESSAGE `cli.tsx` ALREADY PRINTS rather than a second wording for the
405
+ // same fact (RV-16).
406
+ const colon = raw.indexOf(':');
407
+ if (colon > 0) {
408
+ const provider = raw.slice(0, colon);
409
+ const model = raw.slice(colon + 1);
410
+ if (!isAdapterProvider(provider)) {
411
+ ctx.notify('warn', `Unknown provider "${provider}". Choose: ${ADAPTER_PROVIDERS.join(', ')}.`);
412
+ return;
413
+ }
414
+ persist({ provider, model });
415
+ ctx.toast('success', `Fast model set to ${provider}:${model}.`);
416
+ return;
417
+ }
418
+ persist({ model: raw });
419
+ ctx.toast('success', `Fast model set to ${raw}.`);
420
+ return;
421
+ }
422
+ if (verb === 'provider') {
423
+ const raw = (value ?? '').trim();
424
+ if (!raw) {
425
+ ctx.notify('warn', 'Usage: /fast provider <id>');
426
+ return;
427
+ }
428
+ if (!isAdapterProvider(raw)) {
429
+ ctx.notify('warn', `Unknown provider "${raw}". Choose: ${ADAPTER_PROVIDERS.join(', ')}.`);
430
+ return;
431
+ }
432
+ persist({ provider: raw });
433
+ ctx.toast('success', `Fast provider set to ${raw}.`);
434
+ return;
435
+ }
436
+ if (verb === 'same') {
437
+ // R-e IN ONE WORD. `sameAsMain` is display-only and no branch reads it
438
+ // (R-13): the user is allowed to run both tiers on one model, and the
439
+ // review still costs what it costs.
440
+ const cfg = controller.getConfig();
441
+ persist({
442
+ provider: cfg.provider,
443
+ model: cfg.model,
444
+ baseUrl: cfg.baseUrl ?? '',
445
+ });
446
+ ctx.toast('success', `Fast tier set to ${cfg.provider}:${cfg.model} (same as main).`);
447
+ return;
448
+ }
449
+ if (verb === 'review') {
450
+ const raw = (value ?? '').trim().toLowerCase();
451
+ if (raw === 'off') {
452
+ persist({ review: false });
453
+ ctx.toast('success', 'Fast review off.');
454
+ return;
455
+ }
456
+ const n = Number.parseInt(raw, 10);
457
+ if (!Number.isFinite(n) || n <= 0) {
458
+ ctx.notify('warn', 'Usage: /fast review <n> | off');
459
+ return;
460
+ }
461
+ // Clamped by `setFastConfig` and again on the persist path, so a value
462
+ // outside the range can reach neither the runtime nor the file.
463
+ const applied = controller.setFastConfig({ review: true, reviewEveryTurns: n });
464
+ ctx.persistConfig({
465
+ fast: { review: true, reviewEveryTurns: applied.reviewEveryTurns },
466
+ });
467
+ ctx.toast('success', `Fast review every ${applied.reviewEveryTurns} turns.`);
468
+ return;
469
+ }
470
+ if (verb === 'budget') {
471
+ const snapshot = controller.getFastStatus().snapshot;
472
+ const raw = (value ?? '').trim();
473
+ if (!raw) {
474
+ // The READ. Answerable mid-dispatch (see the guard above), and it
475
+ // reports the same pair `/fast status` renders as `n/N`.
476
+ ctx.notify('info', `Fast review budget: ${snapshot.reviews} of ${snapshot.reviewBudget} used ` +
477
+ 'this session. Use /fast budget <1-500> to raise it.');
478
+ return;
479
+ }
480
+ const n = Number.parseInt(raw, 10);
481
+ if (!Number.isFinite(n) || n <= 0) {
482
+ // Change NOTHING on a typo, the discipline `/max-tokens` sets: a
483
+ // silent resolve to the default discards the number the user meant.
484
+ ctx.notify('warn', 'Usage: /fast budget <1-500>');
485
+ return;
486
+ }
487
+ // BOTH CALLS, IN THIS ORDER (RV-H4 / AC-H17). `setFastConfig` is what
488
+ // makes a raised budget take effect LIVE - the reviewer's `budgetLimit()`
489
+ // reads the controller's in-memory config, never disk - and it also
490
+ // re-emits `tier_changed`, which is how the new denominator reaches the
491
+ // status chip without a restart. See the comment on this command: a
492
+ // persist alone reports success and changes nothing until relaunch.
493
+ const applied = controller.setFastConfig({ reviewMaxPerSession: n });
494
+ ctx.persistConfig({
495
+ fast: { reviewMaxPerSession: applied.reviewMaxPerSession },
496
+ });
497
+ // The APPLIED value, not the requested one, so a clamp is visible.
498
+ ctx.toast('success', `Fast review budget: ${applied.reviewMaxPerSession} per session. ` +
499
+ `${snapshot.reviews} used.`);
500
+ return;
501
+ }
502
+ if (verb === 'delegate') {
503
+ const raw = (value ?? '').trim().toLowerCase();
504
+ if (raw !== 'on' && raw !== 'off') {
505
+ ctx.notify('warn', 'Usage: /fast delegate <on|off>');
506
+ return;
507
+ }
508
+ persist({ delegate: raw === 'on' });
509
+ ctx.toast('success', `Fast delegation ${raw}.`);
510
+ return;
511
+ }
512
+ ctx.notify('warn', `Unknown argument "${verb}" - use /fast ` +
513
+ '[status|on|off|model <id>|provider <id>|same|review <n|off>|budget <n>|' +
514
+ 'delegate on|off].');
515
+ },
516
+ },
517
+ {
518
+ name: 'update',
519
+ description: 'Auto-update: status | now | skip | off',
520
+ /**
521
+ * THE GUARANTEED REPORTING SURFACE for the updater (C-12 / D-18).
522
+ *
523
+ * The bottom row can be preempted by a toast or by the activity line, and it
524
+ * is deliberately last in that precedence (D-2), so a user who is actually
525
+ * working may never see it. This command always answers "is it on, what did
526
+ * it find, and when does it look again".
527
+ *
528
+ * REACHES THE SERVICE THROUGH `ctx.update`, NOT THROUGH `ctx.controller`
529
+ * (C-14 / P0-2). `/fast`, `/team` and `/todo` ride on the controller because
530
+ * their subsystems live there; `update/` may not import `agent/` at all, so
531
+ * it carries its own optional port. `undefined` is a legitimate state - `off`,
532
+ * a non-TTY, or CI - and says so rather than pretending to have failed.
533
+ */
534
+ run: (ctx) => {
535
+ const arg = ctx.args.trim();
536
+ const [rawVerb] = arg.split(/\s+/, 1);
537
+ const verb = (rawVerb ?? '').toLowerCase();
538
+ const port = ctx.update;
539
+ // `/update off` is the one branch that works WITHOUT a service: turning
540
+ // something off must not require it to be running, or a user who launched
541
+ // with `--no-update` can never make that stick.
542
+ if (verb === 'off') {
543
+ ctx.persistConfig({ update: { mode: 'off' } });
544
+ ctx.toast('success', 'Auto-update off. Takes effect on the next launch.');
545
+ return;
546
+ }
547
+ if (!port) {
548
+ ctx.notify('info', 'Updates are disabled for this session (update.mode: off, a non-TTY, or CI). ' +
549
+ 'Enable with: aragon config set update.mode auto');
550
+ return;
551
+ }
552
+ if (!verb || verb === 'status') {
553
+ ctx.notify('info', formatUpdateStatus(port));
554
+ return;
555
+ }
556
+ if (verb === 'now') {
557
+ // `force: true` bypasses the machine-wide throttle - a human typing this
558
+ // is the one caller entitled to ignore `lastCheckAt`.
559
+ void port
560
+ .checkNow({ force: true })
561
+ .then((snapshot) => ctx.notify('info', formatUpdateStatus(port, snapshot)))
562
+ .catch(() => ctx.notify('warn', 'Update check failed. See the log for details.'));
563
+ ctx.toast('info', 'Checking for updates...');
564
+ return;
565
+ }
566
+ if (verb === 'skip') {
567
+ const version = port.snapshot().latestVersion;
568
+ if (!version) {
569
+ ctx.notify('info', 'No update is being offered, so there is nothing to skip.');
570
+ return;
571
+ }
572
+ port.skip(version);
573
+ ctx.toast('success', `Skipped ${version}. You will hear about the next release.`);
574
+ return;
575
+ }
576
+ ctx.notify('warn', `Unknown argument "${verb}" - use /update [status|now|skip|off].`);
577
+ },
578
+ },
226
579
  {
227
580
  name: 'todo',
228
581
  description: 'Todo planning: status | on | off | panel on|off | follow <mode> | clear | continue',
@@ -377,6 +730,77 @@ const COMMANDS = [
377
730
  '/todo [status|on|off|panel on|off|follow <mode>|clear|continue].');
378
731
  },
379
732
  },
733
+ {
734
+ name: 'retry',
735
+ description: 'API retry: show | on | off | max <n>',
736
+ /**
737
+ * TWO CALLS FOR EVERY MUTATION, IN THIS ORDER, and neither is optional
738
+ * (llm-api-retry-backoff §6.9 / §6.3):
739
+ *
740
+ * `setRetryConfig` replaces the policy on the live `ProviderRegistry`, so
741
+ * the change takes effect in THIS session — and, because
742
+ * `TeamRuntime` holds the same registry instance, for every
743
+ * child too. Without it `/retry off` reports success and
744
+ * changes nothing until the next launch.
745
+ * `persistConfig` is what survives the session.
746
+ *
747
+ * UNLIKE `/team on` AND `/todo on` there is no "off for this session" branch:
748
+ * retry is not a tool, nothing is registered at construction, and there is
749
+ * therefore no state a flag could have made unreachable. `--no-retry` is a
750
+ * starting value, not a permanent verdict.
751
+ */
752
+ run: (ctx) => {
753
+ const arg = ctx.args.trim().toLowerCase();
754
+ const [verb, value] = arg.split(/\s+/, 2);
755
+ const controller = ctx.controller;
756
+ if (!verb || verb === 'show' || verb === 'status') {
757
+ const cfg = controller.getRetryConfig();
758
+ if (!cfg.enabled || cfg.maxRetries <= 0) {
759
+ ctx.notify('info', `API retry: off (maxRetries ${cfg.maxRetries}). Use /retry on or /retry max <n>.`);
760
+ return;
761
+ }
762
+ ctx.notify('info', `API retry: on. Up to ${cfg.maxRetries} retries, ${cfg.initialDelayMs}ms doubling to ` +
763
+ `${cfg.maxDelayMs}ms${cfg.jitter ? ' with jitter' : ''}, ` +
764
+ `giving up after ${Math.round(cfg.maxElapsedMs / 1000)}s. ` +
765
+ `Retry-After ${cfg.respectRetryAfter ? 'honoured' : 'ignored'}.`);
766
+ return;
767
+ }
768
+ if (verb === 'on' || verb === 'off') {
769
+ const enabled = verb === 'on';
770
+ const applied = controller.setRetryConfig({ enabled });
771
+ // A PARTIAL `retry` section, safe only because `updatePersistedConfig`
772
+ // deep-merges it — the same contract `/team on` relies on.
773
+ ctx.persistConfig({ retry: { enabled } });
774
+ if (enabled && applied.maxRetries <= 0) {
775
+ // Honest rather than cheerful: `enabled` is on but the count is the
776
+ // kill switch, so nothing will actually retry.
777
+ ctx.notify('warn', 'API retry enabled, but maxRetries is 0 - use /retry max <n>.');
778
+ return;
779
+ }
780
+ ctx.toast('success', `API retry ${enabled ? 'on' : 'off'}.`);
781
+ return;
782
+ }
783
+ if (verb === 'max') {
784
+ // `Number.isFinite` and `>= 0`, NOT `> 0`: `0` is this key's documented
785
+ // floor and its kill switch, and rejecting it here would make `/retry max 0`
786
+ // the one documented control that does not work (R-16).
787
+ const n = Number.parseInt(value ?? '', 10);
788
+ if (!Number.isFinite(n) || n < 0) {
789
+ ctx.notify('warn', `Usage: /retry max <0-${HARD_MAX_RETRIES}> (0 turns retry off)`);
790
+ return;
791
+ }
792
+ const applied = controller.setRetryConfig({ maxRetries: n });
793
+ ctx.persistConfig({
794
+ retry: { maxRetries: applied.maxRetries },
795
+ });
796
+ ctx.toast('success', applied.maxRetries === 0
797
+ ? 'API retry off (maxRetries 0).'
798
+ : `API retry: up to ${applied.maxRetries} retries.`);
799
+ return;
800
+ }
801
+ ctx.notify('warn', `Unknown argument "${verb}" - use /retry [show|on|off|max <n>].`);
802
+ },
803
+ },
380
804
  {
381
805
  name: 'expand',
382
806
  description: 'Expand / collapse a recent tool card (default: last)',
@@ -7,6 +7,7 @@ import type { AgentMode } from '../agent/agent-mode.js';
7
7
  import type { NoticeLevel, Overlay, ToastLevel, ViewAction, ViewState } from '../agent/reducer.js';
8
8
  import type { PersistedConfig } from '../config/schema.js';
9
9
  import type { FollowThroughBudget } from '../todo/follow-through.js';
10
+ import type { UpdateCommandPort } from '../update/types.js';
10
11
  export interface CommandContext {
11
12
  /** Raw argument string after the command word (may be empty). */
12
13
  args: string;
@@ -52,6 +53,22 @@ export interface CommandContext {
52
53
  * change; absent means the clause degrades to naming the ceiling alone.
53
54
  */
54
55
  followBudget?: FollowThroughBudget;
56
+ /**
57
+ * The updater, when this session has one (cli-auto-update §4.4 / C-14 / P0-2).
58
+ *
59
+ * `/fast`, `/team` and `/todo` reach their subsystems through `controller`,
60
+ * because those subsystems live on `AgentController`. This one does not, and
61
+ * by §3.1 it MUST NOT — `update/` may not import `agent/` — so it needs its
62
+ * own field, and this is the only seam a slash command has.
63
+ *
64
+ * OPTIONAL, for two independent reasons. The service is legitimately absent
65
+ * under `update.mode: 'off'`, on a non-TTY and in CI (§3.8), so `/update` has
66
+ * to have something honest to say when it is missing; and a REQUIRED field
67
+ * would break every existing `CommandContext` fixture in this package at
68
+ * compile time, for a feature none of them are about. `followBudget` above is
69
+ * the precedent for an optional, read-at-command-time member.
70
+ */
71
+ update?: UpdateCommandPort;
55
72
  /** Rebuild the system prompt and the dynamic command list after a skill change. */
56
73
  refreshSkills: () => void;
57
74
  /**
@@ -14,6 +14,9 @@
14
14
  * ├── config.json.bak `aragon config edit` pre-edit backup
15
15
  * ├── prompt-history.jsonl submitted prompts, append-only (0600 on POSIX)
16
16
  * ├── state.json UI bookkeeping scalars (0600 on POSIX)
17
+ * ├── update-state.json auto-update coordination (0600 on POSIX)
18
+ * ├── update-install.lock held by the running `npm i -g` child
19
+ * ├── update-install.log that child's stdout+stderr; unlinked on exit
17
20
  * ├── logs/ JSONL logs (0700 on POSIX)
18
21
  * ├── sessions/ /save · /resume
19
22
  * ├── skills/ `aragon skills install` target
@@ -79,6 +82,34 @@ export declare function getConfigBackupPath(): string;
79
82
  export declare function getPromptHistoryPath(): string;
80
83
  /** `<home>/state.json` — UI bookkeeping scalars only; see `ui-state.ts`. */
81
84
  export declare function getUiStatePath(): string;
85
+ /**
86
+ * `<home>/update-state.json` — cross-process auto-update coordination
87
+ * (cli-auto-update §5.2).
88
+ *
89
+ * Deliberately NOT inside `state.json`: that file is documented as UI
90
+ * bookkeeping scalars only (C-6), and this is coordination state that a SECOND
91
+ * aragon process reads to decide whether anyone has already asked the registry.
92
+ * Different lifetime, different failure policy, different reader.
93
+ */
94
+ export declare function getUpdateStatePath(): string;
95
+ /**
96
+ * `<home>/update-install.lock` — the `wx` lock the running installer child holds
97
+ * (cli-auto-update §3.7).
98
+ *
99
+ * A FILE, and a direct child of the root, for the reason the two files above
100
+ * are: `logs/`, `sessions/` and `skills/` are all scan roots, and every extra
101
+ * file inside one is another thing each walk has to recognise and skip.
102
+ */
103
+ export declare function getUpdateLockPath(): string;
104
+ /**
105
+ * `<home>/update-install.log` — the installer child's stdout+stderr
106
+ * (cli-auto-update §3.5 / U-5).
107
+ *
108
+ * A REAL FILE and not a pipe: the child is detached and outlives the parent
109
+ * (D-10), and a pipe whose parent has exited takes EPIPE on the next write. The
110
+ * parent tails it on the child's exit, redacts, logs, and unlinks.
111
+ */
112
+ export declare function getUpdateInstallLogPath(): string;
82
113
  export declare function getLogsDir(): string;
83
114
  export declare function getSessionsDir(): string;
84
115
  /** `<home>` — parent of `skills/`, and where sidecar state such as usage lives. */
@@ -14,6 +14,9 @@
14
14
  * ├── config.json.bak `aragon config edit` pre-edit backup
15
15
  * ├── prompt-history.jsonl submitted prompts, append-only (0600 on POSIX)
16
16
  * ├── state.json UI bookkeeping scalars (0600 on POSIX)
17
+ * ├── update-state.json auto-update coordination (0600 on POSIX)
18
+ * ├── update-install.lock held by the running `npm i -g` child
19
+ * ├── update-install.log that child's stdout+stderr; unlinked on exit
17
20
  * ├── logs/ JSONL logs (0700 on POSIX)
18
21
  * ├── sessions/ /save · /resume
19
22
  * ├── skills/ `aragon skills install` target
@@ -140,6 +143,40 @@ export function getPromptHistoryPath() {
140
143
  export function getUiStatePath() {
141
144
  return join(resolution.root, 'state.json');
142
145
  }
146
+ /**
147
+ * `<home>/update-state.json` — cross-process auto-update coordination
148
+ * (cli-auto-update §5.2).
149
+ *
150
+ * Deliberately NOT inside `state.json`: that file is documented as UI
151
+ * bookkeeping scalars only (C-6), and this is coordination state that a SECOND
152
+ * aragon process reads to decide whether anyone has already asked the registry.
153
+ * Different lifetime, different failure policy, different reader.
154
+ */
155
+ export function getUpdateStatePath() {
156
+ return join(resolution.root, 'update-state.json');
157
+ }
158
+ /**
159
+ * `<home>/update-install.lock` — the `wx` lock the running installer child holds
160
+ * (cli-auto-update §3.7).
161
+ *
162
+ * A FILE, and a direct child of the root, for the reason the two files above
163
+ * are: `logs/`, `sessions/` and `skills/` are all scan roots, and every extra
164
+ * file inside one is another thing each walk has to recognise and skip.
165
+ */
166
+ export function getUpdateLockPath() {
167
+ return join(resolution.root, 'update-install.lock');
168
+ }
169
+ /**
170
+ * `<home>/update-install.log` — the installer child's stdout+stderr
171
+ * (cli-auto-update §3.5 / U-5).
172
+ *
173
+ * A REAL FILE and not a pipe: the child is detached and outlives the parent
174
+ * (D-10), and a pipe whose parent has exited takes EPIPE on the next write. The
175
+ * parent tails it on the child's exit, redacts, logs, and unlinks.
176
+ */
177
+ export function getUpdateInstallLogPath() {
178
+ return join(resolution.root, 'update-install.log');
179
+ }
143
180
  export function getLogsDir() {
144
181
  return join(resolution.root, 'logs');
145
182
  }