cargo-hauler 0.4.5 → 0.4.7

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 +346 -223
  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 +6489 -6636
  8. package/artifact/claude/bin/cargo-hauler.mjs +5452 -5210
  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 +518 -307
  14. package/artifact/claude/mcp/mcp-hauler-2b8242f9-flight.mjs +5649 -5471
  15. package/artifact/claude/mcp/mcp-hauler-2b8242f9.mjs +5902 -6060
  16. package/artifact/claude/mcp-apps/dashboard.html +1 -1
  17. package/artifact/claude/scripts/hauler.mjs +894 -203
  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 +6489 -6636
  21. package/artifact/codex/bin/cargo-hauler.mjs +5452 -5210
  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 +518 -307
  27. package/artifact/codex/mcp/mcp-hauler-2b8242f9-flight.mjs +5649 -5471
  28. package/artifact/codex/mcp/mcp-hauler-2b8242f9.mjs +5902 -6060
  29. package/artifact/codex/mcp-apps/dashboard.html +1 -1
  30. package/artifact/codex/scripts/hauler.mjs +894 -203
  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 +6489 -6636
  34. package/artifact/cursor/bin/cargo-hauler.mjs +5452 -5210
  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 +518 -307
  40. package/artifact/cursor/install.mjs +1 -1
  41. package/artifact/cursor/mcp/mcp-hauler-2b8242f9-flight.mjs +5649 -5471
  42. package/artifact/cursor/mcp/mcp-hauler-2b8242f9.mjs +5902 -6060
  43. package/artifact/cursor/mcp-apps/dashboard.html +1 -1
  44. package/artifact/cursor/scripts/hauler.mjs +894 -203
  45. package/artifact/portable/INSTALL.md +1 -1
  46. package/artifact/portable/bin/cargo-hauler-flight.mjs +6489 -6636
  47. package/artifact/portable/bin/cargo-hauler.mjs +5452 -5210
  48. package/artifact/portable/install.mjs +1 -1
  49. package/artifact/portable/mcp/mcp-hauler-2b8242f9-flight.mjs +11092 -11241
  50. package/artifact/portable/mcp/mcp-hauler-2b8242f9.mjs +5895 -6053
  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 +894 -203
  54. package/dist/bin/cargo-hauler-flight.mjs +6489 -6636
  55. package/dist/bin/cargo-hauler.js +5452 -5210
  56. package/dist/bin/hauler.js +894 -203
  57. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.7
