harness-dispatch 0.4.0 → 0.5.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 (140) hide show
  1. package/README.md +228 -86
  2. package/config.default.yaml +416 -397
  3. package/dist/auth.d.ts.map +1 -1
  4. package/dist/auth.js +2 -2
  5. package/dist/auth.js.map +1 -1
  6. package/dist/billing.d.ts +35 -4
  7. package/dist/billing.d.ts.map +1 -1
  8. package/dist/billing.js +48 -17
  9. package/dist/billing.js.map +1 -1
  10. package/dist/bin.d.ts.map +1 -1
  11. package/dist/bin.js +253 -23
  12. package/dist/bin.js.map +1 -1
  13. package/dist/breaker-store.d.ts +60 -9
  14. package/dist/breaker-store.d.ts.map +1 -1
  15. package/dist/breaker-store.js +246 -50
  16. package/dist/breaker-store.js.map +1 -1
  17. package/dist/circuit-breaker.d.ts +35 -0
  18. package/dist/circuit-breaker.d.ts.map +1 -1
  19. package/dist/circuit-breaker.js +68 -11
  20. package/dist/circuit-breaker.js.map +1 -1
  21. package/dist/config/coercions.d.ts +31 -0
  22. package/dist/config/coercions.d.ts.map +1 -0
  23. package/dist/config/coercions.js +169 -0
  24. package/dist/config/coercions.js.map +1 -0
  25. package/dist/config/env-interpolation.d.ts +36 -0
  26. package/dist/config/env-interpolation.d.ts.map +1 -0
  27. package/dist/config/env-interpolation.js +73 -0
  28. package/dist/config/env-interpolation.js.map +1 -0
  29. package/dist/config/protocol.d.ts +51 -0
  30. package/dist/config/protocol.d.ts.map +1 -0
  31. package/dist/config/protocol.js +300 -0
  32. package/dist/config/protocol.js.map +1 -0
  33. package/dist/config/validation.d.ts +72 -0
  34. package/dist/config/validation.d.ts.map +1 -0
  35. package/dist/config/validation.js +189 -0
  36. package/dist/config/validation.js.map +1 -0
  37. package/dist/config.d.ts +19 -2
  38. package/dist/config.d.ts.map +1 -1
  39. package/dist/config.js +229 -511
  40. package/dist/config.js.map +1 -1
  41. package/dist/dispatch-log.d.ts.map +1 -1
  42. package/dist/dispatch-log.js +2 -3
  43. package/dist/dispatch-log.js.map +1 -1
  44. package/dist/dispatchers/generic-cli.d.ts +15 -1
  45. package/dist/dispatchers/generic-cli.d.ts.map +1 -1
  46. package/dist/dispatchers/generic-cli.js +65 -5
  47. package/dist/dispatchers/generic-cli.js.map +1 -1
  48. package/dist/dispatchers/openai-compatible.d.ts.map +1 -1
  49. package/dist/dispatchers/openai-compatible.js +52 -2
  50. package/dist/dispatchers/openai-compatible.js.map +1 -1
  51. package/dist/dispatchers/shared/kill-tree.d.ts +17 -4
  52. package/dist/dispatchers/shared/kill-tree.d.ts.map +1 -1
  53. package/dist/dispatchers/shared/kill-tree.js +28 -4
  54. package/dist/dispatchers/shared/kill-tree.js.map +1 -1
  55. package/dist/dispatchers/shared/rate-limit-headers.d.ts +17 -0
  56. package/dist/dispatchers/shared/rate-limit-headers.d.ts.map +1 -1
  57. package/dist/dispatchers/shared/rate-limit-headers.js +27 -0
  58. package/dist/dispatchers/shared/rate-limit-headers.js.map +1 -1
  59. package/dist/dispatchers/shared/stream-subprocess.d.ts.map +1 -1
  60. package/dist/dispatchers/shared/stream-subprocess.js +5 -0
  61. package/dist/dispatchers/shared/stream-subprocess.js.map +1 -1
  62. package/dist/dispatchers/shared/subprocess.d.ts.map +1 -1
  63. package/dist/dispatchers/shared/subprocess.js +3 -0
  64. package/dist/dispatchers/shared/subprocess.js.map +1 -1
  65. package/dist/dispatchers/shared/which-available.d.ts.map +1 -1
  66. package/dist/dispatchers/shared/which-available.js +16 -2
  67. package/dist/dispatchers/shared/which-available.js.map +1 -1
  68. package/dist/file-lock.d.ts +17 -0
  69. package/dist/file-lock.d.ts.map +1 -0
  70. package/dist/file-lock.js +143 -0
  71. package/dist/file-lock.js.map +1 -0
  72. package/dist/http/server.d.ts +11 -0
  73. package/dist/http/server.d.ts.map +1 -1
  74. package/dist/http/server.js +331 -162
  75. package/dist/http/server.js.map +1 -1
  76. package/dist/job-runner.js +23 -5
  77. package/dist/job-runner.js.map +1 -1
  78. package/dist/jobs.d.ts +91 -1
  79. package/dist/jobs.d.ts.map +1 -1
  80. package/dist/jobs.js +647 -36
  81. package/dist/jobs.js.map +1 -1
  82. package/dist/leaderboard.d.ts +24 -1
  83. package/dist/leaderboard.d.ts.map +1 -1
  84. package/dist/leaderboard.js +94 -2
  85. package/dist/leaderboard.js.map +1 -1
  86. package/dist/mcp/config-hot-reload.d.ts.map +1 -1
  87. package/dist/mcp/config-hot-reload.js +1 -1
  88. package/dist/mcp/config-hot-reload.js.map +1 -1
  89. package/dist/mcp/dispatcher-factory.d.ts +16 -1
  90. package/dist/mcp/dispatcher-factory.d.ts.map +1 -1
  91. package/dist/mcp/dispatcher-factory.js +41 -3
  92. package/dist/mcp/dispatcher-factory.js.map +1 -1
  93. package/dist/mcp/tools.d.ts +23 -4
  94. package/dist/mcp/tools.d.ts.map +1 -1
  95. package/dist/mcp/tools.js +163 -16
  96. package/dist/mcp/tools.js.map +1 -1
  97. package/dist/observability/index.d.ts +16 -3
  98. package/dist/observability/index.d.ts.map +1 -1
  99. package/dist/observability/index.js +23 -7
  100. package/dist/observability/index.js.map +1 -1
  101. package/dist/observability/spans.d.ts.map +1 -1
  102. package/dist/observability/spans.js +4 -1
  103. package/dist/observability/spans.js.map +1 -1
  104. package/dist/quota.d.ts +88 -22
  105. package/dist/quota.d.ts.map +1 -1
  106. package/dist/quota.js +163 -58
  107. package/dist/quota.js.map +1 -1
  108. package/dist/route-policy.d.ts.map +1 -1
  109. package/dist/route-policy.js +13 -3
  110. package/dist/route-policy.js.map +1 -1
  111. package/dist/router.d.ts +25 -1
  112. package/dist/router.d.ts.map +1 -1
  113. package/dist/router.js +94 -40
  114. package/dist/router.js.map +1 -1
  115. package/dist/safety.d.ts.map +1 -1
  116. package/dist/safety.js +11 -2
  117. package/dist/safety.js.map +1 -1
  118. package/dist/state-dir.d.ts +17 -0
  119. package/dist/state-dir.d.ts.map +1 -0
  120. package/dist/state-dir.js +21 -0
  121. package/dist/state-dir.js.map +1 -0
  122. package/dist/status.d.ts +36 -0
  123. package/dist/status.d.ts.map +1 -1
  124. package/dist/status.js +61 -4
  125. package/dist/status.js.map +1 -1
  126. package/dist/types.d.ts +50 -1
  127. package/dist/types.d.ts.map +1 -1
  128. package/dist/working-dir.d.ts +13 -0
  129. package/dist/working-dir.d.ts.map +1 -1
  130. package/dist/working-dir.js +27 -0
  131. package/dist/working-dir.js.map +1 -1
  132. package/dist/workspace-lock.d.ts +40 -0
  133. package/dist/workspace-lock.d.ts.map +1 -0
  134. package/dist/workspace-lock.js +258 -0
  135. package/dist/workspace-lock.js.map +1 -0
  136. package/dist/workspaces.d.ts +15 -0
  137. package/dist/workspaces.d.ts.map +1 -1
  138. package/dist/workspaces.js +104 -12
  139. package/dist/workspaces.js.map +1 -1
  140. package/package.json +98 -96
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,8 +357,9 @@ 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.5.0 vocabulary.
269
363
 
