@aragon-agent/cli 0.6.35 → 0.6.36

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
@@ -94,6 +94,17 @@ export interface BandPerf {
94
94
  * reply, which is exactly the state that used to be unrecoverable.
95
95
  */
96
96
  headRows: number;
97
+ /**
98
+ * Which rung the acceptance section reached inside the rail
99
+ * (todo-acceptance-criteria §6.9.6): `visible` (header plus rows), `summary` (the
100
+ * one `AC 2/5` line) or `absent` (nothing - the count is on the status chip).
101
+ *
102
+ * `n/a` when there are no criteria at all, which is the same spelling `/perf` uses
103
+ * for the band outside native mode. It is reported on the EXISTING band line rather
104
+ * than as a new row, and it comes from `buildPlanSections` - the same function the
105
+ * rail rendered from, so this is a report and not a second opinion.
106
+ */
107
+ acceptance?: 'visible' | 'summary' | 'absent' | 'n/a';
97
108
  }
98
109
  /**
99
110
  * Why the right-edge scrollbar is or is not usable, in one record
@@ -139,7 +139,8 @@ function formatBandLine(band) {
139
139
  `tail=${band.previewRows} pool=${band.bandRows} ` +
140
140
  `cols=${band.contentCols}/${band.previewCols} ` +
141
141
  `reason=${band.reason} held=${band.heldRailRows}/${band.heldTeamRows} ` +
142
- `window=${band.windowStart}/${band.windowEntries} head=${band.headRows}`;
142
+ `window=${band.windowStart}/${band.windowEntries} head=${band.headRows}` +
143
+ ` acc=${band.acceptance ?? 'n/a'}`;
143
144
  }
144
145
  /**
145
146
  * Format a snapshot as the seven-line block documented in §5.4 (plus the scrollbar
@@ -12,6 +12,7 @@ import type { FollowThroughBudget } from '../todo/follow-through.js';
12
12
  import type { UpdateCommandPort } from '../update/types.js';
13
13
  import type { SessionCommandPort } from './session-command.js';
14
14
  import type { BranchCommandPort } from './branch-command.js';
15
+ import type { PromptCommandPort } from '../prompts/types.js';
15
16
  export interface CommandContext {
16
17
  /** App-owned shared lock and single feedback path for command and selection copies. */
17
18
  requestCopy?: (request: CopyRequest) => Promise<CopyRequestResult>;
@@ -161,6 +162,19 @@ export interface CommandContext {
161
162
  * value at run time.
162
163
  */
163
164
  branch?: BranchCommandPort;
165
+ /**
166
+ * The quick-prompt library's three capabilities (`/prompts`, quick-prompt-library
167
+ * §4.3 / P1-3).
168
+ *
169
+ * OPTIONAL, and this is the fifth time this file records the same rule: every
170
+ * fixture in this package builds `CommandContext` by literal, so a REQUIRED field
171
+ * would fail to compile every one of them for a feature none of them is about
172
+ * (F-14). Absent means the build has no library, and `/prompts` says exactly that.
173
+ *
174
+ * TYPE-ONLY import, for the reason the `branch` note above records: the command
175
+ * needs the SHAPE of the port, never a value at run time.
176
+ */
177
+ prompts?: PromptCommandPort;
164
178
  }
165
179
  /** What `/mouse` can see and do. Read at command time, never captured. */
