@aragon-agent/cli 0.6.35 → 0.6.37
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.
- package/CHANGELOG.md +21 -0
- package/README.md +3173 -3071
- package/dist/agent/agent-mode.d.ts +20 -4
- package/dist/agent/agent-mode.js +20 -4
- package/dist/agent/controller.d.ts +38 -3
- package/dist/agent/controller.js +64 -4
- package/dist/agent/headless.js +31 -0
- package/dist/agent/reducer.d.ts +13 -2
- package/dist/agent/reducer.js +1 -0
- package/dist/cli.js +12 -0
- package/dist/commands/agents-command.d.ts +15 -0
- package/dist/commands/agents-command.js +27 -0
- package/dist/commands/builtins.js +131 -4
- package/dist/commands/perf.d.ts +11 -0
- package/dist/commands/perf.js +2 -1
- package/dist/commands/registry.d.ts +14 -0
- package/dist/config/app-paths.d.ts +15 -0
- package/dist/config/app-paths.js +33 -2
- package/dist/config/env.d.ts +4 -1
- package/dist/config/env.js +28 -2
- package/dist/config/load.d.ts +10 -0
- package/dist/config/load.js +8 -1
- package/dist/config/schema.d.ts +68 -0
- package/dist/config/schema.js +39 -0
- package/dist/config/store.js +12 -1
- package/dist/config/ui-state.d.ts +18 -0
- package/dist/config/ui-state.js +9 -0
- package/dist/exec/events.d.ts +25 -0
- package/dist/exec/events.js +4 -0
- package/dist/exec/index.js +16 -1
- package/dist/exec/runner.js +10 -0
- package/dist/proc/limits.d.ts +16 -0
- package/dist/proc/limits.js +16 -0
- package/dist/proc/supervisor.d.ts +20 -1
- package/dist/proc/supervisor.js +42 -14
- package/dist/prompts/command.d.ts +34 -0
- package/dist/prompts/command.js +237 -0
- package/dist/prompts/copy.d.ts +89 -0
- package/dist/prompts/copy.js +131 -0
- package/dist/prompts/fill.d.ts +51 -0
- package/dist/prompts/fill.js +46 -0
- package/dist/prompts/limits.d.ts +53 -0
- package/dist/prompts/limits.js +53 -0
- package/dist/prompts/normalize.d.ts +53 -0
- package/dist/prompts/normalize.js +190 -0
- package/dist/prompts/resolve.d.ts +31 -0
- package/dist/prompts/resolve.js +60 -0
- package/dist/prompts/search.d.ts +58 -0
- package/dist/prompts/search.js +146 -0
- package/dist/prompts/store.d.ts +86 -0
- package/dist/prompts/store.js +453 -0
- package/dist/prompts/types.d.ts +204 -0
- package/dist/prompts/types.js +13 -0
- package/dist/session/checkpoint.d.ts +8 -1
- package/dist/session/checkpoint.js +6 -0
- package/dist/session/persist.d.ts +19 -1
- package/dist/session/persist.js +13 -1
- package/dist/session/startup-session.d.ts +7 -2
- package/dist/session/startup-session.js +5 -2
- package/dist/session/store.js +3 -0
- package/dist/session/validate-session.js +64 -1
- package/dist/team/limits.d.ts +25 -0
- package/dist/team/limits.js +26 -0
- package/dist/team/runtime.js +27 -2
- package/dist/team/settled.d.ts +57 -0
- package/dist/team/settled.js +87 -0
- package/dist/team/subagent.js +48 -2
- package/dist/team/timeline.d.ts +74 -0
- package/dist/team/timeline.js +101 -0
- package/dist/team/types.d.ts +24 -1
- package/dist/todo/acceptance.d.ts +120 -0
- package/dist/todo/acceptance.js +399 -0
- package/dist/todo/follow-through.d.ts +48 -23
- package/dist/todo/follow-through.js +110 -44
- package/dist/todo/limits.d.ts +62 -1
- package/dist/todo/limits.js +62 -1
- package/dist/todo/panel-rows.d.ts +29 -39
- package/dist/todo/panel-rows.js +37 -83
- package/dist/todo/plan-sections.d.ts +60 -0
- package/dist/todo/plan-sections.js +108 -0
- package/dist/todo/prompt.d.ts +17 -12
- package/dist/todo/prompt.js +59 -40
- package/dist/todo/row-window.d.ts +51 -0
- package/dist/todo/row-window.js +107 -0
- package/dist/todo/store.d.ts +139 -31
- package/dist/todo/store.js +271 -73
- package/dist/todo/todo-tool.d.ts +33 -8
- package/dist/todo/todo-tool.js +186 -30
- package/dist/todo/types.d.ts +58 -0
- package/dist/ui/App.js +310 -46
- package/dist/ui/Composer.d.ts +6 -2
- package/dist/ui/Composer.js +2 -2
- package/dist/ui/Header.d.ts +9 -1
- package/dist/ui/Header.js +2 -2
- package/dist/ui/PromptInput.d.ts +21 -1
- package/dist/ui/PromptInput.js +21 -1
- package/dist/ui/ScrollbackShell.d.ts +19 -19
- package/dist/ui/ScrollbackShell.js +8 -1
- package/dist/ui/StatusBar.d.ts +5 -0
- package/dist/ui/StatusBar.js +1 -0
- package/dist/ui/TeamPanel.d.ts +26 -44
- package/dist/ui/TeamPanel.js +46 -135
- package/dist/ui/TodoPanel.d.ts +17 -14
- package/dist/ui/TodoPanel.js +146 -38
- package/dist/ui/Transcript.js +1 -1
- package/dist/ui/composer-input.d.ts +10 -0
- package/dist/ui/composer-input.js +57 -24
- package/dist/ui/entries/TodoCard.d.ts +26 -11
- package/dist/ui/entries/TodoCard.js +56 -15
- package/dist/ui/glyphs.d.ts +19 -0
- package/dist/ui/glyphs.js +4 -0
- package/dist/ui/layout/agents-board.d.ts +102 -0
- package/dist/ui/layout/agents-board.js +389 -0
- package/dist/ui/layout/cells.d.ts +61 -0
- package/dist/ui/layout/cells.js +156 -0
- package/dist/ui/layout/header-hints.d.ts +13 -3
- package/dist/ui/layout/header-hints.js +48 -14
- package/dist/ui/layout/status-detail-layout.js +9 -0
- package/dist/ui/layout/status-layout.d.ts +13 -0
- package/dist/ui/layout/team-row.d.ts +139 -0
- package/dist/ui/layout/team-row.js +246 -0
- package/dist/ui/layout/todo-layout.d.ts +38 -0
- package/dist/ui/layout/todo-layout.js +75 -0
- package/dist/ui/layout/virtual-window.js +23 -3
- package/dist/ui/overlays/AgentsBoardOverlay.d.ts +57 -0
- package/dist/ui/overlays/AgentsBoardOverlay.js +183 -0
- package/dist/ui/overlays/HelpOverlay.js +17 -2
- package/dist/ui/overlays/PromptEditorPage.d.ts +48 -0
- package/dist/ui/overlays/PromptEditorPage.js +227 -0
- package/dist/ui/overlays/PromptLibraryOverlay.d.ts +37 -0
- package/dist/ui/overlays/PromptLibraryOverlay.js +191 -0
- package/dist/ui/overlays/PromptPicker.d.ts +36 -0
- package/dist/ui/overlays/PromptPicker.js +129 -0
- package/dist/ui/overlays/PromptTextArea.d.ts +86 -0
- package/dist/ui/overlays/PromptTextArea.js +141 -0
- package/dist/ui/overlays/prompt-library-state.d.ts +162 -0
- package/dist/ui/overlays/prompt-library-state.js +335 -0
- package/dist/ui/overlays/prompt-picker-state.d.ts +71 -0
- package/dist/ui/overlays/prompt-picker-state.js +98 -0
- package/dist/ui/scrollback/band.js +14 -1
- package/dist/ui/team-status.d.ts +25 -0
- package/dist/ui/team-status.js +31 -3
- package/dist/ui/use-startup-notices.d.ts +35 -1
- package/dist/ui/use-startup-notices.js +29 -2
- package/dist/ui/use-wheel-routing.js +9 -1
- package/package.json +2 -2
package/dist/todo/store.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `TodoStore` — the session's todo list
|
|
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
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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 {
|
|
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
|
|
56
|
-
* nothing usable is a BUG, and destroying
|
|
57
|
-
* malformed call is the worst available reaction
|
|
58
|
-
*
|
|
59
|
-
* empty list is a FACT about a file and therefore means
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
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
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
* file
|
|
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
|
|
108
|
-
if (
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
this.
|
|
115
|
-
const
|
|
116
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
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
|
|
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
|
-
//
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
//
|
|
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]) {
|
package/dist/todo/todo-tool.d.ts
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `todo_write` — the ONLY writer of `TodoStore` (todo-plan-execution §3.3 /
|
|
3
|
-
* §4
|
|
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
|
-
*
|
|
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 `
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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;
|