@aragon-agent/cli 0.6.38 → 0.6.39

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 (156) hide show
  1. package/CHANGELOG.md +2523 -2326
  2. package/README.md +3614 -3173
  3. package/dist/agent/controller.d.ts +142 -1
  4. package/dist/agent/controller.js +171 -3
  5. package/dist/agent/reducer.d.ts +41 -1
  6. package/dist/agent/reducer.js +25 -0
  7. package/dist/agent/system-prompt.d.ts +14 -0
  8. package/dist/agent/system-prompt.js +5 -0
  9. package/dist/cli.js +85 -27
  10. package/dist/commands/builtins.js +13 -1
  11. package/dist/commands/keys-command.d.ts +23 -0
  12. package/dist/commands/keys-command.js +38 -0
  13. package/dist/commands/registry.d.ts +39 -0
  14. package/dist/commands/task-command.d.ts +39 -0
  15. package/dist/commands/task-command.js +102 -0
  16. package/dist/config/cli-commands.d.ts +23 -0
  17. package/dist/config/cli-commands.js +36 -0
  18. package/dist/config/env.js +17 -4
  19. package/dist/config/load.js +6 -1
  20. package/dist/config/schema.d.ts +43 -4
  21. package/dist/config/schema.js +31 -5
  22. package/dist/config/store.js +17 -2
  23. package/dist/diagnostics/doctor.d.ts +9 -0
  24. package/dist/diagnostics/doctor.js +34 -0
  25. package/dist/diagnostics/info.d.ts +71 -0
  26. package/dist/diagnostics/info.js +91 -5
  27. package/dist/exec/bridge/caller-tools.d.ts +96 -0
  28. package/dist/exec/bridge/caller-tools.js +213 -0
  29. package/dist/exec/bridge/decisions.d.ts +78 -0
  30. package/dist/exec/bridge/decisions.js +116 -0
  31. package/dist/exec/bridge/human-answers.d.ts +38 -0
  32. package/dist/exec/bridge/human-answers.js +164 -0
  33. package/dist/exec/bridge/limits.d.ts +68 -0
  34. package/dist/exec/bridge/limits.js +68 -0
  35. package/dist/exec/bridge/ndjson.d.ts +17 -0
  36. package/dist/exec/bridge/ndjson.js +83 -0
  37. package/dist/exec/bridge/pending.d.ts +70 -0
  38. package/dist/exec/bridge/pending.js +132 -0
  39. package/dist/exec/bridge/protocol.d.ts +346 -0
  40. package/dist/exec/bridge/protocol.js +256 -0
  41. package/dist/exec/bridge/server.d.ts +142 -0
  42. package/dist/exec/bridge/server.js +502 -0
  43. package/dist/exec/bridge/source.d.ts +58 -0
  44. package/dist/exec/bridge/source.js +145 -0
  45. package/dist/exec/bridge/writer.d.ts +26 -0
  46. package/dist/exec/bridge/writer.js +156 -0
  47. package/dist/exec/cli-commands.js +30 -2
  48. package/dist/exec/emitter.d.ts +16 -2
  49. package/dist/exec/emitter.js +26 -6
  50. package/dist/exec/events.d.ts +465 -10
  51. package/dist/exec/events.js +47 -1
  52. package/dist/exec/hooks/dispatch.d.ts +76 -0
  53. package/dist/exec/hooks/dispatch.js +169 -0
  54. package/dist/exec/hooks/index.d.ts +83 -0
  55. package/dist/exec/hooks/index.js +205 -0
  56. package/dist/exec/hooks/limits.d.ts +56 -0
  57. package/dist/exec/hooks/limits.js +49 -0
  58. package/dist/exec/hooks/registry.d.ts +49 -0
  59. package/dist/exec/hooks/registry.js +91 -0
  60. package/dist/exec/hooks/tool-wrapper.d.ts +94 -0
  61. package/dist/exec/hooks/tool-wrapper.js +154 -0
  62. package/dist/exec/hooks/turn.d.ts +61 -0
  63. package/dist/exec/hooks/turn.js +59 -0
  64. package/dist/exec/hooks/validate.d.ts +85 -0
  65. package/dist/exec/hooks/validate.js +310 -0
  66. package/dist/exec/index.d.ts +130 -4
  67. package/dist/exec/index.js +501 -10
  68. package/dist/exec/options.d.ts +124 -2
  69. package/dist/exec/options.js +413 -6
  70. package/dist/exec/permission.d.ts +9 -1
  71. package/dist/exec/permission.js +7 -1
  72. package/dist/exec/request/limits.d.ts +51 -0
  73. package/dist/exec/request/limits.js +49 -0
  74. package/dist/exec/request/scope.d.ts +106 -0
  75. package/dist/exec/request/scope.js +149 -0
  76. package/dist/exec/result/limits.d.ts +74 -0
  77. package/dist/exec/result/limits.js +72 -0
  78. package/dist/exec/result/prompt.d.ts +28 -0
  79. package/dist/exec/result/prompt.js +74 -0
  80. package/dist/exec/result/schema.d.ts +99 -0
  81. package/dist/exec/result/schema.js +464 -0
  82. package/dist/exec/result/sink.d.ts +56 -0
  83. package/dist/exec/result/sink.js +50 -0
  84. package/dist/exec/result/tool.d.ts +37 -0
  85. package/dist/exec/result/tool.js +118 -0
  86. package/dist/exec/runner.d.ts +395 -1
  87. package/dist/exec/runner.js +860 -8
  88. package/dist/exec/threads/aggregate.d.ts +34 -0
  89. package/dist/exec/threads/aggregate.js +30 -0
  90. package/dist/exec/threads/capacity.d.ts +23 -0
  91. package/dist/exec/threads/capacity.js +38 -0
  92. package/dist/exec/threads/dispatch-source.d.ts +60 -0
  93. package/dist/exec/threads/dispatch-source.js +106 -0
  94. package/dist/exec/threads/execute-threads.d.ts +53 -0
  95. package/dist/exec/threads/execute-threads.js +234 -0
  96. package/dist/exec/threads/gate.d.ts +42 -0
  97. package/dist/exec/threads/gate.js +81 -0
  98. package/dist/exec/threads/identity.d.ts +36 -0
  99. package/dist/exec/threads/identity.js +67 -0
  100. package/dist/exec/threads/interrupt.d.ts +29 -0
  101. package/dist/exec/threads/interrupt.js +79 -0
  102. package/dist/exec/threads/limits.d.ts +58 -0
  103. package/dist/exec/threads/limits.js +35 -0
  104. package/dist/exec/threads/outcome.d.ts +47 -0
  105. package/dist/exec/threads/outcome.js +26 -0
  106. package/dist/exec/threads/registry.d.ts +81 -0
  107. package/dist/exec/threads/registry.js +427 -0
  108. package/dist/exec/threads/runtime.d.ts +107 -0
  109. package/dist/exec/threads/runtime.js +523 -0
  110. package/dist/exec/threads/thread-source.d.ts +20 -0
  111. package/dist/exec/threads/thread-source.js +134 -0
  112. package/dist/fast/limits.d.ts +54 -0
  113. package/dist/fast/limits.js +54 -0
  114. package/dist/fast/summary-call.d.ts +72 -0
  115. package/dist/fast/summary-call.js +133 -0
  116. package/dist/fast/summary-prompt.d.ts +106 -0
  117. package/dist/fast/summary-prompt.js +320 -0
  118. package/dist/fast/task-summary-types.d.ts +66 -0
  119. package/dist/fast/task-summary-types.js +17 -0
  120. package/dist/logging/logger.d.ts +1 -1
  121. package/dist/proc/limits.d.ts +7 -0
  122. package/dist/proc/limits.js +7 -0
  123. package/dist/proc/output-streams.d.ts +50 -0
  124. package/dist/proc/output-streams.js +54 -0
  125. package/dist/team/runtime.d.ts +9 -0
  126. package/dist/team/runtime.js +8 -0
  127. package/dist/team/subagent.d.ts +15 -0
  128. package/dist/team/subagent.js +45 -1
  129. package/dist/tools/bash-tool.d.ts +10 -0
  130. package/dist/tools/bash-tool.js +71 -6
  131. package/dist/tools/index.d.ts +93 -1
  132. package/dist/tools/index.js +112 -3
  133. package/dist/tools/tool-gate.d.ts +100 -0
  134. package/dist/tools/tool-gate.js +87 -0
  135. package/dist/ui/App.js +224 -11
  136. package/dist/ui/Header.d.ts +28 -16
  137. package/dist/ui/Header.js +21 -19
  138. package/dist/ui/interaction-hints.js +6 -0
  139. package/dist/ui/layout/OverlayFrame.d.ts +18 -0
  140. package/dist/ui/layout/OverlayFrame.js +19 -4
  141. package/dist/ui/layout/header-hints.d.ts +55 -62
  142. package/dist/ui/layout/header-hints.js +59 -166
  143. package/dist/ui/layout/header-left.d.ts +115 -0
  144. package/dist/ui/layout/header-left.js +206 -0
  145. package/dist/ui/layout/shortcut-panel-layout.d.ts +191 -0
  146. package/dist/ui/layout/shortcut-panel-layout.js +321 -0
  147. package/dist/ui/layout/status-detail-layout.js +5 -1
  148. package/dist/ui/layout/task-summary-cell.d.ts +61 -0
  149. package/dist/ui/layout/task-summary-cell.js +65 -0
  150. package/dist/ui/overlays/HelpOverlay.d.ts +12 -0
  151. package/dist/ui/overlays/HelpOverlay.js +29 -2
  152. package/dist/ui/overlays/ShortcutPanel.d.ts +39 -0
  153. package/dist/ui/overlays/ShortcutPanel.js +106 -0
  154. package/dist/ui/shortcuts.d.ts +81 -0
  155. package/dist/ui/shortcuts.js +221 -0
  156. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -25,7 +25,7 @@ import { formatMigrationNotice, migrateLegacyState } from './config/migrate-lega
