@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
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import { type ThinkingLevel } from '@aragon-agent/core';
9
9
  import { type LogLevelName } from '../logging/levels.js';
10
+ import type { FollowThroughMode } from '../todo/types.js';
10
11
  /**
11
12
  * `ModelRegistry` seeds builtin lists for five providers, but `initProviders()`
12
13
  * registers only three adapters. Running a model whose provider has no adapter
@@ -63,15 +64,40 @@ export declare const DEFAULT_IDLE_TIMEOUT_MS: number;
63
64
  */
64
65
  export declare const PROMPT_HISTORY_CAP = 100;
65
66
  /**
66
- * How many trailing entries the full-screen viewport renders (invariant I-3).
67
- * Yoga lays out every child even when `overflow: hidden` clips it, so the cost
68
- * of a long session is linear in the entry count regardless of visibility. The
69
- * reducer and `/save` keep the full history — this is a render-layer bound only.
67
+ * The SCROLL HORIZON: entries older than this are not reachable inside the app
68
+ * and render as the "N earlier entries collapsed" line instead (invariant I-3).
69
+ *
70
+ * It used to be a rendering budget as well, because yoga laid out every child
71
+ * even when `overflow: hidden` clipped it. The viewport is virtualised now
72
+ * (tui-render-performance L3) — off-screen entries are not mounted at all — so
73
+ * the horizon costs O(entries) of arithmetic rather than O(characters) of layout
74
+ * and the default rises 300 -> 1000 with a 20000 ceiling. An existing persisted
75
+ * `300` is still valid and still clamps; no config migration is required.
70
76
  */
71
- export declare const DEFAULT_TRANSCRIPT_WINDOW = 300;
77
+ export declare const DEFAULT_TRANSCRIPT_WINDOW = 1000;
72
78
  export declare const MIN_TRANSCRIPT_WINDOW = 50;
73
- export declare const MAX_TRANSCRIPT_WINDOW = 2000;
79
+ export declare const MAX_TRANSCRIPT_WINDOW = 20000;
74
80
  export declare function clampTranscriptWindow(v: unknown, fallback: number): number;
81
+ /**
82
+ * Entries kept in `ViewState` at all (tui-render-performance L1 / R5).
83
+ *
84
+ * A DIFFERENT BOUND FROM `transcriptWindow`, and the two must not be merged:
85
+ * the window is what the user can scroll to, this is what the process keeps in
86
+ * memory and what `/save` can export. Dropped entries are COUNTED and reported
87
+ * (`/perf`, and a leading line in the exported transcript) — never silent.
88
+ */
89
+ export declare const DEFAULT_TRANSCRIPT_RETAIN = 1000;
90
+ export declare const MIN_TRANSCRIPT_RETAIN = 200;
91
+ export declare const MAX_TRANSCRIPT_RETAIN = 20000;
92
+ export declare function clampTranscriptRetain(v: unknown, fallback: number): number;
93
+ /** Adaptive coalescing under load. On by default; `--no-render-governor` kills it. */
94
+ export declare const DEFAULT_RENDER_GOVERNOR = true;
95
+ /**
96
+ * Ceiling of the governor ladder, in ms. `33` flattens the ladder to a single
97
+ * rung, which is the pre-feature behaviour without needing the kill switch.
98
+ */
99
+ export declare const DEFAULT_MAX_RENDER_INTERVAL_MS = 320;
100
+ export declare function clampMaxRenderInterval(v: unknown, fallback: number): number;
75
101
  /** `ask_user` calls allowed per user turn. */
76
102
  export declare const DEFAULT_PLAN_MAX_ASK_ROUNDS = 4;
77
103
  /**
@@ -239,6 +265,40 @@ export declare const DEFAULT_TEAM_CONFIG: TeamConfig;
239
265
  * the two disagree would make the panel's `n/m` readout confusing for no gain.
240
266
  */
