@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
@@ -0,0 +1,242 @@
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 { buildForcedDirective, parseSubagentArgs } from '../team/forced-dispatch.js';
22
+ import { subagentModelSummary } from '../ui/overlays/model-profile-state.js';
23
+ /**
24
+ * The default count when the user names none (D-11).
25
+ *
26
+ * FOUR, not `maxSubagents`: `<team_mode>` tells the model two to four is usually
27
+ * enough, so "force one dispatch" must not silently become the most expensive
28
+ * call the session can make. The status line advertises the real ceiling so the
29
+ * user who wants the widest fan-out knows which flag to add.
30
+ */
31
+ const DEFAULT_FORCED_COUNT = 4;
32
+ /** `off (--no-team)` / `off` / `on`: the three states the status line tells apart. */
33
+ function teamState(ctx) {
34
+ if (!ctx.controller.isTeamRegistered())
35
+ return 'off (--no-team)';
36
+ return ctx.controller.isTeamEnabled() ? 'on' : 'off';
37
+ }
38
+ /** The subagent model line, phrased by the same authority `/team status` uses. */
39
+ function modelLine(ctx) {
40
+ const config = ctx.controller.getConfig();
41
+ const team = ctx.controller.getTeamConfig();
42
+ const fast = config.fast;
43
+ return subagentModelSummary({
44
+ mode: team.model,
45
+ modelProfileId: team.modelProfileId,
46
+ mainConnection: `${config.provider}:${config.model}`,
47
+ fastConnection: fast.enabled && fast.model
48
+ ? `${fast.provider || config.provider}:${fast.model}` : null,
49
+ profiles: config.modelProfiles,
50
+ hasKey: (id) => ctx.controller.hasApiKey(id),
51
+ });
52
+ }
53
+ /**
54
+ * The no-argument answer: five lines, honest in every session state.
55
+ *
56
+ * IT RUNS BEFORE EVERY GUARD (R-P1-11). "How wide can I go?" is a question, not
57
+ * a request, and a question deserves an answer even in a `--no-team` session -
58
+ * which is exactly where the user is most likely to ask it.
59
+ */
60
+ function reportStatus(ctx) {
61
+ const team = ctx.controller.getTeamConfig();
62
+ const max = team.maxSubagents;
63
+ // The bracket's bounds are the LIVE width, never a literal: at capacity 2 this
64
+ // line has to read `<2-2>`, or the status line teaches a flag that arrives
65
+ // narrowed every time.
66
+ const low = Math.min(2, max);
67
+ const lines = [
68
+ `Forced fan-out: /subagent [-n <${low}-${max}>] [--parts a,b,c] [--read-only] <brief>`,
69
+ `Capacity: ${max} per dispatch, ${team.maxConcurrent} at once `
70
+ + '(team.maxSubagents / team.maxConcurrent)',
71
+ `Subagent model: ${modelLine(ctx)}`,
72
+ `Supervisor: ${team.overseer ? 'on' : 'off'}. Team mode: ${teamState(ctx)}.`,
73
+ ];
74
+ // Disclosed, never refused (R-P2-6): plan mode may remove `task` from this
75
+ // turn's tool array, and a user who is told so will not read the silence as a
76
+ // broken command.
77
+ if (ctx.controller.getAgentMode() === 'plan') {
78
+ lines.push('Plan mode is on: the task tool may be unavailable this turn.');
79
+ }
80
+ ctx.notify('info', lines.join('\n'));
81
+ }
82
+ /**
83
+ * Guard 5's sentence. The provider is named because "no API key" alone leaves
84
+ * the user guessing which of their configured providers is the empty one.
85
+ */
86
+ function reportMissingKey(ctx, team) {
87
+ const config = ctx.controller.getConfig();
88
+ const resolution = ctx.controller.subagentModelResolution();
89
+ const provider = resolution.effective === 'main'
90
+ ? config.provider : resolution.ref.providerId;
91
+ const note = team.model === 'fast' && !ctx.controller.hasApiKey(undefined, 'fast')
92
+ ? ' Fast tier has no key either; children will fail and the report will say so.'
93
+ : '';
94
+ ctx.notify('warn', `No API key for ${provider}. Set one with /settings or an environment variable.${note}`);
95
+ }
96
+ /** The five capability refusals, in the fixed order 3.5.3 records. */
97
+ function refused(ctx, team) {
98
+ if (!ctx.controller.isTeamRegistered()) {
99
+ ctx.notify('warn', 'Team mode is off for this session (started with --no-team). '
100
+ + 'Restart without --no-team to use /subagent.');
101
+ return true;
102
+ }
103
+ if (!ctx.controller.isTeamEnabled()) {
104
+ ctx.notify('warn', 'Team mode is off. Run /team on, then /subagent again.');
105
+ return true;
106
+ }
107
+ // The team lock and the lead's turn are separate refusals for one reason: both
108
+ // would collide with `task`'s own BUSY_REFUSAL, and the user would see a failed
109
+ // tool card instead of a sentence explaining why nothing ran.
110
+ if (ctx.controller.isTeamBusy()) {
111
+ ctx.notify('warn', 'A team dispatch is already running. '
112
+ + 'Wait for its report, or press Esc to stop it.');
113
+ return true;
114
+ }
115
+ if (ctx.controller.isRunning()) {
116
+ ctx.notify('warn', 'A turn is in progress. Press Esc to stop it, then run /subagent again.');
117
+ return true;
118
+ }
119
+ if (!ctx.controller.hasApiKey()) {
120
+ reportMissingKey(ctx, team);
121
+ return true;
122
+ }
123
+ return false;
124
+ }
125
+ /** The four parse failures, each with the one sentence that fixes it. */
126
+ function reportParseError(ctx, parsed) {
127
+ if (parsed.error === 'count') {
128
+ ctx.notify('warn', '/subagent: -n needs a number of 1 or more, e.g. -n 4.');
129
+ return;
130
+ }
131
+ if (parsed.error === 'conflict') {
132
+ ctx.notify('warn', '/subagent: use -n or --parts, not both.');
133
+ return;
134
+ }
135
+ if (parsed.error === 'flag') {
136
+ ctx.notify('warn', `/subagent: unknown option "${parsed.token ?? ''}". Try /subagent for usage.`);
137
+ return;
138
+ }
139
+ ctx.notify('warn', '/subagent <brief> - describe the work; add -n <count> to fix the fan-out.');
140
+ }
141
+ /**
142
+ * Narrow the parsed intent against the LIVE width.
143
+ *
144
+ * TWO CEILINGS, IN THIS ORDER (R-P1-3): the structural `forcedPartsMax` bounds
145
+ * what one argument may carry (the parser already applied it), and the policy
146
+ * `maxSubagents` bounds what this session may dispatch - which only this layer
147
+ * can apply, because only it can read the live config.
148
+ */
149
+ function planForced(args, team) {
150
+ const max = team.maxSubagents;
151
+ if (args.parts.length > 0) {
152
+ // A PARTS LIST STARTS WITH A CHILD PER SURVIVING PART, and `requested` is the
153
+ // number the user TYPED - not the structural 8 the parser already applied.
154
+ // Quoting the post-truncation count made the old notice tell a user who asked
155
+ // for eleven parts that they had asked for eight (R-4: the notice and the
156
+ // directive must both name the user's number).
157
+ const parts = args.parts.slice(0, max);
158
+ return {
159
+ brief: args.brief,
160
+ parts,
161
+ partsAdmitted: args.parts.length,
162
+ partsRequested: args.partsRequested,
163
+ requested: args.partsRequested,
164
+ count: parts.length,
165
+ readOnly: args.readOnly,
166
+ };
167
+ }
168
+ const requested = args.count ?? Math.min(DEFAULT_FORCED_COUNT, max);
169
+ return {
170
+ brief: args.brief,
171
+ parts: [],
172
+ partsAdmitted: 0,
173
+ partsRequested: 0,
174
+ requested,
175
+ count: Math.max(1, Math.min(requested, max)),
176
+ readOnly: args.readOnly,
177
+ };
178
+ }
179
+ /** The one line that says what the NEXT turn will do - a plan, not a result. */
180
+ function planNotice(planned, team) {
181
+ const noun = planned.count === 1 ? 'subagent' : 'subagents';
182
+ const head = `Forcing a fan-out of ${planned.count} ${noun}`;
183
+ const followUp = `${head} (max ${team.maxSubagents}) - the next turn dispatches them. `
184
+ + 'Esc stops the turn.';
185
+ // TWO CEILINGS, TWO SENTENCES. `forcedPartsMax` bounds what one argument may
186
+ // carry and `team.maxSubagents` bounds what may be dispatched; naming the wrong
187
+ // one sends the user to change a number that is not the reason.
188
+ if (planned.parts.length > 0) {
189
+ if (planned.parts.length < planned.partsAdmitted) {
190
+ return `${head} (asked for ${planned.partsRequested} parts; `
191
+ + `team.maxSubagents is ${team.maxSubagents}).`;
192
+ }
193
+ if (planned.partsAdmitted < planned.partsRequested) {
194
+ return `${head} (asked for ${planned.partsRequested} parts, `
195
+ + `the first ${planned.parts.length} are used).`;
196
+ }
197
+ return followUp;
198
+ }
199
+ if (planned.requested > planned.count) {
200
+ return `${head} (asked for ${planned.requested}; `
201
+ + `team.maxSubagents is ${team.maxSubagents}).`;
202
+ }
203
+ return followUp;
204
+ }
205
+ /** The command body; `registerSubagentCommand` is the only registration site. */
206
+ export function runSubagentCommand(ctx) {
207
+ if (ctx.args.trim().length === 0) {
208
+ reportStatus(ctx);
209
+ return;
210
+ }
211
+ const team = ctx.controller.getTeamConfig();
212
+ if (refused(ctx, team))
213
+ return;
214
+ const parsed = parseSubagentArgs(ctx.args);
215
+ if (!parsed.ok) {
216
+ reportParseError(ctx, parsed);
217
+ return;
218
+ }
219
+ const planned = planForced(parsed.args, team);
220
+ const directive = buildForcedDirective({
221
+ brief: planned.brief,
222
+ count: planned.count,
223
+ readOnly: planned.readOnly,
224
+ parts: planned.parts,
225
+ maxSubagents: team.maxSubagents,
226
+ clamped: planned.requested > planned.count,
227
+ requested: planned.requested,
228
+ planMode: ctx.controller.getAgentMode() === 'plan',
229
+ });
230
+ ctx.notify('info', planNotice(planned, team));
231
+ // `userInitiated: false`: a directive the user did not type belongs in neither
232
+ // the prompt history nor the hint-fade counter (todo-plan-followthrough C-12).
233
+ ctx.submit(directive, { userInitiated: false });
234
+ }
235
+ /** Registered from `builtins.ts` with one line, next to every other subsystem. */
236
+ export function registerSubagentCommand(registry) {
237
+ registry.register({
238
+ name: 'subagent',
239
+ description: 'Force the next turn to fan out into a parallel team',
240
+ run: (ctx) => { runSubagentCommand(ctx); },
241
+ });
242
+ }
@@ -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
+ };
@@ -16,6 +16,7 @@ import { parseThresholdInput } from '../config/schema.js';
16
16
  import { formatCost, formatTokens } from '../agent/usage.js';
