@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
@@ -0,0 +1,103 @@
1
+ /**
2
+ * `<home>/update-state.json` (cli-auto-update section 5.2).
3
+ *
4
+ * ASCII ONLY - this tree is inside the glyph scanner's scope (C-2 / C-3).
5
+ *
6
+ * WHY NOT `state.json` (D-14 / C-6). That file is documented as UI bookkeeping
7
+ * SCALARS ONLY, and its header names itself "the most inviting drawer in the
8
+ * codebase". What lives here is CROSS-PROCESS COORDINATION state with a
9
+ * completely different lifetime and failure policy: ten terminals read it to
10
+ * decide whether anyone has already asked the registry today. The rule exists
11
+ * precisely to stop the merge that looks convenient today.
12
+ *
13
+ * FAILURE POLICY: copies `ui-state.ts` verbatim - per-field tolerance, whole
14
+ * object fallback on a wrong `schema`, atomic temp-file + rename + `chmod 0600`,
15
+ * and NEVER THROWS. Losing the file costs one extra registry request.
16
+ *
17
+ * NO PROCESS-LIFETIME CACHE, unlike `ui-state.ts`. The whole point of
18
+ * `lastCheckAt` is that a SECOND aragon process wrote it (section 3.7), so a
19
+ * value memoized at first read would defeat the throttle it exists to implement.
20
+ *
21
+ * ==========================================================================
22
+ * C-17 - THIS FILE IS A CROSS-VERSION CHANNEL, AND `readUpdateState` ERASES
23
+ * WHAT IT DOES NOT KNOW.
24
+ *
25
+ * The reader below rebuilds the object FIELD BY FIELD from a fixed list, so an
26
+ * OLDER `aragon` reading and writing this file destroys every field added after
27
+ * it shipped. Two consequences, and both have already been reasoned through
28
+ * (cli-auto-update-hardening section 7.1 / D-27 / D-28):
29
+ *
30
+ * 1. DO NOT BUMP `UPDATE_STATE_SCHEMA` TO ADD A FIELD. A bump makes the reader
31
+ * fall back to the WHOLE default object on the first read after an upgrade
32
+ * (see the `parsed.schema !== ...` line), which discards `skippedVersion` -
33
+ * the field that stops an `install-ineffective` loop (R-12) and a
34
+ * bad-release loop (H1) - on every machine at once. Per-field tolerance
35
+ * already handles absence: a missing field reads as its default.
36
+ * 2. A ROLLBACK MUST LATCH THROUGH `skippedVersion`, which is a schema-1 field
37
+ * every shipped version honours. Latching only in one of the four fields
38
+ * below would be invisible to a CLI that predates them, which would decide
39
+ * `install` on its next check and reinstall the release that just bricked
40
+ * the machine - once per interval, forever.
41
+ * ==========================================================================
42
+ */
43
+ export declare const UPDATE_STATE_FILENAME = "update-state.json";
44
+ export declare const UPDATE_STATE_SCHEMA = 1;
45
+ export interface UpdateState {
46
+ schema: number;
47
+ /** Epoch ms of the last registry request by ANY process; `0` = never. */
48
+ lastCheckAt: number;
49
+ /** `''` = unknown. */
50
+ lastKnownVersion: string;
51
+ /** Installed and awaiting a relaunch; `''` = none. */
52
+ pendingRestartVersion: string;
53
+ /** `''` = none. Set by `/update skip` and by an ineffective install (U-6). */
54
+ skippedVersion: string;
55
+ consecutiveFailures: number;
56
+ lastFailureAt: number;
57
+ /**
58
+ * The version THIS UPDATER installed and has not yet seen boot healthy.
59
+ * `''` = the boot guard is disarmed, which is every launch on every machine
60
+ * except the first one after an auto-update.
61
+ */
62
+ autoInstalledVersion: string;
63
+ /**
64
+ * The version that was running when the last auto-install ran, or the last
65
+ * version seen to boot healthy. THE ROLLBACK TARGET.
66
+ */
67
+ lastGoodVersion: string;
68
+ /**
69
+ * Launches of `autoInstalledVersion` that EXITED NON-ZERO. Reset by
70
+ * `markBootHealthy`, by a rollback, and by each new arming.
71
+ *
72
+ * NAMED FOR FAILURES AND NOT FOR ATTEMPTS, and the name is the specification
73
+ * (P1-5). A counter named for attempts invites an increment at boot, and an
74
+ * increment at boot has no per-process identity: three terminals starting
75
+ * inside the window between the first one's increment and its
76
+ * `markBootHealthy` walk it to the threshold, and the third rolls back a
77
+ * release running fine in the other two. A counter named for failures can only
78
+ * be incremented where a failure is observable, which is the exit hook.
79
+ */
80
+ bootFailures: number;
81
+ /**
82
+ * The version the last rollback moved away from. Written by `performRollback`
83
+ * and consumed EXACTLY ONCE by `UpdateService.start()`. `''` = nothing to
84
+ * report.
85
+ */
86
+ rolledBackFrom: string;
87
+ }
88
+ export declare const DEFAULT_UPDATE_STATE: UpdateState;
89
+ /**
90
+ * Per-field tolerance: one nonsense value must not take the others with it, and
91
+ * a field this version has never heard of reads as its default rather than
92
+ * failing the parse. A wrong SCHEMA is different - the shape is no longer known,
93
+ * so the whole object falls back (the rule `ui-state.ts` and `skills/usage.ts`
94
+ * follow). See C-17 in the header before adding a field OR touching the schema.
95
+ */
96
+ export declare function readUpdateState(): UpdateState;
97
+ /**
98
+ * Read-modify-write. READ-MODIFY-WRITE AND NOT WRITE, deliberately: another
99
+ * `aragon` may have recorded a check or an install between our read and this
100
+ * call, and clobbering its `lastCheckAt` is how ten terminals turn back into ten
101
+ * registry requests.
102
+ */
103
+ export declare function updateUpdateState(patch: Partial<Omit<UpdateState, 'schema'>>): UpdateState;
@@ -0,0 +1,142 @@
1
+ /**
2
+ * `<home>/update-state.json` (cli-auto-update section 5.2).
3
+ *
4
+ * ASCII ONLY - this tree is inside the glyph scanner's scope (C-2 / C-3).
5
+ *
6
+ * WHY NOT `state.json` (D-14 / C-6). That file is documented as UI bookkeeping
7
+ * SCALARS ONLY, and its header names itself "the most inviting drawer in the
8
+ * codebase". What lives here is CROSS-PROCESS COORDINATION state with a
9
+ * completely different lifetime and failure policy: ten terminals read it to
10
+ * decide whether anyone has already asked the registry today. The rule exists
11
+ * precisely to stop the merge that looks convenient today.
12
+ *
13
+ * FAILURE POLICY: copies `ui-state.ts` verbatim - per-field tolerance, whole
14
+ * object fallback on a wrong `schema`, atomic temp-file + rename + `chmod 0600`,
15
+ * and NEVER THROWS. Losing the file costs one extra registry request.
16
+ *
17
+ * NO PROCESS-LIFETIME CACHE, unlike `ui-state.ts`. The whole point of
18
+ * `lastCheckAt` is that a SECOND aragon process wrote it (section 3.7), so a
19
+ * value memoized at first read would defeat the throttle it exists to implement.
20
+ *
21
+ * ==========================================================================
22
+ * C-17 - THIS FILE IS A CROSS-VERSION CHANNEL, AND `readUpdateState` ERASES
23
+ * WHAT IT DOES NOT KNOW.
24
+ *
25
+ * The reader below rebuilds the object FIELD BY FIELD from a fixed list, so an
26
+ * OLDER `aragon` reading and writing this file destroys every field added after
27
+ * it shipped. Two consequences, and both have already been reasoned through
28
+ * (cli-auto-update-hardening section 7.1 / D-27 / D-28):
29
+ *
30
+ * 1. DO NOT BUMP `UPDATE_STATE_SCHEMA` TO ADD A FIELD. A bump makes the reader
31
+ * fall back to the WHOLE default object on the first read after an upgrade
32
+ * (see the `parsed.schema !== ...` line), which discards `skippedVersion` -
33
+ * the field that stops an `install-ineffective` loop (R-12) and a
34
+ * bad-release loop (H1) - on every machine at once. Per-field tolerance
35
+ * already handles absence: a missing field reads as its default.
36
+ * 2. A ROLLBACK MUST LATCH THROUGH `skippedVersion`, which is a schema-1 field
37
+ * every shipped version honours. Latching only in one of the four fields
38
+ * below would be invisible to a CLI that predates them, which would decide
39
+ * `install` on its next check and reinstall the release that just bricked
40
+ * the machine - once per interval, forever.
41
+ * ==========================================================================
42
+ */
43
+ import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
44
+ import process from 'node:process';
45
+ import { getHomeRoot, getUpdateStatePath } from '../config/app-paths.js';
46
+ export const UPDATE_STATE_FILENAME = 'update-state.json';
47
+ export const UPDATE_STATE_SCHEMA = 1;
48
+ export const DEFAULT_UPDATE_STATE = {
49
+ schema: UPDATE_STATE_SCHEMA,
50
+ lastCheckAt: 0,
51
+ lastKnownVersion: '',
52
+ pendingRestartVersion: '',
53
+ skippedVersion: '',
54
+ consecutiveFailures: 0,
55
+ lastFailureAt: 0,
56
+ autoInstalledVersion: '',
57
+ lastGoodVersion: '',
58
+ bootFailures: 0,
59
+ rolledBackFrom: '',
60
+ };
61
+ function count(value, fallback) {
62
+ const n = typeof value === 'number' ? value : Number.parseInt(String(value ?? ''), 10);
63
+ if (!Number.isFinite(n) || n < 0)
64
+ return fallback;
65
+ return Math.floor(n);
66
+ }
67
+ function text(value, fallback) {
68
+ return typeof value === 'string' ? value : fallback;
69
+ }
70
+ /**
71
+ * Per-field tolerance: one nonsense value must not take the others with it, and
72
+ * a field this version has never heard of reads as its default rather than
73
+ * failing the parse. A wrong SCHEMA is different - the shape is no longer known,
74
+ * so the whole object falls back (the rule `ui-state.ts` and `skills/usage.ts`
75
+ * follow). See C-17 in the header before adding a field OR touching the schema.
76
+ */
77
+ export function readUpdateState() {
78
+ const path = getUpdateStatePath();
79
+ if (!existsSync(path))
80
+ return { ...DEFAULT_UPDATE_STATE };
81
+ try {
82
+ const parsed = JSON.parse(readFileSync(path, 'utf-8'));
83
+ if (!parsed || typeof parsed !== 'object')
84
+ return { ...DEFAULT_UPDATE_STATE };
85
+ if (parsed.schema !== UPDATE_STATE_SCHEMA)
86
+ return { ...DEFAULT_UPDATE_STATE };
87
+ return {
88
+ schema: UPDATE_STATE_SCHEMA,
89
+ lastCheckAt: count(parsed.lastCheckAt, DEFAULT_UPDATE_STATE.lastCheckAt),
90
+ lastKnownVersion: text(parsed.lastKnownVersion, DEFAULT_UPDATE_STATE.lastKnownVersion),
91
+ pendingRestartVersion: text(parsed.pendingRestartVersion, DEFAULT_UPDATE_STATE.pendingRestartVersion),
92
+ skippedVersion: text(parsed.skippedVersion, DEFAULT_UPDATE_STATE.skippedVersion),
93
+ consecutiveFailures: count(parsed.consecutiveFailures, DEFAULT_UPDATE_STATE.consecutiveFailures),
94
+ lastFailureAt: count(parsed.lastFailureAt, DEFAULT_UPDATE_STATE.lastFailureAt),
95
+ autoInstalledVersion: text(parsed.autoInstalledVersion, DEFAULT_UPDATE_STATE.autoInstalledVersion),
96
+ lastGoodVersion: text(parsed.lastGoodVersion, DEFAULT_UPDATE_STATE.lastGoodVersion),
97
+ bootFailures: count(parsed.bootFailures, DEFAULT_UPDATE_STATE.bootFailures),
98
+ rolledBackFrom: text(parsed.rolledBackFrom, DEFAULT_UPDATE_STATE.rolledBackFrom),
99
+ };
100
+ }
101
+ catch {
102
+ return { ...DEFAULT_UPDATE_STATE };
103
+ }
104
+ }
105
+ /**
106
+ * Atomic write: temp file, rename, re-chmod. Never throws.
107
+ *
108
+ * `mkdirSync` first for the reason `prompt-history.ts` and `ui-state.ts` both
109
+ * record: `<home>` is not guaranteed to exist, and without it the throttle would
110
+ * silently never persist on a fresh install - which presents as one registry
111
+ * request per launch forever.
112
+ */
113
+ function writeUpdateState(state) {
114
+ const path = getUpdateStatePath();
115
+ const tmp = `${path}.tmp-${process.pid}`;
116
+ try {
117
+ mkdirSync(getHomeRoot(), { recursive: true });
118
+ writeFileSync(tmp, `${JSON.stringify(state, null, 2)}\n`, { encoding: 'utf-8', mode: 0o600 });
119
+ renameSync(tmp, path);
120
+ if (process.platform !== 'win32')
121
+ chmodSync(path, 0o600);
122
+ }
123
+ catch {
124
+ try {
125
+ rmSync(tmp, { force: true });
126
+ }
127
+ catch {
128
+ // Nothing further to try; the value simply stays in memory.
129
+ }
130
+ }
131
+ }
132
+ /**
133
+ * Read-modify-write. READ-MODIFY-WRITE AND NOT WRITE, deliberately: another
134
+ * `aragon` may have recorded a check or an install between our read and this
135
+ * call, and clobbering its `lastCheckAt` is how ten terminals turn back into ten
136
+ * registry requests.
137
+ */
138
+ export function updateUpdateState(patch) {
139
+ const next = { ...readUpdateState(), ...patch, schema: UPDATE_STATE_SCHEMA };
140
+ writeUpdateState(next);
141
+ return next;
142
+ }
@@ -0,0 +1,169 @@
1
+ /**
2
+ * The updater's runtime shapes, plus the ONE pure predicate the renderer needs
3
+ * (cli-auto-update sections 5.3 / 5.3a).
4
+ *
5
+ * ASCII ONLY - this tree is inside the glyph scanner's scope (C-2 / C-3).
6
+ *
7
+ * THE ONLY VALUE EXPORTED HERE IS `shouldRenderUpdateLine`. Everything else is a
8
+ * type, so `ui/UpdateLine.tsx` and `commands/registry.ts` can import from this
9
+ * module with `import type` and tsc erases the specifier entirely - no runtime
10
+ * edge from the renderer or the command registry into `update/` (section 3.1
11
+ * rule 1 / P1-7). This module and `limits.ts` import nothing but each other, are
12
+ * side-effect-free, and open no socket, spawn no process and create no timer.
13
+ */
14
+ export type UpdatePhase = 'idle' | 'checking' | 'available' | 'installing' | 'ready' | 'failed';
15
+ /**
16
+ * How the running copy of `aragon` got onto this machine (section 3.2).
17
+ *
18
+ * `npm-global` is the ONLY value that may be auto-installed, because it is the
19
+ * only one whose install target is unambiguous and verifiable from this machine
20
+ * (D-5). Everything else is advice, not execution.
21
+ */
22
+ export type InstallSource = 'dev-monorepo' | 'npx' | 'pnpm' | 'yarn' | 'bun' | 'volta' | 'npm-local' | 'npm-global' | 'unknown';
23
+ /** Why we are not installing, when we are not. Drives the advice line. */
24
+ export type UpdateReason = 'up-to-date' | 'prerelease' | 'deprecated' | 'node-too-old' | 'skipped' | 'not-writable' | 'source-ineligible' | 'network' | 'install-failed' | 'locked'
25
+ /** npm exited 0 but THIS root did not move - see U-6. */
26
+ | 'install-ineffective'
27
+ /** EPERM / EBUSY / ETXTBSY - something holds the file we must replace. */
28
+ | 'blocked-by-os'
29
+ /** ENOSPC. */
30
+ | 'no-space';
31
+ /** The slice of the registry's dist-tag document this feature reads. */
32
+ export interface LatestManifest {
33
+ name: string;
34
+ version: string;
35
+ engines?: {
36
+ node?: string;
37
+ };
38
+ /** Present ONLY when the version is deprecated; the string is the reason. */
39
+ deprecated?: string;
40
+ }
41
+ export interface UpdateSnapshot {
42
+ phase: UpdatePhase;
43
+ currentVersion: string;
44
+ latestVersion: string | null;
45
+ source: InstallSource;
46
+ reason?: UpdateReason;
47
+ /** Copy-pasteable command for an ineligible source. */
48
+ advice?: string;
49
+ /**
50
+ * The `engines.node` range the offered version demands, and the Node actually
51
+ * running - set ONLY on the `node-too-old` path.
52
+ *
53
+ * They exist because section 6.2 specifies that row as
54
+ * `<warn> 0.6.0 needs Node >=20 (running 18.19.0)`, and neither number is
55
+ * derivable from anything else on this object. "Needs a newer Node" without
56
+ * saying WHICH is the kind of notice a user cannot act on, which is exactly
57
+ * what D-7 spends the rest of this feature avoiding (IF-2).
58
+ */
59
+ requiredNode?: string;
60
+ runningNode?: string;
61
+ /**
62
+ * Set for the WHOLE session after a rollback, and deliberately NOT a `reason`
63
+ * (cli-auto-update-hardening section 5.2a / D-39).
64
+ *
65
+ * `reason` is rewritten by `setPhase` on EVERY transition, so the first
66
+ * scheduled check three seconds into the session would erase it before anyone
67
+ * read it. A fact that is true for a whole session needs a carrier the state
68
+ * machine does not own.
69
+ *
70
+ * BOTH, OR NEITHER, on the `requiredNode` / `runningNode` precedent above -
71
+ * half the pair is a notice nobody can read. Here it holds BY CONSTRUCTION
72
+ * rather than by discipline: `rolledBackTo` is the running version, assigned
73
+ * in the same two lines that read `rolledBackFrom` out of the state file.
74
+ */
75
+ rolledBackFrom?: string;
76
+ rolledBackTo?: string;
77
+ /**
78
+ * What `aragon update --rollback` would reinstall, read from the state file.
79
+ *
80
+ * On the snapshot rather than derived because `/update status` is the only
81
+ * place a user can find out what that command would DO before running it
82
+ * (section 6.3). See IF-1 in the hardening spec.
83
+ */
84
+ lastGoodVersion?: string;
85
+ /** Which probe answered the last successful check (H3). */
86
+ probe?: 'http' | 'npm';
87
+ nextCheckAt: number | null;
88
+ consecutiveFailures: number;
89
+ }
90
+ export type UpdateEvent = {
91
+ type: 'phase';
92
+ snapshot: UpdateSnapshot;
93
+ } | {
94
+ type: 'log';
95
+ level: 'info' | 'warn';
96
+ msg: string;
97
+ };
98
+ /**
99
+ * The narrow surface `/update` sees (section 4.4 / C-14 / P0-2).
100
+ *
101
+ * `CommandContext` carries this rather than the service itself, and carries it
102
+ * as an OPTIONAL member: the service is legitimately absent under
103
+ * `update.mode: 'off'`, on a non-TTY and in CI, and a required field would break
104
+ * every existing `CommandContext` fixture at compile time.
105
+ *
106
+ * `UpdateService` satisfies it STRUCTURALLY, so nothing implements it explicitly
107
+ * and there is no adapter to keep in sync.
108
+ */
109
+ export interface UpdateCommandPort {
110
+ snapshot(): UpdateSnapshot;
111
+ checkNow(opts?: {
112
+ force?: boolean;
113
+ }): Promise<UpdateSnapshot>;
114
+ skip(version: string): void;
115
+ nextCheckAt(): number | null;
116
+ }
117
+ /** What the renderer needs on top of the command port: a subscription. */
118
+ export interface UpdateServiceHandle extends UpdateCommandPort {
119
+ subscribe(fn: (snapshot: UpdateSnapshot) => void): () => void;
120
+ }
121
+ /**
122
+ * The late-arrival channel between `cli.tsx` and `App` (IF-4).
123
+ *
124
+ * The service is constructed through a DYNAMIC `import()` fired AFTER
125
+ * `render()` (section 3.8 / D-25), so it does not exist when `App` mounts and
126
+ * cannot be an ordinary prop. This is the same shape `ConfirmBridge` and
127
+ * `HumanInputBridge` already use for exactly the same reason: a mutable object
128
+ * created before `render()`, populated by whichever side arrives second.
129
+ *
130
+ * A PLAIN INTERFACE AND NOT A CLASS, so `cli.tsx` builds one with an object
131
+ * literal and needs no runtime import from `update/` (AC-1).
132
+ */
133
+ export interface UpdateBridge {
134
+ /** Set once the dynamically-imported service exists; `null` until then. */
135
+ service: UpdateServiceHandle | null;
136
+ /** Set by `App` on mount, so a service that arrives later can wake it. */
137
+ onAttach: ((service: UpdateServiceHandle) => void) | null;
138
+ }
139
+ /**
140
+ * The reasons allowed to break the three-strike silence rule (D-7 / D-33),
141
+ * because each is both ACTIONABLE BY THE USER and NOT SELF-HEALING.
142
+ *
143
+ * THE MEMBERSHIP RULE IS THE WHOLE MECHANISM, not the list. `network` fails both
144
+ * halves - the user cannot fix the registry and the next check may well succeed -
145
+ * so it stays silent and backs off, which is the entire reason a background
146
+ * updater is tolerable at all. The three below will otherwise repeat identically
147
+ * until the user does something, which is the definition of a notice worth
148
+ * showing once. Anything added here without passing both halves is the first
149
+ * step of the notification fatigue the rest of this feature is built to avoid
150
+ * (R-20).
151
+ */
152
+ export declare const IMMEDIATE_NOTICE_REASONS: ReadonlySet<UpdateReason>;
153
+ /**
154
+ * Whether the bottom row has anything to say. PURE, and deliberately NOT
155
+ * expressed as `UpdateLine` returning `null` (C-15 / P0-1 / D-19).
156
+ *
157
+ * A component that returns `null` still satisfies `if (update)` at the call site
158
+ * - the prop is a React ELEMENT, and an element is truthy however it renders.
159
+ * The row would become `<Box flexShrink={0}>{nothing}</Box>`, which in
160
+ * full-screen is ZERO rows, and the transcript silently gains a row for as long
161
+ * as the updater is idle and loses it again the moment it has news. That is
162
+ * precisely the layout shift `BottomStatusRow` exists to prevent, and
163
+ * `bottom-status-row.test.tsx`'s "holds one row in all four states" is the
164
+ * assertion it would defeat.
165
+ *
166
+ * `ActivityLine` already settles the pattern: it ALWAYS returns an element, and
167
+ * `App.tsx` decides presence at the call site with `running && !overlayNode`.
168
+ */
169
+ export declare function shouldRenderUpdateLine(snapshot: UpdateSnapshot): boolean;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * The updater's runtime shapes, plus the ONE pure predicate the renderer needs
3
+ * (cli-auto-update sections 5.3 / 5.3a).
4
+ *
5
+ * ASCII ONLY - this tree is inside the glyph scanner's scope (C-2 / C-3).
6
+ *
7
+ * THE ONLY VALUE EXPORTED HERE IS `shouldRenderUpdateLine`. Everything else is a
8
+ * type, so `ui/UpdateLine.tsx` and `commands/registry.ts` can import from this
9
+ * module with `import type` and tsc erases the specifier entirely - no runtime
10
+ * edge from the renderer or the command registry into `update/` (section 3.1
11
+ * rule 1 / P1-7). This module and `limits.ts` import nothing but each other, are
12
+ * side-effect-free, and open no socket, spawn no process and create no timer.
13
+ */
14
+ import { UPDATE_LIMITS } from './limits.js';
15
+ /**
16
+ * The reasons allowed to break the three-strike silence rule (D-7 / D-33),
17
+ * because each is both ACTIONABLE BY THE USER and NOT SELF-HEALING.
18
+ *
19
+ * THE MEMBERSHIP RULE IS THE WHOLE MECHANISM, not the list. `network` fails both
20
+ * halves - the user cannot fix the registry and the next check may well succeed -
21
+ * so it stays silent and backs off, which is the entire reason a background
22
+ * updater is tolerable at all. The three below will otherwise repeat identically
23
+ * until the user does something, which is the definition of a notice worth
24
+ * showing once. Anything added here without passing both halves is the first
25
+ * step of the notification fatigue the rest of this feature is built to avoid
26
+ * (R-20).
27
+ */
28
+ export const IMMEDIATE_NOTICE_REASONS = new Set([
29
+ 'blocked-by-os',
30
+ 'no-space',
31
+ 'not-writable',
32
+ ]);
33
+ /**
34
+ * Whether the bottom row has anything to say. PURE, and deliberately NOT
35
+ * expressed as `UpdateLine` returning `null` (C-15 / P0-1 / D-19).
36
+ *
37
+ * A component that returns `null` still satisfies `if (update)` at the call site
38
+ * - the prop is a React ELEMENT, and an element is truthy however it renders.
39
+ * The row would become `<Box flexShrink={0}>{nothing}</Box>`, which in
40
+ * full-screen is ZERO rows, and the transcript silently gains a row for as long
41
+ * as the updater is idle and loses it again the moment it has news. That is
42
+ * precisely the layout shift `BottomStatusRow` exists to prevent, and
43
+ * `bottom-status-row.test.tsx`'s "holds one row in all four states" is the
44
+ * assertion it would defeat.
45
+ *
46
+ * `ActivityLine` already settles the pattern: it ALWAYS returns an element, and
47
+ * `App.tsx` decides presence at the call site with `running && !overlayNode`.
48
+ */
49
+ export function shouldRenderUpdateLine(snapshot) {
50
+ if (snapshot.phase === 'available' ||
51
+ snapshot.phase === 'installing' ||
52
+ snapshot.phase === 'ready') {
53
+ return true;
54
+ }
55
+ if (snapshot.phase === 'failed') {
56
+ if (snapshot.reason && IMMEDIATE_NOTICE_REASONS.has(snapshot.reason))
57
+ return true;
58
+ return snapshot.consecutiveFailures >= UPDATE_LIMITS.failuresBeforeNotice;
59
+ }
60
+ // `idle` / `checking` - the two phases the user must never be told about,
61
+ // EXCEPT for a rollback this session has not yet reported. Placed LAST on
62
+ // purpose: any real news above owns the row first, and the budget is one row
63
+ // (C-15). It is therefore possible for the notice never to appear on a machine
64
+ // that has an update waiting on the very next launch, which is the correct
65
+ // trade - the newer fact is the more actionable one, and the rollback is still
66
+ // in `/update status` and in the log.
67
+ return Boolean(snapshot.rolledBackFrom);
68
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aragon-agent/cli",
3
- "version": "0.5.8",
3
+ "version": "0.5.10",
4
4
  "description": "AragonAgent CLI — a Claude-Code / Codex-style interactive terminal UI (TUI) on top of the @aragon-agent/core engine. Installable and runnable via npm / npx.",
5
5
  "license": "MIT",
6
6
  "author": "AragonAgent contributors",
@@ -28,7 +28,7 @@
28
28
  },
