@aragon-agent/cli 0.6.35 → 0.6.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +3173 -3071
  3. package/dist/agent/agent-mode.d.ts +20 -4
  4. package/dist/agent/agent-mode.js +20 -4
  5. package/dist/agent/controller.d.ts +38 -3
  6. package/dist/agent/controller.js +64 -4
  7. package/dist/agent/headless.js +31 -0
  8. package/dist/agent/reducer.d.ts +13 -2
  9. package/dist/agent/reducer.js +1 -0
  10. package/dist/cli.js +12 -0
  11. package/dist/commands/agents-command.d.ts +15 -0
  12. package/dist/commands/agents-command.js +27 -0
  13. package/dist/commands/builtins.js +131 -4
  14. package/dist/commands/perf.d.ts +11 -0
  15. package/dist/commands/perf.js +2 -1
  16. package/dist/commands/registry.d.ts +14 -0
  17. package/dist/config/app-paths.d.ts +15 -0
  18. package/dist/config/app-paths.js +33 -2
  19. package/dist/config/env.d.ts +4 -1
  20. package/dist/config/env.js +28 -2
  21. package/dist/config/load.d.ts +10 -0
  22. package/dist/config/load.js +8 -1
  23. package/dist/config/schema.d.ts +68 -0
  24. package/dist/config/schema.js +39 -0
  25. package/dist/config/store.js +12 -1
  26. package/dist/config/ui-state.d.ts +18 -0
  27. package/dist/config/ui-state.js +9 -0
  28. package/dist/exec/events.d.ts +25 -0
  29. package/dist/exec/events.js +4 -0
  30. package/dist/exec/index.js +16 -1
  31. package/dist/exec/runner.js +10 -0
  32. package/dist/proc/limits.d.ts +16 -0
  33. package/dist/proc/limits.js +16 -0
  34. package/dist/proc/supervisor.d.ts +20 -1
  35. package/dist/proc/supervisor.js +42 -14
  36. package/dist/prompts/command.d.ts +34 -0
  37. package/dist/prompts/command.js +237 -0
  38. package/dist/prompts/copy.d.ts +89 -0
  39. package/dist/prompts/copy.js +131 -0
  40. package/dist/prompts/fill.d.ts +51 -0
  41. package/dist/prompts/fill.js +46 -0
  42. package/dist/prompts/limits.d.ts +53 -0
  43. package/dist/prompts/limits.js +53 -0
  44. package/dist/prompts/normalize.d.ts +53 -0
  45. package/dist/prompts/normalize.js +190 -0
  46. package/dist/prompts/resolve.d.ts +31 -0
  47. package/dist/prompts/resolve.js +60 -0
  48. package/dist/prompts/search.d.ts +58 -0
  49. package/dist/prompts/search.js +146 -0
  50. package/dist/prompts/store.d.ts +86 -0
  51. package/dist/prompts/store.js +453 -0
  52. package/dist/prompts/types.d.ts +204 -0
  53. package/dist/prompts/types.js +13 -0
  54. package/dist/session/checkpoint.d.ts +8 -1
  55. package/dist/session/checkpoint.js +6 -0
  56. package/dist/session/persist.d.ts +19 -1
  57. package/dist/session/persist.js +13 -1
  58. package/dist/session/startup-session.d.ts +7 -2
  59. package/dist/session/startup-session.js +5 -2
  60. package/dist/session/store.js +3 -0
  61. package/dist/session/validate-session.js +64 -1
  62. package/dist/team/limits.d.ts +25 -0
  63. package/dist/team/limits.js +26 -0
  64. package/dist/team/runtime.js +27 -2
  65. package/dist/team/settled.d.ts +57 -0
  66. package/dist/team/settled.js +87 -0
  67. package/dist/team/subagent.js +48 -2
  68. package/dist/team/timeline.d.ts +74 -0
  69. package/dist/team/timeline.js +101 -0
  70. package/dist/team/types.d.ts +24 -1
  71. package/dist/todo/acceptance.d.ts +120 -0
  72. package/dist/todo/acceptance.js +399 -0
  73. package/dist/todo/follow-through.d.ts +48 -23
  74. package/dist/todo/follow-through.js +110 -44
  75. package/dist/todo/limits.d.ts +62 -1
  76. package/dist/todo/limits.js +62 -1
  77. package/dist/todo/panel-rows.d.ts +29 -39
  78. package/dist/todo/panel-rows.js +37 -83
  79. package/dist/todo/plan-sections.d.ts +60 -0
  80. package/dist/todo/plan-sections.js +108 -0
  81. package/dist/todo/prompt.d.ts +17 -12
  82. package/dist/todo/prompt.js +59 -40
  83. package/dist/todo/row-window.d.ts +51 -0
  84. package/dist/todo/row-window.js +107 -0
  85. package/dist/todo/store.d.ts +139 -31
  86. package/dist/todo/store.js +271 -73
  87. package/dist/todo/todo-tool.d.ts +33 -8
  88. package/dist/todo/todo-tool.js +186 -30
  89. package/dist/todo/types.d.ts +58 -0
  90. package/dist/ui/App.js +310 -46
  91. package/dist/ui/Composer.d.ts +6 -2
  92. package/dist/ui/Composer.js +2 -2
  93. package/dist/ui/Header.d.ts +9 -1
  94. package/dist/ui/Header.js +2 -2
  95. package/dist/ui/PromptInput.d.ts +21 -1
  96. package/dist/ui/PromptInput.js +21 -1
  97. package/dist/ui/ScrollbackShell.d.ts +19 -19
  98. package/dist/ui/ScrollbackShell.js +8 -1
  99. package/dist/ui/StatusBar.d.ts +5 -0
  100. package/dist/ui/StatusBar.js +1 -0
  101. package/dist/ui/TeamPanel.d.ts +26 -44
  102. package/dist/ui/TeamPanel.js +46 -135
  103. package/dist/ui/TodoPanel.d.ts +17 -14
  104. package/dist/ui/TodoPanel.js +146 -38
  105. package/dist/ui/Transcript.js +1 -1
  106. package/dist/ui/composer-input.d.ts +10 -0
  107. package/dist/ui/composer-input.js +57 -24
  108. package/dist/ui/entries/TodoCard.d.ts +26 -11
  109. package/dist/ui/entries/TodoCard.js +56 -15
  110. package/dist/ui/glyphs.d.ts +19 -0
  111. package/dist/ui/glyphs.js +4 -0
  112. package/dist/ui/layout/agents-board.d.ts +102 -0
  113. package/dist/ui/layout/agents-board.js +389 -0
  114. package/dist/ui/layout/cells.d.ts +61 -0
  115. package/dist/ui/layout/cells.js +156 -0
  116. package/dist/ui/layout/header-hints.d.ts +13 -3
  117. package/dist/ui/layout/header-hints.js +48 -14
  118. package/dist/ui/layout/status-detail-layout.js +9 -0
  119. package/dist/ui/layout/status-layout.d.ts +13 -0
  120. package/dist/ui/layout/team-row.d.ts +139 -0
  121. package/dist/ui/layout/team-row.js +246 -0
  122. package/dist/ui/layout/todo-layout.d.ts +38 -0
  123. package/dist/ui/layout/todo-layout.js +75 -0
  124. package/dist/ui/layout/virtual-window.js +23 -3
  125. package/dist/ui/overlays/AgentsBoardOverlay.d.ts +57 -0
  126. package/dist/ui/overlays/AgentsBoardOverlay.js +183 -0
  127. package/dist/ui/overlays/HelpOverlay.js +17 -2
  128. package/dist/ui/overlays/PromptEditorPage.d.ts +48 -0
  129. package/dist/ui/overlays/PromptEditorPage.js +227 -0
  130. package/dist/ui/overlays/PromptLibraryOverlay.d.ts +37 -0
  131. package/dist/ui/overlays/PromptLibraryOverlay.js +191 -0
  132. package/dist/ui/overlays/PromptPicker.d.ts +36 -0
  133. package/dist/ui/overlays/PromptPicker.js +129 -0
  134. package/dist/ui/overlays/PromptTextArea.d.ts +86 -0
  135. package/dist/ui/overlays/PromptTextArea.js +141 -0
  136. package/dist/ui/overlays/prompt-library-state.d.ts +162 -0
  137. package/dist/ui/overlays/prompt-library-state.js +335 -0
  138. package/dist/ui/overlays/prompt-picker-state.d.ts +71 -0
  139. package/dist/ui/overlays/prompt-picker-state.js +98 -0
  140. package/dist/ui/scrollback/band.js +14 -1
  141. package/dist/ui/team-status.d.ts +25 -0
  142. package/dist/ui/team-status.js +31 -3
  143. package/dist/ui/use-startup-notices.d.ts +35 -1
  144. package/dist/ui/use-startup-notices.js +29 -2
  145. package/dist/ui/use-wheel-routing.js +9 -1
  146. package/package.json +2 -2