4
+
5
+ ### Patch Changes
6
+
7
+ - 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`.
8
+
9
+ ## 0.4.6
10
+
11
+ ### Patch Changes
12
+
13
+ - 4257982: Survive a CLI or plugin upgrade under a daemon left running from the older install, and add `hauler daemon restart`. The client schemas now default the fields older daemons never send (`outputPath`, `after`), so `hauler status`, `hauler result`, `hauler await`, and the `hauler_*` tools read a 0.4.1 daemon's replies instead of printing a Zod issue array; a reply that still cannot be read fails as `daemon is 0.4.1 (pid N, since 3h ago), this CLI is 0.4.4 — restart it with \`hauler daemon restart\`` with the first mismatch (`active[0].outputPath expected string, received undefined`) on a second line, and `hauler daemon status` says the same. The status report carries the daemon `version` (older daemons are asked with one `ping`), `hauler status --json` exposes it as `daemonVersion`, and every document header — plus the status summary — warns `daemon 0.4.2 ≠ cli 0.4.4 — restart it with \`hauler daemon restart\`` while the versions differ. `hauler daemon restart` sends the graceful stop, waits up to 5 s for the old pid to exit, starts a daemon from this install, and prints both pids and versions (`previousPid` in the JSON result; exit `1` when the old daemon did not exit, in which case nothing is started). Tickets in flight at a restart are not handed over: the new daemon marks them `killed` with `orphaned by daemon restart`, and `hauler result` / `hauler_result` explain that instead of showing a plain kill. The shell header ends with `state dir <path>` so a moved `CARGO_HAULER_STATE_DIR` is visible on every command. (#75)
14
+
3
15
  ## 0.4.5
4
16
 
5
17
  ### Patch Changes
package/README.md CHANGED
@@ -2,218 +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:
45
- `cargo-hauler · daemon running (pid 4021) · 2/5 permits +1 riding, 1 queued
46
- · 2 lanes busy · up since 3h ago`, or `daemon stopped · no socket; it starts
47
- on demand…`, or `daemon unresponsive · did not accept a connection within
48
- 750ms (machine saturated)…`.
49
- - **Body:** the route's own document, unchanged. The route keeps its
50
- `<Agent.Result value>`; the runtime merges it into the shell so
51
- `structuredContent` and `--json` are exactly what the route declared.
52
- - **Footer:** `<LineageFooter>` names the conversation the request belongs to
53
- (`Requested by conversation conv-7f (depth 1 under conv-2a; registry)`),
54
- read synchronously with `useAgent()`, and stays silent when the host cannot
55
- place the request rather than guessing.
56
- - **`_meta.hauler`** on every MCP result: `route`, `surface`, `server`,
57
- `version`, `daemon: { state, pid? }`, `lineage: { conversation, root, depth } | null`.
58
-
59
- Event routes are host protocol responses and are never wrapped.
60
-
61
- ### The daemon provider (`src/providers/hauler-daemon.ts`)
62
-
63
- One request-context provider mounts `providers.haulerDaemon` for every tool,
64
- command, event, and script: the resolved `config` (state dir, socket, ledger)
65
- and a `health` value from one bounded `status` probe:
66
-
67
- | `health.state` | meaning |
68
- | --- | --- |
69
- | `running` | `pid`, `startedAtMs`, `latencyMs`, `running` (permit holders), `riding` (attached), `queued`, `busyLanes`, `maxConcurrent` |
70
- | `stopped` | `socket-missing` (starts on demand) or `connection-refused` (stale socket) |
71
- | `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 |
72
- | `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 |
73
- | `unprobed` | `event-surface`: hooks run on every shell command and skip the probe by design |
74
-
75
- The provider fails closed on nothing it can observe and fabricates nothing.
76
- Routes read it through `requestDaemon(context)` / `requestDaemonConfig(context)`;
77
- tests inject a fixture through the harness `context.providers` seam.
78
-
79
- ### Components (`src/components/`)
80
-
81
- Components render view-models and nothing else. The models are pure functions
82
- in `view-models.ts`, so the MCP document, the CLI Markdown, and a test
83
- assertion share one derivation.
84
-
85
- | Component | Renders |
86
- | --- | --- |
87
- | `<TicketCard>` | one ticket: headline, attribution, lane, queue position, attach mode, timings, exit, then `<BuildDiagnostics>` and `<LogTail>` |
88
- | `<TicketList>` | the in-flight and recent tables of status, and the whole of log |
89
- | `<LaneBoard>` | busy lanes with their leader ticket, its command, and how long it has run |
90
- | `<AdmissionState>` | permits in use, load, memory clamp, sharing savings; calls out a paused admission gate |
91
- | `<KacheStats>` | kache coverage and freshness, slowest crates by profile, or an honest "not detected" |
92
- | `<LogTail>` | the captured output tail, labelled live while the run is in progress |
93
- | `<FullOutput>` | where the ticket's whole output log lives and how large it is; under `full`, the log itself in code-block chunks |
94
- | `<BuildDiagnostics>` | an index of cargo `error[E…]`/`warning:` blocks (level / code / message / location) followed by every captured block verbatim |
95
- | `<DashboardLink>` | where the MCP App lives and how to open it elsewhere |
96
- | `<TicketGuidance>` | what to do next, one component per ticket status |
97
- | `<DaemonBadge>`, `<LineageFooter>` | the shell header and footer |
98
- | `<EmptyState>`, `<UnavailableState>`, `<ErrorState>` | the three non-happy shapes every document may take |
99
-
100
- `documents.tsx` composes them into one document per hauler result
101
- (`StatusDocument`, `LogDocument`, `LastDocument`, `ResultDocument`,
102
- `AwaitDocument`, `RequestDocument`); the MCP tool and the CLI command for the
103
- same operation render the same document with different command spellings
104
- (`surface.ts`).
105
-
106
- ### Streaming (`src/components/streaming.tsx`)
107
-
108
- `hauler_await` and `hauler_log` are progressive documents. Each is a
109
- valueless `Agent.Result` container around one `Suspense` boundary:
110
-
111
- - `<AwaitStream>`: the fallback is the ticket **as it is now** — its live
112
- output tail and a progress node — rendered before the daemon-side wait
113
- blocks; the settled child is the ordinary `AwaitDocument`. MCP hosts receive
114
- the fallback's progress as notifications and the settled value as
115
- `structuredContent`; the routed CLI updates the terminal in place. Heartbeats
116
- (queue position, elapsed time, cost estimate) still flow through
117
- `context.progress`.
118
- - `<LogStream>`: a "reading the ledger" progress frame, then the listing.
119
-
120
- ### Attribution and lineage
121
-
122
- `hauler_request` attributes tickets from the request context: an explicit
123
- `host`/`session` wins; otherwise the negotiated host and native session are
124
- used; and when the transport publishes no session id (bare stdio MCP), the
125
- conversation from `request.lineage` becomes the session of record. That is
126
- what makes parallel agents' builds attributable in the ledger, the dashboard,
127
- and `hauler status --session <conversation>` (the `hauler_status` tool takes
128
- the same filter as its `session` field). Results carry
129
- `attribution: { host, session, lineage }`.
130
-
131
- ### Routes
132
-
133
- | Route | Surface | Document |
134
- | --- | --- | --- |
135
- | `tool:hauler/hauler_status` · `cli:status` | queue, lanes, admission, kache, filters | `StatusDocument`; the tool advertises the dashboard App |
136
- | `tool:hauler/hauler_log` · `cli:log` | recent requests | `LogStream` → `LogDocument` |
137
- | `tool:hauler/hauler_last` · `cli:last` | most recent request | `LastDocument` |
138
- | `tool:hauler/hauler_await` · `cli:await` | long-poll a ticket (≤ 2 h) | `AwaitStream` → `AwaitDocument` |
139
- | `tool:hauler/hauler_result` · `cli:result` | one ticket, live tail while running; `full` renders the whole on-disk output log | `ResultDocument` (`<FullOutput>`) |
140
- | `tool:hauler/hauler_kill` · `cli:kill` | stop a queued or running ticket | `KillDocument` |
141
- | `tool:hauler/hauler_request` · `cli:request` | submit a background request | `RequestDocument` |
142
- | `cli:daemon` | `run` / `start` / `stop` / `status` | plain JSON, exit code from the result |
143
- | `event:session/start` | new session | daemon state and the no-kill rule as context |
144
- | `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 |
145
- | `event:tool/after` | shell tool finished | injects finished background-ticket results once per session |
146
- | `event:stop` | agent stopping | holds the stop while a foreground ticket is pending (bounded, re-deniable) |
147
-
148
- ### Skills
149
-
150
- `skills/cargo-hauler/SKILL.md` is the operating rule set (do not kill
151
- in-flight cargo, scope with `-p`, await tickets, fail open when the daemon is
152
- unreachable). `skills/hauler-dashboard/SKILL.tsx` is a rendered skill: the
153
- build computes its Markdown from the tool and CLI spellings and the App
154
- resource URI it describes, so the document cannot drift from the surface.
155
-
156
- ### Dashboard
157
-
158
- `src/mcp/hauler/apps/dashboard.tsx` is the MCP App at
159
- `ui://cargo-hauler/dashboard.html`, attached to `hauler_status` on hosts that
160
- render MCP Apps. It shows contention and admission, in-flight and queued
161
- work, metrics windows, optional kache data, lanes, and history, with a live
162
- output drawer per ticket.
163
-
164
- ![cargo-hauler metrics for one-hour, 24-hour, and all-time windows](docs/media/dashboard-metrics.png)
165
-
166
- ## Install
167
-
168
- Requirements: Node 22.19 or newer, Cargo, and Linux or macOS (Windows is
169
- experimental: named-pipe transport, no PATH shim).
170
-
171
- ```sh
172
- pnpm install
173
- pnpm run build # artifact/{claude,codex,cursor,portable} + dist/bin
174
- ```
47
+ ## Quick start
175
48
 
176
- Each host pack under `artifact/<host>` is independently installable through
177
- the framework's installer. The packs are framework-owned; this project ships
178
- no installer of its own.
49
+ Install from npm the package carries a ready-made plugin for each host and
50
+ its own installer, so nothing else is needed:
179
51
 
180
52
  ```sh
181
- # Claude Code (local marketplace + plugin install)
182
- pnpm exec agent-bundle install claude --from artifact/claude --scope user
53
+ npm install -g cargo-hauler
183
54
 
184
- # Codex
185
- pnpm exec agent-bundle install codex --from artifact/codex
186
-
187
- # Cursor: safe-copy into ~/.cursor/plugins/local/cargo-hauler (default), or
188
- # stage a local marketplace repository for Customize → Add Plugins from Local Repository
189
- pnpm exec agent-bundle install cursor --from artifact/cursor --mode local
190
- 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
191
59
  ```
192
60
 
193
- Add `--replace` to any of them after a same-version rebuild. From an `npm
194
- pack`ed tarball the same operations are
195
- `npx cargo-hauler-install install <host> [--scope …] [--mode …] [--json]`
196
- (`dist/bin/cargo-hauler-install.js`, generated by the build and gated by
197
- `agent-bundle prepack`). `agent-bundle doctor --host <host>` reports the
198
- installed copy versus the artifact (`current`, `stale`, `version-mismatch`,
199
- `foreign`, `not-installed`) and, for Cursor, whether the manifest hooks are
200
- registered. Each pack's `INSTALL.md` carries the same commands with the exact
201
- compiled names. Restart or reload the host after installing so new sessions
202
- load the hooks. Per-host notes, hook timeouts, and the optional PATH shim are
203
- in [docs/install.md](docs/install.md).
204
-
205
- The first brokered request makes one daemon-start attempt. Hooks cover Cargo
206
- commands submitted through supported agent shells; the optional PATH shim
207
- (`node dist/bin/hauler.js install-shim`) also covers Cargo invoked by scripts
208
- 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).
209
66
 
