harness-dispatch 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/CHANGELOG.md +152 -0
  2. package/README.md +232 -87
  3. package/config.default.yaml +416 -397
  4. package/dist/auth.d.ts.map +1 -1
  5. package/dist/auth.js +2 -2
  6. package/dist/auth.js.map +1 -1
  7. package/dist/billing.d.ts +35 -4
  8. package/dist/billing.d.ts.map +1 -1
  9. package/dist/billing.js +48 -17
  10. package/dist/billing.js.map +1 -1
  11. package/dist/bin.d.ts.map +1 -1
  12. package/dist/bin.js +147 -91
  13. package/dist/bin.js.map +1 -1
  14. package/dist/breaker-store.d.ts +60 -9
  15. package/dist/breaker-store.d.ts.map +1 -1
  16. package/dist/breaker-store.js +246 -50
  17. package/dist/breaker-store.js.map +1 -1
  18. package/dist/circuit-breaker.d.ts +35 -0
  19. package/dist/circuit-breaker.d.ts.map +1 -1
  20. package/dist/circuit-breaker.js +68 -11
  21. package/dist/circuit-breaker.js.map +1 -1
  22. package/dist/config/coercions.d.ts +31 -0
  23. package/dist/config/coercions.d.ts.map +1 -0
  24. package/dist/config/coercions.js +169 -0
  25. package/dist/config/coercions.js.map +1 -0
  26. package/dist/config/env-interpolation.d.ts +36 -0
  27. package/dist/config/env-interpolation.d.ts.map +1 -0
  28. package/dist/config/env-interpolation.js +73 -0
  29. package/dist/config/env-interpolation.js.map +1 -0
  30. package/dist/config/protocol.d.ts +51 -0
  31. package/dist/config/protocol.d.ts.map +1 -0
  32. package/dist/config/protocol.js +300 -0
  33. package/dist/config/protocol.js.map +1 -0
  34. package/dist/config/route-fields.d.ts +72 -0
  35. package/dist/config/route-fields.d.ts.map +1 -0
  36. package/dist/config/route-fields.js +128 -0
  37. package/dist/config/route-fields.js.map +1 -0
  38. package/dist/config/validation.d.ts +72 -0
  39. package/dist/config/validation.d.ts.map +1 -0
  40. package/dist/config/validation.js +189 -0
  41. package/dist/config/validation.js.map +1 -0
  42. package/dist/config.d.ts +19 -2
  43. package/dist/config.d.ts.map +1 -1
  44. package/dist/config.js +214 -635
  45. package/dist/config.js.map +1 -1
  46. package/dist/configure-yaml.d.ts +23 -0
  47. package/dist/configure-yaml.d.ts.map +1 -0
  48. package/dist/configure-yaml.js +193 -0
  49. package/dist/configure-yaml.js.map +1 -0
  50. package/dist/dispatch-log.d.ts.map +1 -1
  51. package/dist/dispatch-log.js +2 -3
  52. package/dist/dispatch-log.js.map +1 -1
  53. package/dist/dispatchers/base.d.ts +3 -0
  54. package/dist/dispatchers/base.d.ts.map +1 -1
  55. package/dist/dispatchers/base.js.map +1 -1
  56. package/dist/dispatchers/generic-cli.d.ts +15 -1
  57. package/dist/dispatchers/generic-cli.d.ts.map +1 -1
  58. package/dist/dispatchers/generic-cli.js +69 -5
  59. package/dist/dispatchers/generic-cli.js.map +1 -1
  60. package/dist/dispatchers/openai-compatible.d.ts.map +1 -1
  61. package/dist/dispatchers/openai-compatible.js +70 -2
  62. package/dist/dispatchers/openai-compatible.js.map +1 -1
  63. package/dist/dispatchers/shared/kill-tree.d.ts +17 -4
  64. package/dist/dispatchers/shared/kill-tree.d.ts.map +1 -1
  65. package/dist/dispatchers/shared/kill-tree.js +28 -4
  66. package/dist/dispatchers/shared/kill-tree.js.map +1 -1
  67. package/dist/dispatchers/shared/rate-limit-headers.d.ts +17 -0
  68. package/dist/dispatchers/shared/rate-limit-headers.d.ts.map +1 -1
  69. package/dist/dispatchers/shared/rate-limit-headers.js +27 -0
  70. package/dist/dispatchers/shared/rate-limit-headers.js.map +1 -1
  71. package/dist/dispatchers/shared/stream-subprocess.d.ts +11 -0
  72. package/dist/dispatchers/shared/stream-subprocess.d.ts.map +1 -1
  73. package/dist/dispatchers/shared/stream-subprocess.js +62 -35
  74. package/dist/dispatchers/shared/stream-subprocess.js.map +1 -1
  75. package/dist/dispatchers/shared/subprocess.d.ts.map +1 -1
  76. package/dist/dispatchers/shared/subprocess.js +3 -0
  77. package/dist/dispatchers/shared/subprocess.js.map +1 -1
  78. package/dist/dispatchers/shared/which-available.d.ts.map +1 -1
  79. package/dist/dispatchers/shared/which-available.js +16 -2
  80. package/dist/dispatchers/shared/which-available.js.map +1 -1
  81. package/dist/file-lock.d.ts +17 -0
  82. package/dist/file-lock.d.ts.map +1 -0
  83. package/dist/file-lock.js +143 -0
  84. package/dist/file-lock.js.map +1 -0
  85. package/dist/http/parse.d.ts +64 -0
  86. package/dist/http/parse.d.ts.map +1 -0
  87. package/dist/http/parse.js +230 -0
  88. package/dist/http/parse.js.map +1 -0
  89. package/dist/http/server.d.ts +2 -0
  90. package/dist/http/server.d.ts.map +1 -1
  91. package/dist/http/server.js +249 -286
  92. package/dist/http/server.js.map +1 -1
  93. package/dist/index.d.ts +1 -1
  94. package/dist/index.d.ts.map +1 -1
  95. package/dist/index.js +3 -2
  96. package/dist/index.js.map +1 -1
  97. package/dist/job-runner.js +23 -5
  98. package/dist/job-runner.js.map +1 -1
  99. package/dist/jobs/context.d.ts +17 -0
  100. package/dist/jobs/context.d.ts.map +1 -0
  101. package/dist/jobs/context.js +84 -0
  102. package/dist/jobs/context.js.map +1 -0
  103. package/dist/jobs/store.d.ts +80 -0
  104. package/dist/jobs/store.d.ts.map +1 -0
  105. package/dist/jobs/store.js +279 -0
  106. package/dist/jobs/store.js.map +1 -0
  107. package/dist/jobs/types.d.ts +135 -0
  108. package/dist/jobs/types.d.ts.map +1 -0
  109. package/dist/jobs/types.js +11 -0
  110. package/dist/jobs/types.js.map +1 -0
  111. package/dist/jobs.d.ts +165 -85
  112. package/dist/jobs.d.ts.map +1 -1
  113. package/dist/jobs.js +824 -208
  114. package/dist/jobs.js.map +1 -1
  115. package/dist/leaderboard.d.ts +24 -1
  116. package/dist/leaderboard.d.ts.map +1 -1
  117. package/dist/leaderboard.js +94 -2
  118. package/dist/leaderboard.js.map +1 -1
  119. package/dist/mcp/config-hot-reload.d.ts.map +1 -1
  120. package/dist/mcp/config-hot-reload.js +1 -1
  121. package/dist/mcp/config-hot-reload.js.map +1 -1
  122. package/dist/mcp/dispatcher-factory.d.ts +16 -1
  123. package/dist/mcp/dispatcher-factory.d.ts.map +1 -1
  124. package/dist/mcp/dispatcher-factory.js +41 -3
  125. package/dist/mcp/dispatcher-factory.js.map +1 -1
  126. package/dist/mcp/tool-schemas.d.ts +194 -0
  127. package/dist/mcp/tool-schemas.d.ts.map +1 -0
  128. package/dist/mcp/tool-schemas.js +311 -0
  129. package/dist/mcp/tool-schemas.js.map +1 -0
  130. package/dist/mcp/tools.d.ts +26 -51
  131. package/dist/mcp/tools.d.ts.map +1 -1
  132. package/dist/mcp/tools.js +122 -151
  133. package/dist/mcp/tools.js.map +1 -1
  134. package/dist/observability/index.d.ts +16 -3
  135. package/dist/observability/index.d.ts.map +1 -1
  136. package/dist/observability/index.js +23 -7
  137. package/dist/observability/index.js.map +1 -1
  138. package/dist/observability/spans.d.ts.map +1 -1
  139. package/dist/observability/spans.js +4 -1
  140. package/dist/observability/spans.js.map +1 -1
  141. package/dist/quota.d.ts +106 -22
  142. package/dist/quota.d.ts.map +1 -1
  143. package/dist/quota.js +198 -58
  144. package/dist/quota.js.map +1 -1
  145. package/dist/route-policy.d.ts.map +1 -1
  146. package/dist/route-policy.js +13 -3
  147. package/dist/route-policy.js.map +1 -1
  148. package/dist/router.d.ts +29 -1
  149. package/dist/router.d.ts.map +1 -1
  150. package/dist/router.js +102 -40
  151. package/dist/router.js.map +1 -1
  152. package/dist/safety.d.ts.map +1 -1
  153. package/dist/safety.js +11 -2
  154. package/dist/safety.js.map +1 -1
  155. package/dist/state-dir.d.ts +17 -0
  156. package/dist/state-dir.d.ts.map +1 -0
  157. package/dist/state-dir.js +21 -0
  158. package/dist/state-dir.js.map +1 -0
  159. package/dist/status.d.ts +49 -0
  160. package/dist/status.d.ts.map +1 -1
  161. package/dist/status.js +67 -4
  162. package/dist/status.js.map +1 -1
  163. package/dist/types.d.ts +74 -1
  164. package/dist/types.d.ts.map +1 -1
  165. package/dist/working-dir.d.ts +13 -0
  166. package/dist/working-dir.d.ts.map +1 -1
  167. package/dist/working-dir.js +27 -0
  168. package/dist/working-dir.js.map +1 -1
  169. package/dist/workspace-lock.d.ts +40 -0
  170. package/dist/workspace-lock.d.ts.map +1 -0
  171. package/dist/workspace-lock.js +258 -0
  172. package/dist/workspace-lock.js.map +1 -0
  173. package/dist/workspace-resolve.d.ts +98 -0
  174. package/dist/workspace-resolve.d.ts.map +1 -0
  175. package/dist/workspace-resolve.js +382 -0
  176. package/dist/workspace-resolve.js.map +1 -0
  177. package/dist/workspaces.d.ts +16 -0
  178. package/dist/workspaces.d.ts.map +1 -1
  179. package/dist/workspaces.js +166 -22
  180. package/dist/workspaces.js.map +1 -1
  181. package/package.json +99 -96
  182. package/dist/dashboard/live.d.ts +0 -49
  183. package/dist/dashboard/live.d.ts.map +0 -1
  184. package/dist/dashboard/live.js +0 -149
  185. package/dist/dashboard/live.js.map +0 -1
