@aragon-agent/cli 0.6.34 → 0.6.36

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 (185) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +173 -7
  3. package/dist/agent/agent-mode.d.ts +20 -4
  4. package/dist/agent/agent-mode.js +20 -4
  5. package/dist/agent/controller.d.ts +67 -3
  6. package/dist/agent/controller.js +106 -5
  7. package/dist/agent/headless.js +31 -0
  8. package/dist/agent/kit-apply.d.ts +56 -0
  9. package/dist/agent/kit-apply.js +209 -0
  10. package/dist/agent/model-profile-settings.d.ts +28 -0
  11. package/dist/agent/model-profile-settings.js +142 -29
  12. package/dist/agent/reducer.d.ts +13 -2
  13. package/dist/agent/reducer.js +1 -0
  14. package/dist/cli.js +12 -0
  15. package/dist/commands/agents-command.d.ts +15 -0
  16. package/dist/commands/agents-command.js +27 -0
  17. package/dist/commands/builtins.js +135 -4
  18. package/dist/commands/kit-command.d.ts +20 -0
  19. package/dist/commands/kit-command.js +131 -0
  20. package/dist/commands/perf.d.ts +11 -0
  21. package/dist/commands/perf.js +2 -1
  22. package/dist/commands/registry.d.ts +14 -0
  23. package/dist/config/app-paths.d.ts +15 -0
  24. package/dist/config/app-paths.js +33 -2
  25. package/dist/config/env.d.ts +4 -1
  26. package/dist/config/env.js +28 -2
  27. package/dist/config/kit-resolution.d.ts +93 -0
  28. package/dist/config/kit-resolution.js +226 -0
  29. package/dist/config/load.d.ts +10 -0
  30. package/dist/config/load.js +8 -1
  31. package/dist/config/model-kits.d.ts +224 -0
  32. package/dist/config/model-kits.js +302 -0
  33. package/dist/config/model-profile-store.d.ts +25 -0
  34. package/dist/config/model-profiles.d.ts +41 -3
  35. package/dist/config/model-profiles.js +46 -5
  36. package/dist/config/schema.d.ts +84 -0
  37. package/dist/config/schema.js +39 -0
  38. package/dist/config/store.js +12 -1
  39. package/dist/config/ui-state.d.ts +18 -0
  40. package/dist/config/ui-state.js +9 -0
  41. package/dist/exec/events.d.ts +25 -0
  42. package/dist/exec/events.js +4 -0
  43. package/dist/exec/index.js +16 -1
  44. package/dist/exec/runner.js +10 -0
  45. package/dist/input/keymap.js +18 -4
  46. package/dist/proc/limits.d.ts +16 -0
  47. package/dist/proc/limits.js +16 -0
  48. package/dist/proc/supervisor.d.ts +20 -1
  49. package/dist/proc/supervisor.js +42 -14
  50. package/dist/prompts/command.d.ts +34 -0
  51. package/dist/prompts/command.js +237 -0
  52. package/dist/prompts/copy.d.ts +89 -0
  53. package/dist/prompts/copy.js +131 -0
  54. package/dist/prompts/fill.d.ts +51 -0
  55. package/dist/prompts/fill.js +46 -0
  56. package/dist/prompts/limits.d.ts +53 -0
  57. package/dist/prompts/limits.js +53 -0
  58. package/dist/prompts/normalize.d.ts +53 -0
  59. package/dist/prompts/normalize.js +190 -0
  60. package/dist/prompts/resolve.d.ts +31 -0
  61. package/dist/prompts/resolve.js +60 -0
  62. package/dist/prompts/search.d.ts +58 -0
  63. package/dist/prompts/search.js +146 -0
  64. package/dist/prompts/store.d.ts +86 -0
  65. package/dist/prompts/store.js +453 -0
  66. package/dist/prompts/types.d.ts +204 -0
  67. package/dist/prompts/types.js +13 -0
  68. package/dist/session/checkpoint.d.ts +8 -1
  69. package/dist/session/checkpoint.js +6 -0
  70. package/dist/session/persist.d.ts +19 -1
  71. package/dist/session/persist.js +13 -1
  72. package/dist/session/startup-session.d.ts +7 -2
  73. package/dist/session/startup-session.js +5 -2
  74. package/dist/session/store.js +3 -0
  75. package/dist/session/validate-session.js +64 -1
  76. package/dist/team/limits.d.ts +25 -0
  77. package/dist/team/limits.js +26 -0
  78. package/dist/team/runtime.js +27 -2
  79. package/dist/team/settled.d.ts +57 -0
  80. package/dist/team/settled.js +87 -0
  81. package/dist/team/subagent.js +48 -2
  82. package/dist/team/timeline.d.ts +74 -0
  83. package/dist/team/timeline.js +101 -0
  84. package/dist/team/types.d.ts +24 -1
  85. package/dist/todo/acceptance.d.ts +120 -0
  86. package/dist/todo/acceptance.js +399 -0
  87. package/dist/todo/follow-through.d.ts +48 -23
  88. package/dist/todo/follow-through.js +110 -44
  89. package/dist/todo/limits.d.ts +62 -1
  90. package/dist/todo/limits.js +62 -1
  91. package/dist/todo/panel-rows.d.ts +29 -39
  92. package/dist/todo/panel-rows.js +37 -83
  93. package/dist/todo/plan-sections.d.ts +60 -0
  94. package/dist/todo/plan-sections.js +108 -0
  95. package/dist/todo/prompt.d.ts +17 -12
  96. package/dist/todo/prompt.js +59 -40
  97. package/dist/todo/row-window.d.ts +51 -0
  98. package/dist/todo/row-window.js +107 -0
  99. package/dist/todo/store.d.ts +139 -31
  100. package/dist/todo/store.js +271 -73
  101. package/dist/todo/todo-tool.d.ts +33 -8
  102. package/dist/todo/todo-tool.js +186 -30
  103. package/dist/todo/types.d.ts +58 -0
  104. package/dist/ui/App.js +414 -54
  105. package/dist/ui/Composer.d.ts +6 -2
  106. package/dist/ui/Composer.js +2 -2
  107. package/dist/ui/Header.d.ts +17 -1
  108. package/dist/ui/Header.js +8 -2
  109. package/dist/ui/PromptInput.d.ts +21 -1
  110. package/dist/ui/PromptInput.js +22 -2
  111. package/dist/ui/ScrollbackShell.d.ts +22 -7
  112. package/dist/ui/ScrollbackShell.js +8 -1
  113. package/dist/ui/StatusBar.d.ts +18 -0
  114. package/dist/ui/StatusBar.js +11 -3
  115. package/dist/ui/TeamPanel.d.ts +35 -41
  116. package/dist/ui/TeamPanel.js +53 -163
  117. package/dist/ui/TodoPanel.d.ts +17 -14
  118. package/dist/ui/TodoPanel.js +146 -38
  119. package/dist/ui/Transcript.js +23 -3
  120. package/dist/ui/composer-input.d.ts +10 -0
  121. package/dist/ui/composer-input.js +57 -24
  122. package/dist/ui/entries/TeamCard.d.ts +18 -2
  123. package/dist/ui/entries/TeamCard.js +47 -13
  124. package/dist/ui/entries/TodoCard.d.ts +26 -11
  125. package/dist/ui/entries/TodoCard.js +56 -15
  126. package/dist/ui/glyphs.d.ts +19 -0
  127. package/dist/ui/glyphs.js +4 -0
  128. package/dist/ui/layout/agents-board.d.ts +102 -0
  129. package/dist/ui/layout/agents-board.js +389 -0
  130. package/dist/ui/layout/cells.d.ts +61 -0
  131. package/dist/ui/layout/cells.js +156 -0
  132. package/dist/ui/layout/header-hints.d.ts +23 -3
  133. package/dist/ui/layout/header-hints.js +73 -7
  134. package/dist/ui/layout/kit-chip.d.ts +50 -0
  135. package/dist/ui/layout/kit-chip.js +81 -0
  136. package/dist/ui/layout/status-detail-layout.js +9 -0
  137. package/dist/ui/layout/status-layout.d.ts +13 -0
  138. package/dist/ui/layout/team-row.d.ts +139 -0
  139. package/dist/ui/layout/team-row.js +246 -0
  140. package/dist/ui/layout/todo-layout.d.ts +38 -0
  141. package/dist/ui/layout/todo-layout.js +75 -0
  142. package/dist/ui/layout/virtual-window.js +32 -3
  143. package/dist/ui/overlays/AgentsBoardOverlay.d.ts +57 -0
  144. package/dist/ui/overlays/AgentsBoardOverlay.js +183 -0
  145. package/dist/ui/overlays/HelpOverlay.js +24 -2
  146. package/dist/ui/overlays/KitEditor.d.ts +23 -0
  147. package/dist/ui/overlays/KitEditor.js +101 -0
  148. package/dist/ui/overlays/KitManager.d.ts +23 -0
  149. package/dist/ui/overlays/KitManager.js +104 -0
  150. package/dist/ui/overlays/KitPicker.d.ts +49 -0
  151. package/dist/ui/overlays/KitPicker.js +58 -0
  152. package/dist/ui/overlays/ModelProfilePicker.js +16 -6
  153. package/dist/ui/overlays/PromptEditorPage.d.ts +48 -0
  154. package/dist/ui/overlays/PromptEditorPage.js +227 -0
  155. package/dist/ui/overlays/PromptLibraryOverlay.d.ts +37 -0
  156. package/dist/ui/overlays/PromptLibraryOverlay.js +191 -0
  157. package/dist/ui/overlays/PromptPicker.d.ts +36 -0
  158. package/dist/ui/overlays/PromptPicker.js +129 -0
  159. package/dist/ui/overlays/PromptTextArea.d.ts +86 -0
  160. package/dist/ui/overlays/PromptTextArea.js +141 -0
  161. package/dist/ui/overlays/SettingsScreen.js +139 -16
  162. package/dist/ui/overlays/kit-picker-state.d.ts +87 -0
  163. package/dist/ui/overlays/kit-picker-state.js +136 -0
  164. package/dist/ui/overlays/kit-settings-state.d.ts +129 -0
  165. package/dist/ui/overlays/kit-settings-state.js +267 -0
  166. package/dist/ui/overlays/model-profile-state.d.ts +26 -3
  167. package/dist/ui/overlays/model-profile-state.js +59 -6
  168. package/dist/ui/overlays/prompt-library-state.d.ts +162 -0
  169. package/dist/ui/overlays/prompt-library-state.js +335 -0
  170. package/dist/ui/overlays/prompt-picker-state.d.ts +71 -0
  171. package/dist/ui/overlays/prompt-picker-state.js +98 -0
  172. package/dist/ui/scrollback/LiveTail.d.ts +2 -2
  173. package/dist/ui/scrollback/LiveTail.js +7 -2
  174. package/dist/ui/scrollback/band.js +14 -1
  175. package/dist/ui/scrollback/live-preview.js +10 -3
  176. package/dist/ui/scrollback/tail-window.d.ts +12 -0
  177. package/dist/ui/scrollback/tail-window.js +17 -2
  178. package/dist/ui/scrollback/use-scrollback-view.js +7 -0
  179. package/dist/ui/team-status.d.ts +166 -0
  180. package/dist/ui/team-status.js +252 -0
  181. package/dist/ui/transcript-text.js +13 -4
  182. package/dist/ui/use-startup-notices.d.ts +35 -1
  183. package/dist/ui/use-startup-notices.js +29 -2
  184. package/dist/ui/use-wheel-routing.js +10 -2
  185. package/package.json +2 -2
