@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,3 +1,4 @@
1
+ import { type AgentKit } from './model-kits.js';
1
2
  export type ModelRole = 'main' | 'fast';
2
3
  export interface ModelProfile {
3
4
  id: string;
@@ -14,6 +15,16 @@ export interface ModelProfilesConfig {
14
15
  entries: ModelProfile[];
15
16
  mainId: string | null;
16
17
  fastId: string | null;
18
+ /**
19
+ * Agent model kits (agent-model-kits D-2). OPTIONAL because a file written by
20
+ * a pre-KIT build simply has no such key; `emptyModelProfiles` still fills it
21
+ * in, so every library this build CREATES is explicit.
22
+ */
23
+ kits?: AgentKit[];
24
+ /** The `Set default` pointer (D-13); a dangling value normalizes to null. */
25
+ defaultKitId?: string | null;
26
+ /** Forward migration marker (D-3). Always 1; never a functional decision. */
27
+ kitsVersion?: 1;
17
28
  }
18
29
  export interface ProfileBindingState {
19
30
  selectedId: string | null;
@@ -42,7 +53,16 @@ export declare function emptyModelProfileState(invalid?: boolean): ModelProfiles
42
53
  * copy of these four clauses is one edit away from disagreeing with the config layer.
43
54
  */
44
55
  export declare function isValidEndpointUrl(value: string | null | undefined): boolean;
45
- /** Validate untrusted input; diagnostics never include supplied values. */
56
+ /**
57
+ * Validate untrusted input; diagnostics never include supplied values.
58
+ *
59
+ * THE KIT SECTION IS STRUCTURAL ONLY (agent-model-kits RV-P1-1). `mainId` /
60
+ * `fastId` above are `missing_reference` because a dangling ROLE BINDING leaves
61
+ * the whole library unreadable (there is no "nothing bound" recovery path once
62
+ * a bound id is gone), while a dangling KIT SLOT degrades at resolution time
63
+ * into `Profile missing` and a refused apply - the same discipline
64
+ * `team.modelProfileId` already follows (`schema.ts` clampTeamConfig).
65
+ */
46
66
  export declare function validateModelProfiles(value: unknown): ProfileValidationIssue[];
47
67
  /**
48
68
  * Disk-tolerance gate for a profile-level context window (profile-context-window 5.3).
@@ -55,11 +75,29 @@ export declare function validateModelProfiles(value: unknown): ProfileValidation
55
75
  * (invariant 4 - one bad number must not drop the user's credentials).
56
76
  */
57
77
  export declare function normalizeProfileContextWindow(value: unknown): number | null;
58
- /** Normalize a valid library into a safe field whitelist; throws a fixed error if invalid. */
78
+ /**
79
+ * Normalize a valid library into a safe field whitelist; throws a fixed error if invalid.
80
+ *
81
+ * THE THREE KIT KEYS ARE LISTED EXPLICITLY, NEVER `...spread` (R-2): this
82
+ * literal IS the whitelist (F-3), so a key missing from it disappears at the
83
+ * next write - a save that only touched `thinking` would silently delete every
84
+ * kit the user owns. `normalizeKits` is TOTAL, so a hand-edited kit degrades
85
+ * instead of taking the library (and the credentials in it) with it.
86
+ */
59
87
  export declare function normalizeModelProfiles(value: unknown): ModelProfilesConfig;
60
88
  /** Create an entry with a stable UUID and a frozen default name. */
61
89
  export declare function createModelProfile(input: Omit<ModelProfile, 'id'>): ModelProfile;
62
90
  /** Duplicate the current draft entry, including its explicitly edited credential. */
63
91
  export declare function duplicateModelProfile(entry: ModelProfile): ModelProfile;
64
- /** Remove an unused entry; binding references must be changed explicitly first. */
92
+ /** Ids of the kits whose three slots name `id`, for the reference message. */
93
+ export declare function kitNamesForProfile(library: ModelProfilesConfig, id: string): string[];
94
+ /**
95
+ * Remove an unused entry; binding references must be changed explicitly first.
96
+ *
97
+ * The message NAMES the kits that would be orphaned (D-9): a throw that only
98
+ * says "still in use" sends the user hunting through three separate screens for
99
+ * the reference it means, and the kits are the reference nobody thinks of.
100
+ * (The UI never reaches this throw - it unbinds, and deletes the kits, inside
101
+ * the draft first - so this is the library-level backstop, F-25.)
102
+ */
65
103
  export declare function removeModelProfile(library: ModelProfilesConfig, id: string): ModelProfilesConfig;
@@ -1,9 +1,11 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { ADAPTER_PROVIDERS, clampContextWindow, isAutoToken } from './schema.js';
3
+ import { normalizeKits, validateKits } from './model-kits.js';
3
4
  const CONTROL = /[\u0000-\u001f\u007f-\u009f]/;
4
5
  /** Return a fresh empty library without initiating a disk migration. */
5
6
  export function emptyModelProfiles() {
6
- return { version: 1, entries: [], mainId: null, fastId: null };
7
+ return { version: 1, entries: [], mainId: null, fastId: null, kits: [],
8
+ defaultKitId: null, kitsVersion: 1 };
7
9
  }
8
10
  /** Return independently mutable role state. */
9
11
  export function emptyModelProfileState(invalid = false) {
@@ -69,7 +71,16 @@ function validateEntry(value, index) {
69
71
  }
70
72
  return issues;
71
73
  }
72
- /** Validate untrusted input; diagnostics never include supplied values. */
74
+ /**
75
+ * Validate untrusted input; diagnostics never include supplied values.
76
+ *
77
+ * THE KIT SECTION IS STRUCTURAL ONLY (agent-model-kits RV-P1-1). `mainId` /
78
+ * `fastId` above are `missing_reference` because a dangling ROLE BINDING leaves
79
+ * the whole library unreadable (there is no "nothing bound" recovery path once
80
+ * a bound id is gone), while a dangling KIT SLOT degrades at resolution time
81
+ * into `Profile missing` and a refused apply - the same discipline
82
+ * `team.modelProfileId` already follows (`schema.ts` clampTeamConfig).
83
+ */
73
84
  export function validateModelProfiles(value) {
74
85
  if (value === undefined)
75
86
  return [];
@@ -99,6 +110,7 @@ export function validateModelProfiles(value) {
99
110
  message: 'Select an existing profile or Current custom.' });
100
111
  }
101
112
  }
