@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
@@ -19,6 +19,8 @@ import type { CliConfig, CompactionConfig, FastConfig, RetryConfig, TeamConfig,
19
19
  import type { ModelRole } from '../config/model-profiles.js';
20
20
  import { ModelWindows } from '../config/model-windows.js';
21
21
  import { type ConfirmRequest } from '../tools/index.js';
22
+ import type { ToolGate } from '../tools/tool-gate.js';
23
+ import type { ToolHookRunner } from '../exec/hooks/tool-wrapper.js';
22
24
  import type { ToolPermission } from '../exec/permission.js';
23
25
  import { type ToolOutputListener } from '../tools/tool-output-store.js';
24
26
  import type { FilePatch } from '../tools/patch.js';
@@ -28,6 +30,7 @@ import { ProcSupervisor } from '../proc/supervisor.js';
28
30
  import type { ProcEventListener, ServiceSnapshot } from '../proc/types.js';
29
31
  import { type FastStatus } from '../fast/wiring.js';
30
32
  import type { FastEventListener, FastSnapshot } from '../fast/types.js';
33
+ import type { TaskSummaryOutcome } from '../fast/task-summary-types.js';
31
34
  import { ContextMeter, type ContextUsageListener } from '../compaction/meter.js';
32
35
  import type { CompactionEventListener, CompactionSnapshot, ContextUsageSnapshot } from '../compaction/types.js';
33
36
  import type { AcceptanceItem, TodoEventListener, TodoSnapshot } from '../todo/types.js';
@@ -100,6 +103,36 @@ export interface ControllerDeps {
100
103
  * `tools/index.ts`.
101
104
  */
102
105
  permission?: ToolPermission;
106
+ /**
107
+ * The caller's per-call approval gate (headless-host-bridge §2.3.3).
108
+ *
109
+ * FORWARDED INTO `createBuiltinTools` AND NOWHERE ELSE. Unlike `permission` it
110
+ * does NOT reach subagents in this round: every child builds its own tool array
111
+ * through `team/runtime.ts`, and a fan-out that asked "may I run bash?" once per
112
+ * child for one decision is an approval waterfall, not a control. Absent on
113
+ * every existing path, which is what keeps the tool array identical there.
114
+ */
115
+ toolGate?: ToolGate;
116
+ /**
117
+ * How long a decision may wait, added to the tool's own budget.
118
+ *
119
+ * SEE THE TWO-PHASE FORMULA AT `toolTimeoutOverrides`. The executor arms its
120
+ * timer BEFORE the approval wrapper runs, so a ceiling of `toolTimeoutMs` alone
121
+ * would give the tool less than its configured time by exactly however long the
122
+ * caller took to answer.
123
+ */
124
+ decisionBudgetMs?: number;
125
+ /**
126
+ * The gate each SUBAGENT gets, keyed by its label (headless-host-bridge
127
+ * section 2.3.7).
128
+ *
129
+ * A SEPARATE FIELD BECAUSE THE SCOPE DIFFERS, NOT BECAUSE THE GATE DOES: every
130
+ * child reports `{id, label, depth: 1}` so the caller can render "subagent A
131
+ * wants to run bash", while the lead reports `depth: 0`. One shared instance
132
+ * would attribute every child's request to the lead. Absent outside bridge mode,
133
+ * which keeps a child's tool array identical today.
134
+ */
135
+ toolGateForChild?: (label: string) => ToolGate;
103
136
  /**
104
137
  * `--append-system-prompt` text, appended verbatim to the composed prompt
105
138
  * (§4.1). Threaded through `composeSystemPrompt` so it survives every rebuild
@@ -107,6 +140,41 @@ export interface ControllerDeps {
107
140
  * once at construction and lost at the first `rebuildSystemPrompt()`.
108
141
  */
109
142
  appendSystemPrompt?: string;
143
+ /**
144
+ * `submit_result`, registered with the lead's toolset when the caller supplied
145
+ * `--result-schema` (headless-request-scope-and-typed-result section 2.3.9).
146
+ *
147
+ * FORWARDED INTO `createBuiltinTools` AND NOWHERE ELSE. It must NOT reach a
148
+ * subagent: a child's product is a report that goes back through `task`, and a
149
+ * second submitter inside one request would compete with the lead's schema for the
150
+ * one accepted value. The child's `createBuiltinTools` call site is
151
+ * `team/subagent.ts`, and it deliberately passes nothing here.
152
+ *
153
+ * Absent on every existing path (the TUI, `-p`, every subcommand), which is what
154
+ * keeps their tool arrays byte-identical (I-S2).
155
+ */
156
+ resultTool?: AgentTool;
157
+ /**
158
+ * The rendered `<structured_result>` block, or absent.
159
+ *
160
+ * SEE THE FIELD'S OWN COMMENT for why this is threaded through the controller
161
+ * rather than spliced once: the prompt is rebuilt, and a block that vanished on a
162
+ * rebuild would leave the model unaware of the contract it is being graded against.
163
+ */
164
+ structuredResultBlock?: string;
165
+ /**
166
+ * The caller's lifecycle hooks (headless-threads-and-lifecycle-hooks section 2.4.9).
167
+ *
168
+ * FORWARDED INTO `createBuiltinTools` AND NOWHERE ELSE, in the same shape `resultTool`
169
+ * is: absent on every existing path (the TUI, `-p`, every subcommand and every
170
+ * subagent), which is what keeps their tool arrays byte-identical.
171
+ *
172
+ * THE CHILD'S CALL SITE IS DELIBERATELY SILENT ABOUT IT. Hooks see the LEAD's tool calls;
173
+ * a subagent builds its own array in `team/subagent.ts` and a hook installed for tenant
174
+ * isolation on the lead must not silently become a hook on every child's identical
175
+ * `read_file` - that is a different security decision with its own design (section 9.5).
176
+ */
177
+ hookRunner?: ToolHookRunner;
110
178
  /**
111
179
  * The per-model window table from ~/.aragon-agent/model-windows.json.
112
180
  * Injectable so tests can hand the controller a table without writing into
@@ -131,6 +199,17 @@ export interface PreflightResult {
131
199
  /** 'config' for a missing/invalid key or unusable provider. */
132
200
  kind?: 'config';
133
201
  }
202
+ /** Inputs for one task-label attempt (`AgentController.summarizeTask`). */
203
+ export interface SummarizeTaskOptions {
204
+ /**
205
+ * Cancels the call. The CALLER also owns the epoch guard
206
+ * (tui-shortcut-drawer-and-task-summary D-10): aborting frees the connection,
207
+ * the epoch decides correctness, and neither substitutes for the other.
208
+ */
209
+ signal?: AbortSignal;
210
+ /** The glyph a cut label ends with. ASCII by default so a headless caller is safe. */
211
+ ellipsis?: string;
212
+ }
134
213
  export declare class AgentController {
135
214
  private config;
136
215
  private readonly agent;
@@ -143,6 +222,16 @@ export declare class AgentController {
143
222
  private modelMetadataAbort;
144
223
  private disposed;
145
224
  private readonly tools;
225
+ /**
226
+ * Tools the CALLER serves, replaced on every `tool_register` (I-13).
227
+ *
228
+ * A SECOND ARRAY, NOT ONE GROWN ONE. `this.tools` is read by `listTools()`,
229
+ * `toolNames()`, `composeSystemPrompt()` and three "fixed at construction" flags
230
+ * (bash_output/bash_kill, task, todo_write). Growing it would silently change
231
+ * what those flags mean; keeping the caller's tools beside it gives one honest
232
+ * rule - the REGISTRY is the union, `this.tools` stays the host's own set.
233
+ */
234
+ private callerTools;
146
235
  private readonly skills;
147
236
  private readonly skillsEnabled;
148
237
  private onSkillsChanged?;
@@ -157,6 +246,17 @@ export declare class AgentController {
157
246
  * Empty on every path but `aragon exec`.
158
247
  */
159
248
  private readonly appendSystemPrompt;
249
+ /**
250
+ * The `<structured_result>` block (headless-request-scope-and-typed-result
251
+ * section 2.3.8), or `''`.
252
+ *
253
+ * A FIELD FOR THE SAME REASON `appendSystemPrompt` IS ONE: `composeSystemPrompt()`
254
+ * runs again on every rebuild, so a value spliced once at construction would vanish
255
+ * at the first `/cwd` change, skills rescan or mode flip - and the model would then
256
+ * be asked to submit a result it had never been told about. Empty on every path but
257
+ * `aragon exec --result-schema`.
258
+ */
259
+ private readonly structuredResultBlock;
160
260
  private effectiveMode;
161
261
  private pendingMode;
162
262
  private askRounds;
@@ -870,6 +970,24 @@ export declare class AgentController {
870
970
  * consumed by `App` too, and this round promised to be purely additive.
871
971
  */
872
972
  fastSnapshot(): FastSnapshot | null;
973
+ /**
974
+ * Summarise one user prompt into a header label, using the FAST tier only.
975
+ *
976
+ * NEVER REJECTS - the same contract `prompt()` has, and the reason `App` needs
977
+ * no `.catch`. Every failure is folded into a discriminated
978
+ * `TaskSummaryOutcome`, because a header label must not be able to produce an
979
+ * unhandled rejection in a TUI that has nowhere to show one.
980
+ *
981
+ * THE CONTROLLER OWNS THE `AbortController` AND ABORTS IT UNCONDITIONALLY on the
982
+ * way out: `runTaskSummaryCall` may return a `timeout` outcome while the request
983
+ * is still open (it is forbidden to abort its own call - see that file's
984
+ * header), so this `finally` is what actually releases the connection.
985
+ *
986
+ * EVERY `this.fast` READ IS BEHIND THE CLOSURE, exactly as the reviewer and the
987
+ * overseer wiring do it: `complete` is only reached from inside the call, which
988
+ * runs long after construction, so there is no use-before-assign.
989
+ */
990
+ summarizeTask(anchor: string, opts?: SummarizeTaskOptions): Promise<TaskSummaryOutcome>;
873
991
  /**
874
992
  * Subscribe to occupancy publications. Returns an unsubscribe function.
875
993
  *
@@ -970,6 +1088,29 @@ export declare class AgentController {
970
1088
  * `subscribeTodos` and `subscribeFast` already set.
971
1089
  */
972
1090
  subscribeCompaction(listener: CompactionEventListener): () => void;
973
- /** List usable tools (name + description) for the /tools command. */
1091
+ /**
1092
+ * List usable tools (name + description) for the /tools command.
1093
+ *
1094
+ * THE UNION, SO THE VIEW AND THE REGISTRY CANNOT DISAGREE (I-13). `system/init`
1095
+ * builds `init.tools` from here, as do `/tools` and `/skills`; reporting only the
1096
+ * host's own set while the model is calling `db_query` is the exact failure that
1097
+ * invariant names - a caller reading "the model has no such tool" about a tool it
1098
+ * is watching run.
1099
+ */
974
1100
  listTools(): AgentTool[];
1101
+ /**
1102
+ * Replace the caller-served tool set. Called ONLY at a turn boundary.
1103
+ *
1104
+ * `Agent.setTools()` unregisters and re-registers entries in the LIVE registry.
1105
+ * This class already learned the lesson once: `submit_plan` mutates the mode from
1106
+ * INSIDE a tool execution, so rebuilding the array at that moment would mutate
1107
+ * the registry mid-iteration (see the comment on the plan tools). Hence the
1108
+ * refusal rather than a silently deferred write: a caller that registers
1109
+ * mid-turn gets an error it can act on, and the bridge defers the frame instead.
1110
+ *
1111
+ * `[]` is meaningful and not an omission: it means "restore the factory set".
1112
+ */
1113
+ applyCallerTools(tools: AgentTool[]): void;
1114
+ /** The names the caller currently serves, for `ack.tools`. */
1115
+ callerToolNames(): string[];
975
1116
  }
@@ -11,7 +11,7 @@ import { resolveModelProfileState } from '../config/model-profile-resolution.js'
11
11
  import { makeGetApiKey } from '../config/load.js';
12
12
  import { updatePersistedConfig } from '../config/store.js';
13
13
  import { ModelWindows } from '../config/model-windows.js';
14
- import { createBuiltinTools } from '../tools/index.js';
14
+ import { createBuiltinTools, gatedToolTimeouts, } from '../tools/index.js';
15
15
  import { createFileChangeStore } from '../tools/file-change-store.js';
16
16
  import { createToolOutputStore, } from '../tools/tool-output-store.js';
17
17
  import { TeamRuntime } from '../team/runtime.js';
@@ -34,6 +34,11 @@ import { getHomeRoot } from '../config/app-paths.js';
34
34
  import { addSignalHook } from '../logging/install.js';
35
35
  import { describeFastTierProblem, fastProviderOf, resolveFastTier } from '../fast/resolve.js';
36
36
  import { FastWiring, offFastStatus } from '../fast/wiring.js';
37
+ // ONE-SHOT FAST CALLS (tui-shortcut-drawer-and-task-summary §6.1). The TYPES come
38
+ // from `fast/task-summary-types.ts` and only `fast/` produces them, so this module
39
+ // never learns how a label is made - it owns the controller and the epoch, which
40
+ // is the pair `runTaskSummaryCall` is forbidden to own (see its header).
41
+ import { createTaskSummaryDeps, runTaskSummaryCall } from '../fast/summary-call.js';
37
42
  import { CompactionWiring, offCompactionSnapshot } from '../compaction/wiring.js';
38
43
  import { ContextMeter } from '../compaction/meter.js';
39
44
  import { TodoStore } from '../todo/store.js';
@@ -74,6 +79,16 @@ export class AgentController {
74
79
  modelMetadataAbort = new AbortController();
75
80
  disposed = false;
76
81
  tools;
82
+ /**
83
+ * Tools the CALLER serves, replaced on every `tool_register` (I-13).
84
+ *
85
+ * A SECOND ARRAY, NOT ONE GROWN ONE. `this.tools` is read by `listTools()`,
86
+ * `toolNames()`, `composeSystemPrompt()` and three "fixed at construction" flags
87
+ * (bash_output/bash_kill, task, todo_write). Growing it would silently change
88
+ * what those flags mean; keeping the caller's tools beside it gives one honest
89
+ * rule - the REGISTRY is the union, `this.tools` stays the host's own set.
90
+ */
91
+ callerTools = [];
77
92
  skills;
78
93
  skillsEnabled;
79
94
  onSkillsChanged;
@@ -88,6 +103,17 @@ export class AgentController {
88
103
  * Empty on every path but `aragon exec`.
89
104
  */
90
105
  appendSystemPrompt;
106
+ /**
107
+ * The `<structured_result>` block (headless-request-scope-and-typed-result
108
+ * section 2.3.8), or `''`.
109
+ *
110
+ * A FIELD FOR THE SAME REASON `appendSystemPrompt` IS ONE: `composeSystemPrompt()`
111
+ * runs again on every rebuild, so a value spliced once at construction would vanish
112
+ * at the first `/cwd` change, skills rescan or mode flip - and the model would then
113
+ * be asked to submit a result it had never been told about. Empty on every path but
114
+ * `aragon exec --result-schema`.
115
+ */
116
+ structuredResultBlock;
91
117
  // -----------------------------------------------------------------------
92
118
  // Plan mode (§3.1) — THE CONTROLLER OWNS THE MODE.
93
119
  //
@@ -308,6 +334,10 @@ export class AgentController {
308
334
  this.config = config;
309
335
  this.cwd = config.cwd;
310
336
  this.appendSystemPrompt = deps.appendSystemPrompt ?? '';
337
+ // ABOVE `createBuiltinTools` FOR THE REASON THE PROMPT FIELD IS: the tool has to
338
+ // exist before the array is built, and the block has to exist before the prompt is
339
+ // composed - and both are read again on every rebuild.
340
+ this.structuredResultBlock = deps.structuredResultBlock ?? '';
311
341
  const humanInput = deps.humanInput ?? DENY_ALL_HUMAN_INPUT;
312
342
  this.planInteractive = humanInput.neverPrompts !== true;
313
343
  this.effectiveMode = config.startInPlanMode ? 'plan' : 'build';
@@ -431,6 +461,14 @@ export class AgentController {
431
461
  // mid-dispatch and abort it at `idleTimeoutMs`. The queue pauses the
432
462
  // CHILD's watchdog instead, which is the one that needs it (P0-4).
433
463
  ...(confirm ? { humanQueue: new TeamHumanQueue(confirm) } : {}),
464
+ // SITE ONE OF TWO FOR THE CHILD GATE (headless-host-bridge 2.3.7). The
465
+ // second is `runtime.ts::subDeps`; declaring it here and forgetting that
466
+ // one type-checks and forwards nothing, and every child would then run
467
+ // unapproved while the caller believes it is approving.
468
+ ...(deps.toolGateForChild ? { toolGateFor: deps.toolGateForChild } : {}),
469
+ ...(deps.decisionBudgetMs !== undefined
470
+ ? { decisionBudgetMs: deps.decisionBudgetMs }
471
+ : {}),
434
472
  // CHILDREN INHERIT THE POLICY (§4.2 invariant 3 / R-4 / AC-12).
435
473
  // Without this line `--deny-tool bash` is bypassed by one `task` call
436
474
  // and nothing anywhere reports it. Spread conditionally so a session
@@ -599,6 +637,15 @@ export class AgentController {
599
637
  }),
600
638
  ]
601
639
  : [],
640
+ // SPREAD, NEVER `resultTool: deps.resultTool` (the D-12 lesson, and the shape
641
+ // every optional dep in this bag follows). With no schema the options bag has no
642
+ // such key, so `createBuiltinTools` adds nothing and the tool array is the one it
643
+ // builds today - which is what makes I-S2 provable by object identity.
644
+ ...(deps.resultTool ? { resultTool: deps.resultTool } : {}),
645
+ // THE HOOK PAIR, SPREAD FOR THE SAME REASON AND AT THE SAME SITE (D-12): a dep that
646
+ // is declared but not forwarded is dropped in silence, and the only symptom is that
647
+ // the caller's hooks never fire.
648
+ ...(deps.hookRunner ? { hookRunner: deps.hookRunner } : {}),
602
649
  // The PULL port. Absent when jobs are off, which leaves every tool object
603
650
  // untouched and provable by identity.
604
651
  ...(this.jobsRegistered
@@ -663,6 +710,21 @@ export class AgentController {
663
710
  // today — which is what makes "the TUI and `-p` are unchanged" provable by
664
711
  // identity rather than by inspection.
665
712
  ...(deps.permission ? { permission: deps.permission } : {}),
713
+ // THE CALLER'S APPROVAL GATE, spread the same way and for the same reason
714
+ // (headless-host-bridge section 2.3.3): with no gate the options bag has no
715
+ // such key, so `createBuiltinTools` never enters its mapping branch and the
716
+ // tool OBJECTS are the ones it builds today.
717
+ //
718
+ // THE PAUSE IS `withHumanWait`, NOT A SECOND WRAPPER. `withPausedWatchdog` is
719
+ // documented as not reentrant, and this is the same function the plan tools
720
+ // already receive - so an approval wait and a question wait can never nest
721
+ // two pauses and resume the watchdog one level early.
722
+ ...(deps.toolGate
723
+ ? {
724
+ toolGate: deps.toolGate,
725
+ withToolWait: (fn) => this.withHumanWait(fn),
726
+ }
727
+ : {}),
666
728
  // Omitted entirely when skills are off, which is what leaves the seven
667
729
  // built-ins unwrapped and AC-G17 provable by object identity.
668
730
  ...(this.skillsEnabled
@@ -747,6 +809,22 @@ export class AgentController {
747
809
  toolTimeoutOverrides: {
748
810
  ask_user: config.planModeHumanTimeoutMs,
749
811
  submit_plan: config.planModeHumanTimeoutMs,
812
+ // THE TWO PHASES, ADDED (headless-host-bridge §2.3.4). The executor
813
+ // arms its timer BEFORE the approval wrapper runs, so a ceiling of
814
+ // `toolTimeoutMs` alone would leave the tool less than its configured
815
+ // budget by exactly the time a human took to answer. ADMITTED
816
+ // OVER-GRANT: a caller that answers instantly still gets the sum. The
817
+ // opposite trade - charging the wait to the tool - would make "the
818
+ // human thought for two minutes" read as "this command is too slow",
819
+ // and a tool timeout is supposed to mean the latter.
820
+ //
821
+ // THE KEY SET IS `CALLER_GATED_TOOLS`, the same constant
822
+ // `ToolGate.applies` is built from (I-6): two lists here would drift,
823
+ // and the drift would be a gated tool with no wait budget - a
824
+ // five-minute approval that kills its own tool at 180 s.
825
+ ...(deps.decisionBudgetMs !== undefined
826
+ ? gatedToolTimeouts(config.toolTimeoutMs, deps.decisionBudgetMs)
827
+ : {}),
750
828
  // A fan-out is not a hung tool either. Like the two above, this
751
829
  // number only bites BECAUSE `task` races `ctx.signal` — the
752
830
  // executor's timeout is cooperative, so an override is inert for any
@@ -946,6 +1024,11 @@ export class AgentController {
946
1024
  // tools that make its promises achievable are registered. With the flag off
947
1025
  // the prompt is byte-identical to the pre-feature text (I-3).
948
1026
  jobsBlock: this.jobsRegistered ? buildBackgroundJobsBlock() : '',
1027
+ // FROM THE FIELD, so the block survives every rebuild for the reason the field's
1028
+ // own comment records. Empty everywhere but `aragon exec --result-schema`, and
1029
+ // `buildSystemPrompt` splices it conditionally - so every existing prompt is
1030
+ // byte-identical (I-S1).
1031
+ structuredResultBlock: this.structuredResultBlock,
949
1032
  // Read from the FIELD on every rebuild, for the reason the field's own
950
1033
  // comment gives. Empty everywhere but `aragon exec`, and `buildSystemPrompt`
951
1034
  // splices it conditionally, so every existing prompt is byte-identical.
@@ -2273,6 +2356,60 @@ export class AgentController {
2273
2356
  fastSnapshot() {
2274
2357
  return this.fast?.snapshot() ?? null;
2275
2358
  }
2359
+ /**
2360
+ * Summarise one user prompt into a header label, using the FAST tier only.
2361
+ *
2362
+ * NEVER REJECTS - the same contract `prompt()` has, and the reason `App` needs
2363
+ * no `.catch`. Every failure is folded into a discriminated
2364
+ * `TaskSummaryOutcome`, because a header label must not be able to produce an
2365
+ * unhandled rejection in a TUI that has nowhere to show one.
2366
+ *
2367
+ * THE CONTROLLER OWNS THE `AbortController` AND ABORTS IT UNCONDITIONALLY on the
2368
+ * way out: `runTaskSummaryCall` may return a `timeout` outcome while the request
2369
+ * is still open (it is forbidden to abort its own call - see that file's
2370
+ * header), so this `finally` is what actually releases the connection.
2371
+ *
2372
+ * EVERY `this.fast` READ IS BEHIND THE CLOSURE, exactly as the reviewer and the
2373
+ * overseer wiring do it: `complete` is only reached from inside the call, which
2374
+ * runs long after construction, so there is no use-before-assign.
2375
+ */
2376
+ async summarizeTask(anchor, opts = {}) {
2377
+ const deps = createTaskSummaryDeps({
2378
+ getConfig: () => this.config,
2379
+ hasKey: (id, role) => this.hasApiKey(id, role),
2380
+ getApiKey: (id, role) => this.resolveKey(id, role),
2381
+ complete: (providerId, request) => {
2382
+ if (this.fast === null) {
2383
+ // A session whose tier never registered has no fast transport at all;
2384
+ // rejecting here is caught by `runTaskSummaryCall` and reported as a
2385
+ // tier reason rather than as a crash.
2386
+ return Promise.reject(new Error('Fast tier is not registered for this session.'));
2387
+ }
2388
+ return this.fast.completeViaFastRegistry(providerId, request);
2389
+ },
2390
+ ...(opts.ellipsis ? { ellipsis: () => opts.ellipsis } : {}),
2391
+ });
2392
+ const controller = new AbortController();
2393
+ const upstream = opts.signal;
2394
+ const onAbort = () => controller.abort();
2395
+ if (upstream) {
2396
+ if (upstream.aborted)
2397
+ controller.abort();
2398
+ else
2399
+ upstream.addEventListener('abort', onAbort);
2400
+ }
2401
+ try {
2402
+ return await runTaskSummaryCall(deps, {
2403
+ anchor,
2404
+ signal: controller.signal,
2405
+ now: Date.now(),
2406
+ });
2407
+ }
2408
+ finally {
2409
+ upstream?.removeEventListener('abort', onAbort);
2410
+ controller.abort();
2411
+ }
2412
+ }
2276
2413
  // -----------------------------------------------------------------------
2277
2414
  // Context occupancy (context-usage-gauge-accuracy §4.2) — THREE FORWARDERS
2278
2415
  //
@@ -2485,8 +2622,39 @@ export class AgentController {
2485
2622
  return () => { };
2486
2623
  return this.compaction.subscribe(listener);
2487
2624
  }
2488
- /** List usable tools (name + description) for the /tools command. */
2625
+ /**
2626
+ * List usable tools (name + description) for the /tools command.
2627
+ *
2628
+ * THE UNION, SO THE VIEW AND THE REGISTRY CANNOT DISAGREE (I-13). `system/init`
2629
+ * builds `init.tools` from here, as do `/tools` and `/skills`; reporting only the
2630
+ * host's own set while the model is calling `db_query` is the exact failure that
2631
+ * invariant names - a caller reading "the model has no such tool" about a tool it
2632
+ * is watching run.
2633
+ */
2489
2634
  listTools() {
2490
- return this.tools;
2635
+ return this.callerTools.length === 0 ? this.tools : [...this.tools, ...this.callerTools];
2636
+ }
2637
+ /**
2638
+ * Replace the caller-served tool set. Called ONLY at a turn boundary.
2639
+ *
2640
+ * `Agent.setTools()` unregisters and re-registers entries in the LIVE registry.
2641
+ * This class already learned the lesson once: `submit_plan` mutates the mode from
2642
+ * INSIDE a tool execution, so rebuilding the array at that moment would mutate
2643
+ * the registry mid-iteration (see the comment on the plan tools). Hence the
2644
+ * refusal rather than a silently deferred write: a caller that registers
2645
+ * mid-turn gets an error it can act on, and the bridge defers the frame instead.
2646
+ *
2647
+ * `[]` is meaningful and not an omission: it means "restore the factory set".
2648
+ */
2649
+ applyCallerTools(tools) {
2650
+ if (this.agent.state.isRunning) {
2651
+ throw new Error('applyCallerTools() called mid-turn; defer to the turn boundary.');
2652
+ }
2653
+ this.callerTools = [...tools];
2654
+ this.agent.setTools([...this.tools, ...this.callerTools]);
2655
+ }
2656
+ /** The names the caller currently serves, for `ack.tools`. */
2657
+ callerToolNames() {
2658
+ return this.callerTools.map((tool) => tool.name);
2491
2659
  }
2492
2660
  }
@@ -20,9 +20,17 @@ import type { AcceptanceSnapshot, TodoItem, TodoSnapshot } from '../todo/types.j
20
20
  import type { JobKind, ServiceSnapshot, ServiceStatus } from '../proc/types.js';
21
21
  import type { RetryPhase } from './retry-view.js';
22
22
  import type { FilePatch } from '../tools/patch.js';
23
+ import type { TaskSummaryView } from '../fast/task-summary-types.js';
23
24
  export type NoticeLevel = 'info' | 'warn' | 'error';
24
25
  export type ToastLevel = NoticeLevel | 'success';
25
- export type Overlay = null | 'settings' | 'model' | 'help' | 'queue' | 'confirm' | 'question' | 'plan' | 'kit' | 'prompts' | 'prompt-pick' | 'agents';
26
+ export type Overlay = null | 'settings' | 'model' | 'help' | 'queue' | 'confirm' | 'question' | 'plan' | 'kit' | 'prompts' | 'prompt-pick' | 'agents'
27
+ /**
28
+ * tui-shortcut-drawer-and-task-summary §4.5: the keyboard drawer (`Ctrl+G` /
29
+ * `/keys`). A single field, so the drawer and `/help` cannot both hold the
30
+ * keyboard - which is also why `Ctrl+G` TAKES OVER from another overlay instead
31
+ * of being swallowed while one is open (P2-3).
32
+ */
33
+ | 'shortcuts';
26
34
  export type ToolStatus = 'pending' | 'running' | 'done' | 'error';
27
35
  /** Ephemeral, auto-dismissed acknowledgement (spec §3.9). */
28
36
  export interface Toast {
@@ -391,6 +399,23 @@ export interface ViewState {
391
399
  * totals. `null` is what leaves an ordinary session's status bar unchanged.
392
400
  */
393
401
  compaction: CompactionSnapshot | null;
402
+ /**
403
+ * The session's task label, or `null` while there is none
404
+ * (tui-shortcut-drawer-and-task-summary D-13).
405
+ *
406
+ * PURE VIEW STATE: never persisted, never in the conversation history, never in
407
+ * `config.json`. It lives here because the header and the drawer must read the
408
+ * SAME field - two copies would be two labels for one task.
409
+ *
410
+ * THE TWO WRITERS ARE `taskSummaryPending` / `taskSummaryReady`, and the two
411
+ * CLEARERS are `resetConversation` and `restoreEntries`: clearing is the
412
+ * reducer's job (it is the only place that knows a task ended), while
413
+ * REBUILDING after `/resume` is the App wrapper's job, because only it can see
414
+ * the restored entries before they are reduced.
415
+ */
416
+ taskSummary: TaskSummaryView | null;
417
+ /** A summary call is in flight. Drives the drawer's `summarizing` line and `/task`. */
418
+ taskSummaryPending: boolean;
394
419
  seq: number;
395
420
  /** The `kind: 'team'` entry the live dispatch is writing into. */
396
421
  teamEntryId?: string;
@@ -562,6 +587,21 @@ export type ViewAction = {
562
587
  } | {
563
588
  type: 'restoreEntries';
564
589
  entries: Entry[];
590
+ }
591
+ /**
592
+ * The task label lifecycle (tui-shortcut-drawer-and-task-summary §7.1).
593
+ *
594
+ * TWO ACTIONS, because "a call started" and "a call finished" are different
595
+ * facts and the drawer reports them differently (`summarizing` vs the label).
596
+ * `taskSummaryReady` carries `null` in exactly one case: the call failed AND
597
+ * `taskSummary.fallback` is off, which is a deliberate "show nothing" rather
598
+ * than an empty string that would read as a broken feature.
599
+ */
600
+ | {
601
+ type: 'taskSummaryPending';
602
+ } | {
603
+ type: 'taskSummaryReady';
604
+ view: TaskSummaryView | null;
565
605
  } | {
566
606
  type: 'setAgentMode';
567
607
  mode: AgentMode;
@@ -67,6 +67,8 @@ export function initialViewState(seed = {}) {
67
67
  retry: null,
68
68
  fast: null,
69
69
  compaction: null,
70
+ taskSummary: null,
71
+ taskSummaryPending: false,
70
72
  seq: 0,
71
73
  teamEntryId: undefined,
72
74
  todoEntryId: undefined,
@@ -906,9 +908,25 @@ export function viewReducer(state, action) {
906
908
  turnProduced: false,
907
909
  errorNoticed: false,
908
910
  aborted: false,
911
+ // THE TASK LABEL IS PART OF THE TASK (D-13). `/clear`, `/reset` and a
912
+ // session switch all land here, so the header loses its label without the
913
+ // App needing a second cleanup path. `/clear screen` does NOT come here -
914
+ // it keeps the conversation, so it keeps the label.
915
+ taskSummary: null,
916
+ taskSummaryPending: false,
909
917
  };
910
918
  case 'setOverlay':
911
919
  return { ...state, overlay: action.overlay };
920
+ // --- Task summary (tui-shortcut-drawer-and-task-summary §7.1) ----------
921
+ //
922
+ // TWO FIELDS, ONE DIRECTION: `pending` is a latch the App raises before the
923
+ // call and releases on the result, and `taskSummary` is only ever replaced
924
+ // wholesale. Neither is persisted and neither is derived from `entries`, so a
925
+ // resumed conversation shows nothing until the App rebuilds the label.
926
+ case 'taskSummaryPending':
927
+ return { ...state, taskSummaryPending: true };
928
+ case 'taskSummaryReady':
929
+ return { ...state, taskSummary: action.view, taskSummaryPending: false };
912
930
  // Pure mirror write. The App only ever dispatches what
913
931
  // `controller.setAgentMode()` reported it ADOPTED, so this reducer has no
914
932
  // deferral logic of its own to get out of step with (§3.1 / R-P7).
@@ -1423,6 +1441,13 @@ export function viewReducer(state, action) {
1423
1441
  // NOT cleared — it is the session's own tier projection, which a
1424
1442
  // `/resume` does not change.
1425
1443
  fastEntryId: undefined,
1444
+ // `/resume` ADOPTS ANOTHER CONVERSATION, so the previous conversation's
1445
+ // label describes something that is no longer on screen. The reducer only
1446
+ // CLEARS it; the App's `makeCtx().dispatch` wrapper rebuilds one from
1447
+ // `action.entries` (D-13), because it is the only layer that sees the
1448
+ // restored entries BEFORE this branch runs.
1449
+ taskSummary: null,
1450
+ taskSummaryPending: false,
1426
1451
  };
1427
1452
  }
1428
1453
  // --- Background services (§3.10 / D-11) -----------------------------
@@ -114,6 +114,20 @@ export interface SystemPromptParams {
114
114
  * the child does not have.
115
115
  */
116
116
  jobsBlock?: string;
117
+ /**
118
+ * The rendered `<structured_result>` block, or `''`
119
+ * (headless-request-scope-and-typed-result section 2.3.8).
120
+ *
121
+ * Spliced conditionally exactly as every block above is, which is what keeps I-S1
122
+ * true: a run without `--result-schema` produces the pre-feature prompt BYTE FOR
123
+ * BYTE for a fixed tool array.
124
+ *
125
+ * POSITION IS THE SEMANTICS. It goes after every subsystem block and before
126
+ * `## Additional instructions`: it describes the SHAPE OF THE ANSWER, which is
127
+ * closer to the end of the work than any "how to behave" block, while the caller's
128
+ * own appended instructions still come last and still win.
129
+ */
130
+ structuredResultBlock?: string;
117
131
  }
118
132
  /** Describe the active shell so the model does not emit incompatible syntax. */
119
133
  export declare function describeShell(): {
@@ -44,6 +44,7 @@ export function buildSystemPrompt(params) {
44
44
  const fastBlock = params.fastBlock ?? '';
45
45
  const backgroundBlock = params.backgroundBlock ?? '';
46
46
  const jobsBlock = params.jobsBlock ?? '';
47
+ const structuredResultBlock = params.structuredResultBlock ?? '';
47
48
  const appended = (params.appendSystemPrompt ?? '').trim();
48
49
  return [
49
50
  'You are AragonAgent, an autonomous coding assistant operating inside a terminal (TUI).',
@@ -67,6 +68,10 @@ export function buildSystemPrompt(params) {
67
68
  ...(fastBlock ? ['', fastBlock] : []),
68
69
  ...(backgroundBlock ? ['', backgroundBlock] : []),
69
70
  ...(jobsBlock ? ['', jobsBlock] : []),
71
+ // AFTER every subsystem block, BEFORE the caller's own instructions: it describes
72
+ // the shape of the answer, which is the last thing to know before answering, while
73
+ // `## Additional instructions` still has the final word (section 2.3.8).
74
+ ...(structuredResultBlock ? ['', structuredResultBlock] : []),
70
75
  '',
71
76
  'Operating guidance:',
72
77
  '- Prefer reading files and inspecting the workspace before making changes.',