@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
@@ -41,17 +41,33 @@ export declare function nextMode(current: AgentMode): AgentMode;
41
41
  * PowerShell, a foreign process resetting the mode), so a rung that depends on
42
42
  * NOTHING has to exist underneath it.
43
43
  *
44
- * `ctrl+p` IS MEASURED, not guessed. On the lossy path it arrives as `0x10`,
44
+ * `ctrl+n` IS MEASURED, not guessed. On the lossy path it arrives as `0x0e`,
45
45
  * because Windows generates a real control character for `Ctrl+<letter>` and
46
46
  * libuv's character branch forwards it untouched; `Alt+M` produces no bytes at
47
47
  * all and `Ctrl+Tab` collapses exactly like `Shift+Tab` (analysis section 3.6).
48
48
  * Any idle `Ctrl+<letter>` in the `0x01-0x1A` range would work; this one is free
49
- * throughout the package - `keymap.ts` claims a/e/w/u/k, `App` claims c/l/t/o,
49
+ * throughout the package - `keymap.ts` claims a/e/w/u/k, `App` claims c/g/k/l/o/r/t,
50
50
  * and `PromptInput` returns early on every `key.ctrl` - so binding it takes no
51
51
  * behaviour away from anyone. `Ctrl+B` survives the same path but is tmux's
52
52
  * default prefix, and this binding is unconditional and cross-platform, so it
53
53
  * would be silently dead for a whole class of macOS and Linux users.
54
54
  *
55
+ * THIS KEY MOVED FROM `ctrl+p`, WHICH NOW OPENS THE PROMPT LIBRARY
56
+ * (quick-prompt-library D-1). The requirement names `ctrl+p` as the first entry
57
+ * to a feature users will build muscle memory for, and a key that is sometimes
58
+ * the prompt library and sometimes the mode switch is a key nobody can learn.
59
+ * `ctrl+n` was the only idle C0 byte left that survives the same lossy path, and
60
+ * it carries no conflicting behaviour: readline's `next-history` is `Up`/`Down`
61
+ * in this product.
62
+ *
63
+ * `^N` IS NOT AN ACCEPTABLE SPELLING OF IT, ANYWHERE. `^N` is already this
64
+ * product's word for TRANSCRIPT LINE OFFSET - the header's scroll chip reads
65
+ * `^<n>` and the help overlay has a `^N` row about the distance from the bottom -
66
+ * while every other `^X` in the same row is a key. Writing the mode fallback as
67
+ * `^N` would put two meanings of `^` on one line, so the header, the help
68
+ * overlay, `doctor`, the README and the one-shot migration notice all spell out
69
+ * `Ctrl+N`.
70
+ *
55
71
  * BOUND UNCONDITIONALLY, on every platform. A binding that only exists where it
56
72
  * is needed cannot be documented in one place, cannot be taught by muscle
57
73
  * memory, and is missing on precisely the machine whose capability probe was
@@ -59,13 +75,13 @@ export declare function nextMode(current: AgentMode): AgentMode;
59
75
  *
60
76
  * THESE ARE THE LABELS, NOT THE BINDING. Editing a value here changes only what
61
77
  * the user is TOLD to press - the hint row, the startup notice, `doctor`. The
62
- * key that is actually listened for is `input === 'p'` in `App`'s `useInput`,
78
+ * key that is actually listened for is `input === 'n'` in `App`'s `useInput`,
63
79
  * and the help overlay and README spell both keys out in their own title case.
64
80
  * Change one without the others and the product advertises a key it ignores.
65
81
  */
66
82
  export declare const MODE_TOGGLE_KEYS: {
67
83
  readonly primary: "shift+tab";
68
- readonly fallback: "ctrl+p";
84
+ readonly fallback: "ctrl+n";
69
85
  };
70
86
  /** The two tools that only exist because a human is attached. */
71
87
  export declare const PLAN_TOOL_NAMES: readonly ["ask_user", "submit_plan"];
@@ -43,17 +43,33 @@ export function nextMode(current) {
43
43
  * PowerShell, a foreign process resetting the mode), so a rung that depends on
44
44
  * NOTHING has to exist underneath it.
45
45
  *
46
- * `ctrl+p` IS MEASURED, not guessed. On the lossy path it arrives as `0x10`,
46
+ * `ctrl+n` IS MEASURED, not guessed. On the lossy path it arrives as `0x0e`,
47
47
  * because Windows generates a real control character for `Ctrl+<letter>` and
48
48
  * libuv's character branch forwards it untouched; `Alt+M` produces no bytes at
49
49
  * all and `Ctrl+Tab` collapses exactly like `Shift+Tab` (analysis section 3.6).
50
50
  * Any idle `Ctrl+<letter>` in the `0x01-0x1A` range would work; this one is free
51
- * throughout the package - `keymap.ts` claims a/e/w/u/k, `App` claims c/l/t/o,
51
+ * throughout the package - `keymap.ts` claims a/e/w/u/k, `App` claims c/g/k/l/o/r/t,
52
52
  * and `PromptInput` returns early on every `key.ctrl` - so binding it takes no