17
17
  import { computeCost } from '../agent/usage.js';
18
18
  import { COMPACTION_LIMITS } from './limits.js';
19
+ import { compactionReasonLine } from './reason-text.js';
19
20
  import { findArchive, listArchives } from './archive.js';
20
21
  /**
21
22
  * `/compact status` — THE GUARANTEED REPORTING SURFACE (§6.5 / AC-17).
@@ -46,7 +47,7 @@ export function formatCompactionStatus(ctx) {
46
47
  return lines.join('\n');
47
48
  }
48
49
  const state = snapshot.selfDisabled
49
- ? `self-disabled (${snapshot.selfDisabledReason ?? 'no progress'})`
50
+ ? `self-disabled (${compactionReasonLine(snapshot.selfDisabledReason ?? 'no progress')})`
50
51
  : controller.isCompactionEnabled()
51
52
  ? snapshot.live
52
53
  ? 'on'
@@ -61,7 +62,13 @@ export function formatCompactionStatus(ctx) {
61
62
  `[${p.source === 'usage' ? 'measured' : 'estimated'}]`);
62
63
  lines.push(` Automatic trigger: occupancy >= ${(config.threshold * 100).toFixed(2)}%; ` +
63
64
  `manual: /compact (${Math.ceil(p.contextWindow * config.threshold)} tokens)`);
65
+ // TWO LINES, BECAUSE THE TAIL GUARANTEE IS NOW CONDITIONAL (never-fires B).
66
+ // `keepRecentTurns` stays the promise; the second line is what happens when a run
67
+ // has too few user turns for that promise to be satisfiable, and saying it here
68
+ // is what makes the card's `kept M of N` readable rather than surprising.
64
69
  lines.push(` Keeps: the original task + ${config.keepRecentTurns} recent turns, verbatim`);
70
+ lines.push(` Relaxes to: the last ${COMPACTION_LIMITS.minRetainedMessages} messages ` +
71
+ 'when a run has too few user turns for that');
65
72
  lines.push(` Summarizer: ${snapshot.model || '(none)'}` +
66
73
  (config.useFastTier ? ' (fast tier preferred)' : ' (fast tier not used)'));
67
74
  lines.push(' On summarize failure: ' + (config.onFailure === 'truncate'
@@ -79,8 +86,14 @@ export function formatCompactionStatus(ctx) {
79
86
  if (declined > 0) {
80
87
  sessionNotes.push(`${declined} ${declined === 1 ? 'checkpoint' : 'checkpoints'} declined`);
81
88
  }
82
- lines.push(` This session: ${snapshot.compactions} ` +
83
- `${snapshot.compactions === 1 ? 'compaction' : 'compactions'}` +
89
+ // ATTEMPTS, THEN APPLIED (never-fires C). `compactions` counts every card the
90
+ // user saw, which now includes the ones that declined, so reporting it alone
91
+ // would overstate how much the feature actually achieved.
92
+ const attempts = snapshot.applied === undefined
93
+ ? `${snapshot.compactions} ${snapshot.compactions === 1 ? 'compaction' : 'compactions'}`
94
+ : `${snapshot.compactions} ${snapshot.compactions === 1 ? 'attempt' : 'attempts'}` +
95
+ ` (${snapshot.applied} applied)`;
96
+ lines.push(` This session: ${attempts}` +
84
97
  (sessionNotes.length > 0 ? ` (${sessionNotes.join(', ')})` : '') +
85
98
  `, ${formatTokens(snapshot.tokensReclaimed)} tokens reclaimed`);
86
99
  const cost = snapshot.pricingUnknown
@@ -93,6 +93,7 @@ export declare class Compactor implements ContextManager {
93
93
  private compactionsThisRun;
94
94
  private lastCompactionTurn;
95
95
  private compactions;
96
+ private appliedCompactions;
96
97
  private declined;
97
98
  private tokensReclaimed;
98
99
  private sessionUsage;
@@ -132,8 +133,31 @@ export declare class Compactor implements ContextManager {
132
133
  private callOnce;
133
134
  private checkCandidate;
134
135
  private isCurrent;
136
+ /**
137
+ * Open the card for THIS attempt (C).
138
+ *
139
+ * CALLED FROM `compact()`'S ENTRY, so the two pre-model rejections are visible
140
+ * too. The old position - inside `callOnce` - made an attempt that never called
141
+ * a model indistinguishable from a checkpoint that never fired, which is exactly
142
+ * how this feature spent 96 attempts without a single user-visible symptom.
143
+ *
144
+ * `state.index !== undefined` keeps it idempotent: `callOnce` no longer calls it,
145
+ * but the guard stays so a future re-announce cannot open a second card for one
146
+ * attempt.
147
+ */
135
148
  private announce;
149
+ /** Publish the current phase, for the live card only. */
150
+ private phase;
136
151
  private keep;
152
+ /**
153
+ * The relaxation disclosure, as record fields.
154
+ *
155
+ * ONE PLACE BUILDS IT, so the card, the reducer and the diagnostic log cannot
156
+ * describe one relaxed cut three different ways. Absent entirely when the first
157
+ * planning pass found a cut, which is what keeps a normal compaction's record
158
+ * byte-identical to the round before this change.
159
+ */
160
+ private relaxedFields;
137
161
  private finishRecord;
138
162
  /** Commit only the engine/host verdict; candidates have no authority before this point. */
139
163
  settleOperation(verdict: {
@@ -152,6 +176,7 @@ export declare class Compactor implements ContextManager {
152
176
  sessionTotals(): {
153
177
  selfDisabledReason?: string | undefined;
154
178
  compactions: number;
179
+ applied: number;
155
180
  declined: number;
156
181
  tokensReclaimed: number;
157
182
  usage: TokenUsage;