package/CHANGELOG.md ADDED
@@ -0,0 +1,152 @@
1
+ # Changelog
2
+
3
+ Notable changes per release. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/);
4
+ this project uses [semantic versioning](https://semver.org/spec/v2.0.0.html) and is
5
+ pre-1.0, so minor versions can carry behaviour changes.
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.6.0] — 2026-08-21
10
+
11
+ The job lifecycle is complete: start, watch, **stop**, **retry**, and **resolve the
12
+ isolated result**. Every verb was exercised against real harness CLIs rather than
13
+ fakes. The MCP surface grows from three tools to six.
14
+
15
+ ### Added
16
+
17
+ - **`retry_job`** — run a finished job's task again from its own record: the same
18
+ prompt the delegate actually saw (context preamble included), files, working
19
+ directory, hints and workspace policy. Pass `service` to send the retry to a
20
+ DIFFERENT route, which is the usual reason to retry — the task was fine and the
21
+ route was not. Returns a new jobId; the original is untouched, and retrying a
22
+ still-running job is refused so two attempts cannot race on one directory.
23
+ - **Token totals in `usage`** — `inputTokens` and `outputTokens` per route, summed
24
+ from what harnesses actually report. Deliberately not money: subscription CLIs have
25
+ no per-call price, and pricing tokens would mean shipping a rate card that goes
26
+ stale silently. Zero means the harness reported nothing, not that nothing was spent.
27
+ - **`workspace`** — inspect, keep or discard the isolated result of a job. `copy` and
28
+ `git_worktree` dispatches already ran the agent in isolation and never touched your
29
+ project, but there was no way to see the actual change, no way to keep it, and no
30
+ cleanup but by hand — so isolated runs were effectively write-only. `action: "diff"`
31
+ returns the real patch, `"apply"` applies it to your project (refusing when the
32
+ project has uncommitted changes, because the patch was built against a clean base),
33
+ and `"discard"` removes the workspace. The patch is always written to the job
34
+ directory, so applying it by hand is available even when the automatic path declines.
35
+ - **`resource_weight`** on a route — the concurrency bound now counts CAPACITY, not
36
+ jobs. `max_concurrent_runs` priced an HTTP call to a local endpoint the same as a
37
+ whole Claude Code process, so four cheap endpoint calls could lock out a real
38
+ dispatch. Defaults to 1.0 for CLI routes and 0.1 for endpoints; with every weight at
39
+ 1.0 the arithmetic is exactly the old count, so existing configs are unchanged.
40
+ - **`cancel_job`** — stop a dispatch you already started. Until now the product could
41
+ start a 60-minute detached run and offer no way to stop it, so a misdirected agent
42
+ kept spending subscription quota and editing a workspace until it finished or timed
43
+ out. A queued job stops outright; a running one tears down within about a second,
44
+ killing the agent CLI and its child processes. Two things it does not do: it does
45
+ not revert files the agent already changed, and it does not count as a route
46
+ failure — the caller changing their mind says nothing about whether the route works.
47
+
48
+ ### Changed
49
+
50
+ - All three route shapes (`clis:`, `endpoints:`, `services:`) resolve their shared
51
+ settings through one table instead of three hand-written lists. This retires the
52
+ class of bug that produced five separate silent drops, where a correctly spelled,
53
+ correctly valued setting was read by one route shape and ignored by another. A
54
+ legacy `services:` entry naming a harness now also inherits that harness's
55
+ `leaderboard_model` and `thinking_level`, which it previously lost.
56
+ - `HARNESS_DISPATCH_CONFIG` pointing at a file that does not exist is now reported by
57
+ the server instead of being silently replaced with auto-detected defaults.
58
+ - `install-codex.mjs --config` resolves to an absolute path and is checked at install
59
+ time. A relative path used to be stored as written and resolved later against a
60
+ different working directory, which silently meant "no config".
61
+
62
+ ### Removed
63
+
64
+ - The unused dashboard renderer (`renderDashboard`, `DashboardState`). Nothing in the
65
+ product called it; the `dashboard` CLI alias maps to `status` and is unaffected.
66
+
67
+ ### Fixed
68
+
69
+ - Copying a live working directory survives a file disappearing mid-copy. A working
70
+ directory is being written to while it is read — an editor saving, a build watcher
71
+ cleaning, another fanout arm — and any of those used to fail the whole dispatch with
72
+ a raw ENOENT. A vanished file is now skipped and named in the workspace notes; a
73
+ permission error or a full disk still fails loudly.
74
+ - Workspace copies ask the filesystem for a copy-on-write reflink
75
+ (`COPYFILE_FICLONE`), which makes a clone near-instant and allocation-free on APFS,
76
+ Btrfs/XFS and ReFS/Dev Drive, and falls back to an ordinary copy everywhere else.
77
+ - `workspace` on a `copy` job no longer emits a spurious deletion of the file it is
78
+ editing, and no longer refuses to apply because of its own scratch directory. The
79
+ workspace lives inside the project, which broke both.
80
+ - Live agent smoke tests no longer write circuit-breaker state into the real install,
81
+ so a smoke failure cannot block a healthy route for real dispatches.
82
+
83
+ ## [0.5.0] — 2026-08-20
84
+
85
+ Published to npm on 2026-08-21, with a signed provenance attestation from GitHub
86
+ Actions. Anyone on 0.4.0 should upgrade: that release writes resolved API keys into
87
+ `configure` output and can destroy a hand-written config.
88
+
89
+ ### Security
90
+
91
+ - `configure` no longer writes resolved API keys into its output. Keys that came from
92
+ a `${VAR}` reference round-trip as that reference; a literal key is redacted in
93
+ `--print` (which people paste into bug reports) and preserved on disk.
94
+ - A dispatch asking for a lower safety profile can no longer be silently ignored. The
95
+ keys that grant or restrict access are rejected by name when misplaced, on the MCP
96
+ surface that actually ships — a previous guard existed only on a code path nothing
97
+ called.
98
+ - Symlinks can no longer carry an agent out of an isolated (`copy`) workspace.
99
+ - Endpoint hostnames are redacted from status output.
100
+
101
+ ### Added
102
+
103
+ - Chain delegated work: pass `contextJobs` and an earlier job's prompt and output are
104
+ rendered into the new prompt directly, without routing them through the orchestrator's
105
+ own context.
106
+ - `max_concurrent_runs` (default 4) bounds how many agent CLIs run at once,
107
+ machine-wide. Extra dispatches queue and start as slots free; nothing is rejected.
108
+ - A route's capability floor can vary by requested safety profile, which lets Cursor
109
+ serve read-only work on Windows instead of being skipped entirely.
110
+ - `usage` reports per-route call counts, quota, billing kind and breaker state,
111
+ including work done by detached job runners in other processes.
112
+
113
+ ### Changed
114
+
115
+ - Arena ELO scoring is opt-in (`leaderboard: { enabled: true }`). Routing defaults to
116
+ the tier and weight you configured rather than a benchmark maintained elsewhere.
117
+ - Rate-limited calls are counted separately from failures, so a busy route does not
118
+ accumulate a permanent record of being unreliable.
119
+ - `retention: { jobs_days: 0 }` means keep forever, and a running job with a live
120
+ heartbeat is never pruned.
121
+
122
+ ### Fixed
123
+
124
+ - Nothing is lost to a timeout. Every dispatch runs as a detached background job that
125
+ survives a client timeout, a server restart, and the MCP call's own deadline.
126
+ - `workspace_policy: shared_locked` serializes across all processes, not just within
127
+ one. Stale-lock takeover is atomic, so two dispatches can no longer both decide a
128
+ lock is free and edit the same directory.
129
+ - Circuit-breaker cooldowns and failure decay survive process boundaries and upgrades;
130
+ `status` and `dispatch` no longer disagree about whether a route is available.
131
+ - The OpenAI-compatible HTTP endpoint is backed by a persisted job, so a client that
132
+ times out mid-run can still retrieve the result.
133
+ - A rate limit is detected from HTTP status context rather than any occurrence of the
134
+ digits 429 in an agent's output, which used to block healthy routes.
135
+ - Bad input gets one actionable line instead of a stack trace or a silent default —
136
+ a missing `--config`, a malformed YAML file, an unknown route id, a typo'd setting.
137
+ - `configure --yes --force` no longer deletes harnesses the user added.
138
+ - On macOS and Linux, timing out an agent CLI also stops the shells and test runners
139
+ it spawned.
140
+
141
+ ## [0.4.0] — 2026-07-25
142
+
143
+ First release under the `harness-dispatch` name (previously `harness-router`). Bumped
144
+ the MCP surface to three tools: `dispatch`, `job_status`, `usage`.
145
+
146
+ Known issues in this release, fixed in 0.5.0: `configure` writes resolved API keys into
147
+ its output, and `configure --yes --force` can delete user-added harnesses.
148
+
149
+ [Unreleased]: https://github.com/fstubner/harness-dispatch/compare/v0.6.0...HEAD
150
+ [0.6.0]: https://github.com/fstubner/harness-dispatch/compare/v0.5.0...v0.6.0
151
+ [0.5.0]: https://github.com/fstubner/harness-dispatch/compare/v0.4.0...v0.5.0
152
+ [0.4.0]: https://github.com/fstubner/harness-dispatch/releases/tag/v0.4.0
package/README.md CHANGED
@@ -1,97 +1,190 @@
1
1
  # harness-dispatch
