@aragon-agent/cli 0.5.9 → 0.5.11

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 (62) hide show
  1. package/CHANGELOG.md +1157 -1026
  2. package/README.md +123 -0
  3. package/dist/boot/guard.d.ts +135 -0
  4. package/dist/boot/guard.js +218 -0
  5. package/dist/boot/rollback.d.ts +86 -0
  6. package/dist/boot/rollback.js +185 -0
  7. package/dist/cli.js +376 -2
  8. package/dist/commands/builtins.js +118 -0
  9. package/dist/commands/perf.d.ts +5 -1
  10. package/dist/commands/perf.js +43 -1
  11. package/dist/commands/registry.d.ts +17 -0
  12. package/dist/config/app-paths.d.ts +31 -0
  13. package/dist/config/app-paths.js +37 -0
  14. package/dist/config/cli-commands.d.ts +27 -1
  15. package/dist/config/cli-commands.js +51 -2
  16. package/dist/config/env.d.ts +11 -1
  17. package/dist/config/env.js +58 -1
  18. package/dist/config/load.d.ts +30 -0
  19. package/dist/config/load.js +56 -1
  20. package/dist/config/schema.d.ts +91 -0
  21. package/dist/config/schema.js +107 -0
  22. package/dist/config/store.js +13 -1
  23. package/dist/launcher.d.ts +14 -0
  24. package/dist/launcher.js +43 -0
  25. package/dist/logging/logger.d.ts +1 -1
  26. package/dist/ui/App.d.ts +11 -1
  27. package/dist/ui/App.js +88 -2
  28. package/dist/ui/BottomStatusRow.d.ts +25 -3
  29. package/dist/ui/BottomStatusRow.js +10 -7
  30. package/dist/ui/PromptInput.d.ts +10 -0
  31. package/dist/ui/PromptInput.js +41 -48
  32. package/dist/ui/UpdateLine.d.ts +44 -0
  33. package/dist/ui/UpdateLine.js +129 -0
  34. package/dist/ui/editor-reducer.d.ts +62 -0
  35. package/dist/ui/editor-reducer.js +90 -0
  36. package/dist/ui/frame-differ.d.ts +95 -0
  37. package/dist/ui/frame-differ.js +230 -0
  38. package/dist/ui/stdout-frame-writer.d.ts +41 -0
  39. package/dist/ui/stdout-frame-writer.js +102 -0
  40. package/dist/update/classify-failure.d.ts +32 -0
  41. package/dist/update/classify-failure.js +60 -0
  42. package/dist/update/install-lock.d.ts +55 -0
  43. package/dist/update/install-lock.js +184 -0
  44. package/dist/update/install-source.d.ts +97 -0
  45. package/dist/update/install-source.js +218 -0
  46. package/dist/update/installer.d.ts +108 -0
  47. package/dist/update/installer.js +242 -0
  48. package/dist/update/limits.d.ts +111 -0
  49. package/dist/update/limits.js +111 -0
  50. package/dist/update/npm-view.d.ts +62 -0
  51. package/dist/update/npm-view.js +133 -0
  52. package/dist/update/registry.d.ts +52 -0
  53. package/dist/update/registry.js +161 -0
  54. package/dist/update/semver.d.ts +89 -0
  55. package/dist/update/semver.js +295 -0
  56. package/dist/update/service.d.ts +193 -0
  57. package/dist/update/service.js +636 -0
  58. package/dist/update/state.d.ts +103 -0
  59. package/dist/update/state.js +142 -0
  60. package/dist/update/types.d.ts +169 -0
  61. package/dist/update/types.js +68 -0
  62. package/package.json +3 -3
@@ -30,10 +30,51 @@ let resetGovernor = null;
30
30
  export function setPerfResetHook(fn) {
31
31
  resetGovernor = fn;
32
32
  }
