cargo-hauler 0.4.6 → 0.4.8

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 (57) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +325 -224
  3. package/artifact/agent-bundle.manifest.json +1 -1
  4. package/artifact/claude/.claude-plugin/marketplace.json +1 -1
  5. package/artifact/claude/.claude-plugin/plugin.json +1 -1
  6. package/artifact/claude/INSTALL.md +1 -1
  7. package/artifact/claude/bin/cargo-hauler-flight.mjs +165 -477
  8. package/artifact/claude/bin/cargo-hauler.mjs +519 -528
  9. package/artifact/claude/hooks/event-route-session-start.mjs +2 -2
  10. package/artifact/claude/hooks/event-route-stop.mjs +2 -2
  11. package/artifact/claude/hooks/event-route-tool-after.mjs +2 -2
  12. package/artifact/claude/hooks/event-route-tool-before.mjs +2 -2
  13. package/artifact/claude/hooks/hooks-flight.mjs +146 -36
  14. package/artifact/claude/mcp/mcp-hauler-2b8242f9-flight.mjs +504 -491
  15. package/artifact/claude/mcp/mcp-hauler-2b8242f9.mjs +171 -479
  16. package/artifact/claude/mcp-apps/dashboard.html +1 -1
  17. package/artifact/claude/scripts/hauler.mjs +156 -163
  18. package/artifact/codex/.codex-plugin/plugin.json +1 -1
  19. package/artifact/codex/INSTALL.md +1 -1
  20. package/artifact/codex/bin/cargo-hauler-flight.mjs +165 -477
  21. package/artifact/codex/bin/cargo-hauler.mjs +519 -528
  22. package/artifact/codex/hooks/event-route-session-start.mjs +2 -2
  23. package/artifact/codex/hooks/event-route-stop.mjs +2 -2
  24. package/artifact/codex/hooks/event-route-tool-after.mjs +2 -2
  25. package/artifact/codex/hooks/event-route-tool-before.mjs +2 -2
  26. package/artifact/codex/hooks/hooks-flight.mjs +146 -36
  27. package/artifact/codex/mcp/mcp-hauler-2b8242f9-flight.mjs +504 -491
  28. package/artifact/codex/mcp/mcp-hauler-2b8242f9.mjs +171 -479
  29. package/artifact/codex/mcp-apps/dashboard.html +1 -1
  30. package/artifact/codex/scripts/hauler.mjs +156 -163
  31. package/artifact/cursor/.cursor-plugin/plugin.json +1 -1
  32. package/artifact/cursor/INSTALL.md +1 -1
  33. package/artifact/cursor/bin/cargo-hauler-flight.mjs +165 -477
  34. package/artifact/cursor/bin/cargo-hauler.mjs +519 -528
  35. package/artifact/cursor/hooks/event-route-session-start.mjs +2 -2
  36. package/artifact/cursor/hooks/event-route-stop.mjs +2 -2
  37. package/artifact/cursor/hooks/event-route-tool-after.mjs +2 -2
  38. package/artifact/cursor/hooks/event-route-tool-before.mjs +2 -2
  39. package/artifact/cursor/hooks/hooks-flight.mjs +146 -36
  40. package/artifact/cursor/install.mjs +1 -1
  41. package/artifact/cursor/mcp/mcp-hauler-2b8242f9-flight.mjs +504 -491
  42. package/artifact/cursor/mcp/mcp-hauler-2b8242f9.mjs +171 -479
  43. package/artifact/cursor/mcp-apps/dashboard.html +1 -1
  44. package/artifact/cursor/scripts/hauler.mjs +156 -163
  45. package/artifact/portable/INSTALL.md +1 -1
  46. package/artifact/portable/bin/cargo-hauler-flight.mjs +165 -477
  47. package/artifact/portable/bin/cargo-hauler.mjs +519 -528
  48. package/artifact/portable/install.mjs +1 -1
  49. package/artifact/portable/mcp/mcp-hauler-2b8242f9-flight.mjs +161 -475
  50. package/artifact/portable/mcp/mcp-hauler-2b8242f9.mjs +164 -472
  51. package/artifact/portable/mcp-apps/dashboard.html +1 -1
  52. package/artifact/portable/plugin.json +1 -1
  53. package/artifact/portable/scripts/hauler.mjs +156 -163
  54. package/dist/bin/cargo-hauler-flight.mjs +165 -477
  55. package/dist/bin/cargo-hauler.js +519 -528
  56. package/dist/bin/hauler.js +156 -163
  57. package/package.json +12 -14
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.8
4
+
5
+ ### Patch Changes
6
+
7
+ - 3ad6b7e: Declare no runtime `dependencies`: `npm install -g cargo-hauler` fetches one tarball and nothing else. 0.4.7 listed the build-time stack (`@agent-bundle/runtime` as a pkg.pr.new tarball, `bashjsast` as a `github:` ref, `effect`, `react`, `zod`, …) under `dependencies` even though every shipped file bundles what it uses, so npm 12's default `allow-remote=none` / `allow-git=none` refused the install (`EALLOWREMOTE` for the tarball, `EALLOWGIT` for the git ref). The whole stack now lives under `devDependencies`; the emitted packs and `dist/bin` executables are unchanged. (#82)
8
+
9
+ ## 0.4.7
10
+
11
+ ### Patch Changes
12
+
13
+ - f13360f: Internal simplification sweep: one socket-errno walker (`src/lib/socket-errors.ts`) serves the client, the health probe, and the hooks; `defaultCargoProfile`, the ms clamps, and `diagnosticCounts` are shared instead of copied; request statuses and attach modes are one `as const` list that feeds the types, the zod schemas, and the ledger's SQL filters; the ack is the broker's `SubmitResult` spread once; the before-shell hook skips the bash parse for commands that cannot name cargo, and the after-shell hook no longer rewrites its state file when no ticket finished. The `hauler await` heartbeat spells whole minutes as `2m` rather than `2m0s`.
14
+
3
15
  ## 0.4.6
