@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
@@ -4,7 +4,10 @@ import { mergePersistedConfig } from '../config/store.js';
4
4
  import { adaptLegacyConnectionPatch, assertLegacyFastTarget, commitModelSettings, readModelSettingsDisk, } from '../config/model-profile-store.js';
5
5
  import { projectModelProfiles, resolveModelProfileState } from '../config/model-profile-resolution.js';
6
6
  import { validateModelProfiles } from '../config/model-profiles.js';
7
+ import { applyHeldToLive, carrySessionKit } from '../config/kit-resolution.js';
8
+ import { findKit } from '../config/model-kits.js';
7
9
  import { registerProfileSecrets } from '../logging/secret-registry.js';
10
+ import { getLogger } from '../logging/logger.js';
8
11
  /** Capture both baselines when the editor opens, without migrating or writing anything. */
9
12
  export function createModelSettingsDraft(live, liveRevision) {
10
13
  const disk = readModelSettingsDisk();
@@ -15,6 +18,11 @@ export function createModelSettingsDraft(live, liveRevision) {
15
18
  profiles: disk.config?.modelProfiles
16
19
  ? structuredClone(disk.config.modelProfiles) : undefined,
17
20
  activateRoles: [], patch: {},
21
+ // SEEDED FROM THE LIVE CONFIG (RV-P0-2). Without this line `draft.sessionKit`
22
+ // is always `undefined`, the `undefined` case below means "release", and any
23
+ // save made from the settings screen would silently drop a session kit the
24
+ // user just applied - the exact opposite of "this session only".
25
+ sessionKit: live.sessionKit ? structuredClone(live.sessionKit) : undefined,
18
26
  ...(!disk.ok ? { readError: 'Cannot read config. Repair it with aragon config edit.' } : {}),
19
27
  };
20
28
  }
@@ -61,6 +69,47 @@ function hasConnectionPatch(patch, role) {
61
69
  const section = role === 'main' ? patch : patch.fast;
62
70
  return !!section && ['provider', 'model', 'baseUrl'].some((key) => key in section);
63
71
  }
72
+ /**
73
+ * The slots THIS transaction explicitly decided, and that therefore leave the
74
+ * session kit (§4.3 / D-6).
75
+ *
76
+ * Released BY SLOT, never wholesale: a user who only retargeted the subagent
77
+ * must not lose the main and fast choices the kit made for this session, and a
78
+ * user who retargeted main must not have it silently ignored. Per-slot is the
79
+ * only rule shaped like the data (three independent slots).
80
+ */
81
+ export function releasedSlots(draft) {
82
+ const held = draft.sessionKit?.held ?? [];
83
+ if (!held.length)
84
+ return [];
85
+ // THE APPLY ITSELF RELEASES NOTHING (`kit-apply.ts` sets this marker). Without
86
+ // it the apply's own writes - `activateRoles`, `patch.team`, the re-bound
87
+ // library - read as "the user decided every slot", and the save would drop the
88
+ // three slots it was just handed.
89
+ if (draft.sessionKitFresh)
90
+ return [];
91
+ const kit = findKit(draft.profiles ?? draft.baseline.modelProfiles, draft.sessionKit?.id);
92
+ if (!kit)
93
+ return [...held];
94
+ const released = new Set();
95
+ for (const role of ['main', 'fast']) {
96
+ if (draft.activateRoles.includes(role) || connectionChanged(draft, role)
97
+ || hasConnectionPatch(draft.patch, role))
98
+ released.add(role);
99
+ }
100
+ if (draft.patch.team !== undefined)
101
+ released.add('subagent');
102
+ return [...released].filter((slot) => held.includes(slot));
103
+ }
104
+ /** The session kit as it stands AFTER this transaction's explicit decisions. */
105
+ function narrowedSessionKit(draft) {
106
+ const current = draft.sessionKit;
107
+ if (!current)
108
+ return null;
109
+ const released = releasedSlots(draft);
110
+ const held = current.held.filter((slot) => !released.includes(slot));
111
+ return held.length ? { id: current.id, held } : null;
112
+ }
64
113
  function liveCandidate(live, candidate, draft) {
65
114
  const patch = draft.patch;
66
115
  const normalizedPatch = {};
@@ -128,8 +177,16 @@ function liveCandidate(live, candidate, draft) {
128
177
  next.apiKeyOverrideTarget = undefined;
129
178
  }
130
179
  else {
131
- next.fast = { ...next.fast, provider: projected.fast.provider,
132
- model: projected.fast.model, baseUrl: projected.fast.baseUrl };
180
+ // I-13 (RV-P1-7): `projectModelProfiles` projects fast ONLY when the
181
+ // library binds one. With `fastId: null` the projection's `fast` is just the
182
+ // file's untouched section (`projected` starts as `{...file}`), so copying it
183
+ // here would resurrect the disk's fast endpoint underneath a kit that
184
+ // deliberately turned the tier off - the live config would then say "off"
185
+ // while naming a model nobody chose.
186
+ if (projected.modelProfiles?.fastId) {
187
+ next.fast = { ...next.fast, provider: projected.fast.provider,
188
+ model: projected.fast.model, baseUrl: projected.fast.baseUrl };
189
+ }
133
190
  }
134
191
  }
