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.
- package/CHANGELOG.md +152 -0
- package/README.md +232 -87
- package/config.default.yaml +416 -397
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +2 -2
- package/dist/auth.js.map +1 -1
- package/dist/billing.d.ts +35 -4
- package/dist/billing.d.ts.map +1 -1
- package/dist/billing.js +48 -17
- package/dist/billing.js.map +1 -1
- package/dist/bin.d.ts.map +1 -1
- package/dist/bin.js +147 -91
- package/dist/bin.js.map +1 -1
- package/dist/breaker-store.d.ts +60 -9
- package/dist/breaker-store.d.ts.map +1 -1
- package/dist/breaker-store.js +246 -50
- package/dist/breaker-store.js.map +1 -1
- package/dist/circuit-breaker.d.ts +35 -0
- package/dist/circuit-breaker.d.ts.map +1 -1
- package/dist/circuit-breaker.js +68 -11
- package/dist/circuit-breaker.js.map +1 -1
- package/dist/config/coercions.d.ts +31 -0
- package/dist/config/coercions.d.ts.map +1 -0
- package/dist/config/coercions.js +169 -0
- package/dist/config/coercions.js.map +1 -0
- package/dist/config/env-interpolation.d.ts +36 -0
- package/dist/config/env-interpolation.d.ts.map +1 -0
- package/dist/config/env-interpolation.js +73 -0
- package/dist/config/env-interpolation.js.map +1 -0
- package/dist/config/protocol.d.ts +51 -0
- package/dist/config/protocol.d.ts.map +1 -0
- package/dist/config/protocol.js +300 -0
- package/dist/config/protocol.js.map +1 -0
- package/dist/config/route-fields.d.ts +72 -0
- package/dist/config/route-fields.d.ts.map +1 -0
- package/dist/config/route-fields.js +128 -0
- package/dist/config/route-fields.js.map +1 -0
- package/dist/config/validation.d.ts +72 -0
- package/dist/config/validation.d.ts.map +1 -0
- package/dist/config/validation.js +189 -0
- package/dist/config/validation.js.map +1 -0
- package/dist/config.d.ts +19 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +214 -635
- package/dist/config.js.map +1 -1
- package/dist/configure-yaml.d.ts +23 -0
- package/dist/configure-yaml.d.ts.map +1 -0
- package/dist/configure-yaml.js +193 -0
- package/dist/configure-yaml.js.map +1 -0
- package/dist/dispatch-log.d.ts.map +1 -1
- package/dist/dispatch-log.js +2 -3
- package/dist/dispatch-log.js.map +1 -1
- package/dist/dispatchers/base.d.ts +3 -0
- package/dist/dispatchers/base.d.ts.map +1 -1
- package/dist/dispatchers/base.js.map +1 -1
- package/dist/dispatchers/generic-cli.d.ts +15 -1
- package/dist/dispatchers/generic-cli.d.ts.map +1 -1
- package/dist/dispatchers/generic-cli.js +69 -5
- package/dist/dispatchers/generic-cli.js.map +1 -1
- package/dist/dispatchers/openai-compatible.d.ts.map +1 -1
- package/dist/dispatchers/openai-compatible.js +70 -2
- package/dist/dispatchers/openai-compatible.js.map +1 -1
- package/dist/dispatchers/shared/kill-tree.d.ts +17 -4
- package/dist/dispatchers/shared/kill-tree.d.ts.map +1 -1
- package/dist/dispatchers/shared/kill-tree.js +28 -4
- package/dist/dispatchers/shared/kill-tree.js.map +1 -1
- package/dist/dispatchers/shared/rate-limit-headers.d.ts +17 -0
- package/dist/dispatchers/shared/rate-limit-headers.d.ts.map +1 -1
- package/dist/dispatchers/shared/rate-limit-headers.js +27 -0
- package/dist/dispatchers/shared/rate-limit-headers.js.map +1 -1
- package/dist/dispatchers/shared/stream-subprocess.d.ts +11 -0
- package/dist/dispatchers/shared/stream-subprocess.d.ts.map +1 -1
- package/dist/dispatchers/shared/stream-subprocess.js +62 -35
- package/dist/dispatchers/shared/stream-subprocess.js.map +1 -1
- package/dist/dispatchers/shared/subprocess.d.ts.map +1 -1
- package/dist/dispatchers/shared/subprocess.js +3 -0
- package/dist/dispatchers/shared/subprocess.js.map +1 -1
- package/dist/dispatchers/shared/which-available.d.ts.map +1 -1
- package/dist/dispatchers/shared/which-available.js +16 -2
- package/dist/dispatchers/shared/which-available.js.map +1 -1
- package/dist/file-lock.d.ts +17 -0
- package/dist/file-lock.d.ts.map +1 -0
- package/dist/file-lock.js +143 -0
- package/dist/file-lock.js.map +1 -0
- package/dist/http/parse.d.ts +64 -0
- package/dist/http/parse.d.ts.map +1 -0
- package/dist/http/parse.js +230 -0
- package/dist/http/parse.js.map +1 -0
- package/dist/http/server.d.ts +2 -0
- package/dist/http/server.d.ts.map +1 -1
- package/dist/http/server.js +249 -286
- package/dist/http/server.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/job-runner.js +23 -5
- package/dist/job-runner.js.map +1 -1
- package/dist/jobs/context.d.ts +17 -0
- package/dist/jobs/context.d.ts.map +1 -0
- package/dist/jobs/context.js +84 -0
- package/dist/jobs/context.js.map +1 -0
- package/dist/jobs/store.d.ts +80 -0
- package/dist/jobs/store.d.ts.map +1 -0
- package/dist/jobs/store.js +279 -0
- package/dist/jobs/store.js.map +1 -0
- package/dist/jobs/types.d.ts +135 -0
- package/dist/jobs/types.d.ts.map +1 -0
- package/dist/jobs/types.js +11 -0
- package/dist/jobs/types.js.map +1 -0
- package/dist/jobs.d.ts +165 -85
- package/dist/jobs.d.ts.map +1 -1
- package/dist/jobs.js +824 -208
- package/dist/jobs.js.map +1 -1
- package/dist/leaderboard.d.ts +24 -1
- package/dist/leaderboard.d.ts.map +1 -1
- package/dist/leaderboard.js +94 -2
- package/dist/leaderboard.js.map +1 -1
- package/dist/mcp/config-hot-reload.d.ts.map +1 -1
- package/dist/mcp/config-hot-reload.js +1 -1
- package/dist/mcp/config-hot-reload.js.map +1 -1
- package/dist/mcp/dispatcher-factory.d.ts +16 -1
- package/dist/mcp/dispatcher-factory.d.ts.map +1 -1
- package/dist/mcp/dispatcher-factory.js +41 -3
- package/dist/mcp/dispatcher-factory.js.map +1 -1
- package/dist/mcp/tool-schemas.d.ts +194 -0
- package/dist/mcp/tool-schemas.d.ts.map +1 -0
- package/dist/mcp/tool-schemas.js +311 -0
- package/dist/mcp/tool-schemas.js.map +1 -0
- package/dist/mcp/tools.d.ts +26 -51
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +122 -151
- package/dist/mcp/tools.js.map +1 -1
- package/dist/observability/index.d.ts +16 -3
- package/dist/observability/index.d.ts.map +1 -1
- package/dist/observability/index.js +23 -7
- package/dist/observability/index.js.map +1 -1
- package/dist/observability/spans.d.ts.map +1 -1
- package/dist/observability/spans.js +4 -1
- package/dist/observability/spans.js.map +1 -1
- package/dist/quota.d.ts +106 -22
- package/dist/quota.d.ts.map +1 -1
- package/dist/quota.js +198 -58
- package/dist/quota.js.map +1 -1
- package/dist/route-policy.d.ts.map +1 -1
- package/dist/route-policy.js +13 -3
- package/dist/route-policy.js.map +1 -1
- package/dist/router.d.ts +29 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +102 -40
- package/dist/router.js.map +1 -1
- package/dist/safety.d.ts.map +1 -1
- package/dist/safety.js +11 -2
- package/dist/safety.js.map +1 -1
- package/dist/state-dir.d.ts +17 -0
- package/dist/state-dir.d.ts.map +1 -0
- package/dist/state-dir.js +21 -0
- package/dist/state-dir.js.map +1 -0
- package/dist/status.d.ts +49 -0
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +67 -4
- package/dist/status.js.map +1 -1
- package/dist/types.d.ts +74 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/working-dir.d.ts +13 -0
- package/dist/working-dir.d.ts.map +1 -1
- package/dist/working-dir.js +27 -0
- package/dist/working-dir.js.map +1 -1
- package/dist/workspace-lock.d.ts +40 -0
- package/dist/workspace-lock.d.ts.map +1 -0
- package/dist/workspace-lock.js +258 -0
- package/dist/workspace-lock.js.map +1 -0
- package/dist/workspace-resolve.d.ts +98 -0
- package/dist/workspace-resolve.d.ts.map +1 -0
- package/dist/workspace-resolve.js +382 -0
- package/dist/workspace-resolve.js.map +1 -0
- package/dist/workspaces.d.ts +16 -0
- package/dist/workspaces.d.ts.map +1 -1
- package/dist/workspaces.js +166 -22
- package/dist/workspaces.js.map +1 -1
- package/package.json +99 -96
- package/dist/dashboard/live.d.ts +0 -49
- package/dist/dashboard/live.d.ts.map +0 -1
- package/dist/dashboard/live.js +0 -149
- 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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
83
|
-
|
|
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](
|
|
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`,
|
|
268
|
-
|
|
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
|
|
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
|
-
|
|
488
|
+
harness-dispatch supports two local/custom endpoint patterns:
|
|
385
489
|
|
|
386
|
-
- `direct_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
|
|
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
|
|
458
|
-
excludes that route instead of retrying an exhausted
|
|
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
|
|
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
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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
|
|