33
+ /**
34
+ * Frame-writer counters (tui-input-flicker-fix §5.4).
35
+ *
36
+ * A SECOND MODULE-LEVEL SLOT rather than a prop on `App`, and that is this
37
+ * file's own documented convention rather than a shortcut: the writer is built
38
+ * in `runInteractive`, lives entirely outside React, and its counters never
39
+ * render — so routing them through a 1900-line component's props would buy
40
+ * nothing and cost `App.tsx` plus `app.test.tsx` churn (P1-2).
41
+ *
42
+ * `null` means the writer was never built — `--no-diff-render`, inline mode, or
43
+ * a non-interactive path. `/perf` says so in words, so "the flag is off" and
44
+ * "the writer crashed" never look alike.
45
+ */
46
+ let frameStats = null;
47
+ /** Called by `cli.tsx` after building the writer. Passing `null` detaches. */
48
+ export function setFrameStatsProvider(fn) {
49
+ frameStats = fn;
50
+ }
51
+ export function readFrameStats() {
52
+ return frameStats ? frameStats() : null;
53
+ }
33
54
  function mb(bytes) {
34
55
  return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
35
56
  }
36
- /** Format a snapshot as the five-line block documented in §5.4. */
57
+ function ratio(total, frames) {
58
+ return frames > 0 ? (total / frames).toFixed(1) : '0.0';
59
+ }
60
+ /**
61
+ * The writer line (§5.4), in this function's existing ASCII separator shape.
62
+ *
63
+ * `fallbacks > 0` on a healthy session is the single most useful diagnostic this
64
+ * feature can emit: it means something wrote to stdout behind Ink's back and
65
+ * absolute addressing lost its origin.
66
+ */
67
+ function formatWriterLine() {
68
+ const stats = readFrameStats();
69
+ if (!stats)
70
+ return 'writer full - diff render off';
71
+ const bytesPerFrame = stats.framesTotal > 0 ? Math.round(stats.bytesWritten / stats.framesTotal) : 0;
72
+ return (`writer diff - frames ${stats.framesTotal} ` +
73
+ `(diffed ${stats.framesDiffed}, full ${stats.framesFull}) - ` +
74
+ `lines/frame ${ratio(stats.linesWritten, stats.framesTotal)} - ` +
75
+ `bytes/frame ${bytesPerFrame} - fallbacks ${stats.fallbacks}`);
76
+ }
77
+ /** Format a snapshot as the six-line block documented in §5.4. */
37
78
  export function formatPerfReport(snap) {
38
79
  const caches = renderCacheStats();
39
80
  const eco = snap.rung > 0 ? ' - eco' : '';
@@ -49,6 +90,7 @@ export function formatPerfReport(snap) {
49
90
  `hl ${caches.highlightEntries} (${mb(caches.highlightBytes)}) - ` +
50
91
  `lines ${caches.lineEntries}`,
51
92
  `mode ${snap.mode} - viewport ${snap.viewportRows} rows - offset ${snap.offset}`,
93
+ formatWriterLine(),
52
94
  ].join('\n');
53
95
  }