@@ -0,0 +1,204 @@
1
+ /**
2
+ * The quick-prompt library's data model (quick-prompt-library §6.1).
3
+ *
4
+ * ASCII ONLY - `src/prompts/**` is inside `glyphs.test.ts::inScope`. Chinese copy
5
+ * lives in `copy.ts` as `\uXXXX` escapes, never as literal text in this tree.
6
+ *
7
+ * THE DISK SHAPE IS THE ONLY TRUTH (D-3 / I-6). `App` holds a `PromptLibrary`
8
+ * snapshot for rendering, every mutation goes through `store.ts`, and the store
9
+ * hands the new snapshot back. No component computes an optimistic library of its
10
+ * own, because "the UI says it was added and the disk disagrees" is a failure
11
+ * mode with no error message.
12
+ */
13
+ /**
14
+ * One entry.
15
+ *
16
+ * `useCount` / `createdAt` / `updatedAt` are ALWAYS present: a missing number
17
+ * would make every consumer write `?? 0` and one of them would eventually forget.
18
+ * `description` / `tags` / `favorite` / `lastUsedAt` are optional because "absent"
19
+ * is their honest state.
20
+ */
21
+ export interface QuickPrompt {
22
+ /** `randomUUID()`. Never shown in full; the list uses an 8-character prefix. */
23
+ id: string;
24
+ /** 1..80 code points, trimmed, no control characters. Never empty (I-5). */
25
+ title: string;
26
+ /** 1..8000 code points. Original newlines preserved; `\r\n` normalised to `\n`. */
27
+ content: string;
28
+ /** 0..200 code points. */
29
+ description?: string;
30
+ /** 0..6 tags, each 0..24 code points. */
31
+ tags?: string[];
32
+ favorite?: boolean;
33
+ /** Monotonic per entry (I-4). */
34
+ useCount: number;
35
+ /** Epoch ms. */
36
+ createdAt: number;
37
+ updatedAt: number;
38
+ lastUsedAt?: number;
39
+ }
40
+ /**
41
+ * The whole library.
42
+ *
43
+ * `prompts` IS the manual order (I-1): index 0 is what the list shows first and
44
+ * what `Ctrl+Up/Down` rearranges. There is no `order` field, because a second
45
+ * ordering authority is a second thing that can disagree with the first.
46
+ */
47
+ export interface PromptLibrary {
48
+ schema: 1;
49
+ prompts: QuickPrompt[];
50
+ /** Epoch ms of the last successful write; strictly increasing (I-3). */
51
+ updatedAt: number;
52
+ }
53
+ /**
54
+ * The persistent re-orderings `/prompts sort <key>` can name (D-9).
55
+ *
56
+ * A WRITE key, not a view: `PromptSortView` is what the picker renders and never
57
+ * persists, this is what the user may ask to have rewritten on disk. They overlap by two
58
+ * words and are deliberately separate types, because "sort by recent" and "show me the
59
+ * recent order for a moment" are different promises.
60
+ */
61
+ export type PromptSortKey = 'name' | 'recent' | 'favorite';
62
+ /** The picker's three presentations. A VIEW: never persisted (D-9). */
63
+ export type PromptSortView = 'manual' | 'recent' | 'favorite';
64
+ /**
65
+ * Why a write or a lookup failed. A stable code, never a sentence: the sentence
66
+ * comes from `copy.ts`, so the same failure has one wording on every surface.
67
+ */
68
+ export type PromptWriteError = 'title_required' | 'content_required' | 'title_too_long' | 'content_too_long' | 'description_too_long' | 'tag_too_long' | 'too_many_tags' | 'too_many_prompts' | 'not_found' | 'ambiguous' | 'write_failed';
69
+ /**
70
+ * The result of one mutation.
71
+ *
72
+ * THE LIBRARY TRAVELS WITH THE FAILURE TOO, and that is deliberate: a
73
+ * `write_failed` still applied in memory (D-4), so the caller has to be able to
74
+ * render what the user just created rather than pretending nothing happened.
75
+ */
76
+ export type PromptMutationResult = {
77
+ ok: true;
78
+ library: PromptLibrary;
79
+ id?: string;
80
+ } | {
81
+ ok: false;
82
+ code: PromptWriteError;
83
+ library: PromptLibrary;
84
+ };
85
+ /**
86
+ * A one-shot hand-off into the manager overlay (D-21).
87
+ *
88
+ * `App` never edits the overlay's state directly; it hands over an intent, and the
89
+ * overlay consumes it exactly once by nonce. That is what makes `/prompts edit 2`
90
+ * land in the editor with a prefilled draft instead of on row 0 of the list.
91
+ */
92
+ export type PromptLibraryEntry = {
93
+ kind: 'list';
94
+ focusId?: string;
95
+ } | {
96
+ kind: 'create';
97
+ seed?: {
98
+ title?: string;
99
+ content?: string;
100
+ };
101
+ } | {
102
+ kind: 'edit';
103
+ id: string;
104
+ } | {
105
+ kind: 'delete';
106
+ id: string;
107
+ };
108
+ /** `nonce` is monotonic; the same value is consumed at most once (I-8). */
109
+ export interface PromptLibraryIntent {
110
+ nonce: number;
111
+ entry: PromptLibraryEntry;
112
+ }
113
+ /**
114
+ * `<n|title>` resolved to an entry (D-22).
115
+ *
116
+ * `ambiguous` CARRIES ITS CANDIDATES. Titles may repeat (I-2), and a lookup that
117
+ * failed without saying which entries matched would leave the user with nothing
118
+ * to type next - which is exactly how `/prompts rm <title>` deletes the wrong
119
+ * prompt, and there is no recycle bin (D-13).
120
+ */
121
+ export type PromptRefResolution = {
122
+ ok: true;
123
+ prompt: QuickPrompt;
124
+ } | {
125
+ ok: false;
126
+ code: 'not_found';
127
+ } | {
128
+ ok: false;
129
+ code: 'ambiguous';
130
+ candidates: QuickPrompt[];
131
+ };
132
+ /**
133
+ * The editable form of one entry.
134
+ *
135
+ * `id: null` means "not saved yet". `tags` is the RAW field text: parsing it on
136
+ * every keystroke would rewrite what the user is typing as they type it.
137
+ */
138
+ export interface PromptEditorDraft {
139
+ id: string | null;
140
+ title: string;
141
+ content: string;
142
+ description: string;
143
+ tags: string;
144
+ favorite: boolean;
145
+ }
146
+ /** The three things the manager overlay may ask the store to do (P2-1: idempotent). */
147
+ export type PromptLibraryMutation = {
148
+ kind: 'create';
149
+ draft: PromptEditorDraft;
150
+ } | {
151
+ kind: 'update';
152
+ id: string;
153
+ draft: PromptEditorDraft;
154
+ } | {
155
+ kind: 'delete';
156
+ id: string;
157
+ } | {
158
+ kind: 'move';
159
+ id: string;
160
+ delta: -1 | 1;
161
+ } | {
162
+ kind: 'favorite';
163
+ id: string;
164
+ favorite: boolean;
165
+ };
166
+ /**
167
+ * What a command (or the picker) may ask the host to do (§6.3 / P1-3).
168
+ *
169
+ * THREE METHODS, and each one is something the command layer genuinely cannot do
170
+ * itself: open a surface, hand that surface a payload, or read the library through
171
+ * the same resolver the user's typing is interpreted with. Everything else
172
+ * (notifications, toasts, overlays in general) already exists on `CommandContext`
173
+ * and is not repeated here.
174
+ */
175
+ export interface PromptCommandPort {
176
+ /** Open the picker (`overlay: 'prompt-pick'`). */
177
+ openPicker(): void;
178
+ /** Open the manager on a specific page, with an optional prefilled draft. */
179
+ openManager(entry: PromptLibraryEntry): void;
180
+ /** `<n|title>` -> entry, through `resolvePromptRef` (D-22). */
181
+ resolve(ref: string): PromptRefResolution;
182
+ }
183
+ /** A validated draft, ready for the store. Produced by `finishPromptEditor`. */
184
+ export interface PromptDraftInput {
185
+ title: string;
186
+ content: string;
187
+ description?: string;
188
+ tags?: string[];
189
+ favorite?: boolean;
190
+ }
191
+ /**
192
+ * The verdict of validating a draft (§4.4 / D-12).
193
+ *
194
+ * `field` is the row that should own focus after a refusal, so the editor can jump
195
+ * there instead of leaving the user staring at a footer with an error code in it.
196
+ */
197
+ export type PromptDraftVerdict = {
198
+ ok: true;
199
+ input: PromptDraftInput;
200
+ } | {
201
+ ok: false;
202
+ code: PromptWriteError;
203
+ field: 'title' | 'content' | 'description' | 'tags';
204
+ };
@@ -0,0 +1,13 @@
1
+ /**
2
+ * The quick-prompt library's data model (quick-prompt-library §6.1).
3
+ *
4
+ * ASCII ONLY - `src/prompts/**` is inside `glyphs.test.ts::inScope`. Chinese copy
5
+ * lives in `copy.ts` as `\uXXXX` escapes, never as literal text in this tree.
6
+ *
7
+ * THE DISK SHAPE IS THE ONLY TRUTH (D-3 / I-6). `App` holds a `PromptLibrary`
8
+ * snapshot for rendering, every mutation goes through `store.ts`, and the store
9
+ * hands the new snapshot back. No component computes an optimistic library of its
10
+ * own, because "the UI says it was added and the disk disagrees" is a failure
11
+ * mode with no error message.
12
+ */
13
+ export {};
@@ -31,7 +31,7 @@
31
31
  */
