cargo-hauler 0.4.9 → 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 +38 -0
  2. package/README.md +142 -33
  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 +705 -66
  9. package/artifact/claude/bin/cargo-hauler.mjs +2240 -307
  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 +187 -3434
  15. package/artifact/claude/hooks/hooks.json +1 -1
  16. package/artifact/claude/mcp/mcp-hauler-2b8242f9-flight.mjs +840 -3408
  17. package/artifact/claude/mcp/mcp-hauler-2b8242f9.mjs +707 -68
  18. package/artifact/claude/mcp-apps/dashboard.html +54 -6
  19. package/artifact/claude/scripts/hauler.mjs +2077 -319
  20. package/artifact/claude/skills/cargo-hauler/SKILL.md +19 -9
  21. package/artifact/claude/skills/hauler-dashboard/SKILL.md +5 -3
  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 +705 -66
  25. package/artifact/codex/bin/cargo-hauler.mjs +2240 -307
  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 +187 -3434
  31. package/artifact/codex/hooks/hooks.json +1 -1
  32. package/artifact/codex/mcp/mcp-hauler-2b8242f9-flight.mjs +840 -3408
  33. package/artifact/codex/mcp/mcp-hauler-2b8242f9.mjs +707 -68
  34. package/artifact/codex/mcp-apps/dashboard.html +54 -6
  35. package/artifact/codex/scripts/hauler.mjs +2077 -319
  36. package/artifact/codex/skills/cargo-hauler/SKILL.md +19 -9
  37. package/artifact/codex/skills/hauler-dashboard/SKILL.md +5 -3
  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 +705 -66
  41. package/artifact/cursor/bin/cargo-hauler.mjs +2240 -307
  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 +187 -3434
  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 +840 -3408
  50. package/artifact/cursor/mcp/mcp-hauler-2b8242f9.mjs +707 -68
  51. package/artifact/cursor/mcp-apps/dashboard.html +54 -6
  52. package/artifact/cursor/scripts/hauler.mjs +2077 -319
  53. package/artifact/cursor/skills/cargo-hauler/SKILL.md +19 -9
  54. package/artifact/cursor/skills/hauler-dashboard/SKILL.md +5 -3
  55. package/artifact/portable/INSTALL.md +1 -1
  56. package/artifact/portable/bin/cargo-hauler-flight.mjs +705 -66
  57. package/artifact/portable/bin/cargo-hauler.mjs +2240 -307
  58. package/artifact/portable/install.mjs +1 -1
  59. package/artifact/portable/mcp/mcp-hauler-2b8242f9-flight.mjs +706 -67
  60. package/artifact/portable/mcp/mcp-hauler-2b8242f9.mjs +706 -67
  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 +2077 -319
  64. package/artifact/portable/skills/cargo-hauler/SKILL.md +19 -9
  65. package/artifact/portable/skills/hauler-dashboard/SKILL.md +5 -3
  66. package/dist/bin/cargo-hauler-flight.mjs +705 -66
  67. package/dist/bin/cargo-hauler.js +2240 -307
  68. package/dist/bin/hauler.js +2077 -319
  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,43 @@
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
+
30
+ ## 0.5.0
31
+
32
+ ### Minor Changes
33
+
34
+ - b80d9db: Cut the latency the hauler adds on a shared target directory, and stop charging lane wait to attaching.
35
+
36
+ - A lane now hands its slot to the next request as soon as a `test`, `nextest`, `bench`, or `run` leader prints Cargo's `Finished … target(s)` line: Cargo drops the build-directory lock there, so the next compile overlaps the leader's test run instead of waiting for it. The leader keeps its admission permit until it settles. `hauler status` and the dashboard list such leaders under the lane's `executing` tickets, and the ledger stamps `buildFinishedAtMs` on the leader and its riders. `CARGO_HAULER_OVERLAP_EXECUTION=0` restores strict one-process-per-lane.
37
+ - Default admission permits scale with the machine: one per eight cores, clamped between the previous five and sixteen. `CARGO_HAULER_MAX_CONCURRENT` still overrides.
38
+ - The cost model times edited and cached runs of an intent separately, and floors an edited intent that has only ever been timed cached at the crate compile priors, so a recompiling test run no longer reaches the head of the lane on a seconds-long estimate; unedited runs no longer teach the per-crate compile priors.
39
+ - Saved latency for riders is measured from the later of the rider's creation and the leader's start: time queued behind a leader that had not started is lane wait the rider would have paid alone, not a cost of attaching. Existing ledgers recompute the column once on open (`user_version` 2); the dashboard's all-time latency tile changes accordingly.
40
+
3
41
  ## 0.4.9