270
364
  ## MCP Surface
271
365
 
@@ -379,11 +473,18 @@ curl http://127.0.0.1:3333/v1/chat/completions \
379
473
  The REST surface is OpenAI-compatible enough for local clients that can speak
380
474
  `/v1/chat/completions`. The `model` field is treated as a routing/model hint.
381
475
 
476
+ Non-streaming completions are backed by the same persisted job pipeline as the
477
+ MCP `dispatch` tool: the reply carries `harness_dispatch.jobId`, and the same id
478
+ is sent early as an `x-harness-dispatch-job-id` response header. If your client
479
+ times out mid-run (curl defaults, CI step limits), the run still finishes and
480
+ the result persists — recover it with the `job_status` MCP tool or by reading
481
+ `~/.harness-dispatch/jobs/<jobId>/output/`.
482
+
382
483
  ## Endpoint Modes
383
484
 
384
- Harness Router supports two local/custom endpoint patterns:
485
+ harness-dispatch supports two local/custom endpoint patterns:
385
486
 
386
- - `direct_openai_compatible`: Harness Router calls an OpenAI-compatible
487
+ - `direct_openai_compatible`: harness-dispatch calls an OpenAI-compatible
387
488
  `/v1/chat/completions` endpoint directly. This is the right mode for Ollama,