32
32
  import { type Message, type ModelRef } from '@aragon-agent/core';
33
33
  import type { Entry, UsageTotal } from '../agent/reducer.js';
34
- import type { TodoItem } from '../todo/types.js';
34
+ import type { AcceptanceItem, TodoItem } from '../todo/types.js';
35
35
  import type { CompactionIdentity } from '../compaction/memory-identity.js';
36
36
  import { type SessionLockHandle } from './store.js';
37
37
  /**
@@ -58,6 +58,13 @@ export interface CheckpointDeps {
58
58
  compactionIdentity?: CompactionIdentity;
59
59
  };
60
60
  readTodos(): TodoItem[];
61
+ /**
62
+ * The acceptance verdicts, for the same reason `readTodos` exists: a checkpoint
63
+ * must not lose the conclusions the user was just shown
64
+ * (todo-acceptance-criteria §6.12). OPTIONAL so a caller that has no table — and
65
+ * every existing test double — keeps compiling and writes no key.
66
+ */
67
+ readAcceptance?(): AcceptanceItem[];
61
68
  readModel(): ModelRef;
62
69
  /** `meta.cwd`; read at every write because `/cwd` can change it. */
63
70
  readCwd(): string;
@@ -322,6 +322,9 @@ export class SessionCheckpointWriter {
322
322
  ...(input.compactionIdentity ? { compactionIdentity: input.compactionIdentity } : {}),
323
323
  entries: input.entries,
324
324
  todos: this.deps.readTodos(),
325
+ ...(this.deps.readAcceptance
326
+ ? { acceptance: this.deps.readAcceptance() }
327
+ : {}),
325
328
  meta: this.buildMeta(input),
326
329
  };