4
42
 
5
43
  ### Patch Changes
package/README.md CHANGED
@@ -130,35 +130,89 @@ 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
- A lane is keyed by workspace root and resolved target directory. It runs one
134
- job at a time. Different lanes may run concurrently after acquiring one of the
135
- global admission permits. `CARGO_HAULER_MAX_CONCURRENT` controls the
136
- machine-wide permit count and defaults to five Cargo processes. Attached
137
- requests (riders) do not hold permits; the admission meter counts permit
138
- holders and reports riders separately.
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
+
150
+ A lane is keyed by workspace root and resolved target directory. It compiles
151
+ one job at a time — Cargo's own build-directory lock would serialize them
152
+ anyway. Once a `test`, `nextest`, `bench`, or `run` leader reports its build
153
+ finished, Cargo has dropped that lock, so the lane hands its slot to the next
154
+ request and that compile overlaps the leader's test run
155
+ (`CARGO_HAULER_OVERLAP_EXECUTION=0` restores strict one-at-a-time). Different
156
+ lanes may run concurrently after acquiring one of the global admission
157
+ permits. `CARGO_HAULER_MAX_CONCURRENT` controls the machine-wide permit count;
158
+ the default is one permit per eight cores, clamped between five and sixteen,
159
+ since the shared jobserver already bounds compile parallelism and the pressure
160
+ arms defer admission under load. Attached requests (riders) do not hold
161
+ permits; the admission meter counts permit holders and reports riders
162
+ separately.
139
163
 
140
164
  Within a lane, the daemon can reduce work in three ways:
141
165
 
142
166
  1. **Identity attachment:** a byte-identical request attaches to an in-flight
143
167
  run.
144
168
  2. **Coverage attachment:** a narrower `check` attaches to a compatible
145
- `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.
146
179
  3. **Batch folding:** compatible queued compile or test requests are combined
147
180
  into one invocation.
148
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
+
149
189
  Each admitted leader starts one Cargo process. Identity, coverage, and folded
150
190
  batch requests share that process and receive its streamed output. A failed
151
191
  stronger compile does not satisfy a coverage or compile-batch attachment; the
152
192
  attached request returns to its lane unless its required compilation units were
153
- already observed as successful. Folded tests share the composite process and
154
- output. `cargo test` / `cargo nextest run` requests fold only when their test
155
- selection is identical the same `--test` targets, name filters, arguments
156
- after `--`, and nextest filterset so only the package set differs:
157
- `cargo test -p a` and `cargo test -p b` become
158
- `cargo test -p a -p b --no-fail-fast`. On success every participant shares
159
- the exit. When the composite fails, a participant inherits that failure only
160
- if it named every package the composite ran; otherwise the failing tests may
161
- 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
162
216
  (cargo's test output does not attribute failures to packages). The leader
163
217
  keeps the composite exit, as compile-batch leaders do.
164
218
 
@@ -172,7 +226,9 @@ stdout.
172
226
 
173
227
  ![cargo-hauler request normalization, lane-local serialization, scheduling, admission, and concurrent Cargo processes](docs/media/how-it-works.png)
174
228
 
175
- 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
176
232
  per-crate timing data from kache. Lower-cost work, requests with more attached
177
233
  callers, dependency-unblocking work, and recently edited packages receive a
178
234
  lower scheduling score. Waiting time lowers the score further so broad work
@@ -227,14 +283,14 @@ own `-j` flag or `CARGO_BUILD_JOBS` always wins over both.
227
283
 
228
284
  | Capability | Behavior |
229
285
  | --- | --- |
230
- | 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. |
231
287
  | Lane isolation | A workspace-root and target-directory pair is serialized independently from other lanes. |
232
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. |
233
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. |
234
- | 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. |
235
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`. |
236
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. |
237
- | 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. |
238
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. |
239
295
 
