harness-dispatch 0.4.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.
- package/LICENSE +21 -0
- package/README.md +582 -0
- package/config.default.yaml +397 -0
- package/data/coding_benchmarks.json +431 -0
- package/dist/auth.d.ts +9 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +73 -0
- package/dist/auth.js.map +1 -0
- package/dist/billing.d.ts +7 -0
- package/dist/billing.d.ts.map +1 -0
- package/dist/billing.js +201 -0
- package/dist/billing.js.map +1 -0
- package/dist/bin.d.ts +6 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +488 -0
- package/dist/bin.js.map +1 -0
- package/dist/breaker-store.d.ts +31 -0
- package/dist/breaker-store.d.ts.map +1 -0
- package/dist/breaker-store.js +103 -0
- package/dist/breaker-store.js.map +1 -0
- package/dist/circuit-breaker.d.ts +47 -0
- package/dist/circuit-breaker.d.ts.map +1 -0
- package/dist/circuit-breaker.js +102 -0
- package/dist/circuit-breaker.js.map +1 -0
- package/dist/config.d.ts +48 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +1248 -0
- package/dist/config.js.map +1 -0
- package/dist/dashboard/live.d.ts +49 -0
- package/dist/dashboard/live.d.ts.map +1 -0
- package/dist/dashboard/live.js +149 -0
- package/dist/dashboard/live.js.map +1 -0
- package/dist/dispatch-log.d.ts +46 -0
- package/dist/dispatch-log.d.ts.map +1 -0
- package/dist/dispatch-log.js +89 -0
- package/dist/dispatch-log.js.map +1 -0
- package/dist/dispatchers/base.d.ts +55 -0
- package/dist/dispatchers/base.d.ts.map +1 -0
- package/dist/dispatchers/base.js +80 -0
- package/dist/dispatchers/base.js.map +1 -0
- package/dist/dispatchers/generic-cli.d.ts +30 -0
- package/dist/dispatchers/generic-cli.d.ts.map +1 -0
- package/dist/dispatchers/generic-cli.js +471 -0
- package/dist/dispatchers/generic-cli.js.map +1 -0
- package/dist/dispatchers/openai-compatible.d.ts +57 -0
- package/dist/dispatchers/openai-compatible.d.ts.map +1 -0
- package/dist/dispatchers/openai-compatible.js +662 -0
- package/dist/dispatchers/openai-compatible.js.map +1 -0
- package/dist/dispatchers/shared/kill-tree.d.ts +21 -0
- package/dist/dispatchers/shared/kill-tree.d.ts.map +1 -0
- package/dist/dispatchers/shared/kill-tree.js +35 -0
- package/dist/dispatchers/shared/kill-tree.js.map +1 -0
- package/dist/dispatchers/shared/rate-limit-headers.d.ts +22 -0
- package/dist/dispatchers/shared/rate-limit-headers.d.ts.map +1 -0
- package/dist/dispatchers/shared/rate-limit-headers.js +124 -0
- package/dist/dispatchers/shared/rate-limit-headers.js.map +1 -0
- package/dist/dispatchers/shared/stream-subprocess.d.ts +53 -0
- package/dist/dispatchers/shared/stream-subprocess.d.ts.map +1 -0
- package/dist/dispatchers/shared/stream-subprocess.js +274 -0
- package/dist/dispatchers/shared/stream-subprocess.js.map +1 -0
- package/dist/dispatchers/shared/subprocess.d.ts +25 -0
- package/dist/dispatchers/shared/subprocess.d.ts.map +1 -0
- package/dist/dispatchers/shared/subprocess.js +110 -0
- package/dist/dispatchers/shared/subprocess.js.map +1 -0
- package/dist/dispatchers/shared/which-available.d.ts +2 -0
- package/dist/dispatchers/shared/which-available.d.ts.map +1 -0
- package/dist/dispatchers/shared/which-available.js +8 -0
- package/dist/dispatchers/shared/which-available.js.map +1 -0
- package/dist/dispatchers/shared/windows-cmd.d.ts +38 -0
- package/dist/dispatchers/shared/windows-cmd.d.ts.map +1 -0
- package/dist/dispatchers/shared/windows-cmd.js +91 -0
- package/dist/dispatchers/shared/windows-cmd.js.map +1 -0
- package/dist/http/server.d.ts +15 -0
- package/dist/http/server.d.ts.map +1 -0
- package/dist/http/server.js +495 -0
- package/dist/http/server.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/job-runner.d.ts +19 -0
- package/dist/job-runner.d.ts.map +1 -0
- package/dist/job-runner.js +42 -0
- package/dist/job-runner.js.map +1 -0
- package/dist/jobs.d.ts +116 -0
- package/dist/jobs.d.ts.map +1 -0
- package/dist/jobs.js +542 -0
- package/dist/jobs.js.map +1 -0
- package/dist/leaderboard.d.ts +62 -0
- package/dist/leaderboard.d.ts.map +1 -0
- package/dist/leaderboard.js +287 -0
- package/dist/leaderboard.js.map +1 -0
- package/dist/mcp/config-hot-reload.d.ts +64 -0
- package/dist/mcp/config-hot-reload.d.ts.map +1 -0
- package/dist/mcp/config-hot-reload.js +144 -0
- package/dist/mcp/config-hot-reload.js.map +1 -0
- package/dist/mcp/dispatcher-factory.d.ts +20 -0
- package/dist/mcp/dispatcher-factory.d.ts.map +1 -0
- package/dist/mcp/dispatcher-factory.js +30 -0
- package/dist/mcp/dispatcher-factory.js.map +1 -0
- package/dist/mcp/resources.d.ts +8 -0
- package/dist/mcp/resources.d.ts.map +1 -0
- package/dist/mcp/resources.js +45 -0
- package/dist/mcp/resources.js.map +1 -0
- package/dist/mcp/server.d.ts +36 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +80 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools.d.ts +171 -0
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/mcp/tools.js +658 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/observability/index.d.ts +52 -0
- package/dist/observability/index.d.ts.map +1 -0
- package/dist/observability/index.js +111 -0
- package/dist/observability/index.js.map +1 -0
- package/dist/observability/spans.d.ts +47 -0
- package/dist/observability/spans.d.ts.map +1 -0
- package/dist/observability/spans.js +72 -0
- package/dist/observability/spans.js.map +1 -0
- package/dist/quota.d.ts +91 -0
- package/dist/quota.d.ts.map +1 -0
- package/dist/quota.js +331 -0
- package/dist/quota.js.map +1 -0
- package/dist/route-policy.d.ts +24 -0
- package/dist/route-policy.d.ts.map +1 -0
- package/dist/route-policy.js +92 -0
- package/dist/route-policy.js.map +1 -0
- package/dist/router.d.ts +162 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +878 -0
- package/dist/router.js.map +1 -0
- package/dist/safety.d.ts +7 -0
- package/dist/safety.d.ts.map +1 -0
- package/dist/safety.js +33 -0
- package/dist/safety.js.map +1 -0
- package/dist/status.d.ts +100 -0
- package/dist/status.d.ts.map +1 -0
- package/dist/status.js +228 -0
- package/dist/status.js.map +1 -0
- package/dist/types.d.ts +402 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +13 -0
- package/dist/version.js.map +1 -0
- package/dist/working-dir.d.ts +15 -0
- package/dist/working-dir.d.ts.map +1 -0
- package/dist/working-dir.js +22 -0
- package/dist/working-dir.js.map +1 -0
- package/dist/workspaces.d.ts +19 -0
- package/dist/workspaces.d.ts.map +1 -0
- package/dist/workspaces.js +352 -0
- package/dist/workspaces.js.map +1 -0
- package/package.json +96 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Felix Stubner
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
# harness-dispatch
|
|
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
|
|
43
|
+
|
|
44
|
+
## Install
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npm install -g harness-dispatch
|
|
48
|
+
harness-dispatch configure --yes
|
|
49
|
+
harness-dispatch doctor --live
|
|
50
|
+
```
|
|
51
|
+
|
|
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:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
npx harness-dispatch configure
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Plugin install (Claude Code / Claude Desktop / Codex)
|
|
87
|
+
|
|
88
|
+
The `plugin/` directory packages the MCP server plus a delegation skill and
|
|
89
|
+
`/route` + `/jobs` commands for one-step installs — see
|
|
90
|
+
[plugin/README.md](./plugin/README.md). Claude Code:
|
|
91
|
+
`/plugin marketplace add <repo path or URL>` then
|
|
92
|
+
`/plugin install harness-dispatch@harness-dispatch`. Codex:
|
|
93
|
+
`node plugin/scripts/install-codex.mjs`.
|
|
94
|
+
|
|
95
|
+
## Adding a harness
|
|
96
|
+
|
|
97
|
+
`config.yaml` is entirely optional. There is no separate hidden defaults format —
|
|
98
|
+
harness-dispatch ships with its own [`config.default.yaml`](config.default.yaml), the
|
|
99
|
+
same shape you'd write yourself, and reads it as its built-in config. With no
|
|
100
|
+
`config.yaml` of your own, the shipped one is filtered down to whichever of `claude`,
|
|
101
|
+
`codex`, `agy` (Antigravity), and `cursor-agent` are on your PATH:
|
|
102
|
+
|
|
103
|
+
```yaml
|
|
104
|
+
# config.yaml can be empty, or not exist at all.
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Adding a harness that isn't auto-detected — a second Codex route pinned to a specific
|
|
108
|
+
model, or a local/hosted OpenAI-compatible endpoint — is a few lines:
|
|
109
|
+
|
|
110
|
+
```yaml
|
|
111
|
+
clis:
|
|
112
|
+
- name: codex_sol
|
|
113
|
+
harness: codex # picks the dispatcher: claude_code | codex | cursor | antigravity_cli | generic
|
|
114
|
+
model: gpt-5.6-sol
|
|
115
|
+
tier: 1
|
|
116
|
+
|
|
117
|
+
endpoints:
|
|
118
|
+
- name: ollama
|
|
119
|
+
base_url: http://localhost:11434/v1
|
|
120
|
+
model: qwen2.5-coder
|
|
121
|
+
tier: 3
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
See the shipped [`config.default.yaml`](config.default.yaml) for the full field
|
|
125
|
+
reference (capability weights, tiers, escalation, workspace policy, and more) — copy
|
|
126
|
+
it to your own `config.yaml` and edit, or run `harness-dispatch configure` to generate
|
|
127
|
+
a starting point.
|
|
128
|
+
|
|
129
|
+
**A wholly new CLI harness — one of the 4 built in isn't it — needs no new code
|
|
130
|
+
either.** `harness: generic` takes a `protocol:` block instead of reusing one of the
|
|
131
|
+
4 built-in harnesses' flag/output conventions. `protocol.args` is a literal
|
|
132
|
+
command-line argument list, written the same way you'd type it by hand — a handful of
|
|
133
|
+
reserved `{{name}}` tokens are substituted (or expanded to zero or more real tokens) at
|
|
134
|
+
dispatch time; everything else passes through verbatim:
|
|
135
|
+
|
|
136
|
+
```yaml
|
|
137
|
+
clis:
|
|
138
|
+
- name: my_custom_cli
|
|
139
|
+
harness: generic
|
|
140
|
+
command: my-cli # the binary, resolved on PATH like any other
|
|
141
|
+
tier: 3
|
|
142
|
+
protocol:
|
|
143
|
+
args: ["-p", "{{prompt}}", "{{working_dir}}", "{{model}}", "{{safety}}", "--json"]
|
|
144
|
+
working_dir: { flag: "--cd" } # omit {{working_dir}}/this to rely on process cwd alone
|
|
145
|
+
model: { flag: "--model" } # omit {{model}}/this if the CLI has no model override
|
|
146
|
+
safety: # args per requested safety profile, via {{safety}}
|
|
147
|
+
read_only: ["--mode", "plan"]
|
|
148
|
+
workspace_edit: ["--mode", "accept-edits"]
|
|
149
|
+
full_auto: ["--dangerously-skip-permissions"]
|
|
150
|
+
output:
|
|
151
|
+
mode: json_field # text | json_field | jsonl_stream
|
|
152
|
+
fields: [result, output, text] # checked in order; dotted paths work ("message.content")
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
The full token reference:
|
|
156
|
+
|
|
157
|
+
| Token | Expands to |
|
|
158
|
+
| --- | --- |
|
|
159
|
+
| `{{prompt}}` | the prompt text (one token) — omitted entirely if `stdin: true` |
|
|
160
|
+
| `{{model}}` | `[model.flag, value]` if a model is set, else nothing |
|
|
161
|
+
| `{{safety}}` | `safety[requested profile]` — zero or more tokens |
|
|
162
|
+
| `{{working_dir}}` | `[working_dir.flag, dir, ...working_dir.extra_args_when_set]` if set, else nothing |
|
|
163
|
+
| `{{file_dirs}}` | `[file_dirs.flag, dir]` repeated once per included file's directory |
|
|
164
|
+
| `{{native_args}}` | `endpoint_native_args[endpoint_provider]`, only under `endpoint_mode: harness_native_endpoint` |
|
|
165
|
+
|
|
166
|
+
**Protocols are named and selectable, not just inline.** `claude_code`, `codex`,
|
|
167
|
+
`cursor`, and `antigravity_cli` are registered presets — every entry's `harness:` value
|
|
168
|
+
in the shipped [`config.default.yaml`](config.default.yaml)'s `clis:` list is
|
|
169
|
+
automatically selectable as a preset name. Reference one by name instead of retyping it:
|
|
170
|
+
|
|
171
|
+
```yaml
|
|
172
|
+
clis:
|
|
173
|
+
- name: my_cursor_fork
|
|
174
|
+
harness: generic
|
|
175
|
+
command: my-cursor-fork-cli # a different binary that happens to share Cursor's CLI shape
|
|
176
|
+
protocol: cursor
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Or start from a preset and override just what differs, for the common "95% the
|
|
180
|
+
same, one flag different" case — `safety` merges per-profile (overriding just
|
|
181
|
+
`full_auto` doesn't erase `read_only`/`workspace_edit` from the preset):
|
|
182
|
+
|
|
183
|
+
```yaml
|
|
184
|
+
clis:
|
|
185
|
+
- name: my_codex_fork
|
|
186
|
+
harness: generic
|
|
187
|
+
command: my-codex-fork-cli
|
|
188
|
+
protocol:
|
|
189
|
+
extends: codex
|
|
190
|
+
model: { flag: "--llm-model" } # only this differs from the codex preset
|
|
191
|
+
safety:
|
|
192
|
+
full_auto: ["--yolo"] # only this profile's args are replaced
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
A built-in route's own `protocol:` (under `overrides.claude_code_cli`, etc.) accepts a
|
|
196
|
+
preset name or `extends:` too — it's parsed through the exact same code path as any
|
|
197
|
+
other route.
|
|
198
|
+
|
|
199
|
+
The `harness: claude_code | codex | cursor | antigravity_cli` routes aren't special
|
|
200
|
+
either — there is no per-harness dispatcher class or hardcoded TypeScript data for any
|
|
201
|
+
of them in this codebase. All 4 are ordinary `clis:` entries in the shipped
|
|
202
|
+
[`config.default.yaml`](config.default.yaml) — not a separate "defaults registry" in
|
|
203
|
+
some other format, loaded through the exact same parser as your own `config.yaml`,
|
|
204
|
+
covering each CLI's real flags including Codex's mid-run tool_use/thinking/usage
|
|
205
|
+
streaming events via `event_rules` (see below). Every CLI-type route — built-in or
|
|
206
|
+
user-added — runs through the one `GenericCliDispatcher` interpreter. Copy an entry
|
|
207
|
+
from the shipped file into your own `config.yaml` and edit it directly (or add a
|
|
208
|
+
`protocol:` block under `overrides.claude_code_cli`, etc.) and it replaces the default
|
|
209
|
+
entirely — nothing about the 4 built-ins is more hardcoded than a route you add
|
|
210
|
+
yourself.
|
|
211
|
+
|
|
212
|
+
For a CLI whose events don't fit `text`/`json_field`'s single-parse-at-exit model —
|
|
213
|
+
mid-run tool_use/thinking surfacing, token-usage aggregation across lines — use
|
|
214
|
+
`output.mode: jsonl_stream` with `output.event_rules`:
|
|
215
|
+
|
|
216
|
+
```yaml
|
|
217
|
+
output:
|
|
218
|
+
mode: jsonl_stream
|
|
219
|
+
event_rules:
|
|
220
|
+
- when: { type: "message" } # every listed field must match this line
|
|
221
|
+
emit: text
|
|
222
|
+
text_field: message.content # dotted paths work
|
|
223
|
+
- when: { "item.type": "tool_use" }
|
|
224
|
+
emit: tool_use
|
|
225
|
+
name_field: item.name
|
|
226
|
+
input_field: item.input
|
|
227
|
+
- when: { type: "thinking" }
|
|
228
|
+
emit: thinking
|
|
229
|
+
chunk_field: item.text
|
|
230
|
+
- when: {} # omit `when` (or leave it empty) to match every line
|
|
231
|
+
emit: usage
|
|
232
|
+
input_token_fields: [usage.input_tokens, usage.prompt_tokens] # first present wins
|
|
233
|
+
output_token_fields: [usage.output_tokens, usage.completion_tokens]
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Other fields worth knowing: `file_dirs: { flag: ... }` (paired with `{{file_dirs}}`)
|
|
237
|
+
repeats a flag once per unique file directory (Antigravity's `--add-dir`);
|
|
238
|
+
`api_key_env_var` injects `api_key` under a named env var for the child process (and
|
|
239
|
+
clears it if ambient but unconfigured, so a stray key never leaks into a
|
|
240
|
+
subscription-auth call); `success_requires_output: false` switches from the default
|
|
241
|
+
strict contract (exit 0 AND a non-empty parsed field) to the lenient one Claude
|
|
242
|
+
Code/Codex/Antigravity use (exit 0 alone, falling back to raw stdout/stderr text when
|
|
243
|
+
parsing yields nothing). Billing for a `generic` route defaults to `unknown` (blocked
|
|
244
|
+
until you classify it — there's no way to know an arbitrary CLI's real billing model) —
|
|
245
|
+
set `billing_kind:` / `paid_usage_possible:` explicitly once you know it.
|
|
246
|
+
|
|
247
|
+
## CLI
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
harness-dispatch # stdio MCP
|
|
251
|
+
harness-dispatch configure # detect harnesses and prepare config
|
|
252
|
+
harness-dispatch configure --print # inspect generated config YAML
|
|
253
|
+
harness-dispatch doctor # validate install, auth, config, and routes
|
|
254
|
+
harness-dispatch doctor --live # run one eligible live routed probe
|
|
255
|
+
harness-dispatch doctor --live --allow-paid
|
|
256
|
+
harness-dispatch status # readable route readiness
|
|
257
|
+
harness-dispatch status --json # structured route metadata
|
|
258
|
+
harness-dispatch status --watch # live status refresh
|
|
259
|
+
harness-dispatch usage # per-route call counts, quota, billing kind
|
|
260
|
+
harness-dispatch usage --json # structured usage metadata
|
|
261
|
+
harness-dispatch serve --port 3333 # /mcp and /v1/* over local HTTP
|
|
262
|
+
harness-dispatch auth show # print HTTP bearer token
|
|
263
|
+
harness-dispatch auth rotate # rotate HTTP bearer token
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
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.
|
|
269
|
+
|
|
270
|
+
## MCP Surface
|
|
271
|
+
|
|
272
|
+
`tools/list` returns three tools:
|
|
273
|
+
|
|
274
|
+
| Tool | Purpose |
|
|
275
|
+
| --- | --- |
|
|
276
|
+
| `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
|
+
| `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). |
|
|
278
|
+
| `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
|
+
|
|
280
|
+
`workingDir` is effectively required when starting work: if you omit it, the task runs
|
|
281
|
+
in the router server's own process directory instead of your project, and the response
|
|
282
|
+
carries a `warning` field saying so.
|
|
283
|
+
|
|
284
|
+
**How the grace window works.** `dispatch` starts the task as a background job
|
|
285
|
+
immediately, then waits up to `graceSeconds` (default 25) for it to finish. Within the
|
|
286
|
+
window you get the complete result inline, exactly as if the call had blocked. Past it
|
|
287
|
+
you get the `jobId` — call `job_status` with that `jobId` to see a `partialOutput` tail
|
|
288
|
+
while it runs and the full `result` once `completed`. Because the run never depends on
|
|
289
|
+
the MCP call staying open, a client-side timeout costs you the inline reply, never the
|
|
290
|
+
work. Background runs default to a generous 60-minute ceiling meant only to catch a
|
|
291
|
+
genuinely hung process (stuck waiting on input, a stalled network call), not to cap
|
|
292
|
+
normal work — raise it per call with `hints.timeoutMs` (milliseconds), or set a
|
|
293
|
+
permanent per-route default with `timeout_ms:` in that service's config entry.
|
|
294
|
+
Precedence is `hints.timeoutMs` > the service's `timeout_ms` > the 60-minute default.
|
|
295
|
+
|
|
296
|
+
Starting a task:
|
|
297
|
+
|
|
298
|
+
```json
|
|
299
|
+
{
|
|
300
|
+
"prompt": "Review this package for release blockers.",
|
|
301
|
+
"files": [],
|
|
302
|
+
"workingDir": "/path/to/project",
|
|
303
|
+
"workspacePolicy": "shared_locked",
|
|
304
|
+
"hints": {
|
|
305
|
+
"model": "gpt-5.4",
|
|
306
|
+
"taskType": "review",
|
|
307
|
+
"preferLargeContext": false,
|
|
308
|
+
"safetyProfile": "workspace_edit"
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
For fanout (each route that outlives the grace window returns its own `jobId`):
|
|
314
|
+
|
|
315
|
+
```json
|
|
316
|
+
{
|
|
317
|
+
"mode": "fanout",
|
|
318
|
+
"prompt": "Compare the maintainability tradeoffs in this refactor.",
|
|
319
|
+
"models": ["claude-opus-4-6", "gpt-5.4"],
|
|
320
|
+
"workspacePolicy": "copy",
|
|
321
|
+
"hints": {
|
|
322
|
+
"taskType": "plan",
|
|
323
|
+
"safetyProfile": "workspace_edit"
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
Checking and listing (`job_status`): `{"jobId": "job-..."}` returns status plus
|
|
329
|
+
`partialOutput` or the final `result`; `{}` (no `jobId`) returns every known background
|
|
330
|
+
dispatch. On `dispatch`, force pure async with `"graceSeconds": 0`, or force a specific
|
|
331
|
+
backend with a top-level `"service"` (single mode only). Nothing is lost by checking
|
|
332
|
+
late — everything persists under `~/.harness-dispatch/jobs/<jobId>/`.
|
|
333
|
+
|
|
334
|
+
Status is exposed as resources:
|
|
335
|
+
|
|
336
|
+
- `harness-dispatch://status`
|
|
337
|
+
- `harness-dispatch://status.json`
|
|
338
|
+
|
|
339
|
+
## HTTP Surface
|
|
340
|
+
|
|
341
|
+
`harness-dispatch serve` starts an authenticated local server on `127.0.0.1`.
|
|
342
|
+
|
|
343
|
+
Endpoints:
|
|
344
|
+
|
|
345
|
+
- `POST /mcp` for streamable HTTP MCP
|
|
346
|
+
- `GET /v1/status` — full route/quota/billing/breaker detail (same shape as
|
|
347
|
+
`harness-dispatch://status.json`)
|
|
348
|
+
- `GET /v1/usage` — per-route call counts, quota, billing kind, and breaker state only
|
|
349
|
+
- `GET /v1/models` — OpenAI-style model list; each entry's `id` is a route id you can
|
|
350
|
+
pass as `model` in `/v1/chat/completions`
|
|
351
|
+
- `POST /v1/chat/completions`
|
|
352
|
+
|
|
353
|
+
HTTP uses the bearer token from `harness-dispatch auth show`. The same token protects
|
|
354
|
+
MCP-over-HTTP and `/v1/*`.
|
|
355
|
+
|
|
356
|
+
`--host <host>` overrides the default `127.0.0.1` bind address if you need to reach the
|
|
357
|
+
server from another machine. Only pass a non-loopback host if you actually mean to —
|
|
358
|
+
this exposes a bearer-token-gated server, and everything the dispatched harness can do
|
|
359
|
+
(spawn CLIs, read/write files in `workingDir`), to your network. `serve` prints a
|
|
360
|
+
warning to stderr when it detects this so it isn't silent.
|
|
361
|
+
|
|
362
|
+
Example:
|
|
363
|
+
|
|
364
|
+
```bash
|
|
365
|
+
TOKEN="$(harness-dispatch auth show)"
|
|
366
|
+
|
|
367
|
+
curl http://127.0.0.1:3333/v1/chat/completions \
|
|
368
|
+
-H "authorization: Bearer $TOKEN" \
|
|
369
|
+
-H "content-type: application/json" \
|
|
370
|
+
-d '{
|
|
371
|
+
"model": "gpt-5.4",
|
|
372
|
+
"messages": [{"role": "user", "content": "Fix the failing tests."}],
|
|
373
|
+
"workingDir": "/path/to/project",
|
|
374
|
+
"safetyProfile": "workspace_edit",
|
|
375
|
+
"workspacePolicy": "copy"
|
|
376
|
+
}'
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
The REST surface is OpenAI-compatible enough for local clients that can speak
|
|
380
|
+
`/v1/chat/completions`. The `model` field is treated as a routing/model hint.
|
|
381
|
+
|
|
382
|
+
## Endpoint Modes
|
|
383
|
+
|
|
384
|
+
Harness Router supports two local/custom endpoint patterns:
|
|
385
|
+
|
|
386
|
+
- `direct_openai_compatible`: Harness Router calls an OpenAI-compatible
|
|
387
|
+
`/v1/chat/completions` endpoint directly. This is the right mode for Ollama,
|
|
388
|
+
LM Studio, vLLM, LiteLLM, and private local HTTP model servers.
|
|
389
|
+
- `harness_native_endpoint`: a downstream CLI keeps its agent scaffold but is
|
|
390
|
+
pointed at a supported local provider. Codex currently supports this for
|
|
391
|
+
`ollama` and `lmstudio` through `--oss --local-provider`.
|
|
392
|
+
|
|
393
|
+
Example direct local route:
|
|
394
|
+
|
|
395
|
+
```yaml
|
|
396
|
+
endpoints:
|
|
397
|
+
- name: ollama
|
|
398
|
+
base_url: http://localhost:11434/v1
|
|
399
|
+
model: qwen2.5-coder
|
|
400
|
+
endpoint_mode: direct_openai_compatible
|
|
401
|
+
endpoint_provider: ollama
|
|
402
|
+
wire_protocol: openai_chat_completions
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
Example Codex harness-native local route:
|
|
406
|
+
|
|
407
|
+
```yaml
|
|
408
|
+
services:
|
|
409
|
+
codex_ollama:
|
|
410
|
+
enabled: true
|
|
411
|
+
type: cli
|
|
412
|
+
harness: codex
|
|
413
|
+
command: codex
|
|
414
|
+
model: qwen3-coder:latest
|
|
415
|
+
endpoint_mode: harness_native_endpoint
|
|
416
|
+
endpoint_provider: ollama
|
|
417
|
+
wire_protocol: openai_chat_completions
|
|
418
|
+
billing_kind: local_compute
|
|
419
|
+
paid_usage_possible: false
|
|
420
|
+
tier: 3
|
|
421
|
+
weight: 0.75
|
|
422
|
+
cli_capability: 1.0
|
|
423
|
+
timeout_ms: 900000 # optional; overrides the dispatcher's default (10 min for CLIs)
|
|
424
|
+
capabilities:
|
|
425
|
+
execute: 0.8
|
|
426
|
+
plan: 0.7
|
|
427
|
+
review: 0.7
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
## Configure
|
|
431
|
+
|
|
432
|
+
`configure` is the main setup flow:
|
|
433
|
+
|
|
434
|
+
1. Detect installed harnesses.
|
|
435
|
+
2. Verify configured routes without spending quota where possible.
|
|
436
|
+
3. Classify auth and billing so paid or unknown-paid routes are not selected by accident.
|
|
437
|
+
4. Choose routed harnesses, model priority, and safety profile.
|
|
438
|
+
5. Write v0.4 config YAML.
|
|
439
|
+
6. Connect selected MCP agents or print snippets.
|
|
440
|
+
|
|
441
|
+
The current command is conservative: it prints detected routes by default and writes
|
|
442
|
+
only when explicitly asked with `--yes`.
|
|
443
|
+
|
|
444
|
+
## Status Model
|
|
445
|
+
|
|
446
|
+
`status --json`, `/v1/status`, and `harness-dispatch://status.json` share the same
|
|
447
|
+
shape. Each route includes:
|
|
448
|
+
|
|
449
|
+
- route id and harness
|
|
450
|
+
- billing provider, surface, auth source, billing kind, paid-use flags, and confidence
|
|
451
|
+
- configured and effective safety profile
|
|
452
|
+
- effective workspace policy
|
|
453
|
+
- availability
|
|
454
|
+
- tier and model metadata
|
|
455
|
+
- quota score and local call count
|
|
456
|
+
- 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
|
|
459
|
+
- skip reason when a route is disabled, unavailable, paid-blocked, unknown-billing,
|
|
460
|
+
safety-incompatible, or circuit-broken
|
|
461
|
+
- token limits when known
|
|
462
|
+
|
|
463
|
+
Safety profiles:
|
|
464
|
+
|
|
465
|
+
- `read_only`: inspect-only routes.
|
|
466
|
+
- `workspace_edit`: default; routes may edit files in the workspace without broad shell access.
|
|
467
|
+
- `full_auto`: permits routes that require shell/write automation beyond workspace-edit mode.
|
|
468
|
+
|
|
469
|
+
Workspace policy:
|
|
470
|
+
|
|
471
|
+
- `shared`: run directly in the caller's `workingDir`.
|
|
472
|
+
- `shared_locked`: run directly in `workingDir`, but serialize write-capable
|
|
473
|
+
dispatches for the same directory inside one router process.
|
|
474
|
+
- `copy`: copy the project into `.harness-dispatch/workspaces/...`, run the agent
|
|
475
|
+
there, and return the isolated workspace path plus changed-file metadata.
|
|
476
|
+
- `git_worktree`: create a detached git worktree for the route and return the
|
|
477
|
+
worktree path plus changed-file metadata. This starts from `HEAD`, so
|
|
478
|
+
uncommitted source-workspace changes are not copied.
|
|
479
|
+
|
|
480
|
+
Write-capable fanout is allowed only with `workspacePolicy: "copy"` or
|
|
481
|
+
`workspacePolicy: "git_worktree"`. These modes isolate project state and process
|
|
482
|
+
cwd. They are not hardened OS sandboxes: a route with broad shell permission can
|
|
483
|
+
still access the host unless the downstream harness or operating system enforces
|
|
484
|
+
that boundary.
|
|
485
|
+
|
|
486
|
+
Provider notes:
|
|
487
|
+
|
|
488
|
+
- Claude Code `claude -p` is treated date-aware: before June 15, 2026 it is classified
|
|
489
|
+
as plan usage; from June 15, 2026 it is classified as Agent SDK credits with possible
|
|
490
|
+
overage.
|
|
491
|
+
- Codex CLI/SDK uses the official Codex product surface unless a route is explicitly
|
|
492
|
+
configured with an API key, in which case it is API billing.
|
|
493
|
+
- Cursor Agent CLI is classified as included usage with possible on-demand continuation.
|
|
494
|
+
- OpenAI-compatible `api.openai.com` routes are metered; known local runtimes are local;
|
|
495
|
+
unknown loopback/custom endpoints require explicit billing metadata.
|
|
496
|
+
|
|
497
|
+
## Observability & Privacy
|
|
498
|
+
|
|
499
|
+
harness-dispatch contains **no phone-home telemetry** — nothing is ever sent to
|
|
500
|
+
the author or any third party. OpenTelemetry tracing is available for your own
|
|
501
|
+
use, but **it is off by default** — nothing OpenTelemetry-related initializes
|
|
502
|
+
unless you opt in:
|
|
503
|
+
|
|
504
|
+
- Enable it with `telemetry: { enabled: true }` in `config.yaml`, or the
|
|
505
|
+
`HARNESS_DISPATCH_TELEMETRY=1` env var.
|
|
506
|
+
- Once enabled, traces export via OTLP/HTTP to `http://localhost:4318` (the
|
|
507
|
+
standard local collector port) by default. If nothing is listening there,
|
|
508
|
+
spans are simply dropped — no data leaves your machine.
|
|
509
|
+
- Traces only go somewhere else if *you* set `OTEL_EXPORTER_OTLP_ENDPOINT` to
|
|
510
|
+
a remote collector.
|
|
511
|
+
- `OTEL_SDK_DISABLED=true` forces initialization off even if `telemetry:` is
|
|
512
|
+
enabled in config.
|
|
513
|
+
|
|
514
|
+
Every dispatch also appends one JSONL line to a local
|
|
515
|
+
dispatch log at `~/.harness-dispatch/logs/dispatches.jsonl` (override the
|
|
516
|
+
directory with `HARNESS_DISPATCH_LOG_DIR`) — route, success, duration, token
|
|
517
|
+
counts, and a capped error string, for post-hoc debugging. It's local-only,
|
|
518
|
+
size-capped via single-file rotation, and never sent anywhere. Job artifacts
|
|
519
|
+
(prompt, snapshotted files, stdout/stderr, result) live under
|
|
520
|
+
`~/.harness-dispatch/jobs/<jobId>/` and are pruned after 7 days of inactivity by
|
|
521
|
+
default — set `retention: { jobs_days: N }` in `config.yaml` (or
|
|
522
|
+
`HARNESS_DISPATCH_JOB_MAX_AGE_MS` for a millisecond override) to change that
|
|
523
|
+
window.
|
|
524
|
+
|
|
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.
|
|
529
|
+
|
|
530
|
+
## Development
|
|
531
|
+
|
|
532
|
+
```bash
|
|
533
|
+
npm ci
|
|
534
|
+
npm run typecheck
|
|
535
|
+
npm test
|
|
536
|
+
npm run build
|
|
537
|
+
npm run smoke
|
|
538
|
+
npm audit --omit=dev
|
|
539
|
+
npm pack --dry-run
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
Live agent workflow smoke tests are opt-in because they call real harnesses and
|
|
543
|
+
can consume quota or product-plan usage. They create a disposable tiny Node
|
|
544
|
+
project under `.harness-dispatch/smoke-workspaces`, write the detailed task into a
|
|
545
|
+
workspace-local `.harness-dispatch/agent-task.md`, send the harness a short prompt
|
|
546
|
+
pointing at that brief, then verify `node test.mjs` passes.
|
|
547
|
+
|
|
548
|
+
```powershell
|
|
549
|
+
$env:HARNESS_DISPATCH_LIVE_AGENT_SMOKE = '1'
|
|
550
|
+
npm run build
|
|
551
|
+
npm run smoke:agents -- --config config.yaml
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
To temporarily include routes that can incur paid usage:
|
|
555
|
+
|
|
556
|
+
```powershell
|
|
557
|
+
$env:HARNESS_DISPATCH_LIVE_AGENT_SMOKE = '1'
|
|
558
|
+
npm run smoke:agents -- --config config.yaml --allow-paid
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
To include Cursor's full-auto print-mode route:
|
|
562
|
+
|
|
563
|
+
```powershell
|
|
564
|
+
$env:HARNESS_DISPATCH_LIVE_AGENT_SMOKE = '1'
|
|
565
|
+
npm run smoke:agents -- --config config.yaml --allow-paid --safety full_auto
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
Release gates:
|
|
569
|
+
|
|
570
|
+
```bash
|
|
571
|
+
npm run check
|
|
572
|
+
npm run build
|
|
573
|
+
npm run test:coverage
|
|
574
|
+
npm run smoke
|
|
575
|
+
npm audit --omit=dev
|
|
576
|
+
npm pack --dry-run
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
Before publishing, also run `smoke:agents` with the installed harnesses you want
|
|
580
|
+
to claim as validated, and record which routes passed, failed, or were skipped.
|
|
581
|
+
Set `HARNESS_DISPATCH_AGENT_SMOKE_ROOT` only when you need the disposable
|
|
582
|
+
workspaces somewhere other than the repo-local shared smoke cache.
|