@aragon-agent/cli 0.6.38 → 0.6.40

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 (209) hide show
  1. package/CHANGELOG.md +263 -0
  2. package/README.md +544 -11
  3. package/dist/agent/controller.d.ts +245 -10
  4. package/dist/agent/controller.js +367 -22
  5. package/dist/agent/model-profile-settings.js +12 -1
  6. package/dist/agent/plan-prompt.d.ts +1 -3
  7. package/dist/agent/plan-prompt.js +13 -7
  8. package/dist/agent/reducer.d.ts +70 -2
  9. package/dist/agent/reducer.js +59 -6
  10. package/dist/agent/skill-triage-prompt.d.ts +42 -0
  11. package/dist/agent/skill-triage-prompt.js +74 -0
  12. package/dist/agent/system-prompt.d.ts +28 -2
  13. package/dist/agent/system-prompt.js +12 -4
  14. package/dist/cli.js +106 -32
  15. package/dist/commands/builtins.js +24 -3
  16. package/dist/commands/keys-command.d.ts +23 -0
  17. package/dist/commands/keys-command.js +38 -0
  18. package/dist/commands/registry.d.ts +39 -0
  19. package/dist/commands/skills.js +86 -1
  20. package/dist/commands/subagent-command.d.ts +25 -0
  21. package/dist/commands/subagent-command.js +242 -0
  22. package/dist/commands/task-command.d.ts +39 -0
  23. package/dist/commands/task-command.js +102 -0
  24. package/dist/compaction/command.js +16 -3
  25. package/dist/compaction/compactor.d.ts +25 -0
  26. package/dist/compaction/compactor.js +111 -10
  27. package/dist/compaction/digest.d.ts +24 -1
  28. package/dist/compaction/digest.js +139 -17
  29. package/dist/compaction/limits.d.ts +21 -0
  30. package/dist/compaction/limits.js +21 -0
  31. package/dist/compaction/reason-text.d.ts +36 -0
  32. package/dist/compaction/reason-text.js +78 -0
  33. package/dist/compaction/types.d.ts +82 -14
  34. package/dist/compaction/wiring.d.ts +13 -8
  35. package/dist/compaction/wiring.js +1 -0
  36. package/dist/config/cli-commands.d.ts +23 -0
  37. package/dist/config/cli-commands.js +36 -0
  38. package/dist/config/env.js +17 -4
  39. package/dist/config/load.js +55 -3
  40. package/dist/config/schema.d.ts +125 -10
  41. package/dist/config/schema.js +138 -14
  42. package/dist/config/store.js +23 -3
  43. package/dist/diagnostics/doctor.d.ts +9 -0
  44. package/dist/diagnostics/doctor.js +34 -0
  45. package/dist/diagnostics/info.d.ts +71 -0
  46. package/dist/diagnostics/info.js +91 -5
  47. package/dist/exec/bridge/caller-tools.d.ts +96 -0
  48. package/dist/exec/bridge/caller-tools.js +213 -0
  49. package/dist/exec/bridge/decisions.d.ts +78 -0
  50. package/dist/exec/bridge/decisions.js +116 -0
  51. package/dist/exec/bridge/human-answers.d.ts +38 -0
  52. package/dist/exec/bridge/human-answers.js +164 -0
  53. package/dist/exec/bridge/limits.d.ts +68 -0
  54. package/dist/exec/bridge/limits.js +68 -0
  55. package/dist/exec/bridge/ndjson.d.ts +17 -0
  56. package/dist/exec/bridge/ndjson.js +83 -0
  57. package/dist/exec/bridge/pending.d.ts +70 -0
  58. package/dist/exec/bridge/pending.js +132 -0
  59. package/dist/exec/bridge/protocol.d.ts +346 -0
  60. package/dist/exec/bridge/protocol.js +256 -0
  61. package/dist/exec/bridge/server.d.ts +142 -0
  62. package/dist/exec/bridge/server.js +502 -0
  63. package/dist/exec/bridge/source.d.ts +58 -0
  64. package/dist/exec/bridge/source.js +145 -0
  65. package/dist/exec/bridge/writer.d.ts +26 -0
  66. package/dist/exec/bridge/writer.js +156 -0
  67. package/dist/exec/cli-commands.js +30 -2
  68. package/dist/exec/emitter.d.ts +16 -2
  69. package/dist/exec/emitter.js +26 -6
  70. package/dist/exec/events.d.ts +465 -10
  71. package/dist/exec/events.js +47 -1
  72. package/dist/exec/hooks/dispatch.d.ts +76 -0
  73. package/dist/exec/hooks/dispatch.js +169 -0
  74. package/dist/exec/hooks/index.d.ts +83 -0
  75. package/dist/exec/hooks/index.js +205 -0
  76. package/dist/exec/hooks/limits.d.ts +56 -0
  77. package/dist/exec/hooks/limits.js +49 -0
  78. package/dist/exec/hooks/registry.d.ts +49 -0
  79. package/dist/exec/hooks/registry.js +91 -0
  80. package/dist/exec/hooks/tool-wrapper.d.ts +94 -0
  81. package/dist/exec/hooks/tool-wrapper.js +154 -0
  82. package/dist/exec/hooks/turn.d.ts +61 -0
  83. package/dist/exec/hooks/turn.js +59 -0
  84. package/dist/exec/hooks/validate.d.ts +85 -0
  85. package/dist/exec/hooks/validate.js +310 -0
  86. package/dist/exec/index.d.ts +130 -4
  87. package/dist/exec/index.js +501 -10
  88. package/dist/exec/options.d.ts +124 -2
  89. package/dist/exec/options.js +413 -6
  90. package/dist/exec/permission.d.ts +9 -1
  91. package/dist/exec/permission.js +7 -1
  92. package/dist/exec/request/limits.d.ts +51 -0
  93. package/dist/exec/request/limits.js +49 -0
  94. package/dist/exec/request/scope.d.ts +106 -0
  95. package/dist/exec/request/scope.js +149 -0
  96. package/dist/exec/result/limits.d.ts +74 -0
  97. package/dist/exec/result/limits.js +72 -0
  98. package/dist/exec/result/prompt.d.ts +28 -0
  99. package/dist/exec/result/prompt.js +74 -0
  100. package/dist/exec/result/schema.d.ts +99 -0
  101. package/dist/exec/result/schema.js +464 -0
  102. package/dist/exec/result/sink.d.ts +56 -0
  103. package/dist/exec/result/sink.js +50 -0
  104. package/dist/exec/result/tool.d.ts +37 -0
  105. package/dist/exec/result/tool.js +118 -0
  106. package/dist/exec/runner.d.ts +395 -1
  107. package/dist/exec/runner.js +860 -8
  108. package/dist/exec/threads/aggregate.d.ts +34 -0
  109. package/dist/exec/threads/aggregate.js +30 -0
  110. package/dist/exec/threads/capacity.d.ts +23 -0
  111. package/dist/exec/threads/capacity.js +38 -0
  112. package/dist/exec/threads/dispatch-source.d.ts +60 -0
  113. package/dist/exec/threads/dispatch-source.js +106 -0
  114. package/dist/exec/threads/execute-threads.d.ts +53 -0
  115. package/dist/exec/threads/execute-threads.js +234 -0
  116. package/dist/exec/threads/gate.d.ts +42 -0
  117. package/dist/exec/threads/gate.js +81 -0
  118. package/dist/exec/threads/identity.d.ts +36 -0
  119. package/dist/exec/threads/identity.js +67 -0
  120. package/dist/exec/threads/interrupt.d.ts +29 -0
  121. package/dist/exec/threads/interrupt.js +79 -0
  122. package/dist/exec/threads/limits.d.ts +58 -0
  123. package/dist/exec/threads/limits.js +35 -0
  124. package/dist/exec/threads/outcome.d.ts +47 -0
  125. package/dist/exec/threads/outcome.js +26 -0
  126. package/dist/exec/threads/registry.d.ts +81 -0
  127. package/dist/exec/threads/registry.js +427 -0
  128. package/dist/exec/threads/runtime.d.ts +107 -0
  129. package/dist/exec/threads/runtime.js +523 -0
  130. package/dist/exec/threads/thread-source.d.ts +20 -0
  131. package/dist/exec/threads/thread-source.js +134 -0
  132. package/dist/fast/limits.d.ts +54 -0
  133. package/dist/fast/limits.js +54 -0
  134. package/dist/fast/summary-call.d.ts +72 -0
  135. package/dist/fast/summary-call.js +133 -0
  136. package/dist/fast/summary-prompt.d.ts +106 -0
  137. package/dist/fast/summary-prompt.js +320 -0
  138. package/dist/fast/task-summary-types.d.ts +66 -0
  139. package/dist/fast/task-summary-types.js +17 -0
  140. package/dist/logging/logger.d.ts +1 -1
  141. package/dist/proc/limits.d.ts +7 -0
  142. package/dist/proc/limits.js +7 -0
  143. package/dist/proc/log-mirror.d.ts +21 -4
  144. package/dist/proc/log-mirror.js +29 -7
  145. package/dist/proc/output-streams.d.ts +50 -0
  146. package/dist/proc/output-streams.js +54 -0
  147. package/dist/skills/service.d.ts +41 -0
  148. package/dist/skills/service.js +62 -0
  149. package/dist/skills/triage.d.ts +194 -0
  150. package/dist/skills/triage.js +393 -0
  151. package/dist/team/forced-dispatch.d.ts +95 -0
  152. package/dist/team/forced-dispatch.js +208 -0
  153. package/dist/team/limits.d.ts +21 -1
  154. package/dist/team/limits.js +21 -1
  155. package/dist/team/normalize.d.ts +15 -0
  156. package/dist/team/normalize.js +21 -4
  157. package/dist/team/prompt.d.ts +6 -3
  158. package/dist/team/prompt.js +9 -5
  159. package/dist/team/runtime.d.ts +9 -0
  160. package/dist/team/runtime.js +8 -0
  161. package/dist/team/subagent.d.ts +15 -0
  162. package/dist/team/subagent.js +45 -1
  163. package/dist/team/task-tool.js +11 -7
  164. package/dist/tools/bash-tool.d.ts +10 -0
  165. package/dist/tools/bash-tool.js +71 -6
  166. package/dist/tools/human-input.d.ts +35 -2
  167. package/dist/tools/human-input.js +38 -0
  168. package/dist/tools/index.d.ts +93 -1
  169. package/dist/tools/index.js +112 -3
  170. package/dist/tools/plan-tools.d.ts +20 -12
  171. package/dist/tools/plan-tools.js +24 -17
  172. package/dist/tools/tool-gate.d.ts +100 -0
  173. package/dist/tools/tool-gate.js +87 -0
  174. package/dist/ui/App.js +238 -12
  175. package/dist/ui/Header.d.ts +28 -16
  176. package/dist/ui/Header.js +21 -19
  177. package/dist/ui/StatusBar.d.ts +1 -0
  178. package/dist/ui/StatusBar.js +1 -0
  179. package/dist/ui/Transcript.js +1 -1
  180. package/dist/ui/entries/CompactionCard.d.ts +11 -1
  181. package/dist/ui/entries/CompactionCard.js +65 -4
  182. package/dist/ui/interaction-hints.js +6 -0
  183. package/dist/ui/layout/OverlayFrame.d.ts +18 -0
  184. package/dist/ui/layout/OverlayFrame.js +19 -4
  185. package/dist/ui/layout/header-hints.d.ts +55 -62
  186. package/dist/ui/layout/header-hints.js +59 -166
  187. package/dist/ui/layout/header-left.d.ts +115 -0
  188. package/dist/ui/layout/header-left.js +206 -0
  189. package/dist/ui/layout/shortcut-panel-layout.d.ts +191 -0
  190. package/dist/ui/layout/shortcut-panel-layout.js +321 -0
  191. package/dist/ui/layout/status-detail-layout.js +5 -1
  192. package/dist/ui/layout/status-layout.d.ts +10 -0
  193. package/dist/ui/layout/status-layout.js +18 -1
  194. package/dist/ui/layout/task-summary-cell.d.ts +61 -0
  195. package/dist/ui/layout/task-summary-cell.js +65 -0
  196. package/dist/ui/overlays/HelpOverlay.d.ts +12 -0
  197. package/dist/ui/overlays/HelpOverlay.js +29 -2
  198. package/dist/ui/overlays/SettingsScreen.js +29 -5
  199. package/dist/ui/overlays/ShortcutPanel.d.ts +39 -0
  200. package/dist/ui/overlays/ShortcutPanel.js +106 -0
  201. package/dist/ui/overlays/TeamFanoutPage.d.ts +35 -0
  202. package/dist/ui/overlays/TeamFanoutPage.js +78 -0
  203. package/dist/ui/overlays/model-profile-state.d.ts +19 -1
  204. package/dist/ui/overlays/model-profile-state.js +12 -0
  205. package/dist/ui/overlays/team-fanout-state.d.ts +73 -0
  206. package/dist/ui/overlays/team-fanout-state.js +97 -0
  207. package/dist/ui/shortcuts.d.ts +81 -0
  208. package/dist/ui/shortcuts.js +221 -0
  209. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -25,11 +25,11 @@ 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';