240
296
  ### Tickets and long-running requests
@@ -287,7 +343,12 @@ macOS; other platforms do not detect stalls). A ticket is flagged `stalled`
287
343
  when its elapsed time exceeds `CARGO_HAULER_STALL_ESTIMATE_FACTOR` (3) times
288
344
  its estimate, the tree's CPU time has not changed for
289
345
  `CARGO_HAULER_STALL_IDLE_MS` (ten minutes), and it printed nothing in that
290
- 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`
291
352
  with the idle duration; `hauler result` and `hauler_result` answer `ticket
292
353
  looks stalled (no CPU for Nm) — hauler kill cc-N`; `hauler await` heartbeats
293
354
  say the same. Riders of a stalled leader report the leader's stall and its
@@ -318,7 +379,7 @@ send (`outputPath`, `after`), so a plain version difference alone does not
318
379
  break `status`, `result`, or `await`; finish or `hauler kill` what is in
319
380
  flight before restarting if the work matters.
320
381
 
321
- The `tool/after` route checks the session's background tickets — `--bg`,
382
+ The `tool/after` hook checks the session's background tickets — `--bg`,
322
383
  `hauler_request`, and synchronous requests the client converted to a ticket
323
384
  — and, on the first tool call after one finishes, adds its result to the
324
385
  agent context. A foreground ticket streamed its exit to the shell the agent
@@ -385,6 +446,17 @@ reports the slowest crates by profile (`<KacheStats>`). Without that index,
385
446
  estimates come from the daemon's EWMA history. A missing or incompatible index
386
447
  is reported as unavailable and never rejects a request.
387
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
+
388
460
  ![cargo-hauler dashboard kache timing panel](docs/media/dashboard-kache.png)
389
461
 
390
462
  ## Install
@@ -470,7 +542,8 @@ Per-host notes and hook timeouts are in [docs/install.md](docs/install.md).
470
542
  | --- | --- | --- |
471
543
  | `CARGO_HAULER_STATE_DIR` | Per-user cache directory | Unix socket or Windows named pipe source, SQLite ledger, daemon log, pid lock, `hook-state.json`, `hook-events.jsonl`, and the per-ticket output logs under `tickets/`. No legacy alias. |
472
544
  | `CARGO_HAULER_CARGO_BIN` | `$CARGO_HOME/bin/cargo` | Cargo binary for daemon-started work; bare `cargo` is the last fallback. Never resolved through `PATH`. Read from the daemon's own environment (export it where the daemon starts, or before `hauler daemon start`); clients do not forward it. |
473
- | `CARGO_HAULER_MAX_CONCURRENT` | `5` | Global admission permits for Cargo processes across all lanes; an integer >= 1. |
545
+ | `CARGO_HAULER_MAX_CONCURRENT` | cores ÷ 8, clamped to 5–16 | Global admission permits for Cargo processes across all lanes; an integer >= 1. |
546
+ | `CARGO_HAULER_OVERLAP_EXECUTION` | `1` | Hand a lane to its next request once a `test`/`nextest`/`bench`/`run` leader reports its build finished, overlapping the next compile with the leader's execution phase. `0` keeps a lane strictly one process at a time. |
474
547
  | `CARGO_HAULER_JOBS_GRANT` | `max(4, cores / max concurrent)` | `CARGO_BUILD_JOBS` added to each Cargo process only while the shared jobserver FIFO is not armed; an armed daemon injects `MAKEFLAGS` instead and leaves `CARGO_BUILD_JOBS` unset. `0` disables injection. |
475
548
  | `CARGO_HAULER_JOBSERVER` | `auto` | Machine-wide fifo jobserver for daemon-spawned cargo: `auto` arms it only when the host `make` is 4.4+ (or absent) because older makes reject `--jobserver-auth=fifo:` in build scripts; `fifo` forces it on, `off` disables it (per-run `CARGO_BUILD_JOBS` grants apply instead). |
476
549
  | `CARGO_HAULER_LOAD_THRESHOLD` | Disabled | Per-core one-minute load threshold for deferring new admissions. |
@@ -529,9 +602,14 @@ unset, the daemon reads kache's configured local store from
529
602
  document names the one in use (`state dir …` in the header; `stateRoot` in
530
603
  `--json`), so a `CARGO_HAULER_STATE_DIR` change is visible on the next
531
604
  command rather than discovered from an empty ledger.
532
- - Test sharing uses identity attachment or batch folding, never coverage.
533
- Folded `test` and `nextest` requests receive the composite output and exit
534
- 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.
535
613
  - The `cargo clean` guard probes the daemon for 250 ms. Active work denies
536
614
  the clean; an idle daemon brokers it; a daemon that accepts but does not
537
615
  answer in time is busy, so the clean is brokered and the lane serializes
@@ -550,9 +628,9 @@ unset, the daemon reads kache's configured local store from
550
628
  <summary><strong>How the app is built</strong> — agent-bundle application structure, testing, and development (click to expand)</summary>
551
629
 
552
630
  The plugin is an [agent-bundle](https://github.com/ScriptedAlchemy/agent-bundle)
553
- application: six MCP tools, a routed CLI, four hook routes, two skills, and a
554
- browser dashboard, all rendered from one component library through one shared
555
- 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.
556
634
 
557
635
  ### Application structure
558
636
 
@@ -569,7 +647,8 @@ src/
569
647
  mcp/hauler/tools/*.tsx hauler_status, _log, _last, _await, _result, _request
570
648
  mcp/hauler/apps/dashboard.tsx the MCP App (ui://cargo-hauler/dashboard.html)
571
649
  cli/*.tsx, cli/daemon.ts the routed `cargo-hauler` CLI, same components
572
- 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
573
652
  skills/cargo-hauler/SKILL.md, skills/hauler-dashboard/SKILL.tsx
574
653
  scripts/hauler.ts the `hauler` process entry hooks rewrite cargo to
575
654
  daemon/, client/, hooks/, shim/, lib/ the broker and its libraries
@@ -600,6 +679,24 @@ through one layout, the way a page framework's `layout.tsx` wraps every page:
600
679
 
601
680
  Event routes are host protocol responses and are never wrapped.
602
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
+
603
700
  #### The daemon provider (`src/providers/hauler-daemon.ts`)
604
701
 
605
702
  One request-context provider mounts `providers.haulerDaemon` for every tool,
@@ -683,9 +780,9 @@ the same filter as its `session` field). Results carry
683
780
  | `tool:hauler/hauler_request` · `cli:request` | submit a background request | `RequestDocument` |
684
781
  | `cli:daemon` | `run` / `start` / `stop` / `status` | plain JSON, exit code from the result |
685
782
  | `event:session/start` | new session | daemon state and the no-kill rule as context |
686
- | `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 |
687
- | `event:tool/after` | shell tool finished | injects finished background-ticket results once per session |
688
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 |
689
786
 
690
787
  #### Skills
691
788
 
@@ -703,6 +800,18 @@ render MCP Apps. It shows contention and admission, in-flight and queued
703
800
  work, metrics windows, optional kache data, lanes, and history, with a live
704
801
  output drawer per ticket.
705
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
+
706
815
  ### Testing
707
816
 
708
817
  ```sh
@@ -714,7 +823,7 @@ artifact build, at the harness proof levels:
714
823
 
715
824
  | Level | Suite | What it proves |
716
825
  | --- | --- | --- |
717
- | 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`) |
718
827
  | cli-dispatch | `cli-dispatch`, `layout` | argv through the routed CLI shell; Markdown wrapped by the shell, `--json` bare |
719
828
  | script-dispatch | `script-dispatch` | the `hauler` entry through its `main` envelope as its own process |
720
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}]}