54
96
  export const perfCommand = {
@@ -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
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * `aragon config get | list | edit | home`, plus the `log.*`, `team.*`,
3
- * `retry.*` and `fast.*` branches of `config set`.
3
+ * `retry.*`, `fast.*` and `update.*` 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 —
@@ -43,6 +43,17 @@ export declare const RETRY_CONFIG_SET_KEYS: readonly ["retry.enabled", "retry.ma
43
43
  * `Set fast.model = haiku`.
44
44
  */
45
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"];
46
+ /**
47
+ * Config keys under `update.` that `aragon config set` accepts (cli-auto-update
48
+ * §4.1).
49
+ *
50
+ * A DEDICATED SETTER RATHER THAN A GENERIC DOTTED WRITER, for the reason the
51
+ * four sets above each record: a generic setter would write `update` as a FLAT
52
+ * string key (`"update.mode": "notify"` at the top level of the file), the
53
+ * section would silently never take effect, and `config set` would still print
54
+ * `Set update.mode = notify`.
55
+ */
56
+ export declare const UPDATE_CONFIG_SET_KEYS: readonly ["update.mode", "update.checkIntervalMs", "update.registry", "update.distTag"];
46
57
  /**
47
58
  * Translate a `log.*` dotted key into a patch, or `null` for anything else.
48
59
  *
@@ -94,6 +105,21 @@ export declare function applyRetryConfigSet(key: string, value: string): Partial
94
105
  * the clamped 50 while the caller echoes the STORED value (AC-13).
95
106
  */
96
107
  export declare function applyFastConfigSet(key: string, value: string): Partial<PersistedConfig> | null;
108
+ /**
109
+ * Translate an `update.*` dotted key into a patch, or `null` for anything else.
110
+ *
111
+ * The patch is a PARTIAL `update` section, safe only because
112
+ * `updatePersistedConfig` deep-merges that section — with a shallow merge,
113
+ * setting the mode would silently reset the interval, the registry mirror and
114
+ * the dist-tag (C-4 / R-10).
115
+ *
116
+ * EVERY KEY ROUTES THROUGH `clampUpdateConfig`, not through a local parse, for
117
+ * the reason `applyRetryConfigSet` and `applyFastConfigSet` both record: one
118
+ * gate for both directions means no key here can pick up a different coercion
119
+ * later, and `config set update.checkIntervalMs 5` writes the clamped 900000
120
+ * while the caller echoes the STORED value (AC-20).
121
+ */
122
+ export declare function applyUpdateConfigSet(key: string, value: string): Partial<PersistedConfig> | null;
97
123
  /**
98
124
  * `log.redactSecrets false` is the one setting that lets credentials reach the
99
125
  * disk. It is command-line-only (there is no settings-screen entry) and it says
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * `aragon config get | list | edit | home`, plus the `log.*`, `team.*`,
3
- * `retry.*` and `fast.*` branches of `config set`.
3
+ * `retry.*`, `fast.*` and `update.*` 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 { clampFastConfig, clampRetryConfig, coercePositiveInt, maskSecret, DEFAULT_FAST_CONFIG, DEFAULT_RETRY_CONFIG, DEFAULT_TEAM_CONFIG, } from './schema.js';
16
+ import { clampFastConfig, clampRetryConfig, clampUpdateConfig, coercePositiveInt, maskSecret, DEFAULT_FAST_CONFIG, DEFAULT_RETRY_CONFIG, DEFAULT_TEAM_CONFIG, DEFAULT_UPDATE_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 = [
@@ -87,6 +87,22 @@ export const FAST_CONFIG_SET_KEYS = [
87
87
  'fast.reviewMaxChars',
88
88
  'fast.reviewMaxPerSession',
89
89
  ];
90
+ /**
91
+ * Config keys under `update.` that `aragon config set` accepts (cli-auto-update
92
+ * §4.1).
93
+ *
94
+ * A DEDICATED SETTER RATHER THAN A GENERIC DOTTED WRITER, for the reason the
95
+ * four sets above each record: a generic setter would write `update` as a FLAT
96
+ * string key (`"update.mode": "notify"` at the top level of the file), the
97
+ * section would silently never take effect, and `config set` would still print
98
+ * `Set update.mode = notify`.
99
+ */
100
+ export const UPDATE_CONFIG_SET_KEYS = [
101
+ 'update.mode',
102
+ 'update.checkIntervalMs',
103
+ 'update.registry',
104
+ 'update.distTag',
105
+ ];
90
106
  function isTrue(value) {
91
107
  return value === 'true' || value === '1';
92
108
  }
@@ -245,6 +261,39 @@ export function applyFastConfigSet(key, value) {
245
261
  return null;
246
262
  }
247
263
  }
264
+ /**
265
+ * Translate an `update.*` dotted key into a patch, or `null` for anything else.
266
+ *
267
+ * The patch is a PARTIAL `update` section, safe only because
268
+ * `updatePersistedConfig` deep-merges that section — with a shallow merge,
269
+ * setting the mode would silently reset the interval, the registry mirror and
270
+ * the dist-tag (C-4 / R-10).
271
+ *
272
+ * EVERY KEY ROUTES THROUGH `clampUpdateConfig`, not through a local parse, for
273
+ * the reason `applyRetryConfigSet` and `applyFastConfigSet` both record: one
274
+ * gate for both directions means no key here can pick up a different coercion
275
+ * later, and `config set update.checkIntervalMs 5` writes the clamped 900000
276
+ * while the caller echoes the STORED value (AC-20).
277
+ */
278
+ export function applyUpdateConfigSet(key, value) {
279
+ const patch = (update) => ({ update });
280
+ const clampOne = (field) => {
281
+ const merged = clampUpdateConfig({ ...DEFAULT_UPDATE_CONFIG, [field]: value });
282
+ return { [field]: merged[field] };
283
+ };
284
+ switch (key) {
285
+ case 'update.mode':
286
+ return patch(clampOne('mode'));
287
+ case 'update.checkIntervalMs':
288
+ return patch(clampOne('checkIntervalMs'));
289
+ case 'update.registry':
290
+ return patch(clampOne('registry'));
291
+ case 'update.distTag':
292
+ return patch(clampOne('distTag'));
293
+ default:
294
+ return null;
295
+ }
296
+ }
248
297
  /**
249
298
  * `log.redactSecrets false` is the one setting that lets credentials reach the
250
299
  * disk. It is command-line-only (there is no settings-screen entry) and it says
@@ -12,7 +12,17 @@
12
12
  * ARAGON_FAST_PROVIDER, ARAGON_FAST_MODEL, ARAGON_FAST_BASE_URL,
13
13
  * ARAGON_LOG_LEVEL,
14
14
  * ARAGON_LOG_FILE, ARAGON_LOG_DIR, ARAGON_TRANSCRIPT_RETAIN,
15
- * ARAGON_RENDER_GOVERNOR, ARAGON_MAX_RENDER_INTERVAL_MS.
15
+ * ARAGON_RENDER_GOVERNOR, ARAGON_MAX_RENDER_INTERVAL_MS,
16
+ * ARAGON_DIFF_RENDER, ARAGON_SYNC_OUTPUT,
17
+ * ARAGON_UPDATE.
18
+ *
19
+ * READ ELSEWHERE, ON PURPOSE: `ARAGON_UPDATE_REGISTRY` and `npm_config_registry`
20
+ * are resolved by `update/registry.ts::resolveRegistryUrl`, NOT here, because
21
+ * §3.3 puts them BELOW `config.update.registry` in precedence — the opposite of
22
+ * the env-beats-file rule this file implements. Writing them into `partial`
23
+ * would silently invert that order and make a config-file mirror unreachable on
24
+ * any machine where npm exports its own registry (which is every machine npm
25
+ * runs a script on).
16
26
  *
17
27
  * NOT here: `ARAGON_HOME`. It is read when `config/app-paths.ts` is first
18
28
  * evaluated, which is long before `loadDotenv()` runs, so a `.env` can never
@@ -12,7 +12,17 @@
12
12
  * ARAGON_FAST_PROVIDER, ARAGON_FAST_MODEL, ARAGON_FAST_BASE_URL,
13
13
  * ARAGON_LOG_LEVEL,
14
14
  * ARAGON_LOG_FILE, ARAGON_LOG_DIR, ARAGON_TRANSCRIPT_RETAIN,
15
- * ARAGON_RENDER_GOVERNOR, ARAGON_MAX_RENDER_INTERVAL_MS.
15
+ * ARAGON_RENDER_GOVERNOR, ARAGON_MAX_RENDER_INTERVAL_MS,
16
+ * ARAGON_DIFF_RENDER, ARAGON_SYNC_OUTPUT,
17
+ * ARAGON_UPDATE.
18
+ *
19
+ * READ ELSEWHERE, ON PURPOSE: `ARAGON_UPDATE_REGISTRY` and `npm_config_registry`
20
+ * are resolved by `update/registry.ts::resolveRegistryUrl`, NOT here, because
21
+ * §3.3 puts them BELOW `config.update.registry` in precedence — the opposite of
22
+ * the env-beats-file rule this file implements. Writing them into `partial`
23
+ * would silently invert that order and make a config-file mirror unreachable on
24
+ * any machine where npm exports its own registry (which is every machine npm
25
+ * runs a script on).
16
26
  *
17
27
  * NOT here: `ARAGON_HOME`. It is read when `config/app-paths.ts` is first
18
28
  * evaluated, which is long before `loadDotenv()` runs, so a `.env` can never
@@ -279,6 +289,37 @@ export function readEnvConfig() {
279
289
  if (Object.keys(section).length > 0)
280
290
  partial.fast = section;
281
291
  }
292
+ // ARAGON_UPDATE=0|off|false turns auto-update off; `notify` reports without
293
+ // installing; 1|on|true|yes turns it on (cli-auto-update §4.2).
294
+ //
295
+ // A TRI-STATE, so it is parsed by an EXPLICIT POSITIVE LIST PER OUTCOME rather
296
+ // than by the boolean positive list its neighbours use — and emphatically not
297
+ // by `envBool` below, whose NEGATIVE list would resolve `ARAGON_UPDATE=disable`
298
+ // to `true`.
299
+ //
300
+ // ANYTHING UNRECOGNISED IS LEFT ABSENT, which is the invariant this file
301
+ // states three times over (`ARAGON_TEAM_MAX`, `ARAGON_RETRY_MAX`,
302
+ // `ARAGON_MAX_TOKENS`): a value that resolves to a default looks PERMANENTLY
303
+ // SUPPLIED, and the config file underneath can then never win again. For a
304
+ // kill switch that is the worst possible failure — `ARAGON_UPDATE=disabled`
305
+ // would read as "on" while the user believes they turned it off.
306
+ //
307
+ // `ARAGON_TODO_FOLLOW` is the tri-state precedent: the recognised string is
308
+ // written through and `clampUpdateConfig` stays the single gate, so there is
309
+ // never a second validator that can disagree with the first.
310
+ const update = process.env.ARAGON_UPDATE?.trim().toLowerCase();
311
+ if (update !== undefined && update.length > 0) {
312
+ const mode = update === '0' || update === 'false' || update === 'off' || update === 'no'
313
+ ? 'off'
314
+ : update === 'notify'
315
+ ? 'notify'
316
+ : update === '1' || update === 'true' || update === 'on' || update === 'yes'
317
+ ? 'auto'
318
+ : undefined;
319
+ if (mode !== undefined) {
320
+ partial.update = { mode };
321
+ }
322
+ }
282
323
  // Render bounds (tui-render-performance §5.3). Same channel story as
283
324
  // ARAGON_FULLSCREEN: a container or a wrapper that owns argv can reach neither
284
325
  // a flag nor a config file, and a session that freezes is exactly the case
@@ -307,6 +348,22 @@ export function readEnvConfig() {
307
348
  if (Number.isFinite(parsed) && parsed > 0)
308
349
  partial.maxRenderIntervalMs = parsed;
309
350
  }
351
+ // ARAGON_DIFF_RENDER=0 restores the pre-fix full-frame repaint;
352
+ // ARAGON_SYNC_OUTPUT=0 drops the DEC 2026 envelope (tui-input-flicker-fix
353
+ // §5.3). Parsed by the POSITIVE list, exactly as ARAGON_FULLSCREEN /
354
+ // ARAGON_MOUSE / ARAGON_PLAN / ARAGON_TEAM / ARAGON_TODO / ARAGON_RENDER_GOVERNOR
355
+ // are — NOT by `envBool` below, whose NEGATIVE list disagrees with it on values
356
+ // like `ARAGON_DIFF_RENDER=disable`.
357
+ const diffRender = process.env.ARAGON_DIFF_RENDER?.trim().toLowerCase();
358
+ if (diffRender !== undefined && diffRender.length > 0) {
359
+ partial.diffRender =
360
+ diffRender === '1' || diffRender === 'true' || diffRender === 'on' || diffRender === 'yes';
361
+ }
362
+ const syncOutput = process.env.ARAGON_SYNC_OUTPUT?.trim().toLowerCase();
363
+ if (syncOutput !== undefined && syncOutput.length > 0) {
364
+ partial.syncOutput =
365
+ syncOutput === '1' || syncOutput === 'true' || syncOutput === 'on' || syncOutput === 'yes';
366
+ }
310
367
  const log = readEnvLogConfig();
311
368
  if (log)
312
369
  partial.log = log;
@@ -106,6 +106,19 @@ export interface CliFlags {
106
106
  renderGovernor?: boolean;
107
107
  /** `--max-render-interval <ms>` — the governor ceiling for this run (L4). */
108
108
  maxRenderInterval?: string | number;
109
+ /**
110
+ * `--diff-render` ⇒ true, `--no-diff-render` ⇒ false, absent ⇒ undefined
111
+ * (tui-input-flicker-fix §5.2).
112
+ *
113
+ * DECLARED AS A PAIR for the reason `--render-governor` records one field up:
114
+ * `diffRender` is PERSISTED and defaults to `true`, so a lone
115
+ * `--no-diff-render` would make commander default `opts.diffRender` to `true`
116
+ * and silently overwrite a stored `false` on every run that passed no flag at
117
+ * all — i.e. the documented escape hatch would quietly un-set itself.
118
+ */
119
+ diffRender?: boolean;
120
+ /** `--sync-output` ⇒ true, `--no-sync-output` ⇒ false, absent ⇒ undefined (§5.2). */
121
+ syncOutput?: boolean;
109
122
  /**
110
123
  * `--retry` ⇒ true, `--no-retry` ⇒ false, absent ⇒ undefined
111
124
  * (llm-api-retry-backoff §6.2).
@@ -148,6 +161,23 @@ export interface CliFlags {
148
161
  * to keep in step with `toFlags`.
149
162
  */
150
163
  fastReview?: string;
164
+ /**
165
+ * `--update` ⇒ true, `--no-update` ⇒ false, absent ⇒ undefined
166
+ * (cli-auto-update §4.2).
167
+ *
168
+ * DECLARED AS A PAIR — MANDATORY (C-11), for the reason `--team`,
169
+ * `--todo-panel`, `--render-governor`, `--retry` and `--fast` each record in
170
+ * turn. `update.mode` is PERSISTED and defaults to `'auto'`, so a lone
171
+ * `--no-update` would make commander default `opts.update` to `true` and
172
+ * silently overwrite a stored `off` on every run that passed no flag at all —
173
+ * i.e. the kill switch would un-set itself.
174
+ *
175
+ * A BOOLEAN FOR A TRI-STATE KEY, deliberately: the flag is a SESSION override
176
+ * and `notify` is a preference, not something a user reaches for once. `true`
177
+ * maps to `'auto'`, `false` to `'off'`; `notify` is reachable through the
178
+ * config file and `ARAGON_UPDATE=notify`.
179
+ */
180
+ update?: boolean;
151
181
  }
