cargo-hauler 0.5.0 → 0.6.0

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 (75) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +127 -26
  3. package/artifact/agent-bundle.hooks.json +1 -1
  4. package/artifact/agent-bundle.manifest.json +1 -1
  5. package/artifact/claude/.claude-plugin/marketplace.json +1 -1
  6. package/artifact/claude/.claude-plugin/plugin.json +1 -1
  7. package/artifact/claude/INSTALL.md +1 -1
  8. package/artifact/claude/bin/cargo-hauler-flight.mjs +631 -40
  9. package/artifact/claude/bin/cargo-hauler.mjs +2046 -319
  10. package/artifact/claude/hooks/after-tool-shell-after-264de610.mjs +1162 -0
  11. package/artifact/claude/hooks/before-tool-shell-before-9212219e.mjs +4021 -0
  12. package/artifact/claude/hooks/event-route-session-start.mjs +2 -2
  13. package/artifact/claude/hooks/event-route-stop.mjs +2 -2
  14. package/artifact/claude/hooks/hooks-flight.mjs +166 -3426
  15. package/artifact/claude/hooks/hooks.json +1 -1
  16. package/artifact/claude/mcp/mcp-hauler-2b8242f9-flight.mjs +766 -3382
  17. package/artifact/claude/mcp/mcp-hauler-2b8242f9.mjs +634 -43
  18. package/artifact/claude/mcp-apps/dashboard.html +54 -6
  19. package/artifact/claude/scripts/hauler.mjs +1891 -335
  20. package/artifact/claude/skills/cargo-hauler/SKILL.md +19 -9
  21. package/artifact/claude/skills/hauler-dashboard/SKILL.md +4 -2
  22. package/artifact/codex/.codex-plugin/plugin.json +1 -1
  23. package/artifact/codex/INSTALL.md +1 -1
  24. package/artifact/codex/bin/cargo-hauler-flight.mjs +631 -40
  25. package/artifact/codex/bin/cargo-hauler.mjs +2046 -319
  26. package/artifact/codex/hooks/after-tool-shell-after-264de610.mjs +1167 -0
  27. package/artifact/codex/hooks/before-tool-shell-before-9212219e.mjs +4026 -0
  28. package/artifact/codex/hooks/event-route-session-start.mjs +2 -2
  29. package/artifact/codex/hooks/event-route-stop.mjs +2 -2
  30. package/artifact/codex/hooks/hooks-flight.mjs +166 -3426
  31. package/artifact/codex/hooks/hooks.json +1 -1
  32. package/artifact/codex/mcp/mcp-hauler-2b8242f9-flight.mjs +766 -3382
  33. package/artifact/codex/mcp/mcp-hauler-2b8242f9.mjs +634 -43
  34. package/artifact/codex/mcp-apps/dashboard.html +54 -6
  35. package/artifact/codex/scripts/hauler.mjs +1891 -335
  36. package/artifact/codex/skills/cargo-hauler/SKILL.md +19 -9
  37. package/artifact/codex/skills/hauler-dashboard/SKILL.md +4 -2
  38. package/artifact/cursor/.cursor-plugin/plugin.json +1 -1
  39. package/artifact/cursor/INSTALL.md +1 -1
  40. package/artifact/cursor/bin/cargo-hauler-flight.mjs +631 -40
  41. package/artifact/cursor/bin/cargo-hauler.mjs +2046 -319
  42. package/artifact/cursor/hooks/after-tool-shell-after-264de610.mjs +1204 -0
  43. package/artifact/cursor/hooks/before-tool-shell-before-9212219e.mjs +4063 -0
  44. package/artifact/cursor/hooks/event-route-session-start.mjs +2 -2
  45. package/artifact/cursor/hooks/event-route-stop.mjs +2 -2
  46. package/artifact/cursor/hooks/hooks-flight.mjs +166 -3426
  47. package/artifact/cursor/hooks/hooks.json +1 -1
  48. package/artifact/cursor/install.mjs +1 -1
  49. package/artifact/cursor/mcp/mcp-hauler-2b8242f9-flight.mjs +766 -3382
  50. package/artifact/cursor/mcp/mcp-hauler-2b8242f9.mjs +634 -43
  51. package/artifact/cursor/mcp-apps/dashboard.html +54 -6
  52. package/artifact/cursor/scripts/hauler.mjs +1891 -335
  53. package/artifact/cursor/skills/cargo-hauler/SKILL.md +19 -9
  54. package/artifact/cursor/skills/hauler-dashboard/SKILL.md +4 -2
  55. package/artifact/portable/INSTALL.md +1 -1
  56. package/artifact/portable/bin/cargo-hauler-flight.mjs +631 -40
  57. package/artifact/portable/bin/cargo-hauler.mjs +2046 -319
  58. package/artifact/portable/install.mjs +1 -1
  59. package/artifact/portable/mcp/mcp-hauler-2b8242f9-flight.mjs +632 -41
  60. package/artifact/portable/mcp/mcp-hauler-2b8242f9.mjs +633 -42
  61. package/artifact/portable/mcp-apps/dashboard.html +54 -6
  62. package/artifact/portable/plugin.json +1 -1
  63. package/artifact/portable/scripts/hauler.mjs +1891 -335
  64. package/artifact/portable/skills/cargo-hauler/SKILL.md +19 -9
  65. package/artifact/portable/skills/hauler-dashboard/SKILL.md +4 -2
  66. package/dist/bin/cargo-hauler-flight.mjs +631 -40
  67. package/dist/bin/cargo-hauler.js +2046 -319
  68. package/dist/bin/hauler.js +1891 -335
  69. package/package.json +1 -1
  70. package/artifact/claude/hooks/event-route-tool-after.mjs +0 -106595
  71. package/artifact/claude/hooks/event-route-tool-before.mjs +0 -106595
  72. package/artifact/codex/hooks/event-route-tool-after.mjs +0 -106595
  73. package/artifact/codex/hooks/event-route-tool-before.mjs +0 -106595
  74. package/artifact/cursor/hooks/event-route-tool-after.mjs +0 -106595
  75. package/artifact/cursor/hooks/event-route-tool-before.mjs +0 -106595
