@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
@@ -13,6 +13,7 @@
13
13
  import type { AgentEvent, ModelCost, ToolResult, TokenUsage } from '@aragon-agent/core';
14
14
  import type { AgentMode } from './agent-mode.js';
15
15
  import type { DispatchOutcome, SubagentRun, SubagentSpec, TeamSnapshot } from '../team/types.js';
16
+ import type { TodoItem, TodoSnapshot } from '../todo/types.js';
16
17
  export type NoticeLevel = 'info' | 'warn' | 'error';
17
18
  export type ToastLevel = NoticeLevel | 'success';
18
19
  export type Overlay = null | 'settings' | 'model' | 'help' | 'confirm' | 'question' | 'plan';
@@ -78,6 +79,34 @@ export type Entry = {
78
79
  aborted: boolean;
79
80
  durationMs?: number;
80
81
  active: boolean;
82
+ }
83
+ /**
84
+ * The turn's todo checklist (todo-plan-execution §3.8). ONE per user turn,
85
+ * rewritten in place — a 7-item list updated twice per item would otherwise be
86
+ * 14 near-identical tool cards in the one surface the user reads most.
87
+ *
88
+ * JSON-SERIALIZABLE by construction, so `/save` needs no format change.
89
+ *
90
+ * `live: true` is normalized away ON LOAD, in `session/persist.ts`, for the
91
+ * same reason the team entry's `active` is (C-5): `Transcript`'s settled
92
+ * boundary is MONOTONIC, so an entry that never settles never reaches
93
+ * `<Static>` and is re-rendered on every frame for the rest of the session.
94
+ */
95
+ | {
96
+ id: string;
97
+ kind: 'todo';
98
+ items: TodoItem[];
99
+ doneCount: number;
100
+ total: number;
101
+ /** The turn that owns this card is still running, so it may be rewritten. */
102
+ live: boolean;
103
+ /**
104
+ * Set by `normalizeLoadedEntries` on a card saved while its run was live
105
+ * (P2-6). The todo analogue of the team entry's `aborted`, and true for the
106
+ * same reason: the run behind it died with the process. Absent on every
107
+ * card this session produced.
108
+ */
109
+ interrupted?: boolean;
81
110
  };