152
182
  /**
153
183
  * Resolve the effective config by merging all layers, then derive the runtime
@@ -6,7 +6,7 @@
6
6
  * legitimately long single tool run can never be watchdog-killed.
7
7
  */
8
8
  import process from 'node:process';
9
- import { DEFAULT_CONFIG, IDLE_TIMEOUT_MARGIN_MS, clampAskRounds, clampDensity, clampHumanTimeout, clampLogConfig, clampFastConfig, clampRetryConfig, clampSkillsConfig, clampTeamConfig, clampTodoConfig, clampTheme, clampThinkingLevel, clampMaxTokens, clampMaxRenderInterval, clampTranscriptRetain, clampTranscriptWindow, coercePositiveInt, isAutoToken, isSkillsToolPolicyMode, } from './schema.js';
9
+ import { DEFAULT_CONFIG, IDLE_TIMEOUT_MARGIN_MS, clampAskRounds, clampDensity, clampHumanTimeout, clampLogConfig, clampFastConfig, clampRetryConfig, clampSkillsConfig, clampTeamConfig, clampTodoConfig, clampUpdateConfig, 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';
@@ -185,6 +185,29 @@ function resolveFastConfig(flags, env, file) {
185
185
  ...reviewPatch,
186
186
  });
187
187
  }
