@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.
Files changed (100) hide show
  1. package/CHANGELOG.md +747 -577
  2. package/README.md +1163 -995
  3. package/dist/agent/controller.d.ts +71 -1
  4. package/dist/agent/controller.js +135 -1
  5. package/dist/agent/entry-limits.d.ts +65 -0
  6. package/dist/agent/entry-limits.js +133 -0
  7. package/dist/agent/headless.d.ts +29 -0
  8. package/dist/agent/headless.js +86 -1
  9. package/dist/agent/reducer.d.ts +53 -0
  10. package/dist/agent/reducer.js +145 -14
  11. package/dist/agent/system-prompt.d.ts +8 -0
  12. package/dist/agent/system-prompt.js +2 -0
  13. package/dist/cli.js +76 -3
  14. package/dist/commands/builtins.js +171 -0
  15. package/dist/commands/perf.d.ts +45 -0
  16. package/dist/commands/perf.js +78 -0
  17. package/dist/commands/registry.d.ts +27 -1
  18. package/dist/config/env.d.ts +3 -1
  19. package/dist/config/env.js +74 -1
  20. package/dist/config/load.d.ts +41 -0
  21. package/dist/config/load.js +67 -2
  22. package/dist/config/schema.d.ts +108 -8
  23. package/dist/config/schema.js +81 -6
  24. package/dist/config/store.js +10 -1
  25. package/dist/session/persist.d.ts +19 -1
  26. package/dist/session/persist.js +21 -2
  27. package/dist/team/subagent.js +11 -0
  28. package/dist/todo/follow-through.d.ts +117 -0
  29. package/dist/todo/follow-through.js +186 -0
  30. package/dist/todo/limits.d.ts +114 -0
  31. package/dist/todo/limits.js +114 -0
  32. package/dist/todo/normalize.d.ts +33 -0
  33. package/dist/todo/normalize.js +117 -0
  34. package/dist/todo/panel-rows.d.ts +53 -0
  35. package/dist/todo/panel-rows.js +103 -0
  36. package/dist/todo/prompt.d.ts +54 -0
  37. package/dist/todo/prompt.js +76 -0
  38. package/dist/todo/store.d.ts +86 -0
  39. package/dist/todo/store.js +183 -0
  40. package/dist/todo/todo-tool.d.ts +38 -0
  41. package/dist/todo/todo-tool.js +134 -0
  42. package/dist/todo/types.d.ts +62 -0
  43. package/dist/todo/types.js +12 -0
  44. package/dist/tools/index.d.ts +16 -2
  45. package/dist/tools/index.js +13 -0
  46. package/dist/ui/App.js +328 -14
  47. package/dist/ui/Markdown.d.ts +7 -1
  48. package/dist/ui/Markdown.js +15 -48
  49. package/dist/ui/StatusBar.d.ts +31 -0
  50. package/dist/ui/StatusBar.js +7 -2
  51. package/dist/ui/TodoPanel.d.ts +48 -0
  52. package/dist/ui/TodoPanel.js +74 -0
  53. package/dist/ui/TodoStrip.d.ts +36 -0
  54. package/dist/ui/TodoStrip.js +32 -0
  55. package/dist/ui/Transcript.d.ts +56 -4
  56. package/dist/ui/Transcript.js +123 -11
  57. package/dist/ui/entries/AssistantEntry.d.ts +18 -1
  58. package/dist/ui/entries/AssistantEntry.js +33 -3
  59. package/dist/ui/entries/TeamCard.d.ts +9 -1
  60. package/dist/ui/entries/TeamCard.js +22 -1
  61. package/dist/ui/entries/TodoCard.d.ts +39 -0
  62. package/dist/ui/entries/TodoCard.js +57 -0
  63. package/dist/ui/entries/ToolCard.d.ts +13 -1
  64. package/dist/ui/entries/ToolCard.js +39 -5
  65. package/dist/ui/entries/ToolPreview.d.ts +2 -2
  66. package/dist/ui/entries/UserEntry.d.ts +9 -1
  67. package/dist/ui/entries/UserEntry.js +17 -1
  68. package/dist/ui/exit-snapshot.d.ts +7 -0
  69. package/dist/ui/glyphs.d.ts +12 -0
  70. package/dist/ui/glyphs.js +8 -0
  71. package/dist/ui/layout/AppShell.d.ts +34 -1
  72. package/dist/ui/layout/AppShell.js +3 -3
  73. package/dist/ui/layout/MeasuredEntry.d.ts +21 -0
  74. package/dist/ui/layout/MeasuredEntry.js +30 -0
  75. package/dist/ui/layout/ScrollViewport.d.ts +13 -1
  76. package/dist/ui/layout/ScrollViewport.js +10 -4
  77. package/dist/ui/layout/rail.d.ts +56 -0
  78. package/dist/ui/layout/rail.js +68 -0
  79. package/dist/ui/layout/viewport-geometry.d.ts +28 -0
  80. package/dist/ui/layout/viewport-geometry.js +27 -0
  81. package/dist/ui/layout/virtual-window.d.ts +100 -0
  82. package/dist/ui/layout/virtual-window.js +204 -0
  83. package/dist/ui/live-clamp.d.ts +42 -0
  84. package/dist/ui/live-clamp.js +74 -0
  85. package/dist/ui/markdown-blocks.d.ts +25 -1
  86. package/dist/ui/markdown-blocks.js +43 -0
  87. package/dist/ui/overlays/HelpOverlay.js +4 -0
  88. package/dist/ui/render-cache.d.ts +81 -0
  89. package/dist/ui/render-cache.js +195 -0
  90. package/dist/ui/render-governor.d.ts +54 -0
  91. package/dist/ui/render-governor.js +78 -0
  92. package/dist/ui/transcript-text.d.ts +11 -0
  93. package/dist/ui/transcript-text.js +5 -0
  94. package/dist/ui/use-height-store.d.ts +31 -0
  95. package/dist/ui/use-height-store.js +76 -0
  96. package/dist/ui/use-render-governor.d.ts +39 -0
  97. package/dist/ui/use-render-governor.js +57 -0
  98. package/dist/ui/use-startup-notices.d.ts +9 -1
  99. package/dist/ui/use-startup-notices.js +35 -1
  100. package/package.json +3 -2
