@aragon-agent/cli 0.6.34 → 0.6.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +173 -7
  3. package/dist/agent/agent-mode.d.ts +20 -4
  4. package/dist/agent/agent-mode.js +20 -4
  5. package/dist/agent/controller.d.ts +67 -3
  6. package/dist/agent/controller.js +106 -5
  7. package/dist/agent/headless.js +31 -0
  8. package/dist/agent/kit-apply.d.ts +56 -0
  9. package/dist/agent/kit-apply.js +209 -0
  10. package/dist/agent/model-profile-settings.d.ts +28 -0
  11. package/dist/agent/model-profile-settings.js +142 -29
  12. package/dist/agent/reducer.d.ts +13 -2
  13. package/dist/agent/reducer.js +1 -0
  14. package/dist/cli.js +12 -0
  15. package/dist/commands/agents-command.d.ts +15 -0
  16. package/dist/commands/agents-command.js +27 -0
  17. package/dist/commands/builtins.js +135 -4
  18. package/dist/commands/kit-command.d.ts +20 -0
  19. package/dist/commands/kit-command.js +131 -0
  20. package/dist/commands/perf.d.ts +11 -0
  21. package/dist/commands/perf.js +2 -1
  22. package/dist/commands/registry.d.ts +14 -0
  23. package/dist/config/app-paths.d.ts +15 -0
  24. package/dist/config/app-paths.js +33 -2
  25. package/dist/config/env.d.ts +4 -1
  26. package/dist/config/env.js +28 -2
  27. package/dist/config/kit-resolution.d.ts +93 -0
  28. package/dist/config/kit-resolution.js +226 -0
  29. package/dist/config/load.d.ts +10 -0
  30. package/dist/config/load.js +8 -1
  31. package/dist/config/model-kits.d.ts +224 -0
  32. package/dist/config/model-kits.js +302 -0
  33. package/dist/config/model-profile-store.d.ts +25 -0
  34. package/dist/config/model-profiles.d.ts +41 -3
  35. package/dist/config/model-profiles.js +46 -5
  36. package/dist/config/schema.d.ts +84 -0
  37. package/dist/config/schema.js +39 -0
  38. package/dist/config/store.js +12 -1
  39. package/dist/config/ui-state.d.ts +18 -0
  40. package/dist/config/ui-state.js +9 -0
  41. package/dist/exec/events.d.ts +25 -0
  42. package/dist/exec/events.js +4 -0
  43. package/dist/exec/index.js +16 -1
  44. package/dist/exec/runner.js +10 -0
  45. package/dist/input/keymap.js +18 -4
  46. package/dist/proc/limits.d.ts +16 -0
  47. package/dist/proc/limits.js +16 -0
  48. package/dist/proc/supervisor.d.ts +20 -1
  49. package/dist/proc/supervisor.js +42 -14
  50. package/dist/prompts/command.d.ts +34 -0
  51. package/dist/prompts/command.js +237 -0
  52. package/dist/prompts/copy.d.ts +89 -0
  53. package/dist/prompts/copy.js +131 -0
  54. package/dist/prompts/fill.d.ts +51 -0
  55. package/dist/prompts/fill.js +46 -0
  56. package/dist/prompts/limits.d.ts +53 -0
  57. package/dist/prompts/limits.js +53 -0
  58. package/dist/prompts/normalize.d.ts +53 -0
  59. package/dist/prompts/normalize.js +190 -0
  60. package/dist/prompts/resolve.d.ts +31 -0
  61. package/dist/prompts/resolve.js +60 -0
  62. package/dist/prompts/search.d.ts +58 -0
  63. package/dist/prompts/search.js +146 -0
  64. package/dist/prompts/store.d.ts +86 -0
  65. package/dist/prompts/store.js +453 -0
  66. package/dist/prompts/types.d.ts +204 -0
  67. package/dist/prompts/types.js +13 -0
  68. package/dist/session/checkpoint.d.ts +8 -1
  69. package/dist/session/checkpoint.js +6 -0
  70. package/dist/session/persist.d.ts +19 -1
  71. package/dist/session/persist.js +13 -1
  72. package/dist/session/startup-session.d.ts +7 -2
  73. package/dist/session/startup-session.js +5 -2
  74. package/dist/session/store.js +3 -0
  75. package/dist/session/validate-session.js +64 -1
  76. package/dist/team/limits.d.ts +25 -0
  77. package/dist/team/limits.js +26 -0
  78. package/dist/team/runtime.js +27 -2
  79. package/dist/team/settled.d.ts +57 -0
  80. package/dist/team/settled.js +87 -0
  81. package/dist/team/subagent.js +48 -2
  82. package/dist/team/timeline.d.ts +74 -0
  83. package/dist/team/timeline.js +101 -0
  84. package/dist/team/types.d.ts +24 -1
  85. package/dist/todo/acceptance.d.ts +120 -0
  86. package/dist/todo/acceptance.js +399 -0
  87. package/dist/todo/follow-through.d.ts +48 -23
  88. package/dist/todo/follow-through.js +110 -44
  89. package/dist/todo/limits.d.ts +62 -1
  90. package/dist/todo/limits.js +62 -1
  91. package/dist/todo/panel-rows.d.ts +29 -39
  92. package/dist/todo/panel-rows.js +37 -83
  93. package/dist/todo/plan-sections.d.ts +60 -0
  94. package/dist/todo/plan-sections.js +108 -0
  95. package/dist/todo/prompt.d.ts +17 -12
  96. package/dist/todo/prompt.js +59 -40
  97. package/dist/todo/row-window.d.ts +51 -0
  98. package/dist/todo/row-window.js +107 -0
  99. package/dist/todo/store.d.ts +139 -31
  100. package/dist/todo/store.js +271 -73
  101. package/dist/todo/todo-tool.d.ts +33 -8
  102. package/dist/todo/todo-tool.js +186 -30
  103. package/dist/todo/types.d.ts +58 -0
  104. package/dist/ui/App.js +414 -54
  105. package/dist/ui/Composer.d.ts +6 -2
  106. package/dist/ui/Composer.js +2 -2
  107. package/dist/ui/Header.d.ts +17 -1
  108. package/dist/ui/Header.js +8 -2
  109. package/dist/ui/PromptInput.d.ts +21 -1
  110. package/dist/ui/PromptInput.js +22 -2
  111. package/dist/ui/ScrollbackShell.d.ts +22 -7
  112. package/dist/ui/ScrollbackShell.js +8 -1
  113. package/dist/ui/StatusBar.d.ts +18 -0
  114. package/dist/ui/StatusBar.js +11 -3
  115. package/dist/ui/TeamPanel.d.ts +35 -41
  116. package/dist/ui/TeamPanel.js +53 -163
  117. package/dist/ui/TodoPanel.d.ts +17 -14
  118. package/dist/ui/TodoPanel.js +146 -38
  119. package/dist/ui/Transcript.js +23 -3
  120. package/dist/ui/composer-input.d.ts +10 -0
  121. package/dist/ui/composer-input.js +57 -24
  122. package/dist/ui/entries/TeamCard.d.ts +18 -2
  123. package/dist/ui/entries/TeamCard.js +47 -13
  124. package/dist/ui/entries/TodoCard.d.ts +26 -11
  125. package/dist/ui/entries/TodoCard.js +56 -15
  126. package/dist/ui/glyphs.d.ts +19 -0
  127. package/dist/ui/glyphs.js +4 -0
  128. package/dist/ui/layout/agents-board.d.ts +102 -0
  129. package/dist/ui/layout/agents-board.js +389 -0
  130. package/dist/ui/layout/cells.d.ts +61 -0
  131. package/dist/ui/layout/cells.js +156 -0
  132. package/dist/ui/layout/header-hints.d.ts +23 -3
  133. package/dist/ui/layout/header-hints.js +73 -7
  134. package/dist/ui/layout/kit-chip.d.ts +50 -0
  135. package/dist/ui/layout/kit-chip.js +81 -0
  136. package/dist/ui/layout/status-detail-layout.js +9 -0
  137. package/dist/ui/layout/status-layout.d.ts +13 -0
  138. package/dist/ui/layout/team-row.d.ts +139 -0
  139. package/dist/ui/layout/team-row.js +246 -0
  140. package/dist/ui/layout/todo-layout.d.ts +38 -0
  141. package/dist/ui/layout/todo-layout.js +75 -0
  142. package/dist/ui/layout/virtual-window.js +32 -3
  143. package/dist/ui/overlays/AgentsBoardOverlay.d.ts +57 -0
  144. package/dist/ui/overlays/AgentsBoardOverlay.js +183 -0
  145. package/dist/ui/overlays/HelpOverlay.js +24 -2
  146. package/dist/ui/overlays/KitEditor.d.ts +23 -0
  147. package/dist/ui/overlays/KitEditor.js +101 -0
  148. package/dist/ui/overlays/KitManager.d.ts +23 -0
  149. package/dist/ui/overlays/KitManager.js +104 -0
  150. package/dist/ui/overlays/KitPicker.d.ts +49 -0
  151. package/dist/ui/overlays/KitPicker.js +58 -0
  152. package/dist/ui/overlays/ModelProfilePicker.js +16 -6
  153. package/dist/ui/overlays/PromptEditorPage.d.ts +48 -0
  154. package/dist/ui/overlays/PromptEditorPage.js +227 -0
  155. package/dist/ui/overlays/PromptLibraryOverlay.d.ts +37 -0
  156. package/dist/ui/overlays/PromptLibraryOverlay.js +191 -0
  157. package/dist/ui/overlays/PromptPicker.d.ts +36 -0
  158. package/dist/ui/overlays/PromptPicker.js +129 -0
  159. package/dist/ui/overlays/PromptTextArea.d.ts +86 -0
  160. package/dist/ui/overlays/PromptTextArea.js +141 -0
  161. package/dist/ui/overlays/SettingsScreen.js +139 -16
  162. package/dist/ui/overlays/kit-picker-state.d.ts +87 -0
  163. package/dist/ui/overlays/kit-picker-state.js +136 -0
  164. package/dist/ui/overlays/kit-settings-state.d.ts +129 -0
  165. package/dist/ui/overlays/kit-settings-state.js +267 -0
  166. package/dist/ui/overlays/model-profile-state.d.ts +26 -3
  167. package/dist/ui/overlays/model-profile-state.js +59 -6
  168. package/dist/ui/overlays/prompt-library-state.d.ts +162 -0
  169. package/dist/ui/overlays/prompt-library-state.js +335 -0
  170. package/dist/ui/overlays/prompt-picker-state.d.ts +71 -0
  171. package/dist/ui/overlays/prompt-picker-state.js +98 -0
  172. package/dist/ui/scrollback/LiveTail.d.ts +2 -2
  173. package/dist/ui/scrollback/LiveTail.js +7 -2
  174. package/dist/ui/scrollback/band.js +14 -1
  175. package/dist/ui/scrollback/live-preview.js +10 -3
  176. package/dist/ui/scrollback/tail-window.d.ts +12 -0
  177. package/dist/ui/scrollback/tail-window.js +17 -2
  178. package/dist/ui/scrollback/use-scrollback-view.js +7 -0
  179. package/dist/ui/team-status.d.ts +166 -0
  180. package/dist/ui/team-status.js +252 -0
  181. package/dist/ui/transcript-text.js +13 -4
  182. package/dist/ui/use-startup-notices.d.ts +35 -1
  183. package/dist/ui/use-startup-notices.js +29 -2
  184. package/dist/ui/use-wheel-routing.js +10 -2
  185. package/package.json +2 -2