327
330
  try {
@@ -447,6 +450,9 @@ export class SessionCheckpointWriter {
447
450
  view.entries.length,
448
451
  lastEntry,
449
452
  this.deps.readTodos().length,
453
+ // A verdict landing must force a checkpoint even when the step list is
454
+ // unchanged: that is the one transition a `todos.length` key cannot see.
455
+ this.deps.readAcceptance?.().filter((item) => item.status !== 'pending').length ?? 0,
450
456
  this.pinned ? 'pin' : '-',
451
457
  ].join('|');
452
458
  }
@@ -7,7 +7,7 @@
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
+ import type { AcceptanceItem, TodoItem } from '../todo/types.js';
11
11
  import { type CompactionIdentity } from '../compaction/memory-identity.js';
12
12
  /**
13
13
  * Bookkeeping `aragon exec` attaches to the sessions it owns
@@ -75,6 +75,19 @@ export interface SavedSession {
75
75
  * right answer for a session that predates the feature (D-20 / D-22).
76
76
  */
77
77
  todos?: TodoItem[];
78
+ /**
79
+ * The acceptance table's VERDICTS at save time (todo-acceptance-criteria §6.12).
80
+ *
81
+ * ADDITIVE AND OPTIONAL, exactly like `todos` above, so `SESSION_VERSION` stays 1
82
+ * and an older file simply yields `undefined` - which restores as "no criteria"
83
+ * and leaves the rail, the card and the chip exactly as they were (AC-30).
84
+ *
85
+ * `reopen` is consumed before anything is stored and `in_progress` is a derived
86
+ * state, so each entry on disk carries only `content` / `check` / `status`
87
+ * (three-valued here) / `evidence`; `restore` lets the store re-derive which
88
+ * criterion is being checked.
89
+ */
90
+ acceptance?: AcceptanceItem[];
78
91
  /**
79
92
  * Written by `aragon exec` (cli-integration-surface §5.2 / D-10) AND by the
80
93
  * TUI's session checkpoint (tui-session-id-resume §6.1).
@@ -109,6 +122,11 @@ export declare function saveSession(filePath: string, data: {
109
122
  compactionIdentity?: CompactionIdentity;
110
123
  entries: Entry[];
111
124
  todos: TodoItem[];
125
+ /**
126
+ * The acceptance verdicts. OPTIONAL HERE, unlike `todos`, only to keep the field
127
+ * additive for callers that predate it; every writer in this package passes it.
128
+ */
129
+ acceptance?: AcceptanceItem[];
112
130
  meta?: SessionMeta;
