@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,19 +1,22 @@
1
1
  /**
2
- * `todo_write` — the ONLY writer of `TodoStore` (todo-plan-execution §3.3 /
3
- * §4.1).
2
+ * `todo_write` — the ONLY writer of `TodoStore` (todo-plan-execution §3.3 / §4.1;
3
+ * todo-acceptance-criteria §6.4).
4
4
  *
5
5
  * ASCII ONLY: `src/todo/**` is inside the glyph scanner's scope.
6
6
  *
7
7
  * FULL-LIST REPLACEMENT, NO INCREMENTAL OPERATIONS (D-1). A delta protocol
8
8
  * (`todo_add` / `todo_complete`) lets the model's belief and the rendered panel
9
- * drift apart with nothing anywhere able to notice, and needs an id the model
10
- * must keep stable across calls - a field it will eventually get wrong. A full
11
- * replacement is idempotent, order-free and self-healing.
9
+ * drift apart with nothing anywhere able to notice, and needs an id the model must
10
+ * keep stable across calls - a field it will eventually get wrong. A full
11
+ * replacement is idempotent, order-free and self-healing. The acceptance table
12
+ * rides in the SAME call for exactly that reason: a second tool would be a second
13
+ * writer, with two refusal texts and a model free to update only half its plan.
12
14
  *
13
15
  * THE SCHEMA DECLARES SHAPE, NOT POLICY (C-8): no `enum`, no `maxItems`, no
14
- * `minItems`, and `activeForm` is not `required`. `ajv` is an OPTIONAL
15
- * dependency of core, so any of those would reject a payload where it happens to
16
- * be installed and repair the identical payload where it is not.
16
+ * `minItems`, and `acceptance` is not `required`. `ajv` is an OPTIONAL dependency
17
+ * of core, so any of those would reject a payload where it happens to be installed
18
+ * and repair the identical payload where it is not. Every bound lives in the
19
+ * `description` (what the model reads) and in `write()` (what is tested).
17
20
  *
18
21
  * PURE STATE: no filesystem, no network, no child process, no human wait. It
19
22
  * therefore must NOT be routed through `withPausedWatchdog` (C-9) -
@@ -22,7 +25,8 @@
22
25
  * human wait.
23
26
  */
24
27
  import { errorResult, textResult } from '@aragon-agent/core';