@@ -0,0 +1,399 @@
1
+ /**
2
+ * The acceptance-criteria model (todo-acceptance-criteria §6.2 / §6.3 / §6.6.2).
3
+ *
4
+ * ASCII ONLY: `src/todo/**` is inside the glyph scanner's scope.
5
+ *
6
+ * PURE: no I/O, no timers, no `Date.now()`. The clock is injected where a
7
+ * timestamp is needed, exactly as `TodoStore`'s constructor does (P2-4).
8
+ *
9
+ * ONE PRINCIPLE RUNS THROUGH THE WHOLE FILE, and it is the reason this module
10
+ * exists: **a verdict is evidence, a plan is a belief**. A verdict is never
11
+ * destroyed by omission - not by re-sending the criterion as `pending`, and not
12
+ * by leaving it out of the list altogether - because a full replacement means the
13
+ * model re-sends the whole table on every turn and one lapse would silently erase
14
+ * a conclusion the user was shown. An unchecked criterion, by contrast, IS a
15
+ * plan, so a re-plan is free to drop it.
16
+ *
17
+ * NEVER THROWS, NEVER REJECTS, ONLY REPAIRS (I-9): the same discipline
18
+ * `normalize.ts` records for the todo list itself.
19
+ */
20
+ import { ACCEPT_LIMITS, VERIFY_MESSAGE_LIMITS } from './limits.js';
21
+ /**
22
+ * Collapse whitespace and clamp.
23
+ *
24
+ * LOCAL RATHER THAN SHARED WITH `normalize.ts`: that module is not in this
25
+ * feature's change plan (§7.2), and the whole helper is four lines. Sanitizing at
26
+ * the store is the argument `normalize.ts::clean` records - a newline inside a
27
+ * rail row breaks the layout - and doing it here means the transcript card, the
28
+ * headless writer and `/todo status` all inherit the guarantee for free.
29
+ */
30
+ function clean(value, maxChars) {
31
+ if (typeof value !== 'string')
32
+ return '';
33
+ const collapsed = value.replace(/\s+/g, ' ').trim();
34
+ return collapsed.length > maxChars ? collapsed.slice(0, maxChars) : collapsed;
35
+ }
36
+ /** The three states the MODEL may name. `in_progress` is store-owned (P0-1). */
37
+ const MODEL_STATUSES = new Set([
38
+ 'pending',
39
+ 'passed',
40
+ 'failed',
41
+ ]);
42
+ const EMPTY = { items: [], requested: 0, repairs: [] };
43
+ /**
44
+ * Repair whatever arrived in the `acceptance` field of a `todo_write` call.
45
+ *
46
+ * ORDER IS SEMANTICS, and one step of that order is load-bearing enough to be a
47
+ * review condition (C-2): `enabled` short-circuits FIRST, then `undefined`, then
48
+ * `Array.isArray`. `undefined` (the key is absent) and `[]` (the model sent an
49
+ * empty array) are NOT the same thing: absent means "I am only updating steps,
50
+ * keep the table", empty means "this plan needs no criteria". This is the one
51
+ * place in the feature where the two differ (§9).
52
+ */
53
+ export function normalizeAcceptance(raw, previous, options) {
54
+ // 1. The capability is off. STRONGEST SHORT-CIRCUIT: no data may survive it, or
55
+ // `/todo acceptance off` would look like it did nothing while the rail kept
56
+ // drawing an ACCEPT section (P0-2). No repair note either - the model
57
+ // sending an extra field it learned elsewhere is not its mistake, and a note
58
+ // would turn a harmless extra key into a wasted turn.
59
+ if (!options.enabled)
60
+ return { ...EMPTY, items: [] };
61
+ // 2. The key is ABSENT: this turn is about the steps. Keep the table.
62
+ if (raw === undefined) {
63
+ return { items: [...previous], requested: previous.length, repairs: [] };
64
+ }
65
+ // 3. Not an array at all: nothing usable. Same disposition as `normalizeTodos`.
66
+ if (!Array.isArray(raw))
67
+ return { ...EMPTY, items: [] };
68
+ const requested = raw.length;
69
+ const repairs = [];
70
+ const kept = [];
71
+ for (const entry of raw) {
72
+ const item = readItem(entry);
73
+ if (!item)
74
+ continue;
75
+ kept.push(item);
76
+ }
77
+ const dropped = requested - kept.length;
78
+ if (dropped > 0) {
79
+ repairs.push(`${dropped} ${dropped === 1 ? 'criterion was' : 'criteria were'} dropped: each needs a non-empty content string.`);
80
+ }
81
+ const verdicts = applyVerdictRules(kept, previous, options.restoredNotes, repairs);
82
+ // AN EXPLICIT EMPTY ARRAY IS A STATEMENT, NOT AN OMISSION (§9): the model is saying
83
+ // "this plan needs no criteria", so nothing is restored from the previous table.
84
+ // Only a NON-empty replacement can be missing a row it did not mean to drop.
85
+ const restored = requested === 0
86
+ ? verdicts
87
+ : restoreOmitted(verdicts, previous, options.restoredNotes, repairs);
88
+ // STEP 11: `reopen` is CONSUMED, not stored. A table carrying it would put an input
89
+ // flag into the snapshot, the session file and the transcript card, where nothing
90
+ // reads it and everything would have to learn to ignore it.
91
+ return { items: cap(restored, requested, repairs).map(withoutReopen), requested, repairs };
92
+ }
93
+ function withoutReopen(item) {
94
+ if (item.reopen === undefined)
95
+ return item;
96
+ const { reopen: _reopen, ...rest } = item;
97
+ return rest;
98
+ }
99
+ /**
100
+ * One payload entry, repaired. `null` means it is not a criterion at all.
101
+ *
102
+ * `in_progress` IS DOWNGRADED SILENTLY (P0-1): the store owns that state
103
+ * (`TodoStore.write` re-derives it after every commit), the schema never offers
104
+ * it, and a note would be re-printed on every turn for a model that keeps echoing
105
+ * it. Downgrading produces exactly the same result as sending `pending`, which is
106
+ * the idempotence we want.
107
+ */
108
+ function readItem(entry) {
109
+ if (!entry || typeof entry !== 'object')
110
+ return null;
111
+ const src = entry;
112
+ const content = clean(src.content, ACCEPT_LIMITS.contentChars);
113
+ // The one field with no repair: a criterion with nothing to say is not a
114
+ // criterion.
115
+ if (content.length === 0)
116
+ return null;
117
+ const status = (typeof src.status === 'string' && MODEL_STATUSES.has(src.status) ? src.status : 'pending');
118
+ const item = {
119
+ content,
120
+ check: clean(src.check, ACCEPT_LIMITS.checkChars),
121
+ status,
122
+ evidence: clean(src.evidence, ACCEPT_LIMITS.evidenceChars),
123
+ };
124
+ // Carried just far enough for the verdict rules to see it; stripped again before
125
+ // the table is returned (§6.2.1 step 11: the flag is input, never storage).
126
+ if (src.reopen === true)
127
+ item.reopen = true;
128
+ return item;
129
+ }
130
+ /**
131
+ * A verdict without evidence is not a verdict (D-3), and a verdict is not undone
132
+ * by re-sending the same criterion as `pending` (D-4, first half).
133
+ *
134
+ * `reopen` is the only door back to `pending`, and it also clears the evidence:
135
+ * re-opening a criterion means giving up what it claimed before.
136
+ */
137
+ function applyVerdictRules(items, previous, restoredNotes, repairs) {
138
+ const byContent = new Map();
139
+ for (const prior of previous) {
140
+ if (!byContent.has(prior.content))
141
+ byContent.set(prior.content, prior);
142
+ }
143
+ return items.map((item, index) => {
144
+ if (item.status !== 'pending') {
145
+ if (item.evidence.length > 0)
146
+ return item;
147
+ repairs.push(`criterion ${index + 1} had a verdict but no evidence; it is back to unchecked.`);
148
+ return { ...item, status: 'pending', evidence: '' };
149
+ }
150
+ // RE-OPENING IS THE ONE DELIBERATE WAY BACK TO UNCHECKED (D-4), and it also
151
+ // forfeits the old evidence: a criterion being checked again has no verdict yet.
152
+ if (item.reopen === true)
153
+ return { ...item, evidence: '' };
154
+ const prior = byContent.get(item.content);
155
+ if (!prior || prior.status === 'pending')
156
+ return item;
157
+ noteOnce(restoredNotes, item.content, repairs, `criterion ${index + 1} kept its earlier verdict (a verdict is not undone by ` +
158
+ 'omission; send reopen: true to re-check it).');
159
+ return { ...item, status: prior.status, evidence: prior.evidence };
160
+ });
161
+ }
162
+ /**
163
+ * Record a restoration note AT MOST ONCE PER CRITERION (P2-4).
164
+ *
165
+ * The model does not know the server restored anything, so it sends the same row
166
+ * again on the next turn. Without this the identical `Note:` is billed every turn,
167
+ * for as long as the plan lives.
168
+ */
169
+ function noteOnce(restoredNotes, content, repairs, text) {
170
+ if (!restoredNotes) {
171
+ repairs.push(text);
172
+ return;
173
+ }
174
+ if (restoredNotes.has(content))
175
+ return;
176
+ restoredNotes.add(content);
177
+ repairs.push(text);
178
+ }
179
+ /**
180
+ * D-4's OTHER HALF (P0-4): a criterion that is MISSING from a full replacement
181
+ * keeps its verdict too.
182
+ *
183
+ * The commonest shape of "omission" is not a re-sent `pending` row, it is a
184
+ * table that is one row shorter - and without this the criterion, its verdict and
185
+ * its evidence all disappear while `total` silently shrinks (`4/5` becomes
186
+ * `3/4` and nothing says so).
187
+ *
188
+ * Restored rows go to the TAIL, which is also where `cap` starts cutting, so a
189
+ * re-plan that grows past the ceiling loses restored rows rather than the model's
190
+ * newest intent. An UNCHECKED omitted criterion is dropped on purpose: that is a
191
+ * plan, not a verdict.
192
+ */
193
+ function restoreOmitted(items, previous, restoredNotes, repairs) {
194
+ if (previous.length === 0)
195
+ return items;
196
+ const present = new Set(items.map((item) => item.content));
197
+ const restored = [...items];
198
+ for (const prior of previous) {
199
+ if (present.has(prior.content) || prior.status === 'pending')
200
+ continue;
201
+ present.add(prior.content);
202
+ restored.push({
203
+ content: prior.content,
204
+ check: prior.check,
205
+ status: prior.status,
206
+ evidence: prior.evidence,
207
+ });
208
+ // ALWAYS RESTORED; the NOTE is what is deduped, never the restoration itself.
209
+ noteOnce(restoredNotes, prior.content, repairs, `criterion ${restored.length} was missing from the list but had a verdict; it was kept ` +
210
+ 'with its evidence (send reopen: true to re-check it).');
211
+ }
212
+ return restored;
213
+ }
214
+ /**
215
+ * DROP FIRST AND CAP SECOND would be wrong here, and the ORDER is reversed on
216
+ * purpose: restored rows sit at the tail exactly so that this `slice` takes them
217
+ * before it takes anything the model just sent.
218
+ */
219
+ function cap(items, requested, repairs) {
220
+ const ceiling = ACCEPT_LIMITS.maxItems;
221
+ if (items.length <= ceiling)
222
+ return items;
223
+ const dropped = items.length - ceiling;
224
+ repairs.push(`${requested} criteria were sent; the last ${dropped} were dropped (maximum ${ceiling}).`);
225
+ return items.slice(0, ceiling);
226
+ }
227
+ /**
228
+ * The immutable projection of one committed table (§6.2.2).
229
+ *
230
+ * CALLED FROM EXACTLY ONE PLACE PER COMMIT - `TodoStore`'s `write` / `restore` /
231
+ * `clear` / `reopenAll` - and stored, never recomputed on read. `snapshot()` runs
232
+ * on every event; recomputing here would stamp a new `now()` and a new object
233
+ * each frame, invalidating `TodoCard`'s `React.memo` and the transcript's height
234
+ * cache (P1-3).
235
+ */
236
+ export function acceptanceSummary(items, now) {
237
+ let passedCount = 0;
238
+ let failedCount = 0;
239
+ for (const item of items) {
240
+ if (item.status === 'passed')
241
+ passedCount += 1;
242
+ else if (item.status === 'failed')
243
+ failedCount += 1;
244
+ }
245
+ return {
246
+ items: [...items],
247
+ total: items.length,
248
+ checkedCount: passedCount + failedCount,
249
+ passedCount,
250
+ failedCount,
251
+ updatedAt: now(),
252
+ };
253
+ }
254
+ /**
255
+ * The ONE judge of "which phase is this plan in" (I-3 / D-5).
256
+ *
257
+ * Derived rather than stored, so the model, the rail, the transcript card and the
258
+ * machine event stream cannot disagree: a stored field would be a second writer
259
+ * of a fact that two lists already determine. Callers must not invoke this for a
260
+ * `null` plan.
261
+ */
262
+ export function derivePlanPhase(items, acceptance) {
263
+ const allStepsDone = items.length > 0 && items.every((item) => item.status === 'completed');
264
+ if (!allStepsDone)
265
+ return 'work';
266
+ if (!acceptance || acceptance.length === 0)
267
+ return 'done';
268
+ // "Unresolved" is `pending` and `in_progress`; the other two states of the
269
+ // four-state model are already verdicts.
270
+ const unresolved = acceptance.some((item) => item.status === 'pending' || item.status === 'in_progress');
271
+ return unresolved ? 'verify' : 'done';
272
+ }
273
+ /**
274
+ * "Is there nothing left to do in this plan" - THE single judge (G-4 / I-3).
275
+ *
276
+ * Referenced by follow-through, by `App`'s grace-window re-check and by
277
+ * `TodoStore.beginUserTurn`, because "every step is completed" stopped being the
278
+ * same claim as "the work is done" the moment acceptance criteria existed. Three
279
+ * copies of the old equation is exactly how a plan gets cleared while its last
280
+ * criterion is still unchecked.
281
+ */
282
+ export function planIsResolved(items, acceptance) {
283
+ return items.length > 0 && derivePlanPhase(items, acceptance) === 'done';
284
+ }
285
+ export function countUnfinishedSteps(items) {
286
+ let count = 0;
287
+ for (const item of items)
288
+ if (item.status !== 'completed')
289
+ count += 1;
290
+ return count;
291
+ }
292
+ export function countUncheckedCriteria(acceptance) {
293
+ if (!acceptance)
294
+ return 0;
295
+ let count = 0;
296
+ for (const item of acceptance) {
297
+ if (item.status === 'pending' || item.status === 'in_progress')
298
+ count += 1;
299
+ }
300
+ return count;
301
+ }
302
+ /**
303
+ * The six follow-through exits all need "how much is left", and after this
304
+ * feature that quantity has two halves (§6.6.1.1). One function decides it, so no
305
+ * exit can print `0 steps left` on a plan whose steps are all done but whose
306
+ * criteria are not - which is what `total - doneCount` would say on every verify
307
+ * turn.
308
+ */
309
+ export function outstandingOf(snapshot) {
310
+ return {
311
+ steps: countUnfinishedSteps(snapshot.items),
312
+ checks: countUncheckedCriteria(snapshot.acceptance?.items ?? null),
313
+ };
314
+ }
315
+ /**
316
+ * Singular and plural decided in ONE place (§6.6.1.1): six call sites must not
317
+ * each spell their own.
318
+ */
319
+ export function criteriaPhrase(count) {
320
+ return count === 1 ? '1 acceptance criterion' : `${count} acceptance criteria`;
321
+ }
322
+ /**
323
+ * A bounded enumeration of what a plan still owes, for the tool result text
324
+ * (§6.3). Ordered `todoFirst` because the tool result is read top-down and the
325
+ * step being worked on is the more urgent of the two when both exist.
326
+ *
327
+ * BOUNDED BY CONSTRUCTION at `VERIFY_MESSAGE_LIMITS.uncheckedItems` rows: this
328
+ * string is paid for on every call (I-11).
329
+ */
330
+ export function planOutstanding(items, acceptance, todoFirst) {
331
+ const steps = items
332
+ .map((item, index) => ({ item, index }))
333
+ .filter(({ item }) => item.status !== 'completed')
334
+ .map(({ item, index }) => `${index + 1}. ${item.content}`);
335
+ const checks = (acceptance ?? [])
336
+ .map((item, index) => ({ item, index }))
337
+ .filter(({ item }) => item.status === 'pending' || item.status === 'in_progress')
338
+ .map(({ item, index }) => `${index + 1}. ${item.content}`);
339
+ const limit = VERIFY_MESSAGE_LIMITS.uncheckedItems;
340
+ return (todoFirst ? [...steps, ...checks] : [...checks, ...steps]).slice(0, limit);
341
+ }
342
+ /**
343
+ * The continuation text for the verification phase (§6.6.2).
344
+ *
345
+ * CONSTRUCTIVELY BOUNDED, which is what AC-22 asserts: the unchecked list stops
346
+ * at 5 rows, the failed list at 2, `check` is clipped to 100 characters and
347
+ * `evidence` to 60. Both truncations are deliberate - the full text is already in
348
+ * the model's own previous payload, so re-sending it is paying twice for the same
349
+ * bytes; what the model needs is a reminder of WHICH rows are open.
350
+ */
351
+ export function buildVerifyMessage(snapshot) {
352
+ const criteria = snapshot.acceptance?.items ?? [];
353
+ const unchecked = selectRows(criteria, (item) => item.status === 'pending' || item.status === 'in_progress', VERIFY_MESSAGE_LIMITS.uncheckedItems);
354
+ const failed = selectRows(criteria, (item) => item.status === 'failed', VERIFY_MESSAGE_LIMITS.failedItems);
355
+ const lines = ['Every step is complete. Verify the acceptance criteria one at a time.'];
356
+ if (unchecked.rows.length > 0) {
357
+ lines.push('Unchecked:');
358
+ for (const { item, index } of unchecked.rows) {
359
+ lines.push(`${index + 1}. ${item.content}`);
360
+ const hint = clip(item.check, VERIFY_MESSAGE_LIMITS.checkChars);
361
+ if (hint.length > 0)
362
+ lines.push(` check: ${hint}`);
363
+ }
364
+ if (unchecked.hidden > 0)
365
+ lines.push(` ... and ${unchecked.hidden} more`);
366
+ }
367
+ if (failed.rows.length > 0) {
368
+ lines.push(`${failed.total} failed so far - fix those before claiming the work is done:`);
369
+ for (const { item, index } of failed.rows) {
370
+ lines.push(`${index + 1}. ${item.content}`);
371
+ const seen = clip(item.evidence, VERIFY_MESSAGE_LIMITS.evidenceChars);
372
+ if (seen.length > 0)
373
+ lines.push(` evidence: ${seen}`);
374
+ }
375
+ }
376
+ lines.push('Check ONE, then call todo_write with that verdict and its evidence.');
377
+ return lines.join('\n');
378
+ }
379
+ /**
380
+ * The first `limit` matches, each carrying its 1-BASED position in the WHOLE
381
+ * table: "step 5" must mean the same thing here as it does in the rail, which is
382
+ * why the position is read from the full walk rather than from the filtered list.
383
+ */
384
+ function selectRows(all, keep, limit) {
385
+ const rows = [];
386
+ let total = 0;
387
+ for (let index = 0; index < all.length; index += 1) {
388
+ const item = all[index];
389
+ if (!keep(item))
390
+ continue;
391
+ total += 1;
392
+ if (rows.length < limit)
393
+ rows.push({ item, index });
394
+ }
395
+ return { rows, total, hidden: Math.max(0, total - rows.length) };
396
+ }
397
+ function clip(text, maxChars) {
398
+ return text.length > maxChars ? text.slice(0, maxChars) : text;
399
+ }
@@ -1,6 +1,6 @@
1
1
  /**
2
- * follow-through — what happens when a run ends with unfinished steps
3
- * (todo-plan-followthrough §3.2 / §3.3).
2
+ * follow-through — what happens when a run ends with unfinished work
3
+ * (todo-plan-followthrough §3.2 / §3.3; todo-acceptance-criteria §6.6).
4
4
  *
5
5
  * ASCII ONLY: `src/todo/**` is inside the glyph scanner's scope
6
6
  * (`glyphs.test.ts`), and every string this module builds is user-visible.
@@ -14,6 +14,14 @@
14
14
  * IT NEVER WRITES TO `TodoStore` (I-1 / C-3). Follow-through reads snapshots and
15
15
  * produces a MESSAGE; the plan changes only when the model calls `todo_write`,
16
16
  * which keeps the list a projection of what the model believes.
17
+ *
18
+ * "UNFINISHED WORK" HAS TWO HALVES SINCE ACCEPTANCE CRITERIA EXISTED (§6.6.1):
19
+ * steps still to do, and criteria still unchecked. Every exit below reads them
20
+ * from `outstandingOf`, and while ANY step is left the wording is byte-identical
21
+ * to the version without this feature (I-5) - because that is the phase in which
22
+ * the old sentences are already true. In the verification phase, where
23
+ * `total - doneCount` is always 0, saying "0 steps left" would be the exact
24
+ * opposite of the truth at the moment the user most wants reassurance.
17
25
  */