32
- import { ADAPTER_PROVIDERS, clampDensity, clampTheme, clampThinkingLevel, clampAskRounds, clampHumanTimeout, clampSkillsIntegrity, clampSkillsToolPolicy, clampContextWindow, clampMaxRenderInterval, clampScrollMode, clampScrollResumeMs, clampTranscriptRetain, clampTranscriptWindow, coercePositiveInt, isAdapterProvider, isAutoToken, parseMaxTokensInput, DEFAULT_MAX_RENDER_INTERVAL_MS, DEFAULT_MAX_TOKENS, DEFAULT_PLAN_HUMAN_TIMEOUT_MS, DEFAULT_PLAN_MAX_ASK_ROUNDS, DEFAULT_SCROLL_MODE, DEFAULT_SCROLL_RESUME_MS, DEFAULT_SKILLS_CONFIG, DEFAULT_TRANSCRIPT_RETAIN, DEFAULT_TRANSCRIPT_WINDOW, } from './config/schema.js';
32
+ import { ADAPTER_PROVIDERS, clampDensity, clampTheme, clampThinkingLevel, clampHumanTimeout, clampSkillsIntegrity, clampSkillsToolPolicy, clampContextWindow, clampMaxRenderInterval, clampScrollMode, clampScrollResumeMs, clampTranscriptRetain, clampTranscriptWindow, coercePositiveInt, isAdapterProvider, isAutoToken, parseMaxTokensInput, RETIRED_CONFIG_KEYS, DEFAULT_MAX_RENDER_INTERVAL_MS, DEFAULT_MAX_TOKENS, DEFAULT_PLAN_HUMAN_TIMEOUT_MS, DEFAULT_SCROLL_MODE, DEFAULT_SCROLL_RESUME_MS, DEFAULT_SKILLS_CONFIG, DEFAULT_TRANSCRIPT_RETAIN, DEFAULT_TRANSCRIPT_WINDOW, } from './config/schema.js';
33
33
  import { makeGetApiKey } from './config/load.js';