188
+ /**
189
+ * Merge the `update` section across all four layers (defaults › file › env ›
190
+ * flags), cli-auto-update §4.2.
191
+ *
192
+ * `!== undefined` ON `flags.update`, which is the rule `resolveTeamConfig`,
193
+ * `resolveTodoConfig`, `resolveRetryConfig` and `resolveFastConfig` each state
194
+ * once (C-11): commander materialises a lone `--no-update` as
195
+ * `opts.update = true` when the flag is absent, so a truthiness check cannot
196
+ * tell `--no-update` from "not passed" — and here that would silently re-enable
197
+ * a subsystem the user turned off, which is the one direction this feature must
198
+ * never fail in.
199
+ *
200
+ * The flag maps onto `mode` rather than carrying its own key, so there is
201
+ * exactly one place that says whether the updater runs.
202
+ */
203
+ function resolveUpdateConfig(flags, env, file) {
204
+ return clampUpdateConfig({
205
+ ...DEFAULT_CONFIG.update,
206
+ ...(file.update ?? {}),
207
+ ...(env.update ?? {}),
208
+ ...(flags.update !== undefined ? { mode: flags.update ? 'auto' : 'off' } : {}),
209
+ });
210
+ }
188
211
  /**
189
212
  * Resolve the adaptive render governor: flag › env › file › default `true`
190
213
  * (tui-render-performance L4).
@@ -204,6 +227,35 @@ function resolveRenderGovernor(flags, env, file) {
204
227
  return file.renderGovernor;
205
228
  return DEFAULT_CONFIG.renderGovernor;
206
229
  }
230
+ /**
231
+ * Resolve per-line frame diffing: flag › env › file › default `true`
232
+ * (tui-input-flicker-fix §5.1).
233
+ *
234
+ * Shaped exactly like `resolveRenderGovernor` above, and for the same reason:
235
+ * there are no heuristics for an explicit `true` to override, so a persisted
236
+ * `true` is a value rather than an opinion that has to be told apart from
237
+ * silence. `!== undefined` on the flag is what makes `--no-diff-render` beat a
238
+ * config file that left the key at its default.
239
+ */
240
+ function resolveDiffRender(flags, env, file) {
241
+ if (flags.diffRender !== undefined)
242
+ return flags.diffRender;
243
+ if (env.diffRender !== undefined)
244
+ return env.diffRender;
245
+ if (file.diffRender !== undefined)
246
+ return file.diffRender;
247
+ return DEFAULT_CONFIG.diffRender;
248
+ }
249
+ /** Same ladder for the DEC 2026 envelope (§5.1). */
250
+ function resolveSyncOutput(flags, env, file) {
251
+ if (flags.syncOutput !== undefined)
252
+ return flags.syncOutput;
253
+ if (env.syncOutput !== undefined)
254
+ return env.syncOutput;
255
+ if (file.syncOutput !== undefined)
256
+ return file.syncOutput;
257
+ return DEFAULT_CONFIG.syncOutput;
258
+ }
207
259
  /** Merge the `log` section across all four layers (defaults › file › env › flags). */