53
53
  * behaviour away from anyone. `Ctrl+B` survives the same path but is tmux's
54
54
  * default prefix, and this binding is unconditional and cross-platform, so it
55
55
  * would be silently dead for a whole class of macOS and Linux users.
56
56
  *
57
+ * THIS KEY MOVED FROM `ctrl+p`, WHICH NOW OPENS THE PROMPT LIBRARY
58
+ * (quick-prompt-library D-1). The requirement names `ctrl+p` as the first entry
59
+ * to a feature users will build muscle memory for, and a key that is sometimes
60
+ * the prompt library and sometimes the mode switch is a key nobody can learn.
61
+ * `ctrl+n` was the only idle C0 byte left that survives the same lossy path, and
62
+ * it carries no conflicting behaviour: readline's `next-history` is `Up`/`Down`
63
+ * in this product.
64
+ *
65
+ * `^N` IS NOT AN ACCEPTABLE SPELLING OF IT, ANYWHERE. `^N` is already this
66
+ * product's word for TRANSCRIPT LINE OFFSET - the header's scroll chip reads
67
+ * `^<n>` and the help overlay has a `^N` row about the distance from the bottom -
68
+ * while every other `^X` in the same row is a key. Writing the mode fallback as
69
+ * `^N` would put two meanings of `^` on one line, so the header, the help
70
+ * overlay, `doctor`, the README and the one-shot migration notice all spell out
71
+ * `Ctrl+N`.
72
+ *
57
73
  * BOUND UNCONDITIONALLY, on every platform. A binding that only exists where it
58
74
  * is needed cannot be documented in one place, cannot be taught by muscle
59
75
  * memory, and is missing on precisely the machine whose capability probe was