@@ -0,0 +1,131 @@
1
+ /**
2
+ * `/kit` - the slash surface for agent model kits (agent-model-kits §4.9).
3
+ *
4
+ * ASCII ONLY - `src/commands/**` is inside `glyphs.test.ts::inScope`.
5
+ *
6
+ * WHY A COMMAND AT ALL, WHEN `ctrl+K` EXISTS: a key that a terminal, tmux, an
7
+ * input method or a IME can swallow needs an equivalent that cannot be swallowed
8
+ * (`R-4`), and the same reading is what a user pastes into an issue. The two
9
+ * surfaces call the SAME controller methods, so they can never diverge in
10
+ * behaviour - only in how the answer is rendered.
11
+ *
12
+ * THERE IS NO `/kit default` (D-7): the session path is forbidden from writing
13
+ * the file, so a "make this the default" verb here would be the one hole in that
14
+ * promise. Setting a default is a settings action, with a Save the user sees.
15
+ */
16
+ import { kitMatches, findKit, kitSlotConnections } from '../config/model-kits.js';
17
+ import { kitApplyMessage, kitNoteMessages } from '../agent/kit-apply.js';
18
+ /** `main a:b / sub same as main / fast off`, shared by every branch below. */
19
+ function describeKit(config, kit) {
20
+ const connections = kitSlotConnections(config.modelProfiles, kit);
21
+ const main = connections.main
22
+ ? `${connections.main.provider}:${connections.main.model}` : 'Profile missing';
23
+ const fast = kit.fastProfileId === null ? 'off'
24
+ : connections.fast ? `${connections.fast.provider}:${connections.fast.model}`
25
+ : 'Profile missing';
26
+ const subagent = kit.subagent.model === 'main' ? 'same as main'
27
+ : kit.subagent.model === 'fast' ? 'fast tier'
28
+ : connections.subagent.name ? `profile ${connections.subagent.name}` : 'Profile missing';
29
+ return `main ${main} / sub ${subagent} / fast ${fast}`;
30
+ }
31
+ function kitNames(config) {
32
+ return (config.modelProfiles?.kits ?? []).map((kit) => kit.name);
33
+ }
34
+ /** `/kit` with no argument: what is in effect, and what it resolves to. */
35
+ function reportActive(ctx) {
36
+ const config = ctx.controller.getConfig();
37
+ const active = ctx.controller.getActiveKit();
38
+ const kits = config.modelProfiles?.kits ?? [];
39
+ if (!kits.length) {
40
+ ctx.notify('info', 'No kits yet. Create one in /settings (Settings > Kit pack).');
41
+ return;
42
+ }
43
+ const session = active.source === 'session'
44
+ ? `session: ${active.name}${active.held.length < 3
45
+ ? `, holds ${active.held.join('+')}` : ''}`
46
+ : active.source === 'default' ? 'default' : 'current setup';
47
+ const kit = findKit(config.modelProfiles, active.id);
48
+ const detail = kit ? ` ${describeKit(config, kit)}.` : '';
49
+ const drift = active.modified && active.driftReasons.length
50
+ ? ` Drifted: ${active.driftReasons.join('; ')}.` : '';
51
+ ctx.notify('info', `Kit pack: ${active.name} (${session}).${detail}${drift}`);
52
+ }
53
+ /** `/kit list`: every kit, its markers and its three connections. */
54
+ function listKits(ctx) {
55
+ const config = ctx.controller.getConfig();
56
+ const kits = config.modelProfiles?.kits ?? [];
57
+ const active = ctx.controller.getActiveKit();
58
+ const fallback = config.modelProfiles?.defaultKitId ?? null;
59
+ if (!kits.length) {
60
+ ctx.notify('info', 'No kits yet. Create one in /settings.');
61
+ return;
62
+ }
63
+ const lines = kits.map((kit) => {
64
+ const markers = [kit.id === fallback ? '[Default]' : '',
65
+ active.source !== 'none' && active.id === kit.id ? '[In use]' : '',
66
+ active.source !== 'none' && active.id === kit.id && active.modified ? '~' : '']
67
+ .filter(Boolean).join(' ');
68
+ return `${markers ? `${markers} ` : ''}${kit.name}: ${describeKit(config, kit)}`;
69
+ });
70
+ ctx.notify('info', `Kits (${kits.length}):\n${lines.join('\n')}`);
71
+ }
72
+ /** `/kit use current` and `/kit use <name|#prefix>`. */
73
+ function useKit(ctx, ref) {
74
+ const config = ctx.controller.getConfig();
75
+ if (ref.toLowerCase() === 'current') {
76
+ const released = ctx.controller.clearSessionKit();
77
+ ctx.toast(released.ok ? 'success' : 'warn', kitApplyMessage(released));
78
+ return;
79
+ }
80
+ const matches = kitMatches(config.modelProfiles, ref);
81
+ if (!matches.length) {
82
+ const names = kitNames(config);
83
+ ctx.notify('warn', `No kit named "${ref}".`
84
+ + (names.length ? ` Available: ${names.join(', ')}.` : ' Create one in /settings first.'));
85
+ return;
86
+ }
87
+ if (matches.length > 1) {
88
+ const ids = matches.map((kit) => `#${kit.id.slice(0, 8)}`).join(', ');
89
+ ctx.notify('warn', `Two or more kits are named "${ref}": ${ids}. Use one of those ids.`);
90
+ return;
91
+ }
92
+ const kit = matches[0];
93
+ const result = ctx.controller.applySessionKit(kit.id);
94
+ if (!result.ok) {
95
+ // `kit_references_missing_profile` names the SLOT in its issues; without them
96
+ // the user would have to open three screens to find out which one is dead.
97
+ const detail = (result.issues ?? []).map((issue) => issue.message).join(' ');
98
+ ctx.notify('warn', [kitApplyMessage(result), detail].filter(Boolean).join(' '));
99
+ return;
100
+ }
101
+ const notes = kitNoteMessages(result.notes);
102
+ ctx.toast(notes.length ? 'warn' : 'success', [`Kit ${kit.name} applied for this session.`, ...notes].join(' '));
103
+ }
104
+ /** The command body; `registerKitCommand` is the only registration site. */
105
+ export function runKitCommand(ctx) {
106
+ const args = ctx.args.trim();
107
+ if (!args) {
108
+ reportActive(ctx);
109
+ return;
110
+ }
111
+ const [verb = '', ...rest] = args.split(/\s+/);
112
+ if (verb.toLowerCase() === 'list') {
113
+ listKits(ctx);
114
+ return;
115
+ }
116
+ if (verb.toLowerCase() === 'use') {
117
+ const ref = rest.join(' ').trim();
118
+ if (!ref) {
119
+ ctx.notify('warn', 'Which kit? Use /kit use <name|#id prefix>, or /kit use current.');
120
+ return;
121
+ }
122
+ useKit(ctx, ref);
123
+ return;
124
+ }
125
+ ctx.notify('warn', 'Unknown /kit subcommand. Try /kit, /kit list, /kit use <name>.');
126
+ }
127
+ /** Registered from `builtins.ts` with one line, next to every other subsystem. */
128
+ export function registerKitCommand(registry) {
129
+ registry.register({ name: 'kit', description: 'Agent kits: status, list, use for this session',
130
+ run: (ctx) => { runKitCommand(ctx); } });
131
+ }
@@ -94,6 +94,17 @@ export interface BandPerf {
94
94
  * reply, which is exactly the state that used to be unrecoverable.
95
95
  */
96
96
  headRows: number;
97
+ /**
98
+ * Which rung the acceptance section reached inside the rail
99
+ * (todo-acceptance-criteria §6.9.6): `visible` (header plus rows), `summary` (the
100
+ * one `AC 2/5` line) or `absent` (nothing - the count is on the status chip).
101
+ *
102
+ * `n/a` when there are no criteria at all, which is the same spelling `/perf` uses
103
+ * for the band outside native mode. It is reported on the EXISTING band line rather
104
+ * than as a new row, and it comes from `buildPlanSections` - the same function the
105
+ * rail rendered from, so this is a report and not a second opinion.
106
+ */
107
+ acceptance?: 'visible' | 'summary' | 'absent' | 'n/a';
97
108
  }