34
34
  import { AgentController, DENY_ALL_APPROVAL } from './agent/controller.js';
35
35
  import { runSkillsCommand } from './skills/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
  /**
@@ -1517,7 +1559,6 @@ const CONFIG_SET_KEYS = new Set([
1517
1559
  'scrollResumeMs',
1518
1560
  'historyEnabled',
1519
1561
  'planModeDefault',
1520
- 'planModeMaxAskRounds',
1521
1562
  'planModeHumanTimeoutMs',
1522
1563
  // Dotted keys route into the nested `skills` object; see the switch below.
1523
1564
  'skills.enabled',
@@ -1527,6 +1568,7 @@ const CONFIG_SET_KEYS = new Set([
1527
1568
  'skills.integrity',
1528
1569
  'skills.usageTracking',
1529
1570
  'skills.toolPolicy',
1571
+ 'skills.triage',
1530
1572
  // `log.*` keys are handled by `applyLogConfigSet` below, NOT by the switch:
1531
1573
  // that switch is already past this package's complexity ceiling.
1532
1574
  ...LOG_CONFIG_SET_KEYS,
@@ -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>`.
@@ -1612,6 +1660,18 @@ function runConfigSet(key, value) {
1612
1660
  }
1613
1661
  }
1614
1662
  function runConfigSetValue(key, value) {
1663
+ // RETIRED KEYS ARE REFUSED BY NAME, BEFORE THE UNKNOWN-KEY CHECK. Falling
1664
+ // through to `Unknown config key` would be technically true and practically
1665
+ // useless: a script that has run `config set planModeMaxAskRounds 5` for months
1666
+ // needs to be told what happened to it, not that it never existed. Exit 2 for
1667
+ // the same reason a bad value is 2 — the caller's argument is wrong, the file
1668
+ // is not.
1669
+ const retired = RETIRED_CONFIG_KEYS[key];
1670
+ if (retired !== undefined) {
1671
+ process.stderr.write(`"${key}" was removed: ${retired}\n`);
1672
+ process.exitCode = 2;
1673
+ return;
1674
+ }
1615
1675
  if (!CONFIG_SET_KEYS.has(key)) {
1616
1676
  process.stderr.write(`Unknown config key "${key}". Known keys: ${[...CONFIG_SET_KEYS].join(', ')}.\n`);
1617
1677
  process.exitCode = 2;
@@ -1701,6 +1761,16 @@ function runConfigSetValue(key, value) {
1701
1761
  process.stdout.write(`Set ${key} = ${String(merged.jobs[field])}\n`);
1702
1762
  return;
1703
1763
  }
1764
+ const taskSummaryPatch = applyTaskSummaryConfigSet(key, value);
1765
+ if (taskSummaryPatch) {
1766
+ // Echoed from the STORED value, like every section above: `config set
1767
+ // taskSummary.enabled banana` prints `false`, which is what the read path will
1768
+ // report and therefore the only honest answer.
1769
+ const merged = updatePersistedConfig(taskSummaryPatch);
1770
+ const field = key.slice('taskSummary.'.length);
1771
+ process.stdout.write(`Set ${key} = ${String(merged.taskSummary[field])}\n`);
1772
+ return;
1773
+ }
1704
1774
  const patch = {};
1705
1775
  switch (key) {
1706
1776
  case 'provider':
@@ -1794,9 +1864,6 @@ function runConfigSetValue(key, value) {
1794
1864
  case 'planModeDefault':
1795
1865
  patch.planModeDefault = value === 'true' || value === '1';
1796
1866
  break;
1797
- case 'planModeMaxAskRounds':
1798
- patch.planModeMaxAskRounds = clampAskRounds(value, DEFAULT_PLAN_MAX_ASK_ROUNDS);
1799
- break;
1800
1867
  case 'planModeHumanTimeoutMs':
1801
1868
  patch.planModeHumanTimeoutMs = clampHumanTimeout(value, DEFAULT_PLAN_HUMAN_TIMEOUT_MS);
1802
1869
  break;
@@ -1835,6 +1902,13 @@ function runConfigSetValue(key, value) {
1835
1902
  toolPolicy: clampSkillsToolPolicy(value, DEFAULT_SKILLS_CONFIG.toolPolicy),
1836
1903
  };
1837
1904
  break;
1905
+ // The triage master switch. `false` restores the pre-feature prompt BYTE FOR
1906
+ // BYTE (invariant I-T1), which is the reason it is a key rather than a flag.
1907
+ case 'skills.triage':
1908
+ patch.skills = {
1909
+ triage: value === 'true' || value === '1',
1910
+ };
1911
+ break;
1838
1912
  }
1839
1913
  updatePersistedConfig(patch);
1840
1914
  process.stdout.write(`Set ${key} = ${value}\n`);
@@ -27,6 +27,13 @@ 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
+ import { registerSubagentCommand } from './subagent-command.js';
31
+ // THE DRAWER AND THE HEADER LABEL (tui-shortcut-drawer-and-task-summary §4.7):
32
+ // two single-purpose commands in their own modules, registered from here. Both are
33
+ // UNCONDITIONAL, even with `taskSummary.enabled=false`, because they are the only
34
+ // surfaces that can explain the switch.
35
+ import { keysCommand } from './keys-command.js';
36
+ import { taskCommand } from './task-command.js';
30
37
  import { perfCommand } from './perf.js';
31
38
  import { runCompactCommand } from '../compaction/command.js';
32
39
  import { runClearCommand } from './clear-command.js';
@@ -406,8 +413,10 @@ const COMMANDS = [
406
413
  if (arg === 'status') {
407
414
  const s = ctx.controller.getPlanStatus();
408
415
  const pending = s.pending ? ` (pending ${MODE_LABEL[s.pending]} after this run)` : '';
409
- ctx.notify('info', `Mode: ${MODE_LABEL[s.effective]}${pending}. ` +
410
- `Ask rounds used: ${s.askRoundsUsed}/${s.maxAskRounds}.`);
416
+ // NO ROUND COUNT. The old line ended with `Ask rounds used: 3/4.`, which
417
+ // told the user their agent was rationing questions - a limit that no
418
+ // longer exists. Nothing replaced it: "0 of unlimited" is noise.
419
+ ctx.notify('info', `Mode: ${MODE_LABEL[s.effective]}${pending}.`);
411
420
  return;
412
421
  }
413
422
  if (arg.length > 0 && arg !== 'on' && arg !== 'off') {
@@ -1502,6 +1511,16 @@ export function registerBuiltinCommands(registry) {
1502
1511
  // The read-only subagent board (tui-subagent-panel §6.4). Same shape again: the
1503
1512
  // body is `commands/agents-command.ts` and the registration is this one line.
1504
1513
  registerAgentsCommand(registry);
1514
+ // `/subagent` (subagent-fanout 3.5). REGISTERED UNCONDITIONALLY, like `/prompts`
1515
+ // and for the same reason: a `--no-team` session must still be able to ask the
1516
+ // command why it cannot force anything (I-7 / AC-15) - so registering it may
1517
+ // not read config, build a connection or send a request.
1518
+ registerSubagentCommand(registry);
1519
+ // The keyboard drawer (`Ctrl+G` / `/keys`) and the task label (`/task`). Two
1520
+ // registrations, no new mechanism: `/keys` sets the panel's pre-filter and opens
1521
+ // the same overlay `Ctrl+G` opens, and `/task` reports what the header cell says.
1522
+ registry.register(keysCommand);
1523
+ registry.register(taskCommand);
1505
1524
  }
1506
1525
  /**
1507
1526
  * Every built-in command name INCLUDING aliases (`quit`), for documentation and
@@ -1511,7 +1530,9 @@ export function registerBuiltinCommands(registry) {
1511
1530
  * `registry.get(name)` instead, because that is the only check that stays
1512
1531
  * correct when commands are registered dynamically (§7.1 / P1-2).
1513
1532
  */
