@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
- * `TodoStore` — the session's todo list plus its CLI-local event stream
3
- * (todo-plan-execution §3.5).
2
+ * `TodoStore` — the session's todo list, its acceptance table, and its CLI-local
3
+ * event stream (todo-plan-execution §3.5; todo-acceptance-criteria §6.2.3).
4
4
  *
5
5
  * ASCII ONLY: `src/todo/**` is inside the glyph scanner's scope.
6
6
  *
@@ -9,30 +9,62 @@
9
9
  * is why `AgentController` can own this object as unconditionally as it owns
10
10
  * `TeamRuntime`.
11
11
  *
12
- * The list projects the current plan. Explicit clear/reset and a new TUI task
13
- * may remove that projection while the model still remembers the old plan.
14
- * Continuations preserve it; legacy callers retain completion/staleness rules.
12
+ * TWO TABLES, ONE WRITER, ONE COMMIT. `items` and `acceptance` are the plan's two
13
+ * halves: they are replaced together, cleared together, and refused together
14
+ * (I-4). Nothing here is a partial update, which is what makes I-9 ("the panel is
15
+ * never a contradiction of the model") provable rather than argued.
15
16
  *
16
- * Every write is a FULL REPLACEMENT and every clear is a FULL REMOVAL - there is
17
- * no partial update anywhere, which is what makes I-9 ("the panel is never a
18
- * contradiction of the model") provable rather than argued.
17
+ * `snapshot()` IS NOT A COMPUTATION. Every projection is rebuilt at the commit
18
+ * points (`write` / `restore` / `clear` / `reopenAll`, plus
19
+ * `refreshAcceptanceProjection` when `todo.acceptance` is flipped) and merely READ
20
+ * here, because `App` calls `snapshot()` on every event: re-deriving and
21
+ * re-stamping on read would give the reducer a new object every frame, invalidate
22
+ * `TodoCard`'s `React.memo` and the transcript's height cache, and undo the redraw
23
+ * savings `mapEntry` buys with object identity (P1-3).
19
24
  */
20
25
  import { getLogger } from '../logging/logger.js';
21
- import { TODO_LIMITS } from './limits.js';
26
+ import { acceptanceSummary, normalizeAcceptance, planIsResolved } from './acceptance.js';
27
+ import { ACCEPT_LIMITS, TODO_LIMITS } from './limits.js';
22
28
  import { normalizeTodos } from './normalize.js';
29
+ /** The one model-readable refusal that is about the PLAN, not about the payload. */
30
+ export const FRESH_ACCEPTANCE_REFUSAL = 'A plan needs acceptance criteria: how will you know the work is done? Send the ' +
31
+ 'complete list again with an acceptance array - one checkable statement per line, ' +
32
+ 'with the check you will run for it.';
33
+ const NO_USABLE_ITEMS = 'No usable todo items: each needs a non-empty content string.';
23
34
  export class TodoStore {
24
35
  now;
36
+ acceptanceEnabled;
25
37
  items = [];
38
+ acceptance = [];
26
39
  updatedAt = 0;
27
40
  /** Consecutive `beginUserTurn()` calls since the last `write()`. */
28
41
  turnsSinceWrite = 0;
42
+ /**
43
+ * The acceptance projection. REBUILT AT COMMIT TIME ONLY — see the file header.
44
+ * `null` when there are no criteria, which is also what keeps the rail's second
45
+ * section, the card's second block and the status chip unrendered.
46
+ */
47
+ acceptanceSnapshot = null;
48
+ /**
49
+ * Criteria whose verdict has already been reported as restored. Without it the
50
+ * model would receive the SAME `Note:` on every turn, because it cannot know the
51
+ * server restored anything and keeps re-sending the row as `pending` (P2-4).
52
+ * Cleared by `clear()` / `restore()`; a criterion's key is removed when the criterion
53
+ * itself leaves the table (not when it stops being `pending`, which would delete the
54
+ * key in the same commit that added it - see `pruneRestoredNotes`).
55
+ */
56
+ restoredNotes = new Set();
29
57
  listeners = new Set();
30
58
  /**
31
59
  * @param now Injected clock. Defaults to `Date.now`; a test passes a counter
32
60
  * so `TodoSnapshot.updatedAt` is not wall time (P2-4).
61
+ * @param acceptanceEnabled Live read of `todo.acceptance` (D-11). Read, never
62
+ * snapshotted: `/todo acceptance off` must take effect mid-session,
63
+ * because the tool array cannot change (C-1).
33
64
  */
34
- constructor(now = Date.now) {
65
+ constructor(now = Date.now, acceptanceEnabled = () => true) {
35
66
  this.now = now;
67
+ this.acceptanceEnabled = acceptanceEnabled;
36
68
  }
37
69
  /** `null` means no list, which is what leaves the rail unmounted entirely. */
38
70
  snapshot() {
@@ -44,80 +76,105 @@ export class TodoStore {
44
76
  doneCount: this.items.filter((item) => item.status === 'completed').length,
45
77
  activeIndex: this.items.findIndex((item) => item.status === 'in_progress'),
46
78
  updatedAt: this.updatedAt,
79
+ acceptance: this.acceptanceSnapshot,
47
80
  };
48
81
  }
82
+ /** The acceptance table itself, for the transcript entry and the session file. */
83
+ acceptanceItems() {
84
+ return this.acceptance;
85
+ }
49
86
  isEmpty() {
50
87
  return this.items.length === 0;
51
88
  }
52
89
  /**
53
90
  * Commit a model payload.
54
91
  *
55
- * ZERO SURVIVORS LEAVES THE PREVIOUS LIST UNTOUCHED (AC-9). A model sending
56
- * nothing usable is a BUG, and destroying a working plan because of one
57
- * malformed call is the worst available reaction; the caller turns the empty
58
- * result into an error the model can act on. Contrast `restore()`, where an
59
- * empty list is a FACT about a file and therefore means "clear".
92
+ * ZERO SURVIVORS LEAVES THE PREVIOUS PLAN UNTOUCHED (AC-9 / I-4), and so does an
93
+ * incomplete fresh plan: a model sending nothing usable is a BUG, and destroying
94
+ * a working plan because of one malformed call is the worst available reaction.
95
+ * The caller turns the refusal into something the model can act on. Contrast
96
+ * `restore()`, where an empty list is a FACT about a file and therefore means
97
+ * "clear".
60
98
  */
61
99
  write(raw) {
62
- const result = normalizeTodos(raw, TODO_LIMITS.maxItems);
63
- if (result.items.length === 0) {
64
- this.emit({
65
- type: 'rejected',
66
- reason: 'No usable todo items: each needs a non-empty content string.',
67
- });
68
- return result;
100
+ // TWO ACCEPTED SHAPES, and the second is what every caller that predates the
101
+ // acceptance table sends: a bare array of steps. `todo_write` passes the whole
102
+ // params OBJECT (so a plan and its criteria arrive together), while a plain
103
+ // `store.write(todos)` still means "here is the list". Reading both keeps the
104
+ // one existing contract that a full replacement is expressed by the payload
105
+ // itself, and it means the acceptance table simply stays empty for the array
106
+ // form rather than being cleared by it.
107
+ const payload = (Array.isArray(raw)
108
+ ? { todos: raw }
109
+ : (raw && typeof raw === 'object' ? raw : {}));
110
+ const todoResult = normalizeTodos(payload.todos, TODO_LIMITS.maxItems);
111
+ // 1. A broken payload: the existing refusal, byte for byte.
112
+ if (todoResult.items.length === 0) {
113
+ this.emit({ type: 'rejected', reason: NO_USABLE_ITEMS });
114
+ return this.refusal(NO_USABLE_ITEMS, 'payload');
69
115
  }
70
- this.items = result.items;
71
- this.updatedAt = this.now();
72
- this.turnsSinceWrite = 0;
73
- const snapshot = this.snapshot();
74
- // COUNTS ONLY, NEVER ITEM TEXT (D-19 / §3.14). It is user-task content, it
75
- // is unbounded in aggregate, and the counts answer every question a log can
76
- // answer here: did the list move, and was the payload repaired?
77
- //
78
- // SCOPE `'agent'`, NOT `'todo'`. §3.14 writes `'todo'`, but `LogScope` is a
79
- // CLOSED UNION in `logging/logger.ts` and that file is not in the change
80
- // plan; team mode faced the identical choice and files its CLI-local events
81
- // under `'agent'` (`install.ts::attachTeamEvents`). Following that precedent
82
- // keeps the diff focused and keeps a support reader filtering on one scope
83
- // for everything a turn did. See spec §15 IF-1.
84
- getLogger().debug('agent', 'todo_write', {
85
- total: snapshot?.total ?? 0,
86
- done: snapshot?.doneCount ?? 0,
87
- active: snapshot?.activeIndex ?? -1,
88
- repairs: result.repairs.length,
116
+ const enabled = this.acceptanceEnabled();
117
+ const accResult = normalizeAcceptance(payload.acceptance, this.acceptance, {
118
+ enabled,
119
+ restoredNotes: this.restoredNotes,
89
120
  });
90
- if (snapshot)
91
- this.emit({ type: 'updated', snapshot });
92
- return result;
121
+ // 2. A FRESH plan must declare criteria - but ONLY while the capability is on.
122
+ // `normalizeAcceptance` returns an empty table whenever it is off, so
123
+ // without that precondition every fresh plan in a `todo.acceptance: false`
124
+ // session would be refused: turning off a DISPLAY option would break the
125
+ // tool itself, and no log line would say why (P0-3 / AC-11b).
126
+ const fresh = this.items.length === 0;
127
+ if (enabled && fresh && accResult.items.length < ACCEPT_LIMITS.minFreshCriteria) {
128
+ this.emit({ type: 'rejected', reason: FRESH_ACCEPTANCE_REFUSAL });
129
+ return this.refusal(FRESH_ACCEPTANCE_REFUSAL, 'policy');
130
+ }
131
+ // BOTH TABLES' REPAIR NOTES, or the acceptance half is repaired SILENTLY. A
132
+ // verdict dropped for having no evidence, a criterion restored by omission, a
133
+ // table truncated at the ceiling: each is a change the model did not ask for and
134
+ // cannot see, and this result text is the only channel that can tell it. Returning
135
+ // `todoResult.repairs` alone would make D-3's repair loop ("send the evidence next
136
+ // time") unreachable in practice.
137
+ const repairs = [...todoResult.repairs, ...accResult.repairs];
138
+ this.commit(todoResult.items, accResult.items, repairs);
139
+ return {
140
+ accepted: true,
141
+ items: this.items,
142
+ acceptance: this.acceptance,
143
+ repairs,
144
+ };
93
145
  }
94
146
  /**
95
- * `/resume`. Runs through `normalizeTodos` — a session file is user-editable
147
+ * `/resume`. Runs through both normalizers — a session file is user-editable
96
148
  * input and gets a model payload's treatment.
97
149
  *
98
- * AN EMPTY (OR ALL-DROPPED) ARRAY CLEARS; IT DOES NOT NO-OP (P0-2 / D-22).
150
+ * AN EMPTY (OR ALL-DROPPED) TODO ARRAY CLEARS; IT DOES NOT NO-OP (P0-2 / D-22).
99
151
  * `/resume` has just called `replaceMessages`, so the conversation the current
100
- * list belonged to is gone; leaving it on screen is I-2 INVERTED — a panel
101
- * that disagrees with the model, which §1.2 ranks below having no panel at
102
- * all. The two entry points share normalization and differ only here, which is
103
- * exactly the difference between "the model sent nothing" (a bug) and "the
104
- * file has nothing" (a fact).
152
+ * plan belonged to is gone; leaving it on screen is I-2 INVERTED — a panel that
153
+ * disagrees with the model, which §1.2 ranks below having no panel at all. The
154
+ * acceptance table goes with it: no plan, no criteria.
155
+ *
156
+ * `enabled: true` is passed to the normalizer on purpose: the file is a fact
157
+ * about what this plan contained, and the current switch only governs RENDERING
158
+ * and PROMPTING. A user who turns the capability off and back on must not find
159
+ * that a resume quietly deleted the stored verdicts.
105
160
  */
106
- restore(items) {
107
- const result = normalizeTodos(items, TODO_LIMITS.maxItems);
108
- if (result.items.length === 0) {
161
+ restore(items, acceptance = undefined) {
162
+ const todoResult = normalizeTodos(items, TODO_LIMITS.maxItems);
163
+ if (todoResult.items.length === 0) {
109
164
  this.clear('reset');
110
165
  return;
111
166
  }
112
- this.items = result.items;
113
- this.updatedAt = this.now();
114
- this.turnsSinceWrite = 0;
115
- const snapshot = this.snapshot();
116
- if (snapshot)
117
- this.emit({ type: 'updated', snapshot });
167
+ // `previous: []` and a fresh dedupe set: a resume is a brand new load, so the
168
+ // "a verdict is not undone by omission" rule has nothing to compare against.
169
+ this.restoredNotes.clear();
170
+ const accResult = normalizeAcceptance(acceptance, [], { enabled: true });
171
+ this.commit(todoResult.items, accResult.items, []);
118
172
  }
119
173
  clear(reason) {
120
174
  this.items = [];
175
+ this.acceptance = [];
176
+ this.acceptanceSnapshot = null;
177
+ this.restoredNotes.clear();
121
178
  this.updatedAt = this.now();
122
179
  this.turnsSinceWrite = 0;
123
180
  this.emit({ type: 'cleared', reason });
@@ -128,13 +185,14 @@ export class TodoStore {
128
185
  * Called from `AgentController.prompt()` and NOT from `steer()` — the same
129
186
  * asymmetry, for the same reason, that `askRounds = 0` already has: a steer is
130
187
  * a mid-turn interjection, not a new task. The stale counter inherits that
131
- * asymmetry for free, which is the behaviour you want: a steer must not age
132
- * out the plan it is steering.
188
+ * asymmetry for free, which is the behaviour you want: a steer must not age out
189
+ * the plan it is steering.
133
190
  *
134
- * A new TUI task drops the old plan here, while a continuation preserves it.
135
- * With no policy, legacy callers retain completed/stale-plan expiration.
136
- * Clearing at `agent_end` would erase the final progress before the user can
137
- * inspect it and remove the plan that automatic continuation still needs.
191
+ * "EVERY STEP IS COMPLETED" NO LONGER MEANS "THERE IS NOTHING LEFT" (G-4):
192
+ * the rule is `planIsResolved`, which also asks whether every criterion has a
193
+ * verdict. A plan that is done but unverified is not idle work, it is one step
194
+ * short - so it ages by `staleTurns` instead of vanishing the moment the model
195
+ * ticks its last step.
138
196
  */
139
197
  beginUserTurn(policy) {
140
198
  if (this.items.length === 0 || policy === 'continue')
@@ -143,20 +201,43 @@ export class TodoStore {
143
201
  this.clear('turn');
144
202
  return;
145
203
  }
146
- if (this.items.every((item) => item.status === 'completed')) {
204
+ if (planIsResolved(this.items, this.acceptance)) {
147
205
  this.clear('turn');
148
206
  return;
149
207
  }
150
- // ...but a partial list is not immortal (P1-7 / D-21). Without a bound, a
151
- // plan abandoned mid-way holds a fifth of the terminal for the rest of the
152
- // session, showing work nobody is doing - the "furniture when idle" R-g
153
- // rules out. Self-healing, because the model's next write is a full
154
- // replacement: the panel is ABSENT while the model still knows the plan,
155
- // which is the safe direction (I-9).
208
+ // ...but a partial list is not immortal (P1-7 / D-21). Without a bound, a plan
209
+ // abandoned mid-way holds a fifth of the terminal for the rest of the session,
210
+ // showing work nobody is doing - the "furniture when idle" R-g rules out.
211
+ // Self-healing, because the model's next write is a full replacement: the
212
+ // panel is ABSENT while the model still knows the plan, which is the safe
213
+ // direction (I-9).
156
214
  this.turnsSinceWrite += 1;
157
215
  if (this.turnsSinceWrite > TODO_LIMITS.staleTurns)
158
216
  this.clear('stale');
159
217
  }
218
+ /**
219
+ * Send every verdict back to `pending` and clear the evidence (§6.2.3).
220
+ *
221
+ * THE NAMED WRITER `/todo acceptance reset` NEEDS, so the command layer never
222
+ * walks the table itself. It is not a second writer of the plan: `items` stays
223
+ * exactly as it is, no `cleared` event is emitted (`TodoClearReason` therefore
224
+ * does not change), and only the verdict fields move - the same rule as
225
+ * `reopen: true`, entered from the user's side instead of the model's (AC-6).
226
+ *
227
+ * IDEMPOTENT: with nothing to re-open it returns 0 and emits NOTHING, so an
228
+ * empty command cannot make the transcript card blink for no reason.
229
+ */
230
+ reopenAll() {
231
+ const reopenable = this.acceptance.filter((item) => item.status !== 'pending');
232
+ if (reopenable.length === 0)
233
+ return 0;
234
+ this.acceptance = this.deriveInProgress(this.acceptance.map((item) => (item.status === 'pending' ? item : { ...item, status: 'pending', evidence: '' })));
235
+ for (const item of this.acceptance)
236
+ this.restoredNotes.delete(item.content);
237
+ this.commitAcceptance();
238
+ this.emit({ type: 'updated', snapshot: this.snapshot() });
239
+ return reopenable.length;
240
+ }
160
241
  /**
161
242
  * Report a refusal that never reached `write()` (§3.3 steps 1 and 2).
162
243
  *
@@ -173,6 +254,123 @@ export class TodoStore {
173
254
  this.listeners.delete(listener);
174
255
  };
175
256
  }
257
+ /** A refusal that changed nothing, expressed as a result the tool can branch on. */
258
+ refusal(refusal, refusalKind) {
259
+ return {
260
+ accepted: false,
261
+ items: this.items,
262
+ acceptance: this.acceptance,
263
+ repairs: [],
264
+ refusal,
265
+ refusalKind,
266
+ };
267
+ }
268
+ /** Both tables, both projections, one timestamp, one event. */
269
+ commit(items, acceptance, repairs) {
270
+ this.items = items;
271
+ this.acceptance = this.deriveInProgress(acceptance);
272
+ this.updatedAt = this.now();
273
+ this.turnsSinceWrite = 0;
274
+ this.commitAcceptance();
275
+ this.pruneRestoredNotes();
276
+ const snapshot = this.snapshot();
277
+ // COUNTS ONLY, NEVER ITEM TEXT (D-19 / §3.14). It is user-task content, it is
278
+ // unbounded in aggregate, and the counts answer every question a log can
279
+ // answer here: did the plan move, and was the payload repaired?
280
+ //
281
+ // SCOPE `'agent'`, NOT `'todo'`: `LogScope` is a CLOSED UNION in
282
+ // `logging/logger.ts` and that file is not in the change plan; team mode faced
283
+ // the identical choice and files its CLI-local events under `'agent'`
284
+ // (`install.ts::attachTeamEvents`). See todo-plan-execution §15 IF-1.
285
+ getLogger().debug('agent', 'todo_write', {
286
+ total: snapshot?.total ?? 0,
287
+ done: snapshot?.doneCount ?? 0,
288
+ active: snapshot?.activeIndex ?? -1,
289
+ repairs: repairs.length,
290
+ accTotal: this.acceptance.length,
291
+ accChecked: this.acceptanceSnapshot?.checkedCount ?? 0,
292
+ accFailed: this.acceptanceSnapshot?.failedCount ?? 0,
293
+ });
294
+ if (snapshot)
295
+ this.emit({ type: 'updated', snapshot });
296
+ }
297
+ /**
298
+ * THE ONLY PRODUCER OF `in_progress` (P0-1 / AC-43).
299
+ *
300
+ * Normalization only ever DOWNGRADES that state; rendering and phase derivation
301
+ * only READ it. One state, one writer - otherwise the rail's anchor and the
302
+ * derived phase would each pick a different "criterion being checked".
303
+ *
304
+ * Forward only: criteria are promoted once EVERY step is completed, because a
305
+ * criterion cannot be under inspection while its steps are still running.
306
+ */
307
+ deriveInProgress(acceptance) {
308
+ const cleared = acceptance.some((item) => item.status === 'in_progress')
309
+ ? acceptance.map((item) => (item.status === 'in_progress' ? { ...item, status: 'pending' } : item))
310
+ : acceptance;
311
+ const allStepsDone = this.items.length > 0
312
+ && this.items.every((item) => item.status === 'completed');
313
+ if (!allStepsDone)
314
+ return cleared;
315
+ const firstPending = cleared.findIndex((item) => item.status === 'pending');
316
+ if (firstPending < 0)
317
+ return cleared;
318
+ const next = [...cleared];
319
+ next[firstPending] = { ...next[firstPending], status: 'in_progress' };
320
+ return next;
321
+ }
322
+ /**
323
+ * Re-project the acceptance table after `todo.acceptance` was flipped mid-session.
324
+ *
325
+ * THE SWITCH GOVERNS RENDERING, and the rail, the card and the status chip all read
326
+ * `snapshot().acceptance` rather than the flag - so without this, `/todo acceptance
327
+ * off` on a plan that already had criteria would leave the second section on screen
328
+ * until the model's next write, which is exactly the complaint P0-2 raised one path
329
+ * further out. The DATA is kept either way: turning the capability back on restores
330
+ * the same verdicts without a round trip through the model.
331
+ *
332
+ * Emits only when the projection's VISIBILITY actually changed, so a no-op flip can
333
+ * never make the transcript card blink.
334
+ */
335
+ refreshAcceptanceProjection() {
336
+ const shouldShow = this.acceptance.length > 0 && this.acceptanceEnabled();
337
+ if (shouldShow === (this.acceptanceSnapshot !== null))
338
+ return false;
339
+ this.commitAcceptance();
340
+ const snapshot = this.snapshot();
341
+ if (snapshot)
342
+ this.emit({ type: 'updated', snapshot });
343
+ return true;
344
+ }
345
+ /** Rebuild the acceptance projection. The ONLY place `acceptanceSummary` runs. */
346
+ commitAcceptance() {
347
+ // `acceptanceEnabled()` HERE TOO, not only on write: a resume rehydrates a file
348
+ // whose verdicts must not be lost, but a capability that is off must not draw
349
+ // them either. Data is a fact; the projection is what the user sees.
350
+ this.acceptanceSnapshot = this.acceptance.length > 0 && this.acceptanceEnabled()
351
+ ? acceptanceSummary(this.acceptance, this.now)
352
+ : null;
353
+ }
354
+ /**
355
+ * Forget a restoration note once its criterion has LEFT the table.
356
+ *
357
+ * `filtered on pending` would be the wrong rule and was the first version of this:
358
+ * the criterion that `noteOnce` just announced is restored WITH ITS VERDICT
359
+ * (`passed` / `failed`), so a pending filter deletes the key in the same commit that
360
+ * added it, the model re-sends the row as `pending` on the next turn, and the note is
361
+ * re-printed every turn for the life of the plan - the unbounded billing P2-4 exists
362
+ * to prevent. Keeping the key until the criterion is gone is also free: the set can
363
+ * never hold more than `ACCEPT_LIMITS.maxItems` entries.
364
+ */
365
+ pruneRestoredNotes() {
366
+ if (this.restoredNotes.size === 0)
367
+ return;
368
+ const present = new Set(this.acceptance.map((item) => item.content));
369
+ for (const content of [...this.restoredNotes]) {
370
+ if (!present.has(content))
371
+ this.restoredNotes.delete(content);
372
+ }
373
+ }
176
374
  /** Per-listener isolation: one bad subscriber cannot fail a tool call. */
177
375
  emit(event) {
178
376
  for (const listener of [...this.listeners]) {
@@ -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) -
@@ -23,10 +26,16 @@
23
26
  */
24
27
  import { type AgentTool } from '@aragon-agent/core';
25
28
  import type { TodoStore } from './store.js';
29
+ import type { AcceptanceItem, TodoItem } from './types.js';
26
30
  export interface TodoToolDeps {
27
31
  store: TodoStore;
28
32
  /** Read LIVE, never cached: `/todo off` flips this; it cannot unregister (C-1). */
29
33
  isEnabled: () => boolean;
34
+ /**
35
+ * Read LIVE, never cached (D-11): `/todo acceptance off` must take effect in the
36
+ * session that is already running, because the tool array is immutable (C-1).
37
+ */
38
+ hasAcceptance: () => boolean;
30
39
  }
31
40
  /**
32
41
  * Turned off mid-session. A NON-ERROR result on purpose: the user turned a
@@ -36,3 +45,19 @@ export declare const TODO_OFF_REFUSAL = "Todo planning is off for this session.
36
45
  /** The structural half of R-c: a one-step list is not a plan. */
37
46
  export declare const TOO_SMALL_REFUSAL = "A one-step list is not a plan. Just do the work and summarize when you are done.";
38
47
  export declare function createTodoTool(deps: TodoToolDeps): AgentTool;
48
+ /**
49
+ * The tool result — THE SINGLE STRONGEST LEVER ON R-b ("one at a time"), because
50
+ * unlike the system prompt it is re-read on every single call. There is
51
+ * deliberately NO RATE LIMIT to compete with it: a cap would fire exactly when the
52
+ * model is being most diligent and produce a refusal it cannot act on, and the
53
+ * costs a cap would have contained are already gone (§3.8 collapses N calls into
54
+ * ONE transcript entry, and the store is memory-only).
55
+ *
56
+ * FOUR PHASES, and the acceptance paragraph is inserted after the "In progress /
57
+ * Every item" paragraph and before any `Note:`. In `verify` and in the two `done`
58
+ * cases it also REPLACES that paragraph's closing sentence, because a plan that
59
+ * carries criteria has something more specific to say about being finished. With the
60
+ * capability off, or with no criteria, every branch below is byte-identical to the
61
+ * version without this feature (AC-28).
62
+ */
63
+ export declare function buildResultText(items: readonly TodoItem[], acceptance: readonly AcceptanceItem[], repairs: readonly string[], acceptanceEnabled: boolean): string;