package/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - cfb8d3c: Make coverage attachment fire in practice, say why it did not, and let a compile-only `cargo test --no-run` ride a running `cargo test`.
8
+
9
+ - A `test --no-run` / `bench --no-run` submitted while a `test` / `bench` with the same packages, target selection, features, and profile is in flight in the lane attaches to it and is released — `done`, exit 0, with the leader's compile time so far as its saved compute — the moment the leader prints Cargo's `Finished` line, while the leader goes on running its tests. The leader's test filters, `--test-threads`, and `--no-fail-fast` do not disqualify the rider; a `--lib` rider never attaches to a `--test foo` leader, or a named-target rider to a default-target one. A leader whose build fails requeues the rider through the existing failed-stronger path; a leader that never reports its build finished (`--quiet`, `CARGO_HAULER_OVERLAP_EXECUTION=0`) releases it at settlement. A rider that arrives after the leader's build already finished is refused (`leader-build-finished`) and runs its own, mostly fresh, cargo at once. Both rows carry `buildFinishedAtMs`. (#88)
10
+ - Coverage gates widened where safe: flags the daemon does not model (`--locked`, `--offline`, `--message-format`, …) and arguments after `--` disqualify a `check` riding a `build` / `check` only when they differ between the two requests; `check --tests` rides `build --tests` / `build --all-targets`; a `--bin X` / `--example X` rider rides a `--bins` / `--examples` leader. `check` still never rides `clippy` (a lint failure would misreport the check), a `-p` rider is still not proven by a leader compiling its unnamed default package set, and `--tests` does not stand in for a named `--test X`. (#89)
11
+ - Every refused attachment is logged at debug level (`attach rejected`) with the gate — `subcommand`, `opaque-arguments`, `passthrough`, `compile-surface`, `packages`, `targets`, `channels`, `leader-build-finished` — a one-line detail, and both tickets; the status report counts one refusal per request under `metrics.attach_rejections`, keyed by the nearest gate any leader reached (`hauler status --json`, `hauler_status`). (#89)
12
+ - d5ae3e9: Show what the hauler costs, not only what it saves: queue wait vs run time per metrics window, the compile/execute split per command, and kache store pressure. (#92)
13
+
14
+ - Each metrics window (`1h`, `24h`, `all`) now reports total queue wait against total run time for leaders, and splits the wait by cause: *lane-bound* (a same-lane leader was still compiling — before its `Finished` line or exit), *permit-bound* (every admission permit was held and no same-lane compile was to blame), and *other* (admission holds, `--after` prerequisites, scheduling latency). The classification is a pure sweep over ledger rows, run against the daemon's current permit count; the tile states that assumption, since runs admitted under an earlier cap are classified against today's.
15
+ - With `buildFinishedAtMs` on the row, the by-command split adds compile vs execution p50s for `test`/`run`/`bench` leaders, and the window reports the lane time the execution-phase hand-back released. Pure compiles have no split and say nothing.
16
+ - The kache panel (`hauler status`, `hauler_status`, and the dashboard) surfaces store pressure: blob bytes recorded in the index against `local_max_size` (from `KACHE_MAX_SIZE` or kache's `config.toml`; "limit unknown" names why when it cannot be read), the last GC from `gc_stats.json` — when, how long, what it evicted — with any `gc: skipping eviction` warnings matched from kache's `auto-gc.log`/`daemon.log` during that run, and `key_ms` mean/p95 over the events tail. Warnings appear when the store is over its limit or the last GC declined or skipped evictions. Missing or unparsable files render as unavailable with their reason, never as an empty store.
17
+ - Protocol additions are optional fields (`metrics.windows[].waitSplit`, `handBack`, `runTotalMs`, `waitTotalMs`, `bySubcommand[].phases`, `kache.pressure`); clients reading an older daemon see the tiles as unavailable rather than as zeros.
18
+ - ec9bf65: Fold more queued work into one Cargo run: compile batches with a `--` trailer, and test runs whose filters differ.
19
+
20
+ - `cargo build` / `check` / `clippy` requests with the same arguments after `--` now batch: `cargo clippy -p a -- -D warnings` and `cargo clippy -p b -- -D warnings` run as `cargo clippy -p a -p b -- -D warnings`, the trailer once. Such runs are demultiplexed like any other compile (the `--message-format` rewrite goes before the `--`), so under `-D warnings` a participant whose own units compiled cleanly is released as done when another participant's warnings fail the composite; the rest rerun alone. Trailers that differ, or a trailer on one side only, still keep the runs apart, as do differing `--all-targets` / `--tests` / `--features`. (#86)
21
+ - `cargo test` requests naming different packages fold even when their bare name filters differ: `cargo test -p a -- f1` and `cargo test -p b -- f2` run as `cargo test -p a -p b --no-fail-fast -- f1 f2`, the union of packages with the union of filters. An identical set of `--test-threads=N`, `--nocapture`, `--quiet` (or `-q`) after `--` folds too, carried once from the leader; a mismatched set does not, and `--exact`, `--skip`, `--ignored`, `--include-ignored`, `--list`, `--format`, `--logfile`, or any other harness flag keeps a run out of composites. `--lib` runs fold like `--test NAME` runs; the target selection still has to match. Requests naming the same packages still need the same filters, and unfiltered runs fold only with unfiltered runs. A folded participant inherits a composite failure only when it asked for every package and every filter the composite ran; otherwise it reruns alone. (#87)
22
+ - c27662d: Estimate compile and execute separately, and keep a realistic queue ETA when a live head overruns.
23
+
24
+ `hauler status` reports `compileEstimateMs`, `executeEstimateMs`, and the current `phase` on a ticket. Followers waiting on a `test`/`nextest`/`bench`/`run` head see only the remaining execution time in `queue.waitEtaMs` (zero once overlap has handed the lane back), and queued tickets ahead of them count only their compile when overlap will hand the lane back. A head that has passed `CARGO_HAULER_STALL_ESTIMATE_FACTOR` times its estimate but is still burning CPU or printing is flagged `estimateState: overrun` with the intent's p90 (`p90Ms`), and its followers see `queue.headEstimateState: overrun`; its remaining time is re-estimated from that p90 — never less than one more estimate's worth — instead of contributing zero to the queue. A silent no-CPU head stays `stalled`. Filtered `--test <name>` runs with no history of their own borrow a same-package neighbor's timing before falling back to the crate-wide prior.
25
+
26
+ ### Patch Changes
27
+
28
+ - f01833d: Cut what the hooks cost every shell tool call. The `tool/before` and `tool/after` hooks are now declared handlers compiled into standalone entries (`hooks/before-tool-shell-before-*.mjs`, `hooks/after-tool-shell-after-*.mjs`) instead of event routes that loaded the whole rendering runtime before looking at the command. `tool/before` reads `tool_input.command` and answers `continue` at once when it has no `cargo`, `hauler`, or `conductor` word (word-boundary aware; `mycargo` and `CARGO_HOME=… ls` do not match, `~/.cargo/bin/cargo`, `cargo-hauler`, and `echo cargo` do — a doubtful command takes the full path). `tool/after` runs that test plus one bounded 500 ms socket ping (`session-completed` with the session's hook-state cursor, no Effect runtime), and loads the telemetry and notification code only when the command was cargo-related or the daemon reported finished tickets; with no daemon it exits quietly with no output. Rewrite, `cargo clean` denial, `hauler exec` pass-through, and completion notices are unchanged. Measured with `/usr/bin/time -v`, median of 5, Claude `ls -la` envelope: `tool/before` 0.05 s / 48 MB and `tool/after` 0.05 s / 49 MB, against 0.54 s / 143 MB and 0.56 s / 144 MB for the 0.5.0 wrappers without a shared runtime (0.10 s / 64 MB with one, per #90); a `cargo test -p foo` envelope is 0.05 s / 49 MB either way. (#90)
29
+
3
30
  ## 0.5.0
4
31
 
5
32
  ### Minor Changes
package/README.md CHANGED
@@ -130,6 +130,23 @@ followed by another statement, `elif`, and `function name { … }` — are left
130
130
  untouched and run as plain Cargo rather than risk emitting a changed
131
131
  command.
132
132
 
133
+ Both shell hooks run on every shell tool call, so they decide cheaply before
134
+ they do anything else. The `tool/before` entry reads `tool_input.command` and
135
+ answers `continue` for a command with no `cargo`, `hauler`, or `conductor`
136
+ word in it (word-boundary aware: `mycargo` and `CARGO_HOME=… ls` are not
137
+ matches, `~/.cargo/bin/cargo`, `cargo-hauler`, and `echo cargo` are — a false
138
+ negative would bypass the broker, so anything that looks like a mention takes
139
+ the full path). Only a matching command evaluates the parser and the rewrite.
140
+ The `tool/after` entry runs the token test and one bounded socket ping to the
141
+ daemon (the `session-completed` request with the session's hook-state cursor,
142
+ 500 ms, no Effect runtime); it loads the telemetry and notification code only
143
+ when the command was cargo-related or the daemon reported finished tickets. A
144
+ non-cargo call with nothing finished — or no daemon at all — exits with no
145
+ output. Measured with `/usr/bin/time -v` on a Claude `ls -la` envelope, the
146
+ compiled entries take ~50 ms wall and ~49 MB RSS, against ~100 ms and 64 MB
147
+ for the 0.4.8 event-route wrappers with a shared runtime available and
148
+ ~560 ms and 144 MB without one.
149
+
133
150
  A lane is keyed by workspace root and resolved target directory. It compiles
134
151
  one job at a time — Cargo's own build-directory lock would serialize them
135
152
  anyway. Once a `test`, `nextest`, `bench`, or `run` leader reports its build
@@ -149,23 +166,53 @@ Within a lane, the daemon can reduce work in three ways:
149
166
  1. **Identity attachment:** a byte-identical request attaches to an in-flight
150
167
  run.
151
168
  2. **Coverage attachment:** a narrower `check` attaches to a compatible
152
- `build` or `check` that covers its package and target scope.
169
+ `build` or `check` that covers its package and target scope (`--tests`
170
+ under `--tests` or `--all-targets` included); a compile-only
171
+ `test --no-run` / `bench --no-run` attaches to a running `test` / `bench`
172
+ with the same packages, target selection, features, and profile — the
173
+ leader's test filters and `--test-threads` select what runs, not what
174
+ compiles — and is released as soon as the leader prints its `Finished`
175
+ line rather than when its tests end. Flags the daemon does not model
176
+ (`--locked`, `--offline`, …) and arguments after `--` disqualify a pair
177
+ only when they differ between the two requests. `check` never rides
178
+ `clippy`: a lint failure would misreport the check.
153
179
  3. **Batch folding:** compatible queued compile or test requests are combined
154
180
  into one invocation.
155
181
 
182
+ A request that could not attach is logged at debug level with the gate that
183
+ refused it (`subcommand`, `opaque-arguments`, `passthrough`,
184
+ `compile-surface`, `packages`, `targets`, `channels`,
185
+ `leader-build-finished`) and both tickets, and `hauler status --json`
186
+ counts refusals per gate under `metrics.attach_rejections` (the nearest
187
+ miss when several leaders were considered).
188
+
156
189
  Each admitted leader starts one Cargo process. Identity, coverage, and folded
157
190
  batch requests share that process and receive its streamed output. A failed
158
191
  stronger compile does not satisfy a coverage or compile-batch attachment; the
159
192
  attached request returns to its lane unless its required compilation units were
160
- already observed as successful. Folded tests share the composite process and
161
- output. `cargo test` / `cargo nextest run` requests fold only when their test
162
- selection is identical — the same `--test` targets, name filters, arguments
163
- after `--`, and nextest filterset — so only the package set differs:
164
- `cargo test -p a` and `cargo test -p b` become
165
- `cargo test -p a -p b --no-fail-fast`. On success every participant shares
166
- the exit. When the composite fails, a participant inherits that failure only
167
- if it named every package the composite ran; otherwise the failing tests may
168
- belong to another participant's package, so it is requeued and runs alone
193
+ already observed as successful. Compile batches (`build`, `check`, `clippy`)
194
+ require the same target selection and features, and the same arguments after
195
+ `--`: `cargo clippy -p a -- -D warnings` and `cargo clippy -p b -- -D warnings`
196
+ become `cargo clippy -p a -p b -- -D warnings`, with the trailer once. Under
197
+ `-D warnings` another participant's warnings fail the composite; a participant
198
+ whose own units compiled cleanly is still released as done, the rest rerun
199
+ alone. Folded tests share the composite process and output. `cargo test`
200
+ requests fold when their `--test` / `--lib` selection and harness flags match
201
+ — `--test-threads=N`, `--nocapture`, and `--quiet` are the only flags a
202
+ composite carries, and only when every participant asked for the same set.
203
+ Requests naming different packages may also differ in bare name filters:
204
+ `cargo test -p a -- f1` and `cargo test -p b -- f2` become `cargo test -p a -p
205
+ b --no-fail-fast -- f1 f2`, a run over the union of packages with the union of
206
+ filters (a filter for one package may also match test names in another).
207
+ Requests naming the same packages share no compile and still need the same
208
+ filters; unfiltered runs fold only with unfiltered runs; `--exact`, `--skip`,
209
+ `--ignored`, `--include-ignored`, `--list`, `--format`, `--logfile`, or any
210
+ other harness flag keeps a run out of composites. `cargo nextest run`
211
+ requests fold only on an identical filterset. On success every participant
212
+ shares the exit. When the composite
213
+ fails, a participant inherits that failure only if it named every package and
214
+ every filter the composite ran; otherwise the failing tests may belong to
215
+ another participant's package or filter, so it is requeued and runs alone
169
216
  (cargo's test output does not attribute failures to packages). The leader
170
217
  keeps the composite exit, as compile-batch leaders do.
171
218
 
@@ -179,7 +226,9 @@ stdout.
179
226
 
180
227
  ![cargo-hauler request normalization, lane-local serialization, scheduling, admission, and concurrent Cargo processes](docs/media/how-it-works.png)
181
228
 
182
- The scheduler estimates run cost from per-intent EWMA history. It can also use
229
+ The scheduler estimates run cost from per-intent EWMA history, split into a
230
+ compile phase and an execution phase once the ledger has seen
231
+ `buildFinishedAtMs` on that intent. It can also use
183
232
  per-crate timing data from kache. Lower-cost work, requests with more attached
184
233
  callers, dependency-unblocking work, and recently edited packages receive a
185
234
  lower scheduling score. Waiting time lowers the score further so broad work
@@ -234,14 +283,14 @@ own `-j` flag or `CARGO_BUILD_JOBS` always wins over both.
234
283
 
235
284
  | Capability | Behavior |
236
285
  | --- | --- |
237
- | Work sharing | Identical requests attach, covered checks attach, and compatible queued compile or test requests fold. |
286
+ | Work sharing | Identical requests attach, covered checks and compile-only `test --no-run` requests attach, and compatible queued compile or test requests fold. |
238
287
  | Lane isolation | A workspace-root and target-directory pair is serialized independently from other lanes. |
239
288
  | Admission | Per-core load, Linux CPU PSI, Linux memory PSI and `MemAvailable`, macOS VM pressure, configured thresholds, and the global permit cap control new starts. |
240
289
  | Parallelism | One daemon-owned jobserver FIFO shared by every spawned Cargo; a per-run `CARGO_BUILD_JOBS` grant only when the FIFO could not be armed. |
241
- | Scheduling | EWMA estimates, optional kache priors, fan-out, dependency topology, recent edits, and request age determine lane order; `--after cc-N` holds a request until the named tickets settle. |
290
+ | Scheduling | Per-phase EWMA estimates (compile vs execute), optional kache priors, fan-out, dependency topology, recent edits, and request age determine lane order; `--after cc-N` holds a request until the named tickets settle. |
242
291
  | Persistence | Tickets, output tails, timings, outcomes, and savings are stored in SQLite; every leader run's whole combined output is kept on disk as `<state dir>/tickets/<ticket>.log`. |
243
292
  | Caller output and status | Output streams to attached callers; late callers receive buffered replay. After 30 seconds without output, the client emits a progress heartbeat every 15 seconds with lane queue position, the lane-head ticket, and an aggregate wait ETA. |
244
- | Wait escalation | A queued request waiting longer than the larger of twice its own estimate and ten minutes is flagged as delayed; running jobs silent for more than five minutes show a quiet-duration hint. A running job past three times its estimate whose process tree has burned no CPU and printed nothing for ten minutes is flagged `stalled`; only a stalled job whose submitting connection is gone is killed automatically. |
293
+ | Wait escalation | A queued request waiting longer than the larger of twice its own estimate and ten minutes is flagged as delayed; running jobs silent for more than five minutes show a quiet-duration hint. A live head past three times its estimate that is still burning CPU or printing is flagged `estimateState: overrun` (its followers see `queue.headEstimateState`) and contributes its history p90 remaining — never less than one more estimate's worth — to the queue ETA instead of zero. A running job past three times its estimate whose process tree has burned no CPU and printed nothing for ten minutes is flagged `stalled`; only a stalled job whose submitting connection is gone is killed automatically. |
245
294
  | Daemon status | `running`, `stopped`, or `unresponsive`: a socket that exists but does not answer within its budget is reported as unresponsive, never as stopped. |
246
295
 
247
296
  ### Tickets and long-running requests
@@ -294,7 +343,12 @@ macOS; other platforms do not detect stalls). A ticket is flagged `stalled`
294
343
  when its elapsed time exceeds `CARGO_HAULER_STALL_ESTIMATE_FACTOR` (3) times
295
344
  its estimate, the tree's CPU time has not changed for
296
345
  `CARGO_HAULER_STALL_IDLE_MS` (ten minutes), and it printed nothing in that
297
- window. `hauler status`, `hauler_status`, and the dashboard show `stalled`
346
+ window. A head that has crossed that estimate multiple but is still burning
347
+ CPU or printing is flagged `estimateState: overrun` instead (with the
348
+ intent's `p90Ms`, and `queue.headEstimateState: overrun` on the tickets
349
+ behind it): the queue ETA uses the intent's p90 remaining — never less than
350
+ one more estimate's worth — rather than zero, and agents can background it
351
+ without treating it as deadlocked. `hauler status`, `hauler_status`, and the dashboard show `stalled`
298
352
  with the idle duration; `hauler result` and `hauler_result` answer `ticket
299
353
  looks stalled (no CPU for Nm) — hauler kill cc-N`; `hauler await` heartbeats
300
354
  say the same. Riders of a stalled leader report the leader's stall and its
@@ -325,7 +379,7 @@ send (`outputPath`, `after`), so a plain version difference alone does not
325
379
  break `status`, `result`, or `await`; finish or `hauler kill` what is in
326
380
  flight before restarting if the work matters.
327
381
 
328
- The `tool/after` route checks the session's background tickets — `--bg`,
382
+ The `tool/after` hook checks the session's background tickets — `--bg`,
329
383
  `hauler_request`, and synchronous requests the client converted to a ticket
330
384
  — and, on the first tool call after one finishes, adds its result to the
331
385
  agent context. A foreground ticket streamed its exit to the shell the agent
@@ -392,6 +446,17 @@ reports the slowest crates by profile (`<KacheStats>`). Without that index,
392
446
  estimates come from the daemon's EWMA history. A missing or incompatible index
393
447
  is reported as unavailable and never rejects a request.
394
448
 
449
+ The same panel surfaces store pressure: blob bytes recorded in the index
450
+ against kache's `local_max_size` (from `KACHE_MAX_SIZE` or
451
+ `$XDG_CONFIG_HOME/kache/config.toml`; when neither applies the panel says
452
+ "limit unknown" and why), the last GC from `gc_stats.json` beside the index
453
+ — when it ran, how long it took, what it evicted, and any `gc: skipping
454
+ eviction` warnings from kache's `auto-gc.log`/`daemon.log` during that run —
455
+ and `key_ms` mean/p95 over the tail of the events sidecar. Warnings appear
456
+ when the store is over its limit or the last GC declined or skipped evictions;
457
+ a missing or unparsable file renders as unavailable with its reason, never as
458
+ an empty store.
459
+
395
460
  ![cargo-hauler dashboard kache timing panel](docs/media/dashboard-kache.png)
396
461
 
397
462
  ## Install
@@ -537,9 +602,14 @@ unset, the daemon reads kache's configured local store from
537
602
  document names the one in use (`state dir …` in the header; `stateRoot` in
538
603
  `--json`), so a `CARGO_HAULER_STATE_DIR` change is visible on the next
539
604
  command rather than discovered from an empty ledger.
540
- - Test sharing uses identity attachment or batch folding, never coverage.
541
- Folded `test` and `nextest` requests receive the composite output and exit
542
- code, so a failure may come from another package in the batch.
605
+ - Test execution is never shared by coverage: a `test`, `nextest`, or `bench`
606
+ that runs tests attaches only by identity or batch folding. Only a
607
+ compile-only `test --no-run` / `bench --no-run` rides a running `test` /
608
+ `bench`, and it is released by the leader's build alone. Folded `test` and
609
+ `nextest` requests receive the composite output, and a composite may run
610
+ more than one participant asked for (another package, another name
611
+ filter); its failure is inherited only by participants that asked for
612
+ everything it ran, the rest rerun alone.
543
613
  - The `cargo clean` guard probes the daemon for 250 ms. Active work denies
544
614
  the clean; an idle daemon brokers it; a daemon that accepts but does not
545
615
  answer in time is busy, so the clean is brokered and the lane serializes
@@ -558,9 +628,9 @@ unset, the daemon reads kache's configured local store from
558
628
  <summary><strong>How the app is built</strong> — agent-bundle application structure, testing, and development (click to expand)</summary>
559
629
 
560
630
  The plugin is an [agent-bundle](https://github.com/ScriptedAlchemy/agent-bundle)
561
- application: six MCP tools, a routed CLI, four hook routes, two skills, and a
562
- browser dashboard, all rendered from one component library through one shared
563
- layout. This section is for contributors; using cargo-hauler needs none of it.
631
+ application: six MCP tools, a routed CLI, two hook routes plus two declared
632
+ shell hooks, two skills, and a browser dashboard, all rendered from one
633
+ component library through one shared layout. This section is for contributors; using cargo-hauler needs none of it.
564
634
 
565
635
  ### Application structure
566
636
 
@@ -577,7 +647,8 @@ src/
577
647
  mcp/hauler/tools/*.tsx hauler_status, _log, _last, _await, _result, _request
578
648
  mcp/hauler/apps/dashboard.tsx the MCP App (ui://cargo-hauler/dashboard.html)
579
649
  cli/*.tsx, cli/daemon.ts the routed `cargo-hauler` CLI, same components
580
- events/{session/start,tool/before,tool/after,stop}.tsx hook routes
650
+ events/{session/start,stop}.tsx rendered hook routes
651
+ hooks/fast-path/ the declared tool/before and tool/after shell hooks
581
652
  skills/cargo-hauler/SKILL.md, skills/hauler-dashboard/SKILL.tsx
582
653
  scripts/hauler.ts the `hauler` process entry hooks rewrite cargo to
583
654
  daemon/, client/, hooks/, shim/, lib/ the broker and its libraries
@@ -608,6 +679,24 @@ through one layout, the way a page framework's `layout.tsx` wraps every page:
608
679
 
609
680
  Event routes are host protocol responses and are never wrapped.
610
681
 
682
+ #### The shell hooks (`src/hooks/fast-path/`)
683
+
684
+ `tool/before` and `tool/after` are not rendered routes. They are declared in
685
+ `agent-bundle.config.ts` under `hooks` as handler modules
686
+ (`shell-before.ts`, `shell-after.ts`), which the framework compiles into
687
+ standalone entries — `hooks/before-tool-shell-before-<hash>.mjs`,
688
+ `hooks/after-tool-shell-after-<hash>.mjs` — that carry no React, no Flight
689
+ worker, and no Effect. Each entry decides on the raw command first
690
+ (`tokens.ts`; `session-ping.ts` for the completion ping) and reaches the
691
+ rewrite (`before-shell.ts`) or the telemetry and notification code
692
+ (`after-shell.ts`) through a deliberate dynamic `import()`, the one place in
693
+ the codebase that imports lazily. The handler contract has no `allow`
694
+ outcome, so a fully brokered rewrite writes the host's own allow shape
695
+ (`allow-output.ts`) instead of `continue` + `updatedInput`, which would make
696
+ the host prompt for the rewrite. Everything else — `continue`, `deny` with a
697
+ reason, `additionalContext` — goes through the generated wrapper's
698
+ projection.
699
+
611
700
  #### The daemon provider (`src/providers/hauler-daemon.ts`)
612
701
 
613
702
  One request-context provider mounts `providers.haulerDaemon` for every tool,
@@ -691,9 +780,9 @@ the same filter as its `session` field). Results carry
691
780
  | `tool:hauler/hauler_request` · `cli:request` | submit a background request | `RequestDocument` |
692
781
  | `cli:daemon` | `run` / `start` / `stop` / `status` | plain JSON, exit code from the result |
693
782
  | `event:session/start` | new session | daemon state and the no-kill rule as context |
694
- | `event:tool/before` | shell tool about to run | rewrites `cargo …` to `hauler exec --session … --host … -- cargo …`; denies `cargo clean` during in-flight builds, brokers it while the daemon is too busy to answer |
695
- | `event:tool/after` | shell tool finished | injects finished background-ticket results once per session |
696
783
  | `event:stop` | agent stopping | holds the stop while a foreground ticket is pending (bounded, re-deniable) |
784
+ | `hooks.beforeTool` (`src/hooks/fast-path/shell-before.ts`) | shell tool about to run | `continue` without loading anything for a non-cargo command; otherwise rewrites `cargo …` to `hauler exec --session … --host … -- cargo …`, denies `cargo clean` during in-flight builds, brokers it while the daemon is too busy to answer |
785
+ | `hooks.afterTool` (`src/hooks/fast-path/shell-after.ts`) | shell tool finished | one bounded completion ping per call; injects finished background-ticket results once per session |
697
786
 
698
787
  #### Skills
699
788
 
@@ -711,6 +800,18 @@ render MCP Apps. It shows contention and admission, in-flight and queued
711
800
  work, metrics windows, optional kache data, lanes, and history, with a live
712
801
  output drawer per ticket.
713
802
 
803
+ Each metrics window also reports queue wait against run time for leaders,
804
+ with the wait split by cause: *lane-bound* (a same-lane leader was still
805
+ compiling — before its `Finished` line or exit), *permit-bound* (every
806
+ admission permit was held and no same-lane compile was to blame), and *other*
807
+ (admission holds, `--after` prerequisites, scheduling latency). The
808
+ classification is a pure sweep over ledger rows (`src/daemon/wait-split.ts`)
809
+ run once per status refresh against the daemon's current permit count, which
810
+ the tile states; runs admitted under an earlier cap are classified against
811
+ today's. With `buildFinishedAtMs` on the row, the by-command split adds
812
+ compile vs execution time for test/run/bench leaders and the window reports
813
+ the lane time the execution-phase hand-back released.
814
+
714
815
  ### Testing
715
816
 
716
817
  ```sh
@@ -722,7 +823,7 @@ artifact build, at the harness proof levels:
722
823
 
723
824
  | Level | Suite | What it proves |
724
825
  | --- | --- | --- |
725
- | route-unit | `routes`, `layout`, `streaming`, `events` | documents, shell metadata, Suspense fallbacks and settled values, lineage attribution, event decisions |
826
+ | route-unit | `routes`, `layout`, `streaming`, `events` | documents, shell metadata, Suspense fallbacks and settled values, lineage attribution, event decisions (the shell hooks are unit-tested in `tests/hook-fast-path.test.ts` and against their compiled entries in `tests/hooks-simulate.test.ts`) |
726
827
  | cli-dispatch | `cli-dispatch`, `layout` | argv through the routed CLI shell; Markdown wrapped by the shell, `--json` bare |
727
828
  | script-dispatch | `script-dispatch` | the `hauler` entry through its `main` envelope as its own process |
728
829
  | mcp-in-memory | `mcp-surface`, `layout` | tool names, `outputSchema`, the dashboard resource link, `_meta.hauler`, and a live fixture broker over the in-memory transport |
@@ -1 +1 @@
1
- {"hooks":[{"event":"sessionStart","id":"hook:event-route:session-start","name":"event-route-session-start","path":"claude/hooks/event-route-session-start.mjs","target":"claude","timeout":5},{"event":"stop","id":"hook:event-route:stop","name":"event-route-stop","path":"claude/hooks/event-route-stop.mjs","target":"claude","timeout":900},{"event":"afterTool","id":"hook:event-route:tool-after","name":"event-route-tool-after","path":"claude/hooks/event-route-tool-after.mjs","target":"claude","timeout":10},{"event":"beforeTool","id":"hook:event-route:tool-before","name":"event-route-tool-before","path":"claude/hooks/event-route-tool-before.mjs","target":"claude","timeout":10},{"event":"sessionStart","id":"hook:event-route:session-start","name":"event-route-session-start","path":"codex/hooks/event-route-session-start.mjs","target":"codex","timeout":5},{"event":"stop","id":"hook:event-route:stop","name":"event-route-stop","path":"codex/hooks/event-route-stop.mjs","target":"codex","timeout":900},{"event":"afterTool","id":"hook:event-route:tool-after","name":"event-route-tool-after","path":"codex/hooks/event-route-tool-after.mjs","target":"codex","timeout":10},{"event":"beforeTool","id":"hook:event-route:tool-before","name":"event-route-tool-before","path":"codex/hooks/event-route-tool-before.mjs","target":"codex","timeout":10},{"event":"sessionStart","id":"hook:event-route:session-start","name":"event-route-session-start","path":"cursor/hooks/event-route-session-start.mjs","target":"cursor","timeout":5},{"event":"stop","id":"hook:event-route:stop","name":"event-route-stop","path":"cursor/hooks/event-route-stop.mjs","target":"cursor","timeout":900},{"event":"afterTool","id":"hook:event-route:tool-after","name":"event-route-tool-after","path":"cursor/hooks/event-route-tool-after.mjs","target":"cursor","timeout":10},{"event":"beforeTool","id":"hook:event-route:tool-before","name":"event-route-tool-before","path":"cursor/hooks/event-route-tool-before.mjs","target":"cursor","timeout":10}]}
1
+ {"hooks":[{"event":"afterTool","id":"hook:after-tool:shell-after:264de610","name":"after-tool-shell-after-264de610","path":"claude/hooks/after-tool-shell-after-264de610.mjs","target":"claude","timeout":10},{"event":"beforeTool","id":"hook:before-tool:shell-before:9212219e","name":"before-tool-shell-before-9212219e","path":"claude/hooks/before-tool-shell-before-9212219e.mjs","target":"claude","timeout":10},{"event":"sessionStart","id":"hook:event-route:session-start","name":"event-route-session-start","path":"claude/hooks/event-route-session-start.mjs","target":"claude","timeout":5},{"event":"stop","id":"hook:event-route:stop","name":"event-route-stop","path":"claude/hooks/event-route-stop.mjs","target":"claude","timeout":900},{"event":"afterTool","id":"hook:after-tool:shell-after:264de610","name":"after-tool-shell-after-264de610","path":"codex/hooks/after-tool-shell-after-264de610.mjs","target":"codex","timeout":10},{"event":"beforeTool","id":"hook:before-tool:shell-before:9212219e","name":"before-tool-shell-before-9212219e","path":"codex/hooks/before-tool-shell-before-9212219e.mjs","target":"codex","timeout":10},{"event":"sessionStart","id":"hook:event-route:session-start","name":"event-route-session-start","path":"codex/hooks/event-route-session-start.mjs","target":"codex","timeout":5},{"event":"stop","id":"hook:event-route:stop","name":"event-route-stop","path":"codex/hooks/event-route-stop.mjs","target":"codex","timeout":900},{"event":"afterTool","id":"hook:after-tool:shell-after:264de610","name":"after-tool-shell-after-264de610","path":"cursor/hooks/after-tool-shell-after-264de610.mjs","target":"cursor","timeout":10},{"event":"beforeTool","id":"hook:before-tool:shell-before:9212219e","name":"before-tool-shell-before-9212219e","path":"cursor/hooks/before-tool-shell-before-9212219e.mjs","target":"cursor","timeout":10},{"event":"sessionStart","id":"hook:event-route:session-start","name":"event-route-session-start","path":"cursor/hooks/event-route-session-start.mjs","target":"cursor","timeout":5},{"event":"stop","id":"hook:event-route:stop","name":"event-route-stop","path":"cursor/hooks/event-route-stop.mjs","target":"cursor","timeout":900}]}