29
29
  "type": "module",
30
30
  "bin": {
31
- "aragon": "./dist/cli.js"
31
+ "aragon": "./dist/launcher.js"
32
32
  },
33
33
  "files": [
34
34
  "dist",
@@ -52,7 +52,7 @@
52
52
  "prepublishOnly": "npm run build"
53
53
  },
54
54
  "dependencies": {
55
- "@aragon-agent/core": "^0.2.8",
55
+ "@aragon-agent/core": "^0.2.10",
56
56
  "chalk": "^5.6.2",
57
57
  "cli-highlight": "^2.0.0",
58
58
  "commander": "^12.1.0",
@@ -1,55 +0,0 @@
1
- /**
2
- * Wheel band hit-testing (mouse-wheel-region-routing §4.5) — pure, React-free.
3
- *
4
- * The requirement is stated in terms of REGIONS ("scrolling over the agent area
5
- * scrolls the transcript; scrolling over the input box steps prompt history"),
6
- * so the router needs to turn a reported terminal row into one of two bands.
7
- *
8
- * The boundary is MEASURED, never computed from `budget.ts`. That arithmetic
9
- * describes the PLANNED chrome, but the real bottom chrome grows and shrinks at
10
- * run time: the toast row comes and goes, the hint row is dropped below
11
- * `HINT_MIN_ROWS`, and the autocomplete popup adds up to nine rows. Routing off
12
- * the plan would put the boundary in the wrong place exactly when the popup is
13
- * open — the moment precision matters most (D-5 / D-16).
14
- */
15
- export type WheelBand = 'content' | 'composer';
16
- export interface FrameGeometry {
17
- /**
18
- * 1-based row of the frame's first row. 1 in the alternate screen.
19
- *
20
- * This is DERIVED, not assumed, and it has a named dependency: it holds
21
- * because the full-screen transcript renders no `<Static>`
22
- * (`Transcript.tsx`), so Ink's `log-update` writes `frameHeight(rows) = rows-1`
23
- * lines plus a newline into a `rows`-row buffer — nothing scrolls, and the
24
- * frame sits at rows `1 … rows-1` with the cursor parked on the undrawn last
25
- * row. Reintroducing `<Static>` into the full-screen path pushes the frame
26
- * down the screen and silently biases every hit test; whoever does that has
27
- * to change `top` here at the same time.
28
- */
29
- top: number;
30
- /** Rows the frame occupies — `frameHeight(rows)`. */
31
- height: number;
32
- /** Measured rows of the bottom chrome (toast + composer + status). */
33
- bottom: number;
34
- }
35
- /**
36
- * The "I do not know where the frame is" value.
37
- *
38
- * `App` writes it into the geometry ref on every render path that does NOT
39
- * render the full-screen `AppShell` — the terminal-too-small placeholder and
40
- * inline mode — so a stale measurement can never outlive the frame it
41
- * described. There is deliberately no arithmetic fallback: an unmeasured frame
42
- * routes to `'content'`, which is the band that cannot destroy anything, and
43
- * one invisible frame after a resize is a better failure than a wrong band
44
- * (§4.5, P1-5 / D-16).
45
- */
46
- export declare const UNMEASURED_GEOMETRY: FrameGeometry;
47
- /**
48
- * Which band does terminal row `y` belong to?
49
- *
50
- * Fails toward `'content'`: the whole point of this feature is that a wheel
51
- * notch must never edit the user's draft (I-3), so every degenerate input —
52
- * nothing measured yet, a row outside the frame, a chrome measurement that
53
- * swallowed the screen — resolves to the harmless direction.
54
- */
55
- export declare function hitTestWheel(y: number, geometry: FrameGeometry): WheelBand;
@@ -1,52 +0,0 @@
1
- /**
2
- * Wheel band hit-testing (mouse-wheel-region-routing §4.5) — pure, React-free.
3
- *
4
- * The requirement is stated in terms of REGIONS ("scrolling over the agent area
5
- * scrolls the transcript; scrolling over the input box steps prompt history"),
6
- * so the router needs to turn a reported terminal row into one of two bands.
7
- *
8
- * The boundary is MEASURED, never computed from `budget.ts`. That arithmetic
9
- * describes the PLANNED chrome, but the real bottom chrome grows and shrinks at
10
- * run time: the toast row comes and goes, the hint row is dropped below
11
- * `HINT_MIN_ROWS`, and the autocomplete popup adds up to nine rows. Routing off
12
- * the plan would put the boundary in the wrong place exactly when the popup is
13
- * open — the moment precision matters most (D-5 / D-16).
14
- */
15
- /**
16
- * The "I do not know where the frame is" value.
17
- *
18
- * `App` writes it into the geometry ref on every render path that does NOT
19
- * render the full-screen `AppShell` — the terminal-too-small placeholder and
20
- * inline mode — so a stale measurement can never outlive the frame it
21
- * described. There is deliberately no arithmetic fallback: an unmeasured frame
22
- * routes to `'content'`, which is the band that cannot destroy anything, and
23
- * one invisible frame after a resize is a better failure than a wrong band
24
- * (§4.5, P1-5 / D-16).
25
- */
26
- export const UNMEASURED_GEOMETRY = { top: 1, height: 0, bottom: 0 };
27
- /**
28
- * Which band does terminal row `y` belong to?
29
- *
30
- * Fails toward `'content'`: the whole point of this feature is that a wheel
31
- * notch must never edit the user's draft (I-3), so every degenerate input —
32
- * nothing measured yet, a row outside the frame, a chrome measurement that
33
- * swallowed the screen — resolves to the harmless direction.
34
- */
35
- export function hitTestWheel(y, geometry) {
36
- const { top, height } = geometry;
37
- if (!Number.isFinite(height) || height <= 0)
38
- return 'content';
39
- if (!Number.isFinite(y))
40
- return 'content';
41
- // Clamp into the frame so the one undrawn bottom row — and any off-by-one
42
- // from a terminal that reports 0-based rows — lands on a real band instead of
43
- // being dropped.
44
- const row = Math.min(Math.max(Math.floor(y), top), top + height - 1);
45
- // I-6: at least one content row always survives. This clamp is a backstop
46
- // that should never fire, and that is checkable rather than hopeful: the
47
- // completion popup is capped at `viewportRows(rows) - 4`, so the measured
48
- // chrome tops out at `rows - 6` against a frame of `rows - 1` — five content
49
- // rows survive the worst case the layout can produce.
50
- const bottom = Math.min(Math.max(Math.floor(geometry.bottom) || 0, 0), height - 1);
51
- return row >= top + height - bottom ? 'composer' : 'content';
52
- }