@kolisachint/hoocode-agent 0.5.30 → 0.5.32

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 (134) hide show
  1. package/CHANGELOG.md +234 -0
  2. package/dist/core/agent-selection-eval.d.ts +91 -0
  3. package/dist/core/agent-selection-eval.d.ts.map +1 -0
  4. package/dist/core/agent-selection-eval.js +186 -0
  5. package/dist/core/agent-selection-eval.js.map +1 -0
  6. package/dist/core/agent-session-runtime.d.ts +28 -0
  7. package/dist/core/agent-session-runtime.d.ts.map +1 -1
  8. package/dist/core/agent-session-runtime.js +61 -2
  9. package/dist/core/agent-session-runtime.js.map +1 -1
  10. package/dist/core/builtin-skills.d.ts +61 -0
  11. package/dist/core/builtin-skills.d.ts.map +1 -0
  12. package/dist/core/builtin-skills.js +106 -0
  13. package/dist/core/builtin-skills.js.map +1 -0
  14. package/dist/core/extensions/plugins/default-marketplace/.agents-plugin/marketplace.json +5 -1
  15. package/dist/core/extensions/plugins/trigger-judge.d.ts +42 -0
  16. package/dist/core/extensions/plugins/trigger-judge.d.ts.map +1 -0
  17. package/dist/core/extensions/plugins/trigger-judge.js +121 -0
  18. package/dist/core/extensions/plugins/trigger-judge.js.map +1 -0
  19. package/dist/core/external-tools.d.ts +67 -0
  20. package/dist/core/external-tools.d.ts.map +1 -0
  21. package/dist/core/external-tools.js +120 -0
  22. package/dist/core/external-tools.js.map +1 -0
  23. package/dist/core/keybindings.d.ts +102 -30
  24. package/dist/core/keybindings.d.ts.map +1 -1
  25. package/dist/core/keybindings.js +123 -29
  26. package/dist/core/keybindings.js.map +1 -1
  27. package/dist/core/mode-prompts.d.ts +15 -3
  28. package/dist/core/mode-prompts.d.ts.map +1 -1
  29. package/dist/core/mode-prompts.js +17 -29
  30. package/dist/core/mode-prompts.js.map +1 -1
  31. package/dist/core/settings-defaults.d.ts +1 -1
  32. package/dist/core/settings-defaults.d.ts.map +1 -1
  33. package/dist/core/settings-defaults.js +1 -1
  34. package/dist/core/settings-defaults.js.map +1 -1
  35. package/dist/core/settings-manager.d.ts +10 -3
  36. package/dist/core/settings-manager.d.ts.map +1 -1
  37. package/dist/core/settings-manager.js +19 -8
  38. package/dist/core/settings-manager.js.map +1 -1
  39. package/dist/core/settings-types.d.ts +1 -1
  40. package/dist/core/settings-types.d.ts.map +1 -1
  41. package/dist/core/settings-types.js.map +1 -1
  42. package/dist/core/slash-commands.d.ts.map +1 -1
  43. package/dist/core/slash-commands.js +5 -1
  44. package/dist/core/slash-commands.js.map +1 -1
  45. package/dist/core/tool-output-view.d.ts +33 -0
  46. package/dist/core/tool-output-view.d.ts.map +1 -0
  47. package/dist/core/tool-output-view.js +31 -0
  48. package/dist/core/tool-output-view.js.map +1 -0
  49. package/dist/core/tools/edit.d.ts.map +1 -1
  50. package/dist/core/tools/edit.js +4 -0
  51. package/dist/core/tools/edit.js.map +1 -1
  52. package/dist/core/tools/propose-plugin.d.ts.map +1 -1
  53. package/dist/core/tools/propose-plugin.js +7 -7
  54. package/dist/core/tools/propose-plugin.js.map +1 -1
  55. package/dist/core/tools/subagent.d.ts +7 -1
  56. package/dist/core/tools/subagent.d.ts.map +1 -1
  57. package/dist/core/tools/subagent.js +12 -24
  58. package/dist/core/tools/subagent.js.map +1 -1
  59. package/dist/extensions/core/modes.d.ts.map +1 -1
  60. package/dist/extensions/core/modes.js +21 -23
  61. package/dist/extensions/core/modes.js.map +1 -1
  62. package/dist/extensions/core/scaffold.d.ts.map +1 -1
  63. package/dist/extensions/core/scaffold.js +28 -24
  64. package/dist/extensions/core/scaffold.js.map +1 -1
  65. package/dist/init-templates.generated.d.ts +3 -0
  66. package/dist/init-templates.generated.d.ts.map +1 -1
  67. package/dist/init-templates.generated.js +12 -0
  68. package/dist/init-templates.generated.js.map +1 -1
  69. package/dist/main.d.ts.map +1 -1
  70. package/dist/main.js +14 -1
  71. package/dist/main.js.map +1 -1
  72. package/dist/modes/interactive/command-executor.d.ts +20 -0
  73. package/dist/modes/interactive/command-executor.d.ts.map +1 -1
  74. package/dist/modes/interactive/command-executor.js +138 -9
  75. package/dist/modes/interactive/command-executor.js.map +1 -1
  76. package/dist/modes/interactive/components/footer.d.ts +4 -0
  77. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  78. package/dist/modes/interactive/components/footer.js +31 -2
  79. package/dist/modes/interactive/components/footer.js.map +1 -1
  80. package/dist/modes/interactive/components/scoped-models-selector.d.ts +2 -2
  81. package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
  82. package/dist/modes/interactive/components/scoped-models-selector.js +2 -2
  83. package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
  84. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  85. package/dist/modes/interactive/components/session-selector.js +2 -2
  86. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  87. package/dist/modes/interactive/components/settings-selector.d.ts +10 -2
  88. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  89. package/dist/modes/interactive/components/settings-selector.js +165 -23
  90. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  91. package/dist/modes/interactive/components/tool-chain-summary.d.ts +65 -0
  92. package/dist/modes/interactive/components/tool-chain-summary.d.ts.map +1 -0
  93. package/dist/modes/interactive/components/tool-chain-summary.js +216 -0
  94. package/dist/modes/interactive/components/tool-chain-summary.js.map +1 -0
  95. package/dist/modes/interactive/components/tool-chain.d.ts +61 -0
  96. package/dist/modes/interactive/components/tool-chain.d.ts.map +1 -0
  97. package/dist/modes/interactive/components/tool-chain.js +156 -0
  98. package/dist/modes/interactive/components/tool-chain.js.map +1 -0
  99. package/dist/modes/interactive/components/tool-execution.d.ts +55 -9
  100. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  101. package/dist/modes/interactive/components/tool-execution.js +173 -33
  102. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  103. package/dist/modes/interactive/components/tool-signal.d.ts +50 -0
  104. package/dist/modes/interactive/components/tool-signal.d.ts.map +1 -0
  105. package/dist/modes/interactive/components/tool-signal.js +151 -0
  106. package/dist/modes/interactive/components/tool-signal.js.map +1 -0
  107. package/dist/modes/interactive/interactive-mode.d.ts +50 -1
  108. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  109. package/dist/modes/interactive/interactive-mode.js +244 -29
  110. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  111. package/dist/modes/interactive/resource-display.d.ts +9 -0
  112. package/dist/modes/interactive/resource-display.d.ts.map +1 -1
  113. package/dist/modes/interactive/resource-display.js +13 -0
  114. package/dist/modes/interactive/resource-display.js.map +1 -1
  115. package/dist/utils/tools-manager.d.ts +30 -0
  116. package/dist/utils/tools-manager.d.ts.map +1 -1
  117. package/dist/utils/tools-manager.js +35 -0
  118. package/dist/utils/tools-manager.js.map +1 -1
  119. package/docs/modes.md +4 -0
  120. package/docs/plugins.md +10 -0
  121. package/docs/settings.md +42 -0
  122. package/docs/skills.md +30 -0
  123. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  124. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  125. package/examples/extensions/sandbox/package.json +1 -1
  126. package/examples/extensions/with-deps/package.json +1 -1
  127. package/package.json +5 -4
  128. package/templates/prompts/grill-bridge.md +1 -0
  129. package/templates/prompts/grill-me.md +7 -0
  130. package/templates/prompts/grill-plan.md +9 -0
  131. package/templates/prompts/task-background-agents.md +2 -0
  132. package/templates/prompts/task-background-none.md +1 -0
  133. package/templates/prompts/task-main.md +19 -0
  134. package/templates/skills/plugin-authoring/SKILL.md +81 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,239 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.5.32] - 2026-08-23