210
- ## Interfaces
67
+ ## Commands and tools
211
68
 
212
- `hauler` is the process entry (`src/scripts/hauler.ts`): `exec`, `daemon`, and
213
- `install-shim`, forwarding every other command to the routed `cargo-hauler`
214
- executable beside it (`dist/bin/cargo-hauler.js` in the package,
215
- `bin/cargo-hauler.mjs` inside every host pack). Routed commands accept
216
- `--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.
217
73
 
218
74
  | Command | Behavior |
219
75
  | --- | --- |
@@ -225,34 +81,22 @@ executable beside it (`dist/bin/cargo-hauler.js` in the package,
225
81
  | `hauler result <ticket> [--full]` | A stored ticket; running tickets include a live output tail. The document names the full on-disk output log (`Full output: <path> (size)`) and `--json` carries it as `request.outputPath`; `--full` prints that whole log instead of the tail (the last ~768 KiB when it does not fit, with the path for the rest). |
226
82
  | `hauler kill <ticket>` | Stop a ticket: drop it from the queue or SIGTERM (then SIGKILL) its cargo process group, freeing the lane. Riders return to their lane or fail with it. |
227
83
  | `hauler request [--session ID] [--host HOST] [--cwd DIR] [--after TICKET …] -- <cargo …>` | Submit a background request and return its ticket, with where it landed in its lane (`queued behind cc-3281 (~13m)`, `waiting for cc-3281`, or `attached to cc-3281`). `--after` works as for `exec`. |
228
- | `hauler daemon <run\|start\|stop\|status>` | Manage the daemon lifecycle. |
84
+ | `hauler daemon <run\|start\|stop\|status\|restart>` | Manage the daemon lifecycle. `restart` sends the graceful stop, waits up to 5 s for the old pid to exit, then starts a daemon from this install and prints both (`restarted: pid 741314 (0.4.1) → pid 742001 (0.4.4)`); a daemon that has not exited by then is reported, not killed, and nothing is started (exit `1`). Tickets in flight at the restart are not handed over: the new daemon marks them `killed` with the error `orphaned by daemon restart`. |
229
85
  | `hauler install-shim [--dir DIR] [--real-cargo PATH] [--force]` | Install the optional PATH shim. |
230
86
 
231
87
  The `hauler` MCP server projects the same operations as `hauler_status`,
232
88
  `hauler_log`, `hauler_last`, `hauler_await`, `hauler_result`, `hauler_kill`,
233
89
  and `hauler_request`, with the same filters as the CLI.
234
90
 
235
- ## Testing
236
-
237
- ```sh
238
- pnpm run check # validate + build + typecheck + Effect diagnostics + rstest + route tests
239
- ```
240
-
241
- `tests/route-unit/` renders the app through the framework compiler with no
242
- artifact build, at the harness proof levels:
91
+ ## Dashboard
243
92
 
244
- | Level | Suite | What it proves |
245
- | --- | --- | --- |
246
- | route-unit | `routes`, `layout`, `streaming`, `events` | documents, shell metadata, Suspense fallbacks and settled values, lineage attribution, event decisions |
247
- | cli-dispatch | `cli-dispatch`, `layout` | argv through the routed CLI shell; Markdown wrapped by the shell, `--json` bare |
248
- | script-dispatch | `script-dispatch` | the `hauler` entry through its `main` envelope as its own process |
249
- | 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 |
250
- | packed-stdio | `packed-contract` | the built `artifact/cursor` server as a real process against a live broker, every tool through the wire-contract matrix |
251
- | 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.
252
98
 
253
- Daemon-backed cases run a real broker in-process with a fake `cargo`
254
- (`tests/harness.ts`) and reach it either through the `haulerDaemon` provider
255
- 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)
256
100
 
257
101
  ## How the broker works
258
102
 
@@ -454,6 +298,23 @@ error `stalled: no CPU for Nm after owner disconnected; killed automatically`.
454
298
  tickets (`--bg`, `hauler_request`) have no streaming connection and are only
455
299
  ever flagged.
456
300
 
301
+ Tickets do not survive a daemon restart. `hauler daemon restart` (or `stop`
302
+ then `start`) ends every queued or running ticket: the cargo processes die
303
+ with the daemon, and the new daemon's first ledger pass marks each of them
304
+ `killed` with the error `orphaned by daemon restart`, so `hauler result cc-N`
305
+ answers `cc-N killed — orphaned by daemon restart: the daemon stopped while it
306
+ was in flight and does not hand runs over; resubmit if the work is still
307
+ needed` rather than looking like a failure of the command itself. Restart when
308
+ the CLI or plugin was upgraded under a running daemon: every document then
309
+ carries `daemon 0.4.2 ≠ cli 0.4.4 — restart it with \`hauler daemon restart\``,
310
+ and a reply the newer client still cannot read is reported as `daemon is 0.4.2
311
+ (pid N, since 3h ago), this CLI is 0.4.4 — restart it with \`hauler daemon
312
+ restart\`` with the first schema mismatch on a second line — never as a raw
313
+ validation dump. The client schemas default the fields older daemons never
314
+ send (`outputPath`, `after`), so a plain version difference alone does not
315
+ break `status`, `result`, or `await`; finish or `hauler kill` what is in
316
+ flight before restarting if the work matters.
317
+
457
318
  The `tool/after` route checks the session's background tickets — `--bg`,