113
+ issues.push(...validateKits(value.kits));
102
114
  return issues;
103
115
  }
104
116
  /**
@@ -116,19 +128,29 @@ export function normalizeProfileContextWindow(value) {
116
128
  return null;
117
129
  return clampContextWindow(value, null);
118
130
  }
119
- /** Normalize a valid library into a safe field whitelist; throws a fixed error if invalid. */
131
+ /**
132
+ * Normalize a valid library into a safe field whitelist; throws a fixed error if invalid.
133
+ *
134
+ * THE THREE KIT KEYS ARE LISTED EXPLICITLY, NEVER `...spread` (R-2): this
135
+ * literal IS the whitelist (F-3), so a key missing from it disappears at the
136
+ * next write - a save that only touched `thinking` would silently delete every
137
+ * kit the user owns. `normalizeKits` is TOTAL, so a hand-edited kit degrades
138
+ * instead of taking the library (and the credentials in it) with it.
139
+ */
120
140
  export function normalizeModelProfiles(value) {
121
141
  if (validateModelProfiles(value).length)
122
142
  throw new Error('Invalid model profiles.');
123
143
  if (value === undefined)
124
144
  return emptyModelProfiles();
125
145
  const library = value;
146
+ const { kits, defaultKitId, kitsVersion } = normalizeKits(library.kits, library.defaultKitId);
126
147
  return { version: 1, mainId: library.mainId, fastId: library.fastId,
127
148
  entries: library.entries.map((entry) => ({ id: entry.id, name: entry.name.trim()
128
149
  || [...entry.model.trim()].slice(0, 80).join(''), provider: entry.provider,
129
150
  model: entry.model.trim(), baseUrl: entry.baseUrl?.trim() || null,
130
151
  apiKey: entry.apiKey?.trim() || null,
131
- contextWindow: normalizeProfileContextWindow(entry.contextWindow) })) };
152
+ contextWindow: normalizeProfileContextWindow(entry.contextWindow) })),
153
+ kits, defaultKitId, kitsVersion };
132
154
  }
133
155
  /** Create an entry with a stable UUID and a frozen default name. */
