@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,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,9 +14,18 @@
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
  */
26
+ import { buildVerifyMessage, criteriaPhrase, outstandingOf } from './acceptance.js';
18
27
  import { TODO_FOLLOW_LIMITS } from './limits.js';
19
- /** The zero budget. A fresh App, a fresh headless run, and every cleared list. */
28
+ /** The zero budget. A fresh App, a fresh headless run, and every cleared plan. */
20
29
  export function emptyBudget() {
21
30
  return { used: 0, noProgressStreak: 0, anchorTotal: 0, doneAtRunStart: 0 };
22
31
  }
@@ -36,13 +45,35 @@ function legacyUnfinishedNotice(left) {
36
45
  function advise(interactive, text) {
37
46
  return interactive ? ` ${text}` : '';
38
47
  }
48
+ /** `<n> acceptance criteria are unchecked` — singular and plural in one place. */
49
+ function uncheckedCriteria(count) {
50
+ return `${criteriaPhrase(count)} ${count === 1 ? 'is' : 'are'} unchecked`;
51
+ }
52
+ /**
53
+ * The verification-phase notice for one exit (§6.6.1.1).
54
+ *
55
+ * ONE FUNCTION, SIX CALLERS, because the noun's number and the verb's number have
56
+ * to agree in the same sentence and six hand-written strings will not stay in
57
+ * agreement.
58
+ */
59
+ function verificationNotice(checks, interactive, tail, advice) {
60
+ return `${uncheckedCriteria(checks)}${tail}${advise(interactive, advice)}`;
61
+ }
39
62
  export function decideFollowThrough(input) {
40
63
  const { mode, snapshot, runEnd, budget, interactive } = input;
41
- // 1. Nothing is unfinished. `beginUserTurn` already drops a fully-completed
42
- // list at the next turn, so this branch must not pre-empt that feedback.
43
- if (!snapshot || snapshot.doneCount >= snapshot.total)
64
+ // 1. Nothing is unfinished - INCLUDING the criteria (§6.6.1). "Every step is
65
+ // completed" stopped being the same claim as "there is nothing left" the
66
+ // moment a plan carried acceptance criteria; `beginUserTurn` already drops a
67
+ // fully-resolved plan at the next turn, so this branch must not pre-empt that
68
+ // feedback.
69
+ const outstanding = snapshot ? outstandingOf(snapshot) : { steps: 0, checks: 0 };
70
+ if (!snapshot || outstanding.steps + outstanding.checks === 0)
44
71
  return { kind: 'none' };
45
- const left = snapshot.total - snapshot.doneCount;
72
+ const left = outstanding.steps;
73
+ // WHILE ANY STEP IS LEFT THE OLD SENTENCES ARE ALREADY TRUE, so they are reused
74
+ // verbatim (I-5) and only the verification phase gets new wording.
75
+ const verifying = left === 0 && outstanding.checks > 0;
76
+ const checks = outstanding.checks;
46
77
  // 2. The user asked for silence and gets silence, INCLUDING no notice.
47
78
  if (mode === 'off')
48
79
  return { kind: 'none' };
@@ -51,25 +82,38 @@ export function decideFollowThrough(input) {
51
82
  // continue it is worse.
52
83
  if (runEnd.aborted)
53
84
  return { kind: 'none' };
54
- // 4. NEVER `continue`, IN ANY MODE (D-6). Auto-continuing into a model that
55
- // just failed authentication or ran out of quota is the unbounded cost loop
56
- // with none of the upside.
85
+ // 4. NEVER `continue`, IN ANY MODE (D-6). Auto-continuing into a model that just
86
+ // failed authentication or ran out of quota is the unbounded cost loop with
87
+ // none of the upside.
57
88
  if (runEnd.errored) {
58
89
  return {
59
90
  kind: 'notify',
60
91
  level: 'warn',
61
- text: `${left} ${left === 1 ? 'step' : 'steps'} left; the run ended with an error.` +
62
- advise(interactive, '/todo continue to retry, /todo clear to drop it.'),
92
+ text: verifying
93
+ ? verificationNotice(checks, interactive, '; the run ended with an error.', '/todo continue to retry, /todo clear to drop it.')
94
+ : `${left} ${left === 1 ? 'step' : 'steps'} left; the run ended with an error.` +
95
+ advise(interactive, '/todo continue to retry, /todo clear to drop it.'),
63
96
  };
64
97
  }
65
- // 5. The default. Byte-identical to round 1 in the TUI (AC-1).
98
+ // 5. The default. Byte-identical to round 1 in the TUI whenever steps remain
99
+ // (AC-1 / AC-44's reverse direction).
66
100
  if (mode === 'notify') {
101
+ if (!verifying) {
102
+ return {
103
+ kind: 'notify',
104
+ level: 'info',
105
+ text: interactive
106
+ ? legacyUnfinishedNotice(left)
107
+ : `${left} ${left === 1 ? 'step' : 'steps'} unfinished`,
108
+ };
109
+ }
67
110
  return {
68
111
  kind: 'notify',
69
112
  level: 'info',
70
113
  text: interactive
71
- ? legacyUnfinishedNotice(left)
72
- : `${left} ${left === 1 ? 'step' : 'steps'} unfinished`,
114
+ ? `${uncheckedCriteria(checks)}. Use /todo continue to finish the check, ` +
115
+ '/todo clear to drop it.'
116
+ : `${criteriaPhrase(checks)} unchecked`,
73
117
  };
74
118
  }
75
119
  // 6. The ceiling that holds when everything else is wrong.
@@ -77,33 +121,45 @@ export function decideFollowThrough(input) {
77
121
  return {
78
122
  kind: 'notify',
79
123
  level: 'warn',
80
- text: `${left} ${left === 1 ? 'step' : 'steps'} left; auto-continue has run ` +
81
- `${TODO_FOLLOW_LIMITS.maxAutoContinuesPerList} times for this plan.` +
82
- advise(interactive, '/todo continue to keep going.'),
124
+ text: verifying
125
+ ? verificationNotice(checks, interactive, `; auto-continue has run ${TODO_FOLLOW_LIMITS.maxAutoContinuesPerList} times ` +
126
+ 'for this plan.', '/todo continue to keep going.')
127
+ : `${left} ${left === 1 ? 'step' : 'steps'} left; auto-continue has run ` +
128
+ `${TODO_FOLLOW_LIMITS.maxAutoContinuesPerList} times for this plan.` +
129
+ advise(interactive, '/todo continue to keep going.'),
83
130
  };
84
131
  }
85
132
  // 7. A model that has stopped engaging with its own plan hands back after one
86
133
  // nudge rather than after twenty-five.
87
134
  if (budget.noProgressStreak >= TODO_FOLLOW_LIMITS.maxNoProgressContinues) {
135
+ const attempts = TODO_FOLLOW_LIMITS.maxNoProgressContinues;
88
136
  return {
89
137
  kind: 'notify',
90
138
  level: 'warn',
91
- text: `${left} ${left === 1 ? 'step' : 'steps'} left; the last ` +
92
- `${TODO_FOLLOW_LIMITS.maxNoProgressContinues} attempts completed none of them.` +
93
- advise(interactive, '/todo continue to retry, or take over.'),
139
+ text: verifying
140
+ ? verificationNotice(checks, interactive, `; the last ${attempts} attempts checked none of them.`, '/todo continue to retry, or take over.')
141
+ : `${left} ${left === 1 ? 'step' : 'steps'} left; the last ` +
142
+ `${attempts} attempts completed none of them.` +
143
+ advise(interactive, '/todo continue to retry, or take over.'),
94
144
  };
95
145
  }
96
146
  const graceMs = interactive ? TODO_FOLLOW_LIMITS.graceMs : 0;
147
+ const seconds = Math.round(graceMs / 1000);
97
148
  return {
98
149
  kind: 'continue',
99
150
  message: buildContinuationMessage(snapshot),
100
151
  graceMs,
101
152
  // The seconds figure is DERIVED from the constant, never spelled again: a
102
153
  // notice that promises 3 s while the timer waits 5 is worse than no notice.
103
- notice: interactive
104
- ? `Continuing with ${left} remaining ${left === 1 ? 'step' : 'steps'} in ` +
105
- `${Math.round(graceMs / 1000)}s - Esc to stop.`
106
- : `continuing (${left} ${left === 1 ? 'step' : 'steps'} left)`,
154
+ notice: verifying
155
+ ? interactive
156
+ ? `Continuing with ${checks} unchecked acceptance ` +
157
+ `${checks === 1 ? 'criterion' : 'criteria'} in ${seconds}s - Esc to stop.`
158
+ : `continuing (${checks} acceptance ${checks === 1 ? 'criterion' : 'criteria'} unchecked)`
159
+ : interactive
160
+ ? `Continuing with ${left} remaining ${left === 1 ? 'step' : 'steps'} in ` +
161
+ `${seconds}s - Esc to stop.`
162
+ : `continuing (${left} ${left === 1 ? 'step' : 'steps'} left)`,
107
163
  };
108
164
  }
109
165
  /**
@@ -111,32 +167,36 @@ export function decideFollowThrough(input) {
111
167
  * every `agent_end` (and on every resolved `prompt()` in headless).
112
168
  *
113
169
  * `used` is NOT incremented here — a decision that is never acted on costs
114
- * nothing. Both fire points charge it themselves (I-5): the TUI's grace timer
115
- * and the headless loop, and BOTH are required. An uncharged counter in `-p` is
116
- * an uncapped loop in the one environment with no human to press Esc (P1-2).
170
+ * nothing. Both fire points charge it themselves (I-5): the TUI's grace timer and
171
+ * the headless loop, and BOTH are required. An uncharged counter in `-p` is an
172
+ * uncapped loop in the one environment with no human to press Esc (P1-2).
117
173
  */
118
174
  export function advanceBudget(prev, snapshot, wasAutoContinuation) {
119
- // THE ONLY PATH THAT CLEARS `used` (I-8). The list is gone; so is its economy.
175
+ // THE ONLY PATH THAT CLEARS `used` (I-8). The plan is gone; so is its economy.
120
176
  // It is not reachable by a model on its own: `TodoStore.write` leaves the
121
- // previous list untouched when a payload yields zero usable items, and the
122
- // other three ways a list disappears are `/todo clear`, `/reset` and
123
- // `beginUserTurn` dropping a list that is either finished (nothing to
124
- // continue) or stale (which the streak rule beats to the punch, §3.5).
177
+ // previous plan untouched when a payload yields zero usable items, and the other
178
+ // three ways a plan disappears are `/todo clear`, `/reset` and `beginUserTurn`
179
+ // dropping a plan that is either finished (nothing to continue) or stale (which
180
+ // the streak rule beats to the punch, §3.5).
125
181
  if (!snapshot)
126
182
  return emptyBudget();
183
+ // The plan's identity is STEPS PLUS CRITERIA (§6.6.3): a re-plan that only
184
+ // changes the number of criteria is still a re-plan.
185
+ const anchor = snapshot.total + (snapshot.acceptance?.total ?? 0);
186
+ const progress = snapshot.doneCount + (snapshot.acceptance?.checkedCount ?? 0);
127
187
  // A RE-PLAN BUYS FORGIVENESS, NOT A FRESH WALLET (D-16). Replanning is
128
188
  // engagement rather than looping, which is why the streak is forgiven; zeroing
129
189
  // `used` as well is what makes the hard ceiling unreachable, because a size
130
190
  // change every turn would reset it every turn.
131
- if (snapshot.total !== prev.anchorTotal) {
191
+ if (anchor !== prev.anchorTotal) {
132
192
  return {
133
193
  used: prev.used,
134
194
  noProgressStreak: 0,
135
- anchorTotal: snapshot.total,
136
- doneAtRunStart: snapshot.doneCount,
195
+ anchorTotal: anchor,
196
+ doneAtRunStart: progress,
137
197
  };
138
198
  }
139
- const progressed = snapshot.doneCount > prev.doneAtRunStart;
199
+ const progressed = progress > prev.doneAtRunStart;
140
200
  // NOTE THE GUARD: a run the USER started never charges the streak, because the
141
201
  // user asking a question mid-plan is not the model failing to advance it.
142
202
  const noProgressStreak = progressed
@@ -150,7 +210,7 @@ export function advanceBudget(prev, snapshot, wasAutoContinuation) {
150
210
  anchorTotal: prev.anchorTotal,
151
211
  // Assigned in EVERY branch, which is what makes the next call's comparison
152
212
  // meaningful.
153
- doneAtRunStart: snapshot.doneCount,
213
+ doneAtRunStart: progress,
154
214
  };
155
215
  }
156
216
  /**
@@ -158,14 +218,18 @@ export function advanceBudget(prev, snapshot, wasAutoContinuation) {
158
218
  *
159
219
  * IT ENUMERATES RATHER THAN REFERRING (D-11). Round 1 submitted the fixed string
160
220
  * "Continue with the remaining todo items.", which is a reference to a
161
- * `todo_write` call that in a long session may have scrolled far enough back
162
- * that the model reconstructs it wrong — and reconstructing it wrong is SILENT,
163
- * because whatever it writes next is a full replacement and becomes the new
164
- * truth.
221
+ * `todo_write` call that in a long session may have scrolled far enough back that
222
+ * the model reconstructs it wrong — and reconstructing it wrong is SILENT, because
223
+ * whatever it writes next is a full replacement and becomes the new truth.
224
+ *
225
+ * IN THE VERIFICATION PHASE IT BRANCHES TO `buildVerifyMessage` (§6.6.2): with
226
+ * every step completed, the old body would emit "These steps are not done yet:"
227
+ * followed by nothing at all, which is what `/todo continue` and auto-continue
228
+ * would both submit at exactly the moment the plan needs its criteria checked.
165
229
  *
166
230
  * `content` and never `activeForm`: the imperative is what an instruction wants.
167
- * Numbers are the item's 1-based position in the WHOLE list, so "step 5" means
168
- * the same thing here as it does in the rail. Completed items are omitted.
231
+ * Numbers are the item's 1-based position in the WHOLE plan, so "step 5" means the
232
+ * same thing here as it does in the rail. Completed items are omitted.
169
233
  * `TODO_LIMITS.maxItems` bounds the whole message at roughly 1.8 kB.
170
234
  */
171
235
  export function buildContinuationMessage(snapshot) {
@@ -173,6 +237,8 @@ export function buildContinuationMessage(snapshot) {
173
237
  .map((item, index) => ({ item, index }))
174
238
  .filter(({ item }) => item.status !== 'completed')
175
239
  .map(({ item, index }) => `${index + 1}. ${item.content}`);
240
+ if (remaining.length === 0)
241
+ return buildVerifyMessage(snapshot);
176
242
  return [
177
243
  'Continue the plan. These steps are not done yet:',
178
244
  ...remaining,
@@ -87,5 +87,66 @@ export declare const TODO_FOLLOW_LIMITS: {
87
87
  *
88
88
  * NOT BUMPED BY follow-through (D-3): the model is never told about the
89
89
  * mechanism, so the block is byte-identical to `bf6aeb1a`.
90
+ *
91
+ * BUMPED BY todo-acceptance-criteria (D-3 inverted): that feature CHANGES what
92
+ * the model reads, so the revision must be greppable from a behaviour report.
93
+ */
94
+ export declare const TODO_BLOCK_VERSION = "v2-2026-10";
95
+ /**
96
+ * ACCEPT_LIMITS - the structural bounds of the acceptance table
97
+ * (todo-acceptance-criteria §6.1). STRUCTURAL, NOT POLICY, exactly like
98
+ * `TODO_LIMITS`: what a rail row, a transcript row and a model payload can
99
+ * physically carry.
90
100
  */
91
- export declare const TODO_BLOCK_VERSION = "v1-2026-07";
101
+ export declare const ACCEPT_LIMITS: {
102
+ /**
103
+ * TWELVE, DELIBERATELY FEWER THAN `TODO_LIMITS.maxItems` (20). A plan with more
104
+ * than a dozen criteria is not a plan, it is a test suite - and `check` is where
105
+ * a suite belongs ("npm test"). The cap is semantic pressure: the model either
106
+ * merges criteria or admits it wrote a suite.
107
+ */
108
+ readonly maxItems: 12;
109
+ /** `AcceptanceItem.content`, clamped (never rejected). */
110
+ readonly contentChars: 100;
111
+ /** `AcceptanceItem.check`, clamped. May be empty: a weak criterion, not an error. */
112
+ readonly checkChars: 120;
113
+ /** `AcceptanceItem.evidence`, clamped. */
114
+ readonly evidenceChars: 200;
115
+ /**
116
+ * A FRESH plan must declare at least this many usable criteria (§6.2.3 step 4).
117
+ * Gated on an EMPTY store AND on `todo.acceptance` being on: the requirement is
118
+ * "plan the criteria too", so it applies exactly where the capability does.
119
+ * Named `...Criteria`, not `...Items`, so it cannot be confused with
120
+ * `TODO_LIMITS.minFreshItems` (2) used two steps earlier.
121
+ */
122
+ readonly minFreshCriteria: 1;
123
+ };
124
+ /**
125
+ * TODO_RESULT_LIMITS - the bound on what `todo_write` returns each turn
126
+ * (I-11 / §6.4.4). The acceptance paragraph pushes the text from about 700
127
+ * characters to roughly 1100, and every character is paid for on every call, so
128
+ * the ceiling is structural rather than advisory.
129
+ */
130
+ export declare const TODO_RESULT_LIMITS: {
131
+ readonly maxChars: 1400;
132
+ /** Appended when the text had to be clipped, so a clipped answer says so. */
133
+ readonly truncationNote: "Note: result text truncated.";
134
+ };
135
+ /**
136
+ * VERIFY_MESSAGE_LIMITS - the CONSTRUCTIVE bound of `buildVerifyMessage`
137
+ * (§6.6.2 / AC-22), in the tradition of `buildChildDigest`: independent of how
138
+ * long the lists are, because every row it emits is clamped.
139
+ *
140
+ * The two numbers are ONE arithmetic, measured rather than estimated: at 5
141
+ * unchecked + 2 failed with `check` at 100 and `evidence` at 60 the worst case is
142
+ * 19 rows / 1699 characters, leaving 101 characters below `maxChars` and one row
143
+ * below `maxRows`.
144
+ */
145
+ export declare const VERIFY_MESSAGE_LIMITS: {
146
+ readonly uncheckedItems: 5;
147
+ readonly failedItems: 2;
148
+ readonly checkChars: 100;
149
+ readonly evidenceChars: 60;
150
+ readonly maxChars: 1800;
151
+ readonly maxRows: 20;
152
+ };
@@ -87,5 +87,66 @@ export const TODO_FOLLOW_LIMITS = {
87
87
  *
88
88
  * NOT BUMPED BY follow-through (D-3): the model is never told about the
89
89
  * mechanism, so the block is byte-identical to `bf6aeb1a`.
90
+ *
91
+ * BUMPED BY todo-acceptance-criteria (D-3 inverted): that feature CHANGES what
92
+ * the model reads, so the revision must be greppable from a behaviour report.
93
+ */
94
+ export const TODO_BLOCK_VERSION = 'v2-2026-10';
95
+ /**
96
+ * ACCEPT_LIMITS - the structural bounds of the acceptance table
97
+ * (todo-acceptance-criteria §6.1). STRUCTURAL, NOT POLICY, exactly like
98
+ * `TODO_LIMITS`: what a rail row, a transcript row and a model payload can
99
+ * physically carry.
90
100
  */
91
- export const TODO_BLOCK_VERSION = 'v1-2026-07';
101
+ export const ACCEPT_LIMITS = {
102
+ /**
103
+ * TWELVE, DELIBERATELY FEWER THAN `TODO_LIMITS.maxItems` (20). A plan with more
104
+ * than a dozen criteria is not a plan, it is a test suite - and `check` is where
105
+ * a suite belongs ("npm test"). The cap is semantic pressure: the model either
106
+ * merges criteria or admits it wrote a suite.
107
+ */
108
+ maxItems: 12,
109
+ /** `AcceptanceItem.content`, clamped (never rejected). */
110
+ contentChars: 100,
111
+ /** `AcceptanceItem.check`, clamped. May be empty: a weak criterion, not an error. */
112
+ checkChars: 120,
113
+ /** `AcceptanceItem.evidence`, clamped. */
114
+ evidenceChars: 200,
115
+ /**
116
+ * A FRESH plan must declare at least this many usable criteria (§6.2.3 step 4).
117
+ * Gated on an EMPTY store AND on `todo.acceptance` being on: the requirement is
118
+ * "plan the criteria too", so it applies exactly where the capability does.
119
+ * Named `...Criteria`, not `...Items`, so it cannot be confused with
120
+ * `TODO_LIMITS.minFreshItems` (2) used two steps earlier.
121
+ */
122
+ minFreshCriteria: 1,
123
+ };
124
+ /**
125
+ * TODO_RESULT_LIMITS - the bound on what `todo_write` returns each turn
126
+ * (I-11 / §6.4.4). The acceptance paragraph pushes the text from about 700
127
+ * characters to roughly 1100, and every character is paid for on every call, so
128
+ * the ceiling is structural rather than advisory.
129
+ */
130
+ export const TODO_RESULT_LIMITS = {
131
+ maxChars: 1400,
132
+ /** Appended when the text had to be clipped, so a clipped answer says so. */
133
+ truncationNote: 'Note: result text truncated.',
134
+ };
135
+ /**
136
+ * VERIFY_MESSAGE_LIMITS - the CONSTRUCTIVE bound of `buildVerifyMessage`
137
+ * (§6.6.2 / AC-22), in the tradition of `buildChildDigest`: independent of how
138
+ * long the lists are, because every row it emits is clamped.
139
+ *
140
+ * The two numbers are ONE arithmetic, measured rather than estimated: at 5
141
+ * unchecked + 2 failed with `check` at 100 and `evidence` at 60 the worst case is
142
+ * 19 rows / 1699 characters, leaving 101 characters below `maxChars` and one row
143
+ * below `maxRows`.
144
+ */
145
+ export const VERIFY_MESSAGE_LIMITS = {
146
+ uncheckedItems: 5,
147
+ failedItems: 2,
148
+ checkChars: 100,
149
+ evidenceChars: 60,
150
+ maxChars: 1800,
151
+ maxRows: 20,
152
+ };
@@ -1,55 +1,45 @@
1
1
  /**
2
- * `selectTodoRows` — which items the rail shows (todo-plan-execution §3.10).
2
+ * Which rows the rail shows (todo-plan-execution §3.10).
3
3
  *
4
4
  * ASCII ONLY: `src/todo/**` is inside the glyph scanner's scope.
5
5
  *
6
- * PURE WINDOWING, NO OFFSET STATE ANYWHERE (non-goal 2 / R-10). Ink has no
7
- * absolute positioning and this codebase already owns one hand-rolled viewport
8
- * (`ScrollViewport`) whose invariants took a whole spec to pin down; a second
9
- * scrollable region with its own offset, its own intent nonce and its own wheel
10
- * band would be the largest source of new bugs in this feature by a wide margin.
11
- * The rail therefore never scrolls - it windows around the item being worked on
12
- * and reports what is off each end.
13
- *
14
- * THE DIRECT DESCENDANT OF `selectPanelRows`'s F-2 LESSON: the team panel
15
- * originally took the first five children by ARRAY INDEX, which showed the five
16
- * that finished FIRST and hid everything still working. Here the equivalent
17
- * mistake is `slice(0, maxRows)`, which on a 20-item plan shows steps 1-8 - all
18
- * completed - and hides the one the user is actually watching.
6
+ * THE ALGORITHM LIVES IN `row-window.ts` since todo-acceptance-criteria §6.9.4:
7
+ * the acceptance section needs the same anchored-window fixpoint over a different
8
+ * item shape, and two copies of it would be two places for the "anchor must stay
9
+ * visible" rule to rot. The three exports below keep their EXACT signatures and
10
+ * their field-for-field output (AC-13), so every existing caller and test is
11
+ * unaffected.
19
12
  */
20
- import type { TodoItem } from './types.js';
21
- export interface TodoRowSelection {
22
- visible: {
23
- item: TodoItem;
24
- index: number;
25
- }[];
26
- hiddenAbove: number;
27
- hiddenBelow: number;
28
- }
13
+ import type { AcceptanceItem, TodoItem } from './types.js';
14
+ import { type RowWindow } from './row-window.js';
15
+ export type TodoRowSelection = RowWindow<TodoItem>;
16
+ export type AcceptanceRowSelection = RowWindow<AcceptanceItem>;
29
17
  /**
30
- * The row the window is built around: the item in progress, or the first
18
+ * The row the todo window is built around: the item in progress, or the first
31
19
  * unfinished one, or the last item when everything is done.
32
20
  *
33
21
  * `normalizeTodos` guarantees an `in_progress` item whenever one is not
34
- * completed (I-3), so the second branch is a belt-and-braces path for a list
35
- * this module was handed without going through the store.
22
+ * completed (I-3), so the second branch is a belt-and-braces path for a list this
23
+ * module was handed without going through the store.
24
+ */
25
+ export declare function todoAnchorIndex(items: readonly TodoItem[]): number;
26
+ /**
27
+ * The row the acceptance window is built around (§6.9.4): the criterion being
28
+ * checked, or the first unchecked one, or the last row when every verdict is in.
29
+ *
30
+ * THE `in_progress` BRANCH IS THE WHOLE POINT. Without it the anchor would be
31
+ * "first pending", so re-sending the list while checking criterion 3 would snap
32
+ * the rail back to criterion 1 - the F-2 lesson again, one feature later.
36
33
  */
37
- export declare function todoAnchorIndex(items: TodoItem[]): number;
34
+ export declare function acceptanceAnchorIndex(items: readonly AcceptanceItem[]): number;
38
35
  /**
39
36
  * @param maxRows Rows available for the item rows AND the two overflow markers
40
37
  * together. The shared layout subtracts actual team/menu rows.
41
- *
42
- * THE ANCHOR IS ALWAYS VISIBLE, and that beats the row bound when the two
43
- * conflict. They can only conflict below `maxRows` 3 with overflow on BOTH
44
- * sides — one item plus two markers is three rows — where this returns the
45
- * anchor and lets the caller's `overflow="hidden"` clip a marker. Hiding the
46
- * one row the user is watching in order to keep a `+N above` would be the wrong
47
- * trade. The full rail has at least four item rows. The compact rail uses
48
- * `selectCompactTodoRows` with a single merged footer instead.
49
38
  */
50
39
  export declare function selectTodoRows(items: TodoItem[], maxRows: number): TodoRowSelection;
51
- /** Select an anchored contiguous window without reserving overflow-marker rows.
52
- * maxItemRows is a row count; invalid/nonpositive values return an empty window
53
- * with every item counted below it. Does not mutate the list or throw.
54
- */
40
+ /** Select an anchored contiguous window without reserving overflow-marker rows. */
55
41
  export declare function selectCompactTodoRows(items: TodoItem[], maxItemRows: number): TodoRowSelection;
42
+ /** The acceptance section's window; same discipline, same anchors, other shape. */
43
+ export declare function selectAcceptanceRows(items: AcceptanceItem[], maxRows: number): AcceptanceRowSelection;
44
+ /** The acceptance section's compact window (summary rung uses this). */
45
+ export declare function selectCompactAcceptanceRows(items: AcceptanceItem[], maxRows: number): AcceptanceRowSelection;
@@ -1,29 +1,23 @@
1
1
  /**
2
- * `selectTodoRows` — which items the rail shows (todo-plan-execution §3.10).
2
+ * Which rows the rail shows (todo-plan-execution §3.10).
3
3
  *
4
4
  * ASCII ONLY: `src/todo/**` is inside the glyph scanner's scope.
5
5
  *
6
- * PURE WINDOWING, NO OFFSET STATE ANYWHERE (non-goal 2 / R-10). Ink has no
7
- * absolute positioning and this codebase already owns one hand-rolled viewport
8
- * (`ScrollViewport`) whose invariants took a whole spec to pin down; a second
9
- * scrollable region with its own offset, its own intent nonce and its own wheel
10
- * band would be the largest source of new bugs in this feature by a wide margin.
11
- * The rail therefore never scrolls - it windows around the item being worked on
12
- * and reports what is off each end.
13
- *
14
- * THE DIRECT DESCENDANT OF `selectPanelRows`'s F-2 LESSON: the team panel
15
- * originally took the first five children by ARRAY INDEX, which showed the five
16
- * that finished FIRST and hid everything still working. Here the equivalent
17
- * mistake is `slice(0, maxRows)`, which on a 20-item plan shows steps 1-8 - all
18
- * completed - and hides the one the user is actually watching.
6
+ * THE ALGORITHM LIVES IN `row-window.ts` since todo-acceptance-criteria §6.9.4:
7
+ * the acceptance section needs the same anchored-window fixpoint over a different
8
+ * item shape, and two copies of it would be two places for the "anchor must stay
9
+ * visible" rule to rot. The three exports below keep their EXACT signatures and
10
+ * their field-for-field output (AC-13), so every existing caller and test is
11
+ * unaffected.
19
12
  */
13
+ import { selectCompactRowsAround, selectRowsAround } from './row-window.js';
20
14
  /**
21
- * The row the window is built around: the item in progress, or the first
15
+ * The row the todo window is built around: the item in progress, or the first
22
16
  * unfinished one, or the last item when everything is done.
23
17
  *
24
18
  * `normalizeTodos` guarantees an `in_progress` item whenever one is not
25
- * completed (I-3), so the second branch is a belt-and-braces path for a list
26
- * this module was handed without going through the store.
19
+ * completed (I-3), so the second branch is a belt-and-braces path for a list this
20
+ * module was handed without going through the store.
27
21
  */
28
22
  export function todoAnchorIndex(items) {
29
23
  if (items.length === 0)
@@ -37,80 +31,40 @@ export function todoAnchorIndex(items) {
37
31
  return items.length - 1;
38
32
  }
39
33
  /**
40
- * How much of the window sits ABOVE the anchor.
34
+ * The row the acceptance window is built around (§6.9.4): the criterion being
35
+ * checked, or the first unchecked one, or the last row when every verdict is in.
41
36
  *
42
- * A QUARTER, and the asymmetry is the point: completed context above the anchor
43
- * is sacrificed before upcoming work below it, because the user's question is
44
- * "what is next", not "what is behind me".
37
+ * THE `in_progress` BRANCH IS THE WHOLE POINT. Without it the anchor would be
38
+ * "first pending", so re-sending the list while checking criterion 3 would snap
39
+ * the rail back to criterion 1 - the F-2 lesson again, one feature later.
45
40
  */
46
- const ABOVE_SHARE = 0.25;
47
- function windowFor(count, anchor, slots) {
48
- if (slots >= count)
49
- return { start: 0, end: count };
50
- const above = Math.min(anchor, Math.max(0, Math.floor((slots - 1) * ABOVE_SHARE)));
51
- let start = anchor - above;
52
- let end = start + slots;
53
- if (end > count) {
54
- end = count;
55
- start = Math.max(0, end - slots);
56
- }
57
- return { start, end };
41
+ export function acceptanceAnchorIndex(items) {
42
+ if (items.length === 0)
43
+ return 0;
44
+ const active = items.findIndex((item) => item.status === 'in_progress');
45
+ if (active >= 0)
46
+ return active;
47
+ const unchecked = items.findIndex((item) => item.status === 'pending' || item.status === 'in_progress');
48
+ if (unchecked >= 0)
49
+ return unchecked;
50
+ return items.length - 1;
58
51
  }
59
52
  /**
60
53
  * @param maxRows Rows available for the item rows AND the two overflow markers
61
54
  * together. The shared layout subtracts actual team/menu rows.
62
- *
63
- * THE ANCHOR IS ALWAYS VISIBLE, and that beats the row bound when the two
64
- * conflict. They can only conflict below `maxRows` 3 with overflow on BOTH
65
- * sides — one item plus two markers is three rows — where this returns the
66
- * anchor and lets the caller's `overflow="hidden"` clip a marker. Hiding the
67
- * one row the user is watching in order to keep a `+N above` would be the wrong
68
- * trade. The full rail has at least four item rows. The compact rail uses
69
- * `selectCompactTodoRows` with a single merged footer instead.
70
55
  */
71
56
  export function selectTodoRows(items, maxRows) {
72
- const count = items.length;
73
- const anchor = todoAnchorIndex(items);
74
- if (count === 0)
75
- return { visible: [], hiddenAbove: 0, hiddenBelow: 0 };
76
- if (maxRows <= 0) {
77
- return { visible: [], hiddenAbove: anchor, hiddenBelow: count - 1 - anchor };
78
- }
79
- // The marker rows and the window depend on each other: a `+N above` row costs
80
- // a slot, which can shrink the window, which can create a `+N below`. Two
81
- // passes reach the fixpoint for every input this can be handed; the loop bound
82
- // is a backstop, not a live case.
83
- let reserved = 0;
84
- let start = 0;
85
- let end = count;
86
- for (let pass = 0; pass < 3; pass += 1) {
87
- const slots = Math.max(1, maxRows - reserved);
88
- const win = windowFor(count, anchor, slots);
89
- start = win.start;
90
- end = win.end;
91
- const need = (start > 0 ? 1 : 0) + (end < count ? 1 : 0);
92
- if (need === reserved)
93
- break;
94
- reserved = need;
95
- }
96
- const visible = [];
97
- for (let i = start; i < end; i += 1)
98
- visible.push({ item: items[i], index: i });
99
- return { visible, hiddenAbove: start, hiddenBelow: count - end };
57
+ return selectRowsAround(items, maxRows, todoAnchorIndex);
100
58
  }
101
- /** Select an anchored contiguous window without reserving overflow-marker rows.
102
- * maxItemRows is a row count; invalid/nonpositive values return an empty window
103
- * with every item counted below it. Does not mutate the list or throw.
104
- */
59
+ /** Select an anchored contiguous window without reserving overflow-marker rows. */
105
60
  export function selectCompactTodoRows(items, maxItemRows) {
106
- const slots = Number.isFinite(maxItemRows) ? Math.max(0, Math.floor(maxItemRows)) : 0;
107
- if (slots === 0 || items.length === 0) {
108
- return { visible: [], hiddenAbove: 0, hiddenBelow: items.length };
109
- }
110
- const { start, end } = windowFor(items.length, todoAnchorIndex(items), slots);
111
- return {
112
- visible: items.slice(start, end).map((item, offset) => ({ item, index: start + offset })),
113
- hiddenAbove: start,
114
- hiddenBelow: items.length - end,
115
- };
61
+ return selectCompactRowsAround(items, maxItemRows, todoAnchorIndex);
62
+ }
63
+ /** The acceptance section's window; same discipline, same anchors, other shape. */
64
+ export function selectAcceptanceRows(items, maxRows) {
65
+ return selectRowsAround(items, maxRows, acceptanceAnchorIndex);
66
+ }
67
+ /** The acceptance section's compact window (summary rung uses this). */
68
+ export function selectCompactAcceptanceRows(items, maxRows) {
69
+ return selectCompactRowsAround(items, maxRows, acceptanceAnchorIndex);
116
70
  }