4
+
5
+ ### Breaking Changes
6
+
7
+ - The tool-output setting is renamed and its values with it: `toolOutputDisplay`
8
+ (`collapsed` / `peek` / `standard`) is now `toolOutputView` (`radar` /
9
+ `glance` / `full`), and the default moves from `standard` to `glance`. Old
10
+ settings files still load — `collapsed` reads as `radar`, `peek` as `glance`,
11
+ `standard` as `full` — and the old key is rewritten the first time the view is
12
+ changed. Nothing is lost, but a session that used to open with every tool
13
+ result on screen now opens with call lines and folds the bodies away.
14
+
15
+ - Keybindings moved off keys the terminal, the shell, or the prompt editor
16
+ already owned. The bindings are configurable as before, so a
17
+ `~/.hoocode/keybindings.json` that pins any of these keeps working:
18
+
19
+ | Action | Was | Now |
20
+ |---|---|---|
21
+ | Open model selector | `ctrl+l` | `alt+m` |
22
+ | Open external editor | `ctrl+g` | `alt+e` |
23
+ | Voice record | `ctrl+r` | `alt+r` |
24
+ | Session picker: toggle path | `ctrl+p` | `alt+p` |
25
+ | Session picker: sort order | `ctrl+s` | `alt+o` |
26
+ | Session picker: rename | `ctrl+r` | `alt+r` |
27
+ | Session picker: delete | `ctrl+d` | `alt+x` |
28
+ | Session picker: named filter | `ctrl+n` | `alt+n` |
29
+ | Model picker: save | `ctrl+s` | `alt+s` |
30
+ | Model picker: enable all | `ctrl+a` | `alt+a` |
31
+ | Model picker: clear all | `ctrl+x` | `alt+x` |
32
+ | Model picker: toggle provider | `ctrl+p` | `alt+g` |
33
+ | Tree filters | `ctrl+d/t/u/l/a` | `alt+1`…`alt+5` |
34
+ | Tree filter cycle | `ctrl+o` / `shift+ctrl+o` | `alt+c` / `shift+alt+c` |
35
+ | Team roster focus | `alt+n` | unchanged |
36
+
37
+ ### Added
38
+
39
+ - A **view dial** for tool output, on `alt+o` (`shift+alt+o` cycles back), with
40
+ three stops from least to most:
41
+
42
+ - **radar** — one line per *chain*: a run of consecutive tool calls. While the
43
+ run is working the line shows its shape, in order, with the failures marked
44
+ — `◐ grep › read › bash✗ › edit › bash… 4 done · 1 failed · running`. Once
45
+ the agent moves on it becomes what the run amounted to —
46
+ `● Edited packages/tui/src/keys.ts 5 calls · 1 failed · 453 lines`.
47
+ Consecutive repeats collapse (`read ×4`) and long chains elide their middle,
48
+ but never a failure. `alt+u` turns a chain back into its individual calls.
49
+ - **glance** — the tool's own call line, one per call, body folded away. The
50
+ new default.
51
+ - **full** — call line plus the result body, as before.
52
+
53
+ - **A failure always shows why it failed, in every view.** Previously a failed
54
+ tool in the folded views was a red dot and nothing else — the one thing you
55
+ always want to see was the one thing they hid.
56
+
57
+ The footer shows where the dial sits, with a glyph that fills up as the view
58
+ widens.
59
+
60
+ - `alt+u` opens one thing, newest first, repeating to peel backwards;
61
+ `shift+alt+u` re-folds. In radar it turns a chain back into its calls;
62
+ elsewhere it opens a single tool body. `ctrl+o` is unchanged and still expands
63
+ everything at once.
64
+
65
+ It works from the newest backwards rather than through a cursor because the
66
+ transcript is bottom-anchored with no app-level scrolling — anything far
67
+ enough up is in the terminal's own scrollback, where this process can neither
68
+ scroll nor place a selection, so a cursor would move somewhere you cannot see.
69
+
70
+ ### Removed
71
+
72
+ - The `▸` disclosure caret on folded tool rows. It is a click-target idiom in a
73
+ TUI with no pointer, it cost two columns on every row, and it advertised a
74
+ per-row action that could only ever be reached from the keyboard. Opening is
75
+ now `ctrl+o` (everything) or `alt+u` (one thing, newest first).
76
+
77
+ - `/cd <path>` moves the whole session to another directory without leaving the
78
+ process — provider auth, the warmed model list and the terminal all survive.
79
+ Everything cwd-bound is rebuilt for the new root (tools, context files,
80
+ project settings, extensions, skills, agents, MCP servers), and because
81
+ sessions are stored per project, a fresh session starts there; the one you
82
+ left is still on disk and `/resume` in the old directory reopens it. Bare
83
+ `/cd` goes home, `/cd -` returns to the previous directory, and the argument
84
+ completes against real subdirectories. Bound to `alt+w`.
85
+
86
+ - Keys for the cockpit moves that had none: `alt+g` cycles the agent mode
87
+ (ask → plan → build → debug, taken from the mode command's own list rather
88
+ than a copy), `alt+s` opens settings, `alt+k` opens the shortcut list,
89
+ `alt+t` opens the session tree, and `alt+h` resumes from history. `/new` and
90
+ `/fork` stay unbound by default — one replaces the transcript, the other needs
91
+ a message picked out of it.
92
+
93
+ ### Changed
94
+
95
+ - The keybinding set is organized into three rings, and which ring a key is in
96
+ is decided by its modifier. `ctrl` is the view: what is on screen right now.
97
+ `alt` is the cockpit: what the agent is and where it works. Inside a picker,
98
+ every `ctrl` key belongs to the query you are typing, so a picker's own verbs
99
+ are all on `alt`. That last rule is what most of the moves above are: the
100
+ session picker took `ctrl+a` (start of line), `ctrl+u` (kill to start) and
101
+ `ctrl+d` (delete character) out of its own search field, and the model picker
102
+ put "save" on `ctrl+s`, which is XOFF on a terminal with flow control on.
103
+
104
+ Every default is checked against what a terminal without the Kitty keyboard
105
+ protocol actually sends. Only `alt+<letter>` and `alt+<digit>` survive there,
106
+ and legacy `alt+p`/`alt+n` double as `alt+up`/`alt+down`, so no scope binds
107
+ both halves of either pair — that is why the model picker's provider toggle is
108
+ `alt+g` rather than the obvious `alt+p`, which would have fired the reorder
109
+ key sitting next to it. `test/keybinding-layout.test.ts` holds all of this.
110
+
111
+ - `ctrl+n` no longer means two things. It was both "cycle the task panel" and
112
+ the session picker's named filter; the filter moves to `alt+n`.
113
+
114
+ - `/hotkeys` is grouped by ring — Flow, View, Cockpit — instead of by
115
+ Navigation / Editing / Other.
116
+
117
+ ### Fixed
118
+
119
+ - A self-rendering tool's call line (`edit`) sat one column left of every other
120
+ row while its body was folded away, because it skipped the padded shell its
121
+ own diff frame needs. Harmless while `peek` was opt-in, visible on every edit
122
+ now that `glance` is the default.
123
+
124
+ - The startup banner names the working directory and never re-read it, so after
125
+ `/cd` it kept advertising the directory you had left.
126
+
127
+ - `edit`'s call line sat one column right of every other tool's in the folded
128
+ views. Its render component pads horizontally to carry the diff's header band,
129
+ and with no band to draw that padding was just an indent. It now pads only
130
+ when the band is there.
131
+
132
+ - A failed call's reason now hangs off its radar row instead of starting back at
133
+ the left margin.
134
+ ## [0.5.31] - 2026-08-23
135
+
136
+ ### Added
137
+
138
+ - `/settings` has an **External tools** category. hoocode ships five optional
139
+ Rust binaries — `rg`, `fd`, `embsearch`, `webtools`, `voicetools` — that
140
+ expand what it can do, and nothing in the product ever said so. hoocode works
141
+ without every one of them, which is exactly why they stayed invisible: search
142
+ silently got slower, semantic ranking silently never happened, and web and
143
+ voice were features nobody knew were there. Each row shows live status
144
+ (installed / found on `PATH` / env override / not installed), and opens a
145
+ detail view naming what it enables, what hoocode does instead when it is
146
+ missing, where the release comes from, and the env vars that steer it.
147
+
148
+ - hoocode can ship skills of its own. It read them from `~/.agents/skills`,
149
+ `.hoocode/skills`, `.claude/skills` and installed packages — every source
150
+ except itself — which is why it shipped three subagents and zero skills while
151
+ telling users skills are the extension unit. Built-ins are catalogued in
152
+ `core/builtin-skills.ts` and load at lowest precedence, so a skill of the same
153
+ name from anywhere else wins and the collision is reported.
154
+
155
+ A skill costs its description on every turn, so each built-in can be gated on
156
+ the feature it serves rather than on everyone's token budget.
157
+
158
+ - The first one: `plugin-authoring`, the craft half of `ProposePlugin`/
159
+ `UpdatePlugin` — when a capability is worth extracting, naming and describing
160
+ it so it triggers again, portability rules, and the hook trap where a changed
161
+ command adds a second hook instead of replacing one. Gated on
162
+ `enablePluginTools`, which is off by default, so a default session pays
163
+ nothing for it.
164
+
165
+ Built-ins are materialized to a content-addressed cache under
166
+ `~/.hoocode/cache/builtin-skills/`. A skill is loaded by reading its file, so
167
+ its location has to be a real path, and the compiled standalone binary has no
168
+ install directory to read from; materializing the same embedded copy
169
+ everywhere keeps the skill set identical across npm, pnpm, source and the
170
+ binary. If the cache cannot be written the built-ins are absent and everything
171
+ else runs normally. `--no-skills` and `--light` suppress them.
172
+
173
+ - An agent-selection eval: `bun run agent-eval` scores the built-in agent roster
174
+ against a gold set of real tasks, reporting how often each agent is chosen
175
+ when it should be, how often the parent correctly keeps work inline, and a
176
+ confusion matrix naming which agent loses to which. It reuses the plugin G4
177
+ trigger harness rather than adding a second one, and describes each agent with
178
+ the summarized text `<available_agents>` actually emits.
179
+
180
+ This exists to settle whether `plan` and `explore` are two agents or one:
181
+ they ship with the same tools, the same isolation and the same background
182
+ flag, and `complexity` on the Task tool already expresses the only other
183
+ difference. That was being argued from intuition; it is now measurable.
184
+
185
+ ### Fixed
186
+
187
+ - The G4 plugin trigger gate never had a judge. `trigger-eval.ts` takes its
188
+ model call as a parameter so scoring stays testable, and nothing in the tree
189
+ ever passed one — so every G4 run has reported `not-run` since it was written.
190
+ `createLlmTriggerJudge` is that judge, shared with the agent-selection eval.
191
+
192
+ - `/new-skill`, `/new-agent` and `/new-command` scaffolded different content
193
+ depending on whether `--platform` was set. Each command has two write paths —
194
+ the per-vendor emitters and the plain `.hoocode/` writer — and each carried
195
+ its own copy of the body, which had drifted in both directions: the
196
+ `.hoocode/` command body documented the `${@:N}` / `${@:N:L}` slice
197
+ placeholders that the platform path silently omitted, and the `.hoocode/`
198
+ agent body identified the agent as running inside hoocode where the platform
199
+ one did not. Both paths read one definition now, and the richer text won in
200
+ each case.
201
+
202
+ ### Changed
203
+
204
+ - The four built-in mode prompts have one home. `templates/modes/<mode>/system.md`
205
+ is now the only copy: `core/mode-prompts.ts` re-exports the embedded copy as
206
+ `DEFAULT_MODE_PROMPTS` instead of carrying a second, hand-written set. The two
207
+ had already drifted — `/init` scaffolds the template text into a project, so a
208
+ user who ran it and a user who did not were getting differently worded mode
209
+ rules from the same version.
210
+
211
+ - The `/grill` phases moved out of TypeScript into `templates/prompts/*.md`.
212
+ They are prose the runtime injects verbatim — no interpolation, no branching —
213
+ so they are edited as prose now. The message `/grill` produces is unchanged.
214
+
215
+ - The Task delegation appendix moved to `templates/prompts/task-main.md` and its
216
+ two `task-background-*` variants. At ~600 tok/turn it is the largest block of
217
+ always-on text hoocode emits once the Task tool is on, and it had exactly one
218
+ interpolation slot in otherwise static prose, so a string constant bought
219
+ nothing. The rendered prompt is byte-identical on both the with- and
220
+ without-background-agents branches.
221
+
222
+ - `ProposePlugin` and `UpdatePlugin` shed ~55% of their always-on prompt
223
+ guidance (~626 to ~301 tok/turn when the plugin system is enabled). What was
224
+ removed was how-to-author-well guidance, which is not a tool contract and now
225
+ lives in the `plugin-authoring` skill; what stayed is the trigger, the
226
+ transparency rule for autonomous authoring, the hook trap, and the two hard
227
+ prohibitions. Several removed lines also restated the tools' own descriptions
228
+ or each other.
229
+
230
+ - Settings rows that are inert without one of those binaries now say so. The
231
+ `web` and `semantic search` tool-group switches, the web tools timeout and the
232
+ voice silence window carry a `needs <binary>` marker and explain the fallback
233
+ and whether hoocode will fetch the binary. The rows stay settable — the
234
+ setting is what makes hoocode fetch the binary in the first place, and hiding
235
+ a row when its dependency is missing would recreate the same silence.
236
+
3
237
  ## [0.5.30] - 2026-08-23