@@ -0,0 +1,134 @@
1
+ /**
2
+ * `todo_write` — the ONLY writer of `TodoStore` (todo-plan-execution §3.3 /
3
+ * §4.1).
4
+ *
5
+ * ASCII ONLY: `src/todo/**` is inside the glyph scanner's scope.
6
+ *
7
+ * FULL-LIST REPLACEMENT, NO INCREMENTAL OPERATIONS (D-1). A delta protocol
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.
12
+ *
13
+ * 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.
17
+ *
18
+ * PURE STATE: no filesystem, no network, no child process, no human wait. It
19
+ * therefore must NOT be routed through `withPausedWatchdog` (C-9) -
20
+ * `IdleWatchdog.pause()` is a boolean rather than a counter, so a nested
21
+ * pause/resume around an instant call would re-arm the watchdog inside an outer
22
+ * human wait.
23
+ */
24
+ import { errorResult, textResult } from '@aragon-agent/core';
25
+ import { TODO_LIMITS } from './limits.js';
26
+ /**
27
+ * Turned off mid-session. A NON-ERROR result on purpose: the user turned a
28
+ * display off, the model did nothing wrong, and an error result invites a retry.
29
+ */
30
+ export const TODO_OFF_REFUSAL = 'Todo planning is off for this session. Just do the work and say what you did.';
31
+ /** The structural half of R-c: a one-step list is not a plan. */
32
+ export const TOO_SMALL_REFUSAL = 'A one-step list is not a plan. Just do the work and summarize when you are done.';
33
+ /**
34
+ * The model-facing description. Deliberately close to Claude Code's `TodoWrite`
35
+ * (R-f): what a model already knows about that tool transfers directly, and a
36
+ * model's priors are worth more than our naming taste - the same argument
37
+ * `team/task-tool.ts` records for mirroring `Task`.
38
+ */
39
+ const TODO_WRITE_DESCRIPTION = 'Record and update the plan for a multi-step task. Send the complete list every time - ' +
40
+ 'this tool replaces the whole list. Use it for work with three or more distinct steps, ' +
41
+ 'and skip it entirely for anything you can finish in one or two. Exactly one item may be ' +
42
+ '`in_progress`; mark an item `completed` and the next one `in_progress` in the same call, ' +
43
+ `the moment the step is done. At most ${TODO_LIMITS.maxItems} items; extras are dropped, so ` +
44
+ 'keep within it. This list is how the user follows a long task, so keep it current.';
45
+ export function createTodoTool(deps) {
46
+ return {
47
+ name: 'todo_write',
48
+ label: 'Update todos',
49
+ description: TODO_WRITE_DESCRIPTION,
50
+ parameters: {
51
+ type: 'object',
52
+ properties: {
53
+ todos: {
54
+ type: 'array',
55
+ description: 'The COMPLETE list, every time. This replaces the previous list; there is ' +
56
+ 'no add or update operation.',
57
+ items: {
58
+ type: 'object',
59
+ properties: {
60
+ content: {
61
+ type: 'string',
62
+ description: `Imperative, <= ${TODO_LIMITS.contentChars} chars. What the step is. ` +
63
+ 'e.g. "Add the rail to AppShell".',
64
+ },
65
+ activeForm: {
66
+ type: 'string',
67
+ description: `Present continuous, <= ${TODO_LIMITS.activeFormChars} chars, shown while ` +
68
+ 'this step is in progress. e.g. "Adding the rail to AppShell".',
69
+ },
70
+ status: {
71
+ type: 'string',
72
+ description: 'pending | in_progress | completed. Exactly one item may be in_progress.',
73
+ },
74
+ },
75
+ required: ['content', 'status'],
76
+ },
77
+ },
78
+ },
79
+ required: ['todos'],
80
+ },
81
+ async execute(_id, params) {
82
+ // 1. Turned off mid-session. The tool array is immutable for the life of
83
+ // the session (C-1), so `/todo off` cannot unregister - it flips this
84
+ // closure instead.
85
+ if (!deps.isEnabled()) {
86
+ deps.store.reject(TODO_OFF_REFUSAL);
87
+ return textResult(TODO_OFF_REFUSAL);
88
+ }
89
+ const todos = params.todos;
90
+ // 2. GATED ON AN EMPTY STORE, and that gate is the whole subtlety.
91
+ // Shrinking an EXISTING list to one item is legitimate - the model
92
+ // merged two steps - so the guard must fire only on a fresh plan.
93
+ if (deps.store.isEmpty() &&
94
+ Array.isArray(todos) &&
95
+ todos.length < TODO_LIMITS.minFreshItems) {
96
+ deps.store.reject(TOO_SMALL_REFUSAL);
97
+ return textResult(TOO_SMALL_REFUSAL);
98
+ }
99
+ // 3. Repair, never reject. Zero survivors is the single hard failure, and
100
+ // it leaves the previous list untouched (AC-9).
101
+ const result = deps.store.write(todos);
102
+ if (result.items.length === 0) {
103
+ return errorResult('No usable todo items: each needs a non-empty content string.');
104
+ }
105
+ return textResult(buildResultText(result.items, result.repairs));
106
+ },
107
+ };
108
+ }
109
+ /**
110
+ * The tool result — THE SINGLE STRONGEST LEVER ON R-b ("one at a time"), because
111
+ * unlike the system prompt it is re-read on every single call. There is
112
+ * deliberately NO RATE LIMIT to compete with it: a cap would fire exactly when
113
+ * the model is being most diligent and produce a refusal it cannot act on, and
114
+ * the costs a cap would have contained are already gone (§3.8 collapses N calls
115
+ * into ONE transcript entry, and the store is memory-only).
116
+ */
117
+ function buildResultText(items, repairs) {
118
+ const done = items.filter((item) => item.status === 'completed').length;
119
+ const activeIndex = items.findIndex((item) => item.status === 'in_progress');
120
+ const lines = [`Todos updated. ${done}/${items.length} done.`];
121
+ if (activeIndex >= 0) {
122
+ lines.push(`In progress: ${items[activeIndex].activeForm}`);
123
+ const next = items.slice(activeIndex + 1).find((item) => item.status === 'pending');
124
+ if (next)
125
+ lines.push(`Next: ${next.content}`);
126
+ lines.push('Call todo_write again the moment that item is finished - exactly one item in', 'progress at a time, and do not batch several completions into one call.');
127
+ }
128
+ else {
129
+ lines.push('Every item is complete. Tell the user what you did.');
130
+ }
131
+ for (const repair of repairs)
132
+ lines.push(`Note: ${repair}`);
133
+ return lines.join('\n');
134
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Runtime shapes for the todo subsystem (todo-plan-execution §5.1).
3
+ *
4
+ * ASCII ONLY: `src/todo/**` is inside the glyph scanner's scope.
5
+ *
6
+ * `TodoEvent` is CLI-LOCAL and is deliberately NOT a member of core's
7
+ * `AgentEvent` union (D-11), exactly as `TeamEvent` is not: core freezes its
8
+ * runtime export list (`public-api.test.ts`) and forbids host coupling
9
+ * (`no-host-coupling.test.ts`), so a new event member would break the first and
10
+ * teach core about a host shape it has no business defining.
11
+ */
12
+ /**
13
+ * Re-exported so `config/schema.ts` can name the mode without importing the
14
+ * decision module (todo-plan-followthrough §3.1). TYPE-ONLY, so the apparent
15
+ * cycle `types -> follow-through -> types` is erased at compile time and there
16
+ * is no runtime import at all.
17
+ */
18
+ export type { FollowThroughMode } from './follow-through.js';
19
+ export type TodoStatus = 'pending' | 'in_progress' | 'completed';
20
+ export interface TodoItem {
21
+ /** Imperative, sanitized, <= TODO_LIMITS.contentChars. */
22
+ content: string;
23
+ /** Present continuous, shown while in progress. Falls back to `content`. */
24
+ activeForm: string;
25
+ status: TodoStatus;
26
+ }
27
+ /** The immutable projection the UI renders. Rebuilt on every write. */
28
+ export interface TodoSnapshot {
29
+ items: TodoItem[];
30
+ total: number;
31
+ doneCount: number;
32
+ /** Index of the single in_progress item, or -1 when everything is completed. */
33
+ activeIndex: number;
34
+ /**
35
+ * Stamped by an INJECTED clock (`TodoStore`'s constructor takes
36
+ * `now?: () => number`, defaulting to `Date.now`), for the reason `TeamPanel`
37
+ * takes a `now?` prop: a snapshot test that reads the wall clock is a test
38
+ * that fails on a slow machine and nowhere else (P2-4).
39
+ */
40
+ updatedAt: number;
41
+ }
42
+ /**
43
+ * Why a list went away.
44
+ *
45
+ * `user` - `/todo clear`.
46
+ * `reset` - `/reset`, or a `/resume` whose file carried no list (§3.13).
47
+ * `turn` - every item was completed when the next user turn started.
48
+ * `stale` - unfinished, and unwritten for more than `TODO_LIMITS.staleTurns`
49
+ * consecutive turns (§3.2 / P1-7).
50
+ */
51
+ export type TodoClearReason = 'user' | 'reset' | 'turn' | 'stale';
52
+ export type TodoEvent = {
53
+ type: 'updated';
54
+ snapshot: TodoSnapshot;
55
+ } | {
56
+ type: 'rejected';
57
+ reason: string;
58
+ } | {
59
+ type: 'cleared';
60
+ reason: TodoClearReason;
61
+ };
62
+ export type TodoEventListener = (event: TodoEvent) => void;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Runtime shapes for the todo subsystem (todo-plan-execution §5.1).
3
+ *
4
+ * ASCII ONLY: `src/todo/**` is inside the glyph scanner's scope.
5
+ *
6
+ * `TodoEvent` is CLI-LOCAL and is deliberately NOT a member of core's
7
+ * `AgentEvent` union (D-11), exactly as `TeamEvent` is not: core freezes its
8
+ * runtime export list (`public-api.test.ts`) and forbids host coupling
9
+ * (`no-host-coupling.test.ts`), so a new event member would break the first and
10
+ * teach core about a host shape it has no business defining.
11
+ */
12
+ export {};
@@ -85,6 +85,20 @@ export interface BuiltinToolsOptions {
85
85
  * burns a turn.
86
86
  */
87
87
  planTools?: AgentTool[];
88
+ /**
89
+ * `todo_write`, appended verbatim (todo-plan-execution §3.6).
90
+ *
91
+ * THROUGH THE FACTORY FOR THE REASON `teamTools` RECORDS BELOW (C-2):
92
+ * `tools.test.ts::C7` asserts `HOST_TOOL_NAMES` equals what this factory
93
+ * PRODUCES, so appending `todo_write` in `AgentController` after the factory
94
+ * returned would turn C7 red with a message about two lists of names that
95
+ * says nothing about todos.
96
+ *
97
+ * An empty array (the `--no-todo` path) leaves the tool list byte-identical to
98
+ * the pre-todo build — AC-30, provable by object identity exactly as
99
+ * `--no-skills` and `--no-team` are.
100
+ */
101
+ todoTools?: AgentTool[];
88
102
  /**
89
103
  * Team-mode tools, appended verbatim. The lead gets `[task]`; a subagent gets
90
104
  * `[team_send, team_wait]`.
@@ -134,7 +148,7 @@ export interface BuiltinToolsOptions {
134
148
  * is the whole of what this mechanism is worth, and saying so plainly beats
135
149
  * implying it covers more.
136
150
  */
137
- export declare const SKILL_TOOL_FLOOR: readonly ["read_file", "list_dir", "glob", "grep", "skill", "skill_find", "ask_user", "submit_plan", "task"];
151
+ export declare const SKILL_TOOL_FLOOR: readonly ["read_file", "list_dir", "glob", "grep", "skill", "skill_find", "ask_user", "submit_plan", "task", "todo_write"];
138
152
  /**
139
153
  * The five tools plan mode refuses: exactly the complement of `SKILL_TOOL_FLOOR`
140
154
  * within `HOST_TOOL_NAMES` — "the five that change something", the set this file
@@ -167,5 +181,5 @@ export declare const PLAN_MODE_BLOCKED_TOOLS: ReadonlySet<string>;
167
181
  * declaration MEANS, and that reading must not change with how the CLI was
168
182
  * started.
169
183
  */
170
- export declare const HOST_TOOL_NAMES: readonly ["read_file", "write_file", "edit_file", "list_dir", "glob", "grep", "bash", "skill", "skill_find", "skill_install", "skill_create", "ask_user", "submit_plan", "task"];
184
+ export declare const HOST_TOOL_NAMES: readonly ["read_file", "write_file", "edit_file", "list_dir", "glob", "grep", "bash", "skill", "skill_find", "skill_install", "skill_create", "ask_user", "submit_plan", "todo_write", "task"];
171
185
  export declare function createBuiltinTools(options: BuiltinToolsOptions): AgentTool[];
@@ -52,6 +52,13 @@ export const SKILL_TOOL_FLOOR = [
52
52
  // level down, with the child inheriting `agentMode` through the same closure.
53
53
  // Blocking it would forfeit parallel research, which is plan mode's best use.
54
54
  'task',
55
+ // `todo_write` joins the floor rather than the blocked set (D-12), and the
56
+ // partition test below is what forces that choice to be conscious. A todo list
57
+ // is a DISPLAY: it writes no file and runs no shell. More concretely, a skill
58
+ // whose `allowed-tools` omitted it would make the planning UI unreachable
59
+ // inside that skill's frame — the identical dead end that put `ask_user` /
60
+ // `submit_plan` / `task` here.
61
+ 'todo_write',
55
62
  ];
56
63
  /**
57
64
  * The five tools plan mode refuses: exactly the complement of `SKILL_TOOL_FLOOR`
@@ -105,6 +112,7 @@ export const HOST_TOOL_NAMES = [
105
112
  'skill_create',
106
113
  'ask_user',
107
114
  'submit_plan',
115
+ 'todo_write',
108
116
  // `team_send` / `team_wait` are DELIBERATELY ABSENT (P2-5). This constant
109
117
  // answers "can a skill's `allowed-tools` declaration ever take effect?", and
110
118
  // those two are never registered on a lead — so a skill naming them genuinely
@@ -227,6 +235,11 @@ export function createBuiltinTools(options) {
227
235
  if (options.planTools && options.planTools.length > 0) {
228
236
  tools = [...tools, ...options.planTools];
229
237
  }
238
+ // AFTER `planTools`, BEFORE `teamTools`, so a lead's `task` stays last and
239
+ // `HOST_TOOL_NAMES` changes by one insertion rather than a reshuffle.
240
+ if (options.todoTools && options.todoTools.length > 0) {
241
+ tools = [...tools, ...options.todoTools];
242
+ }
230
243
  // BEFORE both wrappers, so the lead's `task` is covered by the plan gate and
231
244
  // the ceiling like everything else, and a child's comm tools are covered by
232
245
  // everything except the ceiling (see `policyExempt`).