1514
- export const BUILTIN_COMMAND_NAMES = ['queue', 'prompts', 'agents', ...COMMANDS.flatMap((c) => [
1533
+ export const BUILTIN_COMMAND_NAMES = ['queue', 'prompts', 'agents', 'subagent',
1534
+ 'keys', 'task',
1535
+ ...COMMANDS.flatMap((c) => [
1515
1536
  c.name,
1516
1537
  ...(c.aliases ?? []),
1517
1538
  ])].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 {
@@ -12,6 +12,7 @@ import { checkManifest } from '../skills/manifest.js';
12
12
  import { updateAllSkills, updateSkill, isHardFailure } from '../skills/updater.js';
13
13
  import { currentPlatform } from '../skills/node-host.js';
14
14
  import { listUsage, resetUsage, getUsagePath } from '../skills/usage.js';
15
+ import { SKILL_TRIAGE_LIMITS } from '../skills/triage.js';
15
16
  import { HOST_TOOL_NAMES, SKILL_TOOL_FLOOR } from '../tools/index.js';
16
17
  import { SKILLS_TOOL_POLICY_MODES, isSkillsToolPolicyMode } from '../config/schema.js';
17
18
  import { pickGlyphs } from '../ui/glyphs.js';
@@ -92,12 +93,96 @@ async function runSkills(ctx, service, version) {
92
93
  return unloadFrames(ctx, service);
93
94
  case 'usage':
94
95
  return usageFromCommand(ctx, service, argument);
96
+ case 'triage':
97
+ return triageFromCommand(ctx, service, argument);
98
+ case 'route':
99
+ return routeFromCommand(ctx, service, argument);
95
100
  default:
96
101
  ctx.notify('warn', `Unknown subcommand "/skills ${sub}". Try: list, info, install, update, remove, enable, ` +
97
- 'disable, reload, create, trust, untrust, policy, unload, usage.');
102
+ 'disable, reload, create, trust, untrust, policy, unload, usage, triage, route.');
98
103
  }
99
104
  }
100
105
  // ---------------------------------------------------------------------------
106
+ // /skills triage · route (skill-triage-and-unbounded-ask §4.2.8)
107
+ // ---------------------------------------------------------------------------
108
+ /**
109
+ * `/skills triage [on|off]`.
110
+ *
111
+ * The report names the switch AND the last turn's result, because "on" alone does
112
+ * not answer the question a user actually has, which is "is it finding anything?".
113
+ * `lastSkillRouting` comes from the controller — it is per-turn session state, not
114
+ * skill-service state, and the service must not start owning a copy of it.
115
+ */
116
+ function triageFromCommand(ctx, service, argument) {
117
+ const requested = argument.trim().toLowerCase();
118
+ if (requested.length === 0) {
119
+ const on = service.getConfig().triage === true;
120
+ const last = ctx.controller.getSkillRouting();
121
+ const summary = last === null
122
+ ? 'No turn has been triaged yet.'
123
+ : last.candidates.length === 0
124
+ ? `Last turn: no candidate (${last.eligible} eligible skill(s) scored).`
125
+ : `Last turn: ${last.candidates.length} candidate(s), best ${last.candidates[0].name} (${last.candidates[0].score}).`;
126
+ ctx.notify('info', `Skill triage: ${on ? 'on' : 'off'}. ${summary}`);
127
+ return;
128
+ }
129
+ if (requested !== 'on' && requested !== 'off') {
130
+ ctx.notify('warn', `Unknown argument "${argument}" - use /skills triage [on|off].`);
131
+ return;
132
+ }
133
+ const enabled = requested === 'on';
134
+ // `setTriageEnabled` persists AND fires `onChange`, which rebuilds the prompt in
135
+ // this session. Without that second half the switch would look applied and take
136
+ // effect only after a restart (AC-T23).
137
+ service.setTriageEnabled(enabled);
138
+ // A toast, not a transcript notice: flipping a switch is an acknowledgement, and
139
+ // `toast` is this package's channel for exactly that (see `toggleSkill`). The
140
+ // level still differs by direction, so "off" does not read as a win.
141
+ ctx.toast(enabled ? 'success' : 'info', `Skill triage: ${enabled ? 'on' : 'off'}. ` +
142
+ (enabled
143
+ ? 'Each new task gets a short candidate list.'
144
+ : 'The system prompt no longer carries the triage block.'));
145
+ }
146
+ /**
147
+ * `/skills route [text]`.
148
+ *
149
+ * Two modes, one output shape. With text it routes that text; without text it
150
+ * replays the previous turn's real routing, which is the case a user is most often
151
+ * in — they just watched the model ignore a suggestion. The "never triaged" text
152
+ * names the form that works instead of leaving them to guess.
153
+ */
154
+ function routeFromCommand(ctx, service, argument) {
155
+ const task = argument.trim();
156
+ if (task.length === 0) {
157
+ const last = ctx.controller.getSkillRouting();
158
+ if (last === null) {
159
+ ctx.notify('info', 'No turn has been triaged yet. Run /skills route <text> to try one.');
160
+ return;
161
+ }
162
+ ctx.notify('info', renderRoutingReport(last));
163
+ return;
164
+ }
165
+ // A fresh computation, NOT the cached one: `/skills route <text>` is how a user
166
+ // tests whether a phrasing would have matched.
167
+ ctx.notify('info', renderRoutingReport(service.route(task)));
168
+ }
169
+ /** One `info` notification, one line per candidate, ASCII only (`commands/`). */
170
+ function renderRoutingReport(report) {
171
+ if (report.candidates.length === 0) {
172
+ const close = report.topUnqualified === null
173
+ ? ''
174
+ : ` Closest was ${report.topUnqualified.name} (${report.topUnqualified.score}); ` +
175
+ `the threshold is ${SKILL_TRIAGE_LIMITS.minScore}.`;
176
+ return (`No skill matched "${report.task}". The catalog has ${report.eligible} eligible skills.` +
177
+ close);
178
+ }
179
+ const lines = report.candidates.map((candidate) => {
180
+ const reasons = candidate.reasons.length > 0 ? candidate.reasons.join('+') : 'none';
181
+ return `${candidate.name} (${candidate.scope}) score ${candidate.score} [${reasons}]`;
182
+ });
183
+ return [`Routed "${report.task}" (${report.tokenCount} token(s)):`, ...lines].join('\n');
184
+ }
185
+ // ---------------------------------------------------------------------------
101
186
  // /skills policy · unload · usage (§5.5 / §9)
102
187
  // ---------------------------------------------------------------------------
103
188
  /**
@@ -0,0 +1,25 @@
1
+ /**
2
+ * `/subagent` — force the next turn to fan out into a parallel team
3
+ * (subagent-fanout 3.5 / 5.2).
4
+ *
5
+ * ASCII ONLY: `src/commands/**` is inside `glyphs.test.ts::inScope`.
6
+ *
7
+ * WHAT THIS COMMAND IS AND IS NOT (D-8 / D-9 / D-10). It does not dispatch: it
8
+ * parses the user's intent, refuses honestly when forcing cannot work, prints
9
+ * ONE line saying what will happen next, and submits a normal user turn. The
10
+ * lead then calls `task` on the one dispatch path that already exists, so Esc,
11
+ * the watchdog pause, the report, spend accounting and the concurrency lock all
12
+ * keep working without a second implementation. The consequence the user must
13
+ * be told about is that forcing is INSTRUCTION-LEVEL: the notice says "the next
14
+ * turn dispatches them", never "N subagents started".
15
+ *
16
+ * THE GUARD ORDER IS A CONTRACT (R-P1-10 / R-P1-11): status -> capability ->
17
+ * parse. A `--no-team` session typing `/subagent -n x` is told the session has
18
+ * no team capability, because a syntax hint would send them to fix the one thing
19
+ * that is not wrong.
20
+ */
21
+ import type { CommandContext, CommandRegistry } from './registry.js';
22
+ /** The command body; `registerSubagentCommand` is the only registration site. */
23
+ export declare function runSubagentCommand(ctx: CommandContext): void;
24
+ /** Registered from `builtins.ts` with one line, next to every other subsystem. */
25
+ export declare function registerSubagentCommand(registry: CommandRegistry): void;