135
192
  if (next.apiKeyOverride && !next.apiKeyOverrideTarget
@@ -139,49 +196,100 @@ function liveCandidate(live, candidate, draft) {
139
196
  next.modelProfileState = resolveModelProfileState(next);
140
197
  return next;
141
198
  }
142
- /** Validate and prepare everything before the one synchronous, atomic disk commit. */
143
- export function saveModelSettings(input) {
144
- const { draft, controller } = input;
145
- if (controller.isModelSettingsBusy())
146
- return rejected('busy');
147
- if (draft.liveRevision !== controller.getSettingsRevision())
148
- return rejected('conflict');
149
- const disk = readModelSettingsDisk();
150
- if (!disk.ok || draft.readError)
151
- return rejected('read_failed');
152
- if (disk.revision !== draft.diskRevision)
153
- return rejected('conflict');
154
- const issues = validateModelProfiles(draft.profiles ?? disk.config?.modelProfiles);
199
+ /**
200
+ * Build both candidates and the snapshot.
201
+ *
202
+ * TWO CANDIDATES, NAMED DIFFERENTLY ON PURPOSE (RV-P0-1 / I-14). `diskCandidate`
203
+ * is the commit side and never learns about a held slot; `liveCandidateInput`
204
+ * is what the session sees and is never committed. Collapsing them into one
205
+ * variable is the defect this split exists to prevent: one unrelated save would
206
+ * then write the session's kit into the user's permanent defaults, and nothing
207
+ * visible would change.
208
+ */
209
+ function prepareModelSettings(input) {
210
+ const { draft, controller, current } = input;
211
+ const issues = validateModelProfiles(draft.profiles ?? current.modelProfiles);
155
212
  if (issues.length)
156
- return { ...rejected('invalid'), issues };
157
- let snapshot;
158
- let candidate;
213
+ return { failure: { ...rejected('invalid'), issues } };
159
214
  try {
160
- const current = disk.config ?? structuredClone(DEFAULT_CONFIG);
161
215
  const profilesChanged = draft.activateRoles.length > 0
162
216
  || JSON.stringify(draft.profiles) !== JSON.stringify(draft.baseline.modelProfiles);
163
217
  const implicitLegacy = draft.profiles === undefined || !profilesChanged;
164
218
  const patch = adaptLegacyConnectionPatch({ current, patch: implicitLegacy
165
219
  ? draft.patch : { ...draft.patch, modelProfiles: draft.profiles } });
166
- candidate = mergePersistedConfig(current, patch);
167
- registerProfileSecrets(candidate);
168
- const next = liveCandidate(controller.getConfig(), candidate, draft);
220
+ const diskCandidate = mergePersistedConfig(current, patch);
221
+ registerProfileSecrets(diskCandidate);
222
+ const sessionKit = narrowedSessionKit(draft);
223
+ const applied = applyHeldToLive(diskCandidate, sessionKit);
224
+ if (applied.dropped.length) {
225
+ getLogger().warn('agent', 'session_kit_slots_dropped', { slots: applied.dropped });
226
+ }
227
+ const next = liveCandidate(controller.getConfig(), applied.candidate, draft);
228
+ next.sessionKit = sessionKit ?? undefined;
169
229
  // Disk preferences cannot reveal startup/session gateways retained by the live main role.
170
230
  if (implicitLegacy && current.modelProfiles?.fastId && hasConnectionPatch(draft.patch, 'fast')) {
171
231
  assertLegacyFastTarget(next.fast, next);
172
232
  }
173
- snapshot = controller.prepareModelSettingsSnapshot(next);
233
+ const snapshot = controller.prepareModelSettingsSnapshot(next);
234
+ return { prepared: { diskCandidate, liveCandidateInput: applied.candidate, snapshot } };
174
235
  }
175
236
  catch (error) {
176
237
  const code = error.code ?? 'invalid';
177
- return rejected(code);
238
+ return { failure: rejected(code) };
178
239
  }
179
- const commit = commitModelSettings({ candidate, diskRevision: draft.diskRevision });
240
+ }
241
+ /** Validate and prepare everything before the one synchronous, atomic disk commit. */
242
+ export function saveModelSettings(input) {
243
+ const { draft, controller } = input;
244
+ if (controller.isModelSettingsBusy())
245
+ return rejected('busy');
246
+ if (draft.liveRevision !== controller.getSettingsRevision())
247
+ return rejected('conflict');
248
+ const disk = readModelSettingsDisk();
249
+ if (!disk.ok || draft.readError)
250
+ return rejected('read_failed');
251
+ if (disk.revision !== draft.diskRevision)
252
+ return rejected('conflict');
253
+ const prepared = prepareModelSettings({ draft, controller,
254
+ current: disk.config ?? structuredClone(DEFAULT_CONFIG) });
255
+ if (!prepared.prepared)
256
+ return prepared.failure;
257
+ const commit = commitModelSettings({ candidate: prepared.prepared.diskCandidate,
258
+ diskRevision: draft.diskRevision });
180
259
  if (!commit.ok)
181
260
  return rejected(commit.code ?? 'write_failed');
182
- return applyPreparedSnapshot(controller, snapshot, true);
261
+ return applyPreparedSnapshot(controller, prepared.prepared.snapshot, true);
262
+ }
263
+ /**
264
+ * The SAME prepare chain, without the commit: a session kit (§4.2 exit B).
265
+ *
266
+ * NO `diskRevision` GATE: nothing is written, so a save that landed elsewhere is
267
+ * not this operation's business. It still refuses while work is in flight
268
+ * (`busy`) and still refuses a draft whose disk could not be read, so "nothing
269
+ * changed" is guaranteed for every other failure too (I-10).
270
+ */
271
+ export function previewModelSettings(draft, controller) {
272
+ if (controller.isModelSettingsBusy())
273
+ return rejected('busy');
274
+ if (draft.readError)
275
+ return rejected('read_failed');
276
+ const disk = readModelSettingsDisk();
277
+ if (!disk.ok)
278
+ return rejected('read_failed');
279
+ const prepared = prepareModelSettings({ draft, controller,
280
+ current: disk.config ?? structuredClone(DEFAULT_CONFIG) });
281
+ if (!prepared.prepared)
282
+ return prepared.failure;
283
+ return applyPreparedSnapshot(controller, prepared.prepared.snapshot, false);
183
284
  }
184
- function applyPreparedSnapshot(controller, snapshot, persisted) {
285
+ /**
286
+ * Apply a prepared snapshot.
287
+ *
288
+ * `persisted` HAS NO DEFAULT (RV-P1-4): the one attribute that distinguishes
289
+ * "the file now says this" from "only this process says this" is not something
290
+ * a call site should be able to forget.
291
+ */
292
+ export function applyPreparedSnapshot(controller, snapshot, persisted) {
185
293
  try {
186
294
  controller.applyModelSettingsSnapshot(snapshot);
187
295
  }
@@ -220,14 +328,19 @@ export function reloadModelSettings(controller) {
220
328
  if (live.unicode !== undefined)
221
329
  resolved.unicode = live.unicode;
222
330
  resolved.reducedMotion ||= !live.color;
331
+ // THE SESSION KIT SURVIVES A RELOAD (D-6): the three connections it holds
332
+ // must come from the kit, not from the file we just re-read, and the
333
+ // projection has to be recomputed so the live config stays equivalent to
334
+ // what its own bindings say (I-3).
335
+ const carried = carrySessionKit({ resolved, previous: live });
223
336
  const after = readModelSettingsDisk();
224
337
  if (!after.ok)
225
338
  return rejected('read_failed');
226
339
  if (after.revision !== disk.revision)
227
340
  return rejected('conflict');
228
- if (resolved.modelProfileState?.main.invalid)
341
+ if (carried.modelProfileState?.main.invalid)
229
342
  return rejected('invalid');
230
- return applyPreparedSnapshot(controller, controller.prepareModelSettingsSnapshot(resolved), false);
343
+ return applyPreparedSnapshot(controller, controller.prepareModelSettingsSnapshot(carried), false);
231
344
  }
232
345
  catch {
233
346
  return rejected('invalid');
@@ -16,13 +16,13 @@ import type { AgentMode } from './agent-mode.js';
16
16
  import type { DispatchOutcome, SubagentRun, SubagentSpec, TeamSnapshot } from '../team/types.js';
17
17
  import type { FastReview, FastSnapshot } from '../fast/types.js';
18
18
  import type { CompactionMode, CompactionRecord, CompactionSnapshot, CompactionUiTrigger, ContextUsageSnapshot } from '../compaction/types.js';
19
- import type { TodoItem, TodoSnapshot } from '../todo/types.js';
19
+ import type { AcceptanceSnapshot, TodoItem, TodoSnapshot } from '../todo/types.js';
20
20
  import type { JobKind, ServiceSnapshot, ServiceStatus } from '../proc/types.js';
21
21
  import type { RetryPhase } from './retry-view.js';
22
22
  import type { FilePatch } from '../tools/patch.js';
23
23
  export type NoticeLevel = 'info' | 'warn' | 'error';
24
24
  export type ToastLevel = NoticeLevel | 'success';
25
- export type Overlay = null | 'settings' | 'model' | 'help' | 'queue' | 'confirm' | 'question' | 'plan';
25
+ export type Overlay = null | 'settings' | 'model' | 'help' | 'queue' | 'confirm' | 'question' | 'plan' | 'kit' | 'prompts' | 'prompt-pick' | 'agents';
26
26
  export type ToolStatus = 'pending' | 'running' | 'done' | 'error';
27
27
  /** Ephemeral, auto-dismissed acknowledgement (spec §3.9). */
28
28
  export interface Toast {
@@ -147,6 +147,17 @@ export type Entry = {
147
147
  items: TodoItem[];
148
148
  doneCount: number;
149
149
  total: number;
150
+ /**
151
+ * The acceptance projection, copied from `TodoSnapshot.acceptance` on every
152
+ * `todoUpdate` (todo-acceptance-criteria §6.10).
153
+ *
154
+ * OPTIONAL, NOT `AcceptanceSnapshot | null` REQUIRED, and that asymmetry with
155
+ * the snapshot is deliberate (P1-4): `TodoSnapshot` has exactly one writer, so
156
+ * it can fail closed; an `Entry` does not - its sources include `entries` read
157
+ * back from a session file written before this feature existed, where the key
158
+ * is simply absent.
159
+ */
160
+ acceptance?: AcceptanceSnapshot | null;
150
161
  /** The turn that owns this card is still running, so it may be rewritten. */
151
162
  live: boolean;
152
163
  /**
@@ -1020,6 +1020,7 @@ export function viewReducer(state, action) {
1020
1020
  items: action.snapshot.items,
1021
1021
  doneCount: action.snapshot.doneCount,
1022
1022
  total: action.snapshot.total,
1023
+ acceptance: action.snapshot.acceptance,
1023
1024
  live: true,
1024
1025
  });
1025
1026
  // Rewrite the turn's card when there is one; otherwise append and record
package/dist/cli.js CHANGED
@@ -170,6 +170,10 @@ function toFlags(opts) {
170
170
  // C-9). A value flag is no less inert than a boolean one when this line is
171
171
  // missing.
172
172
  todoFollow: opts.todoFollow,
173
+ // Site three of three once more, for `--todo-acceptance`: a flag declared in
174
+ // commander and on `CliFlags` but missing HERE is inert, with no compile error
175
+ // and no runtime error (todo-acceptance-criteria §6.8.2).
176
+ todoAcceptance: opts.todoAcceptance,
173
177
  // Site three of three for `--retry` / `--retry-max` — five features, same
174
178
  // trap (`mouse` P1-2, `team` P1-4, `todo` C-9, render L4, now retry). A flag
175
179
  // needs the `.option()` declaration below, a field on `CliFlags`, AND a line
@@ -1319,6 +1323,9 @@ function createSessionWriter(input) {
1319
1323
  readView: input.readView,
1320
1324
  readHistory: () => controller.getSessionSnapshot(),
1321
1325
  readTodos: () => controller.getTodoSnapshot()?.items ?? [],
1326
+ // The verdicts travel with the plan; read from the STORE, not from the render
1327
+ // projection, so a checkpoint taken while the capability is off still saves them.
1328
+ readAcceptance: () => controller.getAcceptanceItems(),
1322
1329
  // READ AT WRITE TIME: `/model` rebuilds the config, and a checkpoint that
1323
1330
  // recorded the model from launch would name the wrong one after a switch.
1324
1331
  readModel: () => {
@@ -2068,6 +2075,11 @@ function buildProgram() {
2068
2075
  // passed, so `!== undefined` in `resolveTodoConfig` already distinguishes it
2069
2076
  // from silence. An unrecognized mode is clamped to `notify`, not rejected.
2070
2077
  .option('--todo-follow <mode>', 'What to do when a run ends with unfinished steps: notify (default) | auto | off')
2078
+ // A PAIR, and for the same reason `--todo-panel` needs one: `acceptance` is
2079
+ // PERSISTED, so a lone `--no-todo-acceptance` would make commander default
2080
+ // `opts.todoAcceptance` to `true` and overwrite a stored `false`.
2081
+ .option('--todo-acceptance', 'Plan acceptance criteria with every todo list (the default)')
2082
+ .option('--no-todo-acceptance', 'Plan steps only: no acceptance criteria, no verify phase')
2071
2083
  // BOTH FORMS, positive first, for the reason recorded five times above:
2072
2084
  // `retry.enabled` is PERSISTED, so a lone `--no-retry` would make commander
2073
2085
  // default `opts.retry` to `true` and silently overwrite a stored `false`.
@@ -0,0 +1,15 @@
1
+ /**
2
+ * `/agents` — open the subagent board (tui-subagent-panel §6.4).
3
+ *
4
+ * A SECOND DOOR, not a fallback. `Ctrl+B` is the fast path, but a key is undiscoverable
5
+ * unless the user reads `/help`, and `/agents` is what the palette advertises. Both
6
+ * entries share ONE guard (`state.team === null && lastTeamEntry(...) === null`) so the
7
+ * key and the command can never disagree about whether there is a board to show: an
8
+ * empty board would read as a broken feature, and the honest answer is a notice that
9
+ * says no dispatch has happened yet.
10
+ *
11
+ * The body lives in its own module for the reason `kit-command.ts` and
12
+ * `queue-command.ts` do: `builtins.ts` is already well past a thousand lines.
13
+ */
14
+ import type { CommandRegistry } from './registry.js';
15
+ export declare function registerAgentsCommand(registry: CommandRegistry): void;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * `/agents` — open the subagent board (tui-subagent-panel §6.4).
3
+ *
4
+ * A SECOND DOOR, not a fallback. `Ctrl+B` is the fast path, but a key is undiscoverable
5
+ * unless the user reads `/help`, and `/agents` is what the palette advertises. Both
6
+ * entries share ONE guard (`state.team === null && lastTeamEntry(...) === null`) so the
7
+ * key and the command can never disagree about whether there is a board to show: an
8
+ * empty board would read as a broken feature, and the honest answer is a notice that
9
+ * says no dispatch has happened yet.
10
+ *
11
+ * The body lives in its own module for the reason `kit-command.ts` and
12
+ * `queue-command.ts` do: `builtins.ts` is already well past a thousand lines.
13
+ */
14
+ import { lastTeamEntry } from '../ui/team-status.js';
15
+ export function registerAgentsCommand(registry) {
16
+ registry.register({
17
+ name: 'agents',
18
+ description: 'Open the subagent board: every child, its facts and its timeline',
19
+ run: (ctx) => {
20
+ if (ctx.state.team === null && lastTeamEntry(ctx.state.entries) === null) {
21
+ ctx.notify('info', 'No subagent dispatch yet.');
22
+ return;
23
+ }
24
+ ctx.setOverlay('agents');
25
+ },
26
+ });
27
+ }
@@ -14,6 +14,7 @@ import { computeCost, formatCost, formatDuration, formatTokens } from '../agent/
14
14
  import { mergePendingEntries } from '../agent/queued-messages.js';
15
15
  import { TODO_FOLLOW_LIMITS } from '../todo/limits.js';
16
16
  import { buildContinuationMessage } from '../todo/follow-through.js';
17
+ import { planIsResolved } from '../todo/acceptance.js';
17
18
  import { MODE_LABEL, nextMode } from '../agent/agent-mode.js';
18
19
  import { loadSession, resolveSessionPath, saveSession } from '../session/persist.js';
19
20
  import { applyRestoredSession } from '../session/startup-session.js';
@@ -23,6 +24,9 @@ import { getLogger } from '../logging/logger.js';
23
24
  import { resolveFastTier } from '../fast/resolve.js';
24
25
  import { subagentModelSummary } from '../ui/overlays/model-profile-state.js';
25
26
  import { registerQueueCommand } from './queue-command.js';
27
+ import { registerPromptCommands } from '../prompts/command.js';
28
+ import { registerKitCommand } from './kit-command.js';
29
+ import { registerAgentsCommand } from './agents-command.js';
26
30
  import { perfCommand } from './perf.js';
27
31
  import { runCompactCommand } from '../compaction/command.js';
28
32
  import { runClearCommand } from './clear-command.js';
@@ -157,6 +161,102 @@ function runTeamModelCommand(ctx, subject) {
157
161
  ctx.persistConfig({ team: teamPatch });
158
162
  ctx.toast('success', `Subagent model: ${entry.name}${disambiguation} (${entry.provider}:${entry.model}).`);
159
163
  }
164
+ /** Rows `/todo status` prints before it says how many it did not. */
165
+ const ACCEPT_STATUS_ROWS = 8;
166
+ /**
167
+ * `/todo acceptance on|off|reset`, and the bare form equal to `/todo status`'s
168
+ * acceptance section (todo-acceptance-criteria §6.14).
169
+ *
170
+ * EVERY MUTATING BRANCH IS "TWO CALLS PLUS PERSIST", the rule `/todo panel` and
171
+ * `/todo follow` already follow: `setTodoConfig` (or `setTodoAcceptanceEnabled`) is
172
+ * what the session sees, and `persistConfig` is what survives it. Only one of the two
173
+ * reports success while changing nothing.
174
+ */
175
+ function runTodoAcceptance(ctx, value) {
176
+ const controller = ctx.controller;
177
+ if (!value || value === 'status') {
178
+ const lines = acceptanceStatusLines(controller.getTodoSnapshot()?.acceptance ?? null, controller.getTodoConfig().acceptance);
179
+ if (lines.length === 0) {
180
+ ctx.notify('info', 'No acceptance criteria. Todo planning: ' +
181
+ (controller.isTodoEnabled() ? 'on' : 'off') + '.');
182
+ return;
183
+ }
184
+ for (const line of lines)
185
+ ctx.notify('info', line);
186
+ return;
187
+ }
188
+ if (value === 'on' || value === 'off') {
189
+ const acceptance = value === 'on';
190
+ controller.setTodoAcceptanceEnabled(acceptance);
191
+ ctx.persistConfig({ todo: { acceptance } });
192
+ ctx.toast('success', `Acceptance criteria ${acceptance ? 'on' : 'off'}.`);
193
+ return;
194
+ }
195
+ if (value === 'reset') {
196
+ if (controller.isRunning()) {
197
+ ctx.notify('warn', 'Cannot reset the acceptance criteria while a run is in progress.');
198
+ return;
199
+ }
200
+ // THE COUNT COMES FROM THE STORE (a named writer, §6.2.3), never recomputed here,
201
+ // and `0` means the table was already fully unchecked - in which case a notice
202
+ // would announce that nothing happened.
203
+ const reopened = controller.reopenAcceptance();
204
+ if (reopened === 0) {
205
+ ctx.notify('info', 'No checked criteria to reset.');
206
+ return;
207
+ }
208
+ ctx.toast('info', `${reopened} ${reopened === 1 ? 'criterion is' : 'criteria are'} ` +
209
+ 'unchecked again.');
210
+ return;
211
+ }
212
+ ctx.notify('warn', 'Usage: /todo acceptance [on|off|reset]');
213
+ }
214
+ /**
215
+ * The acceptance section of `/todo status`, as at most two notification bodies.
216
+ *
217
+ * RETURNS NOTHING WHEN THERE IS NOTHING TO SAY, which is what keeps the disabled
218
+ * path and the no-criteria path byte-identical to the pre-feature command (I-5).
219
+ * The list is bounded at eight rows by "unchecked and failed first, then the rest",
220
+ * with `... and N more` closing it: the full table lives in the transcript card, and
221
+ * a command that floods the transcript is not a status command.
222
+ */
223
+ function acceptanceStatusLines(acceptance, enabled) {
224
+ if (!enabled || !acceptance || acceptance.total === 0)
225
+ return [];
226
+ const header = `Acceptance ${acceptance.checkedCount}/${acceptance.total} checked` +
227
+ (acceptance.failedCount > 0 ? ` (${acceptance.failedCount} failed)` : '');
228
+ const rows = acceptance.items
229
+ .map((item, index) => ({ item, index }))
230
+ .sort((left, right) => rank(left.item.status) - rank(right.item.status));
231
+ const shown = rows.slice(0, ACCEPT_STATUS_ROWS);
232
+ const lines = shown.map(({ item, index }) => ` ${String(index + 1).padStart(2)}. ${markerWord(item.status)} ${item.content}` +
233
+ (item.status === 'failed' && item.evidence.length > 0 ? `\n ${item.evidence}` : ''));
234
+ if (rows.length > shown.length)
235
+ lines.push(` ... and ${rows.length - shown.length} more`);
236
+ return [header, ...lines];
237
+ }
238
+ /**
239
+ * Failures first, then everything still open, then the settled verdicts: `/todo status`
240
+ * is read to find out what is WRONG, and the eight-row budget means the order decides
241
+ * what survives the cut.
242
+ */
243
+ function rank(status) {
244
+ if (status === 'failed')
245
+ return 0;
246
+ if (status === 'in_progress' || status === 'pending')
247
+ return 1;
248
+ return 2;
249
+ }
250
+ /** ASCII markers: this module is inside the glyph scanner's scope. */
251
+ function markerWord(status) {
252
+ if (status === 'passed')
253
+ return '[x]';
254
+ if (status === 'failed')
255
+ return '[!]';
256
+ if (status === 'in_progress')
257
+ return ' > ';
258
+ return '[ ]';
259
+ }
160
260
  function formatFastStatus(controller) {
161
261
  const status = controller.getFastStatus();
162
262
  const cfg = controller.getFastConfig();
@@ -687,7 +787,8 @@ const COMMANDS = [
687
787
  },
688
788
  {
689
789
  name: 'todo',
690
- description: 'Todo planning: status | on | off | panel on|off | follow <mode> | clear | continue',
790
+ description: 'Todo planning: status | on | off | panel on|off | follow <mode> | ' +
791
+ 'acceptance [on|off|reset] | clear | continue',
691
792
  /**
692
793
  * `/todo on` CANNOT turn planning on in a session that started with
693
794
  * `--no-todo`, for the reason `/team on` above records at length: the tool
@@ -738,6 +839,16 @@ const COMMANDS = [
738
839
  : '';
739
840
  ctx.notify('info', `Todos: ${snapshot.doneCount}/${snapshot.total} done.${active} Panel: ${panel}. ` +
740
841
  `Follow-through: ${follow}.`);
842
+ // THE ACCEPTANCE SECTION, appended and NOTHING ELSE (todo-acceptance-criteria
843
+ // §6.14): with no criteria the two calls above are byte-identical to the
844
+ // version without this feature.
845
+ for (const line of acceptanceStatusLines(snapshot.acceptance, todoCfg.acceptance)) {
846
+ ctx.notify('info', line);
847
+ }
848
+ return;
849
+ }
850
+ if (verb === 'acceptance') {
851
+ runTodoAcceptance(ctx, value);
741
852
  return;
742
853
  }
743
854
  if (verb === 'on' || verb === 'off') {
@@ -819,7 +930,10 @@ const COMMANDS = [
819
930
  return;
820
931
  }
821
932
  const snapshot = controller.getTodoSnapshot();
822
- if (!snapshot || snapshot.doneCount >= snapshot.total) {
933
+ // `planIsResolved`, NOT `doneCount >= total` (§6.14): in the verification
934
+ // phase every step is done and the command has to keep working, or the user's
935
+ // only manual route into the last stage is refused with "Nothing left".
936
+ if (!snapshot || planIsResolved(snapshot.items, snapshot.acceptance?.items ?? null)) {
823
937
  ctx.notify('info', 'Nothing left to continue.');
824
938
  return;
825
939
  }
@@ -838,7 +952,8 @@ const COMMANDS = [
838
952
  return;
839
953
  }
840
954
  ctx.notify('warn', `Unknown argument "${verb}" - use ` +
841
- '/todo [status|on|off|panel on|off|follow <mode>|clear|continue].');
955
+ '/todo [status|on|off|panel on|off|follow <mode>|acceptance [on|off|reset]|' +
956
+ 'clear|continue].');
842
957
  },
843
958
  },
844
959
  {
@@ -1016,6 +1131,11 @@ const COMMANDS = [
1016
1131
  // `/resume` restores `messages`, so the belief that justifies the
1017
1132
  // panel comes back — and the panel has to come back with it (§3.13).
1018
1133
  todos: ctx.controller.getTodoSnapshot()?.items ?? [],
1134
+ // The verdicts travel with the plan, for the same reason: a resume that
1135
+ // restored the steps and dropped the conclusions would show a plan as
1136
+ // "not started" that the user had already watched being verified. Read
1137
+ // from the STORE, so a save taken while the capability is off keeps them.
1138
+ acceptance: ctx.controller.getAcceptanceItems(),
1019
1139
  });
1020
1140
  ctx.notify('info', `Saved session to ${path}`);
1021
1141
  }
@@ -1371,6 +1491,17 @@ export function registerBuiltinCommands(registry) {
1371
1491
  for (const command of COMMANDS)
1372
1492
  registry.register(command);
1373
1493
  registerQueueCommand(registry);
1494
+ // The command BODY lives in its own module (builtins.ts is already 1.5k lines);
1495
+ // this line is the whole registration (agent-model-kits §4.9).
1496
+ registerKitCommand(registry);
1497
+ // Same shape, one feature later (quick-prompt-library §4.3 / P1-3): the body is
1498
+ // `prompts/command.ts` and its capabilities arrive on `CommandContext.prompts`.
1499
+ // REGISTERED UNCONDITIONALLY, even with `prompts.enabled=false` - that is the only
1500
+ // way the command can report that the feature is off (D-19 / AC-30).
1501
+ registerPromptCommands(registry);
1502
+ // The read-only subagent board (tui-subagent-panel §6.4). Same shape again: the
1503
+ // body is `commands/agents-command.ts` and the registration is this one line.
1504
+ registerAgentsCommand(registry);
1374
1505
  }
1375
1506
  /**
1376
1507
  * Every built-in command name INCLUDING aliases (`quit`), for documentation and
@@ -1380,7 +1511,7 @@ export function registerBuiltinCommands(registry) {
1380
1511
  * `registry.get(name)` instead, because that is the only check that stays
1381
1512
  * correct when commands are registered dynamically (§7.1 / P1-2).
1382
1513
  */
1383
- export const BUILTIN_COMMAND_NAMES = ['queue', ...COMMANDS.flatMap((c) => [
1514
+ export const BUILTIN_COMMAND_NAMES = ['queue', 'prompts', 'agents', ...COMMANDS.flatMap((c) => [
1384
1515
  c.name,
1385
1516
  ...(c.aliases ?? []),
1386
1517
  ])].sort();
@@ -0,0 +1,20 @@
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 type { CommandRegistry, CommandContext } from './registry.js';
17
+ /** The command body; `registerKitCommand` is the only registration site. */
18
+ export declare function runKitCommand(ctx: CommandContext): void;
19
+ /** Registered from `builtins.ts` with one line, next to every other subsystem. */
20
+ export declare function registerKitCommand(registry: CommandRegistry): void;