@aragon-agent/cli 0.5.4 → 0.5.8
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 +747 -577
- package/README.md +1163 -995
- package/dist/agent/controller.d.ts +71 -1
- package/dist/agent/controller.js +135 -1
- package/dist/agent/entry-limits.d.ts +65 -0
- package/dist/agent/entry-limits.js +133 -0
- package/dist/agent/headless.d.ts +29 -0
- package/dist/agent/headless.js +86 -1
- package/dist/agent/reducer.d.ts +53 -0
- package/dist/agent/reducer.js +145 -14
- package/dist/agent/system-prompt.d.ts +8 -0
- package/dist/agent/system-prompt.js +2 -0
- package/dist/cli.js +76 -3
- package/dist/commands/builtins.js +171 -0
- package/dist/commands/perf.d.ts +45 -0
- package/dist/commands/perf.js +78 -0
- package/dist/commands/registry.d.ts +27 -1
- package/dist/config/env.d.ts +3 -1
- package/dist/config/env.js +74 -1
- package/dist/config/load.d.ts +41 -0
- package/dist/config/load.js +67 -2
- package/dist/config/schema.d.ts +108 -8
- package/dist/config/schema.js +81 -6
- package/dist/config/store.js +10 -1
- package/dist/session/persist.d.ts +19 -1
- package/dist/session/persist.js +21 -2
- package/dist/team/subagent.js +11 -0
- package/dist/todo/follow-through.d.ts +117 -0
- package/dist/todo/follow-through.js +186 -0
- package/dist/todo/limits.d.ts +114 -0
- package/dist/todo/limits.js +114 -0
- package/dist/todo/normalize.d.ts +33 -0
- package/dist/todo/normalize.js +117 -0
- package/dist/todo/panel-rows.d.ts +53 -0
- package/dist/todo/panel-rows.js +103 -0
- package/dist/todo/prompt.d.ts +54 -0
- package/dist/todo/prompt.js +76 -0
- package/dist/todo/store.d.ts +86 -0
- package/dist/todo/store.js +183 -0
- package/dist/todo/todo-tool.d.ts +38 -0
- package/dist/todo/todo-tool.js +134 -0
- package/dist/todo/types.d.ts +62 -0
- package/dist/todo/types.js +12 -0
- package/dist/tools/index.d.ts +16 -2
- package/dist/tools/index.js +13 -0
- package/dist/ui/App.js +328 -14
- package/dist/ui/Markdown.d.ts +7 -1
- package/dist/ui/Markdown.js +15 -48
- package/dist/ui/StatusBar.d.ts +31 -0
- package/dist/ui/StatusBar.js +7 -2
- package/dist/ui/TodoPanel.d.ts +48 -0
- package/dist/ui/TodoPanel.js +74 -0
- package/dist/ui/TodoStrip.d.ts +36 -0
- package/dist/ui/TodoStrip.js +32 -0
- package/dist/ui/Transcript.d.ts +56 -4
- package/dist/ui/Transcript.js +123 -11
- package/dist/ui/entries/AssistantEntry.d.ts +18 -1
- package/dist/ui/entries/AssistantEntry.js +33 -3
- package/dist/ui/entries/TeamCard.d.ts +9 -1
- package/dist/ui/entries/TeamCard.js +22 -1
- package/dist/ui/entries/TodoCard.d.ts +39 -0
- package/dist/ui/entries/TodoCard.js +57 -0
- package/dist/ui/entries/ToolCard.d.ts +13 -1
- package/dist/ui/entries/ToolCard.js +39 -5
- package/dist/ui/entries/ToolPreview.d.ts +2 -2
- package/dist/ui/entries/UserEntry.d.ts +9 -1
- package/dist/ui/entries/UserEntry.js +17 -1
- package/dist/ui/exit-snapshot.d.ts +7 -0
- package/dist/ui/glyphs.d.ts +12 -0
- package/dist/ui/glyphs.js +8 -0
- package/dist/ui/layout/AppShell.d.ts +34 -1
- package/dist/ui/layout/AppShell.js +3 -3
- package/dist/ui/layout/MeasuredEntry.d.ts +21 -0
- package/dist/ui/layout/MeasuredEntry.js +30 -0
- package/dist/ui/layout/ScrollViewport.d.ts +13 -1
- package/dist/ui/layout/ScrollViewport.js +10 -4
- package/dist/ui/layout/rail.d.ts +56 -0
- package/dist/ui/layout/rail.js +68 -0
- package/dist/ui/layout/viewport-geometry.d.ts +28 -0
- package/dist/ui/layout/viewport-geometry.js +27 -0
- package/dist/ui/layout/virtual-window.d.ts +100 -0
- package/dist/ui/layout/virtual-window.js +204 -0
- package/dist/ui/live-clamp.d.ts +42 -0
- package/dist/ui/live-clamp.js +74 -0
- package/dist/ui/markdown-blocks.d.ts +25 -1
- package/dist/ui/markdown-blocks.js +43 -0
- package/dist/ui/overlays/HelpOverlay.js +4 -0
- package/dist/ui/render-cache.d.ts +81 -0
- package/dist/ui/render-cache.js +195 -0
- package/dist/ui/render-governor.d.ts +54 -0
- package/dist/ui/render-governor.js +78 -0
- package/dist/ui/transcript-text.d.ts +11 -0
- package/dist/ui/transcript-text.js +5 -0
- package/dist/ui/use-height-store.d.ts +31 -0
- package/dist/ui/use-height-store.js +76 -0
- package/dist/ui/use-render-governor.d.ts +39 -0
- package/dist/ui/use-render-governor.js +57 -0
- package/dist/ui/use-startup-notices.d.ts +9 -1
- package/dist/ui/use-startup-notices.js +35 -1
- package/package.json +3 -2
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* follow-through — what happens when a run ends with unfinished steps
|
|
3
|
+
* (todo-plan-followthrough §3.2 / §3.3).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/todo/**` is inside the glyph scanner's scope
|
|
6
|
+
* (`glyphs.test.ts`), and every string this module builds is user-visible.
|
|
7
|
+
*
|
|
8
|
+
* PURE. No I/O, no timers, no React, no `Date.now()` — the discipline
|
|
9
|
+
* `TodoStore`'s injected clock already establishes. The TUI (`App.tsx`) and
|
|
10
|
+
* headless mode (`agent/headless.ts`) both call `decideFollowThrough`, which is
|
|
11
|
+
* the only reason the two cannot drift; a second copy of this table in either
|
|
12
|
+
* caller is the defect this file exists to prevent.
|
|
13
|
+
*
|
|
14
|
+
* IT NEVER WRITES TO `TodoStore` (I-1 / C-3). Follow-through reads snapshots and
|
|
15
|
+
* produces a MESSAGE; the plan changes only when the model calls `todo_write`,
|
|
16
|
+
* which keeps the list a projection of what the model believes.
|
|
17
|
+
*/
|
|
18
|
+
import { TODO_FOLLOW_LIMITS } from './limits.js';
|
|
19
|
+
/** The zero budget. A fresh App, a fresh headless run, and every cleared list. */
|
|
20
|
+
export function emptyBudget() {
|
|
21
|
+
return { used: 0, noProgressStreak: 0, anchorTotal: 0, doneAtRunStart: 0 };
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Round 1's notice, preserved BYTE FOR BYTE (AC-1).
|
|
25
|
+
*
|
|
26
|
+
* This is what makes `followThrough: 'notify'` — the default — indistinguishable
|
|
27
|
+
* from `bf6aeb1a` on the path that matters. Its singular/plural treatment
|
|
28
|
+
* ("item is" / "items are") is round 1's; the strings added by this round use
|
|
29
|
+
* "steps", the noun `<todo_planning>` and the rail already use.
|
|
30
|
+
*/
|
|
31
|
+
function legacyUnfinishedNotice(left) {
|
|
32
|
+
return (`${left} todo ${left === 1 ? 'item is' : 'items are'} unfinished. ` +
|
|
33
|
+
'Use /todo continue to pick up where this run stopped, or /todo clear to drop it.');
|
|
34
|
+
}
|
|
35
|
+
/** `/todo continue ...` advice, which means nothing in `-p`. */
|
|
36
|
+
function advise(interactive, text) {
|
|
37
|
+
return interactive ? ` ${text}` : '';
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The decision. EVALUATED IN THIS ORDER, AND THE ORDER IS SEMANTICS, NOT STYLE —
|
|
41
|
+
* the same sentence `decideRenderMode` carries.
|
|
42
|
+
*/
|
|
43
|
+
export function decideFollowThrough(input) {
|
|
44
|
+
const { mode, snapshot, runEnd, budget, interactive } = input;
|
|
45
|
+
// 1. Nothing is unfinished. `beginUserTurn` already drops a fully-completed
|
|
46
|
+
// list at the next turn, so this branch must not pre-empt that feedback.
|
|
47
|
+
if (!snapshot || snapshot.doneCount >= snapshot.total)
|
|
48
|
+
return { kind: 'none' };
|
|
49
|
+
const left = snapshot.total - snapshot.doneCount;
|
|
50
|
+
// 2. The user asked for silence and gets silence, INCLUDING no notice.
|
|
51
|
+
if (mode === 'off')
|
|
52
|
+
return { kind: 'none' };
|
|
53
|
+
// 3. A BUG FIX, NOT A POLICY (D-5). The user pressed Esc; restating what they
|
|
54
|
+
// interrupted is the CLI answering a question nobody asked, and offering to
|
|
55
|
+
// continue it is worse.
|
|
56
|
+
if (runEnd.aborted)
|
|
57
|
+
return { kind: 'none' };
|
|
58
|
+
// 4. NEVER `continue`, IN ANY MODE (D-6). Auto-continuing into a model that
|
|
59
|
+
// just failed authentication or ran out of quota is the unbounded cost loop
|
|
60
|
+
// with none of the upside.
|
|
61
|
+
if (runEnd.errored) {
|
|
62
|
+
return {
|
|
63
|
+
kind: 'notify',
|
|
64
|
+
level: 'warn',
|
|
65
|
+
text: `${left} ${left === 1 ? 'step' : 'steps'} left; the run ended with an error.` +
|
|
66
|
+
advise(interactive, '/todo continue to retry, /todo clear to drop it.'),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
// 5. The default. Byte-identical to round 1 in the TUI (AC-1).
|
|
70
|
+
if (mode === 'notify') {
|
|
71
|
+
return {
|
|
72
|
+
kind: 'notify',
|
|
73
|
+
level: 'info',
|
|
74
|
+
text: interactive
|
|
75
|
+
? legacyUnfinishedNotice(left)
|
|
76
|
+
: `${left} ${left === 1 ? 'step' : 'steps'} unfinished`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
// 6. The ceiling that holds when everything else is wrong.
|
|
80
|
+
if (budget.used >= TODO_FOLLOW_LIMITS.maxAutoContinuesPerList) {
|
|
81
|
+
return {
|
|
82
|
+
kind: 'notify',
|
|
83
|
+
level: 'warn',
|
|
84
|
+
text: `${left} ${left === 1 ? 'step' : 'steps'} left; auto-continue has run ` +
|
|
85
|
+
`${TODO_FOLLOW_LIMITS.maxAutoContinuesPerList} times for this plan.` +
|
|
86
|
+
advise(interactive, '/todo continue to keep going.'),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
// 7. A model that has stopped engaging with its own plan hands back after one
|
|
90
|
+
// nudge rather than after twenty-five.
|
|
91
|
+
if (budget.noProgressStreak >= TODO_FOLLOW_LIMITS.maxNoProgressContinues) {
|
|
92
|
+
return {
|
|
93
|
+
kind: 'notify',
|
|
94
|
+
level: 'warn',
|
|
95
|
+
text: `${left} ${left === 1 ? 'step' : 'steps'} left; the last ` +
|
|
96
|
+
`${TODO_FOLLOW_LIMITS.maxNoProgressContinues} attempts completed none of them.` +
|
|
97
|
+
advise(interactive, '/todo continue to retry, or take over.'),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
const graceMs = interactive ? TODO_FOLLOW_LIMITS.graceMs : 0;
|
|
101
|
+
return {
|
|
102
|
+
kind: 'continue',
|
|
103
|
+
message: buildContinuationMessage(snapshot),
|
|
104
|
+
graceMs,
|
|
105
|
+
// The seconds figure is DERIVED from the constant, never spelled again: a
|
|
106
|
+
// notice that promises 3 s while the timer waits 5 is worse than no notice.
|
|
107
|
+
notice: interactive
|
|
108
|
+
? `Continuing with ${left} remaining ${left === 1 ? 'step' : 'steps'} in ` +
|
|
109
|
+
`${Math.round(graceMs / 1000)}s - Esc to stop.`
|
|
110
|
+
: `continuing (${left} ${left === 1 ? 'step' : 'steps'} left)`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Fold a finished run into the budget. Called BEFORE `decideFollowThrough`, on
|
|
115
|
+
* every `agent_end` (and on every resolved `prompt()` in headless).
|
|
116
|
+
*
|
|
117
|
+
* `used` is NOT incremented here — a decision that is never acted on costs
|
|
118
|
+
* nothing. Both fire points charge it themselves (I-5): the TUI's grace timer
|
|
119
|
+
* and the headless loop, and BOTH are required. An uncharged counter in `-p` is
|
|
120
|
+
* an uncapped loop in the one environment with no human to press Esc (P1-2).
|
|
121
|
+
*/
|
|
122
|
+
export function advanceBudget(prev, snapshot, wasAutoContinuation) {
|
|
123
|
+
// THE ONLY PATH THAT CLEARS `used` (I-8). The list is gone; so is its economy.
|
|
124
|
+
// It is not reachable by a model on its own: `TodoStore.write` leaves the
|
|
125
|
+
// previous list untouched when a payload yields zero usable items, and the
|
|
126
|
+
// other three ways a list disappears are `/todo clear`, `/reset` and
|
|
127
|
+
// `beginUserTurn` dropping a list that is either finished (nothing to
|
|
128
|
+
// continue) or stale (which the streak rule beats to the punch, §3.5).
|
|
129
|
+
if (!snapshot)
|
|
130
|
+
return emptyBudget();
|
|
131
|
+
// A RE-PLAN BUYS FORGIVENESS, NOT A FRESH WALLET (D-16). Replanning is
|
|
132
|
+
// engagement rather than looping, which is why the streak is forgiven; zeroing
|
|
133
|
+
// `used` as well is what makes the hard ceiling unreachable, because a size
|
|
134
|
+
// change every turn would reset it every turn.
|
|
135
|
+
if (snapshot.total !== prev.anchorTotal) {
|
|
136
|
+
return {
|
|
137
|
+
used: prev.used,
|
|
138
|
+
noProgressStreak: 0,
|
|
139
|
+
anchorTotal: snapshot.total,
|
|
140
|
+
doneAtRunStart: snapshot.doneCount,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
const progressed = snapshot.doneCount > prev.doneAtRunStart;
|
|
144
|
+
// NOTE THE GUARD: a run the USER started never charges the streak, because the
|
|
145
|
+
// user asking a question mid-plan is not the model failing to advance it.
|
|
146
|
+
const noProgressStreak = progressed
|
|
147
|
+
? 0
|
|
148
|
+
: wasAutoContinuation
|
|
149
|
+
? prev.noProgressStreak + 1
|
|
150
|
+
: prev.noProgressStreak;
|
|
151
|
+
return {
|
|
152
|
+
used: prev.used,
|
|
153
|
+
noProgressStreak,
|
|
154
|
+
anchorTotal: prev.anchorTotal,
|
|
155
|
+
// Assigned in EVERY branch, which is what makes the next call's comparison
|
|
156
|
+
// meaningful.
|
|
157
|
+
doneAtRunStart: snapshot.doneCount,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* The text an auto-continuation — or `/todo continue` — submits.
|
|
162
|
+
*
|
|
163
|
+
* IT ENUMERATES RATHER THAN REFERRING (D-11). Round 1 submitted the fixed string
|
|
164
|
+
* "Continue with the remaining todo items.", which is a reference to a
|
|
165
|
+
* `todo_write` call that in a long session may have scrolled far enough back
|
|
166
|
+
* that the model reconstructs it wrong — and reconstructing it wrong is SILENT,
|
|
167
|
+
* because whatever it writes next is a full replacement and becomes the new
|
|
168
|
+
* truth.
|
|
169
|
+
*
|
|
170
|
+
* `content` and never `activeForm`: the imperative is what an instruction wants.
|
|
171
|
+
* Numbers are the item's 1-based position in the WHOLE list, so "step 5" means
|
|
172
|
+
* the same thing here as it does in the rail. Completed items are omitted.
|
|
173
|
+
* `TODO_LIMITS.maxItems` bounds the whole message at roughly 1.8 kB.
|
|
174
|
+
*/
|
|
175
|
+
export function buildContinuationMessage(snapshot) {
|
|
176
|
+
const remaining = snapshot.items
|
|
177
|
+
.map((item, index) => ({ item, index }))
|
|
178
|
+
.filter(({ item }) => item.status !== 'completed')
|
|
179
|
+
.map(({ item, index }) => `${index + 1}. ${item.content}`);
|
|
180
|
+
return [
|
|
181
|
+
'Continue the plan. These steps are not done yet:',
|
|
182
|
+
...remaining,
|
|
183
|
+
'Work the next one, then call todo_write to mark it completed and the following ' +
|
|
184
|
+
'one in_progress before you move on.',
|
|
185
|
+
].join('\n');
|
|
186
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TODO_LIMITS — the single authority on every structural bound this subsystem
|
|
3
|
+
* enforces (todo-plan-execution §4.2).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/todo/**` is inside the glyph scanner's scope
|
|
6
|
+
* (`glyphs.test.ts`), whose `inScope` regex this feature extends. A NEW TREE IS
|
|
7
|
+
* INVISIBLE TO THAT SCAN UNTIL IT IS ADDED (C-4), which is a guard rail that
|
|
8
|
+
* silently stops guarding — the same hole the team feature had to close for
|
|
9
|
+
* `team/`.
|
|
10
|
+
*
|
|
11
|
+
* TWO KINDS OF NUMBER, AND THEY ARE NOT INTERCHANGEABLE — the distinction
|
|
12
|
+
* `TEAM_LIMITS` records at length:
|
|
13
|
+
*
|
|
14
|
+
* - Everything below is STRUCTURAL: what the panel can physically carry, what
|
|
15
|
+
* a plan can mean, what a payload may weigh. A user has no business tuning
|
|
16
|
+
* them, so they are constants.
|
|
17
|
+
* - The two `todo.*` config keys (`config/schema.ts`) are POLICY: register the
|
|
18
|
+
* tool at all, render the rail at all. They are clamped, persisted and
|
|
19
|
+
* user-facing.
|
|
20
|
+
*/
|
|
21
|
+
export declare const TODO_LIMITS: {
|
|
22
|
+
/** Items one list may hold. Extras are dropped with a repair note. */
|
|
23
|
+
readonly maxItems: 20;
|
|
24
|
+
/** `TodoItem.content`, clamped (never rejected). */
|
|
25
|
+
readonly contentChars: 80;
|
|
26
|
+
/** `TodoItem.activeForm`, clamped. */
|
|
27
|
+
readonly activeFormChars: 80;
|
|
28
|
+
/**
|
|
29
|
+
* Below this, a FRESH list is refused - the structural half of R-c ("simple
|
|
30
|
+
* tasks are done directly, not planned"). Gated on an EMPTY store: shrinking
|
|
31
|
+
* an existing list to one item is legitimate, so the guard must not fire
|
|
32
|
+
* there (§3.3 step 2).
|
|
33
|
+
*/
|
|
34
|
+
readonly minFreshItems: 2;
|
|
35
|
+
/** Rows the in-progress row may wrap to. Every other row truncates. */
|
|
36
|
+
readonly activeWrapRows: 2;
|
|
37
|
+
/**
|
|
38
|
+
* Below this many AVAILABLE rail rows (`todoRailRows`, NOT `viewportBudget`)
|
|
39
|
+
* the rail is not mounted at all.
|
|
40
|
+
*/
|
|
41
|
+
readonly panelMinRows: 6;
|
|
42
|
+
/** Below this many terminal columns the status-bar counter degrades to `[3/7]`. */
|
|
43
|
+
readonly statusCompactCols: 100;
|
|
44
|
+
/**
|
|
45
|
+
* Below this many columns the inline strip drops its `+N done` suffix
|
|
46
|
+
* (todo-plan-followthrough §3.7 / D-19).
|
|
47
|
+
*
|
|
48
|
+
* NAMED RATHER THAN INLINED, and the reason is that it sits between two other
|
|
49
|
+
* real bounds — `statusCompactCols` (100) above and `MIN_FULLSCREEN_COLS` (40)
|
|
50
|
+
* in `frame.ts` — with nothing to tell a reader whether a bare `80` in the
|
|
51
|
+
* component was a third policy or a typo for one of them.
|
|
52
|
+
*/
|
|
53
|
+
readonly stripDoneCols: 80;
|
|
54
|
+
/**
|
|
55
|
+
* Rows `todoRailRows()` subtracts while a team dispatch is live (§3.9 / P1-3).
|
|
56
|
+
*
|
|
57
|
+
* `TEAM_LIMITS.panelMaxRows` (5) + header + `+N more` + the mail line = 8, and
|
|
58
|
+
* this is deliberately the FULL worst case rather than a measurement: an
|
|
59
|
+
* over-subtraction costs one item row, an under-subtraction eats the `+N
|
|
60
|
+
* below` marker - the one row whose absence is indistinguishable from "the
|
|
61
|
+
* list is short" - and those two mistakes are not equally bad.
|
|
62
|
+
*/
|
|
63
|
+
readonly railReservedRows: 8;
|
|
64
|
+
/**
|
|
65
|
+
* Consecutive user turns an UNFINISHED list survives without a `todo_write`
|
|
66
|
+
* before `beginUserTurn()` drops it (§3.2 / P1-7). Three keeps the common
|
|
67
|
+
* interruption sequence ("wait, explain X" / "and Y?" / "ok, continue")
|
|
68
|
+
* intact and returns the column on the fourth unrelated turn.
|
|
69
|
+
*/
|
|
70
|
+
readonly staleTurns: 3;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* TODO_FOLLOW_LIMITS — the economy that makes auto-continue's worst case
|
|
74
|
+
* ARITHMETIC rather than trust (todo-plan-followthrough §8.1).
|
|
75
|
+
*
|
|
76
|
+
* STRUCTURAL, NOT POLICY, exactly like `TODO_LIMITS` above: `todo.followThrough`
|
|
77
|
+
* is the user-facing knob and it decides only WHETHER the loop runs. How far it
|
|
78
|
+
* may run is not something a user has any way to reason about, and a setting
|
|
79
|
+
* here would be a setting whose wrong value is a bill.
|
|
80
|
+
*/
|
|
81
|
+
export declare const TODO_FOLLOW_LIMITS: {
|
|
82
|
+
/**
|
|
83
|
+
* Auto-continuations for ONE LIST LINEAGE, however productive, and NOT reset
|
|
84
|
+
* by a re-plan (P0-1 / D-16). See `FollowThroughBudget.used`.
|
|
85
|
+
*
|
|
86
|
+
* DELIBERATELY GREATER THAN `TODO_LIMITS.maxItems` (20): a fully productive
|
|
87
|
+
* 20-step plan must never be cut off by the total cap, so the cap only ever
|
|
88
|
+
* binds on pathology. Lowering it below `maxItems` would make the ceiling fire
|
|
89
|
+
* on success, which is the one thing it must not do.
|
|
90
|
+
*/
|
|
91
|
+
readonly maxAutoContinuesPerList: 25;
|
|
92
|
+
/**
|
|
93
|
+
* Consecutive auto-continuations that completed nothing before control goes
|
|
94
|
+
* back to the human. Two, so a model that merely forgot to tick an item gets
|
|
95
|
+
* exactly one free nudge and a model that has stopped engaging with its own
|
|
96
|
+
* plan does not get a third.
|
|
97
|
+
*/
|
|
98
|
+
readonly maxNoProgressContinues: 2;
|
|
99
|
+
/**
|
|
100
|
+
* Interactive grace window before an armed continuation fires. The notice text
|
|
101
|
+
* derives its "in Ns" from THIS value rather than spelling 3 again, so the two
|
|
102
|
+
* cannot disagree.
|
|
103
|
+
*/
|
|
104
|
+
readonly graceMs: 3000;
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* Bumped whenever the wording of the `<todo_planning>` block changes - INCLUDING
|
|
108
|
+
* either `{{VISIBILITY}}` variant, which is part of the block - so a behaviour
|
|
109
|
+
* report can be tied to a block revision with one grep.
|
|
110
|
+
*
|
|
111
|
+
* NOT BUMPED BY follow-through (D-3): the model is never told about the
|
|
112
|
+
* mechanism, so the block is byte-identical to `bf6aeb1a`.
|
|
113
|
+
*/
|
|
114
|
+
export declare const TODO_BLOCK_VERSION = "v1-2026-07";
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TODO_LIMITS — the single authority on every structural bound this subsystem
|
|
3
|
+
* enforces (todo-plan-execution §4.2).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/todo/**` is inside the glyph scanner's scope
|
|
6
|
+
* (`glyphs.test.ts`), whose `inScope` regex this feature extends. A NEW TREE IS
|
|
7
|
+
* INVISIBLE TO THAT SCAN UNTIL IT IS ADDED (C-4), which is a guard rail that
|
|
8
|
+
* silently stops guarding — the same hole the team feature had to close for
|
|
9
|
+
* `team/`.
|
|
10
|
+
*
|
|
11
|
+
* TWO KINDS OF NUMBER, AND THEY ARE NOT INTERCHANGEABLE — the distinction
|
|
12
|
+
* `TEAM_LIMITS` records at length:
|
|
13
|
+
*
|
|
14
|
+
* - Everything below is STRUCTURAL: what the panel can physically carry, what
|
|
15
|
+
* a plan can mean, what a payload may weigh. A user has no business tuning
|
|
16
|
+
* them, so they are constants.
|
|
17
|
+
* - The two `todo.*` config keys (`config/schema.ts`) are POLICY: register the
|
|
18
|
+
* tool at all, render the rail at all. They are clamped, persisted and
|
|
19
|
+
* user-facing.
|
|
20
|
+
*/
|
|
21
|
+
export const TODO_LIMITS = {
|
|
22
|
+
/** Items one list may hold. Extras are dropped with a repair note. */
|
|
23
|
+
maxItems: 20,
|
|
24
|
+
/** `TodoItem.content`, clamped (never rejected). */
|
|
25
|
+
contentChars: 80,
|
|
26
|
+
/** `TodoItem.activeForm`, clamped. */
|
|
27
|
+
activeFormChars: 80,
|
|
28
|
+
/**
|
|
29
|
+
* Below this, a FRESH list is refused - the structural half of R-c ("simple
|
|
30
|
+
* tasks are done directly, not planned"). Gated on an EMPTY store: shrinking
|
|
31
|
+
* an existing list to one item is legitimate, so the guard must not fire
|
|
32
|
+
* there (§3.3 step 2).
|
|
33
|
+
*/
|
|
34
|
+
minFreshItems: 2,
|
|
35
|
+
/** Rows the in-progress row may wrap to. Every other row truncates. */
|
|
36
|
+
activeWrapRows: 2,
|
|
37
|
+
/**
|
|
38
|
+
* Below this many AVAILABLE rail rows (`todoRailRows`, NOT `viewportBudget`)
|
|
39
|
+
* the rail is not mounted at all.
|
|
40
|
+
*/
|
|
41
|
+
panelMinRows: 6,
|
|
42
|
+
/** Below this many terminal columns the status-bar counter degrades to `[3/7]`. */
|
|
43
|
+
statusCompactCols: 100,
|
|
44
|
+
/**
|
|
45
|
+
* Below this many columns the inline strip drops its `+N done` suffix
|
|
46
|
+
* (todo-plan-followthrough §3.7 / D-19).
|
|
47
|
+
*
|
|
48
|
+
* NAMED RATHER THAN INLINED, and the reason is that it sits between two other
|
|
49
|
+
* real bounds — `statusCompactCols` (100) above and `MIN_FULLSCREEN_COLS` (40)
|
|
50
|
+
* in `frame.ts` — with nothing to tell a reader whether a bare `80` in the
|
|
51
|
+
* component was a third policy or a typo for one of them.
|
|
52
|
+
*/
|
|
53
|
+
stripDoneCols: 80,
|
|
54
|
+
/**
|
|
55
|
+
* Rows `todoRailRows()` subtracts while a team dispatch is live (§3.9 / P1-3).
|
|
56
|
+
*
|
|
57
|
+
* `TEAM_LIMITS.panelMaxRows` (5) + header + `+N more` + the mail line = 8, and
|
|
58
|
+
* this is deliberately the FULL worst case rather than a measurement: an
|
|
59
|
+
* over-subtraction costs one item row, an under-subtraction eats the `+N
|
|
60
|
+
* below` marker - the one row whose absence is indistinguishable from "the
|
|
61
|
+
* list is short" - and those two mistakes are not equally bad.
|
|
62
|
+
*/
|
|
63
|
+
railReservedRows: 8,
|
|
64
|
+
/**
|
|
65
|
+
* Consecutive user turns an UNFINISHED list survives without a `todo_write`
|
|
66
|
+
* before `beginUserTurn()` drops it (§3.2 / P1-7). Three keeps the common
|
|
67
|
+
* interruption sequence ("wait, explain X" / "and Y?" / "ok, continue")
|
|
68
|
+
* intact and returns the column on the fourth unrelated turn.
|
|
69
|
+
*/
|
|
70
|
+
staleTurns: 3,
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* TODO_FOLLOW_LIMITS — the economy that makes auto-continue's worst case
|
|
74
|
+
* ARITHMETIC rather than trust (todo-plan-followthrough §8.1).
|
|
75
|
+
*
|
|
76
|
+
* STRUCTURAL, NOT POLICY, exactly like `TODO_LIMITS` above: `todo.followThrough`
|
|
77
|
+
* is the user-facing knob and it decides only WHETHER the loop runs. How far it
|
|
78
|
+
* may run is not something a user has any way to reason about, and a setting
|
|
79
|
+
* here would be a setting whose wrong value is a bill.
|
|
80
|
+
*/
|
|
81
|
+
export const TODO_FOLLOW_LIMITS = {
|
|
82
|
+
/**
|
|
83
|
+
* Auto-continuations for ONE LIST LINEAGE, however productive, and NOT reset
|
|
84
|
+
* by a re-plan (P0-1 / D-16). See `FollowThroughBudget.used`.
|
|
85
|
+
*
|
|
86
|
+
* DELIBERATELY GREATER THAN `TODO_LIMITS.maxItems` (20): a fully productive
|
|
87
|
+
* 20-step plan must never be cut off by the total cap, so the cap only ever
|
|
88
|
+
* binds on pathology. Lowering it below `maxItems` would make the ceiling fire
|
|
89
|
+
* on success, which is the one thing it must not do.
|
|
90
|
+
*/
|
|
91
|
+
maxAutoContinuesPerList: 25,
|
|
92
|
+
/**
|
|
93
|
+
* Consecutive auto-continuations that completed nothing before control goes
|
|
94
|
+
* back to the human. Two, so a model that merely forgot to tick an item gets
|
|
95
|
+
* exactly one free nudge and a model that has stopped engaging with its own
|
|
96
|
+
* plan does not get a third.
|
|
97
|
+
*/
|
|
98
|
+
maxNoProgressContinues: 2,
|
|
99
|
+
/**
|
|
100
|
+
* Interactive grace window before an armed continuation fires. The notice text
|
|
101
|
+
* derives its "in Ns" from THIS value rather than spelling 3 again, so the two
|
|
102
|
+
* cannot disagree.
|
|
103
|
+
*/
|
|
104
|
+
graceMs: 3000,
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* Bumped whenever the wording of the `<todo_planning>` block changes - INCLUDING
|
|
108
|
+
* either `{{VISIBILITY}}` variant, which is part of the block - so a behaviour
|
|
109
|
+
* report can be tied to a block revision with one grep.
|
|
110
|
+
*
|
|
111
|
+
* NOT BUMPED BY follow-through (D-3): the model is never told about the
|
|
112
|
+
* mechanism, so the block is byte-identical to `bf6aeb1a`.
|
|
113
|
+
*/
|
|
114
|
+
export const TODO_BLOCK_VERSION = 'v1-2026-07';
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `normalizeTodos` — pure repair of whatever arrived in a `todo_write` call, or
|
|
3
|
+
* of whatever a session file happened to contain (todo-plan-execution §3.4).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/todo/**` is inside the glyph scanner's scope.
|
|
6
|
+
*
|
|
7
|
+
* MODELLED ON `team/normalize.ts`, INCLUDING THE REASON IT EXISTS. The JSON
|
|
8
|
+
* Schema declares SHAPE, not POLICY (C-8): `ajv` is an OPTIONAL dependency of
|
|
9
|
+
* `@aragon-agent/core`, so a schema-level `maxItems` / `enum` would REJECT a
|
|
10
|
+
* payload where it is installed and let the identical payload through to here
|
|
11
|
+
* where it is not. One design, two behaviours, selected by whether an optional
|
|
12
|
+
* install step succeeded. Every bound therefore lives in the tool's
|
|
13
|
+
* `description` (which is what the model reads) and is enforced here (which is
|
|
14
|
+
* deterministic and unit-tested).
|
|
15
|
+
*
|
|
16
|
+
* REPAIR, NEVER REJECT. Zero survivors is the caller's single hard failure.
|
|
17
|
+
* NEVER THROWS.
|
|
18
|
+
*
|
|
19
|
+
* THE LAST TWO RULES ARE THE FEATURE, not defensive tidying. "Exactly one item
|
|
20
|
+
* at a time" (R-b) is made a property of the STORED STATE rather than a request
|
|
21
|
+
* in a prompt, which is what lets the panel render an unconditional "current
|
|
22
|
+
* step" with no null branch and lets a unit test prove the property instead of
|
|
23
|
+
* an operator inferring it from a transcript.
|
|
24
|
+
*/
|
|
25
|
+
import type { TodoItem } from './types.js';
|
|
26
|
+
export interface NormalizeResult {
|
|
27
|
+
items: TodoItem[];
|
|
28
|
+
/** How many entries the payload contained BEFORE dropping and capping. */
|
|
29
|
+
requested: number;
|
|
30
|
+
/** Human-readable repair notes, already bounded. Empty when nothing was wrong. */
|
|
31
|
+
repairs: string[];
|
|
32
|
+
}
|
|
33
|
+
export declare function normalizeTodos(raw: unknown, max: number): NormalizeResult;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `normalizeTodos` — pure repair of whatever arrived in a `todo_write` call, or
|
|
3
|
+
* of whatever a session file happened to contain (todo-plan-execution §3.4).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/todo/**` is inside the glyph scanner's scope.
|
|
6
|
+
*
|
|
7
|
+
* MODELLED ON `team/normalize.ts`, INCLUDING THE REASON IT EXISTS. The JSON
|
|
8
|
+
* Schema declares SHAPE, not POLICY (C-8): `ajv` is an OPTIONAL dependency of
|
|
9
|
+
* `@aragon-agent/core`, so a schema-level `maxItems` / `enum` would REJECT a
|
|
10
|
+
* payload where it is installed and let the identical payload through to here
|
|
11
|
+
* where it is not. One design, two behaviours, selected by whether an optional
|
|
12
|
+
* install step succeeded. Every bound therefore lives in the tool's
|
|
13
|
+
* `description` (which is what the model reads) and is enforced here (which is
|
|
14
|
+
* deterministic and unit-tested).
|
|
15
|
+
*
|
|
16
|
+
* REPAIR, NEVER REJECT. Zero survivors is the caller's single hard failure.
|
|
17
|
+
* NEVER THROWS.
|
|
18
|
+
*
|
|
19
|
+
* THE LAST TWO RULES ARE THE FEATURE, not defensive tidying. "Exactly one item
|
|
20
|
+
* at a time" (R-b) is made a property of the STORED STATE rather than a request
|
|
21
|
+
* in a prompt, which is what lets the panel render an unconditional "current
|
|
22
|
+
* step" with no null branch and lets a unit test prove the property instead of
|
|
23
|
+
* an operator inferring it from a transcript.
|
|
24
|
+
*/
|
|
25
|
+
import { TODO_LIMITS } from './limits.js';
|
|
26
|
+
const STATUSES = new Set([
|
|
27
|
+
'pending',
|
|
28
|
+
'in_progress',
|
|
29
|
+
'completed',
|
|
30
|
+
]);
|
|
31
|
+
/**
|
|
32
|
+
* Collapse whitespace and clamp.
|
|
33
|
+
*
|
|
34
|
+
* SANITIZED AT STORE RATHER THAN AT RENDER, which is the argument
|
|
35
|
+
* `sanitizeActivity` records one subsystem over: a newline inside a rail row
|
|
36
|
+
* would break the layout, and doing it here means the transcript card, the
|
|
37
|
+
* headless writer and `/todo status` all inherit the guarantee for free.
|
|
38
|
+
*/
|
|
39
|
+
function clean(value, maxChars) {
|
|
40
|
+
if (typeof value !== 'string')
|
|
41
|
+
return '';
|
|
42
|
+
const collapsed = value.replace(/\s+/g, ' ').trim();
|
|
43
|
+
return collapsed.length > maxChars ? collapsed.slice(0, maxChars) : collapsed;
|
|
44
|
+
}
|
|
45
|
+
export function normalizeTodos(raw, max) {
|
|
46
|
+
if (!Array.isArray(raw))
|
|
47
|
+
return { items: [], requested: 0, repairs: [] };
|
|
48
|
+
const requested = raw.length;
|
|
49
|
+
const repairs = [];
|
|
50
|
+
const kept = [];
|
|
51
|
+
let dropped = 0;
|
|
52
|
+
for (const entry of raw) {
|
|
53
|
+
if (!entry || typeof entry !== 'object') {
|
|
54
|
+
dropped += 1;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const src = entry;
|
|
58
|
+
const content = clean(src.content, TODO_LIMITS.contentChars);
|
|
59
|
+
// The one field with no repair: an item with nothing to say is not an item.
|
|
60
|
+
if (content.length === 0) {
|
|
61
|
+
dropped += 1;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const activeForm = clean(src.activeForm, TODO_LIMITS.activeFormChars) || content;
|
|
65
|
+
const status = (typeof src.status === 'string' && STATUSES.has(src.status) ? src.status : 'pending');
|
|
66
|
+
kept.push({ content, activeForm, status });
|
|
67
|
+
}
|
|
68
|
+
if (dropped > 0) {
|
|
69
|
+
repairs.push(`${dropped} ${dropped === 1 ? 'item was' : 'items were'} dropped: each needs a non-empty content string.`);
|
|
70
|
+
}
|
|
71
|
+
// DROP FIRST, CAP SECOND — the ordering `normalizeSubagentSpecs` records:
|
|
72
|
+
// capping first would let a blank entry consume a slot the model meant for
|
|
73
|
+
// real work, and nothing would say so.
|
|
74
|
+
const ceiling = Math.max(1, Math.min(Math.floor(max) || 1, TODO_LIMITS.maxItems));
|
|
75
|
+
const items = kept.slice(0, ceiling);
|
|
76
|
+
if (kept.length > ceiling) {
|
|
77
|
+
// COUNTED OFF `kept`, NOT `requested`, because the two differ exactly when
|
|
78
|
+
// the payload ALSO had unusable entries — and those were already reported
|
|
79
|
+
// by the note above. `requested - ceiling` would charge the cap for them a
|
|
80
|
+
// second time and, worse, name a number that is not true of any suffix:
|
|
81
|
+
// "the last 5 were dropped" when the cap took 3 from the end and two blanks
|
|
82
|
+
// came from the middle. The model reads these notes to correct itself, so
|
|
83
|
+
// the arithmetic has to describe what actually happened.
|
|
84
|
+
repairs.push(`${requested} items were sent; the last ${kept.length - ceiling} were dropped (maximum ${ceiling}).`);
|
|
85
|
+
}
|
|
86
|
+
repairExactlyOneActive(items, repairs);
|
|
87
|
+
return { items, requested, repairs };
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Enforce invariant I-3: exactly one item is `in_progress` whenever at least one
|
|
91
|
+
* item is not `completed`.
|
|
92
|
+
*
|
|
93
|
+
* The promotion half deliberately fires on the INITIAL all-pending write. A
|
|
94
|
+
* model that writes its plan and then starts step 1 is doing the right thing,
|
|
95
|
+
* and the panel should say so on the first frame rather than one tool call
|
|
96
|
+
* later.
|
|
97
|
+
*/
|
|
98
|
+
function repairExactlyOneActive(items, repairs) {
|
|
99
|
+
const active = [];
|
|
100
|
+
for (let i = 0; i < items.length; i += 1) {
|
|
101
|
+
if (items[i].status === 'in_progress')
|
|
102
|
+
active.push(i);
|
|
103
|
+
}
|
|
104
|
+
if (active.length > 1) {
|
|
105
|
+
for (const index of active.slice(1)) {
|
|
106
|
+
items[index] = { ...items[index], status: 'pending' };
|
|
107
|
+
}
|
|
108
|
+
repairs.push(`${active.length} items were marked in_progress; only the first was kept.`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (active.length === 1)
|
|
112
|
+
return;
|
|
113
|
+
const firstUnfinished = items.findIndex((item) => item.status !== 'completed');
|
|
114
|
+
if (firstUnfinished >= 0) {
|
|
115
|
+
items[firstUnfinished] = { ...items[firstUnfinished], status: 'in_progress' };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `selectTodoRows` — which items the rail shows (todo-plan-execution §3.10).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY: `src/todo/**` is inside the glyph scanner's scope.
|
|
5
|
+
*
|
|
6
|
+
* PURE WINDOWING, NO OFFSET STATE ANYWHERE (non-goal 2 / R-10). Ink has no
|
|
7
|
+
* absolute positioning and this codebase already owns one hand-rolled viewport
|
|
8
|
+
* (`ScrollViewport`) whose invariants took a whole spec to pin down; a second
|
|
9
|
+
* scrollable region with its own offset, its own intent nonce and its own wheel
|
|
10
|
+
* band would be the largest source of new bugs in this feature by a wide margin.
|
|
11
|
+
* The rail therefore never scrolls - it windows around the item being worked on
|
|
12
|
+
* and reports what is off each end.
|
|
13
|
+
*
|
|
14
|
+
* THE DIRECT DESCENDANT OF `selectPanelRows`'s F-2 LESSON: the team panel
|
|
15
|
+
* originally took the first five children by ARRAY INDEX, which showed the five
|
|
16
|
+
* that finished FIRST and hid everything still working. Here the equivalent
|
|
17
|
+
* mistake is `slice(0, maxRows)`, which on a 20-item plan shows steps 1-8 - all
|
|
18
|
+
* completed - and hides the one the user is actually watching.
|
|
19
|
+
*/
|
|
20
|
+
import type { TodoItem } from './types.js';
|
|
21
|
+
export interface TodoRowSelection {
|
|
22
|
+
visible: {
|
|
23
|
+
item: TodoItem;
|
|
24
|
+
index: number;
|
|
25
|
+
}[];
|
|
26
|
+
hiddenAbove: number;
|
|
27
|
+
hiddenBelow: number;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The row the window is built around: the item in progress, or the first
|
|
31
|
+
* unfinished one, or the last item when everything is done.
|
|
32
|
+
*
|
|
33
|
+
* `normalizeTodos` guarantees an `in_progress` item whenever one is not
|
|
34
|
+
* completed (I-3), so the second branch is a belt-and-braces path for a list
|
|
35
|
+
* this module was handed without going through the store.
|
|
36
|
+
*/
|
|
37
|
+
export declare function todoAnchorIndex(items: TodoItem[]): number;
|
|
38
|
+
/**
|
|
39
|
+
* @param maxRows Rows available for the item rows AND the two overflow markers
|
|
40
|
+
* together. `todoRailRows()` upstream is what makes this number
|
|
41
|
+
* honest while a team dispatch is live (I-10 / P1-3).
|
|
42
|
+
*
|
|
43
|
+
* THE ANCHOR IS ALWAYS VISIBLE, and that beats the row bound when the two
|
|
44
|
+
* conflict. They can only conflict below `maxRows` 3 with overflow on BOTH
|
|
45
|
+
* sides — one item plus two markers is three rows — where this returns the
|
|
46
|
+
* anchor and lets the caller's `overflow="hidden"` clip a marker. Hiding the
|
|
47
|
+
* one row the user is watching in order to keep a `+N above` would be the wrong
|
|
48
|
+
* trade. `TODO_LIMITS.panelMinRows` (6) gates the mount and leaves `TodoPanel`
|
|
49
|
+
* at least 4 item rows, so the case is unreachable from the rail; a future
|
|
50
|
+
* caller with a smaller budget should read this paragraph before assuming
|
|
51
|
+
* otherwise.
|
|
52
|
+
*/
|
|
53
|
+
export declare function selectTodoRows(items: TodoItem[], maxRows: number): TodoRowSelection;
|