98
109
  /**
99
110
  * Why the right-edge scrollbar is or is not usable, in one record
@@ -139,7 +139,8 @@ function formatBandLine(band) {
139
139
  `tail=${band.previewRows} pool=${band.bandRows} ` +
140
140
  `cols=${band.contentCols}/${band.previewCols} ` +
141
141
  `reason=${band.reason} held=${band.heldRailRows}/${band.heldTeamRows} ` +
142
- `window=${band.windowStart}/${band.windowEntries} head=${band.headRows}`;
142
+ `window=${band.windowStart}/${band.windowEntries} head=${band.headRows}` +
143
+ ` acc=${band.acceptance ?? 'n/a'}`;
143
144
  }
144
145
  /**
145
146
  * Format a snapshot as the seven-line block documented in §5.4 (plus the scrollbar
@@ -12,6 +12,7 @@ import type { FollowThroughBudget } from '../todo/follow-through.js';
12
12
  import type { UpdateCommandPort } from '../update/types.js';
13
13
  import type { SessionCommandPort } from './session-command.js';
14
14
  import type { BranchCommandPort } from './branch-command.js';
15
+ import type { PromptCommandPort } from '../prompts/types.js';
15
16
  export interface CommandContext {
16
17
  /** App-owned shared lock and single feedback path for command and selection copies. */