388
489
  LM Studio, vLLM, LiteLLM, and private local HTTP model servers.
389
490
  - `harness_native_endpoint`: a downstream CLI keeps its agent scaffold but is
@@ -435,7 +536,7 @@ services:
435
536
  2. Verify configured routes without spending quota where possible.
436
537
  3. Classify auth and billing so paid or unknown-paid routes are not selected by accident.
437
538
  4. Choose routed harnesses, model priority, and safety profile.
438
- 5. Write v0.4 config YAML.
539
+ 5. Write config YAML.
439
540
  6. Connect selected MCP agents or print snippets.
440
541
 
441
542
  The current command is conservative: it prints detected routes by default and writes
@@ -454,8 +555,10 @@ shape. Each route includes:
454
555
  - tier and model metadata
455
556
  - quota score and local call count
456
557
  - 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
558
+ (one file per route under `~/.harness-dispatch/breaker_state/`), so a server
559
+ restart mid-cooldown still excludes that route instead of retrying an exhausted
560
+ one with a clean slate; a pre-0.5 single-blob `breaker_state.json` is migrated
561
+ automatically on first read
459
562
  - skip reason when a route is disabled, unavailable, paid-blocked, unknown-billing,
460
563
  safety-incompatible, or circuit-broken
461
564
  - token limits when known
@@ -470,7 +573,9 @@ Workspace policy:
470
573
 
471
574
  - `shared`: run directly in the caller's `workingDir`.
472
575
  - `shared_locked`: run directly in `workingDir`, but serialize write-capable
473
- dispatches for the same directory inside one router process.
576
+ dispatches for the same directory across ALL processes — concurrent dispatches
577
+ from separate server instances and detached job runners queue on a heartbeated
578
+ cross-process lock rather than editing the directory at the same time.
474
579
  - `copy`: copy the project into `.harness-dispatch/workspaces/...`, run the agent
475
580
  there, and return the isolated workspace path plus changed-file metadata.
476
581
  - `git_worktree`: create a detached git worktree for the route and return the
@@ -520,12 +625,49 @@ size-capped via single-file rotation, and never sent anywhere. Job artifacts
520
625
  `~/.harness-dispatch/jobs/<jobId>/` and are pruned after 7 days of inactivity by
521
626
  default — set `retention: { jobs_days: N }` in `config.yaml` (or
522
627
  `HARNESS_DISPATCH_JOB_MAX_AGE_MS` for a millisecond override) to change that
523
- window.
628
+ window. `0` means keep forever, and a running or queued job with a live
629
+ heartbeat is never pruned regardless of the window.
630
+
631
+ At most **4 agent CLIs run at once**, machine-wide. Dispatches past that limit
632
+ wait in `queued` and start as slots free — you still get a `jobId` back
633
+ immediately and nothing is rejected or lost, only delayed. The bound exists
634
+ because agent CLIs are heavyweight processes, not fan-outable HTTP calls: a
635
+ measured burst of 13 concurrent runs exhausted memory and failed half of them.
636
+ Change it with `max_concurrent_runs: N` in `config.yaml`; `0` removes the bound.
637
+
638
+ Prompts and outputs flow only to the harnesses/endpoints you configured. **The
639
+ router makes no other network call by default.**
640
+
641
+ Routes rank on the `tier` and `weight` you set. Optionally, public Arena ELO
642
+ benchmark data can inform ranking and derive tiers automatically:
643
+
644
+ ```yaml
645
+ leaderboard:
646
+ enabled: true # default false
647
+ ```
648
+
649
+ Turning it on adds one GET to `api.wulong.dev` per process, refreshed daily.
650
+ It sends nothing about you or your prompts. It is off by default because a
651
+ benchmark maintained elsewhere should not quietly reorder the subscriptions
652
+ you are paying for, and because a routing tool should not need the network to
653
+ decide which of your local CLIs to run.
654
+
655
+ ## Chaining delegated work
656
+
657
+ Pass the jobIds of earlier dispatches as `contextJobs` and their prompts and
658
+ outputs are rendered into the new prompt directly:
659
+
660
+ ```json
661
+ { "prompt": "Now write the migration.", "contextJobs": ["job-1786977300001-0f0aaaaa"] }
662
+ ```
663
+
664
+ Without it, chaining means reading the first job's output into your own context
665
+ and re-summarising it into the second prompt — which spends the context that
666
+ delegating was meant to save, and loses detail in the retelling.
524
667
 
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.
668
+ Injected context is capped (24k characters total, 8k per job, 16 jobs) so it
669
+ cannot crowd out the task itself, and a referenced job that is missing or still
670
+ running is reported in the preamble rather than silently dropped.
529
671
 
530
672
  ## Development
531
673