25
25
  import { formatHomeMigrationNotice, migrateToHome } from './config/migrate-home.js';
26
26
  import { migrateStateOutOfConfig } from './config/migrate-state-out-of-config.js';
27
27
  import { runHistoryCommand } from './config/history-commands.js';
28
- import { COMPACTION_CONFIG_SET_KEYS, FAST_CONFIG_SET_KEYS, LOG_CONFIG_SET_KEYS, RETRY_CONFIG_SET_KEYS, TEAM_CONFIG_SET_KEYS, JOBS_CONFIG_SET_KEYS, UPDATE_CONFIG_SET_KEYS, applyCompactionConfigSet, applyFastConfigSet, applyLogConfigSet, applyRetryConfigSet, applyTeamConfigSet, applyJobsConfigSet, applyUpdateConfigSet, runConfigEdit, runConfigGet, runConfigHome, runConfigList, warnIfRedactionDisabled, } from './config/cli-commands.js';
28
+ import { COMPACTION_CONFIG_SET_KEYS, FAST_CONFIG_SET_KEYS, LOG_CONFIG_SET_KEYS, RETRY_CONFIG_SET_KEYS, TEAM_CONFIG_SET_KEYS, JOBS_CONFIG_SET_KEYS, TASK_SUMMARY_CONFIG_SET_KEYS, UPDATE_CONFIG_SET_KEYS, applyCompactionConfigSet, applyFastConfigSet, applyLogConfigSet, applyRetryConfigSet, applyTeamConfigSet, applyJobsConfigSet, applyTaskSummaryConfigSet, applyUpdateConfigSet, runConfigEdit, runConfigGet, runConfigHome, runConfigList, warnIfRedactionDisabled, } from './config/cli-commands.js';
29
29
  import { attachAgentEvents, attachTeamEvents, installLogging, setScreenRestore, setSignalTerminator, } from './logging/install.js';