17
18
  requestCopy?: (request: CopyRequest) => Promise<CopyRequestResult>;
@@ -161,6 +162,19 @@ export interface CommandContext {
161
162
  * value at run time.
162
163
  */
163
164
  branch?: BranchCommandPort;
165
+ /**
166
+ * The quick-prompt library's three capabilities (`/prompts`, quick-prompt-library
167
+ * §4.3 / P1-3).
168
+ *
169
+ * OPTIONAL, and this is the fifth time this file records the same rule: every
170
+ * fixture in this package builds `CommandContext` by literal, so a REQUIRED field
171
+ * would fail to compile every one of them for a feature none of them is about
172
+ * (F-14). Absent means the build has no library, and `/prompts` says exactly that.
173
+ *
174
+ * TYPE-ONLY import, for the reason the `branch` note above records: the command
175
+ * needs the SHAPE of the port, never a value at run time.
176
+ */
177
+ prompts?: PromptCommandPort;
164
178
  }
165
179
  /** What `/mouse` can see and do. Read at command time, never captured. */
166
180
  export interface MouseCommandPort {
@@ -90,6 +90,21 @@ export declare function getModelWindowsPath(): string;
90
90
  * recall (config-state-separation D-1).
91
91
  */
92
92
  export declare function getPromptHistoryPath(): string;
93
+ /**
94
+ * `<home>/quick-prompts.json` — the user's own quick-prompt library
95
+ * (quick-prompt-library D-3).
96
+ *
97
+ * A DIRECT CHILD OF THE ROOT, and deliberately NOT a section of `config.json`.
98
+ * `ui-state.ts:5-13` already wrote the rule this follows: anything carrying USER
99
+ * CONTENT, and anything that GROWS with usage, opens its own file. A library of
100
+ * two hundred prompts is exactly that, and `config.json` is where an API key
101
+ * lives.
102
+ *
103
+ * THE APPLICATION WRITES IT, unlike `model-windows.json` above: it is the library's
104
+ * only truth, and the store creates it on the first `add` rather than at startup.
105
+ * Naming follows `getPromptHistoryPath` beside it.
106
+ */
107
+ export declare function getQuickPromptsPath(): string;
93
108
  /** `<home>/state.json` — UI bookkeeping scalars only; see `ui-state.ts`. */
94
109
  export declare function getUiStatePath(): string;
95
110
  /**
@@ -89,8 +89,22 @@ function isDirectory(path) {
89
89
  * test that sets `ARAGON_HOME` to something bogus quietly gets the real one.
90
90
  */
91
91
  function fallbackRoot() {
92
- if (process.env.VITEST)
93
- return join(tmpdir(), `aragon-agent-vitest-${process.pid}`);
92
+ if (process.env.VITEST) {
93
+ // ONE DIRECTORY PER WORKER, NOT ONE PER PROCESS.
94
+ //
95
+ // The `threads` pool (vitest.config.ts) gives every worker the SAME
96
+ // `process.pid` - they are threads - so a pid-keyed root is shared by up to
97
+ // `maxWorkers` test files running at the same time. Two of them cleaning up
98
+ // (`rmSync(getSessionsDir())`) while a third writes is a race, and it reports
99
+ // itself as "the session was never written": a red full run whose two files
100
+ // both pass alone. `VITEST_POOL_ID` is the worker's own id and a worker runs
101
+ // one file at a time, so keying on it restores the isolation the contract
102
+ // above promises (measured: `session-store.test.ts` and
103
+ // `exec-text-parity.test.ts` failed together and passed separately).
104
+ const worker = process.env.VITEST_POOL_ID ?? '';
105
+ const suffix = worker.length > 0 ? `-${worker}` : '';
106
+ return join(tmpdir(), `aragon-agent-vitest-${process.pid}${suffix}`);
107
+ }
94
108
  return join(homedir(), HOME_DIR_NAME);
95
109
  }
96
110
  function resolveHomeRoot() {
@@ -151,6 +165,23 @@ export function getModelWindowsPath() {
151
165
  export function getPromptHistoryPath() {
152
166
  return join(resolution.root, 'prompt-history.jsonl');
153
167
  }
168
+ /**
169
+ * `<home>/quick-prompts.json` — the user's own quick-prompt library
170
+ * (quick-prompt-library D-3).
171
+ *
172
+ * A DIRECT CHILD OF THE ROOT, and deliberately NOT a section of `config.json`.
173
+ * `ui-state.ts:5-13` already wrote the rule this follows: anything carrying USER
174
+ * CONTENT, and anything that GROWS with usage, opens its own file. A library of
175
+ * two hundred prompts is exactly that, and `config.json` is where an API key
176
+ * lives.
177
+ *
178
+ * THE APPLICATION WRITES IT, unlike `model-windows.json` above: it is the library's
179
+ * only truth, and the store creates it on the first `add` rather than at startup.
180
+ * Naming follows `getPromptHistoryPath` beside it.
181
+ */
182
+ export function getQuickPromptsPath() {
183
+ return join(resolution.root, 'quick-prompts.json');
184
+ }
154
185
  /** `<home>/state.json` — UI bookkeeping scalars only; see `ui-state.ts`. */
155
186
  export function getUiStatePath() {
156
187
  return join(resolution.root, 'state.json');
@@ -18,7 +18,10 @@
18
18
  * ARAGON_UPDATE,
19
19
  * ARAGON_COMPACTION, ARAGON_COMPACTION_THRESHOLD,
20
20
  * ARAGON_COMPACTION_KEEP_TURNS, ARAGON_COMPACTION_SUBAGENTS,
21
- * ARAGON_COMPACTION_ARCHIVE.
21
+ * ARAGON_COMPACTION_ARCHIVE,
22
+ * ARAGON_JOBS_ENABLED, ARAGON_JOBS_WAIT_SECONDS, ARAGON_JOBS_STALL_SECONDS,
23
+ * ARAGON_JOBS_PERSIST_LOG, ARAGON_JOBS_NOTICE_STEER,
24
+ * ARAGON_PROMPTS (the quick-prompt library's `enabled` switch, and only that).
22
25
  *
23
26
  * READ ELSEWHERE, ON PURPOSE: `ARAGON_UPDATE_REGISTRY` and `npm_config_registry`
24
27
  * are resolved by `update/registry.ts::resolveRegistryUrl`, NOT here, because
@@ -18,7 +18,10 @@
18
18
  * ARAGON_UPDATE,
19
19
  * ARAGON_COMPACTION, ARAGON_COMPACTION_THRESHOLD,
20
20
  * ARAGON_COMPACTION_KEEP_TURNS, ARAGON_COMPACTION_SUBAGENTS,
21
- * ARAGON_COMPACTION_ARCHIVE.
21
+ * ARAGON_COMPACTION_ARCHIVE,
22
+ * ARAGON_JOBS_ENABLED, ARAGON_JOBS_WAIT_SECONDS, ARAGON_JOBS_STALL_SECONDS,
23
+ * ARAGON_JOBS_PERSIST_LOG, ARAGON_JOBS_NOTICE_STEER,
24
+ * ARAGON_PROMPTS (the quick-prompt library's `enabled` switch, and only that).
22
25
  *
23
26
  * READ ELSEWHERE, ON PURPOSE: `ARAGON_UPDATE_REGISTRY` and `npm_config_registry`
24
27
  * are resolved by `update/registry.ts::resolveRegistryUrl`, NOT here, because
@@ -212,8 +215,10 @@ export function readEnvConfig() {
212
215
  }
213
216
  const todo = process.env.ARAGON_TODO?.trim().toLowerCase();
214
217
  const todoFollow = process.env.ARAGON_TODO_FOLLOW?.trim().toLowerCase();
218
+ const todoAcceptance = process.env.ARAGON_TODO_ACCEPTANCE?.trim().toLowerCase();
215
219
  if ((todo !== undefined && todo.length > 0) ||
216
- (todoFollow !== undefined && todoFollow.length > 0)) {
220
+ (todoFollow !== undefined && todoFollow.length > 0) ||
221
+ (todoAcceptance !== undefined && todoAcceptance.length > 0)) {
217
222
  // Keyed by `TodoConfig` but VALUED `unknown`, which is what lets the mode be
218
223
  // written through unvalidated (below) while still failing to compile if a
219
224
  // future key is misspelled.
@@ -227,6 +232,16 @@ export function readEnvConfig() {
227
232
  if (todoFollow !== undefined && todoFollow.length > 0) {
228
233
  section.followThrough = todoFollow;
229
234
  }
235
+ // Same treatment as `enabled`: only the five affirmatives are true, anything
236
+ // else recognized-as-present is false, and an ABSENT variable leaves the key
237
+ // out entirely so the next layer's value survives.
238
+ if (todoAcceptance !== undefined && todoAcceptance.length > 0) {
239
+ section.acceptance =
240
+ todoAcceptance === '1' ||
241
+ todoAcceptance === 'true' ||
242
+ todoAcceptance === 'on' ||
243
+ todoAcceptance === 'yes';
244
+ }
230
245
  // A PARTIAL section, safe only because every consumer deep-merges it — the
231
246
  // same contract `partial.skills`, `partial.log` and `partial.team` rely on.
232
247
  if (Object.keys(section).length > 0)
@@ -292,6 +307,17 @@ export function readEnvConfig() {
292
307
  if (Object.keys(section).length > 0)
293
308
  partial.jobs = section;
294
309
  }
310
+ // ARAGON_PROMPTS=0 is the kill switch for the whole quick-prompt surface: no
311
+ // `Ctrl+P`, no header hint, and `/prompts` reports that the feature is off
312
+ // (D-19 / AC-31). ONLY `enabled` IS REACHABLE HERE. `insertMode` and `hint` are
313
+ // preferences a human sets once, and an environment variable that could silently
314
+ // flip a fill into "overwrite what you had typed" is not a kill switch.
315
+ const prompts = process.env.ARAGON_PROMPTS?.trim().toLowerCase();
316
+ if (prompts !== undefined && prompts.length > 0) {
317
+ partial.prompts = {
318
+ enabled: prompts === '1' || prompts === 'true' || prompts === 'on' || prompts === 'yes',
319
+ };
320
+ }
295
321
  const retry = process.env.ARAGON_RETRY?.trim().toLowerCase();
296
322
  const retryMax = process.env.ARAGON_RETRY_MAX?.trim();
297
323
  if ((retry !== undefined && retry.length > 0) || (retryMax !== undefined && retryMax.length > 0)) {
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Where the ACTIVE kit comes from, and the two places a session kit has to be
3
+ * re-applied (agent-model-kits §4.3 / D-5 / D-6).
4
+ *
5
+ * ASCII ONLY - `src/config/**` is inside `glyphs.test.ts::inScope`.
6
+ *
7
+ * ONE TRUTH, PURELY DERIVED. `CliConfig` carries only `sessionKit: {id, held}`;
8
+ * everything a surface prints about "which kit is running" comes out of
9
+ * `resolveActiveKit(config)`. There is no `config.activeKit` field, because a
10
+ * second copy of that answer is a copy that can disagree with the first.
11
+ */
12
+ import type { CliConfig, FastConfig, TeamConfig } from './schema.js';
13
+ import type { ModelProfilesConfig } from './model-profiles.js';
14
+ import { type KitSlot, type SessionKitState } from './model-kits.js';
15
+ /** The three slots, in the order every surface prints them. */
16
+ export declare const ALL_SLOTS: readonly KitSlot[];
17
+ /** The name the UI gives "no kit is in effect" (D-13). */
18
+ export declare const CURRENT_SETUP_NAME = "Current setup";
19
+ /**
20
+ * The answer when there is no kit, or when the host has no kit support at all.
21
+ *
22
+ * A CONSTANT SO CALLERS CANNOT INVENT A SECOND SHAPE: `App` needs one value that
23
+ * renders "no chip, `Current setup` row" for a controller that predates this
24
+ * port (a test double, an embedded host), and re-deriving it there would be a
25
+ * second definition of "nothing".
26
+ */
27
+ export declare const INACTIVE_KIT: ActiveKit;
28
+ export interface ActiveKit {
29
+ source: 'session' | 'default' | 'none';
30
+ id: string | null;
31
+ /** `Current setup` when `source === 'none'`. */
32
+ name: string;
33
+ /** `[]` when `source === 'none'`; all three for a default kit. */
34
+ held: KitSlot[];
35
+ /** The kit's definition and the live connections no longer agree (§4.8). */
36
+ modified: boolean;
37
+ /** Human-readable ASCII reasons, for `/kit` and the root row. */
38
+ driftReasons: string[];
39
+ /** Slots whose referenced profile is gone (RV-P1-1 / I-8). */
40
+ missingProfiles: KitSlot[];
41
+ }
42
+ /** The session-kit runtime shape, owned by `model-kits.ts` and re-exported here. */
43
+ export type { SessionKitState } from './model-kits.js';
44
+ /**
45
+ * Which kit is in effect, and whether it has drifted (§4.8).
46
+ *
47
+ * Precedence is D-6's: session kit, then the persisted default, then the disk
48
+ * bindings (which the UI calls `Current setup`).
49
+ */
50
+ export declare function resolveActiveKit(config: CliConfig): ActiveKit;
51
+ /**
52
+ * The minimum a candidate has to offer for the held slots to be re-applied.
53
+ *
54
+ * STRUCTURAL, NOT `PersistedConfig`: the same routine serves a disk candidate on
55
+ * its way to `liveCandidate` and a freshly reloaded `CliConfig`, and forcing
56
+ * either through a cast at the call site would hide the fact that only these
57
+ * three sections are ever written.
58
+ */
59
+ export interface HeldTarget {
60
+ modelProfiles?: ModelProfilesConfig;
61
+ fast?: Partial<FastConfig>;
62
+ team?: Partial<TeamConfig>;
63
+ }
64
+ /**
65
+ * Re-apply the still-`held` slots of a session kit onto a candidate.
66
+ *
67
+ * THE RETURN VALUE MAY ONLY EVER FEED `liveCandidate`. It must NEVER be handed
68
+ * to `commitModelSettings` (I-14 / RV-P0-1): "this session only" is a claim
69
+ * about the FILE, and a held slot that reaches the commit turns one unrelated
70
+ * save into a permanent, invisible change of the user's defaults. The one
71
+ * caller is `prepareModelSettings` in `agent/model-profile-settings.ts`, which
72
+ * keeps the two candidates in separate variables for exactly this reason.
73
+ *
74
+ * Slots whose kit, or whose referenced profile, disappeared are DROPPED and
75
+ * reported, so the caller can log the release instead of silently forgetting it.
76
+ */
77
+ export declare function applyHeldToLive<T extends HeldTarget>(candidate: T, sessionKit: SessionKitState | null | undefined): {
78
+ candidate: T;
79
+ dropped: KitSlot[];
80
+ };
81
+ /**
82
+ * Keep a session kit alive across `Reload settings` (D-6).
83
+ *
84
+ * THE PROJECTION HAS TO BE RECOMPUTED, not just the bindings: `I-3` says a
85
+ * library's projections never depend on whether a kit exists, so a live config
86
+ * whose `mainId` says "kit" while `provider`/`model` still name the reloaded
87
+ * file would break the equivalence every other surface relies on. Hence the
88
+ * round trip through `projectModelProfiles` + `resolveModelProfileState`.
89
+ */
90
+ export declare function carrySessionKit(input: {
91
+ resolved: CliConfig;
92
+ previous: CliConfig;
93
+ }): CliConfig;
@@ -0,0 +1,226 @@
1
+ /**
2
+ * Where the ACTIVE kit comes from, and the two places a session kit has to be
3
+ * re-applied (agent-model-kits §4.3 / D-5 / D-6).
4
+ *
5
+ * ASCII ONLY - `src/config/**` is inside `glyphs.test.ts::inScope`.
6
+ *
7
+ * ONE TRUTH, PURELY DERIVED. `CliConfig` carries only `sessionKit: {id, held}`;
8
+ * everything a surface prints about "which kit is running" comes out of
9
+ * `resolveActiveKit(config)`. There is no `config.activeKit` field, because a
10
+ * second copy of that answer is a copy that can disagree with the first.
11
+ */
12
+ import { getLogger } from '../logging/logger.js';
13
+ import { normalizeProfileBaseUrl, projectModelProfiles, resolveModelProfileState } from './model-profile-resolution.js';
14
+ import { findKit, kitCandidate, kitSlotConnections } from './model-kits.js';
15
+ /** The three slots, in the order every surface prints them. */
16
+ export const ALL_SLOTS = ['main', 'fast', 'subagent'];
17
+ /** The name the UI gives "no kit is in effect" (D-13). */
18
+ export const CURRENT_SETUP_NAME = 'Current setup';
19
+ /**
20
+ * The answer when there is no kit, or when the host has no kit support at all.
21
+ *
22
+ * A CONSTANT SO CALLERS CANNOT INVENT A SECOND SHAPE: `App` needs one value that
23
+ * renders "no chip, `Current setup` row" for a controller that predates this
24
+ * port (a test double, an embedded host), and re-deriving it there would be a
25
+ * second definition of "nothing".
26
+ */
27
+ export const INACTIVE_KIT = { source: 'none', id: null,
28
+ name: CURRENT_SETUP_NAME, held: [], modified: false, driftReasons: [],
29
+ missingProfiles: [] };
30
+ function driftOfMain(config, connection) {
31
+ if (!connection)
32
+ return [];
33
+ const changed = [];
34
+ if (config.provider !== connection.provider)
35
+ changed.push('provider');
36
+ if (config.model !== connection.model)
37
+ changed.push('model');
38
+ if (normalizeProfileBaseUrl(config.baseUrl) !== normalizeProfileBaseUrl(connection.baseUrl)) {
39
+ changed.push('endpoint');
40
+ }
41
+ return changed.length ? [`main: ${changed.join(', ')} changed`] : [];
42
+ }
43
+ function driftOfFast(config, kit, connection) {
44
+ if (kit.fastProfileId === null) {
45
+ // D-8: this kit TURNS THE TIER OFF, so the endpoints are irrelevant - only
46
+ // "is it on" can disagree. Comparing endpoints here would report drift for
47
+ // every user who ever ran a fast tier before applying a fast-off kit.
48
+ return config.fast.enabled ? ['fast: turned back on'] : [];
49
+ }
50
+ if (!connection)
51
+ return [];
52
+ const changed = [];
53
+ if (!config.fast.enabled)
54
+ changed.push('off');
55
+ if (config.fast.provider !== connection.provider)
56
+ changed.push('provider');
57
+ if (config.fast.model !== connection.model)
58
+ changed.push('model');
59
+ if (normalizeProfileBaseUrl(config.fast.baseUrl)
60
+ !== normalizeProfileBaseUrl(connection.baseUrl))
61
+ changed.push('endpoint');
62
+ return changed.length ? [`fast: ${changed.join(', ')} changed`] : [];
63
+ }
64
+ function driftOfSubagent(config, kit, connection) {
65
+ if (!connection)
66
+ return [];
67
+ const sameMode = config.team?.model === kit.subagent.model;
68
+ const sameId = kit.subagent.model === 'profile'
69
+ ? (config.team?.modelProfileId ?? null) === kit.subagent.modelProfileId : true;
70
+ return sameMode && sameId ? [] : ['subagent: changed'];
71
+ }
72
+ function subagentConnection(config, kit) {
73
+ if (kit.subagent.model === 'profile')
74
+ return kit.subagent.modelProfileId;
75
+ return kit.subagent.model === 'main' ? 'main' : 'fast';
76
+ }
77
+ /** Build the verdict for a kit that exists, whatever installed it. */
78
+ function describe(config, library, kit, source, held) {
79
+ const connections = kitSlotConnections(library, kit);
80
+ const drift = { reasons: [], missing: [] };
81
+ if (held.includes('main')) {
82
+ if (!connections.main)
83
+ drift.missing.push('main');
84
+ else
85
+ drift.reasons.push(...driftOfMain(config, connections.main));
86
+ }
87
+ if (held.includes('fast')) {
88
+ if (kit.fastProfileId !== null && !connections.fast)
89
+ drift.missing.push('fast');
90
+ else
91
+ drift.reasons.push(...driftOfFast(config, kit, connections.fast));
92
+ }
93
+ if (held.includes('subagent')) {
94
+ const missingProfile = kit.subagent.model === 'profile' && !connections.subagent.name;
95
+ if (missingProfile)
96
+ drift.missing.push('subagent');
97
+ else
98
+ drift.reasons.push(...driftOfSubagent(config, kit, subagentConnection(config, kit)));
99
+ }
100
+ if (held.length < ALL_SLOTS.length)
101
+ drift.reasons.push(`holds ${held.join('+') || 'nothing'}`);
102
+ return { source, id: kit.id, name: kit.name, held: [...held],
103
+ modified: held.length < ALL_SLOTS.length || drift.reasons.length > 0
104
+ || drift.missing.length > 0,
105
+ driftReasons: drift.reasons, missingProfiles: drift.missing };
106
+ }
107
+ /**
108
+ * Which kit is in effect, and whether it has drifted (§4.8).
109
+ *
110
+ * Precedence is D-6's: session kit, then the persisted default, then the disk
111
+ * bindings (which the UI calls `Current setup`).
112
+ */
113
+ export function resolveActiveKit(config) {
114
+ const library = config.modelProfiles;
115
+ const state = config.sessionKit;
116
+ const session = state ? findKit(library, state.id) : undefined;
117
+ if (state && session) {
118
+ const held = state.held.filter((slot) => ALL_SLOTS.includes(slot));
119
+ return describe(config, library, session, 'session', held.length ? held : []);
120
+ }
121
+ const fallback = findKit(library, library?.defaultKitId ?? null);
122
+ if (library && fallback)
123
+ return describe(config, library, fallback, 'default', [...ALL_SLOTS]);
124
+ return { source: 'none', id: null, name: CURRENT_SETUP_NAME, held: [], modified: false,
125
+ driftReasons: [], missingProfiles: [] };
126
+ }
127
+ function slotSurvives(library, kit, slot) {
128
+ const ids = new Set(library.entries.map((entry) => entry.id));
129
+ if (slot === 'main')
130
+ return ids.has(kit.mainProfileId);
131
+ if (slot === 'fast')
132
+ return kit.fastProfileId === null || ids.has(kit.fastProfileId);
133
+ return kit.subagent.model !== 'profile'
134
+ || (!!kit.subagent.modelProfileId && ids.has(kit.subagent.modelProfileId));
135
+ }
136
+ /**
137
+ * Re-apply the still-`held` slots of a session kit onto a candidate.
138
+ *
139
+ * THE RETURN VALUE MAY ONLY EVER FEED `liveCandidate`. It must NEVER be handed
140
+ * to `commitModelSettings` (I-14 / RV-P0-1): "this session only" is a claim
141
+ * about the FILE, and a held slot that reaches the commit turns one unrelated
142
+ * save into a permanent, invisible change of the user's defaults. The one
143
+ * caller is `prepareModelSettings` in `agent/model-profile-settings.ts`, which
144
+ * keeps the two candidates in separate variables for exactly this reason.
145
+ *
146
+ * Slots whose kit, or whose referenced profile, disappeared are DROPPED and
147
+ * reported, so the caller can log the release instead of silently forgetting it.
148
+ */
149
+ export function applyHeldToLive(candidate, sessionKit) {
150
+ const heldIn = sessionKit?.held ?? [];
151
+ if (!sessionKit || heldIn.length === 0)
152
+ return { candidate, dropped: [] };
153
+ const library = candidate.modelProfiles;
154
+ const kit = findKit(library, sessionKit.id);
155
+ if (!library || !kit)
156
+ return { candidate, dropped: [...heldIn] };
157
+ const held = heldIn.filter((slot) => slotSurvives(library, kit, slot));
158
+ const dropped = heldIn.filter((slot) => !held.includes(slot));
159
+ if (!held.length)
160
+ return { candidate, dropped };
161
+ const patch = kitCandidate({ library, kit });
162
+ const next = {
163
+ ...candidate,
164
+ modelProfiles: { ...library,
165
+ ...(held.includes('main') ? { mainId: kit.mainProfileId } : {}),
166
+ ...(held.includes('fast') ? { fastId: kit.fastProfileId } : {}) },
167
+ };
168
+ if (held.includes('subagent')) {
169
+ next.team = { ...candidate.team, ...patch.team };
170
+ }
171
+ if (held.includes('fast')) {
172
+ next.fast = { ...candidate.fast, enabled: kit.fastProfileId !== null };
173
+ }
174
+ return { candidate: next, dropped };
175
+ }
176
+ /**
177
+ * Keep a session kit alive across `Reload settings` (D-6).
178
+ *
179
+ * THE PROJECTION HAS TO BE RECOMPUTED, not just the bindings: `I-3` says a
180
+ * library's projections never depend on whether a kit exists, so a live config
181
+ * whose `mainId` says "kit" while `provider`/`model` still name the reloaded
182
+ * file would break the equivalence every other surface relies on. Hence the
183
+ * round trip through `projectModelProfiles` + `resolveModelProfileState`.
184
+ */
185
+ export function carrySessionKit(input) {
186
+ const state = input.previous.sessionKit;
187
+ const { resolved } = input;
188
+ if (!state)
189
+ return resolved;
190
+ const library = resolved.modelProfiles;
191
+ const kit = findKit(library, state.id);
192
+ if (!library || !kit) {
193
+ getLogger().warn('config', 'session_kit_released', { reason: 'kit_missing' });
194
+ return { ...resolved, sessionKit: undefined };
195
+ }
196
+ const applied = applyHeldToLive(resolved, state);
197
+ const held = state.held.filter((slot) => !applied.dropped.includes(slot));
198
+ if (!held.length) {
199
+ getLogger().warn('config', 'session_kit_released', { reason: 'slots_dropped' });
200
+ return { ...resolved, sessionKit: undefined };
201
+ }
202
+ const file = applied.candidate;
203
+ const projected = projectModelProfiles(file).file;
204
+ const next = { ...resolved, modelProfiles: projected.modelProfiles };
205
+ if (held.includes('main')) {
206
+ next.provider = projected.provider;
207
+ next.model = projected.model;
208
+ next.baseUrl = projected.baseUrl ?? undefined;
209
+ if (next.contextWindowOrigin !== 'env') {
210
+ next.contextWindow = projected.contextWindow ?? null;
211
+ next.contextWindowOrigin = next.contextWindow === null ? undefined : 'profile';
212
+ }
213
+ }
214
+ if (held.includes('fast') && projected.fast) {
215
+ next.fast = { ...next.fast, provider: projected.fast.provider ?? '',
216
+ model: projected.fast.model ?? '', baseUrl: projected.fast.baseUrl ?? '' };
217
+ }
218
+ if (held.includes('subagent') && file.team)
219
+ next.team = { ...next.team, ...file.team };
220
+ next.modelProfileState = resolveModelProfileState(next);
221
+ next.sessionKit = { id: kit.id, held };
222
+ if (applied.dropped.length) {
223
+ getLogger().warn('config', 'session_kit_slots_dropped', { slots: applied.dropped });
224
+ }
225
+ return next;
226
+ }