134
156
  export function createModelProfile(input) {
@@ -139,9 +161,28 @@ export function createModelProfile(input) {
139
161
  export function duplicateModelProfile(entry) {
140
162
  return { ...entry, id: randomUUID(), name: `${[...entry.name].slice(0, 73).join('')} (copy)` };
141
163
  }
142
- /** Remove an unused entry; binding references must be changed explicitly first. */
164
+ /** Ids of the kits whose three slots name `id`, for the reference message. */
165
+ export function kitNamesForProfile(library, id) {
166
+ return (library.kits ?? []).filter((kit) => kit.mainProfileId === id
167
+ || kit.fastProfileId === id
168
+ || (kit.subagent.model === 'profile' && kit.subagent.modelProfileId === id))
169
+ .map((kit) => kit.name);
170
+ }
171
+ /**
172
+ * Remove an unused entry; binding references must be changed explicitly first.
173
+ *
174
+ * The message NAMES the kits that would be orphaned (D-9): a throw that only
175
+ * says "still in use" sends the user hunting through three separate screens for
176
+ * the reference it means, and the kits are the reference nobody thinks of.
177
+ * (The UI never reaches this throw - it unbinds, and deletes the kits, inside
178
+ * the draft first - so this is the library-level backstop, F-25.)
179
+ */
143
180
  export function removeModelProfile(library, id) {
144
181
  if (library.mainId === id || library.fastId === id)
145
182
  throw new Error('Profile is still in use.');
183
+ const kits = kitNamesForProfile(library, id);
184
+ if (kits.length) {
185
+ throw new Error(`Profile is used by kit ${kits.map((name) => `"${name}"`).join(', ')}.`);
186
+ }
146
187
  return { ...library, entries: library.entries.filter((entry) => entry.id !== id) };
147
188
  }
@@ -489,6 +489,17 @@ export interface TodoConfig {
489
489
  * be, and would mean replacing both merges first.
490
490
  */
491
491
  followThrough: FollowThroughMode;
492
+ /**
493
+ * Must a plan also declare acceptance criteria, and does the UI show them
494
+ * (todo-acceptance-criteria §6.8.1). Default TRUE.
495
+ *
496
+ * THE FOURTH SCALAR, and it is a scalar for exactly the reason above: both
497
+ * hand-written merges in `config/store.ts` spread the whole `todo` object, so a
498
+ * new scalar key flows through them automatically. `clampTodoConfig` below is the
499
+ * only other place that has to know about it - a value that is missing THERE
500
+ * would silently keep the default and make `--no-todo-acceptance` look broken.
501
+ */
502
+ acceptance: boolean;
492
503
  }
493
504
  export declare const DEFAULT_TODO_CONFIG: TodoConfig;
494
505
  /**
@@ -851,6 +862,43 @@ export declare const DEFAULT_JOBS_CONFIG: JobsConfig;
851
862
  * resolve to `undefined` and the setting would silently never apply.
852
863
  */
853
864
  export declare function clampJobsConfig(raw: unknown): JobsConfig;
865
+ /**
866
+ * The quick-prompt library's three POLICY scalars (quick-prompt-library D-19).
867
+ *
868
+ * THE LIBRARY ITSELF IS NOT HERE (D-3). Two hundred prompts with bodies is user
869
+ * content that grows with usage, and `ui-state.ts:5-13` already wrote the rule:
870
+ * anything carrying user content opens its own file. `config.json` holds the three
871
+ * decisions that are about BEHAVIOUR - whether the key exists, what a fill does to
872
+ * a non-empty draft, and whether the header teaches the key.
873
+ *
874
+ * ONE LEVEL DEEP, like every section beside it: `runConfigGet` splits on a single
875
+ * dot, so `prompts.hint` resolves and `prompts.some.nested` would not.
876
+ */
877
+ export interface PromptsConfig {
878
+ /**
879
+ * `false` stops `Ctrl+P` and the header hint; `/prompts` STAYS REGISTERED and
880
+ * reports that the feature is off (D-19 / AC-30). Removing the command as well
881
+ * would leave the one surface that can explain the switch unable to run.
882
+ */
883
+ enabled: boolean;
884
+ /**
885
+ * What a fill does to a draft that already has text in it (D-6).
886
+ * `insert` keeps the user's words; `replace` overwrites them.
887
+ */
888
+ insertMode: 'insert' | 'replace';
889
+ /** Whether the header's right-hand column may teach `^P prompts`. */
890
+ hint: boolean;
891
+ }
892
+ export declare const DEFAULT_PROMPTS_CONFIG: PromptsConfig;
893
+ /**
894
+ * THE single gate for every `prompts` read AND write, like the eleven sections
895
+ * above it.
896
+ *
897
+ * An unrecognized `insertMode` falls back to `insert` rather than to `replace`,
898
+ * and that direction is deliberate: a typo must never silently start DISCARDING
899
+ * what the user has already typed into the composer.
900
+ */
901
+ export declare function clampPromptsConfig(raw: unknown): PromptsConfig;
854
902
  /**
855
903
  * Parse `0.9`, `90%` or `90` into a ratio, or `null` when it is neither.
856
904
  *
@@ -1013,6 +1061,8 @@ export interface PersistedConfig {
1013
1061
  bash: BashConfig;
1014
1062
  /** Background jobs and bounded waiting. The ELEVENTH; scalars only (see JobsConfig). */
1015
1063
  jobs: JobsConfig;
1064
+ /** Quick prompts. The TWELFTH; three scalars (see PromptsConfig). */
1065
+ prompts: PromptsConfig;
1016
1066
  }
1017
1067
  export declare const DEFAULT_CONFIG: PersistedConfig;
1018
1068
  /**
@@ -1044,6 +1094,22 @@ export interface CliConfig {
1044
1094
  modelSettingsRestartRequired?: boolean;
1045
1095
  modelProfiles?: import('./model-profiles.js').ModelProfilesConfig;
1046
1096
  modelProfileState?: import('./model-profiles.js').ModelProfilesRuntime;
1097
+ /**
1098
+ * The SESSION kit (agent-model-kits D-5 / §7.2). RUNTIME-ONLY.
1099
+ *
1100
+ * IT IS NOT IN `PersistedConfig` AND IT NEVER WILL BE (I-5 / I-14): `ctrl+K`
1101
+ * and `/kit use` promise "this session only", and that promise is worth more
1102
+ * than a saved convenience - it is what makes "one keystroke changed my
1103
+ * defaults" structurally impossible rather than merely unlikely.
1104
+ *
1105
+ * `held` is the only fact worth storing: WHICH slots still belong to the kit.
1106
+ * The bindings are derivable, and a stored copy of them would be a second
1107
+ * truth (I-7: every held slot must have been written into the live config).
1108
+ */
1109
+ sessionKit?: {
1110
+ id: string;
1111
+ held: import('./model-kits.js').KitSlot[];
1112
+ };
1047
1113
  apiKeyOverrideTarget?: {
1048
1114
  provider: string;
1049
1115
  baseUrl: string | null;
@@ -1335,6 +1401,24 @@ export interface CliConfig {
1335
1401
  * `undefined` and report an empty value for a setting that IS on disk.
1336
1402
  */
1337
1403
  jobs: JobsConfig;
1404
+ /**
1405
+ * Quick prompts (quick-prompt-library D-19). The TWELFTH nested object; three
1406
+ * scalars, one level deep.
1407
+ *
1408
+ * ON `CliConfig` AS WELL AS `PersistedConfig`, for the reason every section above
1409
+ * records: `App` reads `cfg.prompts?.enabled` / `.hint` / `.insertMode` from a
1410
+ * `CliConfig`, so a key added to the persisted shape alone would round-trip
1411
+ * through the `config` commands, appear in `config list`, and change nothing.
1412
+ *
1413
+ * OPTIONAL ON THIS SHAPE ONLY, and `loadConfig` always fills it. The reason is the
1414
+ * one the `jobs` note above does not have to state: `CliConfig` is built by hand in
1415
+ * ~20 test fixtures, and a REQUIRED section turns every one of them into a compile
1416
+ * error for a feature none of them is about. The three readers then fall back to the
1417
+ * DOCUMENTED defaults (`enabled: true`, `insertMode: 'insert'`, `hint: true`), so a
1418
+ * missing section can only ever behave like a default session - never like a
1419
+ * disabled or destructive one.
1420
+ */
1421
+ prompts?: PromptsConfig;
1338
1422
  /**
1339
1423
  * Lifetime submit count driving the composer hint fade (`HINT_FADE_AFTER`).
1340
1424
  *
@@ -578,6 +578,7 @@ export const DEFAULT_TODO_CONFIG = {
578
578
  enabled: true,
579
579
  panel: true,
580
580
  followThrough: 'notify',
581
+ acceptance: true,
581
582
  };
582
583
  /**
583
584
  * There is no enum helper in this file, so this follows the `isThinkingLevel`
@@ -606,6 +607,10 @@ export function clampTodoConfig(raw) {
606
607
  // `ARAGON_TODO_FOLLOW=garbage` therefore resolves to `'notify'` and throws
607
608
  // nothing (AC-5).
608
609
  followThrough: followMode(src.followThrough, DEFAULT_TODO_CONFIG.followThrough),
610
+ // The fourth key needs nothing beyond this line: no migration, because
611
+ // `CONFIG_VERSION` is only stamped and never branched, and an old file reaches
612
+ // the new default through `{ ...DEFAULT, ...partial } + clamp`.
613
+ acceptance: bool(src.acceptance, DEFAULT_TODO_CONFIG.acceptance),
609
614
  };
610
615
  }
611
616
  export const DEFAULT_RETRY_CONFIG = {
@@ -958,6 +963,31 @@ export function clampJobsConfig(raw) {
958
963
  noticeSteer: bool(src.noticeSteer, DEFAULT_JOBS_CONFIG.noticeSteer),
959
964
  };
960
965
  }
966
+ export const DEFAULT_PROMPTS_CONFIG = {
967
+ enabled: true,
968
+ insertMode: 'insert',
969
+ hint: true,
970
+ };
971
+ const PROMPT_INSERT_MODES = ['insert', 'replace'];
972
+ /**
973
+ * THE single gate for every `prompts` read AND write, like the eleven sections
974
+ * above it.
975
+ *
976
+ * An unrecognized `insertMode` falls back to `insert` rather than to `replace`,
977
+ * and that direction is deliberate: a typo must never silently start DISCARDING
978
+ * what the user has already typed into the composer.
979
+ */
980
+ export function clampPromptsConfig(raw) {
981
+ const src = (raw && typeof raw === 'object' ? raw : {});
982
+ const mode = typeof src.insertMode === 'string' ? src.insertMode.trim().toLowerCase() : '';
983
+ return {
984
+ enabled: bool(src.enabled, DEFAULT_PROMPTS_CONFIG.enabled),
985
+ insertMode: PROMPT_INSERT_MODES.includes(mode)
986
+ ? mode
987
+ : DEFAULT_PROMPTS_CONFIG.insertMode,
988
+ hint: bool(src.hint, DEFAULT_PROMPTS_CONFIG.hint),
989
+ };
990
+ }
961
991
  /**
962
992
  * Parse `0.9`, `90%` or `90` into a ratio, or `null` when it is neither.
963
993
  *
@@ -1062,6 +1092,15 @@ export const DEFAULT_CONFIG = {
1062
1092
  // to `undefined` would throw on `config.jobs.waitSeconds` at construction
1063
1093
  // rather than quietly disabling something.
1064
1094
  jobs: DEFAULT_JOBS_CONFIG,
1095
+ // And one section later again, for the seventh time, and this one has BOTH
1096
+ // failure directions at once: `prompts.enabled` and `prompts.hint` are `true` by
1097
+ // default, so a `config.json` predating this feature would resolve them to
1098
+ // `undefined` (falsy) and arrive with the key and its hint silently off - while
1099
+ // `prompts.insertMode` would be `undefined`, which is neither `'insert'` nor
1100
+ // `'replace'` and would make `planPromptFill` fall through to its `replace`
1101
+ // branch. That branch discards the user's own words, so the miss would be a data
1102
+ // loss rather than a missing hint.
1103
+ prompts: DEFAULT_PROMPTS_CONFIG,
1065
1104
  };
1066
1105
  /**
1067
1106
  * Runtime state that used to be stored in `config.json` by mistake
@@ -17,7 +17,7 @@ import { getConfigPath, getSessionsDir } from './app-paths.js';
17
17
  import { registerProfileSecrets, registerSecretsFrom } from '../logging/secret-registry.js';
18
18
  import { adaptLegacyConnectionPatch, ModelProfileConfigError, readModelSettingsDisk } from './model-profile-store.js';
19
19
  import { normalizeModelProfiles } from './model-profiles.js';
20
- import { clampBashConfig, clampJobsConfig, CONFIG_VERSION, DEFAULT_CONFIG, clampCompactionConfig, clampFastConfig, clampLogConfig, clampMaxTokens, clampRetryConfig, clampSkillsConfig, clampTeamConfig, clampTodoConfig, clampContextWindow, clampUpdateConfig, isAutoToken, stripLegacyStateKeys, } from './schema.js';
20
+ import { clampBashConfig, clampJobsConfig, clampPromptsConfig, CONFIG_VERSION, DEFAULT_CONFIG, clampCompactionConfig, clampFastConfig, clampLogConfig, clampMaxTokens, clampRetryConfig, clampSkillsConfig, clampTeamConfig, clampTodoConfig, clampContextWindow, clampUpdateConfig, isAutoToken, stripLegacyStateKeys, } from './schema.js';
21
21
  /**
22
22
  * Normalize `maxTokens` for both directions of the config file.
23
23
  *
@@ -195,6 +195,11 @@ export function normalizePersistedConfig(input) {
195
195
  // outright, which is a crash on the first `job_wait` rather than a silently
196
196
  // wrong setting.
197
197
  jobs: clampJobsConfig({ ...DEFAULT_CONFIG.jobs, ...(partial.jobs ?? {}) }),
198
+ // And the twelfth, on the same line and for the same reason - with one extra
199
+ // edge: without it `prompts.insertMode` reads `undefined`, which is neither
200
+ // `'insert'` nor `'replace'`, so a fill into a non-empty draft would take the
201
+ // branch that REPLACES what the user typed.
202
+ prompts: clampPromptsConfig({ ...DEFAULT_CONFIG.prompts, ...(partial.prompts ?? {}) }),
198
203
  };
199
204
  }
200
205
  /**
@@ -302,6 +307,12 @@ export function mergePersistedConfig(current, patch) {
302
307
  // absent — with `noticeSteer`'s default masking it on the next load, so the
303
308
  // user would never learn that their privacy switch had been turned back on.
304
309
  jobs: clampJobsConfig({ ...current.jobs, ...(patch.jobs ?? {}) }),
310
+ // The twelfth section, and the seventh time this file records the same trap.
311
+ // `aragon config set prompts.hint false` sends `{ prompts: { hint: false } }`;
312
+ // without this line the shallow top-level spread REPLACES the section and
313
+ // `insertMode` is written absent - which reads back as neither `'insert'` nor
314
+ // `'replace'`, i.e. the next fill overwrites the user's own draft.
315
+ prompts: clampPromptsConfig({ ...current.prompts, ...(patch.prompts ?? {}) }),
305
316
  };
306
317
  return merged;
307
318
  }
@@ -90,6 +90,21 @@ export interface UiState {
90
90
  * others for the same reasons the two above record, and NOT a schema bump.
91
91
  */
92
92
  scrollModeNoticeVersion: number;
93
+ /**
94
+ * Which revision of the "Ctrl+P moved to the prompt library" notice this user has
95
+ * been shown (`0` = none; quick-prompt-library D-20).
96
+ *
97
+ * A VERSION BESIDE THE OTHERS AND NOT A SCHEMA BUMP, for the third time in this
98
+ * file: bumping `UI_STATE_SCHEMA` makes `readFromDisk` discard the whole object, so
99
+ * every user's `submitCount` resets and the composer hint un-fades - to re-show one
100
+ * line. A NEW field simply reads as its default on an older file, which is exactly
101
+ * the behaviour wanted here.
102
+ *
103
+ * IT EXISTS BECAUSE THE KEY MOVED. `Ctrl+P` now opens the prompt library and the
104
+ * mode fallback is `Ctrl+N`; the audience for that sentence is every existing user,
105
+ * and a boolean could only ever say "shown", never "shown WHAT".
106
+ */
107
+ promptKeyNoticeVersion: number;
93
108
  }
94
109
  /** Read the state, caching it for the life of the process. Never throws. */
95
110
  export declare function loadUiState(): UiState;
@@ -107,6 +122,9 @@ export declare function setVtInputNoticeVersion(version: number): void;
107
122
  /** `0` on a file written before the field existed. */
108
123
  export declare function getScrollModeNoticeVersion(): number;
109
124
  export declare function setScrollModeNoticeVersion(version: number): void;
125
+ /** `0` on a file written before the field existed; see the field's comment. */
126
+ export declare function getPromptKeyNoticeVersion(): number;
127
+ export declare function setPromptKeyNoticeVersion(version: number): void;
110
128
  /**
111
129
  * Migration-only: fill in fields that are still at their default, and never
112
130
  * overwrite a value already here. A second import — after a half-finished
@@ -38,6 +38,7 @@ const DEFAULT_UI_STATE = {
38
38
  mouseNoticeVersion: 0,
39
39
  vtInputNoticeVersion: 0,
40
40
  scrollModeNoticeVersion: 0,
41
+ promptKeyNoticeVersion: 0,
41
42
  };
42
43
  let memory = null;
43
44
  /** Non-negative integer or `fallback`; shared by both numeric fields. */
@@ -71,6 +72,7 @@ function readFromDisk() {
71
72
  mouseNoticeVersion: coerceCount(parsed.mouseNoticeVersion, DEFAULT_UI_STATE.mouseNoticeVersion),
72
73
  vtInputNoticeVersion: coerceCount(parsed.vtInputNoticeVersion, DEFAULT_UI_STATE.vtInputNoticeVersion),
73
74
  scrollModeNoticeVersion: coerceCount(parsed.scrollModeNoticeVersion, DEFAULT_UI_STATE.scrollModeNoticeVersion),
75
+ promptKeyNoticeVersion: coerceCount(parsed.promptKeyNoticeVersion, DEFAULT_UI_STATE.promptKeyNoticeVersion),
74
76
  };
75
77
  }
76
78
  catch {
@@ -151,6 +153,13 @@ export function getScrollModeNoticeVersion() {
151
153
  export function setScrollModeNoticeVersion(version) {
152
154
  update({ scrollModeNoticeVersion: version });
153
155
  }
156
+ /** `0` on a file written before the field existed; see the field's comment. */
157
+ export function getPromptKeyNoticeVersion() {
158
+ return loadUiState().promptKeyNoticeVersion;
159
+ }
160
+ export function setPromptKeyNoticeVersion(version) {
161
+ update({ promptKeyNoticeVersion: version });
162
+ }
154
163
  /**
155
164
  * Migration-only: fill in fields that are still at their default, and never
156
165
  * overwrite a value already here. A second import — after a half-finished
@@ -192,6 +192,22 @@ export interface ExecTodoEvent {
192
192
  content: string;
193
193
  status: 'pending' | 'in_progress' | 'completed';
194
194
  }[];
195
+ /**
196
+ * Acceptance counts (todo-acceptance-criteria §6.13.2). `null` when the capability
197
+ * is off or the plan has no criteria.
198
+ *
199
+ * REQUIRED BUT NULLABLE, not optional: `runner` is the only producer and can
200
+ * always answer, while for a consumer `null` and "field absent" mean the same
201
+ * thing under the forward-compatibility contract above. Per-criterion `evidence`
202
+ * never reaches this stream (I-8) - a machine consumer that needs it reads the
203
+ * session file.
204
+ */
205
+ acceptance: {
206
+ total: number;
207
+ checked: number;
208
+ passed: number;
209
+ failed: number;
210
+ } | null;
195
211
  }
196
212
  export interface ExecTeamEvent {
197
213
  type: 'team';
@@ -371,6 +387,15 @@ export interface ExecResultEvent {
371
387
  id: string;
372
388
  };
373
389
  todos: ExecTodoCounts | null;
390
+ /**
391
+ * The acceptance tally for the run (todo-acceptance-criteria §6.13.2), on the same
392
+ * terms as `ExecTodoEvent.acceptance`: required but nullable, no evidence.
393
+ */
394
+ acceptance: {
395
+ total: number;
396
+ passed: number;
397
+ failed: number;
398
+ } | null;
374
399
  error: ExecErrorInfo | null;
375
400
  }
376
401
  /**
@@ -44,6 +44,10 @@ export const EXEC_CAPABILITIES = [
44
44
  // Background job lifecycle events (long-task-jobs §4.10). A consumer that does
45
45
  // not know the `job` type ignores it; this string is how it asks first.
46
46
  'background-jobs',
47
+ // Acceptance counts on the todo event and the result event
48
+ // (todo-acceptance-criteria §6.13.2). APPEND-ONLY: a wrapper detects support by
49
+ // asking for this string, and an older wrapper never asks.
50
+ 'todo-acceptance',
47
51
  ];
48
52
  export function buildInitEvent(params) {
49
53
  return {
@@ -112,7 +112,7 @@ async function execute(input) {
112
112
  });
113
113
  if (plan.saved) {
114
114
  controller.replaceMessages(plan.saved.messages, plan.saved.compactionIdentity);
115
- controller.restoreTodos(plan.saved.todos);
115
+ controller.restoreTodos(plan.saved.todos, plan.saved.acceptance);
116
116
  }
117
117
  const config = controller.getConfig();
118
118
  emitter.init({
@@ -190,6 +190,14 @@ async function execute(input) {
190
190
  cost: buildCost(controller, stats.usage, stats.compactionBilling),
191
191
  model: { provider: config.provider, id: config.model },
192
192
  todos: todo ? { total: todo.total, done: todo.doneCount } : null,
193
+ // The verdict tally for the run, with no per-criterion evidence (I-8).
194
+ acceptance: todo?.acceptance
195
+ ? {
196
+ total: todo.acceptance.total,
197
+ passed: todo.acceptance.passedCount,
198
+ failed: todo.acceptance.failedCount,
199
+ }
200
+ : null,
193
201
  error: stats.errorCode && stats.errorMessage
194
202
  ? { code: stats.errorCode, message: stats.errorMessage }
195
203
  : null,
@@ -288,6 +296,7 @@ function failedResult(sessionId, config, message, code, durationMs) {
288
296
  cost: { amount: 0, currency: 'USD', known: false },
289
297
  model: { provider: config.provider, id: config.model },
290
298
  todos: null,
299
+ acceptance: null,
291
300
  error: { code, message },
292
301
  };
293
302
  }
@@ -479,6 +488,12 @@ function persist(plan, controller, turns, usage, cli, at) {
479
488
  ...(controller.getSessionSnapshot?.() ?? { messages: controller.getMessages() }),
480
489
  entries: [],
481
490
  todos: controller.getTodoSnapshot?.()?.items ?? [],
491
+ // From the STORE, not the render projection: a run whose capability is off
492
+ // must still persist the verdicts it already has. OPTIONAL CALL, like the line
493
+ // above: `ExecControllerFactory` is a port, and a caller whose controller
494
+ // predates this feature must keep saving sessions rather than fail on a method
495
+ // it never heard of - the failure mode would be a session silently not written.
496
+ acceptance: controller.getAcceptanceItems?.() ?? [],
482
497
  },
483
498
  meta,
484
499
  });
@@ -108,6 +108,16 @@ export class ExecRunner {
108
108
  done: doneCount,
109
109
  activeIndex,
110
110
  items: items.map((item) => ({ content: item.content, status: item.status })),
111
+ // Counts only, never per-criterion evidence (I-8): this stream is a machine
112
+ // surface, and a criterion's evidence is written for a human to read.
113
+ acceptance: event.snapshot.acceptance
114
+ ? {
115
+ total: event.snapshot.acceptance.total,
116
+ checked: event.snapshot.acceptance.checkedCount,
117
+ passed: event.snapshot.acceptance.passedCount,
118
+ failed: event.snapshot.acceptance.failedCount,
119
+ }
120
+ : null,
111
121
  });
112
122
  });
113
123
  if (todos)
@@ -7,7 +7,15 @@
7
7
  * does not own so the caller can drop unknown control bytes instead of
8
8
  * inserting them into the buffer.
9
9
  */
10
- /** Map a Ctrl-modified key to an intent, if any. */
10
+ /**
11
+ * Map a Ctrl-modified key to an intent, if any.
12
+ *
13
+ * `Ctrl+K` IS DELIBERATELY NOT HERE (agent-model-kits D-15). That byte belongs
14
+ * to the Agent-kit picker on the main screen, and this recognizer runs BEFORE
15
+ * `PromptInput`'s `if (key.ctrl) return` - so keeping the binding would delete
16
+ * the user's draft in one listener while the other opened the overlay. The
17
+ * capability moved to `Alt+K` (`fromMeta`) instead of being dropped.
18
+ */
11
19
  function fromCtrl(input, key) {
12
20
  if (key.leftArrow)
13
21
  return 'wordLeft';
@@ -22,8 +30,6 @@ function fromCtrl(input, key) {
22
30
  return 'deleteWordBack';
23
31
  case 'u':
24
32
  return 'killToStart';
25
- case 'k':
26
- return 'killToEnd';
27
33
  default:
28
34
  return null;
29
35
  }
@@ -34,6 +40,11 @@ function fromMeta(input) {
34
40
  return 'wordLeft';
35
41
  if (input === 'f')
36
42
  return 'wordRight';
43
+ // `killToEnd` LIVES HERE NOW (agent-model-kits D-15). `Alt+K` keeps the
44
+ // capability, and it keeps the kill family together: `M-k` is kill-sentence in
45
+ // the Emacs tradition this keymap already borrows `Ctrl+W` and `Alt+B/F` from.
46
+ if (input === 'k')
47
+ return 'killToEnd';
37
48
  if (input === '\x7f' || input === '\b')
38
49
  return 'deleteWordBack';
39
50
  return null;
@@ -63,7 +74,10 @@ function fromRaw(input) {
63
74
  return 'deleteWordBack';
64
75
  case '\x15': // Ctrl+U (raw)
65
76
  return 'killToStart';
66
- case '\x0b': // Ctrl+K (raw)
77
+ case '\x1bk':
78
+ // Alt+K (raw ESC + k). `\x0b` (Ctrl+K) is deliberately ABSENT: it belongs
79
+ // to the Agent-kit picker, and a case for it here would be a dead binding
80
+ // that still looks alive to every future reader.
67
81
  return 'killToEnd';
68
82
  default:
69
83
  return null;
@@ -79,6 +79,22 @@ export declare const PROC_LIMITS: {
79
79
  readonly outputCoalesceMs: 100;
80
80
  /** `SIGTERM` -> `SIGKILL` escalation window for a graceful stop. */
81
81
  readonly stopGraceMs: 3000;
82
+ /**
83
+ * How long to KEEP ASKING whether the `SIGKILL` actually landed before
84
+ * declaring the job `stopped`.
85
+ *
86
+ * THIS NUMBER IS NOT A GUESS ABOUT THE PLATFORM. `killTree` spawns `taskkill`,
87
+ * which is a process of its own: a Windows tree kill of a shell plus its node
88
+ * child measures ~0.8 s on the reference machine (while starting `cmd` costs
89
+ * 50 ms), so the fixed 250 ms this used to allow expired first and `stop()`
90
+ * reported `stopped` with the child still running - AC-18 caught exactly that.
91
+ * The wait stays BOUNDED against the opposite failure (a promise that never
92
+ * resolves); at this deadline the record keeps the honest `killIncomplete`
93
+ * flag instead.
94
+ */
95
+ readonly killVerifyMs: 2000;
96
+ /** Liveness poll interval while waiting for a `SIGKILL` to land. */
97
+ readonly killVerifyPollMs: 50;
82
98
  /** Below this many terminal columns the status chip degrades to `[N]`. */
83
99
  readonly statusCompactCols: 92;
84
100
  /**
@@ -79,6 +79,22 @@ export const PROC_LIMITS = {
79
79
  outputCoalesceMs: 100,
80
80
  /** `SIGTERM` -> `SIGKILL` escalation window for a graceful stop. */
81
81
  stopGraceMs: 3000,
82
+ /**
83
+ * How long to KEEP ASKING whether the `SIGKILL` actually landed before
84
+ * declaring the job `stopped`.
85
+ *
86
+ * THIS NUMBER IS NOT A GUESS ABOUT THE PLATFORM. `killTree` spawns `taskkill`,
87
+ * which is a process of its own: a Windows tree kill of a shell plus its node
88
+ * child measures ~0.8 s on the reference machine (while starting `cmd` costs
89
+ * 50 ms), so the fixed 250 ms this used to allow expired first and `stop()`
90
+ * reported `stopped` with the child still running - AC-18 caught exactly that.
91
+ * The wait stays BOUNDED against the opposite failure (a promise that never
92
+ * resolves); at this deadline the record keeps the honest `killIncomplete`
93
+ * flag instead.
94
+ */
95
+ killVerifyMs: 2000,
96
+ /** Liveness poll interval while waiting for a `SIGKILL` to land. */
97
+ killVerifyPollMs: 50,
82
98
  /** Below this many terminal columns the status chip degrades to `[N]`. */
83
99
  statusCompactCols: 92,
84
100
  // --- long-task-jobs: bounded waiting, progress, notices, log mirror --------