@@ -61,11 +77,11 @@ export function nextMode(current) {
61
77
  *
62
78
  * THESE ARE THE LABELS, NOT THE BINDING. Editing a value here changes only what
63
79
  * the user is TOLD to press - the hint row, the startup notice, `doctor`. The
64
- * key that is actually listened for is `input === 'p'` in `App`'s `useInput`,
80
+ * key that is actually listened for is `input === 'n'` in `App`'s `useInput`,
65
81
  * and the help overlay and README spell both keys out in their own title case.
66
82
  * Change one without the others and the product advertises a key it ignores.
67
83
  */
68
- export const MODE_TOGGLE_KEYS = { primary: 'shift+tab', fallback: 'ctrl+p' };
84
+ export const MODE_TOGGLE_KEYS = { primary: 'shift+tab', fallback: 'ctrl+n' };
69
85
  /** The two tools that only exist because a human is attached. */
70
86
  export const PLAN_TOOL_NAMES = ['ask_user', 'submit_plan'];
71
87
  /**
@@ -30,7 +30,7 @@ import { type FastStatus } from '../fast/wiring.js';
30
30
  import type { FastEventListener, FastSnapshot } from '../fast/types.js';
31
31
  import { ContextMeter, type ContextUsageListener } from '../compaction/meter.js';
32
32
  import type { CompactionEventListener, CompactionSnapshot, ContextUsageSnapshot } from '../compaction/types.js';
33
- import type { TodoEventListener, TodoSnapshot } from '../todo/types.js';
33
+ import type { AcceptanceItem, TodoEventListener, TodoSnapshot } from '../todo/types.js';
34
34
  import { type HumanInputGate } from '../tools/human-input.js';
35
35
  import { SkillService, type ApprovalGate } from '../skills/service.js';
36
36
  import { type ConversationResetOptions, type ConversationResetResult } from './conversation-reset.js';
@@ -587,6 +587,37 @@ export declare class AgentController {
587
587
  */
588
588
  setTodoConfig(patch: Partial<TodoConfig>): TodoConfig;
589
589
  getTodoSnapshot(): TodoSnapshot | null;
590
+ /**
591
+ * Whether the acceptance table is live for this session: the config key AND the
592
+ * session switch. Two flags, exactly as `isTodoEnabled` needs `isTodoRegistered`
593
+ * first - a capability that is on while `todo_write` refuses is not a
594
+ * capability.
595
+ */
596
+ isTodoAcceptanceEnabled(): boolean;
597
+ /**
598
+ * Flip the acceptance capability and RE-RENDER THE PROMPT, byte for byte the
599
+ * contract `setTodoEnabled` records above: the paragraph is part of
600
+ * `<todo_planning>`, and the tool reads the same live value through its
601
+ * `hasAcceptance` closure (D-11).
602
+ */
603
+ setTodoAcceptanceEnabled(enabled: boolean): void;
604
+ /**
605
+ * The acceptance table as DATA, independent of whether it is being drawn.
606
+ *
607
+ * The persistence surfaces must use this and not `getTodoSnapshot()?.acceptance`:
608
+ * the projection is `null` whenever the capability is off, and a checkpoint or a
609
+ * `/save` that silently dropped the user's verdicts because a display switch was
610
+ * off would be a data loss dressed as a setting.
611
+ */
612
+ getAcceptanceItems(): AcceptanceItem[];
613
+ /**
614
+ * `/todo acceptance reset`: every verdict back to `pending`, evidence dropped.
615
+ *
616
+ * The count comes BACK from the store rather than being recomputed by the
617
+ * command, so the notice and the state cannot disagree about how many criteria
618
+ * moved.
619
+ */
620
+ reopenAcceptance(): number;
590
621
  /**
591
622
  * Subscribe to the CLI-local todo event stream.
592
623
  *
@@ -594,8 +625,12 @@ export declare class AgentController {
594
625
  * every caller can subscribe unconditionally.
595
626
  */
596
627
  subscribeTodos(listener: TodoEventListener): () => void;
597
- /** `/resume`. An absent or empty list CLEARS — see `TodoStore.restore` (P0-2). */
598
- restoreTodos(items: unknown): void;
628
+ /**
629
+ * `/resume`. An absent or empty todo list CLEARS both tables — see
630
+ * `TodoStore.restore` (P0-2). `acceptance` is optional so every existing caller
631
+ * keeps compiling, and an omitted table reads as "this file has none".
632
+ */
633
+ restoreTodos(items: unknown, acceptance?: unknown): void;
599
634
  /**
600
635
  * `/todo clear` and `/clear screen`. A user override of a projection (I-2's
601
636
  * standing exception). `/todo` refuses mid-run before it gets here;
@@ -346,7 +346,9 @@ export class AgentController {
346
346
  this.todoRegistered = config.todo.enabled;
347
347
  this.todoEnabled = config.todo.enabled;
348
348
  this.todoPanelCapable = deps.todoPanelCapable === true;
349
- this.todos = this.todoRegistered ? new TodoStore() : null;
349
+ this.todos = this.todoRegistered
350
+ ? new TodoStore(Date.now, () => this.isTodoAcceptanceEnabled())
351
+ : null;
350
352
  // --- Fast model tier ---------------------------------------------------
351
353
  //
352
354
  // RESOLVED BEFORE THE AGENT, because `fastRegistered` decides whether the
@@ -558,6 +560,12 @@ export class AgentController {
558
560
  store: this.todos,
559
561
  // Read LIVE: `/todo off` flips the flag, it cannot unregister.
560
562
  isEnabled: () => this.todoEnabled,
563
+ // LIVE for the same reason (D-11): `/todo acceptance off` must take
564
+ // effect in a session already running, because the tool array
565
+ // itself cannot change. THE SAME PREDICATE the store was given, on
566
+ // purpose: two switches for one capability that could disagree is
567
+ // the shape of a session that cannot exist (IF-7).
568
+ hasAcceptance: () => this.isTodoAcceptanceEnabled(),
561
569
  }),
562
570
  ]
563
571
  : [],
@@ -912,6 +920,9 @@ export class AgentController {
912
920
  todoBlock: this.todoRegistered && this.todoEnabled
913
921
  ? buildTodoBlock({
914
922
  panelVisible: this.todoPanelCapable && config.todo.panel,
923
+ // LIVE, never snapshotted (D-11): the paragraph is part of the
924
+ // block, and `/todo acceptance off` rebuilds the prompt.
925
+ acceptance: config.todo.acceptance,
915
926
  })
916
927
  : '',
917
928
  // The SAME two flags one more time, plus the live resolution — all three
@@ -1594,6 +1605,51 @@ export class AgentController {
1594
1605
  getTodoSnapshot() {
1595
1606
  return this.todos?.snapshot() ?? null;
1596
1607
  }
1608
+ /**
1609
+ * Whether the acceptance table is live for this session: the config key AND the
1610
+ * session switch. Two flags, exactly as `isTodoEnabled` needs `isTodoRegistered`
1611
+ * first - a capability that is on while `todo_write` refuses is not a
1612
+ * capability.
1613
+ */
1614
+ isTodoAcceptanceEnabled() {
1615
+ return this.config.todo.acceptance && this.todoEnabled;
1616
+ }
1617
+ /**
1618
+ * Flip the acceptance capability and RE-RENDER THE PROMPT, byte for byte the
1619
+ * contract `setTodoEnabled` records above: the paragraph is part of
1620
+ * `<todo_planning>`, and the tool reads the same live value through its
1621
+ * `hasAcceptance` closure (D-11).
1622
+ */
1623
+ setTodoAcceptanceEnabled(enabled) {
1624
+ this.setTodoConfig({ acceptance: enabled });
1625
+ // THE RAIL READS THE PROJECTION, NOT THE FLAG, so the flip has to re-project or
1626
+ // `/todo acceptance off` would leave the second section on screen until the
1627
+ // model's next write (P0-2's complaint, one path further in). The verdicts
1628
+ // themselves are kept: `getAcceptanceItems()` still returns them for `/save`,
1629
+ // the checkpoint and `exec`.
1630
+ this.todos?.refreshAcceptanceProjection();
1631
+ }
1632
+ /**
1633
+ * The acceptance table as DATA, independent of whether it is being drawn.
1634
+ *
1635
+ * The persistence surfaces must use this and not `getTodoSnapshot()?.acceptance`:
1636
+ * the projection is `null` whenever the capability is off, and a checkpoint or a
1637
+ * `/save` that silently dropped the user's verdicts because a display switch was
1638
+ * off would be a data loss dressed as a setting.
1639
+ */
1640
+ getAcceptanceItems() {
1641
+ return this.todos?.acceptanceItems() ?? [];
1642
+ }
1643
+ /**
1644
+ * `/todo acceptance reset`: every verdict back to `pending`, evidence dropped.
1645
+ *
1646
+ * The count comes BACK from the store rather than being recomputed by the
1647
+ * command, so the notice and the state cannot disagree about how many criteria
1648
+ * moved.
1649
+ */
1650
+ reopenAcceptance() {
1651
+ return this.todos?.reopenAll() ?? 0;
1652
+ }
1597
1653
  /**
1598
1654
  * Subscribe to the CLI-local todo event stream.
1599
1655
  *
@@ -1605,9 +1661,13 @@ export class AgentController {
1605
1661
  return () => { };
1606
1662
  return this.todos.subscribe(listener);
1607
1663
  }
1608
- /** `/resume`. An absent or empty list CLEARS — see `TodoStore.restore` (P0-2). */
1609
- restoreTodos(items) {
1610
- this.todos?.restore(items);
1664
+ /**
1665
+ * `/resume`. An absent or empty todo list CLEARS both tables — see
1666
+ * `TodoStore.restore` (P0-2). `acceptance` is optional so every existing caller
1667
+ * keeps compiling, and an omitted table reads as "this file has none".
1668
+ */
1669
+ restoreTodos(items, acceptance = undefined) {
1670
+ this.todos?.restore(items, acceptance);
1611
1671
  }
1612
1672
  /**
1613
1673
  * `/todo clear` and `/clear screen`. A user override of a projection (I-2's
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import { formatCost, formatTokens, formatDuration, computeCost } from './usage.js';
8
8
  import { formatStreamError } from './reducer.js';
9
+ import { derivePlanPhase } from '../todo/acceptance.js';
9
10
  import { advanceBudget, decideFollowThrough, emptyBudget, } from '../todo/follow-through.js';
10
11
  /** Run a single prompt headlessly and resolve with the process exit code. */
11
12
  export async function runHeadless(controller, prompt, options = {}) {
@@ -139,6 +140,11 @@ export async function runHeadless(controller, prompt, options = {}) {
139
140
  // stream that is already `--quiet`-able in full.
140
141
  let announcedTotal = 0;
141
142
  let lastActive = -1;
143
+ // THE ACCEPTANCE SECTION'S DEDUPE KEY (todo-acceptance-criteria §6.13.1 / P2-9).
144
+ // `phase` is DERIVED, never cached (I-3), so the key carries a freshly computed
145
+ // phase plus the two counters that can only move when a verdict lands: a
146
+ // constant-size string is cheaper than a field that could drift from the truth.
147
+ let lastAcceptance = '';
142
148
  const unsubscribeTodos = controller.subscribeTodos?.((event) => {
143
149
  if (quiet || event.type !== 'updated')
144
150
  return;
@@ -147,6 +153,20 @@ export async function runHeadless(controller, prompt, options = {}) {
147
153
  announcedTotal = total;
148
154
  err.write(`\n[todo] ${total} ${total === 1 ? 'step' : 'steps'} planned\n`);
149
155
  lastActive = -1;
156
+ // A NEW PLAN GETS ITS OWN FIRST `[accept]` LINE, for the reason `lastActive`
157
+ // is reset one line up: the key below is (phase, checked, failed) and two
158
+ // different plans can share all three. Without this reset the second plan's
159
+ // 0/3 in a `-p` run that already printed 0/3 would go unreported.
160
+ lastAcceptance = '';
161
+ }
162
+ const acceptance = event.snapshot.acceptance;
163
+ if (acceptance && acceptance.total > 0) {
164
+ const key = `${derivePlanPhase(items, acceptance.items)}|` +
165
+ `${acceptance.checkedCount}|${acceptance.failedCount}`;
166
+ if (key !== lastAcceptance) {
167
+ lastAcceptance = key;
168
+ err.write(`[accept] ${acceptanceLine(acceptance)}\n`);
169
+ }
150
170
  }
151
171
  if (activeIndex < 0) {
152
172
  if (lastActive !== -2) {
@@ -220,6 +240,17 @@ export async function runHeadless(controller, prompt, options = {}) {
220
240
  `(${formatTokens(r.tailRelief.charsRemoved)} chars)\n`);
221
241
  }
222
242
  });
243
+ /**
244
+ * `[accept] <n>/<total> checked`, plus the failure count when there is one.
245
+ *
246
+ * ONE LINE PER TRANSITION, and the transitions are the ones a `-p` reader can
247
+ * act on: a verdict landed, or a criterion failed. `evidence` is NEVER printed
248
+ * (I-8): it belongs to the human-facing surfaces, and a machine log is not one.
249
+ */
250
+ function acceptanceLine(acceptance) {
251
+ const base = `${acceptance.checkedCount}/${acceptance.total} checked`;
252
+ return acceptance.failedCount > 0 ? `${base}, ${acceptance.failedCount} failed` : base;
253
+ }
223
254
  // --- The bounded continuation loop (todo-plan-followthrough §3.6). --------
224
255
  //
225
256
  // THE THREE `unsubscribe` CALLS STAY OUTSIDE IT (rule 4). The loop wraps the
@@ -16,13 +16,13 @@ import type { AgentMode } from './agent-mode.js';
16
16
  import type { DispatchOutcome, SubagentRun, SubagentSpec, TeamSnapshot } from '../team/types.js';
17
17
  import type { FastReview, FastSnapshot } from '../fast/types.js';
18
18
  import type { CompactionMode, CompactionRecord, CompactionSnapshot, CompactionUiTrigger, ContextUsageSnapshot } from '../compaction/types.js';
19
- import type { TodoItem, TodoSnapshot } from '../todo/types.js';
19
+ import type { AcceptanceSnapshot, TodoItem, TodoSnapshot } from '../todo/types.js';
20
20
  import type { JobKind, ServiceSnapshot, ServiceStatus } from '../proc/types.js';
21
21
  import type { RetryPhase } from './retry-view.js';
22
22
  import type { FilePatch } from '../tools/patch.js';
23
23
  export type NoticeLevel = 'info' | 'warn' | 'error';
24
24
  export type ToastLevel = NoticeLevel | 'success';
25
- export type Overlay = null | 'settings' | 'model' | 'help' | 'queue' | 'confirm' | 'question' | 'plan' | 'kit';
25
+ export type Overlay = null | 'settings' | 'model' | 'help' | 'queue' | 'confirm' | 'question' | 'plan' | 'kit' | 'prompts' | 'prompt-pick' | 'agents';
26
26
  export type ToolStatus = 'pending' | 'running' | 'done' | 'error';
27
27
  /** Ephemeral, auto-dismissed acknowledgement (spec §3.9). */
28
28
  export interface Toast {
@@ -147,6 +147,17 @@ export type Entry = {
147
147
  items: TodoItem[];
148
148
  doneCount: number;
149
149
  total: number;
150
+ /**
151
+ * The acceptance projection, copied from `TodoSnapshot.acceptance` on every
152
+ * `todoUpdate` (todo-acceptance-criteria §6.10).
153
+ *
154
+ * OPTIONAL, NOT `AcceptanceSnapshot | null` REQUIRED, and that asymmetry with
155
+ * the snapshot is deliberate (P1-4): `TodoSnapshot` has exactly one writer, so
156
+ * it can fail closed; an `Entry` does not - its sources include `entries` read
157
+ * back from a session file written before this feature existed, where the key
158
+ * is simply absent.
159
+ */
160
+ acceptance?: AcceptanceSnapshot | null;
150
161
  /** The turn that owns this card is still running, so it may be rewritten. */
151
162
  live: boolean;
152
163
  /**
@@ -1020,6 +1020,7 @@ export function viewReducer(state, action) {
1020
1020
  items: action.snapshot.items,
1021
1021
  doneCount: action.snapshot.doneCount,
1022
1022
  total: action.snapshot.total,
1023
+ acceptance: action.snapshot.acceptance,
1023
1024
  live: true,
1024
1025
  });
1025
1026
  // Rewrite the turn's card when there is one; otherwise append and record
package/dist/cli.js CHANGED
@@ -170,6 +170,10 @@ function toFlags(opts) {
170
170
  // C-9). A value flag is no less inert than a boolean one when this line is
171
171
  // missing.
172
172
  todoFollow: opts.todoFollow,
173
+ // Site three of three once more, for `--todo-acceptance`: a flag declared in
174
+ // commander and on `CliFlags` but missing HERE is inert, with no compile error
175
+ // and no runtime error (todo-acceptance-criteria §6.8.2).
176
+ todoAcceptance: opts.todoAcceptance,
173
177
  // Site three of three for `--retry` / `--retry-max` — five features, same
174
178
  // trap (`mouse` P1-2, `team` P1-4, `todo` C-9, render L4, now retry). A flag
175
179
  // needs the `.option()` declaration below, a field on `CliFlags`, AND a line
@@ -1319,6 +1323,9 @@ function createSessionWriter(input) {
1319
1323
  readView: input.readView,
1320
1324
  readHistory: () => controller.getSessionSnapshot(),
1321
1325
  readTodos: () => controller.getTodoSnapshot()?.items ?? [],
1326
+ // The verdicts travel with the plan; read from the STORE, not from the render
1327
+ // projection, so a checkpoint taken while the capability is off still saves them.
1328
+ readAcceptance: () => controller.getAcceptanceItems(),
1322
1329
  // READ AT WRITE TIME: `/model` rebuilds the config, and a checkpoint that
1323
1330
  // recorded the model from launch would name the wrong one after a switch.
1324
1331
  readModel: () => {
@@ -2068,6 +2075,11 @@ function buildProgram() {
2068
2075
  // passed, so `!== undefined` in `resolveTodoConfig` already distinguishes it
2069
2076
  // from silence. An unrecognized mode is clamped to `notify`, not rejected.
2070
2077
  .option('--todo-follow <mode>', 'What to do when a run ends with unfinished steps: notify (default) | auto | off')
2078
+ // A PAIR, and for the same reason `--todo-panel` needs one: `acceptance` is
2079
+ // PERSISTED, so a lone `--no-todo-acceptance` would make commander default
2080
+ // `opts.todoAcceptance` to `true` and overwrite a stored `false`.
2081
+ .option('--todo-acceptance', 'Plan acceptance criteria with every todo list (the default)')
2082
+ .option('--no-todo-acceptance', 'Plan steps only: no acceptance criteria, no verify phase')
2071
2083
  // BOTH FORMS, positive first, for the reason recorded five times above:
2072
2084
  // `retry.enabled` is PERSISTED, so a lone `--no-retry` would make commander
2073
2085
  // default `opts.retry` to `true` and silently overwrite a stored `false`.
@@ -0,0 +1,15 @@
1
+ /**
2
+ * `/agents` — open the subagent board (tui-subagent-panel §6.4).
3
+ *
4
+ * A SECOND DOOR, not a fallback. `Ctrl+B` is the fast path, but a key is undiscoverable
5
+ * unless the user reads `/help`, and `/agents` is what the palette advertises. Both
6
+ * entries share ONE guard (`state.team === null && lastTeamEntry(...) === null`) so the
7
+ * key and the command can never disagree about whether there is a board to show: an
8
+ * empty board would read as a broken feature, and the honest answer is a notice that
9
+ * says no dispatch has happened yet.
10
+ *
11
+ * The body lives in its own module for the reason `kit-command.ts` and
12
+ * `queue-command.ts` do: `builtins.ts` is already well past a thousand lines.
13
+ */
14
+ import type { CommandRegistry } from './registry.js';
15
+ export declare function registerAgentsCommand(registry: CommandRegistry): void;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * `/agents` — open the subagent board (tui-subagent-panel §6.4).
3
+ *
4
+ * A SECOND DOOR, not a fallback. `Ctrl+B` is the fast path, but a key is undiscoverable
5
+ * unless the user reads `/help`, and `/agents` is what the palette advertises. Both
6
+ * entries share ONE guard (`state.team === null && lastTeamEntry(...) === null`) so the
7
+ * key and the command can never disagree about whether there is a board to show: an
8
+ * empty board would read as a broken feature, and the honest answer is a notice that
9
+ * says no dispatch has happened yet.
10
+ *
11
+ * The body lives in its own module for the reason `kit-command.ts` and
12
+ * `queue-command.ts` do: `builtins.ts` is already well past a thousand lines.
13
+ */
14
+ import { lastTeamEntry } from '../ui/team-status.js';
15
+ export function registerAgentsCommand(registry) {
16
+ registry.register({
17
+ name: 'agents',
18
+ description: 'Open the subagent board: every child, its facts and its timeline',
19
+ run: (ctx) => {
20
+ if (ctx.state.team === null && lastTeamEntry(ctx.state.entries) === null) {
21
+ ctx.notify('info', 'No subagent dispatch yet.');
22
+ return;
23
+ }
24
+ ctx.setOverlay('agents');
25
+ },
26
+ });
27
+ }
@@ -14,6 +14,7 @@ import { computeCost, formatCost, formatDuration, formatTokens } from '../agent/
14
14
  import { mergePendingEntries } from '../agent/queued-messages.js';
15
15
  import { TODO_FOLLOW_LIMITS } from '../todo/limits.js';
16
16
  import { buildContinuationMessage } from '../todo/follow-through.js';
17
+ import { planIsResolved } from '../todo/acceptance.js';
17
18
  import { MODE_LABEL, nextMode } from '../agent/agent-mode.js';
18
19
  import { loadSession, resolveSessionPath, saveSession } from '../session/persist.js';
19
20
  import { applyRestoredSession } from '../session/startup-session.js';
@@ -23,7 +24,9 @@ import { getLogger } from '../logging/logger.js';
23
24
  import { resolveFastTier } from '../fast/resolve.js';
24
25
  import { subagentModelSummary } from '../ui/overlays/model-profile-state.js';
25
26
  import { registerQueueCommand } from './queue-command.js';
27
+ import { registerPromptCommands } from '../prompts/command.js';
26
28
  import { registerKitCommand } from './kit-command.js';
29
+ import { registerAgentsCommand } from './agents-command.js';
27
30
  import { perfCommand } from './perf.js';
28
31
  import { runCompactCommand } from '../compaction/command.js';
29
32
  import { runClearCommand } from './clear-command.js';
@@ -158,6 +161,102 @@ function runTeamModelCommand(ctx, subject) {
158
161
  ctx.persistConfig({ team: teamPatch });
159
162
  ctx.toast('success', `Subagent model: ${entry.name}${disambiguation} (${entry.provider}:${entry.model}).`);
160
163
  }
164
+ /** Rows `/todo status` prints before it says how many it did not. */
165
+ const ACCEPT_STATUS_ROWS = 8;
166
+ /**
167
+ * `/todo acceptance on|off|reset`, and the bare form equal to `/todo status`'s
168
+ * acceptance section (todo-acceptance-criteria §6.14).
169
+ *
170
+ * EVERY MUTATING BRANCH IS "TWO CALLS PLUS PERSIST", the rule `/todo panel` and
171
+ * `/todo follow` already follow: `setTodoConfig` (or `setTodoAcceptanceEnabled`) is
172
+ * what the session sees, and `persistConfig` is what survives it. Only one of the two
173
+ * reports success while changing nothing.
174
+ */
175
+ function runTodoAcceptance(ctx, value) {
176
+ const controller = ctx.controller;
177
+ if (!value || value === 'status') {
178
+ const lines = acceptanceStatusLines(controller.getTodoSnapshot()?.acceptance ?? null, controller.getTodoConfig().acceptance);
179
+ if (lines.length === 0) {
180
+ ctx.notify('info', 'No acceptance criteria. Todo planning: ' +
181
+ (controller.isTodoEnabled() ? 'on' : 'off') + '.');
182
+ return;
183
+ }
184
+ for (const line of lines)
185
+ ctx.notify('info', line);
186
+ return;
187
+ }
188
+ if (value === 'on' || value === 'off') {
189
+ const acceptance = value === 'on';
190
+ controller.setTodoAcceptanceEnabled(acceptance);
191
+ ctx.persistConfig({ todo: { acceptance } });
192
+ ctx.toast('success', `Acceptance criteria ${acceptance ? 'on' : 'off'}.`);
193
+ return;
194
+ }
195
+ if (value === 'reset') {
196
+ if (controller.isRunning()) {
197
+ ctx.notify('warn', 'Cannot reset the acceptance criteria while a run is in progress.');
198
+ return;
199
+ }
200
+ // THE COUNT COMES FROM THE STORE (a named writer, §6.2.3), never recomputed here,
201
+ // and `0` means the table was already fully unchecked - in which case a notice
202
+ // would announce that nothing happened.
203
+ const reopened = controller.reopenAcceptance();
204
+ if (reopened === 0) {
205
+ ctx.notify('info', 'No checked criteria to reset.');
206
+ return;
207
+ }
208
+ ctx.toast('info', `${reopened} ${reopened === 1 ? 'criterion is' : 'criteria are'} ` +
209
+ 'unchecked again.');
210
+ return;
211
+ }
212
+ ctx.notify('warn', 'Usage: /todo acceptance [on|off|reset]');
213
+ }
214
+ /**
215
+ * The acceptance section of `/todo status`, as at most two notification bodies.
216
+ *
217
+ * RETURNS NOTHING WHEN THERE IS NOTHING TO SAY, which is what keeps the disabled
218
+ * path and the no-criteria path byte-identical to the pre-feature command (I-5).
219
+ * The list is bounded at eight rows by "unchecked and failed first, then the rest",
220
+ * with `... and N more` closing it: the full table lives in the transcript card, and
221
+ * a command that floods the transcript is not a status command.
222
+ */
223
+ function acceptanceStatusLines(acceptance, enabled) {
224
+ if (!enabled || !acceptance || acceptance.total === 0)
225
+ return [];
226
+ const header = `Acceptance ${acceptance.checkedCount}/${acceptance.total} checked` +
227
+ (acceptance.failedCount > 0 ? ` (${acceptance.failedCount} failed)` : '');
228
+ const rows = acceptance.items
229
+ .map((item, index) => ({ item, index }))
230
+ .sort((left, right) => rank(left.item.status) - rank(right.item.status));
231
+ const shown = rows.slice(0, ACCEPT_STATUS_ROWS);
232
+ const lines = shown.map(({ item, index }) => ` ${String(index + 1).padStart(2)}. ${markerWord(item.status)} ${item.content}` +
233
+ (item.status === 'failed' && item.evidence.length > 0 ? `\n ${item.evidence}` : ''));
234
+ if (rows.length > shown.length)
235
+ lines.push(` ... and ${rows.length - shown.length} more`);
236
+ return [header, ...lines];
237
+ }
238
+ /**
239
+ * Failures first, then everything still open, then the settled verdicts: `/todo status`
240
+ * is read to find out what is WRONG, and the eight-row budget means the order decides
241
+ * what survives the cut.
242
+ */
243
+ function rank(status) {
244
+ if (status === 'failed')
245
+ return 0;
246
+ if (status === 'in_progress' || status === 'pending')
247
+ return 1;
248
+ return 2;
249
+ }
250
+ /** ASCII markers: this module is inside the glyph scanner's scope. */
251
+ function markerWord(status) {
252
+ if (status === 'passed')
253
+ return '[x]';
254
+ if (status === 'failed')
255
+ return '[!]';
256
+ if (status === 'in_progress')
257
+ return ' > ';
258
+ return '[ ]';
259
+ }
161
260
  function formatFastStatus(controller) {
162
261
  const status = controller.getFastStatus();
163
262
  const cfg = controller.getFastConfig();
@@ -688,7 +787,8 @@ const COMMANDS = [
688
787
  },
689
788
  {
690
789
  name: 'todo',
691
- description: 'Todo planning: status | on | off | panel on|off | follow <mode> | clear | continue',
790
+ description: 'Todo planning: status | on | off | panel on|off | follow <mode> | ' +
791
+ 'acceptance [on|off|reset] | clear | continue',
692
792
  /**
693
793
  * `/todo on` CANNOT turn planning on in a session that started with
694
794
  * `--no-todo`, for the reason `/team on` above records at length: the tool
@@ -739,6 +839,16 @@ const COMMANDS = [
739
839
  : '';
740
840
  ctx.notify('info', `Todos: ${snapshot.doneCount}/${snapshot.total} done.${active} Panel: ${panel}. ` +
741
841
  `Follow-through: ${follow}.`);
842
+ // THE ACCEPTANCE SECTION, appended and NOTHING ELSE (todo-acceptance-criteria
843
+ // §6.14): with no criteria the two calls above are byte-identical to the
844
+ // version without this feature.
845
+ for (const line of acceptanceStatusLines(snapshot.acceptance, todoCfg.acceptance)) {
846
+ ctx.notify('info', line);
847
+ }
848
+ return;
849
+ }
850
+ if (verb === 'acceptance') {
851
+ runTodoAcceptance(ctx, value);
742
852
  return;
743
853
  }
744
854
  if (verb === 'on' || verb === 'off') {
@@ -820,7 +930,10 @@ const COMMANDS = [
820
930
  return;
821
931
  }
822
932
  const snapshot = controller.getTodoSnapshot();
823
- if (!snapshot || snapshot.doneCount >= snapshot.total) {
933
+ // `planIsResolved`, NOT `doneCount >= total` (§6.14): in the verification
934
+ // phase every step is done and the command has to keep working, or the user's
935
+ // only manual route into the last stage is refused with "Nothing left".
936
+ if (!snapshot || planIsResolved(snapshot.items, snapshot.acceptance?.items ?? null)) {
824
937
  ctx.notify('info', 'Nothing left to continue.');
825
938
  return;
826
939
  }
@@ -839,7 +952,8 @@ const COMMANDS = [
839
952
  return;
840
953
  }
841
954
  ctx.notify('warn', `Unknown argument "${verb}" - use ` +
842
- '/todo [status|on|off|panel on|off|follow <mode>|clear|continue].');
955
+ '/todo [status|on|off|panel on|off|follow <mode>|acceptance [on|off|reset]|' +
956
+ 'clear|continue].');
843
957
  },
844
958
  },
845
959
  {
@@ -1017,6 +1131,11 @@ const COMMANDS = [
1017
1131
  // `/resume` restores `messages`, so the belief that justifies the
1018
1132
  // panel comes back — and the panel has to come back with it (§3.13).
1019
1133
  todos: ctx.controller.getTodoSnapshot()?.items ?? [],
1134
+ // The verdicts travel with the plan, for the same reason: a resume that
1135
+ // restored the steps and dropped the conclusions would show a plan as
1136
+ // "not started" that the user had already watched being verified. Read
1137
+ // from the STORE, so a save taken while the capability is off keeps them.
1138
+ acceptance: ctx.controller.getAcceptanceItems(),
1020
1139
  });
1021
1140
  ctx.notify('info', `Saved session to ${path}`);
1022
1141
  }
@@ -1375,6 +1494,14 @@ export function registerBuiltinCommands(registry) {
1375
1494
  // The command BODY lives in its own module (builtins.ts is already 1.5k lines);
1376
1495
  // this line is the whole registration (agent-model-kits §4.9).
1377
1496
  registerKitCommand(registry);
1497
+ // Same shape, one feature later (quick-prompt-library §4.3 / P1-3): the body is
1498
+ // `prompts/command.ts` and its capabilities arrive on `CommandContext.prompts`.
1499
+ // REGISTERED UNCONDITIONALLY, even with `prompts.enabled=false` - that is the only
1500
+ // way the command can report that the feature is off (D-19 / AC-30).
1501
+ registerPromptCommands(registry);
1502
+ // The read-only subagent board (tui-subagent-panel §6.4). Same shape again: the
1503
+ // body is `commands/agents-command.ts` and the registration is this one line.
1504
+ registerAgentsCommand(registry);
1378
1505
  }
1379
1506
  /**
1380
1507
  * Every built-in command name INCLUDING aliases (`quit`), for documentation and
@@ -1384,7 +1511,7 @@ export function registerBuiltinCommands(registry) {
1384
1511
  * `registry.get(name)` instead, because that is the only check that stays
1385
1512
  * correct when commands are registered dynamically (§7.1 / P1-2).
1386
1513
  */
1387
- export const BUILTIN_COMMAND_NAMES = ['queue', ...COMMANDS.flatMap((c) => [
1514
+ export const BUILTIN_COMMAND_NAMES = ['queue', 'prompts', 'agents', ...COMMANDS.flatMap((c) => [
1388
1515
  c.name,
1389
1516
  ...(c.aliases ?? []),
1390
1517
  ])].sort();