458
319
  `hauler_request`, and synchronous requests the client converted to a ticket
459
320
  — and, on the first tool call after one finishes, adds its result to the
@@ -523,6 +384,80 @@ is reported as unavailable and never rejects a request.
523
384
 
524
385
  ![cargo-hauler dashboard kache timing panel](docs/media/dashboard-kache.png)
525
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.
397
+
398
+ ### With the bundled installer
399
+
400
+ ```sh
401
+ npm install -g cargo-hauler # or run each command as: npx -p cargo-hauler <command>
402
+
403
+ cargo-hauler-install install claude --scope user # user, project, or local
404
+ cargo-hauler-install install codex
405
+ cargo-hauler-install install cursor --mode local # ~/.cursor/plugins/local/cargo-hauler
406
+ cargo-hauler-install install cursor --mode marketplace # stage a local marketplace repo for Customize → Add Plugins from Local Repository
407
+ ```
408
+
409
+ `cargo-hauler-install` runs the host's own plugin commands for you (below),
410
+ detects an installed copy with the same version but different content and
411
+ replaces it, and takes `--replace` (alias `--force`) to replace a different
412
+ installed version. `--json` prints the result for scripts.
413
+
414
+ ### With the hosts' own plugin commands
415
+
416
+ The same result without the installer, from the package or a build (paths are
417
+ relative to `node_modules/cargo-hauler` or the checkout):
418
+
419
+ ```sh
420
+ # Claude Code — a local marketplace plus a plugin install
421
+ cd artifact/claude
422
+ claude plugin marketplace add ./
423
+ claude plugin install cargo-hauler@cargo-hauler-marketplace --scope user
424
+
425
+ # Codex — a local marketplace snapshot
426
+ cd artifact/codex
427
+ codex plugin marketplace add ./
428
+ codex plugin add cargo-hauler@cargo-hauler-marketplace
429
+
430
+ # Cursor — no non-interactive plugin command exists, so the pack ships one
431
+ node artifact/cursor/install.mjs # local plugin (default)
432
+ node artifact/cursor/install.mjs --mode marketplace # local marketplace repository
433
+ ```
434
+
435
+ Upgrading to a new version: `claude plugin marketplace update cargo-hauler-marketplace
436
+ && claude plugin update cargo-hauler@cargo-hauler-marketplace`, `codex plugin
437
+ remove … && codex plugin marketplace add ./ && codex plugin add …`, and
438
+ `node artifact/cursor/install.mjs --replace`. `claude plugin update` is
439
+ version-gated, so after a rebuild that did not bump the version use
440
+ `claude plugin uninstall … --keep-data` and install again (the installer does
441
+ this automatically). Restart or reload the host after installing.
442
+
443
+ ### From a checkout
444
+
445
+ ```sh
446
+ pnpm install
447
+ pnpm run build # artifact/{claude,codex,cursor,portable} + dist/bin
448
+ ```
449
+
450
+ Then install with either method above from `artifact/<host>`, and run
451
+ `node dist/bin/hauler.js install-shim` for the PATH shim. Building needs the
452
+ repository's dev dependencies (including the agent-bundle framework, pinned as
453
+ a pkg.pr.new preview until it is on npm); using the published package does
454
+ not.
455
+
456
+ The first brokered request makes one daemon-start attempt. Hooks cover Cargo
457
+ commands submitted through supported agent shells; the optional PATH shim
458
+ (`hauler install-shim`) also covers Cargo invoked by scripts and terminals.
459
+ Per-host notes and hook timeouts are in [docs/install.md](docs/install.md).
460
+
526
461
  ## Configuration