18
26
  import type { TodoSnapshot } from './types.js';
19
27
  /**
@@ -25,28 +33,41 @@ export type FollowThroughMode = 'notify' | 'auto' | 'off';
25
33
  /** Carried across runs by the caller. Serializable, comparable, inert. */
26
34
  export interface FollowThroughBudget {
27
35
  /**
28
- * Auto-continuations issued against this list LINEAGE.
36
+ * Auto-continuations issued against this plan LINEAGE.
29
37
  *
30
- * MONOTONIC UNTIL THE LIST IS GONE (P0-1 / D-16 / I-8). It is NOT reset by a
31
- * re-plan: a model that emits a differently-sized list on every continuation
38
+ * MONOTONIC UNTIL THE PLAN IS GONE (P0-1 / D-16 / I-8). It is NOT reset by a
39
+ * re-plan: a model that emits a differently-sized plan on every continuation
32
40
  * would otherwise zero both counters every turn while `staleTurns` also never
33
41
  * fires (it IS writing), which is an unbounded loop reachable by ordinary
34
42
  * "re-plan as you learn" behaviour rather than by anything adversarial.
35
43
  *
36
- * A refactor that "simplifies" `advanceBudget` back to returning a zeroed
37
- * budget on a `total` change re-opens that hole, and the symptom is a bill
38
- * rather than a test failure. `advanceBudget`'s own test pins it.
44
+ * A refactor that "simplifies" `advanceBudget` back to returning a zeroed budget
45
+ * on a `total` change re-opens that hole, and the symptom is a bill rather than a
46
+ * test failure. `advanceBudget`'s own test pins it.
39
47
  */
40
48
  used: number;
41
- /** Consecutive auto-continuations after which `doneCount` did not increase. */
49
+ /** Consecutive auto-continuations after which the PROGRESS POINT did not increase. */
42
50
  noProgressStreak: number;
43
- /** `TodoSnapshot.total` the budget was opened against. Identity, not a count. */
51
+ /**
52
+ * Identity of the plan the budget was opened against: steps PLUS criteria.
53
+ *
54
+ * `todo-acceptance-criteria §6.6.3`: a re-plan that goes from 3 criteria to 4 is
55
+ * a re-plan even when the number of steps is unchanged, and without the second
56
+ * term it would be mistaken for "the same plan made no progress".
57
+ */
44
58
  anchorTotal: number;
45
59
  /**
46
- * `doneCount` as observed at the PREVIOUS `agent_end` — which is what
60
+ * The PROGRESS POINT as observed at the PREVIOUS `agent_end` — which is what
47
61
  * `advanceBudget` assigns, in every branch, and therefore what the next call
48
- * compares against. An implementer who captures it at `runStart` instead
49
- * builds a different state machine that happens to agree in the normal case.
62
+ * compares against.
63
+ *
64
+ * COMPOSITE SINCE todo-acceptance-criteria: it is `doneCount + checkedCount`,
65
+ * not `doneCount`. The name is kept deliberately (Q-6): this object is carried
66
+ * across runs by `App`'s `budgetRef`, so renaming a field changes the shape any
67
+ * external holder - including test fixtures - sees. With only `doneCount`, a
68
+ * verification run that checks one criterion per turn would be recorded as "no
69
+ * progress" and `maxNoProgressContinues` would hand control back after two
70
+ * checks, making it impossible to verify a third criterion automatically.
50
71
  */
51
72
  doneAtRunStart: number;
52
73
  }