2
2
 
3
- Delegate coding tasks to the agent CLIs you already pay for. harness-dispatch is a
4
- local MCP server that turns the coding harnesses on your machine (Claude Code,
5
- Codex, Cursor Agent, Antigravity CLI) and any local or remote API endpoint into
6
- tools any AI can call routing each task to the best-fit agent, spending the
7
- flat-rate subscription quota you already own first. Routes with no billing backstop
8
- at all a raw metered API key, or unknown billing — stay blocked until you opt in.
9
-
10
- Exposing harnesses as explicit tools is more reliable than telling an agent to shell
11
- out to another CLI: models are trained on tool calling, so they actually use tools
12
- they are given. And it routes whole tasks, not API requests each harness keeps its
13
- own scaffolding, test loop, and codebase context; there's no proxy in between. Long
14
- tasks run as async jobs: start one, get an id back immediately, check for partial
15
- output, collect the result when it finishes. Runs execute in a detached runner
16
- process, so nothing is lost to a client timeout — or to the server itself
17
- restarting mid-run.
18
- (There is no Gemini CLI dispatcher; Google discontinued that CLI's backend in
19
- mid-2026, and Antigravity CLI is its replacement.)
20
-
21
- **What this does on your machine**, stated plainly before you install it: it spawns the
22
- CLI subprocesses above with your prompts, which can read and write files under the
23
- `workingDir` you pass it (this is the point it's a coding agent router) and, depending
24
- on the workspace/safety policy in effect, run shell commands via those CLIs. Running
25
- `serve` additionally binds a local HTTP port (loopback by default, bearer-token gated —
26
- see [HTTP Surface](#http-surface) before pointing `--host` anywhere else). Nothing here
27
- is unusual for a coding-agent tool, but it's worth having in one place rather than
28
- inferred from separate sections.
29
-
30
- **A configured harness runs automatically** — nothing extra to switch on. If that
31
- harness's account has paid/overage billing enabled on the *provider's* side (Cursor's
32
- on-demand billing, Claude's usage credits, Codex's flexible credits, a raw metered API
33
- key, etc.), harness-dispatch will use it too — it does not detect or prevent provider-side
34
- billing state. It only blocks a route by default when there's no provider-side backstop
35
- at all (a raw metered API key, or unknown billing), until you set `allow_paid_usage: true`
36
- on it. See [Adding a harness](#adding-a-harness) for the config, and `status --json` /
37
- `status`'s `note:` lines for a given route's billing classification.
38
-
39
- ## Requirements
40
-
41
- - Node.js `>=24.15.0`
42
- - At least one configured harness or OpenAI-compatible endpoint
3
+ **Route whole coding tasks — not API requests — to the agent CLIs you already pay for.**
4
+
5
+ Each harness keeps its own scaffolding, test loop, and codebase index. There's no
6
+ proxy in between and nothing is re-implemented: Claude Code stays Claude Code. One
7
+ orchestrating agent picks the right one per task and spends your flat-rate
8
+ subscription quota before anything metered.
9
+
10
+ It's a local MCP server, so the harnesses on your machine (Claude Code, Codex,
11
+ Cursor Agent, Antigravity CLI, plus any local or remote OpenAI-compatible endpoint)
12
+ become tools any AI can call. Exposing them as real tools beats asking an agent to
13
+ shell out to another CLI: models are trained on tool calling, so they actually use
14
+ the tools they're given. (No Gemini CLI: Google discontinued that CLI's backend in
15
+ mid-2026, and Antigravity CLI replaced it.)
16
+
17
+ ## What it looks like
18
+
19
+ Your agent calls one tool:
20
+
21
+ ```json
22
+ {
23
+ "prompt": "Port the retry logic in src/net/ to the new backoff helper, then run the tests.",
24
+ "workingDir": "/path/to/project",
25
+ "hints": { "taskType": "execute" }
26
+ }
27
+ ```
28
+
29
+ Quick tasks come straight back:
30
+
31
+ ```json
32
+ {
33
+ "mode": "single",
34
+ "completed": true,
35
+ "success": true,
36
+ "route": "codex_cli",
37
+ "model": "gpt-5.6-terra",
38
+ "output": "Ported 4 call sites to withBackoff(); 118 tests pass.",
39
+ "durationMs": 47210,
40
+ "routing": { "tier": 1, "taskType": "execute", "reason": "tier 1 best (3 available)" }
41
+ }
42
+ ```
43
+
44
+ Slow ones hand back a `jobId` after 25 seconds instead, and keep running:
45
+
46
+ ```json
47
+ { "mode": "single", "completed": false, "jobId": "job-1786977316001-b49d1232" }
48
+ ```
49
+
50
+ Carry on working, then call `job_status` with that id for a live output tail or the
51
+ finished result. The run lives in a detached process, so **nothing is lost to a client
52
+ timeout — or to the server itself restarting mid-run.**
43
53
 
44
54
  ## Install
45
55
 
56
+ Needs Node.js `>=22.22.2` (so current LTS works) and at least one harness or endpoint.
57
+
46
58
  ```bash
47
59
  npm install -g harness-dispatch
48
60
  harness-dispatch configure --yes
49
61
  harness-dispatch doctor --live
50
62
  ```
51
63
 
52
- `configure --yes` detects installed harnesses and writes `config.yaml`.
53
- Without `--yes` it only previews what it would detect and writes nothing
54
- useful to check first, but not a substitute for the real run above. `doctor`
55
- then verifies the install end-to-end: binary + config load, harness
56
- detection, auth/billing classification, and route readiness. `--live` goes one
57
- step further and routes a single tiny prompt through the best eligible route so
58
- you see a real completion before wiring the server into your agent. The live
59
- probe respects billing policy — it never touches paid or unknown-billing routes
60
- unless you pass `--allow-paid`.
61
-
62
- Your Claude Code / Codex / Cursor subscriptions run by default, no opt-in needed
63
- — `configure`'s output tells you if anything's blocked and why. See
64
- [Adding a harness](#adding-a-harness) below for the config and the paid-usage note.
65
-
66
- > This project was renamed from `harness-router` to `harness-dispatch` — the npm
67
- > package, CLI command, env var prefix (`HARNESS_DISPATCH_*`), and MCP resource URIs
68
- > (`harness-dispatch://status`) all changed together. If you have an older install,
69
- > `npm uninstall -g harness-router && npm install -g harness-dispatch` and update any
70
- > `mcpServers`/`claude_desktop_config.json` entry to invoke `harness-dispatch`, not the
71
- > old command. Two older packages predate this rename and are no longer maintained:
72
- > `harness-router` (`0.3.2` on the registry) and the separately-published
73
- > `harness-router-mcp` (`0.2.0`) — both lack `usage`, `/v1/models`, `/v1/usage`,
74
- > Antigravity support, and every fix described in this README. Note that `npx -y
75
- > harness-dispatch` (used by the plugin's fallback launch path, see
76
- > [plugin/README.md](./plugin/README.md)) resolves to whatever is currently on the npm
77
- > registry, which can lag behind a local clone's `dist/`. Run `npm ls -g harness-dispatch`
78
- > to check which version is actually installed.
79
-
80
- You can also run without a global install:
64
+ `configure --yes` detects installed harnesses and writes `config.yaml`; without
65
+ `--yes` it previews and writes nothing. `doctor` then checks the whole chain:
66
+ binary, config load, harness detection, auth and billing classification, route
67
+ readiness. `--live` goes further and routes one tiny real prompt, so you see a
68
+ completion before wiring anything into your agent. The live probe never touches paid or
69
+ unknown-billing routes unless you pass `--allow-paid`.
81
70
 
82
- ```bash
83
- npx harness-dispatch configure
84
- ```
71
+ Your Claude Code / Codex / Cursor subscriptions run by default with no opt-in;
72
+ `configure` tells you if anything is blocked and why.
73
+
74
+ No global install needed either: `npx harness-dispatch configure`.
85
75
 
86
76
  ### Plugin install (Claude Code / Claude Desktop / Codex)
87
77
 
88
78
  The `plugin/` directory packages the MCP server plus a delegation skill and
89
79
  `/route` + `/jobs` commands for one-step installs — see
90
- [plugin/README.md](./plugin/README.md). Claude Code:
80
+ [plugin/README.md](https://github.com/fstubner/harness-dispatch/blob/main/plugin/README.md)
81
+ (absolute link on purpose: `plugin/` is not shipped in the npm tarball, so a
82
+ relative link is dead on npmjs.com). Claude Code:
91
83
  `/plugin marketplace add <repo path or URL>` then
92
84
  `/plugin install harness-dispatch@harness-dispatch`. Codex:
93
85
  `node plugin/scripts/install-codex.mjs`.
94
86
 
87
+ ## What it does on your machine
88
+
89
+ Stated plainly, up front, rather than left to be inferred:
90
+
91
+ - It **spawns the CLIs above as subprocesses** with your prompts.
92
+ - Those CLIs **read and write files** under the `workingDir` you pass (that's the
93
+ point of the tool) and **run shell commands**, depending on the workspace and
94
+ safety policy in effect.
95
+ - At most **4 agent CLIs run at once**; extra dispatches queue and start as slots
96
+ free. Tune with `max_concurrent_runs`.
97
+ - `serve` additionally binds a local HTTP port: loopback only by default,
98
+ bearer-token gated. Read [HTTP Surface](#http-surface) before pointing `--host`
99
+ anywhere else.
100
+
101
+ None of this is unusual for a coding-agent tool. It's here in one place so you can
102
+ decide before installing rather than after.
103
+
104
+ ## Billing, and what it can't promise
105
+
106
+ **A configured harness runs automatically**, with nothing to switch on. Routes that
107
+ have no billing backstop at all, meaning a raw metered API key or billing it can't
108
+ classify, stay blocked until you set `allow_paid_usage: true` on them.
109
+
110
+ The honest limit: if a harness's account already has paid or overage billing switched
111
+ on at the **provider** (Cursor on-demand, Claude usage credits, Codex flexible
112
+ credits), harness-dispatch will spend that too. It cannot see or change provider-side
113
+ billing state. What it does is refuse routes where *no* provider-side ceiling exists
114
+ at all.
115
+
116
+ Run `status` (or `status --json`) for any route's billing classification; the `note:`
117
+ lines spell out the reasoning per route.
118
+
119
+ <details>
120
+ <summary>Renamed from <code>harness-router</code> — upgrade notes</summary>
121
+
122
+ The npm package, CLI command, env var prefix (`HARNESS_DISPATCH_*`), and MCP resource
123
+ URIs (`harness-dispatch://status`) all changed together. From an older install:
124
+ `npm uninstall -g harness-router && npm install -g harness-dispatch`, then update any
125
+ `mcpServers` / `claude_desktop_config.json` entry to invoke `harness-dispatch`.
126
+
127
+ Two packages predate the rename and are unmaintained: `harness-router` (`0.3.2`) and
128
+ the separately-published `harness-router-mcp` (`0.2.0`). Both lack `usage`,
129
+ `/v1/models`, `/v1/usage`, Antigravity support, and every fix described here.
130
+
131
+ `npx -y harness-dispatch`, the plugin's fallback launch path, resolves to whatever
132
+ is currently on the npm registry, which can lag a local clone's `dist/`. Check with
133
+ `npm ls -g harness-dispatch`.
134
+
135
+ </details>
136
+
137
+ ## Safety profiles, and Cursor on Windows
138
+
139
+ A caller asks for one of three profiles, and each is a limit rather than a
140
+ capability:
141
+
142
+ | Profile | Means |
143
+ |---|---|
144
+ | `read_only` | Look, don't touch |
145
+ | `workspace_edit` | Edit files in the workspace, no arbitrary shell |
146
+ | `full_auto` | Edit files and run shell |
147
+
148
+ A route declares the floor it actually runs at (`effective_safety`), and is
149
+ skipped when that floor exceeds what was asked for. A route is never quietly
150
+ given more access than the caller requested.
151
+
152
+ `cursor_cli` is the interesting case, because its capability differs by mode:
153
+
154
+ - `read_only` uses `--mode plan`, which is genuinely read-only (verified: asked
155
+ to create one file and overwrite another, it did neither).
156
+ - `full_auto` uses print mode, which edits and runs shell.
157
+ - `workspace_edit` is **skipped on Windows**. Cursor's print mode grants write
158
+ and shell together, and `--sandbox enabled` — the flag that would constrain
159
+ shell while allowing edits — is macOS/Linux only. There is no edit-without-shell
160
+ mode to route to, so claiming that level would mean handing shell access to a
161
+ caller who explicitly asked not to have it.
162
+
163
+ Cursor still edits code on Windows. Ask for `full_auto`.
164
+
165
+ ### Overriding it
166
+
167
+ If you accept that Cursor's editing mode carries shell access and you want it
168
+ to serve `workspace_edit` anyway, declare the floor yourself in `config.yaml` —
169
+ your value replaces the shipped default:
170
+
171
+ ```yaml
172
+ clis:
173
+ - name: cursor_cli
174
+ harness: cursor
175
+ command: cursor-agent
176
+ effective_safety:
177
+ read_only: read_only
178
+ workspace_edit: workspace_edit # you are accepting shell access here
179
+ full_auto: full_auto
180
+ ```
181
+
182
+ That is a deliberate local decision, not a bug workaround: the shipped default
183
+ is conservative because the tool cannot verify what a given `cursor-agent`
184
+ build will do. On macOS and Linux the better route is `--sandbox enabled`,
185
+ which constrains shell for real — untested here, so it is not shipped on by
186
+ default.
187
+
95
188
  ## Adding a harness
96
189
 
97
190
  `config.yaml` is entirely optional. There is no separate hidden defaults format —
@@ -264,17 +357,21 @@ harness-dispatch auth rotate # rotate HTTP bearer token
264
357
  ```
265
358
 
266
359
  Hidden compatibility aliases currently map old alpha commands to the new surface:
267
- `dashboard` and `list-services` map to `status`, and `mcp --http <port>` maps to `serve`.
268
- They are not part of the public v0.4.0 vocabulary.
360
+ `dashboard` and `list-services` map to `status`, `route <prompt>` runs a one-off
361
+ routed dispatch, and `mcp --http <port>` maps to `serve`.
362
+ They are not part of the public v0.6.0 vocabulary.
269
363
 
270
364
  ## MCP Surface
271
365
 
272
- `tools/list` returns three tools:
366
+ `tools/list` returns six tools:
273
367
 
274
368
  | Tool | Purpose |
275
369
  | --- | --- |
276
370
  | `dispatch` | Always starts new routed coding work — one task to the best-fit harness, or a fanout to several for independent opinions. Every call runs as a background job from the first moment: a fast task returns its full result inline (`completed: true`), a slow one returns `completed: false` plus a `jobId` to check on. Nothing is ever lost to a timeout — including the MCP call's own. |
277
371
  | `job_status` | Checks work started by `dispatch`. Pass the `jobId` it returned to get a `partialOutput` tail while running and the full `result` once done; omit `jobId` to list recent background dispatches (compact, newest first). |
372
+ | `cancel_job` | Stops work started by `dispatch` — a wrong turn, a wrong directory, a superseded run. A job still waiting for a slot stops outright; a running one tears down within about a second (poll `job_status` to see it land), killing the agent CLI and its children. Files it already changed are **not** reverted, and a cancelled run is not counted as a route failure. |
373
+ | `retry_job` | Re-runs a finished job's task from its own record — same prompt (as the delegate saw it), files, working directory, hints and workspace policy. Pass `service` to send the retry to a different route, which is the usual reason to retry: the task was fine and the route was not. Returns a new jobId; the original is untouched. |
374
+ | `workspace` | For a job that ran with `workspacePolicy: "copy"` or `"git_worktree"`, the agent's changes live in an isolated workspace and were **never** applied to your project. `action: "diff"` returns the real patch; `"apply"` applies it (refusing when your project has uncommitted changes, since the patch was built against a clean base — `force: true` overrides); `"discard"` deletes the workspace. The full patch is always written to the job directory, so `git apply` by hand is available either way. |
278
375
  | `usage` | Per-route call counts, quota, billing kind, and breaker state — check this before passing an unfamiliar `hints.model`/`service`/`models` value, since those are not validated. Pass `listModels: <route id>` to fetch that `openai_compatible` route's live `GET /models` catalog instead of (or alongside) the summary. |
279
376
 
280
377
  `workingDir` is effectively required when starting work: if you omit it, the task runs
@@ -379,11 +476,18 @@ curl http://127.0.0.1:3333/v1/chat/completions \
379
476
  The REST surface is OpenAI-compatible enough for local clients that can speak
380
477
  `/v1/chat/completions`. The `model` field is treated as a routing/model hint.
381
478
 
479
+ Non-streaming completions are backed by the same persisted job pipeline as the
480
+ MCP `dispatch` tool: the reply carries `harness_dispatch.jobId`, and the same id
481
+ is sent early as an `x-harness-dispatch-job-id` response header. If your client
482
+ times out mid-run (curl defaults, CI step limits), the run still finishes and
483
+ the result persists — recover it with the `job_status` MCP tool or by reading
484
+ `~/.harness-dispatch/jobs/<jobId>/output/`.
485
+
382
486
  ## Endpoint Modes
383
487
 
384
- Harness Router supports two local/custom endpoint patterns:
488
+ harness-dispatch supports two local/custom endpoint patterns:
385
489
 
386
- - `direct_openai_compatible`: Harness Router calls an OpenAI-compatible
490
+ - `direct_openai_compatible`: harness-dispatch calls an OpenAI-compatible
387
491
  `/v1/chat/completions` endpoint directly. This is the right mode for Ollama,
388
492
  LM Studio, vLLM, LiteLLM, and private local HTTP model servers.
389
493
  - `harness_native_endpoint`: a downstream CLI keeps its agent scaffold but is
@@ -435,7 +539,7 @@ services:
435
539
  2. Verify configured routes without spending quota where possible.
436
540
  3. Classify auth and billing so paid or unknown-paid routes are not selected by accident.
437
541
  4. Choose routed harnesses, model priority, and safety profile.
438
- 5. Write v0.4 config YAML.
542
+ 5. Write config YAML.
439
543
  6. Connect selected MCP agents or print snippets.
440
544
 
441
545
  The current command is conservative: it prints detected routes by default and writes
@@ -454,8 +558,10 @@ shape. Each route includes:
454
558
  - tier and model metadata
455
559
  - quota score and local call count
456
560
  - circuit breaker state — a tripped route's remaining cooldown is persisted to disk
457
- (`~/.harness-dispatch/breaker_state.json`), so a server restart mid-cooldown still
458
- excludes that route instead of retrying an exhausted one with a clean slate
561
+ (one file per route under `~/.harness-dispatch/breaker_state/`), so a server
562
+ restart mid-cooldown still excludes that route instead of retrying an exhausted
563
+ one with a clean slate; a pre-0.5 single-blob `breaker_state.json` is migrated
564
+ automatically on first read
459
565
  - skip reason when a route is disabled, unavailable, paid-blocked, unknown-billing,
460
566
  safety-incompatible, or circuit-broken
461
567
  - token limits when known
@@ -470,7 +576,9 @@ Workspace policy:
470
576
 
471
577
  - `shared`: run directly in the caller's `workingDir`.
472
578
  - `shared_locked`: run directly in `workingDir`, but serialize write-capable
473
- dispatches for the same directory inside one router process.
579
+ dispatches for the same directory across ALL processes — concurrent dispatches
580
+ from separate server instances and detached job runners queue on a heartbeated
581
+ cross-process lock rather than editing the directory at the same time.
474
582
  - `copy`: copy the project into `.harness-dispatch/workspaces/...`, run the agent
475
583
  there, and return the isolated workspace path plus changed-file metadata.
476
584
  - `git_worktree`: create a detached git worktree for the route and return the
@@ -520,12 +628,49 @@ size-capped via single-file rotation, and never sent anywhere. Job artifacts
520
628
  `~/.harness-dispatch/jobs/<jobId>/` and are pruned after 7 days of inactivity by
521
629
  default — set `retention: { jobs_days: N }` in `config.yaml` (or
522
630
  `HARNESS_DISPATCH_JOB_MAX_AGE_MS` for a millisecond override) to change that
523
- window.
631
+ window. `0` means keep forever, and a running or queued job with a live
632
+ heartbeat is never pruned regardless of the window.
633
+
634
+ At most **4 agent CLIs run at once**, machine-wide. Dispatches past that limit
635
+ wait in `queued` and start as slots free — you still get a `jobId` back
636
+ immediately and nothing is rejected or lost, only delayed. The bound exists
637
+ because agent CLIs are heavyweight processes, not fan-outable HTTP calls: a
638
+ measured burst of 13 concurrent runs exhausted memory and failed half of them.
639
+ Change it with `max_concurrent_runs: N` in `config.yaml`; `0` removes the bound.
640
+
641
+ Prompts and outputs flow only to the harnesses/endpoints you configured. **The
642
+ router makes no other network call by default.**
643
+
644
+ Routes rank on the `tier` and `weight` you set. Optionally, public Arena ELO
645
+ benchmark data can inform ranking and derive tiers automatically:
646
+
647
+ ```yaml
648
+ leaderboard:
649
+ enabled: true # default false
650
+ ```
651
+
652
+ Turning it on adds one GET to `api.wulong.dev` per process, refreshed daily.
653
+ It sends nothing about you or your prompts. It is off by default because a
654
+ benchmark maintained elsewhere should not quietly reorder the subscriptions
655
+ you are paying for, and because a routing tool should not need the network to
656
+ decide which of your local CLIs to run.
657
+
658
+ ## Chaining delegated work
659
+
660
+ Pass the jobIds of earlier dispatches as `contextJobs` and their prompts and
661
+ outputs are rendered into the new prompt directly:
662
+
663
+ ```json
664
+ { "prompt": "Now write the migration.", "contextJobs": ["job-1786977300001-0f0aaaaa"] }
665
+ ```
666
+
667
+ Without it, chaining means reading the first job's output into your own context
668
+ and re-summarising it into the second prompt — which spends the context that
669
+ delegating was meant to save, and loses detail in the retelling.
524
670
 
525
- Prompts and outputs otherwise flow only to the harnesses/endpoints you
526
- configured. The router's only network call by default is the leaderboard
527
- refresh a GET of public Arena ELO benchmark data from `api.wulong.dev` used
528
- for route scoring; it sends nothing about you or your prompts.
671
+ Injected context is capped (24k characters total, 8k per job, 16 jobs) so it
672
+ cannot crowd out the task itself, and a referenced job that is missing or still
673
+ running is reported in the preamble rather than silently dropped.
529
674
 
530
675
  ## Development
531
676