4
16
 
5
17
  ### Patch Changes
package/README.md CHANGED
@@ -2,221 +2,74 @@
2
2
 
3
3
  # cargo-hauler
4
4
 
5
- Cargo request broker for concurrent Rust development tools, shipped as an
6
- [agent-bundle](https://github.com/ScriptedAlchemy/agent-bundle) application
7
- for Claude Code, Codex, and Cursor.
8
-
9
- cargo-hauler accepts Cargo requests from agent sessions, scripts, and
10
- terminals. A daemon groups compatible work, limits process concurrency, and
11
- returns each result to the callers that requested it. The plugin in this
12
- repository is the app that agents talk to: six MCP tools, a routed CLI, four
13
- hook routes, two skills, and a browser dashboard, all rendered from one
14
- component library through one shared layout.
5
+ **One Cargo, many agents.** cargo-hauler is a broker for `cargo` on a machine
6
+ where several AI coding sessions (Claude Code, Codex, Cursor), scripts, and
7
+ terminals build the same Rust workspaces at once. It stops them from fighting
8
+ over the build directory, runs the work once, and hands every requester its
9
+ own result.
10
+
11
+ ## The problem
12
+
13
+ Agents run `cargo check`, `cargo test`, and `cargo build` constantly and
14
+ independently. On a shared machine that means the same crates compiling five
15
+ times over, everyone blocking on `Blocking waiting for file lock on build
16
+ directory`, a saturated CPU, and shell tools that kill a build at their ten
17
+ minute timeout with nothing to show for it. Nobody can see what is running,
18
+ what is queued, or why their command is slow.
19
+
20
+ ## What it does
21
+
22
+ - **Intercepts** every Cargo invocation: hooks rewrite `cargo …` in agent
23
+ shells, an optional PATH shim covers scripts and terminals, and MCP tools
24
+ let agents submit work directly.
25
+ - **Coalesces** compatible requests: a byte-identical command attaches to the
26
+ run already in flight, a narrower `check` rides a broader `build`, and
27
+ queued tests with the same selection fold into one `--no-fail-fast` run.
28
+ Riders receive the leader's streamed output and exit code as if they had
29
+ run it themselves.
30
+ - **Schedules and admits** work per workspace lane under a machine-wide
31
+ permit cap, with load, memory-pressure, and heavy-profile gates, cost
32
+ estimates from run history and [kache](https://github.com/ScriptedAlchemy/kache)
33
+ priors, and `--after cc-N` dependencies when a test needs a build first.
34
+ - **Keeps a ticket** (`cc-N`) for every request: status, exit code, live
35
+ output tail, the whole output log on disk, timings, and attribution to the
36
+ session that asked. Long builds become background tickets that agents
37
+ `await`, and a stop hook holds an agent's turn until its build lands.
38
+ - **Protects the machine**: `cargo clean` is refused while builds are in
39
+ flight, stalled processes are flagged (and killed when their owner is
40
+ gone), and `hauler kill` frees a wedged lane through the broker instead of
41
+ by PID.
42
+ - **Shows everything** in a live dashboard: contention, in-flight and queued
43
+ work, lanes, per-command timings, compute avoided, and kache data.
15
44
 
16
45
  ![cargo-hauler dashboard with active and queued requests](docs/media/dashboard-overview.png)
17
46
 
18
- ## Tour
19
-
20
- Everything an agent sees is a React Server Component rendered by the
21
- agent-bundle runtime into an Agent Document, then lowered to MCP content, CLI
22
- Markdown, `--json`, or a host hook envelope. There is no hand-written server,
23
- argv parser, or string-concatenated Markdown; the `src/` tree is the app.
24
-
25
- ```text
26
- src/
27
- layout.tsx the hauler shell around every rendered route
28
- providers/hauler-daemon.ts request-scoped daemon connection + health probe
29
- components/ typed components over pure view-models
30
- mcp/hauler/tools/*.tsx hauler_status, _log, _last, _await, _result, _request
31
- mcp/hauler/apps/dashboard.tsx the MCP App (ui://cargo-hauler/dashboard.html)
32
- cli/*.tsx, cli/daemon.ts the routed `cargo-hauler` CLI, same components
33
- events/{session/start,tool/before,tool/after,stop}.tsx hook routes
34
- skills/cargo-hauler/SKILL.md, skills/hauler-dashboard/SKILL.tsx
35
- scripts/hauler.ts the `hauler` process entry hooks rewrite cargo to
36
- daemon/, client/, hooks/, shim/, lib/ the broker and its libraries
37
- ```
38
-
39
- ### The shell (`src/layout.tsx`)
40
-
41
- Every rendered route — MCP tool, CLI command, rendered script — composes
42
- through one layout, the way a page framework's `layout.tsx` wraps every page:
43
-
44
- - **Header:** `<DaemonBadge>` prints what the request-start probe proved and
45
- which state directory it is: `cargo-hauler · daemon running (pid 4021) ·
46
- 2/5 permits +1 riding, 1 queued · 2 lanes busy · up since 3h ago · state dir
47
- /fast/cache/cargo-hauler`, or `daemon stopped · no socket; it starts on
48
- demand…`, or `daemon unresponsive · did not accept a connection within
49
- 750ms (machine saturated)…`. When the daemon is another build than the CLI
50
- or MCP server rendering the document, a second line says so: `cargo-hauler
51
- · daemon 0.4.2 ≠ cli 0.4.4 — restart it with \`hauler daemon restart\``.
52
- - **Body:** the route's own document, unchanged. The route keeps its
53
- `<Agent.Result value>`; the runtime merges it into the shell so
54
- `structuredContent` and `--json` are exactly what the route declared.
55
- - **Footer:** `<LineageFooter>` names the conversation the request belongs to
56
- (`Requested by conversation conv-7f (depth 1 under conv-2a; registry)`),
57
- read synchronously with `useAgent()`, and stays silent when the host cannot
58
- place the request rather than guessing.
59
- - **`_meta.hauler`** on every MCP result: `route`, `surface`, `server`,
60
- `version`, `daemon: { state, pid? }`, `lineage: { conversation, root, depth } | null`.
61
-
62
- Event routes are host protocol responses and are never wrapped.
63
-
64
- ### The daemon provider (`src/providers/hauler-daemon.ts`)
65
-
66
- One request-context provider mounts `providers.haulerDaemon` for every tool,
67
- command, event, and script: the resolved `config` (state dir, socket, ledger)
68
- and a `health` value from one bounded `status` probe:
69
-
70
- | `health.state` | meaning |
71
- | --- | --- |
72
- | `running` | `pid`, `startedAtMs`, `latencyMs`, `running` (permit holders), `riding` (attached), `queued`, `busyLanes`, `maxConcurrent`, and `version` when the daemon states one (on the status report from 0.4.5, by one extra `ping` for older daemons) |
73
- | `stopped` | `socket-missing` (starts on demand) or `connection-refused` (stale socket) |
74
- | `unresponsive` | `accept-timeout` (never accepted), `answer-timeout` (accepted, no `status-result`), or `connection-closed` within the probe budget (750 ms for the accept and for the answer); ledger reads still work |
75
- | `unreachable` | `open-failed` with the errno (`EACCES`, `EMFILE`, …): the socket is present but could not be opened, which is not evidence the daemon is down |
76
- | `unprobed` | `event-surface`: hooks run on every shell command and skip the probe by design |
77
-
78
- The provider fails closed on nothing it can observe and fabricates nothing.
79
- Routes read it through `requestDaemon(context)` / `requestDaemonConfig(context)`;
80
- tests inject a fixture through the harness `context.providers` seam.
81
-
82
- ### Components (`src/components/`)
83
-
84
- Components render view-models and nothing else. The models are pure functions
85
- in `view-models.ts`, so the MCP document, the CLI Markdown, and a test
86
- assertion share one derivation.
87
-
88
- | Component | Renders |
89
- | --- | --- |
90
- | `<TicketCard>` | one ticket: headline, attribution, lane, queue position, attach mode, timings, exit, then `<BuildDiagnostics>` and `<LogTail>` |
91
- | `<TicketList>` | the in-flight and recent tables of status, and the whole of log |
92
- | `<LaneBoard>` | busy lanes with their leader ticket, its command, and how long it has run |
93
- | `<AdmissionState>` | permits in use, load, memory clamp, sharing savings; calls out a paused admission gate |
94
- | `<KacheStats>` | kache coverage and freshness, slowest crates by profile, or an honest "not detected" |
95
- | `<LogTail>` | the captured output tail, labelled live while the run is in progress |
96
- | `<FullOutput>` | where the ticket's whole output log lives and how large it is; under `full`, the log itself in code-block chunks |
97
- | `<BuildDiagnostics>` | an index of cargo `error[E…]`/`warning:` blocks (level / code / message / location) followed by every captured block verbatim |
98
- | `<DashboardLink>` | where the MCP App lives and how to open it elsewhere |
99
- | `<TicketGuidance>` | what to do next, one component per ticket status |
100
- | `<DaemonBadge>`, `<LineageFooter>` | the shell header and footer |
101
- | `<EmptyState>`, `<UnavailableState>`, `<ErrorState>` | the three non-happy shapes every document may take |
102
-
103
- `documents.tsx` composes them into one document per hauler result
104
- (`StatusDocument`, `LogDocument`, `LastDocument`, `ResultDocument`,
105
- `AwaitDocument`, `RequestDocument`); the MCP tool and the CLI command for the
106
- same operation render the same document with different command spellings
107
- (`surface.ts`).
108
-
109
- ### Streaming (`src/components/streaming.tsx`)
110
-
111
- `hauler_await` and `hauler_log` are progressive documents. Each is a
112
- valueless `Agent.Result` container around one `Suspense` boundary:
113
-
114
- - `<AwaitStream>`: the fallback is the ticket **as it is now** — its live
115
- output tail and a progress node — rendered before the daemon-side wait
116
- blocks; the settled child is the ordinary `AwaitDocument`. MCP hosts receive
117
- the fallback's progress as notifications and the settled value as
118
- `structuredContent`; the routed CLI updates the terminal in place. Heartbeats
119
- (queue position, elapsed time, cost estimate) still flow through
120
- `context.progress`.
121
- - `<LogStream>`: a "reading the ledger" progress frame, then the listing.
122
-
123
- ### Attribution and lineage
124
-
125
- `hauler_request` attributes tickets from the request context: an explicit
126
- `host`/`session` wins; otherwise the negotiated host and native session are
127
- used; and when the transport publishes no session id (bare stdio MCP), the
128
- conversation from `request.lineage` becomes the session of record. That is
129
- what makes parallel agents' builds attributable in the ledger, the dashboard,
130
- and `hauler status --session <conversation>` (the `hauler_status` tool takes
131
- the same filter as its `session` field). Results carry
132
- `attribution: { host, session, lineage }`.
133
-
134
- ### Routes
135
-
136
- | Route | Surface | Document |
137
- | --- | --- | --- |
138
- | `tool:hauler/hauler_status` · `cli:status` | queue, lanes, admission, kache, filters | `StatusDocument`; the tool advertises the dashboard App |
139
- | `tool:hauler/hauler_log` · `cli:log` | recent requests | `LogStream` → `LogDocument` |
140
- | `tool:hauler/hauler_last` · `cli:last` | most recent request | `LastDocument` |
141
- | `tool:hauler/hauler_await` · `cli:await` | long-poll a ticket (≤ 2 h) | `AwaitStream` → `AwaitDocument` |
142
- | `tool:hauler/hauler_result` · `cli:result` | one ticket, live tail while running; `full` renders the whole on-disk output log | `ResultDocument` (`<FullOutput>`) |
143
- | `tool:hauler/hauler_kill` · `cli:kill` | stop a queued or running ticket | `KillDocument` |
144
- | `tool:hauler/hauler_request` · `cli:request` | submit a background request | `RequestDocument` |
145
- | `cli:daemon` | `run` / `start` / `stop` / `status` | plain JSON, exit code from the result |
146
- | `event:session/start` | new session | daemon state and the no-kill rule as context |
147
- | `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 |
148
- | `event:tool/after` | shell tool finished | injects finished background-ticket results once per session |
149
- | `event:stop` | agent stopping | holds the stop while a foreground ticket is pending (bounded, re-deniable) |
150
-
151
- ### Skills
152
-
153
- `skills/cargo-hauler/SKILL.md` is the operating rule set (do not kill
154
- in-flight cargo, scope with `-p`, await tickets, fail open when the daemon is
155
- unreachable). `skills/hauler-dashboard/SKILL.tsx` is a rendered skill: the
156
- build computes its Markdown from the tool and CLI spellings and the App
157
- resource URI it describes, so the document cannot drift from the surface.
158
-
159
- ### Dashboard
160
-
161
- `src/mcp/hauler/apps/dashboard.tsx` is the MCP App at
162
- `ui://cargo-hauler/dashboard.html`, attached to `hauler_status` on hosts that
163
- render MCP Apps. It shows contention and admission, in-flight and queued
164
- work, metrics windows, optional kache data, lanes, and history, with a live
165
- output drawer per ticket.
166
-
167
- ![cargo-hauler metrics for one-hour, 24-hour, and all-time windows](docs/media/dashboard-metrics.png)
168
-
169
- ## Install
47
+ ## Quick start
170
48
 
171
- Requirements: Node 22.19 or newer, Cargo, and Linux or macOS (Windows is
172
- experimental: named-pipe transport, no PATH shim).
49
+ Install from npm the package carries a ready-made plugin for each host and
50
+ its own installer, so nothing else is needed:
173
51
 
174
52
  ```sh
175
- pnpm install
176
- pnpm run build # artifact/{claude,codex,cursor,portable} + dist/bin
177
- ```
178
-
179
- Each host pack under `artifact/<host>` is independently installable through
180
- the framework's installer. The packs are framework-owned; this project ships
181
- no installer of its own.
53
+ npm install -g cargo-hauler
182
54
 
183
- ```sh
184
- # Claude Code (local marketplace + plugin install)
185
- pnpm exec agent-bundle install claude --from artifact/claude --scope user
186
-
187
- # Codex
188
- pnpm exec agent-bundle install codex --from artifact/codex
189
-
190
- # Cursor: safe-copy into ~/.cursor/plugins/local/cargo-hauler (default), or
191
- # stage a local marketplace repository for Customize → Add Plugins from Local Repository
192
- pnpm exec agent-bundle install cursor --from artifact/cursor --mode local
193
- pnpm exec agent-bundle install cursor --from artifact/cursor --mode marketplace
55
+ cargo-hauler-install install claude --scope user # Claude Code
56
+ cargo-hauler-install install codex # Codex
57
+ cargo-hauler-install install cursor --mode local # Cursor
58
+ hauler install-shim # optional: cargo from scripts and terminals too
194
59
  ```
195
60
 
196
- Add `--replace` to any of them after a same-version rebuild. From an `npm
197
- pack`ed tarball the same operations are
198
- `npx cargo-hauler-install install <host> [--scope …] [--mode …] [--json]`
199
- (`dist/bin/cargo-hauler-install.js`, generated by the build and gated by
200
- `agent-bundle prepack`). `agent-bundle doctor --host <host>` reports the
201
- installed copy versus the artifact (`current`, `stale`, `version-mismatch`,
202
- `foreign`, `not-installed`) and, for Cursor, whether the manifest hooks are
203
- registered. Each pack's `INSTALL.md` carries the same commands with the exact
204
- compiled names. Restart or reload the host after installing so new sessions
205
- load the hooks. Per-host notes, hook timeouts, and the optional PATH shim are
206
- in [docs/install.md](docs/install.md).
207
-
208
- The first brokered request makes one daemon-start attempt. Hooks cover Cargo
209
- commands submitted through supported agent shells; the optional PATH shim
210
- (`node dist/bin/hauler.js install-shim`) also covers Cargo invoked by scripts
211
- and terminals.
61
+ Restart the host (or reload the window) so new sessions load the hooks. The
62
+ daemon starts on demand with the first brokered request; `hauler status` shows
63
+ what is running, and the `hauler_status` tool opens the dashboard in hosts that
64
+ render MCP Apps. Prefer the hosts' own plugin commands, or building from a
65
+ checkout? See [Install](#install).
212
66
 
213
- ## Interfaces
67
+ ## Commands and tools
214
68
 
215
- `hauler` is the process entry (`src/scripts/hauler.ts`): `exec`, `daemon`, and
216
- `install-shim`, forwarding every other command to the routed `cargo-hauler`
217
- executable beside it (`dist/bin/cargo-hauler.js` in the package,
218
- `bin/cargo-hauler.mjs` inside every host pack). Routed commands accept
219
- `--json` for the canonical value and `--ndjson` for the render-event stream.
69
+ `hauler` is the command line; agents reach the same operations as MCP tools
70
+ (`hauler_status`, `hauler_await`, …) and hooks rewrite plain `cargo …` into
71
+ `hauler exec` automatically. Every command except `exec`, `daemon`, and
72
+ `install-shim` also accepts `--json` for the machine-readable value.
220
73
 
221
74
  | Command | Behavior |
222
75
  | --- | --- |
@@ -235,27 +88,15 @@ The `hauler` MCP server projects the same operations as `hauler_status`,
235
88
  `hauler_log`, `hauler_last`, `hauler_await`, `hauler_result`, `hauler_kill`,
236
89
  and `hauler_request`, with the same filters as the CLI.
237
90
 
238
- ## Testing
239
-
240
- ```sh
241
- pnpm run check # validate + build + typecheck + Effect diagnostics + rstest + route tests
242
- ```
243
-
244
- `tests/route-unit/` renders the app through the framework compiler with no
245
- artifact build, at the harness proof levels:
91
+ ## Dashboard
246
92
 
247
- | Level | Suite | What it proves |
248
- | --- | --- | --- |
249
- | route-unit | `routes`, `layout`, `streaming`, `events` | documents, shell metadata, Suspense fallbacks and settled values, lineage attribution, event decisions |
250
- | cli-dispatch | `cli-dispatch`, `layout` | argv through the routed CLI shell; Markdown wrapped by the shell, `--json` bare |
251
- | script-dispatch | `script-dispatch` | the `hauler` entry through its `main` envelope as its own process |
252
- | 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 |
253
- | packed-stdio | `packed-contract` | the built `artifact/cursor` server as a real process against a live broker, every tool through the wire-contract matrix |
254
- | workbench-surface | `tests/workbench-surface.test.ts` | what `agent-bundle dev` would show: catalog, provider, lifecycles per host, counts |
93
+ The dashboard is an MCP App (`ui://cargo-hauler/dashboard.html`) attached to
94
+ `hauler_status`. It shows contention and admission, in-flight and queued work
95
+ with live output per ticket, metrics over one-hour, 24-hour, and all-time
96
+ windows, per-command timings, optional kache data, lanes, and history. It
97
+ refreshes itself while open.
255
98
 
256
- Daemon-backed cases run a real broker in-process with a fake `cargo`
257
- (`tests/harness.ts`) and reach it either through the `haulerDaemon` provider
258
- seam or through `CARGO_HAULER_STATE_DIR`.
99
+ ![cargo-hauler metrics for one-hour, 24-hour, and all-time windows](docs/media/dashboard-metrics.png)
259
100
 
260
101
  ## How the broker works
261
102
 
@@ -543,6 +384,83 @@ is reported as unavailable and never rejects a request.
543
384
 
544
385
  ![cargo-hauler dashboard kache timing panel](docs/media/dashboard-kache.png)
545
386
 
387
+ ## Install
388
+
389
+ Requirements: Node 22.19 or newer, Cargo, and Linux or macOS (Windows is
390
+ experimental: named-pipe transport, no PATH shim).
391
+
392
+ The npm package ships one plugin per host under `artifact/<host>` — the
393
+ Claude Code plugin with its local marketplace, the Codex plugin, and the Cursor
394
+ plugin with its `install.mjs` — plus three executables: `hauler` (the CLI),
395
+ `cargo-hauler` (the routed commands), and `cargo-hauler-install`. Every pack
396
+ also contains an `INSTALL.md` with the exact commands for that host. The
397
+ package declares no runtime dependencies — every library the packs and
398
+ executables use is bundled into them — so `npm install` fetches this one
399
+ tarball and nothing else.
400
+
401
+ ### With the bundled installer
402
+
403
+ ```sh
404
+ npm install -g cargo-hauler # or run each command as: npx -p cargo-hauler <command>
405
+
406
+ cargo-hauler-install install claude --scope user # user, project, or local
407
+ cargo-hauler-install install codex
408
+ cargo-hauler-install install cursor --mode local # ~/.cursor/plugins/local/cargo-hauler
409
+ cargo-hauler-install install cursor --mode marketplace # stage a local marketplace repo for Customize → Add Plugins from Local Repository
410
+ ```
411
+
412
+ `cargo-hauler-install` runs the host's own plugin commands for you (below),
413
+ detects an installed copy with the same version but different content and
414
+ replaces it, and takes `--replace` (alias `--force`) to replace a different
415
+ installed version. `--json` prints the result for scripts.
416
+
417
+ ### With the hosts' own plugin commands
418
+
419
+ The same result without the installer, from the package or a build (paths are
420
+ relative to `node_modules/cargo-hauler` or the checkout):
421
+
422
+ ```sh
423
+ # Claude Code — a local marketplace plus a plugin install
424
+ cd artifact/claude
425
+ claude plugin marketplace add ./
426
+ claude plugin install cargo-hauler@cargo-hauler-marketplace --scope user
427
+
428
+ # Codex — a local marketplace snapshot
429
+ cd artifact/codex
430
+ codex plugin marketplace add ./
431
+ codex plugin add cargo-hauler@cargo-hauler-marketplace
432
+
433
+ # Cursor — no non-interactive plugin command exists, so the pack ships one
434
+ node artifact/cursor/install.mjs # local plugin (default)
435
+ node artifact/cursor/install.mjs --mode marketplace # local marketplace repository
436
+ ```
437
+
438
+ Upgrading to a new version: `claude plugin marketplace update cargo-hauler-marketplace
439
+ && claude plugin update cargo-hauler@cargo-hauler-marketplace`, `codex plugin
440
+ remove … && codex plugin marketplace add ./ && codex plugin add …`, and
441
+ `node artifact/cursor/install.mjs --replace`. `claude plugin update` is
442
+ version-gated, so after a rebuild that did not bump the version use
443
+ `claude plugin uninstall … --keep-data` and install again (the installer does
444
+ this automatically). Restart or reload the host after installing.
445
+
446
+ ### From a checkout
447
+
448
+ ```sh
449
+ pnpm install
450
+ pnpm run build # artifact/{claude,codex,cursor,portable} + dist/bin
451
+ ```
452
+
453
+ Then install with either method above from `artifact/<host>`, and run
454
+ `node dist/bin/hauler.js install-shim` for the PATH shim. Building needs the
455
+ repository's dev dependencies (including the agent-bundle framework, pinned as
456
+ a pkg.pr.new preview until it is on npm); using the published package does
457
+ not.
458
+
459
+ The first brokered request makes one daemon-start attempt. Hooks cover Cargo
460
+ commands submitted through supported agent shells; the optional PATH shim
461
+ (`hauler install-shim`) also covers Cargo invoked by scripts and terminals.
462
+ Per-host notes and hook timeouts are in [docs/install.md](docs/install.md).
463
+
546
464
  ## Configuration
547
465
 
548
466
  | Variable | Default | Meaning |
@@ -623,7 +541,188 @@ unset, the daemon reads kache's configured local store from
623
541
  the POSIX PATH shim is unavailable and jobserver integration is disabled.
624
542
  - Licensed under MIT.
625
543
 
626
- ## Development
544
+ ## Architecture
545
+
546
+ <details>
547
+ <summary><strong>How the app is built</strong> — agent-bundle application structure, testing, and development (click to expand)</summary>
548
+
549
+ The plugin is an [agent-bundle](https://github.com/ScriptedAlchemy/agent-bundle)
550
+ application: six MCP tools, a routed CLI, four hook routes, two skills, and a
551
+ browser dashboard, all rendered from one component library through one shared
552
+ layout. This section is for contributors; using cargo-hauler needs none of it.
553
+
554
+ ### Application structure
555
+
556
+ Everything an agent sees is a React Server Component rendered by the
557
+ agent-bundle runtime into an Agent Document, then lowered to MCP content, CLI
558
+ Markdown, `--json`, or a host hook envelope. There is no hand-written server,
559
+ argv parser, or string-concatenated Markdown; the `src/` tree is the app.
560
+
561
+ ```text
562
+ src/
563
+ layout.tsx the hauler shell around every rendered route
564
+ providers/hauler-daemon.ts request-scoped daemon connection + health probe
565
+ components/ typed components over pure view-models
566
+ mcp/hauler/tools/*.tsx hauler_status, _log, _last, _await, _result, _request
567
+ mcp/hauler/apps/dashboard.tsx the MCP App (ui://cargo-hauler/dashboard.html)
568
+ cli/*.tsx, cli/daemon.ts the routed `cargo-hauler` CLI, same components
569
+ events/{session/start,tool/before,tool/after,stop}.tsx hook routes
570
+ skills/cargo-hauler/SKILL.md, skills/hauler-dashboard/SKILL.tsx
571
+ scripts/hauler.ts the `hauler` process entry hooks rewrite cargo to
572
+ daemon/, client/, hooks/, shim/, lib/ the broker and its libraries
573
+ ```
574
+
575
+ #### The shell (`src/layout.tsx`)
576
+
577
+ Every rendered route — MCP tool, CLI command, rendered script — composes
578
+ through one layout, the way a page framework's `layout.tsx` wraps every page:
579
+
580
+ - **Header:** `<DaemonBadge>` prints what the request-start probe proved and
581
+ which state directory it is: `cargo-hauler · daemon running (pid 4021) ·
582
+ 2/5 permits +1 riding, 1 queued · 2 lanes busy · up since 3h ago · state dir
583
+ /fast/cache/cargo-hauler`, or `daemon stopped · no socket; it starts on
584
+ demand…`, or `daemon unresponsive · did not accept a connection within
585
+ 750ms (machine saturated)…`. When the daemon is another build than the CLI
586
+ or MCP server rendering the document, a second line says so: `cargo-hauler
587
+ · daemon 0.4.2 ≠ cli 0.4.4 — restart it with \`hauler daemon restart\``.
588
+ - **Body:** the route's own document, unchanged. The route keeps its
589
+ `<Agent.Result value>`; the runtime merges it into the shell so
590
+ `structuredContent` and `--json` are exactly what the route declared.
591
+ - **Footer:** `<LineageFooter>` names the conversation the request belongs to
592
+ (`Requested by conversation conv-7f (depth 1 under conv-2a; registry)`),
593
+ read synchronously with `useAgent()`, and stays silent when the host cannot
594
+ place the request rather than guessing.
595
+ - **`_meta.hauler`** on every MCP result: `route`, `surface`, `server`,
596
+ `version`, `daemon: { state, pid? }`, `lineage: { conversation, root, depth } | null`.
597
+
598
+ Event routes are host protocol responses and are never wrapped.
599
+
600
+ #### The daemon provider (`src/providers/hauler-daemon.ts`)
601
+
602
+ One request-context provider mounts `providers.haulerDaemon` for every tool,
603
+ command, event, and script: the resolved `config` (state dir, socket, ledger)
604
+ and a `health` value from one bounded `status` probe:
605
+
606
+ | `health.state` | meaning |
607
+ | --- | --- |
608
+ | `running` | `pid`, `startedAtMs`, `latencyMs`, `running` (permit holders), `riding` (attached), `queued`, `busyLanes`, `maxConcurrent`, and `version` when the daemon states one (on the status report from 0.4.5, by one extra `ping` for older daemons) |
609
+ | `stopped` | `socket-missing` (starts on demand) or `connection-refused` (stale socket) |
610
+ | `unresponsive` | `accept-timeout` (never accepted), `answer-timeout` (accepted, no `status-result`), or `connection-closed` within the probe budget (750 ms for the accept and for the answer); ledger reads still work |
611
+ | `unreachable` | `open-failed` with the errno (`EACCES`, `EMFILE`, …): the socket is present but could not be opened, which is not evidence the daemon is down |
612
+ | `unprobed` | `event-surface`: hooks run on every shell command and skip the probe by design |
613
+
614
+ The provider fails closed on nothing it can observe and fabricates nothing.
615
+ Routes read it through `requestDaemon(context)` / `requestDaemonConfig(context)`;
616
+ tests inject a fixture through the harness `context.providers` seam.
617
+
618
+ #### Components (`src/components/`)
619
+
620
+ Components render view-models and nothing else. The models are pure functions
621
+ in `view-models.ts`, so the MCP document, the CLI Markdown, and a test
622
+ assertion share one derivation.
623
+
624
+ | Component | Renders |
625
+ | --- | --- |
626
+ | `<TicketCard>` | one ticket: headline, attribution, lane, queue position, attach mode, timings, exit, then `<BuildDiagnostics>` and `<LogTail>` |
627
+ | `<TicketList>` | the in-flight and recent tables of status, and the whole of log |
628
+ | `<LaneBoard>` | busy lanes with their leader ticket, its command, and how long it has run |
629
+ | `<AdmissionState>` | permits in use, load, memory clamp, sharing savings; calls out a paused admission gate |
630
+ | `<KacheStats>` | kache coverage and freshness, slowest crates by profile, or an honest "not detected" |
631
+ | `<LogTail>` | the captured output tail, labelled live while the run is in progress |
632
+ | `<FullOutput>` | where the ticket's whole output log lives and how large it is; under `full`, the log itself in code-block chunks |
633
+ | `<BuildDiagnostics>` | an index of cargo `error[E…]`/`warning:` blocks (level / code / message / location) followed by every captured block verbatim |
634
+ | `<DashboardLink>` | where the MCP App lives and how to open it elsewhere |
635
+ | `<TicketGuidance>` | what to do next, one component per ticket status |
636
+ | `<DaemonBadge>`, `<LineageFooter>` | the shell header and footer |
637
+ | `<EmptyState>`, `<UnavailableState>`, `<ErrorState>` | the three non-happy shapes every document may take |
638
+
639
+ `documents.tsx` composes them into one document per hauler result
640
+ (`StatusDocument`, `LogDocument`, `LastDocument`, `ResultDocument`,
641
+ `AwaitDocument`, `RequestDocument`); the MCP tool and the CLI command for the
642
+ same operation render the same document with different command spellings
643
+ (`surface.ts`).
644
+
645
+ #### Streaming (`src/components/streaming.tsx`)
646
+
647
+ `hauler_await` and `hauler_log` are progressive documents. Each is a
648
+ valueless `Agent.Result` container around one `Suspense` boundary:
649
+
650
+ - `<AwaitStream>`: the fallback is the ticket **as it is now** — its live
651
+ output tail and a progress node — rendered before the daemon-side wait
652
+ blocks; the settled child is the ordinary `AwaitDocument`. MCP hosts receive
653
+ the fallback's progress as notifications and the settled value as
654
+ `structuredContent`; the routed CLI updates the terminal in place. Heartbeats
655
+ (queue position, elapsed time, cost estimate) still flow through
656
+ `context.progress`.
657
+ - `<LogStream>`: a "reading the ledger" progress frame, then the listing.
658
+
659
+ #### Attribution and lineage
660
+
661
+ `hauler_request` attributes tickets from the request context: an explicit
662
+ `host`/`session` wins; otherwise the negotiated host and native session are
663
+ used; and when the transport publishes no session id (bare stdio MCP), the
664
+ conversation from `request.lineage` becomes the session of record. That is
665
+ what makes parallel agents' builds attributable in the ledger, the dashboard,
666
+ and `hauler status --session <conversation>` (the `hauler_status` tool takes
667
+ the same filter as its `session` field). Results carry
668
+ `attribution: { host, session, lineage }`.
669
+
670
+ #### Routes
671
+
672
+ | Route | Surface | Document |
673
+ | --- | --- | --- |
674
+ | `tool:hauler/hauler_status` · `cli:status` | queue, lanes, admission, kache, filters | `StatusDocument`; the tool advertises the dashboard App |
675
+ | `tool:hauler/hauler_log` · `cli:log` | recent requests | `LogStream` → `LogDocument` |
676
+ | `tool:hauler/hauler_last` · `cli:last` | most recent request | `LastDocument` |
677
+ | `tool:hauler/hauler_await` · `cli:await` | long-poll a ticket (≤ 2 h) | `AwaitStream` → `AwaitDocument` |
678
+ | `tool:hauler/hauler_result` · `cli:result` | one ticket, live tail while running; `full` renders the whole on-disk output log | `ResultDocument` (`<FullOutput>`) |
679
+ | `tool:hauler/hauler_kill` · `cli:kill` | stop a queued or running ticket | `KillDocument` |
680
+ | `tool:hauler/hauler_request` · `cli:request` | submit a background request | `RequestDocument` |
681
+ | `cli:daemon` | `run` / `start` / `stop` / `status` | plain JSON, exit code from the result |
682
+ | `event:session/start` | new session | daemon state and the no-kill rule as context |
683
+ | `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 |
684
+ | `event:tool/after` | shell tool finished | injects finished background-ticket results once per session |
685
+ | `event:stop` | agent stopping | holds the stop while a foreground ticket is pending (bounded, re-deniable) |
686
+
687
+ #### Skills
688
+
689
+ `skills/cargo-hauler/SKILL.md` is the operating rule set (do not kill
690
+ in-flight cargo, scope with `-p`, await tickets, fail open when the daemon is
691
+ unreachable). `skills/hauler-dashboard/SKILL.tsx` is a rendered skill: the
692
+ build computes its Markdown from the tool and CLI spellings and the App
693
+ resource URI it describes, so the document cannot drift from the surface.
694
+
695
+ #### Dashboard
696
+
697
+ `src/mcp/hauler/apps/dashboard.tsx` is the MCP App at
698
+ `ui://cargo-hauler/dashboard.html`, attached to `hauler_status` on hosts that
699
+ render MCP Apps. It shows contention and admission, in-flight and queued
700
+ work, metrics windows, optional kache data, lanes, and history, with a live
701
+ output drawer per ticket.
702
+
703
+ ### Testing
704
+
705
+ ```sh
706
+ pnpm run check # validate + build + typecheck + Effect diagnostics + rstest + route tests
707
+ ```
708
+
709
+ `tests/route-unit/` renders the app through the framework compiler with no
710
+ artifact build, at the harness proof levels:
711
+
712
+ | Level | Suite | What it proves |
713
+ | --- | --- | --- |
714
+ | route-unit | `routes`, `layout`, `streaming`, `events` | documents, shell metadata, Suspense fallbacks and settled values, lineage attribution, event decisions |
715
+ | cli-dispatch | `cli-dispatch`, `layout` | argv through the routed CLI shell; Markdown wrapped by the shell, `--json` bare |
716
+ | script-dispatch | `script-dispatch` | the `hauler` entry through its `main` envelope as its own process |
717
+ | 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 |
718
+ | packed-stdio | `packed-contract` | the built `artifact/cursor` server as a real process against a live broker, every tool through the wire-contract matrix |
719
+ | workbench-surface | `tests/workbench-surface.test.ts` | what `agent-bundle dev` would show: catalog, provider, lifecycles per host, counts |
720
+
721
+ Daemon-backed cases run a real broker in-process with a fake `cargo`
722
+ (`tests/harness.ts`) and reach it either through the `haulerDaemon` provider
723
+ seam or through `CARGO_HAULER_STATE_DIR`.
724
+
725
+ ### Development
627
726
 
628
727
  ```sh
629
728
  pnpm run dev # agent-bundle workbench with live rebuilds
@@ -655,3 +754,5 @@ which is why `tests/workbench-surface.test.ts` lives in the plain pool).
655
754
  `repos/effect` is a read-only subtree containing the Effect v4 source pinned to
656
755
  `effect@4.0.0-rc.112`; see `AGENTS.md` before working with Effect code in this
657
756
  repository.
757
+
758
+ </details>