25
- import { TODO_LIMITS } from './limits.js';
28
+ import { acceptanceSummary, countUncheckedCriteria, countUnfinishedSteps, derivePlanPhase, planOutstanding, } from './acceptance.js';
29
+ import { ACCEPT_LIMITS, TODO_LIMITS, TODO_RESULT_LIMITS } from './limits.js';
26
30
  /**
27
31
  * Turned off mid-session. A NON-ERROR result on purpose: the user turned a
28
32
  * display off, the model did nothing wrong, and an error result invites a retry.
@@ -42,11 +46,58 @@ const TODO_WRITE_DESCRIPTION = 'Record and update the plan for a multi-step task
42
46
  '`in_progress`; mark an item `completed` and the next one `in_progress` in the same call, ' +
43
47
  `the moment the step is done. At most ${TODO_LIMITS.maxItems} items; extras are dropped, so ` +
44
48
  'keep within it. This list is how the user follows a long task, so keep it current.';
49
+ /**
50
+ * Appended only while the capability is on, so `todo.acceptance: false` leaves the
51
+ * whole parameters object byte-identical to the version without this feature
52
+ * (AC-29 / I-5).
53
+ */
54
+ const ACCEPTANCE_DESCRIPTION = ' A plan also carries acceptance criteria: how you will know each part is really done. ' +
55
+ 'Send them in the same call. When every step is completed, check the criteria ONE at a ' +
56
+ 'time, actually run the check you named, and record each verdict with its evidence. A ' +
57
+ 'verdict without evidence is not a verdict. Report failures honestly: fix the work, ' +
58
+ 'reopen the criterion, check it again.';
59
+ /** The `acceptance` JSON Schema property (§6.4.1). Shape only - no enum, no bounds. */
60
+ const ACCEPTANCE_PARAMETER = {
61
+ type: 'array',
62
+ description: 'How you will KNOW the work is done: one checkable criterion per line. Send the ' +
63
+ 'complete list every time, same as todos.',
64
+ items: {
65
+ type: 'object',
66
+ properties: {
67
+ content: {
68
+ type: 'string',
69
+ description: `One checkable statement, <= ${ACCEPT_LIMITS.contentChars} chars. Not a wish: ` +
70
+ 'something you can point at and say yes or no.',
71
+ },
72
+ check: {
73
+ type: 'string',
74
+ description: `How you will check it, <= ${ACCEPT_LIMITS.checkChars} chars. ` +
75
+ "e.g. 'npm test -- runs green'.",
76
+ },
77
+ status: {
78
+ type: 'string',
79
+ description: 'pending | passed | failed. Only after you actually checked it.',
80
+ },
81
+ evidence: {
82
+ type: 'string',
83
+ description: `What you saw, <= ${ACCEPT_LIMITS.evidenceChars} chars. Required for passed or ` +
84
+ 'failed; a verdict without evidence does not count.',
85
+ },
86
+ reopen: {
87
+ type: 'boolean',
88
+ description: 'true only to undo an earlier verdict and check the same criterion again.',
89
+ },
90
+ },
91
+ required: ['content'],
92
+ },
93
+ };
45
94
  export function createTodoTool(deps) {
46
95
  return {
47
96
  name: 'todo_write',
48
97
  label: 'Update todos',
49
- description: TODO_WRITE_DESCRIPTION,
98
+ description: deps.hasAcceptance()
99
+ ? TODO_WRITE_DESCRIPTION + ACCEPTANCE_DESCRIPTION
100
+ : TODO_WRITE_DESCRIPTION,
50
101
  parameters: {
51
102
  type: 'object',
52
103
  properties: {
@@ -75,49 +126,71 @@ export function createTodoTool(deps) {
75
126
  required: ['content', 'status'],
76
127
  },
77
128
  },
129
+ // ABSENT ENTIRELY WHILE THE CAPABILITY IS OFF, so the model is not told
130
+ // about a field that would be ignored (I-5 / AC-29).
131
+ ...(deps.hasAcceptance() ? { acceptance: ACCEPTANCE_PARAMETER } : {}),
78
132
  },
133
+ // `acceptance` IS NOT REQUIRED, and cannot be: `ajv` is optional in core, so
134
+ // a schema-level requirement would be enforced in some installs and not in
135
+ // others (C-8). The gate lives in `write()` (AC-11).
79
136
  required: ['todos'],
80
137
  },
81
138
  async execute(_id, params) {
82
- // 1. Turned off mid-session. The tool array is immutable for the life of
83
- // the session (C-1), so `/todo off` cannot unregister - it flips this
84
- // closure instead.
139
+ // 1. Turned off mid-session. The tool array is immutable for the life of the
140
+ // session (C-1), so `/todo off` cannot unregister - it flips this closure.
85
141
  if (!deps.isEnabled()) {
86
142
  deps.store.reject(TODO_OFF_REFUSAL);
87
143
  return textResult(TODO_OFF_REFUSAL);
88
144
  }
89
- const todos = params.todos;
90
- // 2. GATED ON AN EMPTY STORE, and that gate is the whole subtlety.
91
- // Shrinking an EXISTING list to one item is legitimate - the model
92
- // merged two steps - so the guard must fire only on a fresh plan.
145
+ const payload = params;
146
+ const todos = payload.todos;
147
+ // 2. GATED ON AN EMPTY STORE, and that gate is the whole subtlety. Shrinking
148
+ // an EXISTING list to one item is legitimate - the model merged two steps
149
+ // - so the guard must fire only on a fresh plan.
93
150
  if (deps.store.isEmpty() &&
94
151
  Array.isArray(todos) &&
95
152
  todos.length < TODO_LIMITS.minFreshItems) {
96
153
  deps.store.reject(TOO_SMALL_REFUSAL);
97
154
  return textResult(TOO_SMALL_REFUSAL);
98
155
  }
99
- // 3. Repair, never reject. Zero survivors is the single hard failure, and
100
- // it leaves the previous list untouched (AC-9).
101
- const result = deps.store.write(todos);
102
- if (result.items.length === 0) {
103
- return errorResult('No usable todo items: each needs a non-empty content string.');
156
+ // 3. Repair, never reject. A refusal leaves BOTH tables untouched (I-4), and
157
+ // the kind decides the result shape: a broken payload is an error (today's
158
+ // behaviour, unchanged), an incomplete plan is not - an error would invite
159
+ // the model to retry the identical payload.
160
+ const result = deps.store.write(params);
161
+ if (!result.accepted) {
162
+ const refusal = result.refusal
163
+ ?? 'No usable todo items: each needs a non-empty content string.';
164
+ return result.refusalKind === 'policy' ? textResult(refusal) : errorResult(refusal);
104
165
  }
105
- return textResult(buildResultText(result.items, result.repairs));
166
+ return textResult(buildResultText(result.items, result.acceptance, result.repairs, deps.hasAcceptance()));
106
167
  },
107
168
  };
108
169
  }