30
30
  import { getLogger } from './logging/logger.js';
31
31
  import { runLogsCommand } from './logging/cli-commands.js';
@@ -276,15 +276,19 @@ function makeController(flags, opts = { interactive: true }) {
276
276
  version: VERSION,
277
277
  todoPanelCapable,
278
278
  confirm: (req) => confirmBridge.handler ? confirmBridge.handler(req) : Promise.resolve(true),
279
- approval: opts.interactive
280
- ? {
281
- // Read live on every call: the App's effect cleanup nulls the handler
282
- // on unmount, and a cached `true` would keep the gate open after the
283
- // human channel is gone.
284
- canPrompt: () => confirmBridge.handler !== null,
285
- request: (req) => confirmBridge.handler ? confirmBridge.handler(req) : Promise.resolve(false),
286
- }
287
- : DENY_ALL_APPROVAL,
279
+ // THE TWO CAN NEVER BOTH BE PRESENT: `interactive: false` is the only path
280
+ // that supplies a bridge gate, and the interactive branch is the only one that
281
+ // reads the overlay bridge. `??` keeps `-p`'s deny-all default byte-identical.
282
+ approval: opts.approval ??
283
+ (opts.interactive
284
+ ? {
285
+ // Read live on every call: the App's effect cleanup nulls the handler
286
+ // on unmount, and a cached `true` would keep the gate open after the
287
+ // human channel is gone.
288
+ canPrompt: () => confirmBridge.handler !== null,
289
+ request: (req) => confirmBridge.handler ? confirmBridge.handler(req) : Promise.resolve(false),
290
+ }
291
+ : DENY_ALL_APPROVAL),
288
292
  notify: (level, text) => {
289
293
  if (!opts.interactive)
290
294
  process.stderr.write(`[skills] ${level}: ${text}\n`);
@@ -294,17 +298,36 @@ function makeController(flags, opts = { interactive: true }) {
294
298
  // render a question wizard or a plan card. Headless gets the fail-closed
295
299
  // gate, whose STATIC `neverPrompts` is also what stops `createPlanTools`
296
300
  // registering two tools the model could never use there.
297
- humanInput: opts.interactive
298
- ? {
299
- // Live probe, never cached — same contract as `approval` above.
300
- canPrompt: () => humanInputBridge.handler !== null,
301
- request: (req, signal) => humanInputBridge.handler
302
- ? humanInputBridge.handler(req, signal)
303
- : Promise.resolve(null),
304
- }
305
- : DENY_ALL_HUMAN_INPUT,
301
+ humanInput: opts.humanInput ??
302
+ (opts.interactive
303
+ ? {
304
+ // Live probe, never cached - same contract as `approval` above.
305
+ canPrompt: () => humanInputBridge.handler !== null,
306
+ request: (req, signal) => humanInputBridge.handler
307
+ ? humanInputBridge.handler(req, signal)
308
+ : Promise.resolve(null),
309
+ }
310
+ : DENY_ALL_HUMAN_INPUT),
306
311
  ...(opts.permission ? { permission: opts.permission } : {}),
307
312
  ...(opts.appendSystemPrompt ? { appendSystemPrompt: opts.appendSystemPrompt } : {}),
313
+ // SPREAD, never `toolGate: opts.toolGate`: with no gate the deps bag has no
314
+ // such key, `createBuiltinTools` never enters its mapping branch, and the tool
315
+ // array stays provably identical outside bridge mode (I-1).
316
+ ...(opts.toolGate ? { toolGate: opts.toolGate } : {}),
317
+ ...(opts.toolGateForChild ? { toolGateForChild: opts.toolGateForChild } : {}),
318
+ ...(opts.decisionBudgetMs !== undefined ? { decisionBudgetMs: opts.decisionBudgetMs } : {}),
319
+ // THE R3 PAIR, AND THIS LINE IS THE DIFFERENCE BETWEEN A WORKING FEATURE AND A DEAD
320
+ // ONE (headless-request-scope-and-typed-result section 2.6 rows 11/17). This factory
321
+ // takes `deps` as a bag and FORWARDS ONLY WHAT IS NAMED HERE, so a new dep that is
322
+ // not spread below is dropped in silence: every unit test that stubs the factory
323
+ // keeps passing while the real binary never registers the tool. That is the D-12
324
+ // defect this project has already paid for once, which is why both keys are spread
325
+ // conditionally and adjacent to each other.
326
+ ...(opts.resultTool ? { resultTool: opts.resultTool } : {}),
327
+ ...(opts.structuredResultBlock
328
+ ? { structuredResultBlock: opts.structuredResultBlock }
329
+ : {}),
330
+ ...(opts.hookRunner ? { hookRunner: opts.hookRunner } : {}),
308
331
  });
309
332
  return { controller, confirmBridge, humanInputBridge };
310
333
  }