166
180
  export interface MouseCommandPort {
@@ -90,6 +90,21 @@ export declare function getModelWindowsPath(): string;
90
90
  * recall (config-state-separation D-1).
91
91
  */
92
92
  export declare function getPromptHistoryPath(): string;
93
+ /**
94
+ * `<home>/quick-prompts.json` — the user's own quick-prompt library
95
+ * (quick-prompt-library D-3).
96
+ *
97
+ * A DIRECT CHILD OF THE ROOT, and deliberately NOT a section of `config.json`.
98
+ * `ui-state.ts:5-13` already wrote the rule this follows: anything carrying USER
99
+ * CONTENT, and anything that GROWS with usage, opens its own file. A library of
100
+ * two hundred prompts is exactly that, and `config.json` is where an API key
101
+ * lives.
102
+ *
103
+ * THE APPLICATION WRITES IT, unlike `model-windows.json` above: it is the library's
104
+ * only truth, and the store creates it on the first `add` rather than at startup.
105
+ * Naming follows `getPromptHistoryPath` beside it.
106
+ */
107
+ export declare function getQuickPromptsPath(): string;
93
108
  /** `<home>/state.json` — UI bookkeeping scalars only; see `ui-state.ts`. */
94
109
  export declare function getUiStatePath(): string;
95
110
  /**
@@ -89,8 +89,22 @@ function isDirectory(path) {
89
89
  * test that sets `ARAGON_HOME` to something bogus quietly gets the real one.
90
90
  */
91
91
  function fallbackRoot() {
92
- if (process.env.VITEST)
93
- return join(tmpdir(), `aragon-agent-vitest-${process.pid}`);
92
+ if (process.env.VITEST) {
93
+ // ONE DIRECTORY PER WORKER, NOT ONE PER PROCESS.
94
+ //
95
+ // The `threads` pool (vitest.config.ts) gives every worker the SAME
96
+ // `process.pid` - they are threads - so a pid-keyed root is shared by up to
97
+ // `maxWorkers` test files running at the same time. Two of them cleaning up
98
+ // (`rmSync(getSessionsDir())`) while a third writes is a race, and it reports
99
+ // itself as "the session was never written": a red full run whose two files
100
+ // both pass alone. `VITEST_POOL_ID` is the worker's own id and a worker runs
101
+ // one file at a time, so keying on it restores the isolation the contract
102
+ // above promises (measured: `session-store.test.ts` and
103
+ // `exec-text-parity.test.ts` failed together and passed separately).
104
+ const worker = process.env.VITEST_POOL_ID ?? '';
105
+ const suffix = worker.length > 0 ? `-${worker}` : '';
106
+ return join(tmpdir(), `aragon-agent-vitest-${process.pid}${suffix}`);
107
+ }
94
108
  return join(homedir(), HOME_DIR_NAME);
95
109
  }
96
110
  function resolveHomeRoot() {
@@ -151,6 +165,23 @@ export function getModelWindowsPath() {
151
165
  export function getPromptHistoryPath() {
152
166
  return join(resolution.root, 'prompt-history.jsonl');
153
167
  }
168
+ /**
169
+ * `<home>/quick-prompts.json` — the user's own quick-prompt library
170
+ * (quick-prompt-library D-3).
171
+ *
172
+ * A DIRECT CHILD OF THE ROOT, and deliberately NOT a section of `config.json`.
173
+ * `ui-state.ts:5-13` already wrote the rule this follows: anything carrying USER
174
+ * CONTENT, and anything that GROWS with usage, opens its own file. A library of
175
+ * two hundred prompts is exactly that, and `config.json` is where an API key
176
+ * lives.
177
+ *
178
+ * THE APPLICATION WRITES IT, unlike `model-windows.json` above: it is the library's
179
+ * only truth, and the store creates it on the first `add` rather than at startup.
180
+ * Naming follows `getPromptHistoryPath` beside it.
181
+ */
182
+ export function getQuickPromptsPath() {
183
+ return join(resolution.root, 'quick-prompts.json');
184
+ }
154
185
  /** `<home>/state.json` — UI bookkeeping scalars only; see `ui-state.ts`. */
155
186
  export function getUiStatePath() {
156
187
  return join(resolution.root, 'state.json');
@@ -18,7 +18,10 @@
18
18
  * ARAGON_UPDATE,
19
19
  * ARAGON_COMPACTION, ARAGON_COMPACTION_THRESHOLD,
20
20
  * ARAGON_COMPACTION_KEEP_TURNS, ARAGON_COMPACTION_SUBAGENTS,
21
- * ARAGON_COMPACTION_ARCHIVE.
21
+ * ARAGON_COMPACTION_ARCHIVE,
22
+ * ARAGON_JOBS_ENABLED, ARAGON_JOBS_WAIT_SECONDS, ARAGON_JOBS_STALL_SECONDS,
23
+ * ARAGON_JOBS_PERSIST_LOG, ARAGON_JOBS_NOTICE_STEER,
24
+ * ARAGON_PROMPTS (the quick-prompt library's `enabled` switch, and only that).
22
25
  *
23
26
  * READ ELSEWHERE, ON PURPOSE: `ARAGON_UPDATE_REGISTRY` and `npm_config_registry`
24
27
  * are resolved by `update/registry.ts::resolveRegistryUrl`, NOT here, because
@@ -18,7 +18,10 @@
18
18
  * ARAGON_UPDATE,
19
19
  * ARAGON_COMPACTION, ARAGON_COMPACTION_THRESHOLD,
20
20
  * ARAGON_COMPACTION_KEEP_TURNS, ARAGON_COMPACTION_SUBAGENTS,
21
- * ARAGON_COMPACTION_ARCHIVE.
21
+ * ARAGON_COMPACTION_ARCHIVE,
22
+ * ARAGON_JOBS_ENABLED, ARAGON_JOBS_WAIT_SECONDS, ARAGON_JOBS_STALL_SECONDS,
23
+ * ARAGON_JOBS_PERSIST_LOG, ARAGON_JOBS_NOTICE_STEER,
24
+ * ARAGON_PROMPTS (the quick-prompt library's `enabled` switch, and only that).
22
25
  *
23
26
  * READ ELSEWHERE, ON PURPOSE: `ARAGON_UPDATE_REGISTRY` and `npm_config_registry`
24
27
  * are resolved by `update/registry.ts::resolveRegistryUrl`, NOT here, because
@@ -212,8 +215,10 @@ export function readEnvConfig() {
212
215
  }
213
216
  const todo = process.env.ARAGON_TODO?.trim().toLowerCase();
214
217
  const todoFollow = process.env.ARAGON_TODO_FOLLOW?.trim().toLowerCase();
218
+ const todoAcceptance = process.env.ARAGON_TODO_ACCEPTANCE?.trim().toLowerCase();
215
219
  if ((todo !== undefined && todo.length > 0) ||
216
- (todoFollow !== undefined && todoFollow.length > 0)) {
220
+ (todoFollow !== undefined && todoFollow.length > 0) ||
221
+ (todoAcceptance !== undefined && todoAcceptance.length > 0)) {
217
222
  // Keyed by `TodoConfig` but VALUED `unknown`, which is what lets the mode be
218
223
  // written through unvalidated (below) while still failing to compile if a
219
224
  // future key is misspelled.
@@ -227,6 +232,16 @@ export function readEnvConfig() {
227
232
  if (todoFollow !== undefined && todoFollow.length > 0) {
228
233
  section.followThrough = todoFollow;
229
234
  }
235
+ // Same treatment as `enabled`: only the five affirmatives are true, anything
236
+ // else recognized-as-present is false, and an ABSENT variable leaves the key
237
+ // out entirely so the next layer's value survives.
238
+ if (todoAcceptance !== undefined && todoAcceptance.length > 0) {
239
+ section.acceptance =
240
+ todoAcceptance === '1' ||
241
+ todoAcceptance === 'true' ||
242
+ todoAcceptance === 'on' ||
243
+ todoAcceptance === 'yes';
244
+ }
230
245
  // A PARTIAL section, safe only because every consumer deep-merges it — the
231
246
  // same contract `partial.skills`, `partial.log` and `partial.team` rely on.
232
247
  if (Object.keys(section).length > 0)
@@ -292,6 +307,17 @@ export function readEnvConfig() {
292
307
  if (Object.keys(section).length > 0)
293
308
  partial.jobs = section;
294
309
  }
310
+ // ARAGON_PROMPTS=0 is the kill switch for the whole quick-prompt surface: no
311
+ // `Ctrl+P`, no header hint, and `/prompts` reports that the feature is off
312
+ // (D-19 / AC-31). ONLY `enabled` IS REACHABLE HERE. `insertMode` and `hint` are
313
+ // preferences a human sets once, and an environment variable that could silently
314
+ // flip a fill into "overwrite what you had typed" is not a kill switch.
315
+ const prompts = process.env.ARAGON_PROMPTS?.trim().toLowerCase();
316
+ if (prompts !== undefined && prompts.length > 0) {
317
+ partial.prompts = {
318
+ enabled: prompts === '1' || prompts === 'true' || prompts === 'on' || prompts === 'yes',
319
+ };
320
+ }
295
321
  const retry = process.env.ARAGON_RETRY?.trim().toLowerCase();
296
322
  const retryMax = process.env.ARAGON_RETRY_MAX?.trim();
297
323
  if ((retry !== undefined && retry.length > 0) || (retryMax !== undefined && retryMax.length > 0)) {
@@ -118,6 +118,16 @@ export interface CliFlags {
118
118
  * rejected, which is what every other key in this file does.
119
119
  */
120
120
  todoFollow?: string;
121
+ /**
122
+ * `--todo-acceptance` => true, `--no-todo-acceptance` => false, absent =>
123
+ * undefined (todo-acceptance-criteria §6.8.2).
124
+ *
125
+ * DECLARED AS A PAIR, for the reason `--todo-panel` above records at length: it
126
+ * is a PERSISTED setting, so "not passed" has to stay distinguishable from
127
+ * `true`, or commander's lone-`--no-x` default would overwrite a config-file
128
+ * `acceptance: false` on every run that passes no flag at all.
129
+ */
130
+ todoAcceptance?: boolean;
121
131
  /** `--log-level <lv>` — this run only, never persisted. */
122
132
  logLevel?: string;
123
133
  /** `--log-file` ⇒ true, `--no-log-file` ⇒ false, absent ⇒ undefined. */
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import process from 'node:process';
9
9
  import { JOB_WAIT_MARGIN_MS } from '../proc/limits.js';
10
- import { DEFAULT_CONFIG, DEFAULT_SCROLL_MODE, IDLE_TIMEOUT_MARGIN_MS, clampAskRounds, clampDensity, clampHumanTimeout, clampCompactionConfig, clampLogConfig, clampFastConfig, clampRetryConfig, clampSkillsConfig, clampTeamConfig, clampTodoConfig, clampBashConfig, clampJobsConfig, clampUpdateConfig, clampTheme, clampThinkingLevel, clampContextWindow, clampMaxTokens, clampMaxRenderInterval, clampScrollMode, clampScrollResumeMs, clampTranscriptRetain, clampTranscriptWindow, coercePositiveInt, isAutoToken, isSkillsToolPolicyMode, legacyTeamCeilingPatch, parseThresholdInput, } from './schema.js';
10
+ import { DEFAULT_CONFIG, DEFAULT_SCROLL_MODE, IDLE_TIMEOUT_MARGIN_MS, clampAskRounds, clampDensity, clampHumanTimeout, clampCompactionConfig, clampLogConfig, clampFastConfig, clampRetryConfig, clampSkillsConfig, clampTeamConfig, clampTodoConfig, clampBashConfig, clampJobsConfig, clampPromptsConfig, clampUpdateConfig, clampTheme, clampThinkingLevel, clampContextWindow, clampMaxTokens, clampMaxRenderInterval, clampScrollMode, clampScrollResumeMs, clampTranscriptRetain, clampTranscriptWindow, coercePositiveInt, isAutoToken, isSkillsToolPolicyMode, legacyTeamCeilingPatch, parseThresholdInput, } from './schema.js';
11
11
  import { loadDotenv, readEnvConfig, readEnvDisabledSkills } from './env.js';
12
12
  import { readConfigFile, updatePersistedConfig } from './store.js';
13
13
  import { projectModelProfiles, resolveModelProfileState, resolveModelRoleKey, } from './model-profile-resolution.js';
@@ -159,6 +159,7 @@ function resolveTodoConfig(flags, env, file) {
159
159
  ...(env.todo ?? {}),
160
160
  ...(flags.todo !== undefined ? { enabled: flags.todo } : {}),
161
161
  ...(flags.todoPanel !== undefined ? { panel: flags.todoPanel } : {}),
162
+ ...(flags.todoAcceptance !== undefined ? { acceptance: flags.todoAcceptance } : {}),
162
163
  // `!== undefined` for the third time, though for a value flag the risk is
163
164
  // milder: commander leaves `opts.todoFollow` absent unless it was passed.
164
165
  // Written this way anyway so the three lines read as one rule.
@@ -731,6 +732,12 @@ export function loadConfig(flags = {}, options = {}) {
731
732
  // NOT `resolveJobsConfig(...)` a second time: that would re-apply the
732
733
  // un-converged `waitSeconds` and silently undo I-4.
733
734
  jobs,
735
+ // The quick-prompt library's three policy scalars (quick-prompt-library D-19).
736
+ // Resolved through the SAME clamp the read and write paths use, so an env-only
737
+ // `ARAGON_PROMPTS=0` and a file-only `prompts.enabled: false` cannot arrive as
738
+ // two different shapes on `CliConfig`.
739
+ prompts: clampPromptsConfig({ ...DEFAULT_CONFIG.prompts, ...(env.partial.prompts ?? {}),
740
+ ...(file.prompts ?? {}) }),
734
741
  cwd,
735
742
  color,
736
743
  colorLevel,
@@ -489,6 +489,17 @@ export interface TodoConfig {
489
489
  * be, and would mean replacing both merges first.
490
490
  */
491
491
  followThrough: FollowThroughMode;
492
+ /**
493
+ * Must a plan also declare acceptance criteria, and does the UI show them
494
+ * (todo-acceptance-criteria §6.8.1). Default TRUE.
495
+ *
496
+ * THE FOURTH SCALAR, and it is a scalar for exactly the reason above: both
497
+ * hand-written merges in `config/store.ts` spread the whole `todo` object, so a
498
+ * new scalar key flows through them automatically. `clampTodoConfig` below is the
499
+ * only other place that has to know about it - a value that is missing THERE
500
+ * would silently keep the default and make `--no-todo-acceptance` look broken.
501
+ */
502
+ acceptance: boolean;
492
503
  }
493
504
  export declare const DEFAULT_TODO_CONFIG: TodoConfig;
494
505
  /**
@@ -851,6 +862,43 @@ export declare const DEFAULT_JOBS_CONFIG: JobsConfig;
851
862
  * resolve to `undefined` and the setting would silently never apply.
852
863
  */
853
864
  export declare function clampJobsConfig(raw: unknown): JobsConfig;
865
+ /**
866
+ * The quick-prompt library's three POLICY scalars (quick-prompt-library D-19).
867
+ *
868
+ * THE LIBRARY ITSELF IS NOT HERE (D-3). Two hundred prompts with bodies is user
869
+ * content that grows with usage, and `ui-state.ts:5-13` already wrote the rule:
870
+ * anything carrying user content opens its own file. `config.json` holds the three
871
+ * decisions that are about BEHAVIOUR - whether the key exists, what a fill does to
872
+ * a non-empty draft, and whether the header teaches the key.
873
+ *
874
+ * ONE LEVEL DEEP, like every section beside it: `runConfigGet` splits on a single
875
+ * dot, so `prompts.hint` resolves and `prompts.some.nested` would not.
876
+ */
877
+ export interface PromptsConfig {
878
+ /**
879
+ * `false` stops `Ctrl+P` and the header hint; `/prompts` STAYS REGISTERED and
880
+ * reports that the feature is off (D-19 / AC-30). Removing the command as well
881
+ * would leave the one surface that can explain the switch unable to run.
882
+ */
883
+ enabled: boolean;
884
+ /**
885
+ * What a fill does to a draft that already has text in it (D-6).
886
+ * `insert` keeps the user's words; `replace` overwrites them.
887
+ */
888
+ insertMode: 'insert' | 'replace';
889
+ /** Whether the header's right-hand column may teach `^P prompts`. */
890
+ hint: boolean;
891
+ }
892
+ export declare const DEFAULT_PROMPTS_CONFIG: PromptsConfig;
893
+ /**
894
+ * THE single gate for every `prompts` read AND write, like the eleven sections
895
+ * above it.
896
+ *
897
+ * An unrecognized `insertMode` falls back to `insert` rather than to `replace`,
898
+ * and that direction is deliberate: a typo must never silently start DISCARDING
899
+ * what the user has already typed into the composer.
900
+ */
901
+ export declare function clampPromptsConfig(raw: unknown): PromptsConfig;
854
902
  /**
855
903
  * Parse `0.9`, `90%` or `90` into a ratio, or `null` when it is neither.
856
904
  *
@@ -1013,6 +1061,8 @@ export interface PersistedConfig {
1013
1061
  bash: BashConfig;
1014
1062
  /** Background jobs and bounded waiting. The ELEVENTH; scalars only (see JobsConfig). */
1015
1063
  jobs: JobsConfig;
1064
+ /** Quick prompts. The TWELFTH; three scalars (see PromptsConfig). */
1065
+ prompts: PromptsConfig;
1016
1066
  }
1017
1067
  export declare const DEFAULT_CONFIG: PersistedConfig;
1018
1068
  /**
@@ -1351,6 +1401,24 @@ export interface CliConfig {
1351
1401
  * `undefined` and report an empty value for a setting that IS on disk.
1352
1402
  */
1353
1403
  jobs: JobsConfig;
1404
+ /**
1405
+ * Quick prompts (quick-prompt-library D-19). The TWELFTH nested object; three
1406
+ * scalars, one level deep.
1407
+ *
1408
+ * ON `CliConfig` AS WELL AS `PersistedConfig`, for the reason every section above
1409
+ * records: `App` reads `cfg.prompts?.enabled` / `.hint` / `.insertMode` from a
1410
+ * `CliConfig`, so a key added to the persisted shape alone would round-trip
1411
+ * through the `config` commands, appear in `config list`, and change nothing.
1412
+ *
1413
+ * OPTIONAL ON THIS SHAPE ONLY, and `loadConfig` always fills it. The reason is the
1414
+ * one the `jobs` note above does not have to state: `CliConfig` is built by hand in
1415
+ * ~20 test fixtures, and a REQUIRED section turns every one of them into a compile
1416
+ * error for a feature none of them is about. The three readers then fall back to the
1417
+ * DOCUMENTED defaults (`enabled: true`, `insertMode: 'insert'`, `hint: true`), so a
1418
+ * missing section can only ever behave like a default session - never like a
1419
+ * disabled or destructive one.
1420
+ */
1421
+ prompts?: PromptsConfig;
1354
1422
  /**
1355
1423
  * Lifetime submit count driving the composer hint fade (`HINT_FADE_AFTER`).
1356
1424
  *
@@ -578,6 +578,7 @@ export const DEFAULT_TODO_CONFIG = {
578
578
  enabled: true,
579
579
  panel: true,
580
580
  followThrough: 'notify',
581
+ acceptance: true,
581
582
  };
582
583
  /**
583
584
  * There is no enum helper in this file, so this follows the `isThinkingLevel`
@@ -606,6 +607,10 @@ export function clampTodoConfig(raw) {
606
607
  // `ARAGON_TODO_FOLLOW=garbage` therefore resolves to `'notify'` and throws
607
608
  // nothing (AC-5).
608
609
  followThrough: followMode(src.followThrough, DEFAULT_TODO_CONFIG.followThrough),
610
+ // The fourth key needs nothing beyond this line: no migration, because
611
+ // `CONFIG_VERSION` is only stamped and never branched, and an old file reaches
612
+ // the new default through `{ ...DEFAULT, ...partial } + clamp`.
613
+ acceptance: bool(src.acceptance, DEFAULT_TODO_CONFIG.acceptance),
609
614
  };
610
615
  }
611
616
  export const DEFAULT_RETRY_CONFIG = {
@@ -958,6 +963,31 @@ export function clampJobsConfig(raw) {
958
963
  noticeSteer: bool(src.noticeSteer, DEFAULT_JOBS_CONFIG.noticeSteer),
959
964
  };
960
965
  }
966
+ export const DEFAULT_PROMPTS_CONFIG = {
967
+ enabled: true,
968
+ insertMode: 'insert',
969
+ hint: true,
970
+ };
971
+ const PROMPT_INSERT_MODES = ['insert', 'replace'];
972
+ /**
973
+ * THE single gate for every `prompts` read AND write, like the eleven sections
974
+ * above it.
975
+ *
976
+ * An unrecognized `insertMode` falls back to `insert` rather than to `replace`,
977
+ * and that direction is deliberate: a typo must never silently start DISCARDING
978
+ * what the user has already typed into the composer.
979
+ */
980
+ export function clampPromptsConfig(raw) {
981
+ const src = (raw && typeof raw === 'object' ? raw : {});
982
+ const mode = typeof src.insertMode === 'string' ? src.insertMode.trim().toLowerCase() : '';
983
+ return {
984
+ enabled: bool(src.enabled, DEFAULT_PROMPTS_CONFIG.enabled),
985
+ insertMode: PROMPT_INSERT_MODES.includes(mode)
986
+ ? mode
987
+ : DEFAULT_PROMPTS_CONFIG.insertMode,
988
+ hint: bool(src.hint, DEFAULT_PROMPTS_CONFIG.hint),
989
+ };
990
+ }
961
991
  /**
962
992
  * Parse `0.9`, `90%` or `90` into a ratio, or `null` when it is neither.
963
993
  *
@@ -1062,6 +1092,15 @@ export const DEFAULT_CONFIG = {
1062
1092
  // to `undefined` would throw on `config.jobs.waitSeconds` at construction
1063
1093
  // rather than quietly disabling something.
1064
1094
  jobs: DEFAULT_JOBS_CONFIG,
1095
+ // And one section later again, for the seventh time, and this one has BOTH
1096
+ // failure directions at once: `prompts.enabled` and `prompts.hint` are `true` by
1097
+ // default, so a `config.json` predating this feature would resolve them to
1098
+ // `undefined` (falsy) and arrive with the key and its hint silently off - while
1099
+ // `prompts.insertMode` would be `undefined`, which is neither `'insert'` nor
1100
+ // `'replace'` and would make `planPromptFill` fall through to its `replace`
1101
+ // branch. That branch discards the user's own words, so the miss would be a data
1102
+ // loss rather than a missing hint.
1103
+ prompts: DEFAULT_PROMPTS_CONFIG,
1065
1104
  };
1066
1105
  /**
1067
1106
  * Runtime state that used to be stored in `config.json` by mistake
@@ -17,7 +17,7 @@ import { getConfigPath, getSessionsDir } from './app-paths.js';
17
17
  import { registerProfileSecrets, registerSecretsFrom } from '../logging/secret-registry.js';
18
18
  import { adaptLegacyConnectionPatch, ModelProfileConfigError, readModelSettingsDisk } from './model-profile-store.js';
19
19
  import { normalizeModelProfiles } from './model-profiles.js';
20
- import { clampBashConfig, clampJobsConfig, CONFIG_VERSION, DEFAULT_CONFIG, clampCompactionConfig, clampFastConfig, clampLogConfig, clampMaxTokens, clampRetryConfig, clampSkillsConfig, clampTeamConfig, clampTodoConfig, clampContextWindow, clampUpdateConfig, isAutoToken, stripLegacyStateKeys, } from './schema.js';
20
+ import { clampBashConfig, clampJobsConfig, clampPromptsConfig, CONFIG_VERSION, DEFAULT_CONFIG, clampCompactionConfig, clampFastConfig, clampLogConfig, clampMaxTokens, clampRetryConfig, clampSkillsConfig, clampTeamConfig, clampTodoConfig, clampContextWindow, clampUpdateConfig, isAutoToken, stripLegacyStateKeys, } from './schema.js';
21
21
  /**
22
22
  * Normalize `maxTokens` for both directions of the config file.
23
23
  *
@@ -195,6 +195,11 @@ export function normalizePersistedConfig(input) {
195
195
  // outright, which is a crash on the first `job_wait` rather than a silently
196
196
  // wrong setting.
197
197
  jobs: clampJobsConfig({ ...DEFAULT_CONFIG.jobs, ...(partial.jobs ?? {}) }),
198
+ // And the twelfth, on the same line and for the same reason - with one extra
199
+ // edge: without it `prompts.insertMode` reads `undefined`, which is neither
200
+ // `'insert'` nor `'replace'`, so a fill into a non-empty draft would take the
201
+ // branch that REPLACES what the user typed.
202
+ prompts: clampPromptsConfig({ ...DEFAULT_CONFIG.prompts, ...(partial.prompts ?? {}) }),
198
203
  };
199
204
  }
200
205
  /**
@@ -302,6 +307,12 @@ export function mergePersistedConfig(current, patch) {
302
307
  // absent — with `noticeSteer`'s default masking it on the next load, so the
303
308
  // user would never learn that their privacy switch had been turned back on.
304
309
  jobs: clampJobsConfig({ ...current.jobs, ...(patch.jobs ?? {}) }),
310
+ // The twelfth section, and the seventh time this file records the same trap.
311
+ // `aragon config set prompts.hint false` sends `{ prompts: { hint: false } }`;
312
+ // without this line the shallow top-level spread REPLACES the section and
313
+ // `insertMode` is written absent - which reads back as neither `'insert'` nor
314
+ // `'replace'`, i.e. the next fill overwrites the user's own draft.
315
+ prompts: clampPromptsConfig({ ...current.prompts, ...(patch.prompts ?? {}) }),
305
316
  };
306
317
  return merged;
307
318
  }
@@ -90,6 +90,21 @@ export interface UiState {
90
90
  * others for the same reasons the two above record, and NOT a schema bump.
91
91
  */
92
92
  scrollModeNoticeVersion: number;
93
+ /**
94
+ * Which revision of the "Ctrl+P moved to the prompt library" notice this user has
95
+ * been shown (`0` = none; quick-prompt-library D-20).
96
+ *
97
+ * A VERSION BESIDE THE OTHERS AND NOT A SCHEMA BUMP, for the third time in this
98
+ * file: bumping `UI_STATE_SCHEMA` makes `readFromDisk` discard the whole object, so
99
+ * every user's `submitCount` resets and the composer hint un-fades - to re-show one
100
+ * line. A NEW field simply reads as its default on an older file, which is exactly
101
+ * the behaviour wanted here.
102
+ *
103
+ * IT EXISTS BECAUSE THE KEY MOVED. `Ctrl+P` now opens the prompt library and the
104
+ * mode fallback is `Ctrl+N`; the audience for that sentence is every existing user,
105
+ * and a boolean could only ever say "shown", never "shown WHAT".
106
+ */
107
+ promptKeyNoticeVersion: number;
93
108
  }
94
109
  /** Read the state, caching it for the life of the process. Never throws. */
95
110
  export declare function loadUiState(): UiState;
@@ -107,6 +122,9 @@ export declare function setVtInputNoticeVersion(version: number): void;
107
122
  /** `0` on a file written before the field existed. */
108
123
  export declare function getScrollModeNoticeVersion(): number;
109
124
  export declare function setScrollModeNoticeVersion(version: number): void;
125
+ /** `0` on a file written before the field existed; see the field's comment. */
126
+ export declare function getPromptKeyNoticeVersion(): number;
127
+ export declare function setPromptKeyNoticeVersion(version: number): void;
110
128
  /**
111
129
  * Migration-only: fill in fields that are still at their default, and never
112
130
  * overwrite a value already here. A second import — after a half-finished
@@ -38,6 +38,7 @@ const DEFAULT_UI_STATE = {
38
38
  mouseNoticeVersion: 0,
39
39
  vtInputNoticeVersion: 0,
40
40
  scrollModeNoticeVersion: 0,
41
+ promptKeyNoticeVersion: 0,
41
42
  };
42
43
  let memory = null;
43
44
  /** Non-negative integer or `fallback`; shared by both numeric fields. */
@@ -71,6 +72,7 @@ function readFromDisk() {
71
72
  mouseNoticeVersion: coerceCount(parsed.mouseNoticeVersion, DEFAULT_UI_STATE.mouseNoticeVersion),
72
73
  vtInputNoticeVersion: coerceCount(parsed.vtInputNoticeVersion, DEFAULT_UI_STATE.vtInputNoticeVersion),
73
74
  scrollModeNoticeVersion: coerceCount(parsed.scrollModeNoticeVersion, DEFAULT_UI_STATE.scrollModeNoticeVersion),
75
+ promptKeyNoticeVersion: coerceCount(parsed.promptKeyNoticeVersion, DEFAULT_UI_STATE.promptKeyNoticeVersion),
74
76
  };
75
77
  }
76
78
  catch {
@@ -151,6 +153,13 @@ export function getScrollModeNoticeVersion() {
151
153
  export function setScrollModeNoticeVersion(version) {
152
154
  update({ scrollModeNoticeVersion: version });
153
155
  }
156
+ /** `0` on a file written before the field existed; see the field's comment. */
157
+ export function getPromptKeyNoticeVersion() {
158
+ return loadUiState().promptKeyNoticeVersion;
159
+ }
160
+ export function setPromptKeyNoticeVersion(version) {
161
+ update({ promptKeyNoticeVersion: version });
162
+ }
154
163
  /**
155
164
  * Migration-only: fill in fields that are still at their default, and never
156
165
  * overwrite a value already here. A second import — after a half-finished
@@ -192,6 +192,22 @@ export interface ExecTodoEvent {
192
192
  content: string;
193
193
  status: 'pending' | 'in_progress' | 'completed';
194
194
  }[];
195
+ /**
196
+ * Acceptance counts (todo-acceptance-criteria §6.13.2). `null` when the capability
197
+ * is off or the plan has no criteria.
198
+ *
199
+ * REQUIRED BUT NULLABLE, not optional: `runner` is the only producer and can
200
+ * always answer, while for a consumer `null` and "field absent" mean the same
201
+ * thing under the forward-compatibility contract above. Per-criterion `evidence`
202
+ * never reaches this stream (I-8) - a machine consumer that needs it reads the
203
+ * session file.
204
+ */
205
+ acceptance: {
206
+ total: number;
207
+ checked: number;
208
+ passed: number;
209
+ failed: number;
210
+ } | null;
195
211
  }
196
212
  export interface ExecTeamEvent {
197
213
  type: 'team';
@@ -371,6 +387,15 @@ export interface ExecResultEvent {
371
387
  id: string;
372
388
  };
373
389
  todos: ExecTodoCounts | null;
390
+ /**
391
+ * The acceptance tally for the run (todo-acceptance-criteria §6.13.2), on the same
392
+ * terms as `ExecTodoEvent.acceptance`: required but nullable, no evidence.
393
+ */
394
+ acceptance: {
395
+ total: number;
396
+ passed: number;
397
+ failed: number;
398
+ } | null;
374
399
  error: ExecErrorInfo | null;
375
400
  }
376
401
  /**
@@ -44,6 +44,10 @@ export const EXEC_CAPABILITIES = [
44
44
  // Background job lifecycle events (long-task-jobs §4.10). A consumer that does
45
45
  // not know the `job` type ignores it; this string is how it asks first.
46
46
  'background-jobs',
47
+ // Acceptance counts on the todo event and the result event
48
+ // (todo-acceptance-criteria §6.13.2). APPEND-ONLY: a wrapper detects support by
49
+ // asking for this string, and an older wrapper never asks.
50
+ 'todo-acceptance',
47
51
  ];
48
52
  export function buildInitEvent(params) {
49
53
  return {
@@ -112,7 +112,7 @@ async function execute(input) {
112
112
  });
113
113
  if (plan.saved) {
114
114
  controller.replaceMessages(plan.saved.messages, plan.saved.compactionIdentity);
115
- controller.restoreTodos(plan.saved.todos);
115
+ controller.restoreTodos(plan.saved.todos, plan.saved.acceptance);
116
116
  }
117
117
  const config = controller.getConfig();
118
118
  emitter.init({
@@ -190,6 +190,14 @@ async function execute(input) {
190
190
  cost: buildCost(controller, stats.usage, stats.compactionBilling),
191
191
  model: { provider: config.provider, id: config.model },
192
192
  todos: todo ? { total: todo.total, done: todo.doneCount } : null,
193
+ // The verdict tally for the run, with no per-criterion evidence (I-8).
194
+ acceptance: todo?.acceptance
195
+ ? {
196
+ total: todo.acceptance.total,
197
+ passed: todo.acceptance.passedCount,
198
+ failed: todo.acceptance.failedCount,
199
+ }
200
+ : null,
193
201
  error: stats.errorCode && stats.errorMessage
194
202
  ? { code: stats.errorCode, message: stats.errorMessage }
195
203
  : null,
@@ -288,6 +296,7 @@ function failedResult(sessionId, config, message, code, durationMs) {
288
296
  cost: { amount: 0, currency: 'USD', known: false },
289
297
  model: { provider: config.provider, id: config.model },
290
298
  todos: null,
299
+ acceptance: null,
291
300
  error: { code, message },
292
301
  };
293
302
  }
@@ -479,6 +488,12 @@ function persist(plan, controller, turns, usage, cli, at) {
479
488
  ...(controller.getSessionSnapshot?.() ?? { messages: controller.getMessages() }),
480
489
  entries: [],
481
490
  todos: controller.getTodoSnapshot?.()?.items ?? [],
491
+ // From the STORE, not the render projection: a run whose capability is off
492
+ // must still persist the verdicts it already has. OPTIONAL CALL, like the line
493
+ // above: `ExecControllerFactory` is a port, and a caller whose controller
494
+ // predates this feature must keep saving sessions rather than fail on a method
495
+ // it never heard of - the failure mode would be a session silently not written.
496
+ acceptance: controller.getAcceptanceItems?.() ?? [],
482
497
  },
483
498
  meta,
484
499
  });
@@ -108,6 +108,16 @@ export class ExecRunner {
108
108
  done: doneCount,
109
109
  activeIndex,
110
110
  items: items.map((item) => ({ content: item.content, status: item.status })),
111
+ // Counts only, never per-criterion evidence (I-8): this stream is a machine
112
+ // surface, and a criterion's evidence is written for a human to read.
113
+ acceptance: event.snapshot.acceptance
114
+ ? {
115
+ total: event.snapshot.acceptance.total,
116
+ checked: event.snapshot.acceptance.checkedCount,
117
+ passed: event.snapshot.acceptance.passedCount,
118
+ failed: event.snapshot.acceptance.failedCount,
119
+ }
120
+ : null,
111
121
  });
112
122
  });
113
123
  if (todos)
@@ -79,6 +79,22 @@ export declare const PROC_LIMITS: {
79
79
  readonly outputCoalesceMs: 100;
80
80
  /** `SIGTERM` -> `SIGKILL` escalation window for a graceful stop. */
81
81
  readonly stopGraceMs: 3000;
82
+ /**
83
+ * How long to KEEP ASKING whether the `SIGKILL` actually landed before
84
+ * declaring the job `stopped`.
85
+ *
86
+ * THIS NUMBER IS NOT A GUESS ABOUT THE PLATFORM. `killTree` spawns `taskkill`,
87
+ * which is a process of its own: a Windows tree kill of a shell plus its node
88
+ * child measures ~0.8 s on the reference machine (while starting `cmd` costs
89
+ * 50 ms), so the fixed 250 ms this used to allow expired first and `stop()`
90
+ * reported `stopped` with the child still running - AC-18 caught exactly that.
91
+ * The wait stays BOUNDED against the opposite failure (a promise that never
92
+ * resolves); at this deadline the record keeps the honest `killIncomplete`
93
+ * flag instead.
94
+ */
95
+ readonly killVerifyMs: 2000;
96
+ /** Liveness poll interval while waiting for a `SIGKILL` to land. */
97
+ readonly killVerifyPollMs: 50;
82
98
  /** Below this many terminal columns the status chip degrades to `[N]`. */
83
99
  readonly statusCompactCols: 92;
84
100
  /**