241
267
  export declare function clampTeamConfig(raw: unknown): TeamConfig;
268
+ export interface TodoConfig {
269
+ /** Register `todo_write` at all. Default TRUE. */
270
+ enabled: boolean;
271
+ /**
272
+ * Render the right rail.
273
+ *
274
+ * INDEPENDENT OF `enabled` ON PURPOSE (D-16): a screen-reader user wants the
275
+ * planning discipline without the column, and the prompt block varies one
276
+ * sentence rather than disappearing (§3.7).
277
+ */
278
+ panel: boolean;
279
+ /**
280
+ * What happens when a run ends with unfinished steps
281
+ * (todo-plan-followthrough §4.1). Default `'notify'`, which reproduces round
282
+ * 1's behaviour byte for byte.
283
+ *
284
+ * THE THIRD SCALAR, and it stays a scalar for the reason the section header
285
+ * above gives: `store.ts` merges this section BY HAND, and both of its merges
286
+ * spread the whole object (`{ ...DEFAULT_CONFIG.todo, ...(partial.todo ?? {}) }`
287
+ * and `{ ...current.todo, ...(patch.todo ?? {}) }`), so a third scalar key is
288
+ * carried automatically and needs no change there. A NESTED field would not
289
+ * be, and would mean replacing both merges first.
290
+ */
291
+ followThrough: FollowThroughMode;
292
+ }
293
+ export declare const DEFAULT_TODO_CONFIG: TodoConfig;
294
+ /**
295
+ * THE single gate for every todo-config read AND write, mirroring
296
+ * `clampSkillsConfig` / `clampLogConfig` / `clampTeamConfig` for the reason
297
+ * those record: hardening only the read path leaves a bad value on disk that
298
+ * reverts to the default on every launch, which presents to the user as "my
299
+ * setting won't stick".
300
+ */
301
+ export declare function clampTodoConfig(raw: unknown): TodoConfig;
242
302
  export interface PersistedConfig {
243
303
  version: number;
244
304
  provider: string;
@@ -265,8 +325,14 @@ export interface PersistedConfig {
265
325
  fullscreen: boolean;
266
326
  /** Replay a plain-text session summary after leaving the alternate screen. */
267
327
  exitTranscript: boolean;
268
- /** Trailing entries the full-screen viewport renders (I-3). */
328
+ /** Entries reachable by scrolling; older ones show the "collapsed" line (I-3). */
269
329
  transcriptWindow: number;
330
+ /** Entries kept in `ViewState`; older ones are dropped and counted (L1). */
331
+ transcriptRetain: number;
332
+ /** Adaptive coalescing under load (L4). */
333
+ renderGovernor: boolean;
334
+ /** Ceiling of the governor ladder in ms; `33` flattens it (L4). */
335
+ maxRenderIntervalMs: number;
270
336
  confirmTools: boolean;
271
337
  toolTimeoutMs: number;
272
338
  idleTimeoutMs: number;
@@ -305,6 +371,8 @@ export interface PersistedConfig {
305
371
  log: LogConfig;
306
372
  /** Team mode. The FOURTH nested object; scalars only (see TeamConfig). */
307
373
  team: TeamConfig;
374
+ /** Todo planning. The FIFTH nested object; scalars only (see TodoConfig). */
375
+ todo: TodoConfig;
308
376
  }
309
377
  export declare const DEFAULT_CONFIG: PersistedConfig;
310
378
  /**
@@ -353,8 +421,30 @@ export interface CliConfig {
353
421
  fullscreen?: boolean;
354
422
  /** Replay a plain-text summary on exit (full-screen only — see §4.4). */
355
423
  exitTranscript: boolean;
356
- /** Trailing entries the full-screen viewport renders (I-3). */
424
+ /** Entries reachable by scrolling; older ones show the "collapsed" line (I-3). */
357
425
  transcriptWindow: number;
426
+ /**
427
+ * Entries kept in `ViewState` (L1).
428
+ *
429
+ * Resolved to be `>= transcriptWindow`: retaining fewer entries than the
430
+ * window can scroll to would silently make part of the window unreachable.
431
+ */
432
+ transcriptRetain: number;
433
+ /**
434
+ * What the user actually asked `transcriptRetain` to be, present ONLY when
435
+ * that value had to be raised (§5.1).
436
+ *
437
+ * A setting ignored without a word is the one failure mode this feature is
438
+ * not allowed to add: the eco chip, `/perf` and the dropped-entry line all
439
+ * exist so that nothing it does happens unannounced, and a config key
440
+ * overwritten in silence would be the exception. `undefined` means nothing
441
+ * was overridden, which is every default session.
442
+ */
443
+ transcriptRetainRequested?: number;
444
+ /** Adaptive coalescing under load (L4). */
445
+ renderGovernor: boolean;
446
+ /** Ceiling of the governor ladder in ms; `33` flattens it (L4). */
447
+ maxRenderIntervalMs: number;
358
448
  confirmTools: boolean;
359
449
  toolTimeoutMs: number;
360
450
  idleTimeoutMs: number;
@@ -399,6 +489,16 @@ export interface CliConfig {
399
489
  * `/team` flips is a separate flag on the controller.
400
490
  */
401
491
  team: TeamConfig;
492
+ /**
493
+ * Resolved todo-planning settings (todo-plan-execution §4.2).
494
+ *
495
+ * `enabled` here decides whether `todo_write` is REGISTERED, which happens
496
+ * once at construction and can never be undone (C-1). The live on/off switch
497
+ * `/todo` flips is a separate flag on the controller; `panel` is read at
498
+ * RENDER time through `controller.getConfig()`, which is why `/todo panel`
499
+ * must call `setTodoConfig` as well as persisting (P1-2).
500
+ */
501
+ todo: TodoConfig;
402
502
  /**
403
503
  * Lifetime submit count driving the composer hint fade (`HINT_FADE_AFTER`).
404
504
  *
@@ -98,18 +98,52 @@ export const DEFAULT_IDLE_TIMEOUT_MS = DEFAULT_TOOL_TIMEOUT_MS + IDLE_TIMEOUT_MA
98
98
  */
99
99
  export const PROMPT_HISTORY_CAP = 100;
100
100
  /**
101
- * How many trailing entries the full-screen viewport renders (invariant I-3).
102
- * Yoga lays out every child even when `overflow: hidden` clips it, so the cost
103
- * of a long session is linear in the entry count regardless of visibility. The
104
- * reducer and `/save` keep the full history — this is a render-layer bound only.
101
+ * The SCROLL HORIZON: entries older than this are not reachable inside the app
102
+ * and render as the "N earlier entries collapsed" line instead (invariant I-3).
103
+ *
104
+ * It used to be a rendering budget as well, because yoga laid out every child
105
+ * even when `overflow: hidden` clipped it. The viewport is virtualised now
106
+ * (tui-render-performance L3) — off-screen entries are not mounted at all — so
107
+ * the horizon costs O(entries) of arithmetic rather than O(characters) of layout
108
+ * and the default rises 300 -> 1000 with a 20000 ceiling. An existing persisted
109
+ * `300` is still valid and still clamps; no config migration is required.
105
110
  */
106
- export const DEFAULT_TRANSCRIPT_WINDOW = 300;
111
+ export const DEFAULT_TRANSCRIPT_WINDOW = 1000;
107
112
  export const MIN_TRANSCRIPT_WINDOW = 50;
108
- export const MAX_TRANSCRIPT_WINDOW = 2000;
113
+ export const MAX_TRANSCRIPT_WINDOW = 20_000;
109
114
  export function clampTranscriptWindow(v, fallback) {
110
115
  const n = coercePositiveInt(v, fallback);
111
116
  return Math.min(MAX_TRANSCRIPT_WINDOW, Math.max(MIN_TRANSCRIPT_WINDOW, n));
112
117
  }
118
+ /**
119
+ * Entries kept in `ViewState` at all (tui-render-performance L1 / R5).
120
+ *
121
+ * A DIFFERENT BOUND FROM `transcriptWindow`, and the two must not be merged:
122
+ * the window is what the user can scroll to, this is what the process keeps in
123
+ * memory and what `/save` can export. Dropped entries are COUNTED and reported
124
+ * (`/perf`, and a leading line in the exported transcript) — never silent.
125
+ */
126
+ export const DEFAULT_TRANSCRIPT_RETAIN = 1000;
127
+ export const MIN_TRANSCRIPT_RETAIN = 200;
128
+ export const MAX_TRANSCRIPT_RETAIN = 20_000;
129
+ export function clampTranscriptRetain(v, fallback) {
130
+ const n = coercePositiveInt(v, fallback);
131
+ return Math.min(MAX_TRANSCRIPT_RETAIN, Math.max(MIN_TRANSCRIPT_RETAIN, n));
132
+ }
133
+ // ---------------------------------------------------------------------------
134
+ // Adaptive render governor (tui-render-performance L4 / R7)
135
+ // ---------------------------------------------------------------------------
136
+ /** Adaptive coalescing under load. On by default; `--no-render-governor` kills it. */
137
+ export const DEFAULT_RENDER_GOVERNOR = true;
138
+ /**
139
+ * Ceiling of the governor ladder, in ms. `33` flattens the ladder to a single
140
+ * rung, which is the pre-feature behaviour without needing the kill switch.
141
+ */
142
+ export const DEFAULT_MAX_RENDER_INTERVAL_MS = 320;
143
+ const RENDER_INTERVAL_RANGE = { min: 33, max: 1000 };
144
+ export function clampMaxRenderInterval(v, fallback) {
145
+ return clampInt(v, fallback, RENDER_INTERVAL_RANGE);
146
+ }
113
147
  // ---------------------------------------------------------------------------
114
148
  // Plan mode (plan-mode §4.4)
115
149
  //
@@ -312,6 +346,40 @@ export function clampTeamConfig(raw) {
312
346
  maxTurnsPerSubagent: clampInt(src.maxTurnsPerSubagent, DEFAULT_TEAM_CONFIG.maxTurnsPerSubagent, TURNS_RANGE),
313
347
  };
314
348
  }
349
+ export const DEFAULT_TODO_CONFIG = {
350
+ enabled: true,
351
+ panel: true,
352
+ followThrough: 'notify',
353
+ };
354
+ /**
355
+ * There is no enum helper in this file, so this follows the `isThinkingLevel`
356
+ * shape already here rather than inventing a second idiom.
357
+ */
358
+ const FOLLOW_THROUGH_MODES = ['notify', 'auto', 'off'];
359
+ function followMode(value, fallback) {
360
+ return typeof value === 'string' && FOLLOW_THROUGH_MODES.includes(value)
361
+ ? value
362
+ : fallback;
363
+ }
364
+ /**
365
+ * THE single gate for every todo-config read AND write, mirroring
366
+ * `clampSkillsConfig` / `clampLogConfig` / `clampTeamConfig` for the reason
367
+ * those record: hardening only the read path leaves a bad value on disk that
368
+ * reverts to the default on every launch, which presents to the user as "my
369
+ * setting won't stick".
370
+ */
371
+ export function clampTodoConfig(raw) {
372
+ const src = (raw && typeof raw === 'object' ? raw : {});
373
+ return {
374
+ enabled: bool(src.enabled, DEFAULT_TODO_CONFIG.enabled),
375
+ panel: bool(src.panel, DEFAULT_TODO_CONFIG.panel),
376
+ // CLAMPED TO THE DEFAULT, NEVER REJECTED — the discipline every other key in
377
+ // this file follows, and the trade round 1 already made for `--thinking`.
378
+ // `ARAGON_TODO_FOLLOW=garbage` therefore resolves to `'notify'` and throws
379
+ // nothing (AC-5).
380
+ followThrough: followMode(src.followThrough, DEFAULT_TODO_CONFIG.followThrough),
381
+ };
382
+ }
315
383
  export const DEFAULT_CONFIG = {
316
384
  version: CONFIG_VERSION,
317
385
  provider: 'anthropic',
@@ -326,6 +394,9 @@ export const DEFAULT_CONFIG = {
326
394
  fullscreen: true,
327
395
  exitTranscript: true,
328
396
  transcriptWindow: DEFAULT_TRANSCRIPT_WINDOW,
397
+ transcriptRetain: DEFAULT_TRANSCRIPT_RETAIN,
398
+ renderGovernor: DEFAULT_RENDER_GOVERNOR,
399
+ maxRenderIntervalMs: DEFAULT_MAX_RENDER_INTERVAL_MS,
329
400
  confirmTools: false,
330
401
  toolTimeoutMs: DEFAULT_TOOL_TIMEOUT_MS,
331
402
  idleTimeoutMs: DEFAULT_IDLE_TIMEOUT_MS,
@@ -340,6 +411,10 @@ export const DEFAULT_CONFIG = {
340
411
  skills: DEFAULT_SKILLS_CONFIG,
341
412
  log: DEFAULT_LOG_CONFIG,
342
413
  team: DEFAULT_TEAM_CONFIG,
414
+ // The load path's first spread is what supplies the section to a user with no
415
+ // `todo` key at all, so this line is as required as the two merges in
416
+ // `store.ts` (P0-1).
417
+ todo: DEFAULT_TODO_CONFIG,
343
418
  };
344
419
  /**
345
420
  * Runtime state that used to be stored in `config.json` by mistake
@@ -14,7 +14,7 @@ import { dirname } from 'node:path';
14
14
  import process from 'node:process';
15
15
  import { getConfigPath, getSessionsDir } from './app-paths.js';
16
16
  import { registerSecretsFrom } from '../logging/secret-registry.js';
17
- import { CONFIG_VERSION, DEFAULT_CONFIG, clampLogConfig, clampMaxTokens, clampSkillsConfig, clampTeamConfig, isAutoToken, stripLegacyStateKeys, } from './schema.js';
17
+ import { CONFIG_VERSION, DEFAULT_CONFIG, clampLogConfig, clampMaxTokens, clampSkillsConfig, clampTeamConfig, clampTodoConfig, isAutoToken, stripLegacyStateKeys, } from './schema.js';
18
18
  /**
19
19
  * Normalize `maxTokens` for both directions of the config file.
20
20
  *
@@ -122,6 +122,10 @@ export function loadPersistedConfig() {
122
122
  log: clampLogConfig({ ...DEFAULT_CONFIG.log, ...(partial.log ?? {}) }),
123
123
  // `team` is the fourth. Same one-level rule, same merge.
124
124
  team: clampTeamConfig({ ...DEFAULT_CONFIG.team, ...(partial.team ?? {}) }),
125
+ // `todo` is the fifth. Same one-level rule, same merge — and this line is
126
+ // the READ half of the pair that keeps `/todo panel off` from silently
127
+ // unregistering the tool on the next launch (P0-1 / AC-35).
128
+ todo: clampTodoConfig({ ...DEFAULT_CONFIG.todo, ...(partial.todo ?? {}) }),
125
129
  };
126
130
  }
127
131
  /**
@@ -168,6 +172,11 @@ export function updatePersistedConfig(patch) {
168
172
  // 4` sends `{ team: { maxSubagents: 4 } }`, which a shallow spread would
169
173
  // turn into "changing the fan-out width silently reverted every timeout".
170
174
  team: clampTeamConfig({ ...current.team, ...(patch.team ?? {}) }),
175
+ // The fifth section, and its omission is the worst of the five (P0-1):
176
+ // `/todo panel off` sends `{ todo: { panel: false } }`, which a shallow
177
+ // spread turns into "turning the panel off silently unregisters the tool",
178
+ // permanently, from the next launch onward, with nothing anywhere saying so.
179
+ todo: clampTodoConfig({ ...current.todo, ...(patch.todo ?? {}) }),
171
180
  };
172
181
  writeConfigFile(merged);
173
182
  return merged;
@@ -7,22 +7,40 @@
7
7
  */
8
8
  import type { Message, ModelRef } from '@aragon-agent/core';
9
9
  import type { Entry } from '../agent/reducer.js';
10
+ import type { TodoItem } from '../todo/types.js';
10
11
  export interface SavedSession {
11
12
  version: number;
12
13
  savedAt: number;
13
14
  model: ModelRef;
14
15
  messages: Message[];
15
16
  entries: Entry[];
17
+ /**
18
+ * The live todo list at save time (todo-plan-execution §3.13).
19
+ *
20
+ * `SESSION_VERSION` STAYS 1: the field is additive and optional, `loadSession`
21
+ * validates only that `messages` and `entries` are arrays, and an older file
22
+ * simply yields `undefined` — which `restoreTodos` treats as "clear", the
23
+ * right answer for a session that predates the feature (D-20 / D-22).
24
+ */
25
+ todos?: TodoItem[];
16
26
  }
17
27
  /** Resolve a `/save` target: absolute/relative path, or a name in the data dir. */
18
28
  export declare function resolveSessionPath(nameOrPath: string | undefined, cwd: string): string;
29
+ /**
30
+ * `data` GAINS `todos`, AND WIDENING `SavedSession` ALONE IS NOT ENOUGH (P0-2):
31
+ * the writer below builds its payload from THIS parameter, not from the type, so
32
+ * a `todos?` field on the interface with no field here would ship a key nothing
33
+ * ever writes.
34
+ */
19
35
  export declare function saveSession(filePath: string, data: {
20
36
  model: ModelRef;
21
37
  messages: Message[];
22
38
  entries: Entry[];
39
+ todos: TodoItem[];
23
40
  }): void;
24
41
  /**
25
- * Settle any team entry that was saved mid-dispatch (team-subagents §5.3 / P1-5).
42
+ * Settle any team entry that was saved mid-dispatch (team-subagents §5.3 / P1-5),
43
+ * and any todo card that was saved mid-run (todo-plan-execution §3.8 / C-5).
26
44
  *
27
45
  * `saveSession` writes `entries` verbatim and `loadSession` validates only
28
46
  * array-ness, so a session saved while a dispatch was in flight resumes with a
@@ -28,6 +28,12 @@ function defaultStamp() {
28
28
  // Filesystem-safe timestamp without punctuation that breaks on Windows.
29
29
  return new Date().toISOString().replace(/[:.]/g, '-');
30
30
  }
31
+ /**
32
+ * `data` GAINS `todos`, AND WIDENING `SavedSession` ALONE IS NOT ENOUGH (P0-2):
33
+ * the writer below builds its payload from THIS parameter, not from the type, so
34
+ * a `todos?` field on the interface with no field here would ship a key nothing
35
+ * ever writes.
36
+ */
31
37
  export function saveSession(filePath, data) {
32
38
  const dir = dirname(filePath);
33
39
  if (!existsSync(dir))
@@ -38,11 +44,13 @@ export function saveSession(filePath, data) {
38
44
  model: data.model,
39
45
  messages: data.messages,
40
46
  entries: data.entries,
47
+ todos: data.todos,
41
48
  };
42
49
  writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf-8');
43
50
  }
44
51
  /**
45
- * Settle any team entry that was saved mid-dispatch (team-subagents §5.3 / P1-5).
52
+ * Settle any team entry that was saved mid-dispatch (team-subagents §5.3 / P1-5),
53
+ * and any todo card that was saved mid-run (todo-plan-execution §3.8 / C-5).
46
54
  *
47
55
  * `saveSession` writes `entries` verbatim and `loadSession` validates only
48
56
  * array-ness, so a session saved while a dispatch was in flight resumes with a
@@ -57,7 +65,18 @@ export function saveSession(filePath, data) {
57
65
  * never legitimately sees a stale-active entry — only the file does.
58
66
  */
59
67
  export function normalizeLoadedEntries(entries) {
60
- return entries.map((entry) => entry.kind === 'team' && entry.active ? { ...entry, active: false, aborted: true } : entry);
68
+ return entries.map((entry) => {
69
+ if (entry.kind === 'team' && entry.active) {
70
+ return { ...entry, active: false, aborted: true };
71
+ }
72
+ // `interrupted: true` is the todo analogue of the team entry's `aborted`,
73
+ // and true for the same reason: the run behind the card died with the
74
+ // process (P2-6). Without it a resumed `2/7` reads as a run still in flight.
75
+ if (entry.kind === 'todo' && entry.live) {
76
+ return { ...entry, live: false, interrupted: true };
77
+ }
78
+ return entry;
79
+ });
61
80
  }
62
81
  export function loadSession(filePath) {
63
82
  const raw = readFileSync(filePath, 'utf-8');
@@ -89,6 +89,17 @@ export function buildSubagentTools(spec, deps, getAgent) {
89
89
  // registering tools it could never use burns a turn — the same argument that
90
90
  // keeps them out of headless mode.
91
91
  planTools: [],
92
+ // NO `todo_write` FOR CHILDREN (todo-plan-execution §3.11 / D-14 / AC-34).
93
+ // Children run concurrently against ONE list under a full-replacement
94
+ // protocol: the last writer would win and the lead's plan would be destroyed
95
+ // by a subagent's private checklist. A list PER child would be a second
96
+ // panel and a second scroll model for work the lead already tracks as one
97
+ // item.
98
+ //
99
+ // EXPLICIT, WITH THIS COMMENT, even though the option's absence already
100
+ // yields an empty array: a silent default is not a decision anyone can find
101
+ // later.
102
+ todoTools: [],
92
103
  teamTools: commTools,
93
104
  // `spec.readOnly` forces the plan gate on for this child even in a BUILD
94
105
  // session. TIGHTENING ONLY (D-13): there is no field anywhere that gives a
@@ -0,0 +1,117 @@
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 type { TodoSnapshot } from './types.js';
19
+ /**
20
+ * `notify` — say something true and stop (the default; round 1's behaviour).
21
+ * `auto` — continue the plan, under the economy below.
22
+ * `off` — say nothing at all.
23
+ */
24
+ export type FollowThroughMode = 'notify' | 'auto' | 'off';
25
+ /** Carried across runs by the caller. Serializable, comparable, inert. */
26
+ export interface FollowThroughBudget {
27
+ /**
28
+ * Auto-continuations issued against this list LINEAGE.
29
+ *
30
+ * MONOTONIC UNTIL THE LIST IS GONE (P0-1 / D-16 / I-8). It is NOT reset by a
31
+ * re-plan: a model that emits a differently-sized list on every continuation
32
+ * would otherwise zero both counters every turn while `staleTurns` also never
33
+ * fires (it IS writing), which is an unbounded loop reachable by ordinary
34
+ * "re-plan as you learn" behaviour rather than by anything adversarial.
35
+ *
36
+ * A refactor that "simplifies" `advanceBudget` back to returning a zeroed
37
+ * budget on a `total` change re-opens that hole, and the symptom is a bill
38
+ * rather than a test failure. `advanceBudget`'s own test pins it.
39
+ */
40
+ used: number;
41
+ /** Consecutive auto-continuations after which `doneCount` did not increase. */
42
+ noProgressStreak: number;
43
+ /** `TodoSnapshot.total` the budget was opened against. Identity, not a count. */
44
+ anchorTotal: number;
45
+ /**
46
+ * `doneCount` as observed at the PREVIOUS `agent_end` — which is what
47
+ * `advanceBudget` assigns, in every branch, and therefore what the next call
48
+ * compares against. An implementer who captures it at `runStart` instead
49
+ * builds a different state machine that happens to agree in the normal case.
50
+ */
51
+ doneAtRunStart: number;
52
+ }
53
+ export interface FollowThroughInput {
54
+ mode: FollowThroughMode;
55
+ snapshot: TodoSnapshot | null;
56
+ /**
57
+ * Why the run ended. Derived by the caller, never carried by `AgentEndEvent`
58
+ * (C-2): core's event has `messages` and nothing else, and this round does not
59
+ * change core to make it otherwise.
60
+ */
61
+ runEnd: {
62
+ aborted: boolean;
63
+ errored: boolean;
64
+ };
65
+ budget: FollowThroughBudget;
66
+ /**
67
+ * `false` in headless: no grace window (there is nobody to use it), and a
68
+ * different notice verb, because `-p` writes `[todo] ...` lines rather than
69
+ * transcript notices.
70
+ */
71
+ interactive: boolean;
72
+ }
73
+ export type FollowThroughDecision = {
74
+ kind: 'none';
75
+ } | {
76
+ kind: 'notify';
77
+ level: 'info' | 'warn';
78
+ text: string;
79
+ } | {
80
+ kind: 'continue';
81
+ message: string;
82
+ graceMs: number;
83
+ notice: string;
84
+ };
85
+ /** The zero budget. A fresh App, a fresh headless run, and every cleared list. */
86
+ export declare function emptyBudget(): FollowThroughBudget;
87
+ /**
88
+ * The decision. EVALUATED IN THIS ORDER, AND THE ORDER IS SEMANTICS, NOT STYLE —
89
+ * the same sentence `decideRenderMode` carries.
90
+ */
91
+ export declare function decideFollowThrough(input: FollowThroughInput): FollowThroughDecision;
92
+ /**
93
+ * Fold a finished run into the budget. Called BEFORE `decideFollowThrough`, on
94
+ * every `agent_end` (and on every resolved `prompt()` in headless).
95
+ *
96
+ * `used` is NOT incremented here — a decision that is never acted on costs
97
+ * nothing. Both fire points charge it themselves (I-5): the TUI's grace timer
98
+ * and the headless loop, and BOTH are required. An uncharged counter in `-p` is
99
+ * an uncapped loop in the one environment with no human to press Esc (P1-2).
100
+ */
101
+ export declare function advanceBudget(prev: FollowThroughBudget, snapshot: TodoSnapshot | null, wasAutoContinuation: boolean): FollowThroughBudget;
102
+ /**
103
+ * The text an auto-continuation — or `/todo continue` — submits.
104
+ *
105
+ * IT ENUMERATES RATHER THAN REFERRING (D-11). Round 1 submitted the fixed string
106
+ * "Continue with the remaining todo items.", which is a reference to a
107
+ * `todo_write` call that in a long session may have scrolled far enough back
108
+ * that the model reconstructs it wrong — and reconstructing it wrong is SILENT,
109
+ * because whatever it writes next is a full replacement and becomes the new
110
+ * truth.
111
+ *
112
+ * `content` and never `activeForm`: the imperative is what an instruction wants.
113
+ * Numbers are the item's 1-based position in the WHOLE list, so "step 5" means
114
+ * the same thing here as it does in the rail. Completed items are omitted.
115
+ * `TODO_LIMITS.maxItems` bounds the whole message at roughly 1.8 kB.
116
+ */
117
+ export declare function buildContinuationMessage(snapshot: TodoSnapshot): string;