@@ -82,7 +103,7 @@ export type FollowThroughDecision = {
82
103
  graceMs: number;
83
104
  notice: string;
84
105
  };
85
- /** The zero budget. A fresh App, a fresh headless run, and every cleared list. */
106
+ /** The zero budget. A fresh App, a fresh headless run, and every cleared plan. */
86
107
  export declare function emptyBudget(): FollowThroughBudget;
87
108
  export declare function decideFollowThrough(input: FollowThroughInput): FollowThroughDecision;
88
109
  /**
@@ -90,9 +111,9 @@ export declare function decideFollowThrough(input: FollowThroughInput): FollowTh
90
111
  * every `agent_end` (and on every resolved `prompt()` in headless).
91
112
  *
92
113
  * `used` is NOT incremented here — a decision that is never acted on costs
93
- * nothing. Both fire points charge it themselves (I-5): the TUI's grace timer
94
- * and the headless loop, and BOTH are required. An uncharged counter in `-p` is
95
- * an uncapped loop in the one environment with no human to press Esc (P1-2).
114
+ * nothing. Both fire points charge it themselves (I-5): the TUI's grace timer and
115
+ * the headless loop, and BOTH are required. An uncharged counter in `-p` is an
116
+ * uncapped loop in the one environment with no human to press Esc (P1-2).
96
117
  */