@@ -322,15 +345,34 @@ function makeExecController(flags, deps) {
322
345
  const { controller } = makeController(flags, { interactive: false, ...deps });
323
346
  void controller.refreshModelMetadata();
324
347
  const logger = getLogger();
325
- logger.info('cli', 'run_exec', {});
326
- logger.onFailure((reason) => process.stderr.write(`[log] ${reason}\n`));
327
- attachAgentEvents(logger, controller);
328
- attachTeamEvents(logger, controller);
329
- for (const dir of controller.getSkillService().untrustedDirs()) {
330
- // STDERR, NEVER STDOUT. In `json` / `stream-json` mode stdout carries
331
- // nothing but the schema, and a notice on it would corrupt every consumer.
332
- process.stderr.write(`[skills] project skills in ${dir} skipped (untrusted). Run: aragon skills trust ${dir}\n`);
348
+ // PROCESS-LEVEL WORK HAPPENS ONCE, PER-CONTROLLER WORK HAPPENS EVERY TIME
349
+ // (headless-threads-and-lifecycle-hooks section 2.3.4, review P1-8).
350
+ //
351
+ // The four statements in this branch attach to a PROCESS-WIDE singleton - the logger's
352
+ // handler list, the log file, the same untrusted skill directories - so running them per
353
+ // thread would emit N `run_exec` lines and N `[log]` handlers for one process.
354
+ //
355
+ // `attachAgentEvents` / `attachTeamEvents` ARE NOT IN THIS BRANCH and must never be:
356
+ // each binds to THIS controller and returns its own disposer, so skipping them would
357
+ // mean seven of eight tenants' agent events never reach the log at all. That is an
358
+ // observability regression, not an optimisation - the opposite of what the first draft of
359
+ // the design said. The disposers go back to the caller (the thread runtime) and are
360
+ // invoked when the thread closes (I-T6).
361
+ // `processLevelMounts` IS EXPLICIT RATHER THAN DERIVED FROM `threadId` (review P1-8):
362
+ // the default thread carries `threadId: 'main'` like every other thread, so a guard on
363
+ // "has a thread id" would skip these four statements for ALL of them and leave a
364
+ // thread-mode run with no `run_exec` line and no `onFailure` handler at all.
365
+ if (deps.processLevelMounts !== false) {
366
+ logger.info('cli', 'run_exec', {});
367
+ logger.onFailure((reason) => process.stderr.write(`[log] ${reason}\n`));
368
+ for (const dir of controller.getSkillService().untrustedDirs()) {
369
+ // STDERR, NEVER STDOUT. In `json` / `stream-json` mode stdout carries
370
+ // nothing but the schema, and a notice on it would corrupt every consumer.
371
+ process.stderr.write(`[skills] project skills in ${dir} skipped (untrusted). Run: aragon skills trust ${dir}\n`);
372
+ }
333
373
  }
374
+ const disposers = [attachAgentEvents(logger, controller), attachTeamEvents(logger, controller)];
375
+ deps.onAttach?.(disposers);
334
376
  return controller;
335
377
  }
336
378
  /**
@@ -1551,6 +1593,12 @@ const CONFIG_SET_KEYS = new Set([
1551
1593
  // own assertion: the static guard in `config.test.ts` matches LITERAL keys in
1552
1594
  // this file, so a spread set is invisible to it (RV-P2-8).
1553
1595
  ...JOBS_CONFIG_SET_KEYS,
1596
+ // `taskSummary.*` keys are handled by `applyTaskSummaryConfigSet`, same reasoning
1597
+ // again (tui-shortcut-drawer-and-task-summary P0-3). WITHOUT THIS LINE the keys
1598
+ // fall through to `Unknown config key`, and WITHOUT the matching case below the
1599
+ // membership set accepts a write that never happens - the two halves the static
1600
+ // guard in `config.test.ts` exists to pair up.
1601
+ ...TASK_SUMMARY_CONFIG_SET_KEYS,
1554
1602
  ]);
1555
1603
  /**
1556
1604
  * `aragon config set maxTokens <n|auto|default>`.
@@ -1701,6 +1749,16 @@ function runConfigSetValue(key, value) {
1701
1749
  process.stdout.write(`Set ${key} = ${String(merged.jobs[field])}\n`);
1702
1750
  return;
1703
1751
  }
1752
+ const taskSummaryPatch = applyTaskSummaryConfigSet(key, value);
1753
+ if (taskSummaryPatch) {
1754
+ // Echoed from the STORED value, like every section above: `config set
1755
+ // taskSummary.enabled banana` prints `false`, which is what the read path will
1756
+ // report and therefore the only honest answer.
1757
+ const merged = updatePersistedConfig(taskSummaryPatch);
1758
+ const field = key.slice('taskSummary.'.length);
1759
+ process.stdout.write(`Set ${key} = ${String(merged.taskSummary[field])}\n`);
1760
+ return;
1761
+ }
1704
1762
  const patch = {};
1705
1763
  switch (key) {
1706
1764
  case 'provider':
@@ -27,6 +27,12 @@ import { registerQueueCommand } from './queue-command.js';
27
27
  import { registerPromptCommands } from '../prompts/command.js';
28
28
  import { registerKitCommand } from './kit-command.js';
29
29
  import { registerAgentsCommand } from './agents-command.js';
30
+ // THE DRAWER AND THE HEADER LABEL (tui-shortcut-drawer-and-task-summary §4.7):
31
+ // two single-purpose commands in their own modules, registered from here. Both are
32
+ // UNCONDITIONAL, even with `taskSummary.enabled=false`, because they are the only
33
+ // surfaces that can explain the switch.
34
+ import { keysCommand } from './keys-command.js';
35
+ import { taskCommand } from './task-command.js';
30
36
  import { perfCommand } from './perf.js';
31
37
  import { runCompactCommand } from '../compaction/command.js';
32
38
  import { runClearCommand } from './clear-command.js';
@@ -1502,6 +1508,11 @@ export function registerBuiltinCommands(registry) {
1502
1508
  // The read-only subagent board (tui-subagent-panel §6.4). Same shape again: the
1503
1509
  // body is `commands/agents-command.ts` and the registration is this one line.
1504
1510
  registerAgentsCommand(registry);
1511
+ // The keyboard drawer (`Ctrl+G` / `/keys`) and the task label (`/task`). Two
1512
+ // registrations, no new mechanism: `/keys` sets the panel's pre-filter and opens
1513
+ // the same overlay `Ctrl+G` opens, and `/task` reports what the header cell says.
1514
+ registry.register(keysCommand);
1515
+ registry.register(taskCommand);
1505
1516
  }
1506
1517
  /**
1507
1518
  * Every built-in command name INCLUDING aliases (`quit`), for documentation and
@@ -1511,7 +1522,8 @@ export function registerBuiltinCommands(registry) {
1511
1522
  * `registry.get(name)` instead, because that is the only check that stays
1512
1523
  * correct when commands are registered dynamically (§7.1 / P1-2).
1513
1524
  */
1514
- export const BUILTIN_COMMAND_NAMES = ['queue', 'prompts', 'agents', ...COMMANDS.flatMap((c) => [
1525
+ export const BUILTIN_COMMAND_NAMES = ['queue', 'prompts', 'agents', 'keys', 'task',
1526
+ ...COMMANDS.flatMap((c) => [
1515
1527
  c.name,
1516
1528
  ...(c.aliases ?? []),
1517
1529
  ])].sort();
@@ -0,0 +1,23 @@
1
+ /**
2
+ * `/keys [query]` — open the keyboard drawer (tui-shortcut-drawer-and-task-summary
3
+ * §4.7).
4
+ *
5
+ * ASCII ONLY: `src/commands/**` is inside the glyph scanner's scope.
6
+ *
7
+ * THE COMMAND IS THE SECOND ENTRANCE TO THE SAME ROOM, not a second room: it sets
8
+ * the pre-filter and opens `overlay: 'shortcuts'`, exactly as `Ctrl+G` does. That
9
+ * pairing (`/agents` <-> `Ctrl+B`, `/help` <-> `?`) is the established one, and it
10
+ * exists because a key is unreachable in a script, a paste and a screen reader.
11
+ *
12
+ * NOTE ON NAMES: `CommandRegistry.register` overwrites unconditionally while the
13
+ * skill loader only claims a bare `/<name>` when nothing else has it, so a user with
14
+ * a skill called `keys` now reaches it as `/skill:keys`. That is registered in
15
+ * `CHANGELOG.md`; `/keys` deliberately has NO alias, because an alias would multiply
16
+ * the same shadowing.
17
+ */
18
+ import type { SlashCommand } from './registry.js';
19
+ /** The pure part: what to open and with what filter. Separated so it is testable. */
20
+ export declare function parseKeysArgs(args: string): {
21
+ query: string;
22
+ };
23
+ export declare const keysCommand: SlashCommand;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * `/keys [query]` — open the keyboard drawer (tui-shortcut-drawer-and-task-summary
3
+ * §4.7).
4
+ *
5
+ * ASCII ONLY: `src/commands/**` is inside the glyph scanner's scope.
6
+ *
7
+ * THE COMMAND IS THE SECOND ENTRANCE TO THE SAME ROOM, not a second room: it sets
8
+ * the pre-filter and opens `overlay: 'shortcuts'`, exactly as `Ctrl+G` does. That
9
+ * pairing (`/agents` <-> `Ctrl+B`, `/help` <-> `?`) is the established one, and it
10
+ * exists because a key is unreachable in a script, a paste and a screen reader.
11
+ *
12
+ * NOTE ON NAMES: `CommandRegistry.register` overwrites unconditionally while the
13
+ * skill loader only claims a bare `/<name>` when nothing else has it, so a user with
14
+ * a skill called `keys` now reaches it as `/skill:keys`. That is registered in
15
+ * `CHANGELOG.md`; `/keys` deliberately has NO alias, because an alias would multiply
16
+ * the same shadowing.
17
+ */
18
+ /** The pure part: what to open and with what filter. Separated so it is testable. */
19
+ export function parseKeysArgs(args) {
20
+ // ONE TOKEN, TRIMMED: the filter matches `id` / `keys` / `keyword` / aliases, and
21
+ // a multi-word query would only ever match a phrase none of them contain.
22
+ return { query: args.trim() };
23
+ }
24
+ export const keysCommand = {
25
+ name: 'keys',
26
+ description: 'List every keyboard shortcut (opens the drawer)',
27
+ run: (ctx) => {
28
+ const { query } = parseKeysArgs(ctx.args);
29
+ // WITHOUT THE PORT THERE IS NO PANEL TO OPEN, and saying so is better than
30
+ // opening nothing: this is the one command whose failure mode would otherwise be
31
+ // a keystroke that appears to do nothing at all.
32
+ if (!ctx.taskSummary) {
33
+ ctx.notify('warn', 'The shortcut drawer is not available in this build.');
34
+ return;
35
+ }
36
+ ctx.taskSummary.openPanel(query);
37
+ },
38
+ };
@@ -175,6 +175,45 @@ export interface CommandContext {
175
175
  * needs the SHAPE of the port, never a value at run time.
176
176
  */
177
177
  prompts?: PromptCommandPort;
178
+ /**
179
+ * The task label's three capabilities (`/keys`, `/task`; tui-shortcut-drawer-and-
180
+ * task-summary §6.4).
181
+ *
182
+ * OPTIONAL, and this is the sixth time this file records the same rule: every
183
+ * fixture in this package builds `CommandContext` by literal, so a REQUIRED field
184
+ * would fail to compile every one of them for a feature none of them is about.
185
+ * Absent means the build has no label machinery, and both commands say so instead
186
+ * of crashing - which matters most for `/task`, whose entire job is to explain why
187
+ * there is no label.
188
+ *
189
+ * TYPE-ONLY import, for the reason the `branch` note above records.
190
+ */
191
+ taskSummary?: TaskSummaryCommandPort;
192
+ }
193
+ /**
194
+ * What `/keys` and `/task` may do.
195
+ *
196
+ * THREE METHODS, NOT ONE: opening the drawer, reading the label and forcing a
197
+ * regeneration are three different user intentions, and `/task refresh` must not be
198
+ * expressible as "open the panel and press r".
199
+ */
200
+ export interface TaskSummaryCommandPort {
201
+ /** Open the drawer, optionally pre-filtered (`/keys <query>`). */
202
+ openPanel(query?: string): void;
203
+ /** The current label, its provenance and why it is a fallback. */
204
+ read(): {
205
+ view: {
206
+ text: string;
207
+ tone: 'model' | 'fallback';
208
+ reason: string | null;
209
+ by: string;
210
+ at: number;
211
+ } | null;
212
+ pending: boolean;
213
+ enabled: boolean;
214
+ };
215
+ /** Regenerate from the last user message. Returns the anchor it used, or `''`. */
216
+ refresh(): string;
178
217
  }
179
218
  /** What `/mouse` can see and do. Read at command time, never captured. */
180
219
  export interface MouseCommandPort {
@@ -0,0 +1,39 @@
1
+ /**
2
+ * `/task` and `/task refresh` (tui-shortcut-drawer-and-task-summary §4.7).
3
+ *
4
+ * ASCII ONLY: `src/commands/**` is inside the glyph scanner's scope.
5
+ *
6
+ * WHY "task summary" AND NEVER BARE "task": this package already has a TOOL named
7
+ * `task` (the subagent dispatcher, `team/task-tool.ts`), and `HelpOverlay`'s team
8
+ * table prints that name. A help line that said only "task" would make two unrelated
9
+ * things share one word on the same screen.
10
+ *
11
+ * WHY A NOTICE AND NOT AN OVERLAY: three lines of provenance do not deserve a
12
+ * frame, and the guaranteed reporting surface for "why is there no label" has to be
13
+ * somewhere the user can scroll back to - a toast disappears.
14
+ *
15
+ * BOTH COMMANDS ARE REGISTERED UNCONDITIONALLY, even with
16
+ * `taskSummary.enabled = false`: they are the only surface that can EXPLAIN the
17
+ * switch, and a missing command answers "unknown command" instead of "off" (the
18
+ * `/prompts` D-19 rule).
19
+ */
20
+ import type { SlashCommand } from './registry.js';
21
+ /** One line per fact, or a single honest line when there is no label. */
22
+ export declare function formatTaskSummaryReport(input: {
23
+ view: {
24
+ text: string;
25
+ tone: 'model' | 'fallback';
26
+ reason: string | null;
27
+ by: string;
28
+ at: number;
29
+ } | null;
30
+ pending: boolean;
31
+ enabled: boolean;
32
+ now: number;
33
+ }): string[];
34
+ /** `''` for a bare `/task`, `'refresh'` for `/task refresh`, else the raw argument. */
35
+ export declare function parseTaskArgs(args: string): {
36
+ action: 'show' | 'refresh' | 'unknown';
37
+ raw: string;
38
+ };
39
+ export declare const taskCommand: SlashCommand;
@@ -0,0 +1,102 @@
1
+ /**
2
+ * `/task` and `/task refresh` (tui-shortcut-drawer-and-task-summary §4.7).
3
+ *
4
+ * ASCII ONLY: `src/commands/**` is inside the glyph scanner's scope.
5
+ *
6
+ * WHY "task summary" AND NEVER BARE "task": this package already has a TOOL named
7
+ * `task` (the subagent dispatcher, `team/task-tool.ts`), and `HelpOverlay`'s team
8
+ * table prints that name. A help line that said only "task" would make two unrelated
9
+ * things share one word on the same screen.
10
+ *
11
+ * WHY A NOTICE AND NOT AN OVERLAY: three lines of provenance do not deserve a
12
+ * frame, and the guaranteed reporting surface for "why is there no label" has to be
13
+ * somewhere the user can scroll back to - a toast disappears.
14
+ *
15
+ * BOTH COMMANDS ARE REGISTERED UNCONDITIONALLY, even with
16
+ * `taskSummary.enabled = false`: they are the only surface that can EXPLAIN the
17
+ * switch, and a missing command answers "unknown command" instead of "off" (the
18
+ * `/prompts` D-19 rule).
19
+ */
20
+ /** One line per fact, or a single honest line when there is no label. */
21
+ export function formatTaskSummaryReport(input) {
22
+ if (!input.enabled) {
23
+ return [
24
+ 'Task summary is off (config taskSummary.enabled = false).',
25
+ 'Turn it back on with: aragon config set taskSummary.enabled true',
26
+ ];
27
+ }
28
+ if (input.view === null) {
29
+ if (input.pending)
30
+ return ['Task summary: still being generated.'];
31
+ return [
32
+ 'No task summary yet.',
33
+ 'It appears after the first prompt of a task; /task refresh regenerates it.',
34
+ ];
35
+ }
36
+ const age = formatAge(input.now - input.view.at);
37
+ const source = input.view.tone === 'fallback'
38
+ ? `your own words (${input.view.reason ?? 'fallback'})`
39
+ : input.view.by;
40
+ return [
41
+ `Task summary: ${input.view.text}`,
42
+ `Source: ${source}${age === '' ? '' : `, ${age}`}`,
43
+ 'Regenerate with /task refresh, or open the drawer (Ctrl+G) to see why.',
44
+ ];
45
+ }
46
+ /** `12s ago` / `3m ago` / `1h ago`, or `''` for a clock that went backwards. */
47
+ function formatAge(ageMs) {
48
+ if (!Number.isFinite(ageMs) || ageMs < 0)
49
+ return '';
50
+ const seconds = Math.floor(ageMs / 1000);
51
+ if (seconds < 60)
52
+ return `${seconds}s ago`;
53
+ const minutes = Math.floor(seconds / 60);
54
+ if (minutes < 60)
55
+ return `${minutes}m ago`;
56
+ return `${Math.floor(minutes / 60)}h ago`;
57
+ }
58
+ /** `''` for a bare `/task`, `'refresh'` for `/task refresh`, else the raw argument. */
59
+ export function parseTaskArgs(args) {
60
+ const raw = args.trim();
61
+ if (raw.length === 0)
62
+ return { action: 'show', raw };
63
+ if (raw.toLowerCase() === 'refresh')
64
+ return { action: 'refresh', raw };
65
+ return { action: 'unknown', raw };
66
+ }
67
+ /**
68
+ * The command body: parse, report, or regenerate.
69
+ *
70
+ * NAMED FOR WHAT IT DOES, not for the port it touches (review fix RF-5): the old
71
+ * name described a variable, and this function is the whole of `/task`.
72
+ */
73
+ function runTaskCommand(ctx) {
74
+ if (!ctx.taskSummary) {
75
+ ctx.notify('warn', 'The task summary is not available in this build.');
76
+ return;
77
+ }
78
+ const port = ctx.taskSummary;
79
+ const { action, raw } = parseTaskArgs(ctx.args);
80
+ if (action === 'unknown') {
81
+ ctx.notify('warn', `Unknown argument "${raw}" - use /task or /task refresh.`);
82
+ return;
83
+ }
84
+ if (action === 'refresh') {
85
+ const anchor = port.refresh();
86
+ if (anchor === '') {
87
+ ctx.notify('info', 'No prompt to summarise yet - send one first.');
88
+ return;
89
+ }
90
+ ctx.notify('info', 'Regenerating the task summary...');
91
+ return;
92
+ }
93
+ const state = port.read();
94
+ for (const line of formatTaskSummaryReport({ ...state, now: Date.now() })) {
95
+ ctx.notify('info', line);
96
+ }
97
+ }
98
+ export const taskCommand = {
99
+ name: 'task',
100
+ description: 'Show the header task summary (/task refresh regenerates it)',
101
+ run: runTaskCommand,
102
+ };
@@ -34,6 +34,17 @@ export declare const TEAM_CONFIG_SET_KEYS: readonly ["team.enabled", "team.maxSu
34
34
  * so `jobs.waitSeconds` resolves and `jobs.mirror.enabled` would not.
35
35
  */
36
36
  export declare const JOBS_CONFIG_SET_KEYS: readonly ["jobs.enabled", "jobs.waitSeconds", "jobs.stallSeconds", "jobs.persistLog", "jobs.noticeSteer"];
37
+ /**
38
+ * Config keys under `taskSummary.` that `aragon config set` accepts
39
+ * (tui-shortcut-drawer-and-task-summary §4.8).
40
+ *
41
+ * BOTH SWITCHES ARE WRITABLE, which is why this section needs the same pair every
42
+ * other writable section has: a membership set here AND a case in `cli.tsx`'s
43
+ * `runConfigSet`. Only the second makes the key actually write; only the first
44
+ * makes it pass the `Unknown config key` gate. `config.test.ts`'s static guard is
45
+ * what catches a half-done pair, and `jobs` is the precedent it was written for.
46
+ */
47
+ export declare const TASK_SUMMARY_CONFIG_SET_KEYS: readonly ["taskSummary.enabled", "taskSummary.fallback"];
37
48
  /**
38
49
  * Config keys under `retry.` that `aragon config set` accepts
39
50
  * (llm-api-retry-backoff §8.2).
@@ -108,6 +119,18 @@ export declare function applyLogConfigSet(key: string, value: string): Partial<P
108
119
  * recorded in `store.ts`.
109
120
  */
110
121
  export declare function applyJobsConfigSet(key: string, value: string): Partial<PersistedConfig> | null;
122
+ /**
123
+ * Translate a `taskSummary.*` dotted key into a patch, or `null` for anything else.
124
+ *
125
+ * THE PATCH IS A PARTIAL SECTION, safe only because `updatePersistedConfig`
126
+ * deep-merges `taskSummary` - the eighth time that trap is recorded in `store.ts`.
127
+ * Both values are booleans, so `isTrue` is the whole parser. It accepts `1` and
128
+ * `true` only, which is NARROWER than the env layer's `on`/`yes` on purpose: an
129
+ * environment switch is typed by a human at a shell prompt, while `config set` is
130
+ * also driven by scripts, and `config set taskSummary.enabled on` should fail loudly
131
+ * rather than silently mean `false`.
132
+ */
133
+ export declare function applyTaskSummaryConfigSet(key: string, value: string): Partial<PersistedConfig> | null;
111
134
  /**
112
135
  * Translate a `team.*` dotted key into a patch, or `null` for anything else.
113
136
  *
@@ -67,6 +67,20 @@ export const JOBS_CONFIG_SET_KEYS = [
67
67
  'jobs.persistLog',
68
68
  'jobs.noticeSteer',
69
69
  ];
70
+ /**
71
+ * Config keys under `taskSummary.` that `aragon config set` accepts
72
+ * (tui-shortcut-drawer-and-task-summary §4.8).
73
+ *
74
+ * BOTH SWITCHES ARE WRITABLE, which is why this section needs the same pair every
75
+ * other writable section has: a membership set here AND a case in `cli.tsx`'s
76
+ * `runConfigSet`. Only the second makes the key actually write; only the first
77
+ * makes it pass the `Unknown config key` gate. `config.test.ts`'s static guard is
78
+ * what catches a half-done pair, and `jobs` is the precedent it was written for.
79
+ */
80
+ export const TASK_SUMMARY_CONFIG_SET_KEYS = [
81
+ 'taskSummary.enabled',
82
+ 'taskSummary.fallback',
83
+ ];
70
84
  /**
71
85
  * Config keys under `retry.` that `aragon config set` accepts
72
86
  * (llm-api-retry-backoff §8.2).
@@ -219,6 +233,28 @@ export function applyJobsConfigSet(key, value) {
219
233
  return null;
220
234
  }
221
235
  }
236
+ /**
237
+ * Translate a `taskSummary.*` dotted key into a patch, or `null` for anything else.
238
+ *
239
+ * THE PATCH IS A PARTIAL SECTION, safe only because `updatePersistedConfig`
240
+ * deep-merges `taskSummary` - the eighth time that trap is recorded in `store.ts`.
241
+ * Both values are booleans, so `isTrue` is the whole parser. It accepts `1` and
242
+ * `true` only, which is NARROWER than the env layer's `on`/`yes` on purpose: an
243
+ * environment switch is typed by a human at a shell prompt, while `config set` is
244
+ * also driven by scripts, and `config set taskSummary.enabled on` should fail loudly
245
+ * rather than silently mean `false`.
246
+ */
247
+ export function applyTaskSummaryConfigSet(key, value) {
248
+ const patch = (taskSummary) => ({ taskSummary });
249
+ switch (key) {
250
+ case 'taskSummary.enabled':
251
+ return patch({ enabled: isTrue(value) });
252
+ case 'taskSummary.fallback':
253
+ return patch({ fallback: isTrue(value) });
254
+ default:
255
+ return null;
256
+ }
257
+ }
222
258
  /**
223
259
  * Translate a `team.*` dotted key into a patch, or `null` for anything else.
224
260
  *
@@ -308,16 +308,29 @@ export function readEnvConfig() {
308
308
  partial.jobs = section;
309
309
  }
310
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.
311
+ // `Ctrl+P` and `/prompts` reports that the feature is off (D-19 / AC-31). ONLY
312
+ // `enabled` IS REACHABLE HERE. `insertMode` is a preference a human sets once, and
313
+ // an environment variable that could silently flip a fill into "overwrite what you
314
+ // had typed" is not a kill switch.
315
315
  const prompts = process.env.ARAGON_PROMPTS?.trim().toLowerCase();
316
316
  if (prompts !== undefined && prompts.length > 0) {
317
317
  partial.prompts = {
318
318
  enabled: prompts === '1' || prompts === 'true' || prompts === 'on' || prompts === 'yes',
319
319
  };
320
320
  }
321
+ // ARAGON_TASK_SUMMARY is the header label's kill switch
322
+ // (tui-shortcut-drawer-and-task-summary §4.8 / P2-7), and it maps `enabled` and
323
+ // NOTHING ELSE - the same wording and the same reason as the block above. A
324
+ // variable that could also flip `fallback` would let an environment decide
325
+ // whether a FAILED call shows the user's own words or an empty cell, which is a
326
+ // silent change to what the header claims to know.
327
+ const taskSummary = process.env.ARAGON_TASK_SUMMARY?.trim().toLowerCase();
328
+ if (taskSummary !== undefined && taskSummary.length > 0) {
329
+ partial.taskSummary = {
330
+ enabled: taskSummary === '1' || taskSummary === 'true'
331
+ || taskSummary === 'on' || taskSummary === 'yes',
332
+ };
333
+ }
321
334
  const retry = process.env.ARAGON_RETRY?.trim().toLowerCase();
322
335
  const retryMax = process.env.ARAGON_RETRY_MAX?.trim();
323
336
  if ((retry !== undefined && retry.length > 0) || (retryMax !== undefined && retryMax.length > 0)) {
@@ -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, clampPromptsConfig, 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, clampTaskSummaryConfig, 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';
@@ -738,6 +738,11 @@ export function loadConfig(flags = {}, options = {}) {
738
738
  // two different shapes on `CliConfig`.
739
739
  prompts: clampPromptsConfig({ ...DEFAULT_CONFIG.prompts, ...(env.partial.prompts ?? {}),
740
740
  ...(file.prompts ?? {}) }),
741
+ // The header task label's two switches, composed EXACTLY like `prompts` above:
742
+ // env + file + clamp, so an `ARAGON_TASK_SUMMARY=0` and a file-only
743
+ // `taskSummary.enabled: false` cannot arrive as two different shapes.
744
+ taskSummary: clampTaskSummaryConfig({ ...DEFAULT_CONFIG.taskSummary,
745
+ ...(env.partial.taskSummary ?? {}), ...(file.taskSummary ?? {}) }),
741
746
  cwd,
742
747
  color,
743
748
  colorLevel,