82
111
  export interface UsageTotal {
83
112
  inputTokens: number;
@@ -112,14 +141,33 @@ export interface ViewState {
112
141
  * live dispatch to show (§6.4).
113
142
  */
114
143
  team: TeamSnapshot | null;
144
+ /**
145
+ * The live todo list, or `null` when there is none.
146
+ *
147
+ * NEVER PERSISTED HERE — `entries` is the history and `SavedSession.todos` is
148
+ * the list (§3.13). `null` is what leaves the rail unmounted entirely, which
149
+ * is why a session that never plans has byte-identical layout to a pre-feature
150
+ * build (AC-24).
151
+ */
152
+ todos: TodoSnapshot | null;
115
153
  seq: number;
116
154
  /** The `kind: 'team'` entry the live dispatch is writing into. */
117
155
  teamEntryId?: string;
156
+ /** The `kind: 'todo'` entry this turn is writing into. */
157
+ todoEntryId?: string;
118
158
  streamingId?: string;
119
159
  turnProduced: boolean;
120
160
  errorNoticed: boolean;
121
161
  /** The current run was aborted by the user (suppresses the failure guard). */
122
162
  aborted: boolean;
163
+ /**
164
+ * Entries removed by `transcriptRetain` (tui-render-performance L1).
165
+ *
166
+ * Rendered by `transcript-text` and reported by `/perf`; NEVER PERSISTED —
167
+ * `SavedSession` keeps `{ model, messages, entries, todos }` exactly as it is,
168
+ * so `/save` and `/resume` need no format change and old files load unchanged.
169
+ */
170
+ droppedEntries: number;
123
171
  }
124
172
  export declare function initialViewState(): ViewState;
125
173
  export type ViewAction = {
@@ -213,6 +261,11 @@ export type ViewAction = {
213
261
  } | {
214
262
  type: 'teamEnd';
215
263
  outcome: DispatchOutcome;
264
+ } | {
265
+ type: 'todoUpdate';
266
+ snapshot: TodoSnapshot;
267
+ } | {
268
+ type: 'todoCleared';
216
269
  };
217
270
  export declare const STORED_PREVIEW_CHARS = 8000;
218
271
  export declare const STORED_PREVIEW_LINES = 200;
@@ -10,7 +10,9 @@
10
10
  * - `agent_end` after a turn that produced no `turn_end` and no error notice
11
11
  * synthesizes a generic error notice (a swallowed throw is never a no-op).
12
12
  */
13
+ import process from 'node:process';
13
14
  import { computeCost } from './usage.js';
15
+ import { ENTRY_LIMITS, appendBounded, entryRetain, trimEntries } from './entry-limits.js';
14
16
  /** Default auto-dismiss window for a toast. */
15
17
  export const DEFAULT_TOAST_TTL_MS = 2500;
16
18
  export function initialViewState() {
@@ -28,12 +30,15 @@ export function initialViewState() {
28
30
  agentMode: 'build',
29
31
  pendingAgentMode: null,
30
32
  team: null,
33
+ todos: null,
31
34
  seq: 0,
32
35
  teamEntryId: undefined,
36
+ todoEntryId: undefined,
33
37
  streamingId: undefined,
34
38
  turnProduced: false,
35
39
  errorNoticed: false,
36
40
  aborted: false,
41
+ droppedEntries: 0,
37
42
  };
38
43
  }
39
44
  // ---------------------------------------------------------------------------
@@ -118,6 +123,20 @@ export function reduceEvent(event, cost) {
118
123
  return [];
119
124
  }
120
125
  }
126
+ /**
127
+ * Tools that render their own transcript entry, so the generic tool card would
128
+ * be a duplicate. EXACTLY ONE MEMBER; read todo-plan-execution §3.8 before
129
+ * adding a second.
130
+ *
131
+ * SUPPRESSING ONLY `tool_call_start` IS SUFFICIENT, AND THAT IS THE WHOLE REASON
132
+ * THIS IS SAFE (D-10). Every later handler — `toolCallDelta` / `toolCallEnd` /
133
+ * `toolExecStart` / `toolExecEnd` — resolves its target through
134
+ * `findToolEntryId`, which returns `undefined` when no entry was created, and
135
+ * every one of them already early-returns on that. There is no fifth handler. A
136
+ * second suppression rule would be four more places to get the id matching
137
+ * wrong, for nothing.
138
+ */
139
+ const SELF_RENDERING_TOOLS = new Set(['todo_write']);
121
140
  function reduceStreamEvent(se) {
122
141
  switch (se.type) {
123
142
  case 'thinking_start':
@@ -127,6 +146,8 @@ function reduceStreamEvent(se) {
127
146
  case 'text_delta':
128
147
  return [{ type: 'textDelta', delta: se.delta }];
129
148
  case 'tool_call_start':
149
+ if (SELF_RENDERING_TOOLS.has(se.toolName))
150
+ return [];
130
151
  return [
131
152
  { type: 'toolCallStart', toolCallId: se.toolCallId, toolName: se.toolName },
132
153
  ];
@@ -183,6 +204,35 @@ function nextId(state) {
183
204
  function mapEntry(entries, id, fn) {
184
205
  return entries.map((e) => (e.id === id ? fn(e) : e));
185
206
  }
207
+ /**
208
+ * Append one entry and ring-trim the result (tui-render-performance L1).
209
+ *
210
+ * TRIMMING HAPPENS ONLY AT APPEND POINTS, and that is the whole reason this is a
211
+ * helper rather than a line in `viewReducer`. A mid-turn trim triggered by an
212
+ * UPDATE action could drop the entry `streamingId` / `teamEntryId` /
213
+ * `todoEntryId` points at while it is still being written to; appending is the
214
+ * only moment at which the tail is by definition the newest thing.
215
+ *
216
+ * I-L1-1 — the trim must never drop a live target. With `retain >= 200` and a
217
+ * trim that only ever removes from the head this is structurally true, but a
218
+ * future clamp change would break it SILENTLY (the `mapEntry` calls would simply
219
+ * stop matching), so development builds assert it.
220
+ */
221
+ function appendEntry(state, entries, entry) {
222
+ const trimmed = trimEntries([...entries, entry], entryRetain());
223
+ if (trimmed.dropped > 0 && process.env.NODE_ENV !== 'production') {
224
+ const live = [state.streamingId, state.teamEntryId, state.todoEntryId].filter((id) => typeof id === 'string');
225
+ for (const id of live) {
226
+ if (!trimmed.entries.some((e) => e.id === id)) {
227
+ throw new Error(`trimEntries dropped a live entry (${id}) - see I-L1-1`);
228
+ }
229
+ }
230
+ }
231
+ return {
232
+ entries: trimmed.entries,
233
+ droppedEntries: state.droppedEntries + trimmed.dropped,
234
+ };
235
+ }
186
236
  function findToolEntryId(state, toolCallId) {
187
237
  for (let i = state.entries.length - 1; i >= 0; i -= 1) {
188
238
  const e = state.entries[i];
@@ -199,8 +249,12 @@ export function viewReducer(state, action) {
199
249
  return {
200
250
  ...state,
201
251
  seq,
202
- entries: [...state.entries, entry],
252
+ ...appendEntry(state, state.entries, entry),
203
253
  streamingId: undefined,
254
+ // A new turn gets its own card, in that turn's position in the
255
+ // transcript. The PREVIOUS card stays exactly where it is — it is
256
+ // history, and history is not retracted.
257
+ todoEntryId: undefined,
204
258
  turnProduced: false,
205
259
  errorNoticed: false,
206
260
  aborted: false,
@@ -224,7 +278,7 @@ export function viewReducer(state, action) {
224
278
  thinkingOpen: false,
225
279
  streaming: true,
226
280
  };
227
- return { ...state, seq, entries: [...state.entries, entry], streamingId: id };
281
+ return { ...state, seq, ...appendEntry(state, state.entries, entry), streamingId: id };
228
282
  }
229
283
  case 'thinkingStart': {
230
284
  if (!state.streamingId)
@@ -240,7 +294,7 @@ export function viewReducer(state, action) {
240
294
  return {
241
295
  ...state,
242
296
  entries: mapEntry(state.entries, state.streamingId, (e) => e.kind === 'assistant'
243
- ? { ...e, thinking: (e.thinking ?? '') + action.delta }
297
+ ? { ...e, thinking: appendBounded(e.thinking ?? '', action.delta, ENTRY_LIMITS.thinking) }
244
298
  : e),
245
299
  };
246
300
  }
@@ -249,7 +303,9 @@ export function viewReducer(state, action) {
249
303
  return state;
250
304
  return {
251
305
  ...state,
252
- entries: mapEntry(state.entries, state.streamingId, (e) => e.kind === 'assistant' ? { ...e, text: e.text + action.delta } : e),
306
+ entries: mapEntry(state.entries, state.streamingId, (e) => e.kind === 'assistant'
307
+ ? { ...e, text: appendBounded(e.text, action.delta, ENTRY_LIMITS.text) }
308
+ : e),
253
309
  };
254
310
  }
255
311
  case 'toolCallStart': {
@@ -263,7 +319,7 @@ export function viewReducer(state, action) {
263
319
  argsRaw: '',
264
320
  status: 'pending',
265
321
  };
266
- return { ...state, seq, entries: [...state.entries, entry] };
322
+ return { ...state, seq, ...appendEntry(state, state.entries, entry) };
267
323
  }
268
324
  case 'toolCallDelta': {
269
325
  const id = findToolEntryId(state, action.toolCallId);
@@ -271,7 +327,9 @@ export function viewReducer(state, action) {
271
327
  return state;
272
328
  return {
273
329
  ...state,
274
- entries: mapEntry(state.entries, id, (e) => e.kind === 'tool' ? { ...e, argsRaw: e.argsRaw + action.argsDelta } : e),
330
+ entries: mapEntry(state.entries, id, (e) => e.kind === 'tool'
331
+ ? { ...e, argsRaw: appendBounded(e.argsRaw, action.argsDelta, ENTRY_LIMITS.argsRaw) }
332
+ : e),
275
333
  };
276
334
  }
277
335
  case 'toolCallEnd': {
@@ -336,17 +394,27 @@ export function viewReducer(state, action) {
336
394
  ? mapEntry(state.entries, state.streamingId, (e) => e.kind === 'assistant' ? { ...e, streaming: false } : e)
337
395
  : state.entries;
338
396
  let seq = state.seq;
397
+ let droppedEntries = state.droppedEntries;
339
398
  // Silent-failure guard (R2): a swallowed throw shows nothing otherwise.
340
399
  // A user abort is an expected empty run, not a failure — skip the guard.
341
400
  if (!state.turnProduced && !state.errorNoticed && !state.aborted) {
342
401
  const next = nextId({ ...state, seq });
343
402
  seq = next.seq;
344
- entries = [
345
- ...entries,
346
- { id: next.id, kind: 'notice', level: 'error', text: GENERIC_RUN_FAILURE },
347
- ];
403
+ const appended = appendEntry(state, entries, {
404
+ id: next.id,
405
+ kind: 'notice',
406
+ level: 'error',
407
+ text: GENERIC_RUN_FAILURE,
408
+ });
409
+ entries = appended.entries;
410
+ droppedEntries = appended.droppedEntries;
348
411
  }
349
- return { ...state, entries, seq, status: 'idle', streamingId: undefined };
412
+ // The card may now reach `<Static>` (C-5 / I-6): the turn that owns it is
413
+ // over, so nothing will rewrite it.
414
+ if (state.todoEntryId) {
415
+ entries = mapEntry(entries, state.todoEntryId, (e) => e.kind === 'todo' ? { ...e, live: false } : e);
416
+ }
417
+ return { ...state, entries, seq, droppedEntries, status: 'idle', streamingId: undefined };
350
418
  }
351
419
  case 'notice': {
352
420
  const { id, seq } = nextId(state);
@@ -354,7 +422,7 @@ export function viewReducer(state, action) {
354
422
  return {
355
423
  ...state,
356
424
  seq,
357
- entries: [...state.entries, entry],
425
+ ...appendEntry(state, state.entries, entry),
358
426
  errorNoticed: action.level === 'error' ? true : state.errorNoticed,
359
427
  };
360
428
  }
@@ -376,19 +444,35 @@ export function viewReducer(state, action) {
376
444
  return {
377
445
  ...state,
378
446
  entries: [],
447
+ // The count describes what the RETAIN RING removed from the visible
448
+ // transcript. `/clear` empties it by intent, so carrying the number
449
+ // over would make the exported transcript claim a loss that did not
450
+ // happen.
451
+ droppedEntries: 0,
379
452
  streamingId: undefined,
380
453
  expandedToolIds: {},
381
454
  teamEntryId: undefined,
455
+ // The entry it pointed at is gone, so the id goes with it. `todos` is
456
+ // DELIBERATELY UNTOUCHED (I-2): `/clear` wipes the transcript and not
457
+ // the conversation, so the model still believes in the plan, and a panel
458
+ // that disagrees with the model is worse than no panel.
459
+ todoEntryId: undefined,
382
460
  };
383
461
  case 'resetConversation':
384
462
  return {
385
463
  ...state,
386
464
  entries: [],
465
+ droppedEntries: 0,
387
466
  streamingId: undefined,
388
467
  expandedToolIds: {},
389
468
  contextTokens: 0,
390
469
  usageTotal: { inputTokens: 0, outputTokens: 0, costUsd: 0 },
391
470
  teamEntryId: undefined,
471
+ // `/reset` clears `messages`, so the belief is gone and the list goes
472
+ // with it — the other half of I-2. `controller.clearMessages()` clears
473
+ // the STORE; this clears the mirror.
474
+ todos: null,
475
+ todoEntryId: undefined,
392
476
  };
393
477
  case 'setOverlay':
394
478
  return { ...state, overlay: action.overlay };
@@ -444,7 +528,7 @@ export function viewReducer(state, action) {
444
528
  return {
445
529
  ...state,
446
530
  seq,
447
- entries: [...state.entries, entry],
531
+ ...appendEntry(state, state.entries, entry),
448
532
  teamEntryId: id,
449
533
  team: {
450
534
  dispatchId: action.dispatchId,
@@ -496,13 +580,60 @@ export function viewReducer(state, action) {
496
580
  // "(dang you de shi hou)": it renders only while a dispatch is running.
497
581
  return { ...state, entries, team: null, teamEntryId: undefined };
498
582
  }
583
+ // --- Todo planning (todo-plan-execution §5.2) -------------------------
584
+ case 'todoUpdate': {
585
+ const entry = (id) => ({
586
+ id,
587
+ kind: 'todo',
588
+ items: action.snapshot.items,
589
+ doneCount: action.snapshot.doneCount,
590
+ total: action.snapshot.total,
591
+ live: true,
592
+ });
593
+ // Rewrite the turn's card when there is one; otherwise append and record
594
+ // its id. One `mapEntry` on the KNOWN id, never a scan.
595
+ if (state.todoEntryId) {
596
+ const known = state.todoEntryId;
597
+ return {
598
+ ...state,
599
+ todos: action.snapshot,
600
+ entries: mapEntry(state.entries, known, (e) => e.kind === 'todo' ? { ...entry(known), live: e.live } : e),
601
+ };
602
+ }
603
+ const { id, seq } = nextId(state);
604
+ return {
605
+ ...state,
606
+ seq,
607
+ todos: action.snapshot,
608
+ ...appendEntry(state, state.entries, entry(id)),
609
+ todoEntryId: id,
610
+ };
611
+ }
612
+ // THE ENTRY STAYS. It is history, and history is not retracted — only the
613
+ // live projection goes away, which is what unmounts the rail.
614
+ case 'todoCleared':
615
+ return { ...state, todos: null, todoEntryId: undefined };
499
616
  case 'restoreEntries': {
500
617
  // Adopt a seq beyond any restored id to keep future ids unique.
501
618
  const maxSeq = action.entries.reduce((acc, e) => {
502
619
  const n = Number.parseInt(e.id.replace(/^e/, ''), 10);
503
620
  return Number.isFinite(n) ? Math.max(acc, n) : acc;
504
621
  }, state.seq);
505
- return { ...state, entries: [...action.entries], seq: maxSeq, teamEntryId: undefined };
622
+ // A session file can be longer than the retain ring; trimming it here is
623
+ // the one append-free place the bound still has to hold, and the count
624
+ // starts from this restore rather than inheriting the previous session's.
625
+ const restored = trimEntries([...action.entries], entryRetain());
626
+ return {
627
+ ...state,
628
+ entries: restored.entries,
629
+ droppedEntries: restored.dropped,
630
+ seq: maxSeq,
631
+ teamEntryId: undefined,
632
+ // As for `team`: the restored cards are history and no turn owns them.
633
+ // The LIST itself arrives separately, through `controller.restoreTodos`
634
+ // and the store's own event (§3.13).
635
+ todoEntryId: undefined,
636
+ };
506
637
  }
507
638
  default:
508
639
  return state;
@@ -53,6 +53,14 @@ export interface SystemPromptParams {
53
53
  * mixing them a one-character mistake.
54
54
  */
55
55
  subagentBlock?: string;
56
+ /**
57
+ * The rendered `<todo_planning>` block, or `''` (todo-plan-execution §3.7).
58
+ *
59
+ * Spliced conditionally exactly as `skillsBlock`, the plan block and
60
+ * `teamBlock` are, which is what keeps C-10 / I-5 true: `todo.enabled: false`
61
+ * (and `--no-todo`) produce the pre-todo prompt BYTE FOR BYTE.
62
+ */
63
+ todoBlock?: string;
56
64
  }
57
65
  /** Describe the active shell so the model does not emit incompatible syntax. */
58
66
  export declare function describeShell(): {
@@ -39,6 +39,7 @@ export function buildSystemPrompt(params) {
39
39
  : '';
40
40
  const teamBlock = params.teamBlock ?? '';
41
41
  const subagentBlock = params.subagentBlock ?? '';
42
+ const todoBlock = params.todoBlock ?? '';
42
43
  return [
43
44
  'You are AragonAgent, an autonomous coding assistant operating inside a terminal (TUI).',
44
45
  'You help the user accomplish software-engineering tasks by reasoning step by step and using the provided tools.',
@@ -54,6 +55,7 @@ export function buildSystemPrompt(params) {
54
55
  ...(planBlock ? ['', planBlock] : []),
55
56
  ...(teamBlock ? ['', teamBlock] : []),
56
57
  ...(subagentBlock ? ['', subagentBlock] : []),
58
+ ...(todoBlock ? ['', todoBlock] : []),
57
59
  '',
58
60
  'Operating guidance:',
59
61
  '- Prefer reading files and inspecting the workspace before making changes.',
package/dist/cli.js CHANGED
@@ -27,7 +27,7 @@ import { LOG_CONFIG_SET_KEYS, TEAM_CONFIG_SET_KEYS, applyLogConfigSet, applyTeam
27
27
  import { attachAgentEvents, attachTeamEvents, installLogging, setScreenRestore, setSignalTerminator, } from './logging/install.js';
28
28
  import { getLogger } from './logging/logger.js';
29
29
  import { runLogsCommand } from './logging/cli-commands.js';
30
- import { ADAPTER_PROVIDERS, clampDensity, clampTheme, clampThinkingLevel, clampAskRounds, clampHumanTimeout, clampSkillsIntegrity, clampSkillsToolPolicy, clampTranscriptWindow, coercePositiveInt, isAdapterProvider, parseMaxTokensInput, DEFAULT_MAX_TOKENS, DEFAULT_PLAN_HUMAN_TIMEOUT_MS, DEFAULT_PLAN_MAX_ASK_ROUNDS, DEFAULT_SKILLS_CONFIG, } from './config/schema.js';
30
+ import { ADAPTER_PROVIDERS, clampDensity, clampTheme, clampThinkingLevel, clampAskRounds, clampHumanTimeout, clampSkillsIntegrity, clampSkillsToolPolicy, clampMaxRenderInterval, clampTranscriptRetain, clampTranscriptWindow, coercePositiveInt, isAdapterProvider, parseMaxTokensInput, DEFAULT_MAX_RENDER_INTERVAL_MS, DEFAULT_MAX_TOKENS, DEFAULT_PLAN_HUMAN_TIMEOUT_MS, DEFAULT_PLAN_MAX_ASK_ROUNDS, DEFAULT_SKILLS_CONFIG, DEFAULT_TRANSCRIPT_RETAIN, DEFAULT_TRANSCRIPT_WINDOW, } from './config/schema.js';
31
31
  import { makeGetApiKey } from './config/load.js';
32
32
  import { AgentController, DENY_ALL_APPROVAL } from './agent/controller.js';
33
33
  import { runSkillsCommand } from './skills/cli-commands.js';
@@ -86,11 +86,29 @@ function toFlags(opts) {
86
86
  // reach `CliFlags` and are inert, with no compile error and no runtime error.
87
87
  team: opts.team,
88
88
  teamMax: opts.teamMax,
89
+ // Site three of three for `--todo` / `--todo-panel`, and the one this
90
+ // package has now paid for twice (`mouse` P1-2, `team` P1-4): a flag needs
91
+ // the `.option()` declaration below, a field on `CliFlags`, AND a line here.
92
+ // Miss this one and the flag is inert with no compile error and no runtime
93
+ // error.
94
+ todo: opts.todo,
95
+ todoPanel: opts.todoPanel,
96
+ // Site three of three for `--todo-follow` as well (todo-plan-followthrough
97
+ // C-9). A value flag is no less inert than a boolean one when this line is
98
+ // missing.
99
+ todoFollow: opts.todoFollow,
89
100
  // `--verbose` is just a shorthand for `--log-level debug`; an explicit
90
101
  // `--log-level` still wins, so the two can be combined without surprise.
91
102
  logLevel: opts.logLevel ?? (opts.verbose ? 'debug' : undefined),
92
103
  logFile: opts.logFile,
93
104
  logDir: opts.logDir,
105
+ // Site three of three for the render-performance flags. A flag needs the
106
+ // `.option()` declaration below, a field on `CliFlags`, AND a line here;
107
+ // miss this one and it is inert with no compile error and no runtime error
108
+ // (`mouse` P1-2, `team` P1-4, `todo` C-9 -- four features, same trap).
109
+ transcriptRetain: opts.transcriptRetain,
110
+ renderGovernor: opts.renderGovernor,
111
+ maxRenderInterval: opts.maxRenderInterval,
94
112
  };
95
113
  }
96
114
  // ---------------------------------------------------------------------------
@@ -120,12 +138,21 @@ function makeController(flags, opts = { interactive: true }) {
120
138
  // resolved a bootstrap level — it ran before commander and before `.env` were
121
139
  // readable — and without this hand-off the fully resolved one never applies.
122
140
  getLogger().reconfigure(config.log);
141
+ // Whether this session could EVER show the todo rail (todo-plan-execution
142
+ // §3.7 / P1-6). It varies ONE sentence of `<todo_planning>`; `decideRenderMode`
143
+ // is pure and `runInteractive` calls it again with the same inputs, so
144
+ // computing it here rather than threading it back costs nothing and keeps the
145
+ // controller's construction self-contained.
146
+ const todoPanelCapable = opts.interactive &&
147
+ decideRenderMode({ fullscreen: config.fullscreen }, process.env, process.stdout) ===
148
+ 'fullscreen';
123
149
  const confirmBridge = { handler: null };
124
150
  // `cancelPending` is replaced by the App's own implementation on mount; this
125
151
  // no-op keeps the object total so nothing has to null-check it.
126
152
  const humanInputBridge = { handler: null, cancelPending: () => { } };
127
153
  const controller = new AgentController(config, {
128
154
  version: VERSION,
155
+ todoPanelCapable,
129
156
  confirm: (req) => confirmBridge.handler ? confirmBridge.handler(req) : Promise.resolve(true),
130
157
  approval: opts.interactive
131
158
  ? {
@@ -219,6 +246,10 @@ function runInteractive(flags, extras = {}) {
219
246
  provider: snapshot.provider,
220
247
  model: snapshot.model,
221
248
  elapsedMs: Date.now() - snapshot.startedAt,
249
+ // Names what the retain ring removed, so a long session's replay never
250
+ // starts mid-conversation without saying why (tui-render-performance
251
+ // L1 / K-6).
252
+ ...(snapshot.droppedEntries ? { droppedEntries: snapshot.droppedEntries } : {}),
222
253
  }));
223
254
  };
224
255
  if (mode === 'fullscreen') {
@@ -308,7 +339,13 @@ async function runOneShot(flags, prompt, quiet) {
308
339
  for (const dir of untrusted) {
309
340
  process.stderr.write(`[skills] project skills in ${dir} skipped (untrusted). Run: aragon skills trust ${dir}\n`);
310
341
  }
311
- const code = await runHeadless(controller, prompt, { quiet });
342
+ // `followThrough` is read off the REAL controller here rather than through
343
+ // `HeadlessController`, which is deliberately the minimal surface a test can
344
+ // satisfy with an object literal (todo-plan-followthrough §3.6).
345
+ const code = await runHeadless(controller, prompt, {
346
+ quiet,
347
+ followThrough: controller.getTodoConfig().followThrough,
348
+ });
312
349
  // Flushed HERE and not inside `runHeadless`: that module is deliberately
313
350
  // injectable (its own controller interface, its own stdout/stderr) and has an
314
351
  // early `return 2`, so a singleton dependency in there would be both a new
@@ -367,6 +404,12 @@ const CONFIG_SET_KEYS = new Set([
367
404
  'fullscreen',
368
405
  'exitTranscript',
369
406
  'transcriptWindow',
407
+ // tui-render-performance L1 / L4. Listed here AND cased in the switch
408
+ // below: a key present here but absent there falls through, writes
409
+ // nothing, and still prints `Set <key> = <value>` (P1-2).
410
+ 'transcriptRetain',
411
+ 'renderGovernor',
412
+ 'maxRenderIntervalMs',
370
413
  // Without these two, `density` and `hints` would be settable only as one-shot
371
414
  // flags and never persist -- inconsistent with every other config field.
372
415
  'density',
@@ -483,7 +526,16 @@ function runConfigSet(key, value) {
483
526
  patch.exitTranscript = value === 'true' || value === '1';
484
527
  break;
485
528
  case 'transcriptWindow':
486
- patch.transcriptWindow = clampTranscriptWindow(value, 300);
529
+ patch.transcriptWindow = clampTranscriptWindow(value, DEFAULT_TRANSCRIPT_WINDOW);
530
+ break;
531
+ case 'transcriptRetain':
532
+ patch.transcriptRetain = clampTranscriptRetain(value, DEFAULT_TRANSCRIPT_RETAIN);
533
+ break;
534
+ case 'renderGovernor':
535
+ patch.renderGovernor = value === 'true' || value === '1';
536
+ break;
537
+ case 'maxRenderIntervalMs':
538
+ patch.maxRenderIntervalMs = clampMaxRenderInterval(value, DEFAULT_MAX_RENDER_INTERVAL_MS);
487
539
  break;
488
540
  case 'density':
489
541
  patch.density = clampDensity(value, 'comfortable');
@@ -602,6 +654,19 @@ function buildProgram() {
602
654
  .option('--team', 'Enable team subagents for this session (the default)')
603
655
  .option('--no-team', 'Disable team subagents (the task tool is not registered at all)')
604
656
  .option('--team-max <n>', 'Max subagents per dispatch for this run (1-10)')
657
+ // BOTH PAIRS, positive first (todo-plan-execution §4.3 / P1-8). `--todo` is
658
+ // the `--team` story verbatim; `--todo-panel` needs the pair for a second
659
+ // reason on top of it — `panel` is PERSISTED, so a lone `--no-todo-panel`
660
+ // would make commander default `opts.todoPanel` to `true` and silently
661
+ // overwrite a stored `panel: false` on every run that passed no flag.
662
+ .option('--todo', 'Enable todo planning for this session (the default)')
663
+ .option('--no-todo', 'Disable todo planning (the todo_write tool is not registered at all)')
664
+ .option('--todo-panel', 'Show the todo plan in a right-hand rail (the default)')
665
+ .option('--no-todo-panel', 'Keep todo planning but do not render the rail')
666
+ // NOT A PAIR, and it does not need to be: a value flag is absent unless
667
+ // passed, so `!== undefined` in `resolveTodoConfig` already distinguishes it
668
+ // from silence. An unrecognized mode is clamped to `notify`, not rejected.
669
+ .option('--todo-follow <mode>', 'What to do when a run ends with unfinished steps: notify (default) | auto | off')
605
670
  .option('--no-skills', 'Disable the skill system entirely (no catalog, no skill tools)')
606
671
  .option('--skill <name>', 'Force-load a skill at Level 2 for this run (repeatable)', (value, previous = []) => [...previous, value])
607
672
  .option('--skills-yes', 'Approve skill installs for this run (CI / headless)')
@@ -615,6 +680,14 @@ function buildProgram() {
615
680
  // No short form: `-v` is already `--version`.
616
681
  .option('--verbose', 'Shorthand for --log-level debug')
617
682
  .option('--log-dir <dir>', 'Absolute log directory for this run')
683
+ // Render performance (tui-render-performance §5.2). `--render-governor` is
684
+ // declared BEFORE its negative form for the tri-state reason recorded four
685
+ // times above: `renderGovernor` is persisted, so negative-only would default
686
+ // `opts.renderGovernor` to `true` and silently overwrite a stored `false`.
687
+ .option('--transcript-retain <n>', 'Entries kept in memory (200-20000, default 1000)')
688
+ .option('--render-governor', 'Adapt the frame interval under load (the default)')
689
+ .option('--no-render-governor', 'Pin the frame interval at 33ms however expensive a frame is')
690
+ .option('--max-render-interval <ms>', 'Governor ceiling in ms (33-1000, default 320)')
618
691
  .option('--no-color', 'Disable ANSI color')
619
692
  .option('--quiet', '(print mode) suppress tool/usage lines on stderr')
620
693
  .action(async (prompt, opts) => {