97
118
  export declare function advanceBudget(prev: FollowThroughBudget, snapshot: TodoSnapshot | null, wasAutoContinuation: boolean): FollowThroughBudget;
98
119
  /**
@@ -100,14 +121,18 @@ export declare function advanceBudget(prev: FollowThroughBudget, snapshot: TodoS
100
121
  *
101
122
  * IT ENUMERATES RATHER THAN REFERRING (D-11). Round 1 submitted the fixed string
102
123
  * "Continue with the remaining todo items.", which is a reference to a
103
- * `todo_write` call that in a long session may have scrolled far enough back
104
- * that the model reconstructs it wrong — and reconstructing it wrong is SILENT,
105
- * because whatever it writes next is a full replacement and becomes the new
106
- * truth.
124
+ * `todo_write` call that in a long session may have scrolled far enough back that
125
+ * the model reconstructs it wrong — and reconstructing it wrong is SILENT, because
126
+ * whatever it writes next is a full replacement and becomes the new truth.
127
+ *
128
+ * IN THE VERIFICATION PHASE IT BRANCHES TO `buildVerifyMessage` (§6.6.2): with
129
+ * every step completed, the old body would emit "These steps are not done yet:"
130
+ * followed by nothing at all, which is what `/todo continue` and auto-continue
131
+ * would both submit at exactly the moment the plan needs its criteria checked.
107
132
  *
108
133
  * `content` and never `activeForm`: the imperative is what an instruction wants.
109
- * Numbers are the item's 1-based position in the WHOLE list, so "step 5" means
110
- * the same thing here as it does in the rail. Completed items are omitted.
134
+ * Numbers are the item's 1-based position in the WHOLE plan, so "step 5" means the
135
+ * same thing here as it does in the rail. Completed items are omitted.
111
136
  * `TODO_LIMITS.maxItems` bounds the whole message at roughly 1.8 kB.
112
137
  */
113
138
  export declare function buildContinuationMessage(snapshot: TodoSnapshot): string;