@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
@@ -1,4 +1,6 @@
1
- import { createModelSettingsDraft, saveModelSettings, reloadModelSettings, } from './model-profile-settings.js';
1
+ import { createModelSettingsDraft, previewModelSettings, saveModelSettings, reloadModelSettings, } from './model-profile-settings.js';
2
+ import { applySessionKit as applySessionKitPort, clearSessionKit as clearSessionKitPort } from './kit-apply.js';
3
+ import { resolveActiveKit } from '../config/kit-resolution.js';
2
4
  import { randomUUID } from 'node:crypto';
3
5
  import { join } from 'node:path';
4
6
  import { verifyCompactionIdentity } from '../compaction/memory-identity.js';
@@ -344,7 +346,9 @@ export class AgentController {
344
346
  this.todoRegistered = config.todo.enabled;
345
347
  this.todoEnabled = config.todo.enabled;
346
348
  this.todoPanelCapable = deps.todoPanelCapable === true;
347
- this.todos = this.todoRegistered ? new TodoStore() : null;
349
+ this.todos = this.todoRegistered
350
+ ? new TodoStore(Date.now, () => this.isTodoAcceptanceEnabled())
351
+ : null;
348
352
  // --- Fast model tier ---------------------------------------------------
349
353
  //
350
354
  // RESOLVED BEFORE THE AGENT, because `fastRegistered` decides whether the
@@ -556,6 +560,12 @@ export class AgentController {
556
560
  store: this.todos,
557
561
  // Read LIVE: `/todo off` flips the flag, it cannot unregister.
558
562
  isEnabled: () => this.todoEnabled,
563
+ // LIVE for the same reason (D-11): `/todo acceptance off` must take
564
+ // effect in a session already running, because the tool array
565
+ // itself cannot change. THE SAME PREDICATE the store was given, on
566
+ // purpose: two switches for one capability that could disagree is
567
+ // the shape of a session that cannot exist (IF-7).
568
+ hasAcceptance: () => this.isTodoAcceptanceEnabled(),
559
569
  }),
560
570
  ]
561
571
  : [],