113
131
  }): void;
114
132
  /**
@@ -8,6 +8,7 @@
8
8
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
9
9
  import { dirname, isAbsolute, join, resolve } from 'node:path';
10
10
  import { getSessionsDir } from '../config/store.js';
11
+ import { stripEphemeralRuns } from '../team/settled.js';
11
12
  import { validateSession } from './validate-session.js';
12
13
  import { validateCompactionIdentity, verifyCompactionIdentity, } from '../compaction/memory-identity.js';
13
14
  import { getLogger } from '../logging/logger.js';
@@ -109,8 +110,19 @@ export function saveSession(filePath, data) {
109
110
  model: data.model,
110
111
  messages: data.messages,
111
112
  ...(data.compactionIdentity ? { compactionIdentity: data.compactionIdentity } : {}),
112
- entries: stripServiceTail(stripLiveToolOutput(data.entries)),
113
+ // THE EPHEMERAL-RUN GATE IS THE THIRD STRIP IN THIS CHAIN (tui-subagent-panel
114
+ // §4.5.4 / RV-P0-1), and it is the only one that protects EVERY write path:
115
+ // `/save`, the session checkpoint and the exit flush all arrive here. Without it a
116
+ // dispatch-time save would carry each child's 24-event timeline into the file, no
117
+ // matter how careful the dispatch exit is - the live `team` entry holds the LIVE
118
+ // runs while the dispatch is running (`agent/reducer.ts::teamUpdate`).
119
+ entries: stripEphemeralRuns(stripServiceTail(stripLiveToolOutput(data.entries))),
113
120
  todos: data.todos,
121
+ // ONE KEY, WRITTEN ONLY WHEN THERE IS SOMETHING TO WRITE, so a session with the
122
+ // capability off produces a file byte-identical to today's.
123
+ ...(data.acceptance && data.acceptance.length > 0
124
+ ? { acceptance: data.acceptance }
125
+ : {}),
114
126
  ...(data.meta ? { meta: data.meta } : {}),
115
127
  };
116
128
  writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf-8');
@@ -20,7 +20,7 @@
20
20
  */
21
21
  import type { Message } from '@aragon-agent/core';
22
22
  import type { Entry } from '../agent/reducer.js';
23
- import type { TodoItem } from '../todo/types.js';
23
+ import type { AcceptanceItem, TodoItem } from '../todo/types.js';
24
24
  import type { CompactionIdentity } from '../compaction/memory-identity.js';
25
25
  import { type SavedSession } from './persist.js';
26
26
  /** Who this process is, as the rest of the CLI sees it. */