4
238
 
5
239
  ### Added
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Does the agent roster steer a dispatch to the right agent?
3
+ *
4
+ * The `Task` tool asks the model to pick a `subagent_type` from
5
+ * `<available_agents>`, and that block contains nothing but each agent's
6
+ * summarized description. So agent selection is the same question G4 asks about
7
+ * skills — "given these descriptions and this situation, which one fires?" —
8
+ * and it reuses the same harness rather than growing a second one.
9
+ *
10
+ * What this exists to decide: `plan` and `explore` ship with the same tools, the
11
+ * same isolation and the same `background` flag, differing only in model tier
12
+ * and output contract — and `complexity` on the Task tool already expresses the
13
+ * tier. Whether they are two agents or one is a question about whether the model
14
+ * can actually tell them apart from their descriptions, which is measurable and
15
+ * was previously being argued from intuition.
16
+ *
17
+ * The candidates use `summarizeAgentDescription`, not the raw frontmatter: the
18
+ * summary is what the system prompt actually emits, and evaluating the full
19
+ * description would score text the model never sees.
20
+ */
21
+ import { type TriggerCandidate, type TriggerCase, type TriggerEvalOutcome, type TriggerJudge } from "./extensions/plugins/trigger-eval.js";
22
+ /** How often one agent was chosen where another was expected. */
23
+ export interface ConfusionEntry {
24
+ expected: string;
25
+ actual: string;
26
+ count: number;
27
+ }
28
+ export interface AgentSelectionReport {
29
+ corpusHash: string;
30
+ agents: string[];
31
+ caseCount: number;
32
+ /** Cases where the expected agent was chosen, over cases expecting any agent. */
33
+ accuracy?: number;
34
+ /** Cases correctly left to the parent, over cases expecting no delegation. */
35
+ inlineAccuracy?: number;
36
+ /** Every wrong pick, most frequent first. The pairs here are the finding. */
37
+ confusion: ConfusionEntry[];
38
+ /** Per-agent recall: chosen / expected. An agent nobody picks is dead weight. */
39
+ perAgent: Array<{
40
+ agent: string;
41
+ expected: number;
42
+ chosen: number;
43
+ recall: number;
44
+ }>;
45
+ }
46
+ /**
47
+ * The built-in agents as judge candidates, described exactly as the system
48
+ * prompt describes them.
49
+ *
50
+ * `own: true` for all of them: unlike a plugin eval there is no foreign roster
51
+ * to discriminate against, so every case is scored against the same closed set.
52
+ * The `expect: null` cases carry the discriminative half instead — they ask
53
+ * whether the model declines to delegate work it should keep.
54
+ */
55
+ export declare function agentCandidates(cwd?: string): TriggerCandidate[];
56
+ /** Load a gold set: `{ "cases": [{ "prompt": "...", "expect": "explore" | null }] }`. */
57
+ export declare function loadAgentCases(file: string): TriggerCase[] | undefined;
58
+ /**
59
+ * Every expected agent in the gold set must exist in the roster.
60
+ *
61
+ * A typo'd or removed agent name would otherwise score as a permanent miss and
62
+ * read as a description problem, which is the most expensive way to be wrong
63
+ * about an eval.
64
+ */
65
+ export declare function validateAgentCases(candidates: readonly TriggerCandidate[], cases: readonly TriggerCase[]): string[];
66
+ /**
67
+ * Turn a scored run into the report that answers the design question.
68
+ *
69
+ * `runTriggerEval`'s recall/specificity are the right numbers for a plugin
70
+ * defending itself against a foreign roster. Here the roster is closed, so the
71
+ * useful shape is a confusion matrix: which agent loses to which, and how often.
72
+ * "explore and plan are interchangeable" is a claim about one cell.
73
+ */
74
+ export declare function summarizeAgentSelection(outcome: Extract<TriggerEvalOutcome, {
75
+ status: "ran";
76
+ }>, candidates: readonly TriggerCandidate[]): AgentSelectionReport;
77
+ export type AgentSelectionOutcome = {
78
+ status: "not-run";
79
+ reason: string;
80
+ } | {
81
+ status: "ran";
82
+ report: AgentSelectionReport;
83
+ outcome: Extract<TriggerEvalOutcome, {
84
+ status: "ran";
85
+ }>;
86
+ };
87
+ /** Score the roster against a gold set. Never throws; a missing model is `not-run`. */
88
+ export declare function runAgentSelectionEval(candidates: readonly TriggerCandidate[], cases: readonly TriggerCase[] | undefined, judge: TriggerJudge | undefined): Promise<AgentSelectionOutcome>;
89
+ /** Human-readable report for the CLI. */
90
+ export declare function formatAgentSelectionReport(report: AgentSelectionReport): string;
91
+ //# sourceMappingURL=agent-selection-eval.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-selection-eval.d.ts","sourceRoot":"","sources":["../../src/core/agent-selection-eval.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAKH,OAAO,EAEN,KAAK,gBAAgB,EACrB,KAAK,WAAW,EAChB,KAAK,kBAAkB,EACvB,KAAK,YAAY,EACjB,MAAM,sCAAsC,CAAC;AAE9C,iEAAiE;AACjE,MAAM,WAAW,cAAc;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,oBAAoB;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,iFAAiF;IACjF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,8EAA8E;IAC9E,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,6EAA6E;IAC7E,SAAS,EAAE,cAAc,EAAE,CAAC;IAC5B,iFAAiF;IACjF,QAAQ,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACrF;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,GAAG,GAAE,MAAsB,GAAG,gBAAgB,EAAE,CAU/E;AAED,yFAAyF;AACzF,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,EAAE,GAAG,SAAS,CAiBtE;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,SAAS,gBAAgB,EAAE,EAAE,KAAK,EAAE,SAAS,WAAW,EAAE,GAAG,MAAM,EAAE,CASnH;AASD;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CACtC,OAAO,EAAE,OAAO,CAAC,kBAAkB,EAAE;IAAE,MAAM,EAAE,KAAK,CAAA;CAAE,CAAC,EACvD,UAAU,EAAE,SAAS,gBAAgB,EAAE,GACrC,oBAAoB,CAiDtB;AAED,MAAM,MAAM,qBAAqB,GAC9B;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACrC;IAAE,MAAM,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,oBAAoB,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC,kBAAkB,EAAE;QAAE,MAAM,EAAE,KAAK,CAAA;KAAE,CAAC,CAAA;CAAE,CAAC;AAE5G,uFAAuF;AACvF,wBAAsB,qBAAqB,CAC1C,UAAU,EAAE,SAAS,gBAAgB,EAAE,EACvC,KAAK,EAAE,SAAS,WAAW,EAAE,GAAG,SAAS,EACzC,KAAK,EAAE,YAAY,GAAG,SAAS,GAC7B,OAAO,CAAC,qBAAqB,CAAC,CAmBhC;AAED,yCAAyC;AACzC,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,oBAAoB,GAAG,MAAM,CAkB/E","sourcesContent":["/**\n * Does the agent roster steer a dispatch to the right agent?\n *\n * The `Task` tool asks the model to pick a `subagent_type` from\n * `<available_agents>`, and that block contains nothing but each agent's\n * summarized description. So agent selection is the same question G4 asks about\n * skills — \"given these descriptions and this situation, which one fires?\" —\n * and it reuses the same harness rather than growing a second one.\n *\n * What this exists to decide: `plan` and `explore` ship with the same tools, the\n * same isolation and the same `background` flag, differing only in model tier\n * and output contract — and `complexity` on the Task tool already expresses the\n * tier. Whether they are two agents or one is a question about whether the model\n * can actually tell them apart from their descriptions, which is measurable and\n * was previously being argued from intuition.\n *\n * The candidates use `summarizeAgentDescription`, not the raw frontmatter: the\n * summary is what the system prompt actually emits, and evaluating the full\n * description would score text the model never sees.\n */\n\nimport { createHash } from \"node:crypto\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { loadAgentRegistry, summarizeAgentDescription } from \"./agent-registry.js\";\nimport {\n\trunTriggerEval,\n\ttype TriggerCandidate,\n\ttype TriggerCase,\n\ttype TriggerEvalOutcome,\n\ttype TriggerJudge,\n} from \"./extensions/plugins/trigger-eval.js\";\n\n/** How often one agent was chosen where another was expected. */\nexport interface ConfusionEntry {\n\texpected: string;\n\tactual: string;\n\tcount: number;\n}\n\nexport interface AgentSelectionReport {\n\tcorpusHash: string;\n\tagents: string[];\n\tcaseCount: number;\n\t/** Cases where the expected agent was chosen, over cases expecting any agent. */\n\taccuracy?: number;\n\t/** Cases correctly left to the parent, over cases expecting no delegation. */\n\tinlineAccuracy?: number;\n\t/** Every wrong pick, most frequent first. The pairs here are the finding. */\n\tconfusion: ConfusionEntry[];\n\t/** Per-agent recall: chosen / expected. An agent nobody picks is dead weight. */\n\tperAgent: Array<{ agent: string; expected: number; chosen: number; recall: number }>;\n}\n\n/**\n * The built-in agents as judge candidates, described exactly as the system\n * prompt describes them.\n *\n * `own: true` for all of them: unlike a plugin eval there is no foreign roster\n * to discriminate against, so every case is scored against the same closed set.\n * The `expect: null` cases carry the discriminative half instead — they ask\n * whether the model declines to delegate work it should keep.\n */\nexport function agentCandidates(cwd: string = process.cwd()): TriggerCandidate[] {\n\tconst registry = loadAgentRegistry({ cwd, includeBuiltins: true, includeClaude: false });\n\treturn registry\n\t\t.list()\n\t\t.map((agent) => ({\n\t\t\tname: agent.name,\n\t\t\tdescription: summarizeAgentDescription(agent.description ?? \"\"),\n\t\t\town: true,\n\t\t}))\n\t\t.sort((a, b) => a.name.localeCompare(b.name));\n}\n\n/** Load a gold set: `{ \"cases\": [{ \"prompt\": \"...\", \"expect\": \"explore\" | null }] }`. */\nexport function loadAgentCases(file: string): TriggerCase[] | undefined {\n\tif (!existsSync(file)) return undefined;\n\ttry {\n\t\tconst raw = JSON.parse(readFileSync(file, \"utf-8\")) as { cases?: unknown };\n\t\tif (!Array.isArray(raw.cases)) return undefined;\n\t\tconst cases = raw.cases.filter(\n\t\t\t(c): c is TriggerCase =>\n\t\t\t\t!!c &&\n\t\t\t\ttypeof c === \"object\" &&\n\t\t\t\ttypeof (c as TriggerCase).prompt === \"string\" &&\n\t\t\t\t(c as TriggerCase).prompt.trim().length > 0 &&\n\t\t\t\t((c as TriggerCase).expect === null || typeof (c as TriggerCase).expect === \"string\"),\n\t\t);\n\t\treturn cases.length > 0 ? cases : undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/**\n * Every expected agent in the gold set must exist in the roster.\n *\n * A typo'd or removed agent name would otherwise score as a permanent miss and\n * read as a description problem, which is the most expensive way to be wrong\n * about an eval.\n */\nexport function validateAgentCases(candidates: readonly TriggerCandidate[], cases: readonly TriggerCase[]): string[] {\n\tconst known = new Set(candidates.map((c) => c.name));\n\tconst problems: string[] = [];\n\tfor (const [i, testCase] of cases.entries()) {\n\t\tif (testCase.expect !== null && !known.has(testCase.expect)) {\n\t\t\tproblems.push(`case ${i} expects \"${testCase.expect}\", which is not in the roster`);\n\t\t}\n\t}\n\treturn problems;\n}\n\nfunction hashCorpus(candidates: readonly TriggerCandidate[], cases: readonly TriggerCase[]): string {\n\tconst h = createHash(\"sha256\");\n\tfor (const c of candidates) h.update(`${c.name} ${c.description} `);\n\tfor (const c of cases) h.update(`${c.prompt} ${c.expect ?? \"\"} `);\n\treturn h.digest(\"hex\").slice(0, 16);\n}\n\n/**\n * Turn a scored run into the report that answers the design question.\n *\n * `runTriggerEval`'s recall/specificity are the right numbers for a plugin\n * defending itself against a foreign roster. Here the roster is closed, so the\n * useful shape is a confusion matrix: which agent loses to which, and how often.\n * \"explore and plan are interchangeable\" is a claim about one cell.\n */\nexport function summarizeAgentSelection(\n\toutcome: Extract<TriggerEvalOutcome, { status: \"ran\" }>,\n\tcandidates: readonly TriggerCandidate[],\n): AgentSelectionReport {\n\tconst confusion = new Map<string, ConfusionEntry>();\n\tconst expectedCounts = new Map<string, number>();\n\tconst chosenCounts = new Map<string, number>();\n\tlet delegated = 0;\n\tlet delegatedCorrect = 0;\n\tlet inline = 0;\n\tlet inlineCorrect = 0;\n\n\tfor (const result of outcome.record.results) {\n\t\tif (result.expected === null) {\n\t\t\tinline++;\n\t\t\tif (result.actual === null) inlineCorrect++;\n\t\t\tcontinue;\n\t\t}\n\t\tdelegated++;\n\t\texpectedCounts.set(result.expected, (expectedCounts.get(result.expected) ?? 0) + 1);\n\t\tif (result.correct) {\n\t\t\tdelegatedCorrect++;\n\t\t\tchosenCounts.set(result.expected, (chosenCounts.get(result.expected) ?? 0) + 1);\n\t\t\tcontinue;\n\t\t}\n\t\t// \"(none)\" is a real outcome, not a missing value: declining to delegate\n\t\t// work that should have been delegated is a different failure from picking\n\t\t// the wrong agent, and collapsing them would hide which one is happening.\n\t\tconst actual = result.actual ?? \"(none)\";\n\t\tconst key = `${result.expected} ${actual}`;\n\t\tconst entry = confusion.get(key) ?? { expected: result.expected, actual, count: 0 };\n\t\tentry.count++;\n\t\tconfusion.set(key, entry);\n\t}\n\n\tconst perAgent = candidates\n\t\t.map((candidate) => {\n\t\t\tconst expected = expectedCounts.get(candidate.name) ?? 0;\n\t\t\tconst chosen = chosenCounts.get(candidate.name) ?? 0;\n\t\t\treturn { agent: candidate.name, expected, chosen, recall: expected > 0 ? chosen / expected : 0 };\n\t\t})\n\t\t.sort((a, b) => a.recall - b.recall);\n\n\treturn {\n\t\tcorpusHash: outcome.record.corpusHash,\n\t\tagents: candidates.map((c) => c.name),\n\t\tcaseCount: outcome.record.caseCount,\n\t\taccuracy: delegated > 0 ? delegatedCorrect / delegated : undefined,\n\t\tinlineAccuracy: inline > 0 ? inlineCorrect / inline : undefined,\n\t\tconfusion: [...confusion.values()].sort((a, b) => b.count - a.count),\n\t\tperAgent,\n\t};\n}\n\nexport type AgentSelectionOutcome =\n\t| { status: \"not-run\"; reason: string }\n\t| { status: \"ran\"; report: AgentSelectionReport; outcome: Extract<TriggerEvalOutcome, { status: \"ran\" }> };\n\n/** Score the roster against a gold set. Never throws; a missing model is `not-run`. */\nexport async function runAgentSelectionEval(\n\tcandidates: readonly TriggerCandidate[],\n\tcases: readonly TriggerCase[] | undefined,\n\tjudge: TriggerJudge | undefined,\n): Promise<AgentSelectionOutcome> {\n\tif (cases && cases.length > 0) {\n\t\tconst problems = validateAgentCases(candidates, cases);\n\t\tif (problems.length > 0) return { status: \"not-run\", reason: `invalid gold set: ${problems.join(\"; \")}` };\n\t}\n\n\tconst outcome = await runTriggerEval(\"agent-selection\", candidates, cases, judge);\n\tif (outcome.status === \"not-run\") return outcome;\n\n\treturn {\n\t\tstatus: \"ran\",\n\t\treport: {\n\t\t\t...summarizeAgentSelection(outcome, candidates),\n\t\t\t// The shared harness hashes its own way; restate it over exactly what\n\t\t\t// this eval judged so two reports are comparable on their own terms.\n\t\t\tcorpusHash: hashCorpus(candidates, cases ?? []),\n\t\t},\n\t\toutcome,\n\t};\n}\n\n/** Human-readable report for the CLI. */\nexport function formatAgentSelectionReport(report: AgentSelectionReport): string {\n\tconst pct = (v: number | undefined) => (v === undefined ? \"n/a\" : `${Math.round(v * 100)}%`);\n\tconst lines = [\n\t\t`corpus ${report.corpusHash} - ${report.caseCount} case(s) over ${report.agents.length} agent(s): ${report.agents.join(\", \")}`,\n\t\t`delegation accuracy ${pct(report.accuracy)} inline accuracy ${pct(report.inlineAccuracy)}`,\n\t\t\"\",\n\t\t\"per agent (lowest recall first):\",\n\t];\n\tfor (const row of report.perAgent) {\n\t\tlines.push(` ${row.agent.padEnd(18)} ${row.chosen}/${row.expected} chosen (${pct(row.recall)})`);\n\t}\n\tif (report.confusion.length > 0) {\n\t\tlines.push(\"\", \"confusions (expected then actual):\");\n\t\tfor (const entry of report.confusion) {\n\t\t\tlines.push(` ${entry.expected} => ${entry.actual} x${entry.count}`);\n\t\t}\n\t}\n\treturn lines.join(\"\\n\");\n}\n"]}
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Does the agent roster steer a dispatch to the right agent?
3
+ *
4
+ * The `Task` tool asks the model to pick a `subagent_type` from
5
+ * `<available_agents>`, and that block contains nothing but each agent's
6
+ * summarized description. So agent selection is the same question G4 asks about
7
+ * skills — "given these descriptions and this situation, which one fires?" —
8
+ * and it reuses the same harness rather than growing a second one.
9
+ *
10
+ * What this exists to decide: `plan` and `explore` ship with the same tools, the
11
+ * same isolation and the same `background` flag, differing only in model tier
12
+ * and output contract — and `complexity` on the Task tool already expresses the
13
+ * tier. Whether they are two agents or one is a question about whether the model
14
+ * can actually tell them apart from their descriptions, which is measurable and
15
+ * was previously being argued from intuition.
16
+ *
17
+ * The candidates use `summarizeAgentDescription`, not the raw frontmatter: the
18
+ * summary is what the system prompt actually emits, and evaluating the full
19
+ * description would score text the model never sees.
20
+ */
21
+ import { createHash } from "node:crypto";
22
+ import { existsSync, readFileSync } from "node:fs";
23
+ import { loadAgentRegistry, summarizeAgentDescription } from "./agent-registry.js";
24
+ import { runTriggerEval, } from "./extensions/plugins/trigger-eval.js";
25
+ /**
26
+ * The built-in agents as judge candidates, described exactly as the system
27
+ * prompt describes them.
28
+ *
29
+ * `own: true` for all of them: unlike a plugin eval there is no foreign roster
30
+ * to discriminate against, so every case is scored against the same closed set.
31
+ * The `expect: null` cases carry the discriminative half instead — they ask
32
+ * whether the model declines to delegate work it should keep.
33
+ */
34
+ export function agentCandidates(cwd = process.cwd()) {
35
+ const registry = loadAgentRegistry({ cwd, includeBuiltins: true, includeClaude: false });
36
+ return registry
37
+ .list()
38
+ .map((agent) => ({
39
+ name: agent.name,
40
+ description: summarizeAgentDescription(agent.description ?? ""),
41
+ own: true,
42
+ }))
43
+ .sort((a, b) => a.name.localeCompare(b.name));
44
+ }
45
+ /** Load a gold set: `{ "cases": [{ "prompt": "...", "expect": "explore" | null }] }`. */
46
+ export function loadAgentCases(file) {
47
+ if (!existsSync(file))
48
+ return undefined;
49
+ try {
50
+ const raw = JSON.parse(readFileSync(file, "utf-8"));
51
+ if (!Array.isArray(raw.cases))
52
+ return undefined;
53
+ const cases = raw.cases.filter((c) => !!c &&
54
+ typeof c === "object" &&
55
+ typeof c.prompt === "string" &&
56
+ c.prompt.trim().length > 0 &&
57
+ (c.expect === null || typeof c.expect === "string"));
58
+ return cases.length > 0 ? cases : undefined;
59
+ }
60
+ catch {
61
+ return undefined;
62
+ }
63
+ }
64
+ /**
65
+ * Every expected agent in the gold set must exist in the roster.
66
+ *
67
+ * A typo'd or removed agent name would otherwise score as a permanent miss and
68
+ * read as a description problem, which is the most expensive way to be wrong
69
+ * about an eval.
70
+ */
71
+ export function validateAgentCases(candidates, cases) {
72
+ const known = new Set(candidates.map((c) => c.name));
73
+ const problems = [];
74
+ for (const [i, testCase] of cases.entries()) {
75
+ if (testCase.expect !== null && !known.has(testCase.expect)) {
76
+ problems.push(`case ${i} expects "${testCase.expect}", which is not in the roster`);
77
+ }
78
+ }
79
+ return problems;
80
+ }
81
+ function hashCorpus(candidates, cases) {
82
+ const h = createHash("sha256");
83
+ for (const c of candidates)
84
+ h.update(`${c.name} ${c.description} `);
85
+ for (const c of cases)
86
+ h.update(`${c.prompt} ${c.expect ?? ""} `);
87
+ return h.digest("hex").slice(0, 16);
88
+ }
89
+ /**
90
+ * Turn a scored run into the report that answers the design question.
91
+ *
92
+ * `runTriggerEval`'s recall/specificity are the right numbers for a plugin
93
+ * defending itself against a foreign roster. Here the roster is closed, so the
94
+ * useful shape is a confusion matrix: which agent loses to which, and how often.
95
+ * "explore and plan are interchangeable" is a claim about one cell.
96
+ */
97
+ export function summarizeAgentSelection(outcome, candidates) {
98
+ const confusion = new Map();
99
+ const expectedCounts = new Map();
100
+ const chosenCounts = new Map();
101
+ let delegated = 0;
102
+ let delegatedCorrect = 0;
103
+ let inline = 0;
104
+ let inlineCorrect = 0;
105
+ for (const result of outcome.record.results) {
106
+ if (result.expected === null) {
107
+ inline++;
108
+ if (result.actual === null)
109
+ inlineCorrect++;
110
+ continue;
111
+ }
112
+ delegated++;
113
+ expectedCounts.set(result.expected, (expectedCounts.get(result.expected) ?? 0) + 1);
114
+ if (result.correct) {
115
+ delegatedCorrect++;
116
+ chosenCounts.set(result.expected, (chosenCounts.get(result.expected) ?? 0) + 1);
117
+ continue;
118
+ }
119
+ // "(none)" is a real outcome, not a missing value: declining to delegate
120
+ // work that should have been delegated is a different failure from picking
121
+ // the wrong agent, and collapsing them would hide which one is happening.
122
+ const actual = result.actual ?? "(none)";
123
+ const key = `${result.expected} ${actual}`;
124
+ const entry = confusion.get(key) ?? { expected: result.expected, actual, count: 0 };
125
+ entry.count++;
126
+ confusion.set(key, entry);
127
+ }
128
+ const perAgent = candidates
129
+ .map((candidate) => {
130
+ const expected = expectedCounts.get(candidate.name) ?? 0;
131
+ const chosen = chosenCounts.get(candidate.name) ?? 0;
132
+ return { agent: candidate.name, expected, chosen, recall: expected > 0 ? chosen / expected : 0 };
133
+ })
134
+ .sort((a, b) => a.recall - b.recall);
135
+ return {
136
+ corpusHash: outcome.record.corpusHash,
137
+ agents: candidates.map((c) => c.name),
138
+ caseCount: outcome.record.caseCount,
139
+ accuracy: delegated > 0 ? delegatedCorrect / delegated : undefined,
140
+ inlineAccuracy: inline > 0 ? inlineCorrect / inline : undefined,
141
+ confusion: [...confusion.values()].sort((a, b) => b.count - a.count),
142
+ perAgent,
143
+ };
144
+ }
145
+ /** Score the roster against a gold set. Never throws; a missing model is `not-run`. */
146
+ export async function runAgentSelectionEval(candidates, cases, judge) {
147
+ if (cases && cases.length > 0) {
148
+ const problems = validateAgentCases(candidates, cases);
149
+ if (problems.length > 0)
150
+ return { status: "not-run", reason: `invalid gold set: ${problems.join("; ")}` };
151
+ }
152
+ const outcome = await runTriggerEval("agent-selection", candidates, cases, judge);
153
+ if (outcome.status === "not-run")
154
+ return outcome;
155
+ return {
156
+ status: "ran",
157
+ report: {
158
+ ...summarizeAgentSelection(outcome, candidates),
159
+ // The shared harness hashes its own way; restate it over exactly what
160
+ // this eval judged so two reports are comparable on their own terms.
161
+ corpusHash: hashCorpus(candidates, cases ?? []),
162
+ },
163
+ outcome,
164
+ };
165
+ }
166
+ /** Human-readable report for the CLI. */
167
+ export function formatAgentSelectionReport(report) {
168
+ const pct = (v) => (v === undefined ? "n/a" : `${Math.round(v * 100)}%`);
169
+ const lines = [
170
+ `corpus ${report.corpusHash} - ${report.caseCount} case(s) over ${report.agents.length} agent(s): ${report.agents.join(", ")}`,
171
+ `delegation accuracy ${pct(report.accuracy)} inline accuracy ${pct(report.inlineAccuracy)}`,
172
+ "",
173
+ "per agent (lowest recall first):",
174
+ ];
175
+ for (const row of report.perAgent) {
176
+ lines.push(` ${row.agent.padEnd(18)} ${row.chosen}/${row.expected} chosen (${pct(row.recall)})`);
177
+ }
178
+ if (report.confusion.length > 0) {
179
+ lines.push("", "confusions (expected then actual):");
180
+ for (const entry of report.confusion) {
181
+ lines.push(` ${entry.expected} => ${entry.actual} x${entry.count}`);
182
+ }
183
+ }
184
+ return lines.join("\n");
185
+ }
186
+ //# sourceMappingURL=agent-selection-eval.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-selection-eval.js","sourceRoot":"","sources":["../../src/core/agent-selection-eval.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,iBAAiB,EAAE,yBAAyB,EAAE,MAAM,qBAAqB,CAAC;AACnF,OAAO,EACN,cAAc,GAKd,MAAM,sCAAsC,CAAC;AAuB9C;;;;;;;;GAQG;AACH,MAAM,UAAU,eAAe,CAAC,GAAG,GAAW,OAAO,CAAC,GAAG,EAAE,EAAsB;IAChF,MAAM,QAAQ,GAAG,iBAAiB,CAAC,EAAE,GAAG,EAAE,eAAe,EAAE,IAAI,EAAE,aAAa,EAAE,KAAK,EAAE,CAAC,CAAC;IACzF,OAAO,QAAQ;SACb,IAAI,EAAE;SACN,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;QAChB,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,WAAW,EAAE,yBAAyB,CAAC,KAAK,CAAC,WAAW,IAAI,EAAE,CAAC;QAC/D,GAAG,EAAE,IAAI;KACT,CAAC,CAAC;SACF,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;AAAA,CAC/C;AAED,yFAAyF;AACzF,MAAM,UAAU,cAAc,CAAC,IAAY,EAA6B;IACvE,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,SAAS,CAAC;IACxC,IAAI,CAAC;QACJ,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAwB,CAAC;QAC3E,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC;YAAE,OAAO,SAAS,CAAC;QAChD,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,MAAM,CAC7B,CAAC,CAAC,EAAoB,EAAE,CACvB,CAAC,CAAC,CAAC;YACH,OAAO,CAAC,KAAK,QAAQ;YACrB,OAAQ,CAAiB,CAAC,MAAM,KAAK,QAAQ;YAC5C,CAAiB,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC;YAC3C,CAAE,CAAiB,CAAC,MAAM,KAAK,IAAI,IAAI,OAAQ,CAAiB,CAAC,MAAM,KAAK,QAAQ,CAAC,CACtF,CAAC;QACF,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;IAC7C,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,SAAS,CAAC;IAClB,CAAC;AAAA,CACD;AAED;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAAC,UAAuC,EAAE,KAA6B,EAAY;IACpH,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;IACrD,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,KAAK,MAAM,CAAC,CAAC,EAAE,QAAQ,CAAC,IAAI,KAAK,CAAC,OAAO,EAAE,EAAE,CAAC;QAC7C,IAAI,QAAQ,CAAC,MAAM,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;YAC7D,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,aAAa,QAAQ,CAAC,MAAM,+BAA+B,CAAC,CAAC;QACrF,CAAC;IACF,CAAC;IACD,OAAO,QAAQ,CAAC;AAAA,CAChB;AAED,SAAS,UAAU,CAAC,UAAuC,EAAE,KAA6B,EAAU;IACnG,MAAM,CAAC,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC;IAC/B,KAAK,MAAM,CAAC,IAAI,UAAU;QAAE,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,WAAW,GAAG,CAAC,CAAC;IACpE,KAAK,MAAM,CAAC,IAAI,KAAK;QAAE,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,MAAM,IAAI,EAAE,GAAG,CAAC,CAAC;IAClE,OAAO,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AAAA,CACpC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,uBAAuB,CACtC,OAAuD,EACvD,UAAuC,EAChB;IACvB,MAAM,SAAS,GAAG,IAAI,GAAG,EAA0B,CAAC;IACpD,MAAM,cAAc,GAAG,IAAI,GAAG,EAAkB,CAAC;IACjD,MAAM,YAAY,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC/C,IAAI,SAAS,GAAG,CAAC,CAAC;IAClB,IAAI,gBAAgB,GAAG,CAAC,CAAC;IACzB,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,IAAI,aAAa,GAAG,CAAC,CAAC;IAEtB,KAAK,MAAM,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QAC7C,IAAI,MAAM,CAAC,QAAQ,KAAK,IAAI,EAAE,CAAC;YAC9B,MAAM,EAAE,CAAC;YACT,IAAI,MAAM,CAAC,MAAM,KAAK,IAAI;gBAAE,aAAa,EAAE,CAAC;YAC5C,SAAS;QACV,CAAC;QACD,SAAS,EAAE,CAAC;QACZ,cAAc,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC,cAAc,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QACpF,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YACpB,gBAAgB,EAAE,CAAC;YACnB,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YAChF,SAAS;QACV,CAAC;QACD,yEAAyE;QACzE,2EAA2E;QAC3E,0EAA0E;QAC1E,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,IAAI,QAAQ,CAAC;QACzC,MAAM,GAAG,GAAG,GAAG,MAAM,CAAC,QAAQ,IAAI,MAAM,EAAE,CAAC;QAC3C,MAAM,KAAK,GAAG,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC;QACpF,KAAK,CAAC,KAAK,EAAE,CAAC;QACd,SAAS,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IAC3B,CAAC;IAED,MAAM,QAAQ,GAAG,UAAU;SACzB,GAAG,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC;QACnB,MAAM,QAAQ,GAAG,cAAc,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACzD,MAAM,MAAM,GAAG,YAAY,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACrD,OAAO,EAAE,KAAK,EAAE,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAAA,CACjG,CAAC;SACD,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC;IAEtC,OAAO;QACN,UAAU,EAAE,OAAO,CAAC,MAAM,CAAC,UAAU;QACrC,MAAM,EAAE,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;QACrC,SAAS,EAAE,OAAO,CAAC,MAAM,CAAC,SAAS;QACnC,QAAQ,EAAE,SAAS,GAAG,CAAC,CAAC,CAAC,CAAC,gBAAgB,GAAG,SAAS,CAAC,CAAC,CAAC,SAAS;QAClE,cAAc,EAAE,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,aAAa,GAAG,MAAM,CAAC,CAAC,CAAC,SAAS;QAC/D,SAAS,EAAE,CAAC,GAAG,SAAS,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC;QACpE,QAAQ;KACR,CAAC;AAAA,CACF;AAMD,uFAAuF;AACvF,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAC1C,UAAuC,EACvC,KAAyC,EACzC,KAA+B,EACE;IACjC,IAAI,KAAK,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC/B,MAAM,QAAQ,GAAG,kBAAkB,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;QACvD,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,qBAAqB,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC;IAC3G,CAAC;IAED,MAAM,OAAO,GAAG,MAAM,cAAc,CAAC,iBAAiB,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;IAClF,IAAI,OAAO,CAAC,MAAM,KAAK,SAAS;QAAE,OAAO,OAAO,CAAC;IAEjD,OAAO;QACN,MAAM,EAAE,KAAK;QACb,MAAM,EAAE;YACP,GAAG,uBAAuB,CAAC,OAAO,EAAE,UAAU,CAAC;YAC/C,sEAAsE;YACtE,qEAAqE;YACrE,UAAU,EAAE,UAAU,CAAC,UAAU,EAAE,KAAK,IAAI,EAAE,CAAC;SAC/C;QACD,OAAO;KACP,CAAC;AAAA,CACF;AAED,yCAAyC;AACzC,MAAM,UAAU,0BAA0B,CAAC,MAA4B,EAAU;IAChF,MAAM,GAAG,GAAG,CAAC,CAAqB,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC;IAC7F,MAAM,KAAK,GAAG;QACb,UAAU,MAAM,CAAC,UAAU,MAAM,MAAM,CAAC,SAAS,iBAAiB,MAAM,CAAC,MAAM,CAAC,MAAM,cAAc,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;QAC9H,uBAAuB,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,sBAAsB,GAAG,CAAC,MAAM,CAAC,cAAc,CAAC,EAAE;QAC7F,EAAE;QACF,kCAAkC;KAClC,CAAC;IACF,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;QACnC,KAAK,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,QAAQ,aAAa,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACpG,CAAC;IACD,IAAI,MAAM,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACjC,KAAK,CAAC,IAAI,CAAC,EAAE,EAAE,oCAAoC,CAAC,CAAC;QACrD,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC;YACtC,KAAK,CAAC,IAAI,CAAC,KAAK,KAAK,CAAC,QAAQ,OAAO,KAAK,CAAC,MAAM,OAAO,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC;QACxE,CAAC;IACF,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAAA,CACxB","sourcesContent":["/**\n * Does the agent roster steer a dispatch to the right agent?\n *\n * The `Task` tool asks the model to pick a `subagent_type` from\n * `<available_agents>`, and that block contains nothing but each agent's\n * summarized description. So agent selection is the same question G4 asks about\n * skills — \"given these descriptions and this situation, which one fires?\" —\n * and it reuses the same harness rather than growing a second one.\n *\n * What this exists to decide: `plan` and `explore` ship with the same tools, the\n * same isolation and the same `background` flag, differing only in model tier\n * and output contract — and `complexity` on the Task tool already expresses the\n * tier. Whether they are two agents or one is a question about whether the model\n * can actually tell them apart from their descriptions, which is measurable and\n * was previously being argued from intuition.\n *\n * The candidates use `summarizeAgentDescription`, not the raw frontmatter: the\n * summary is what the system prompt actually emits, and evaluating the full\n * description would score text the model never sees.\n */\n\nimport { createHash } from \"node:crypto\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { loadAgentRegistry, summarizeAgentDescription } from \"./agent-registry.js\";\nimport {\n\trunTriggerEval,\n\ttype TriggerCandidate,\n\ttype TriggerCase,\n\ttype TriggerEvalOutcome,\n\ttype TriggerJudge,\n} from \"./extensions/plugins/trigger-eval.js\";\n\n/** How often one agent was chosen where another was expected. */\nexport interface ConfusionEntry {\n\texpected: string;\n\tactual: string;\n\tcount: number;\n}\n\nexport interface AgentSelectionReport {\n\tcorpusHash: string;\n\tagents: string[];\n\tcaseCount: number;\n\t/** Cases where the expected agent was chosen, over cases expecting any agent. */\n\taccuracy?: number;\n\t/** Cases correctly left to the parent, over cases expecting no delegation. */\n\tinlineAccuracy?: number;\n\t/** Every wrong pick, most frequent first. The pairs here are the finding. */\n\tconfusion: ConfusionEntry[];\n\t/** Per-agent recall: chosen / expected. An agent nobody picks is dead weight. */\n\tperAgent: Array<{ agent: string; expected: number; chosen: number; recall: number }>;\n}\n\n/**\n * The built-in agents as judge candidates, described exactly as the system\n * prompt describes them.\n *\n * `own: true` for all of them: unlike a plugin eval there is no foreign roster\n * to discriminate against, so every case is scored against the same closed set.\n * The `expect: null` cases carry the discriminative half instead — they ask\n * whether the model declines to delegate work it should keep.\n */\nexport function agentCandidates(cwd: string = process.cwd()): TriggerCandidate[] {\n\tconst registry = loadAgentRegistry({ cwd, includeBuiltins: true, includeClaude: false });\n\treturn registry\n\t\t.list()\n\t\t.map((agent) => ({\n\t\t\tname: agent.name,\n\t\t\tdescription: summarizeAgentDescription(agent.description ?? \"\"),\n\t\t\town: true,\n\t\t}))\n\t\t.sort((a, b) => a.name.localeCompare(b.name));\n}\n\n/** Load a gold set: `{ \"cases\": [{ \"prompt\": \"...\", \"expect\": \"explore\" | null }] }`. */\nexport function loadAgentCases(file: string): TriggerCase[] | undefined {\n\tif (!existsSync(file)) return undefined;\n\ttry {\n\t\tconst raw = JSON.parse(readFileSync(file, \"utf-8\")) as { cases?: unknown };\n\t\tif (!Array.isArray(raw.cases)) return undefined;\n\t\tconst cases = raw.cases.filter(\n\t\t\t(c): c is TriggerCase =>\n\t\t\t\t!!c &&\n\t\t\t\ttypeof c === \"object\" &&\n\t\t\t\ttypeof (c as TriggerCase).prompt === \"string\" &&\n\t\t\t\t(c as TriggerCase).prompt.trim().length > 0 &&\n\t\t\t\t((c as TriggerCase).expect === null || typeof (c as TriggerCase).expect === \"string\"),\n\t\t);\n\t\treturn cases.length > 0 ? cases : undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/**\n * Every expected agent in the gold set must exist in the roster.\n *\n * A typo'd or removed agent name would otherwise score as a permanent miss and\n * read as a description problem, which is the most expensive way to be wrong\n * about an eval.\n */\nexport function validateAgentCases(candidates: readonly TriggerCandidate[], cases: readonly TriggerCase[]): string[] {\n\tconst known = new Set(candidates.map((c) => c.name));\n\tconst problems: string[] = [];\n\tfor (const [i, testCase] of cases.entries()) {\n\t\tif (testCase.expect !== null && !known.has(testCase.expect)) {\n\t\t\tproblems.push(`case ${i} expects \"${testCase.expect}\", which is not in the roster`);\n\t\t}\n\t}\n\treturn problems;\n}\n\nfunction hashCorpus(candidates: readonly TriggerCandidate[], cases: readonly TriggerCase[]): string {\n\tconst h = createHash(\"sha256\");\n\tfor (const c of candidates) h.update(`${c.name} ${c.description} `);\n\tfor (const c of cases) h.update(`${c.prompt} ${c.expect ?? \"\"} `);\n\treturn h.digest(\"hex\").slice(0, 16);\n}\n\n/**\n * Turn a scored run into the report that answers the design question.\n *\n * `runTriggerEval`'s recall/specificity are the right numbers for a plugin\n * defending itself against a foreign roster. Here the roster is closed, so the\n * useful shape is a confusion matrix: which agent loses to which, and how often.\n * \"explore and plan are interchangeable\" is a claim about one cell.\n */\nexport function summarizeAgentSelection(\n\toutcome: Extract<TriggerEvalOutcome, { status: \"ran\" }>,\n\tcandidates: readonly TriggerCandidate[],\n): AgentSelectionReport {\n\tconst confusion = new Map<string, ConfusionEntry>();\n\tconst expectedCounts = new Map<string, number>();\n\tconst chosenCounts = new Map<string, number>();\n\tlet delegated = 0;\n\tlet delegatedCorrect = 0;\n\tlet inline = 0;\n\tlet inlineCorrect = 0;\n\n\tfor (const result of outcome.record.results) {\n\t\tif (result.expected === null) {\n\t\t\tinline++;\n\t\t\tif (result.actual === null) inlineCorrect++;\n\t\t\tcontinue;\n\t\t}\n\t\tdelegated++;\n\t\texpectedCounts.set(result.expected, (expectedCounts.get(result.expected) ?? 0) + 1);\n\t\tif (result.correct) {\n\t\t\tdelegatedCorrect++;\n\t\t\tchosenCounts.set(result.expected, (chosenCounts.get(result.expected) ?? 0) + 1);\n\t\t\tcontinue;\n\t\t}\n\t\t// \"(none)\" is a real outcome, not a missing value: declining to delegate\n\t\t// work that should have been delegated is a different failure from picking\n\t\t// the wrong agent, and collapsing them would hide which one is happening.\n\t\tconst actual = result.actual ?? \"(none)\";\n\t\tconst key = `${result.expected} ${actual}`;\n\t\tconst entry = confusion.get(key) ?? { expected: result.expected, actual, count: 0 };\n\t\tentry.count++;\n\t\tconfusion.set(key, entry);\n\t}\n\n\tconst perAgent = candidates\n\t\t.map((candidate) => {\n\t\t\tconst expected = expectedCounts.get(candidate.name) ?? 0;\n\t\t\tconst chosen = chosenCounts.get(candidate.name) ?? 0;\n\t\t\treturn { agent: candidate.name, expected, chosen, recall: expected > 0 ? chosen / expected : 0 };\n\t\t})\n\t\t.sort((a, b) => a.recall - b.recall);\n\n\treturn {\n\t\tcorpusHash: outcome.record.corpusHash,\n\t\tagents: candidates.map((c) => c.name),\n\t\tcaseCount: outcome.record.caseCount,\n\t\taccuracy: delegated > 0 ? delegatedCorrect / delegated : undefined,\n\t\tinlineAccuracy: inline > 0 ? inlineCorrect / inline : undefined,\n\t\tconfusion: [...confusion.values()].sort((a, b) => b.count - a.count),\n\t\tperAgent,\n\t};\n}\n\nexport type AgentSelectionOutcome =\n\t| { status: \"not-run\"; reason: string }\n\t| { status: \"ran\"; report: AgentSelectionReport; outcome: Extract<TriggerEvalOutcome, { status: \"ran\" }> };\n\n/** Score the roster against a gold set. Never throws; a missing model is `not-run`. */\nexport async function runAgentSelectionEval(\n\tcandidates: readonly TriggerCandidate[],\n\tcases: readonly TriggerCase[] | undefined,\n\tjudge: TriggerJudge | undefined,\n): Promise<AgentSelectionOutcome> {\n\tif (cases && cases.length > 0) {\n\t\tconst problems = validateAgentCases(candidates, cases);\n\t\tif (problems.length > 0) return { status: \"not-run\", reason: `invalid gold set: ${problems.join(\"; \")}` };\n\t}\n\n\tconst outcome = await runTriggerEval(\"agent-selection\", candidates, cases, judge);\n\tif (outcome.status === \"not-run\") return outcome;\n\n\treturn {\n\t\tstatus: \"ran\",\n\t\treport: {\n\t\t\t...summarizeAgentSelection(outcome, candidates),\n\t\t\t// The shared harness hashes its own way; restate it over exactly what\n\t\t\t// this eval judged so two reports are comparable on their own terms.\n\t\t\tcorpusHash: hashCorpus(candidates, cases ?? []),\n\t\t},\n\t\toutcome,\n\t};\n}\n\n/** Human-readable report for the CLI. */\nexport function formatAgentSelectionReport(report: AgentSelectionReport): string {\n\tconst pct = (v: number | undefined) => (v === undefined ? \"n/a\" : `${Math.round(v * 100)}%`);\n\tconst lines = [\n\t\t`corpus ${report.corpusHash} - ${report.caseCount} case(s) over ${report.agents.length} agent(s): ${report.agents.join(\", \")}`,\n\t\t`delegation accuracy ${pct(report.accuracy)} inline accuracy ${pct(report.inlineAccuracy)}`,\n\t\t\"\",\n\t\t\"per agent (lowest recall first):\",\n\t];\n\tfor (const row of report.perAgent) {\n\t\tlines.push(` ${row.agent.padEnd(18)} ${row.chosen}/${row.expected} chosen (${pct(row.recall)})`);\n\t}\n\tif (report.confusion.length > 0) {\n\t\tlines.push(\"\", \"confusions (expected then actual):\");\n\t\tfor (const entry of report.confusion) {\n\t\t\tlines.push(` ${entry.expected} => ${entry.actual} x${entry.count}`);\n\t\t}\n\t}\n\treturn lines.join(\"\\n\");\n}\n"]}
@@ -26,6 +26,13 @@ export type CreateAgentSessionRuntimeFactory = (options: {
26
26
  sessionManager: SessionManager;
27
27
  sessionStartEvent?: SessionStartEvent;
28
28
  }) => Promise<CreateAgentSessionRuntimeResult>;