@@ -910,6 +920,9 @@ export class AgentController {
910
920
  todoBlock: this.todoRegistered && this.todoEnabled
911
921
  ? buildTodoBlock({
912
922
  panelVisible: this.todoPanelCapable && config.todo.panel,
923
+ // LIVE, never snapshotted (D-11): the paragraph is part of the
924
+ // block, and `/todo acceptance off` rebuilds the prompt.
925
+ acceptance: config.todo.acceptance,
913
926
  })
914
927
  : '',
915
928
  // The SAME two flags one more time, plus the live resolution — all three
@@ -1592,6 +1605,51 @@ export class AgentController {
1592
1605
  getTodoSnapshot() {
1593
1606
  return this.todos?.snapshot() ?? null;
1594
1607
  }
1608
+ /**
1609
+ * Whether the acceptance table is live for this session: the config key AND the
1610
+ * session switch. Two flags, exactly as `isTodoEnabled` needs `isTodoRegistered`
1611
+ * first - a capability that is on while `todo_write` refuses is not a
1612
+ * capability.
1613
+ */
1614
+ isTodoAcceptanceEnabled() {
1615
+ return this.config.todo.acceptance && this.todoEnabled;
1616
+ }
1617
+ /**
1618
+ * Flip the acceptance capability and RE-RENDER THE PROMPT, byte for byte the
1619
+ * contract `setTodoEnabled` records above: the paragraph is part of
1620
+ * `<todo_planning>`, and the tool reads the same live value through its
1621
+ * `hasAcceptance` closure (D-11).
1622
+ */
1623
+ setTodoAcceptanceEnabled(enabled) {
1624
+ this.setTodoConfig({ acceptance: enabled });
1625
+ // THE RAIL READS THE PROJECTION, NOT THE FLAG, so the flip has to re-project or
1626
+ // `/todo acceptance off` would leave the second section on screen until the
1627
+ // model's next write (P0-2's complaint, one path further in). The verdicts
1628
+ // themselves are kept: `getAcceptanceItems()` still returns them for `/save`,
1629
+ // the checkpoint and `exec`.
1630
+ this.todos?.refreshAcceptanceProjection();
1631
+ }
1632
+ /**
1633
+ * The acceptance table as DATA, independent of whether it is being drawn.
1634
+ *
1635
+ * The persistence surfaces must use this and not `getTodoSnapshot()?.acceptance`:
1636
+ * the projection is `null` whenever the capability is off, and a checkpoint or a
1637
+ * `/save` that silently dropped the user's verdicts because a display switch was
1638
+ * off would be a data loss dressed as a setting.
1639
+ */
1640
+ getAcceptanceItems() {
1641
+ return this.todos?.acceptanceItems() ?? [];
1642
+ }
1643
+ /**
1644
+ * `/todo acceptance reset`: every verdict back to `pending`, evidence dropped.
1645
+ *
1646
+ * The count comes BACK from the store rather than being recomputed by the
1647
+ * command, so the notice and the state cannot disagree about how many criteria
1648
+ * moved.
1649
+ */
1650
+ reopenAcceptance() {
1651
+ return this.todos?.reopenAll() ?? 0;
1652
+ }
1595
1653
  /**
1596
1654
  * Subscribe to the CLI-local todo event stream.
1597
1655
  *
@@ -1603,9 +1661,13 @@ export class AgentController {
1603
1661
  return () => { };
1604
1662
  return this.todos.subscribe(listener);
1605
1663
  }
1606
- /** `/resume`. An absent or empty list CLEARS — see `TodoStore.restore` (P0-2). */
1607
- restoreTodos(items) {
1608
- this.todos?.restore(items);
1664
+ /**
1665
+ * `/resume`. An absent or empty todo list CLEARS both tables — see
1666
+ * `TodoStore.restore` (P0-2). `acceptance` is optional so every existing caller
1667
+ * keeps compiling, and an omitted table reads as "this file has none".
1668
+ */
1669
+ restoreTodos(items, acceptance = undefined) {
1670
+ this.todos?.restore(items, acceptance);
1609
1671
  }
1610
1672
  /**
1611
1673
  * `/todo clear` and `/clear screen`. A user override of a projection (I-2's
@@ -1680,6 +1742,45 @@ export class AgentController {
1680
1742
  }
1681
1743
  /** Refresh both role connections and credentials while retaining conversation state. */
1682
1744
  reloadModelSettings() { return reloadModelSettings(this); }
1745
+ /**
1746
+ * The narrow port `agent/kit-apply.ts` runs against (agent-model-kits §6.3).
1747
+ *
1748
+ * A LITERAL ADAPTER RATHER THAN `this`: the port's member names (`isBusy`,
1749
+ * `hasKey`, `preview`) are deliberately NOT the controller's names, because a
1750
+ * port that shadows a public method is one signature change away from silently
1751
+ * calling itself. Three of the five members are one-liners onto existing
1752
+ * methods; nothing here is new state.
1753
+ */
1754
+ kitApplyPort() {
1755
+ return {
1756
+ getConfig: () => this.config,
1757
+ isBusy: () => this.isModelSettingsBusy(),
1758
+ hasKey: (providerId, role) => this.hasApiKey(providerId, role),
1759
+ createDraft: () => this.createModelSettingsDraft(),
1760
+ preview: (draft) => previewModelSettings(draft, this),
1761
+ };
1762
+ }
1763
+ /**
1764
+ * Apply a kit for THIS SESSION ONLY (D-7 / I-5): no commit, ever.
1765
+ *
1766
+ * Failed applies leave the live config and the file untouched (I-10), because
1767
+ * the whole operation is one prepare-and-apply with no commit in between.
1768
+ */
1769
+ applySessionKit(kitId) {
1770
+ return applySessionKitPort({ kitId, port: this.kitApplyPort() });
1771
+ }
1772
+ /** Release the session kit and return to the config file's bindings (RV-P1-6). */
1773
+ clearSessionKit() {
1774
+ return clearSessionKitPort(this.kitApplyPort());
1775
+ }
1776
+ /**
1777
+ * Which kit is in effect right now (session > default > disk bindings).
1778
+ *
1779
+ * DERIVED, never stored (D-5): the header chip, the settings root row and
1780
+ * `/kit` all read this, so there is exactly one answer to "which preset am I
1781
+ * running" and no copy of it that can drift.
1782
+ */
1783
+ getActiveKit() { return resolveActiveKit(this.config); }
1683
1784
  /** Prebuild all potentially failing model/prompt work before committing the file. */
1684
1785
  prepareModelSettingsSnapshot(config) {
1685
1786
  return { config, model: { providerId: config.provider, modelId: config.model,
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import { formatCost, formatTokens, formatDuration, computeCost } from './usage.js';
8
8
  import { formatStreamError } from './reducer.js';
9
+ import { derivePlanPhase } from '../todo/acceptance.js';
9
10
  import { advanceBudget, decideFollowThrough, emptyBudget, } from '../todo/follow-through.js';
10
11
  /** Run a single prompt headlessly and resolve with the process exit code. */
11
12
  export async function runHeadless(controller, prompt, options = {}) {
@@ -139,6 +140,11 @@ export async function runHeadless(controller, prompt, options = {}) {
139
140
  // stream that is already `--quiet`-able in full.
140
141
  let announcedTotal = 0;
141
142
  let lastActive = -1;
143
+ // THE ACCEPTANCE SECTION'S DEDUPE KEY (todo-acceptance-criteria §6.13.1 / P2-9).
144
+ // `phase` is DERIVED, never cached (I-3), so the key carries a freshly computed
145
+ // phase plus the two counters that can only move when a verdict lands: a
146
+ // constant-size string is cheaper than a field that could drift from the truth.
147
+ let lastAcceptance = '';
142
148
  const unsubscribeTodos = controller.subscribeTodos?.((event) => {
143
149
  if (quiet || event.type !== 'updated')
144
150
  return;
@@ -147,6 +153,20 @@ export async function runHeadless(controller, prompt, options = {}) {
147
153
  announcedTotal = total;
148
154
  err.write(`\n[todo] ${total} ${total === 1 ? 'step' : 'steps'} planned\n`);
149
155
  lastActive = -1;
156
+ // A NEW PLAN GETS ITS OWN FIRST `[accept]` LINE, for the reason `lastActive`
157
+ // is reset one line up: the key below is (phase, checked, failed) and two
158
+ // different plans can share all three. Without this reset the second plan's
159
+ // 0/3 in a `-p` run that already printed 0/3 would go unreported.
160
+ lastAcceptance = '';
161
+ }
162
+ const acceptance = event.snapshot.acceptance;
163
+ if (acceptance && acceptance.total > 0) {
164
+ const key = `${derivePlanPhase(items, acceptance.items)}|` +
165
+ `${acceptance.checkedCount}|${acceptance.failedCount}`;
166
+ if (key !== lastAcceptance) {
167
+ lastAcceptance = key;
168
+ err.write(`[accept] ${acceptanceLine(acceptance)}\n`);
169
+ }
150
170
  }
151
171
  if (activeIndex < 0) {
152
172
  if (lastActive !== -2) {
@@ -220,6 +240,17 @@ export async function runHeadless(controller, prompt, options = {}) {
220
240
  `(${formatTokens(r.tailRelief.charsRemoved)} chars)\n`);
221
241
  }
222
242
  });
243
+ /**
244
+ * `[accept] <n>/<total> checked`, plus the failure count when there is one.
245
+ *
246
+ * ONE LINE PER TRANSITION, and the transitions are the ones a `-p` reader can
247
+ * act on: a verdict landed, or a criterion failed. `evidence` is NEVER printed
248
+ * (I-8): it belongs to the human-facing surfaces, and a machine log is not one.
249
+ */
250
+ function acceptanceLine(acceptance) {
251
+ const base = `${acceptance.checkedCount}/${acceptance.total} checked`;
252
+ return acceptance.failedCount > 0 ? `${base}, ${acceptance.failedCount} failed` : base;
253
+ }
223
254
  // --- The bounded continuation loop (todo-plan-followthrough §3.6). --------
224
255
  //
225
256
  // THE THREE `unsubscribe` CALLS STAY OUTSIDE IT (rule 4). The loop wraps the
@@ -0,0 +1,56 @@
1
+ /**
2
+ * The session exit: apply a kit WITHOUT writing the config file (§4.2 exit B).
3
+ *
4
+ * ASCII ONLY - `src/agent/**` is inside `glyphs.test.ts::inScope`.
5
+ *
6
+ * WHY A NARROW PORT AND NOT THE CONTROLLER: the same reason
7
+ * `ModelSettingsController` exists next door - a five-method port can be stubbed
8
+ * by one object literal in a test, so every refusal code is reachable without a
9
+ * controller, a provider, or a disk. The controller supplies the real one.
10
+ *
11
+ * EVERY PATH EITHER APPLIES OR CHANGES NOTHING (I-10). There is no partial state
12
+ * to clean up, because the whole thing is one `preview` call: the reused prepare
13
+ * chain builds the snapshot before it touches the live config, exactly as a save
14
+ * does - only the commit is missing.
15
+ */
16
+ import type { CliConfig } from '../config/schema.js';
17
+ import type { ModelSettingsDraft, ModelSettingsSaveResult } from '../config/model-profile-store.js';
18
+ import type { HasApiKey } from '../team/model.js';
19
+ import { type KitApplyResult, type KitNote } from '../config/model-kits.js';
20
+ export interface KitApplyPort {
21
+ getConfig(): CliConfig;
22
+ /** `controller.isModelSettingsBusy` - a run, a team, a review, a compaction. */
23
+ isBusy(): boolean;
24
+ /** `controller.hasApiKey`: two-parameter form, so a role-scoped key is seen. */
25
+ hasKey: HasApiKey;
26
+ /** `controller.createModelSettingsDraft`: already seeded with `sessionKit`. */
27
+ createDraft(): ModelSettingsDraft;
28
+ /** `previewModelSettings`: validate + prepare + apply, never commit. */
29
+ preview(draft: ModelSettingsDraft): ModelSettingsSaveResult;
30
+ }
31
+ /** One note, as the sentence the toast shows. Exhaustive by construction. */
32
+ export declare function kitNoteMessage(note: KitNote): string;
33
+ /** Every note for a result, in the order the estimator emitted it. */
34
+ export declare function kitNoteMessages(notes: readonly KitNote[]): string[];
35
+ /** A non-empty sentence for EVERY code, including the two kit-only ones. */
36
+ export declare function kitApplyMessage(result: KitApplyResult): string;
37
+ /**
38
+ * Apply a kit for THIS SESSION ONLY.
39
+ *
40
+ * `kitId` is a resolved id: the `/kit use` command and the picker may accept a
41
+ * name or an `#id` prefix, but resolving those is the command layer's job, so
42
+ * an ambiguous name is reported once, at the surface that accepted it.
43
+ */
44
+ export declare function applySessionKit(input: {
45
+ kitId: string;
46
+ port: KitApplyPort;
47
+ }): KitApplyResult;
48
+ /**
49
+ * Release the session kit: back to the bindings in the config file (RV-P1-6).
50
+ *
51
+ * WITHOUT THIS the feature is one-way: `ctrl+K` could enter a session kit and
52
+ * nothing could leave it except restarting the process. It shares the session
53
+ * exit's prepare chain, so it inherits every refusal code rather than defining
54
+ * a second set of failure semantics.
55
+ */
56
+ export declare function clearSessionKit(port: KitApplyPort): KitApplyResult;
@@ -0,0 +1,209 @@
1
+ /**
2
+ * The session exit: apply a kit WITHOUT writing the config file (§4.2 exit B).
3
+ *
4
+ * ASCII ONLY - `src/agent/**` is inside `glyphs.test.ts::inScope`.
5
+ *
6
+ * WHY A NARROW PORT AND NOT THE CONTROLLER: the same reason
7
+ * `ModelSettingsController` exists next door - a five-method port can be stubbed
8
+ * by one object literal in a test, so every refusal code is reachable without a
9
+ * controller, a provider, or a disk. The controller supplies the real one.
10
+ *
11
+ * EVERY PATH EITHER APPLIES OR CHANGES NOTHING (I-10). There is no partial state
12
+ * to clean up, because the whole thing is one `preview` call: the reused prepare
13
+ * chain builds the snapshot before it touches the live config, exactly as a save
14
+ * does - only the commit is missing.
15
+ */
16
+ import { emptyModelProfiles } from '../config/model-profiles.js';
17
+ import { findKit, kitCandidate, kitSlotConnections } from '../config/model-kits.js';
18
+ import { ALL_SLOTS } from '../config/kit-resolution.js';
19
+ import { modelSettingsResultMessage } from './model-profile-settings.js';
20
+ /**
21
+ * Kit-specific wording, layered over `modelSettingsResultMessage` (RV-P1-3).
22
+ *
23
+ * ONLY the codes whose save-screen text would send the user to the wrong screen
24
+ * are overridden. Everything else keeps the wording the settings screen already
25
+ * uses, so a `read_failed` reads the same wherever it appears.
26
+ */
27
+ const KIT_CODE_MESSAGES = {
28
+ busy: 'Wait for the current run to finish before switching kit.',
29
+ conflict: 'Settings changed elsewhere. Reload before switching kit.',
30
+ write_failed: 'Could not write config. Your settings are unchanged.',
31
+ apply_failed: 'Could not apply the kit to this session. Nothing changed.',
32
+ kit_missing: 'That kit is not in your library. Create it in /settings (Settings > Kit pack).',
33
+ kit_references_missing_profile: 'That kit points at a profile that no longer exists.'
34
+ + ' Fix it in /settings (Settings > Kit pack).',
35
+ };
36
+ /**
37
+ * The `KitNote` sentences, verbatim (agent-model-kits §4.2).
38
+ *
39
+ * ONE TABLE, THREE CALLERS: the picker, `/kit use`, and any future surface must
40
+ * say the same thing about the same consequence. `fast_review_enabled` is the
41
+ * one that matters most - it is a recurring charge the user did not ask for by
42
+ * name, only by applying a kit that switches the tier on.
43
+ */
44
+ const KIT_NOTE_MESSAGES = {
45
+ fast_tier_enabled: 'Fast tier enabled by this kit.',
46
+ fast_review_enabled: 'Periodic review is on for this tier (/fast review off to stop it).',
47
+ fast_tier_off: 'Fast tier turned off by this kit.',
48
+ restart_required: 'Fast tier needs a restart to take effect.',
49
+ main_key_missing: 'Main profile has no API key; set one in /settings.',
50
+ subagent_key_missing: 'Subagent profile has no API key; children fall back to main.',
51
+ subagent_fast_unavailable: 'Subagent uses the fast tier, which this kit leaves off; children fall back to main.',
52
+ };
53
+ /** One note, as the sentence the toast shows. Exhaustive by construction. */
54
+ export function kitNoteMessage(note) { return KIT_NOTE_MESSAGES[note]; }
55
+ /** Every note for a result, in the order the estimator emitted it. */
56
+ export function kitNoteMessages(notes) {
57
+ return notes.map(kitNoteMessage);
58
+ }
59
+ /** A non-empty sentence for EVERY code, including the two kit-only ones. */
60
+ export function kitApplyMessage(result) {
61
+ if (result.ok) {
62
+ return result.released
63
+ ? 'Kit released; using the config file\'s settings.'
64
+ : 'Kit applied for this session.';
65
+ }
66
+ const code = result.code ?? 'invalid';
67
+ const specific = KIT_CODE_MESSAGES[code];
68
+ if (specific)
69
+ return specific;
70
+ // NO `apply_failed` BRANCH ABOVE: it is in the table, because a stopped apply
71
+ // must say "nothing changed" rather than repeat the save screen's wording.
72
+ // Every remaining code falls through to the shared table, which is what makes
73
+ // "there is a code with no sentence" unrepresentable instead of merely unlikely.
74
+ return modelSettingsResultMessage({ ok: false, persisted: false, status: 'rejected',
75
+ code: code });
76
+ }
77
+ /** Slots whose referenced profile is gone (I-8 / AC-39). */
78
+ function missingSlots(kit, draft) {
79
+ const connections = kitSlotConnections(draft.profiles, kit);
80
+ const missing = [];
81
+ if (!connections.main)
82
+ missing.push('main');
83
+ if (kit.fastProfileId !== null && !connections.fast)
84
+ missing.push('fast');
85
+ if (kit.subagent.model === 'profile' && !connections.subagent.name)
86
+ missing.push('subagent');
87
+ return missing;
88
+ }
89
+ function missingIssue(slot, kit) {
90
+ const field = slot === 'subagent' ? 'subagent.modelProfileId' : `${slot}ProfileId`;
91
+ return { path: `modelProfiles.kits[${kit.id}].${field}`,
92
+ code: 'missing_reference',
93
+ message: `Kit "${kit.name}" references a profile that no longer exists (${slot}).` };
94
+ }
95
+ function keyMissing(port, draft, id, role) {
96
+ const entry = id ? draft.profiles?.entries.find((candidate) => candidate.id === id) : undefined;
97
+ if (!entry)
98
+ return false;
99
+ return !entry.apiKey && !port.hasKey(entry.provider, role);
100
+ }
101
+ /**
102
+ * Everything the user is entitled to know about the apply (§4.2 `KitNote`).
103
+ *
104
+ * Computed from the config AS IT WAS BEFORE, because half of these are about
105
+ * transitions ("the tier just came ON") and the other half are still true after.
106
+ */
107
+ function kitNotes(input) {
108
+ const { before, draft, kit, port } = input;
109
+ const notes = [];
110
+ const wasEnabled = before.fast.enabled;
111
+ if (kit.fastProfileId !== null && !wasEnabled)
112
+ notes.push('fast_tier_enabled');
113
+ // The honest disclosure of a real cost (F-23): a fast tier with review on
114
+ // spends money on its own, every N turns, without being asked.
115
+ if (kit.fastProfileId !== null && !wasEnabled && before.fast.review) {
116
+ notes.push('fast_review_enabled');
117
+ }
118
+ if (kit.fastProfileId === null && wasEnabled)
119
+ notes.push('fast_tier_off');
120
+ if (input.restartRequired)
121
+ notes.push('restart_required');
122
+ if (keyMissing(port, draft, kit.mainProfileId, 'main'))
123
+ notes.push('main_key_missing');
124
+ if (kit.subagent.model === 'profile'
125
+ && keyMissing(port, draft, kit.subagent.modelProfileId, 'main')) {
126
+ notes.push('subagent_key_missing');
127
+ }
128
+ if (kit.subagent.model === 'fast' && kit.fastProfileId === null) {
129
+ notes.push('subagent_fast_unavailable');
130
+ }
131
+ return notes;
132
+ }
133
+ /**
134
+ * Apply a kit for THIS SESSION ONLY.
135
+ *
136
+ * `kitId` is a resolved id: the `/kit use` command and the picker may accept a
137
+ * name or an `#id` prefix, but resolving those is the command layer's job, so
138
+ * an ambiguous name is reported once, at the surface that accepted it.
139
+ */
140
+ export function applySessionKit(input) {
141
+ const { kitId, port } = input;
142
+ if (port.isBusy())
143
+ return { ok: false, code: 'busy', notes: [] };
144
+ const before = port.getConfig();
145
+ const draft = port.createDraft();
146
+ const library = draft.profiles ?? emptyModelProfiles();
147
+ const kit = findKit(library, kitId);
148
+ if (!kit)
149
+ return { ok: false, code: 'kit_missing', notes: [] };
150
+ const missing = missingSlots(kit, draft);
151
+ if (missing.length) {
152
+ return { ok: false, code: 'kit_references_missing_profile', notes: [],
153
+ issues: missing.map((slot) => missingIssue(slot, kit)) };
154
+ }
155
+ const patch = kitCandidate({ library, kit });
156
+ draft.profiles = patch.modelProfiles;
157
+ draft.patch = { ...draft.patch, team: patch.team, fast: patch.fast };
158
+ // I-13: a kit that turns the tier OFF must NOT project the fast role, or
159
+ // `projectModelProfiles`'s `continue` would copy the DISK fast endpoint into
160
+ // the live config and the kit would read as "restore, then switch off".
161
+ draft.activateRoles = kit.fastProfileId === null ? ['main'] : ['main', 'fast'];
162
+ draft.sessionKit = { id: kit.id, held: [...ALL_SLOTS] };
163
+ // THE MARKER, not a heuristic: this draft IS the kit, so `releasedSlots` must
164
+ // not read the apply's own writes as user decisions (§4.2 exit B).
165
+ draft.sessionKitFresh = true;
166
+ const result = port.preview(draft);
167
+ if (!result.ok) {
168
+ return { ok: false, code: result.code ?? 'invalid', notes: [],
169
+ ...(result.issues ? { issues: result.issues } : {}) };
170
+ }
171
+ const notes = kitNotes({ before, draft, kit, port,
172
+ ...(result.restartRequired ? { restartRequired: true } : {}) });
173
+ return { ok: true, notes };
174
+ }
175
+ /**
176
+ * Release the session kit: back to the bindings in the config file (RV-P1-6).
177
+ *
178
+ * WITHOUT THIS the feature is one-way: `ctrl+K` could enter a session kit and
179
+ * nothing could leave it except restarting the process. It shares the session
180
+ * exit's prepare chain, so it inherits every refusal code rather than defining
181
+ * a second set of failure semantics.
182
+ */
183
+ export function clearSessionKit(port) {
184
+ if (port.isBusy())
185
+ return { ok: false, code: 'busy', notes: [] };
186
+ const before = port.getConfig();
187
+ const draft = port.createDraft();
188
+ const file = draft.baseline;
189
+ draft.sessionKit = null;
190
+ draft.patch = {
191
+ ...draft.patch,
192
+ team: { model: file.team.model, modelProfileId: file.team.modelProfileId },
193
+ // The disk fast section, KEY BY KEY: `enabled` restores the tier state and
194
+ // the other three restore the endpoints the file names, so no trace of the
195
+ // kit's fast choice survives in the live config.
196
+ fast: { enabled: file.fast.enabled, provider: file.fast.provider,
197
+ model: file.fast.model, baseUrl: file.fast.baseUrl },
198
+ };
199
+ draft.activateRoles = ['main', 'fast'];
200
+ const result = port.preview(draft);
201
+ if (!result.ok)
202
+ return { ok: false, code: result.code ?? 'invalid', notes: [] };
203
+ const notes = [];
204
+ if (result.restartRequired)
205
+ notes.push('restart_required');
206
+ if (before.fast.enabled && !file.fast.enabled)
207
+ notes.push('fast_tier_off');
208
+ return { ok: true, released: true, notes };
209
+ }
@@ -1,6 +1,7 @@
1
1
  import type { ModelRef } from '@aragon-agent/core';
2
2
  import { type CliConfig } from '../config/schema.js';
3
3
  import { type ModelSettingsDraft, type ModelSettingsSaveResult } from '../config/model-profile-store.js';
4
+ import { type KitSlot } from '../config/model-kits.js';
4
5
  /** Fully prepared live state; applying it performs no input parsing or disk access. */
5
6
  export interface ModelSettingsSnapshot {
6
7
  config: CliConfig;
@@ -22,10 +23,37 @@ export interface ModelSettingsController {
22
23
  export declare function createModelSettingsDraft(live: CliConfig, liveRevision: number): ModelSettingsDraft;
23
24
  /** Map stable failure codes to safe UI text; never include parser or SDK error messages. */
24
25
  export declare function modelSettingsResultMessage(result: ModelSettingsSaveResult): string;
26
+ /**
27
+ * The slots THIS transaction explicitly decided, and that therefore leave the
28
+ * session kit (§4.3 / D-6).
29
+ *
30
+ * Released BY SLOT, never wholesale: a user who only retargeted the subagent
31
+ * must not lose the main and fast choices the kit made for this session, and a
32
+ * user who retargeted main must not have it silently ignored. Per-slot is the
33
+ * only rule shaped like the data (three independent slots).
34
+ */
35
+ export declare function releasedSlots(draft: ModelSettingsDraft): KitSlot[];
25
36
  /** Validate and prepare everything before the one synchronous, atomic disk commit. */
26
37
  export declare function saveModelSettings(input: {
27
38
  draft: ModelSettingsDraft;
28
39
  controller: ModelSettingsController;
29
40
  }): ModelSettingsSaveResult;
41
+ /**
42
+ * The SAME prepare chain, without the commit: a session kit (§4.2 exit B).
43
+ *
44
+ * NO `diskRevision` GATE: nothing is written, so a save that landed elsewhere is
45
+ * not this operation's business. It still refuses while work is in flight
46
+ * (`busy`) and still refuses a draft whose disk could not be read, so "nothing
47
+ * changed" is guaranteed for every other failure too (I-10).
48
+ */
49
+ export declare function previewModelSettings(draft: ModelSettingsDraft, controller: ModelSettingsController): ModelSettingsSaveResult;
50
+ /**
51
+ * Apply a prepared snapshot.
52
+ *
53
+ * `persisted` HAS NO DEFAULT (RV-P1-4): the one attribute that distinguishes
54
+ * "the file now says this" from "only this process says this" is not something
55
+ * a call site should be able to forget.
56
+ */
57
+ export declare function applyPreparedSnapshot(controller: ModelSettingsController, snapshot: ModelSettingsSnapshot, persisted: boolean): ModelSettingsSaveResult;
30
58
  /** Reload the complete model snapshot; invalid disk content never replaces a live session. */
31
59
  export declare function reloadModelSettings(controller: ModelSettingsController): ModelSettingsSaveResult;