527
462
 
528
463
  | Variable | Default | Meaning |
@@ -582,7 +517,12 @@ unset, the daemon reads kache's configured local store from
582
517
  `hauler_await` fail loudly when the daemon is unreachable instead of
583
518
  reporting a ticket as not found; `hauler_status`, `hauler_log`, and
584
519
  `hauler_last` read the ledger with the daemon marked `stopped` or
585
- `unresponsive`.
520
+ `unresponsive`. A daemon whose reply the client cannot read fails as a
521
+ version difference naming both versions and `hauler daemon restart`.
522
+ - The state directory is not migrated between installs. Every rendered
523
+ document names the one in use (`state dir …` in the header; `stateRoot` in
524
+ `--json`), so a `CARGO_HAULER_STATE_DIR` change is visible on the next
525
+ command rather than discovered from an empty ledger.
586
526
  - Test sharing uses identity attachment or batch folding, never coverage.
587
527
  Folded `test` and `nextest` requests receive the composite output and exit
588
528
  code, so a failure may come from another package in the batch.
@@ -598,7 +538,188 @@ unset, the daemon reads kache's configured local store from
598
538
  the POSIX PATH shim is unavailable and jobserver integration is disabled.
599
539
  - Licensed under MIT.
600
540
 
601
- ## Development
541
+ ## Architecture
542
+
543
+ <details>
544
+ <summary><strong>How the app is built</strong> — agent-bundle application structure, testing, and development (click to expand)</summary>
545
+
546
+ The plugin is an [agent-bundle](https://github.com/ScriptedAlchemy/agent-bundle)
547
+ application: six MCP tools, a routed CLI, four hook routes, two skills, and a
548
+ browser dashboard, all rendered from one component library through one shared
549
+ layout. This section is for contributors; using cargo-hauler needs none of it.
550
+
551
+ ### Application structure
552
+
553
+ Everything an agent sees is a React Server Component rendered by the
554
+ agent-bundle runtime into an Agent Document, then lowered to MCP content, CLI
555
+ Markdown, `--json`, or a host hook envelope. There is no hand-written server,
556
+ argv parser, or string-concatenated Markdown; the `src/` tree is the app.
557
+
558
+ ```text
559
+ src/
560
+ layout.tsx the hauler shell around every rendered route
561
+ providers/hauler-daemon.ts request-scoped daemon connection + health probe
562
+ components/ typed components over pure view-models
563
+ mcp/hauler/tools/*.tsx hauler_status, _log, _last, _await, _result, _request
564
+ mcp/hauler/apps/dashboard.tsx the MCP App (ui://cargo-hauler/dashboard.html)
565
+ cli/*.tsx, cli/daemon.ts the routed `cargo-hauler` CLI, same components
566
+ events/{session/start,tool/before,tool/after,stop}.tsx hook routes
567
+ skills/cargo-hauler/SKILL.md, skills/hauler-dashboard/SKILL.tsx
568
+ scripts/hauler.ts the `hauler` process entry hooks rewrite cargo to
569
+ daemon/, client/, hooks/, shim/, lib/ the broker and its libraries
570
+ ```
571
+
572
+ #### The shell (`src/layout.tsx`)
573
+
574
+ Every rendered route — MCP tool, CLI command, rendered script — composes
575
+ through one layout, the way a page framework's `layout.tsx` wraps every page:
576
+
577
+ - **Header:** `<DaemonBadge>` prints what the request-start probe proved and
578
+ which state directory it is: `cargo-hauler · daemon running (pid 4021) ·
579
+ 2/5 permits +1 riding, 1 queued · 2 lanes busy · up since 3h ago · state dir
580
+ /fast/cache/cargo-hauler`, or `daemon stopped · no socket; it starts on
581
+ demand…`, or `daemon unresponsive · did not accept a connection within
582
+ 750ms (machine saturated)…`. When the daemon is another build than the CLI
583
+ or MCP server rendering the document, a second line says so: `cargo-hauler
584
+ · daemon 0.4.2 ≠ cli 0.4.4 — restart it with \`hauler daemon restart\``.
585
+ - **Body:** the route's own document, unchanged. The route keeps its
586
+ `<Agent.Result value>`; the runtime merges it into the shell so
587
+ `structuredContent` and `--json` are exactly what the route declared.
588
+ - **Footer:** `<LineageFooter>` names the conversation the request belongs to
589
+ (`Requested by conversation conv-7f (depth 1 under conv-2a; registry)`),
590
+ read synchronously with `useAgent()`, and stays silent when the host cannot
591
+ place the request rather than guessing.
592
+ - **`_meta.hauler`** on every MCP result: `route`, `surface`, `server`,
593
+ `version`, `daemon: { state, pid? }`, `lineage: { conversation, root, depth } | null`.
594
+
595
+ Event routes are host protocol responses and are never wrapped.
596
+
597
+ #### The daemon provider (`src/providers/hauler-daemon.ts`)
598
+
599
+ One request-context provider mounts `providers.haulerDaemon` for every tool,
600
+ command, event, and script: the resolved `config` (state dir, socket, ledger)
601
+ and a `health` value from one bounded `status` probe:
602
+
603
+ | `health.state` | meaning |
604
+ | --- | --- |
605
+ | `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) |
606
+ | `stopped` | `socket-missing` (starts on demand) or `connection-refused` (stale socket) |
607
+ | `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 |
608
+ | `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 |
609
+ | `unprobed` | `event-surface`: hooks run on every shell command and skip the probe by design |
610
+
611
+ The provider fails closed on nothing it can observe and fabricates nothing.
612
+ Routes read it through `requestDaemon(context)` / `requestDaemonConfig(context)`;
613
+ tests inject a fixture through the harness `context.providers` seam.
614
+
615
+ #### Components (`src/components/`)
616
+
617
+ Components render view-models and nothing else. The models are pure functions
618
+ in `view-models.ts`, so the MCP document, the CLI Markdown, and a test
619
+ assertion share one derivation.
620
+
621
+ | Component | Renders |
622
+ | --- | --- |
623
+ | `<TicketCard>` | one ticket: headline, attribution, lane, queue position, attach mode, timings, exit, then `<BuildDiagnostics>` and `<LogTail>` |
624
+ | `<TicketList>` | the in-flight and recent tables of status, and the whole of log |
625
+ | `<LaneBoard>` | busy lanes with their leader ticket, its command, and how long it has run |
626
+ | `<AdmissionState>` | permits in use, load, memory clamp, sharing savings; calls out a paused admission gate |
627
+ | `<KacheStats>` | kache coverage and freshness, slowest crates by profile, or an honest "not detected" |
628
+ | `<LogTail>` | the captured output tail, labelled live while the run is in progress |
629
+ | `<FullOutput>` | where the ticket's whole output log lives and how large it is; under `full`, the log itself in code-block chunks |
630
+ | `<BuildDiagnostics>` | an index of cargo `error[E…]`/`warning:` blocks (level / code / message / location) followed by every captured block verbatim |
631
+ | `<DashboardLink>` | where the MCP App lives and how to open it elsewhere |
632
+ | `<TicketGuidance>` | what to do next, one component per ticket status |
633
+ | `<DaemonBadge>`, `<LineageFooter>` | the shell header and footer |
634
+ | `<EmptyState>`, `<UnavailableState>`, `<ErrorState>` | the three non-happy shapes every document may take |
635
+
636
+ `documents.tsx` composes them into one document per hauler result
637
+ (`StatusDocument`, `LogDocument`, `LastDocument`, `ResultDocument`,
638
+ `AwaitDocument`, `RequestDocument`); the MCP tool and the CLI command for the
639
+ same operation render the same document with different command spellings
640
+ (`surface.ts`).
641
+
642
+ #### Streaming (`src/components/streaming.tsx`)
643
+
644
+ `hauler_await` and `hauler_log` are progressive documents. Each is a
645
+ valueless `Agent.Result` container around one `Suspense` boundary:
646
+
647
+ - `<AwaitStream>`: the fallback is the ticket **as it is now** — its live
648
+ output tail and a progress node — rendered before the daemon-side wait
649
+ blocks; the settled child is the ordinary `AwaitDocument`. MCP hosts receive
650
+ the fallback's progress as notifications and the settled value as
651
+ `structuredContent`; the routed CLI updates the terminal in place. Heartbeats
652
+ (queue position, elapsed time, cost estimate) still flow through
653
+ `context.progress`.
654
+ - `<LogStream>`: a "reading the ledger" progress frame, then the listing.
655
+
656
+ #### Attribution and lineage
657
+
658
+ `hauler_request` attributes tickets from the request context: an explicit
659
+ `host`/`session` wins; otherwise the negotiated host and native session are
660
+ used; and when the transport publishes no session id (bare stdio MCP), the
661
+ conversation from `request.lineage` becomes the session of record. That is
662
+ what makes parallel agents' builds attributable in the ledger, the dashboard,
663
+ and `hauler status --session <conversation>` (the `hauler_status` tool takes
664
+ the same filter as its `session` field). Results carry
665
+ `attribution: { host, session, lineage }`.
666
+
667
+ #### Routes
668
+
669
+ | Route | Surface | Document |
670
+ | --- | --- | --- |
671
+ | `tool:hauler/hauler_status` · `cli:status` | queue, lanes, admission, kache, filters | `StatusDocument`; the tool advertises the dashboard App |
672
+ | `tool:hauler/hauler_log` · `cli:log` | recent requests | `LogStream` → `LogDocument` |
673
+ | `tool:hauler/hauler_last` · `cli:last` | most recent request | `LastDocument` |
674
+ | `tool:hauler/hauler_await` · `cli:await` | long-poll a ticket (≤ 2 h) | `AwaitStream` → `AwaitDocument` |
675
+ | `tool:hauler/hauler_result` · `cli:result` | one ticket, live tail while running; `full` renders the whole on-disk output log | `ResultDocument` (`<FullOutput>`) |
676
+ | `tool:hauler/hauler_kill` · `cli:kill` | stop a queued or running ticket | `KillDocument` |
677
+ | `tool:hauler/hauler_request` · `cli:request` | submit a background request | `RequestDocument` |
678
+ | `cli:daemon` | `run` / `start` / `stop` / `status` | plain JSON, exit code from the result |
679
+ | `event:session/start` | new session | daemon state and the no-kill rule as context |
680
+ | `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 |
681
+ | `event:tool/after` | shell tool finished | injects finished background-ticket results once per session |
682
+ | `event:stop` | agent stopping | holds the stop while a foreground ticket is pending (bounded, re-deniable) |
683
+
684
+ #### Skills
685
+
686
+ `skills/cargo-hauler/SKILL.md` is the operating rule set (do not kill
687
+ in-flight cargo, scope with `-p`, await tickets, fail open when the daemon is
688
+ unreachable). `skills/hauler-dashboard/SKILL.tsx` is a rendered skill: the
689
+ build computes its Markdown from the tool and CLI spellings and the App
690
+ resource URI it describes, so the document cannot drift from the surface.
691
+
692
+ #### Dashboard
693
+
694
+ `src/mcp/hauler/apps/dashboard.tsx` is the MCP App at
695
+ `ui://cargo-hauler/dashboard.html`, attached to `hauler_status` on hosts that
696
+ render MCP Apps. It shows contention and admission, in-flight and queued
697
+ work, metrics windows, optional kache data, lanes, and history, with a live
698
+ output drawer per ticket.
699
+
700
+ ### Testing
701
+
702
+ ```sh
703
+ pnpm run check # validate + build + typecheck + Effect diagnostics + rstest + route tests
704
+ ```
705
+
706
+ `tests/route-unit/` renders the app through the framework compiler with no
707
+ artifact build, at the harness proof levels:
708
+
709
+ | Level | Suite | What it proves |
710
+ | --- | --- | --- |
711
+ | route-unit | `routes`, `layout`, `streaming`, `events` | documents, shell metadata, Suspense fallbacks and settled values, lineage attribution, event decisions |
712
+ | cli-dispatch | `cli-dispatch`, `layout` | argv through the routed CLI shell; Markdown wrapped by the shell, `--json` bare |
713
+ | script-dispatch | `script-dispatch` | the `hauler` entry through its `main` envelope as its own process |
714
+ | 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 |
715
+ | packed-stdio | `packed-contract` | the built `artifact/cursor` server as a real process against a live broker, every tool through the wire-contract matrix |
716
+ | workbench-surface | `tests/workbench-surface.test.ts` | what `agent-bundle dev` would show: catalog, provider, lifecycles per host, counts |
717
+
718
+ Daemon-backed cases run a real broker in-process with a fake `cargo`
719
+ (`tests/harness.ts`) and reach it either through the `haulerDaemon` provider
720
+ seam or through `CARGO_HAULER_STATE_DIR`.
721
+
722
+ ### Development
602
723
 
603
724
  ```sh
604
725
  pnpm run dev # agent-bundle workbench with live rebuilds
@@ -630,3 +751,5 @@ which is why `tests/workbench-surface.test.ts` lives in the plain pool).
630
751
  `repos/effect` is a read-only subtree containing the Effect v4 source pinned to
631
752
  `effect@4.0.0-rc.112`; see `AGENTS.md` before working with Effect code in this
632
753
  repository.
754
+
755
+ </details>