29
+ /**
30
+ * Thrown when `/cd` is pointed at something that is not a usable directory.
31
+ */
32
+ export declare class ChangeDirectoryError extends Error {
33
+ readonly path: string;
34
+ constructor(message: string, path: string);
35
+ }
29
36
  /**
30
37
  * Thrown when /import references a JSONL file path that does not exist.
31
38
  */
@@ -82,6 +89,27 @@ export declare class AgentSessionRuntime {
82
89
  }): Promise<{
83
90
  cancelled: boolean;
84
91
  }>;
92
+ /**
93
+ * Move the whole runtime to another directory without leaving the process.
94
+ *
95
+ * Everything cwd-bound is rebuilt for the new root — tools, context files
96
+ * (AGENTS.md and friends), project settings, extensions, skills, agents, the
97
+ * MCP server set — which is exactly why this cannot just reassign a string:
98
+ * those services are constructed per cwd and a half-moved runtime would run
99
+ * the new repo's code against the old repo's rules.
100
+ *
101
+ * Sessions are stored per project, so the move starts a fresh session in the
102
+ * target directory rather than dragging the current transcript across. The
103
+ * old session is already persisted; `/resume` in the old directory reopens
104
+ * it. A session directory the user pinned explicitly (`--session-dir`, the
105
+ * env var, or the setting) is *not* cwd-derived and is carried over as-is.
106
+ *
107
+ * @throws {ChangeDirectoryError} When the target is missing or not a directory.
108
+ */
109
+ changeDirectory(targetCwd: string): Promise<{
110
+ cancelled: boolean;
111
+ cwd: string;
112
+ }>;
85
113
  fork(entryId: string, options?: {
86
114
  position?: "before" | "at";
87
115
  withSession?: (ctx: ReplacedSessionContext) => Promise<void>;