109
170
  /**
110
171
  * The tool result — THE SINGLE STRONGEST LEVER ON R-b ("one at a time"), because
111
172
  * unlike the system prompt it is re-read on every single call. There is
112
- * deliberately NO RATE LIMIT to compete with it: a cap would fire exactly when
113
- * the model is being most diligent and produce a refusal it cannot act on, and
114
- * the costs a cap would have contained are already gone (§3.8 collapses N calls
115
- * into ONE transcript entry, and the store is memory-only).
173
+ * deliberately NO RATE LIMIT to compete with it: a cap would fire exactly when the
174
+ * model is being most diligent and produce a refusal it cannot act on, and the
175
+ * costs a cap would have contained are already gone (§3.8 collapses N calls into
176
+ * ONE transcript entry, and the store is memory-only).
177
+ *
178
+ * FOUR PHASES, and the acceptance paragraph is inserted after the "In progress /
179
+ * Every item" paragraph and before any `Note:`. In `verify` and in the two `done`
180
+ * cases it also REPLACES that paragraph's closing sentence, because a plan that
181
+ * carries criteria has something more specific to say about being finished. With the
182
+ * capability off, or with no criteria, every branch below is byte-identical to the
183
+ * version without this feature (AC-28).
116
184
  */
117
- function buildResultText(items, repairs) {
185
+ export function buildResultText(items, acceptance, repairs, acceptanceEnabled) {
118
186
  const done = items.filter((item) => item.status === 'completed').length;
119
187
  const activeIndex = items.findIndex((item) => item.status === 'in_progress');
188
+ const phase = derivePlanPhase(items, acceptance);
120
189
  const lines = [`Todos updated. ${done}/${items.length} done.`];
190
+ // Only while the capability is on AND there is a table: those are exactly the cases
191
+ // in which `acceptanceParagraph` speaks for the finished plan, and exactly the cases
192
+ // in which `I-5` does not demand the pre-feature bytes.
193
+ const showsCriteria = acceptanceEnabled && acceptance.length > 0;
121
194
  if (activeIndex >= 0) {
122
195
  lines.push(`In progress: ${items[activeIndex].activeForm}`);
123
196
  const next = items.slice(activeIndex + 1).find((item) => item.status === 'pending');
@@ -125,10 +198,93 @@ function buildResultText(items, repairs) {
125
198
  lines.push(`Next: ${next.content}`);
126
199
  lines.push('Call todo_write again the moment that item is finished - exactly one item in', 'progress at a time, and do not batch several completions into one call.');
127
200
  }
128
- else {
201
+ else if (showsCriteria && phase === 'verify') {
202
+ // REPLACES the paragraph rather than joining it (P1-1). Saying "Every item is
203
+ // complete. Tell the user what you did." and then "Check ONE criterion now" in
204
+ // the same result would end the turn on the first sentence.
205
+ lines.push('Every step is complete. Now verify the acceptance criteria.');
206
+ }
207
+ else if (!(showsCriteria && phase === 'done')) {
129
208
  lines.push('Every item is complete. Tell the user what you did.');
130
209
  }
210
+ if (showsCriteria) {
211
+ // THE SAME REPLACEMENT RULE AS THE `verify` BRANCH ABOVE, for both `done` cases
212
+ // (§6.4.4). Leaving the generic line in front of the paragraph would print "Tell
213
+ // the user what you did" immediately before "Do not describe the work as done"
214
+ // when a criterion failed - the one sentence this feature exists to say, cancelled
215
+ // by the line above it.
216
+ lines.push(...acceptanceParagraph(items, acceptance, phase));
217
+ }
131
218
  for (const repair of repairs)
132
219
  lines.push(`Note: ${repair}`);
133
- return lines.join('\n');
220
+ return boundText(lines);
221
+ }
222
+ /** The phase-specific acceptance paragraph (§6.4.4), verbatim. */
223
+ function acceptanceParagraph(items, acceptance, phase) {
224
+ // `acceptanceSummary` with a DEAD clock, not a second copy of the counting: the
225
+ // paragraph and the rail must agree about `checked`/`failed`, and two loops over
226
+ // the same table is exactly how they stop agreeing.
227
+ const snapshotLike = acceptanceSummary(acceptance, () => 0);
228
+ const outstanding = {
229
+ steps: countUnfinishedSteps(items),
230
+ checks: countUncheckedCriteria(acceptance),
231
+ };
232
+ if (phase === 'work') {
233
+ return [
234
+ `Acceptance: ${snapshotLike.checkedCount}/${snapshotLike.total} checked. ` +
235
+ 'Check them one at a time once every step is completed.',
236
+ ];
237
+ }
238
+ if (phase === 'done' && snapshotLike.failedCount === 0) {
239
+ return [
240
+ `Every step is complete and all ${snapshotLike.total} acceptance criteria passed. ` +
241
+ 'Tell the user what you did',
242
+ 'and how you checked it.',
243
+ ];
244
+ }
245
+ if (phase === 'done') {
246
+ return [
247
+ `Every step is complete but ${snapshotLike.failedCount} of ${snapshotLike.total} ` +
248
+ 'acceptance criteria FAILED. Do not describe the work as',
249
+ 'done: fix it, reopen the failed criterion (reopen: true), check it again - or tell the user',
250
+ 'plainly what is still broken.',
251
+ ];
252
+ }
253
+ const lines = [
254
+ `Acceptance: ${snapshotLike.checkedCount}/${snapshotLike.total} checked`
255
+ + (snapshotLike.failedCount > 0 ? `, ${snapshotLike.failedCount} failed.` : '.'),
256
+ ];
257
+ const open = planOutstanding(items, acceptance, false);
258
+ if (open.length > 0)
259
+ lines.push('Unchecked:', ...open.map((row) => ` ${row}`));
260
+ if (outstanding.checks > open.length) {
261
+ const hidden = outstanding.checks - open.length;
262
+ lines.push(` ... and ${hidden} more (send the complete list to see them all)`);
263
+ }
264
+ lines.push('Check ONE criterion now: actually run its check, then send the complete list with', 'that criterion passed or failed and the evidence you saw.');
265
+ return lines;
266
+ }
267
+ /**
268
+ * I-11's ceiling on the one text the model re-reads on every call.
269
+ *
270
+ * Truncation is by LINES, because a paragraph cut mid-sentence reads as a model
271
+ * error while a missing last line reads as brevity - and the note says which one
272
+ * happened.
273
+ */
274
+ function boundText(lines) {
275
+ const text = lines.join('\n');
276
+ if (text.length <= TODO_RESULT_LIMITS.maxChars)
277
+ return text;
278
+ const kept = [];
279
+ let used = 0;
280
+ const budget = TODO_RESULT_LIMITS.maxChars - TODO_RESULT_LIMITS.truncationNote.length - 1;
281
+ for (const line of lines) {
282
+ const cost = line.length + (kept.length > 0 ? 1 : 0);
283
+ if (used + cost > budget)
284
+ break;
285
+ kept.push(line);
286
+ used += cost;
287
+ }
288
+ kept.push(TODO_RESULT_LIMITS.truncationNote);
289
+ return kept.join('\n');
134
290
  }
@@ -24,6 +24,53 @@ export interface TodoItem {
24
24
  activeForm: string;
25
25
  status: TodoStatus;
26
26
  }
27
+ /**
28
+ * The two settled states of a criterion, plus "not checked" and "being checked"
29
+ * (todo-acceptance-criteria §6.1). Mutually exclusive and exhaustive.
30
+ *
31
+ * `in_progress` IS A STORE-OWNED DERIVED STATE (v2, P0-1): the model cannot write
32
+ * it, the tool schema does not expose it, normalization always downgrades it to
33
+ * `pending`, and `TodoStore.write()` re-derives at most one after every commit.
34
+ * That is the same discipline `TodoItem`'s "exactly one in_progress" follows: one
35
+ * state has exactly one writer, otherwise the derived phase, the rail's anchor
36
+ * and the transcript card each read a different "criterion being checked".
37
+ */
38
+ export type AcceptanceStatus = 'pending' | 'in_progress' | 'passed' | 'failed';
39
+ export interface AcceptanceItem {
40
+ /** One checkable statement, sanitized, <= ACCEPT_LIMITS.contentChars. */
41
+ content: string;
42
+ /** How it will be checked (command / file / observation), <= checkChars. */
43
+ check: string;
44
+ status: AcceptanceStatus;
45
+ /** Required for `passed` / `failed`; otherwise the verdict drops to `pending`. */
46
+ evidence: string;
47
+ /**
48
+ * Explicit re-open. Only honoured when the same criterion's normalized
49
+ * `content` is unchanged (§6.2.1 step 9). INPUT ONLY: never stored, consumed
50
+ * and discarded by `write()`.
51
+ */
52
+ reopen?: boolean;
53
+ }
54
+ /** The immutable projection, isomorphic to `TodoSnapshot`. Rebuilt per commit. */
55
+ export interface AcceptanceSnapshot {
56
+ items: AcceptanceItem[];
57
+ total: number;
58
+ /** `passed` + `failed`. */
59
+ checkedCount: number;
60
+ passedCount: number;
61
+ failedCount: number;
62
+ /** Injected clock, for the same reason `TodoSnapshot.updatedAt` has one (P2-4). */
63
+ updatedAt: number;
64
+ }
65
+ /**
66
+ * `work` - at least one step is not completed.
67
+ * `verify` - every step is completed and at least one criterion is unresolved.
68
+ * `done` - every step is completed and every criterion is settled (or there is
69
+ * no acceptance table at all).
70
+ *
71
+ * DERIVED, NEVER STORED (I-3): `derivePlanPhase` is the only judge.
72
+ */
73
+ export type PlanPhase = 'work' | 'verify' | 'done';
27
74
  /** The immutable projection the UI renders. Rebuilt on every write. */
28
75
  export interface TodoSnapshot {
29
76
  items: TodoItem[];
@@ -38,6 +85,17 @@ export interface TodoSnapshot {
38
85
  * that fails on a slow machine and nowhere else (P2-4).
39
86
  */
40
87
  updatedAt: number;
88
+ /**
89
+ * The acceptance projection; `null` when there are no criteria (or the
90
+ * capability is off).
91
+ *
92
+ * REQUIRED, NOT OPTIONAL (fail-closed, §6.1): `TodoStore` is the only writer and
93
+ * it always knows what belongs here. Optional would let one call site silently
94
+ * under-report an entire table. The cost is the 15 existing test fixtures that
95
+ * spell out a `TodoSnapshot` literal (§7.3) - a mechanical edit, done in the
96
+ * same step as this field.
97
+ */
98
+ acceptance: AcceptanceSnapshot | null;
41
99
  }
42
100
  /**
43
101
  * Why a list went away.