208
260
  function resolveLogConfig(flags, env, file) {
209
261
  const merged = clampLogConfig({
@@ -398,6 +450,8 @@ export function loadConfig(flags = {}) {
398
450
  ...(requestedRetain < transcriptRetain ? { transcriptRetainRequested: requestedRetain } : {}),
399
451
  renderGovernor: resolveRenderGovernor(flags, env.partial, file),
400
452
  maxRenderIntervalMs: clampMaxRenderInterval(pick(flags.maxRenderInterval, env.partial.maxRenderIntervalMs, file.maxRenderIntervalMs), DEFAULT_CONFIG.maxRenderIntervalMs),
453
+ diffRender: resolveDiffRender(flags, env.partial, file),
454
+ syncOutput: resolveSyncOutput(flags, env.partial, file),
401
455
  confirmTools,
402
456
  toolTimeoutMs,
403
457
  idleTimeoutMs,
@@ -428,6 +482,7 @@ export function loadConfig(flags = {}) {
428
482
  todo: resolveTodoConfig(flags, env.partial, file),
429
483
  retry: resolveRetryConfig(flags, env.partial, file),
430
484
  fast: resolveFastConfig(flags, env.partial, file),
485
+ update: resolveUpdateConfig(flags, env.partial, file),
431
486
  cwd,
432
487
  color,
433
488
  colorLevel,