@@ -50,7 +50,12 @@ export interface StartupSessionInput {
50
50
  */
51
51
  export interface SessionRestorePort {
52
52
  replaceMessages(messages: Message[], identity?: CompactionIdentity): void;
53
- restoreTodos(items: TodoItem[]): void;
53
+ /**
54
+ * The acceptance table is OPTIONAL on this port, so a test double or an `exec`
55
+ * caller that predates the feature keeps compiling and restores "no criteria" -
56
+ * the same additive shape as `SavedSession.acceptance`.
57
+ */
58
+ restoreTodos(items: TodoItem[], acceptance?: AcceptanceItem[]): void;
54
59
  setModel(providerId: string, modelId: string, baseUrl?: string): void;
55
60
  }
56
61
  export type StartupSessionResolution = {
@@ -39,8 +39,11 @@ import { basenameId, isValidSessionId, looksLikeSessionPath, resolveSessionRef,
39
39
  */
40
40
  export function applyRestoredSession(port, saved, opts = {}) {
41
41
  port.replaceMessages(saved.messages, saved.compactionIdentity);
42
- // Absence clears the previous conversation's list, including old files.
43
- port.restoreTodos(saved.todos ?? []);
42
+ // Absence clears the previous conversation's plan, including old files. The
43
+ // verdicts come back with it, for the reason `SavedSession.acceptance` records:
44
+ // restoring the steps and dropping the conclusions shows a plan as unstarted that
45
+ // the user already watched being verified.
46
+ port.restoreTodos(saved.todos ?? [], saved.acceptance ?? []);
44
47
  if (!opts.skipModel && saved.model?.providerId && saved.model?.modelId) {
45
48
  port.setModel(saved.model.providerId, saved.model.modelId, saved.model.baseUrl);
46
49
  }
@@ -188,6 +188,9 @@ export function writeSession(input) {
188
188
  ? { compactionIdentity: input.session.compactionIdentity } : {}),
189
189
  entries: input.session.entries,
190
190
  todos: input.session.todos ?? [],
191
+ // Optional on the input, so an `exec` caller that has no acceptance table writes
192
+ // no key at all instead of an empty one.
193
+ ...(input.session.acceptance ? { acceptance: input.session.acceptance } : {}),
191
194
  meta: input.meta,
192
195
  });
193
196
  writePointer(input.meta.cwd, input.id, input.meta.updatedAt);
@@ -1,6 +1,7 @@
1
1
  import { isAdapterProvider } from '../config/schema.js';
2
2
  import { normalizeTodos } from '../todo/normalize.js';
3
3
  import { TODO_LIMITS } from '../todo/limits.js';
4
+ import { acceptanceSummary, normalizeAcceptance } from '../todo/acceptance.js';
4
5
  import { validateCompactionIdentity } from '../compaction/memory-identity.js';
5
6
  import { MemoryError } from '../compaction/memory.js';
6
7
  function fail(path, expected) {
@@ -160,7 +161,26 @@ const ENTRY_FIELDS = {
160
161
  team: { dispatchId: string, requested: number, runs: array(subagent), aborted: boolean,
161
162
  durationMs: optional(number), active: boolean },
162
163
  todo: { items: array(todo), doneCount: number, total: number, live: boolean,
163
- interrupted: optional(boolean) },
164
+ interrupted: optional(boolean),
165
+ // THE ACCEPTANCE BLOCK ON A TRANSCRIPT CARD (todo-acceptance-criteria §6.12).
166
+ // IT IS THE PROJECTION, NOT THE RAW TABLE: `Entry.acceptance` is an
167
+ // `AcceptanceSnapshot | null` (`agent/reducer.ts`), because the card reads
168
+ // `checkedCount` / `failedCount` without re-walking the rows - and `null` is a
169
+ // real value there, the one every card written while the capability is off
170
+ // holds. Demanding an items array here would make every session containing a todo
171
+ // card unloadable, and the CLI would tell the user to delete their conversation.
172
+ // OPTIONAL AND LOOSELY TYPED, then normalized per entry below: a card saved before
173
+ // this feature has no such key, and an item that is malformed must cost that one
174
+ // criterion rather than the whole session. "Strict at the boundary" means the
175
+ // SHAPE is enforced, not that a single bad row takes the file down.
176
+ acceptance: optional((value, path) => {
177
+ if (value === null)
178
+ return;
179
+ const snapshot = record(value, path);
180
+ if (!Array.isArray(snapshot.items))
181
+ fail(`${path}.items`, 'an array');
182
+ optional(number)(snapshot.updatedAt, `${path}.updatedAt`);
183
+ }) },
164
184
  retry: {
165
185
  attempt: number, maxRetries: number, errorType: string, message: string, delayMs: number,
166
186
  resumeAt: optional(number), startedAt: number, totalRetries: optional(number),
@@ -256,5 +276,48 @@ export function validateSession(value) {
256
276
  if (source.todos !== undefined) {
257
277
  prepared.todos = normalizeTodos(source.todos, TODO_LIMITS.maxItems).items;
258
278
  }
279
+ // THE SESSION FILE'S OWN TABLE, normalized exactly like a model payload: a
280
+ // user-editable file gets a model payload's treatment (`TodoStore.restore`'s
281
+ // standing argument). Absent stays absent, so an old file loads unchanged.
282
+ if (source.acceptance !== undefined) {
283
+ prepared.acceptance = normalizeAcceptance(source.acceptance, [], { enabled: true }).items;
284
+ }
285
+ prepared.entries = sanitizeEntryAcceptance(prepared.entries);
259
286
  return prepared;
260
287
  }
288
+ /**
289
+ * Drop the acceptance rows a persisted card cannot use, keeping every usable one.
290
+ *
291
+ * ONE BAD ROW MUST NOT COST THE SESSION: the card is HISTORY, and a resume that
292
+ * refuses because one evidence string was a number would throw away the
293
+ * conversation to protect a footnote. Rows are repaired by the same normalizer the
294
+ * model's payloads go through, which is also what guarantees a card on screen and a
295
+ * card in the file agree about what a criterion is.
296
+ *
297
+ * THE PROJECTION IS REBUILT FROM ITS ROWS, never trusted: `total` / `checkedCount` /
298
+ * `failedCount` are derived quantities, and a file that edited them (or a build that
299
+ * later changed how they are counted) would otherwise draw a header that disagrees
300
+ * with the rows underneath it. `updatedAt` is carried over so a card does not claim
301
+ * to have changed when it was only reloaded; a row set that ends up empty becomes
302
+ * `null`, which is what the card means by "no acceptance block".
303
+ */
304
+ function sanitizeEntryAcceptance(entries) {
305
+ if (!Array.isArray(entries))
306
+ return entries;
307
+ return entries.map((entry) => {
308
+ if (!entry || typeof entry !== 'object')
309
+ return entry;
310
+ const card = entry;
311
+ if (card.kind !== 'todo' || card.acceptance === undefined)
312
+ return entry;
313
+ if (card.acceptance === null)
314
+ return card;
315
+ const snapshot = card.acceptance;
316
+ const items = normalizeAcceptance(snapshot.items, [], { enabled: true }).items;
317
+ const updatedAt = typeof snapshot.updatedAt === 'number' ? snapshot.updatedAt : 0;
318
+ return {
319
+ ...card,
320
+ acceptance: items.length > 0 ? acceptanceSummary(items, () => updatedAt) : null,
321
+ };
322
+ });
323
+ }
@@ -96,6 +96,31 @@ export declare const TEAM_LIMITS: {
96
96
  * command; below it, a basename or a program name. Mirrors `statusCompactCols`.
97
97
  * This chooses WHICH FACT to show; `activityBudget` chooses how much of it. */
98
98
  readonly activityWideCols: 100;
99
+ /**
100
+ * Below this many columns the roster's activity cell gives way ENTIRELY
101
+ * (tui-subagent-panel §4.2.2).
102
+ *
103
+ * STRUCTURAL, like everything else in this block: twelve characters of prose in
104
+ * a 40-column terminal push the description column below its own twelve-column
105
+ * floor, and "who is doing what" outranks "what they are writing right now" when
106
+ * there is room for only one. That is why the cell returns 0 rather than
107
+ * `activityMinChars` below this width.
108
+ */
109
+ readonly activityDropCols: 56;
110
+ /**
111
+ * Events kept per child in `SubagentRun.timeline` (§4.5.3). Twenty-four covers
112
+ * a typical dispatch's complete tool sequence; ten children x 24 x 120 chars is
113
+ * about 29 KB at peak, and the ring is EPHEMERAL - it never reaches a file.
114
+ */
115
+ readonly timelineEntries: 24;
116
+ /** Display clamp on one timeline line; the same order as `activityArgChars`. */
117
+ readonly timelineTextChars: 120;
118
+ /**
119
+ * Row ceiling of the Agents board's projection. The overlay scrolls, so this is
120
+ * a memory bound rather than a fit: one row per fact and at most ten children
121
+ * plus one detail section, which no healthy dispatch can reach.
122
+ */
123
+ readonly boardMaxRows: 240;
99
124
  /** Retries of a child that produced NOTHING. One, and not configurable: a
100
125
  * second retry doubles the worst-case latency of a failing dispatch and buys
101
126
  * nothing a re-dispatch by the lead would not buy better. */
@@ -107,6 +107,32 @@ export const TEAM_LIMITS = {
107
107
  * command; below it, a basename or a program name. Mirrors `statusCompactCols`.
108
108
  * This chooses WHICH FACT to show; `activityBudget` chooses how much of it. */
109
109
  activityWideCols: 100,
110
+ /**
111
+ * Below this many columns the roster's activity cell gives way ENTIRELY
112
+ * (tui-subagent-panel §4.2.2).
113
+ *
114
+ * STRUCTURAL, like everything else in this block: twelve characters of prose in
115
+ * a 40-column terminal push the description column below its own twelve-column
116
+ * floor, and "who is doing what" outranks "what they are writing right now" when
117
+ * there is room for only one. That is why the cell returns 0 rather than
118
+ * `activityMinChars` below this width.
119
+ */
120
+ activityDropCols: 56,
121
+ // --- The Agents board (tui-subagent-panel §4.4 / §4.5) ---
122
+ /**
123
+ * Events kept per child in `SubagentRun.timeline` (§4.5.3). Twenty-four covers
124
+ * a typical dispatch's complete tool sequence; ten children x 24 x 120 chars is
125
+ * about 29 KB at peak, and the ring is EPHEMERAL - it never reaches a file.
126
+ */
127
+ timelineEntries: 24,
128
+ /** Display clamp on one timeline line; the same order as `activityArgChars`. */
129
+ timelineTextChars: 120,
130
+ /**
131
+ * Row ceiling of the Agents board's projection. The overlay scrolls, so this is
132
+ * a memory bound rather than a fit: one row per fact and at most ten children
133
+ * plus one detail section, which no healthy dispatch can reach.
134
+ */
135
+ boardMaxRows: 240,
110
136
  // --- Cold-start retry (team-live-activity F-4) ---
111
137
  /** Retries of a child that produced NOTHING. One, and not configurable: a
112
138
  * second retry doubles the worst-case latency of a failing dispatch and buys
@@ -28,6 +28,8 @@
28
28
  * pre-feature behaviour.
29
29
  */
30
30
  import { TeamBus } from './bus.js';
31
+ import { settledRun } from './settled.js';
32
+ import { appendTimeline, withSettleEntry } from './timeline.js';
31
33
  import { TEAM_AGGREGATE_LABEL, TEAM_LIMITS } from './limits.js';
32
34
  import { TeamOverseer, buildReplacementPrompt, nextOverseerLadderStepMs, resolveOverseerFirstCheckMs, } from './overseer.js';
33
35
  import { shouldRetryColdStart } from './retry.js';
@@ -581,7 +583,13 @@ export class TeamRuntime {
581
583
  }), { inputTokens: 0, outputTokens: 0 });
582
584
  const outcome = {
583
585
  dispatchId,
584
- runs: handles.map((h) => ({ ...h.run, filesTouched: [...h.run.filesTouched] })),
586
+ // THE DISPATCH EXIT'S STRIP (tui-subagent-panel §4.5.4). `settledRun` drops the
587
+ // four fields with no reader on a settled run (`activity` / `activityArgs` /
588
+ // `retry` / `timeline`) and KEEPS `lastIntervention`, which the settled card's
589
+ // badge and the supervision report read. The save boundary strips the fifth one
590
+ // (`team/settled.ts` + `session/persist.ts`) - one gate is not enough, because
591
+ // the live entry's runs are written to disk during the dispatch.
592
+ runs: handles.map((h) => settledRun(h.run)),
585
593
  requested,
586
594
  startedAt,
587
595
  endedAt: Date.now(),
@@ -999,6 +1007,16 @@ export class TeamRuntime {
999
1007
  at,
1000
1008
  reasonHead: decision.reason.slice(0, 60),
1001
1009
  };
1010
+ // The board's timeline records the supervisor's ACTIONS only (tui-subagent-panel
1011
+ // §4.5.2): a `wait` decision is an inspection, not an intervention, and one line
1012
+ // per inspection would fill the ring with `nothing happened`.
1013
+ if (decision.action !== 'wait') {
1014
+ handle.run.timeline = appendTimeline(handle.run.timeline, {
1015
+ kind: 'overseer',
1016
+ at,
1017
+ text: decision.action + ': ' + decision.reason.slice(0, 60),
1018
+ });
1019
+ }
1002
1020
  this.publish(dispatchId, handle.run);
1003
1021
  }
1004
1022
  }
@@ -1199,7 +1217,14 @@ export class TeamRuntime {
1199
1217
  // Events
1200
1218
  // -------------------------------------------------------------------------
1201
1219
  publish(dispatchId, run) {
1202
- const copy = { ...run, filesTouched: [...run.filesTouched] };
1220
+ const copy = {
1221
+ // The attempt's terminal event rides the EMITTED snapshot only (see
1222
+ // `timeline.ts::withSettleEntry`): `publish` is every state change's single exit,
1223
+ // so this is the one place that also covers the two paths which set a terminal
1224
+ // phase without going through `settlePhase` (§4.5.2 / RV-P1-3).
1225
+ ...withSettleEntry(run, Date.now()),
1226
+ filesTouched: [...run.filesTouched],
1227
+ };
1203
1228
  if (this.snapshotState && this.snapshotState.dispatchId === dispatchId) {
1204
1229
  this.snapshotState = {
1205
1230
  ...this.snapshotState,
@@ -0,0 +1,57 @@
1
+ /**
2
+ * The two stripping lists, and the two functions that apply them
3
+ * (tui-subagent-panel §4.5.4).
4
+ *
5
+ * WHY TWO LISTS AND NOT ONE. The fields fall into three groups, not two:
6
+ *
7
+ * - `activity` / `activityArgs` / `retry` / `timeline` have NO reader on a settled run
8
+ * (the card and the report read `summary` / `error` / the counters), so they are
9
+ * removed at the dispatch exit as well;
10
+ * - `lastIntervention` MUST survive onto the settled run -
11
+ * `overseer-runtime-supervision.test.ts` asserts it and the supervision report reads
12
+ * it - but `team/types.ts` marks it TRANSIENT / never persisted, and it is not part
13
+ * of `session/validate-session.ts`'s persisted shape either. So it is stripped at
14
+ * the SAVE boundary only;
15
+ * - `retryable`, `truncated`, `compactions`, `lastTool`, `summary`, `error` are facts
16
+ * that still mean something after the dispatch, and are in NEITHER list. Deleting
17
+ * `retryable` in passing would be the kind of "cleanup" that loses a contract.
18
+ *
19
+ * WHY TWO PLACES AND NOT ONE (§4.5.4 / RV-P0-1). `dispatch()`'s exit only cleans the
20
+ * FINAL outcome. Everything written to disk goes through `saveSession`, and during a
21
+ * dispatch the live `team` entry holds the LIVE runs - `teamUpdate` writes them
22
+ * straight into `view.entries`. So a `/save`, a checkpoint or the exit flush issued
23
+ * mid-dispatch would carry the timeline into `<id>.json` even with the dispatch exit
24
+ * perfectly clean. The save boundary is therefore the second, independent gate, and it
25
+ * is the one that protects all three write paths (`R-12`).
26
+ *
27
+ * WHY THIS IS NOT IN `runtime.ts` (RV-P1-4): that file is already past the 1,000-line
28
+ * ceiling `CLAUDE.md` sets and has no dated exception in `.claude-index/config.md`, so
29
+ * new logic goes here and `runtime.ts` keeps one call.
30
+ *
31
+ * ASCII ONLY: `src/team/**` is inside `glyphs.test.ts`'s static scan.
32
+ */
33
+ import type { Entry } from '../agent/reducer.js';
34
+ import type { SubagentRun } from './types.js';
35
+ /** Fields with no reader on a settled run. Stripped at the dispatch exit. */
36
+ export declare const SETTLE_STRIP_FIELDS: readonly ["activity", "activityArgs", "retry", "timeline"];
37
+ /**
38
+ * Fields that may never reach a file. `lastIntervention` joins the four above because
39
+ * it must live on the settled run while staying out of every persisted one.
40
+ */
41
+ export declare const PERSIST_STRIP_FIELDS: readonly ["activity", "activityArgs", "retry", "timeline", "lastIntervention"];
42
+ /**
43
+ * The dispatch exit: one run, without `SETTLE_STRIP_FIELDS`.
44
+ *
45
+ * `lastIntervention` is deliberately KEPT (see this file's header) - the settled card's
46
+ * badge and the supervision report both read it.
47
+ */
48
+ export declare function settledRun(run: SubagentRun): SubagentRun;
49
+ /**
50
+ * The save boundary: every `kind: 'team'` entry's runs, without
51
+ * `PERSIST_STRIP_FIELDS`.
52
+ *
53
+ * Entries that carry no team runs - or whose runs carry none of the fields - are
54
+ * returned UNTOUCHED, which is what keeps an ordinary session file byte-identical to
55
+ * the one this build produced before the feature.
56
+ */
57
+ export declare function stripEphemeralRuns(entries: Entry[]): Entry[];