harness-dispatch 0.8.0 → 0.9.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 +514 -1
- package/README.md +29 -4
- package/config.default.yaml +19 -3
- package/dist/billing.d.ts +24 -0
- package/dist/billing.d.ts.map +1 -1
- package/dist/billing.js +24 -0
- package/dist/billing.js.map +1 -1
- package/dist/bin.d.ts.map +1 -1
- package/dist/bin.js +136 -22
- package/dist/bin.js.map +1 -1
- package/dist/circuit-breaker.d.ts +9 -1
- package/dist/circuit-breaker.d.ts.map +1 -1
- package/dist/circuit-breaker.js +15 -1
- package/dist/circuit-breaker.js.map +1 -1
- package/dist/client-register.d.ts +11 -1
- package/dist/client-register.d.ts.map +1 -1
- package/dist/client-register.js +15 -6
- package/dist/client-register.js.map +1 -1
- package/dist/config/validation.d.ts +12 -0
- package/dist/config/validation.d.ts.map +1 -1
- package/dist/config/validation.js +133 -7
- package/dist/config/validation.js.map +1 -1
- package/dist/config.d.ts +4 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +88 -4
- package/dist/config.js.map +1 -1
- package/dist/configure-yaml.d.ts +2 -0
- package/dist/configure-yaml.d.ts.map +1 -1
- package/dist/configure-yaml.js +69 -3
- package/dist/configure-yaml.js.map +1 -1
- package/dist/dispatchers/generic-cli.d.ts.map +1 -1
- package/dist/dispatchers/generic-cli.js +92 -15
- 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 +22 -2
- package/dist/dispatchers/openai-compatible.js.map +1 -1
- package/dist/dispatchers/shared/harness-login.d.ts +25 -0
- package/dist/dispatchers/shared/harness-login.d.ts.map +1 -0
- package/dist/dispatchers/shared/harness-login.js +68 -0
- package/dist/dispatchers/shared/harness-login.js.map +1 -0
- package/dist/dispatchers/shared/stream-subprocess.d.ts.map +1 -1
- package/dist/dispatchers/shared/stream-subprocess.js +70 -4
- package/dist/dispatchers/shared/stream-subprocess.js.map +1 -1
- package/dist/file-lock.d.ts +29 -1
- package/dist/file-lock.d.ts.map +1 -1
- package/dist/file-lock.js +22 -1
- package/dist/file-lock.js.map +1 -1
- package/dist/http/server.d.ts +2 -0
- package/dist/http/server.d.ts.map +1 -1
- package/dist/http/server.js +179 -14
- package/dist/http/server.js.map +1 -1
- package/dist/jobs/context.d.ts +0 -7
- package/dist/jobs/context.d.ts.map +1 -1
- package/dist/jobs/context.js +95 -7
- package/dist/jobs/context.js.map +1 -1
- package/dist/jobs/types.d.ts +8 -1
- package/dist/jobs/types.d.ts.map +1 -1
- package/dist/jobs.d.ts +15 -0
- package/dist/jobs.d.ts.map +1 -1
- package/dist/jobs.js +106 -14
- package/dist/jobs.js.map +1 -1
- package/dist/leaderboard.d.ts.map +1 -1
- package/dist/leaderboard.js +39 -27
- package/dist/leaderboard.js.map +1 -1
- package/dist/mcp/config-hot-reload.d.ts +14 -3
- package/dist/mcp/config-hot-reload.d.ts.map +1 -1
- package/dist/mcp/config-hot-reload.js +23 -5
- package/dist/mcp/config-hot-reload.js.map +1 -1
- package/dist/mcp/dispatcher-factory.d.ts.map +1 -1
- package/dist/mcp/dispatcher-factory.js +14 -3
- package/dist/mcp/dispatcher-factory.js.map +1 -1
- package/dist/mcp/tool-schemas.d.ts +1 -1
- package/dist/mcp/tool-schemas.d.ts.map +1 -1
- package/dist/mcp/tool-schemas.js +6 -3
- package/dist/mcp/tool-schemas.js.map +1 -1
- package/dist/mcp/tools.d.ts +11 -4
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +47 -8
- package/dist/mcp/tools.js.map +1 -1
- package/dist/mcp-clients.d.ts +9 -0
- package/dist/mcp-clients.d.ts.map +1 -1
- package/dist/mcp-clients.js +4 -0
- package/dist/mcp-clients.js.map +1 -1
- package/dist/quota.d.ts.map +1 -1
- package/dist/quota.js +16 -2
- package/dist/quota.js.map +1 -1
- package/dist/route-policy.d.ts +2 -1
- package/dist/route-policy.d.ts.map +1 -1
- package/dist/route-policy.js +54 -0
- package/dist/route-policy.js.map +1 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +4 -0
- package/dist/router.js.map +1 -1
- package/dist/safety.d.ts.map +1 -1
- package/dist/safety.js +7 -1
- package/dist/safety.js.map +1 -1
- package/dist/state-dir.d.ts +10 -0
- package/dist/state-dir.d.ts.map +1 -1
- package/dist/state-dir.js +12 -0
- package/dist/state-dir.js.map +1 -1
- package/dist/types.d.ts +24 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/workspace-resolve.d.ts.map +1 -1
- package/dist/workspace-resolve.js +77 -0
- package/dist/workspace-resolve.js.map +1 -1
- package/dist/workspaces.d.ts +15 -0
- package/dist/workspaces.d.ts.map +1 -1
- package/dist/workspaces.js +329 -13
- package/dist/workspaces.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,518 @@ pre-1.0, so minor versions can carry behaviour changes.
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.9.0] — 2026-09-02
|
|
10
|
+
|
|
11
|
+
### Changed (breaking)
|
|
12
|
+
|
|
13
|
+
- **A config file that defines routes is now authoritative about them.**
|
|
14
|
+
Auto-detection used to run unconditionally, so a file with `clis:` or
|
|
15
|
+
`endpoints:` entries ADDED to the installed harnesses rather than replacing
|
|
16
|
+
them, and only `disabled:` — naming every route, including ones you might
|
|
17
|
+
not know existed — subtracted.
|
|
18
|
+
|
|
19
|
+
That default caught three acceptance passes in a row despite explicit
|
|
20
|
+
warnings, and caught this project's own test suite: one test dispatched to
|
|
21
|
+
the real Claude Code CLI on every `npm test` and every CI run, under a
|
|
22
|
+
comment asserting it could not reach a route. It also decayed — adding
|
|
23
|
+
support for a new harness would auto-add it to every existing config, so a
|
|
24
|
+
`disabled:` list written today quietly stopped isolating tomorrow.
|
|
25
|
+
|
|
26
|
+
The legacy `services:` format has always behaved this way, so the two shapes
|
|
27
|
+
now agree rather than this being a new rule.
|
|
28
|
+
|
|
29
|
+
**What changes for you.** A config that has a `clis:` or `endpoints:` key
|
|
30
|
+
gets exactly the routes listed under it — including `clis: []`, which means
|
|
31
|
+
no CLI routes. Add `detect: true` to keep the old behaviour. A config
|
|
32
|
+
mentioning NEITHER key — one carrying only `overrides:`, `disabled:` or
|
|
33
|
+
settings — still auto-detects, because a file cannot be authoritative about
|
|
34
|
+
routes it does not describe; it now says so in a warning. `detect: false`
|
|
35
|
+
turns detection off outright.
|
|
36
|
+
|
|
37
|
+
Note that `disabled:` and `overrides:` tune AUTO-DETECTION, so they do
|
|
38
|
+
nothing in a config that lists its own routes — remove the entry instead.
|
|
39
|
+
The config the README tells you to copy said the opposite of this until it
|
|
40
|
+
was corrected in the same release; check yours if you wrote it from that.
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
|
|
44
|
+
- **Re-running `configure` after installing a harness no longer needs
|
|
45
|
+
`--force`.** The natural first-run order — install this tool, configure,
|
|
46
|
+
find out a harness is needed, install one, configure again — was refused
|
|
47
|
+
at the last step: "already exists ... --force". And had the first run found
|
|
48
|
+
anything, the re-run would have loaded that file as authoritative and not
|
|
49
|
+
detected the new harness at all. `configure` now stamps what it writes
|
|
50
|
+
with a fingerprint; a re-run that finds its own unedited output regenerates
|
|
51
|
+
it from a fresh detection, and says so. A file that has been edited, or
|
|
52
|
+
that configure did not write, is still loaded (so its settings migrate)
|
|
53
|
+
and still refused without `--force` — and since such a file lists its own
|
|
54
|
+
routes, `--force` regenerates it from the file, not from a detection. It
|
|
55
|
+
used to print "Detected N harness routes" there anyway; an acceptance pass
|
|
56
|
+
measured that line with a second harness on PATH that never appeared. It
|
|
57
|
+
now says detection did not run and how to merge (`detect: true`). The
|
|
58
|
+
non-interactive hint at the end of `configure --yes` also told the user to
|
|
59
|
+
pass `--yes`, which they just had; it now names `connect`. Seen on the
|
|
60
|
+
cold-install walk.
|
|
61
|
+
|
|
62
|
+
- **`connect` now registers with a Claude Code that has never been opened.**
|
|
63
|
+
A client counted as installed only if its config file existed, and Claude
|
|
64
|
+
Code writes `~/.claude.json` on its first interactive launch — so a user who
|
|
65
|
+
installed Claude Code and this tool together was told "No MCP clients found
|
|
66
|
+
on this machine" with `claude` on PATH, handed JSON to paste by hand, and
|
|
67
|
+
`claude mcp list` stayed empty. A client whose command is on PATH now counts
|
|
68
|
+
as installed; when its file is missing, `connect` creates it holding only
|
|
69
|
+
our entry (0600), which Claude Code accepts as a user-scope registration.
|
|
70
|
+
Doctor's `mcp-clients` line names the installed-but-unregistered client
|
|
71
|
+
instead of a generic "not registered". Seen on the cold-install walk.
|
|
72
|
+
|
|
73
|
+
- **`configure` writes its config where every later command can find it.**
|
|
74
|
+
It wrote `./config.yaml` into whatever directory it was run from, and
|
|
75
|
+
lookup stopped at the current directory, so a config written from `~` was
|
|
76
|
+
invisible to `doctor` or `dispatch` run inside a project — "0 configured
|
|
77
|
+
route(s)", no hint why — while the MCP client, handed the absolute path,
|
|
78
|
+
saw it fine. The default target is now the state directory
|
|
79
|
+
(`~/.harness-dispatch/config.yaml`, or under `HARNESS_DISPATCH_STATE_DIR`),
|
|
80
|
+
lookup falls back to that file after `./config.yaml` (a per-project file
|
|
81
|
+
still wins), `connect` looks in the same place, and doctor's `config` line
|
|
82
|
+
names the file it loaded. `--config` and `HARNESS_DISPATCH_CONFIG` are
|
|
83
|
+
unchanged. Seen on the cold-install walk, where a run from `/` produced
|
|
84
|
+
`/config.yaml`.
|
|
85
|
+
|
|
86
|
+
- **`doctor` now notices a Codex that is installed but not logged in.** A
|
|
87
|
+
ready route meant "the CLI is on PATH", so a never-logged-in Codex passed
|
|
88
|
+
routes, billing and safety, and the first dispatch failed with a raw OpenAI
|
|
89
|
+
`401 Unauthorized ... Missing bearer` that never mentioned `codex login`.
|
|
90
|
+
A new `harness-login` check asks `codex login status` and fails with the
|
|
91
|
+
command to run when it answers "Not logged in". The CLI is asked rather
|
|
92
|
+
than its credential file read, because Codex accepts a ChatGPT login or an
|
|
93
|
+
API key and honours its own home directory, and only its own answer is
|
|
94
|
+
right in every case. Any answer other than a definite "Not logged in" —
|
|
95
|
+
an older Codex without the subcommand, a spawn failure — is reported as
|
|
96
|
+
undetermined and does not fail the install. Codex only: the other
|
|
97
|
+
harnesses have no equivalent subcommand this tool has verified.
|
|
98
|
+
|
|
99
|
+
- **The installed command did nothing on Linux and macOS.** `npm install -g`
|
|
100
|
+
puts `harness-dispatch` on PATH as a symlink to `dist/bin.js`, and node
|
|
101
|
+
hands that unresolved link path to the program, so a run-if-entrypoint guard
|
|
102
|
+
that only looked for a filename ending in `bin.js` decided it was being
|
|
103
|
+
imported, ran nothing, and exited 0. Every documented command — `configure`,
|
|
104
|
+
`doctor`, `connect`, `dispatch`, even `--help` — printed nothing and reported
|
|
105
|
+
success on every non-Windows `npm install -g` or `npx` since the first
|
|
106
|
+
release. Windows was unaffected only because npm's `.cmd` shim passes the
|
|
107
|
+
real file path. The guard now also accepts an invoked path that resolves to
|
|
108
|
+
this file. Found by walking the README's three install steps in a clean
|
|
109
|
+
container, which nothing before had done: every prior walk ran `node
|
|
110
|
+
dist/bin.js` on this Windows machine.
|
|
111
|
+
|
|
112
|
+
- **SECURITY: workspace paths are now built and verified rather than checked
|
|
113
|
+
and trusted.** Four consecutive releases patched a guard that inspected a
|
|
114
|
+
path STRING and then let every later write re-resolve it, and each patch was
|
|
115
|
+
found incomplete by the next review: it followed the link it was checking,
|
|
116
|
+
then covered one of the two isolation policies, then only the last path
|
|
117
|
+
segment, then stopped one directory short of the one the release notes named
|
|
118
|
+
— and was silently inert altogether if the workspaces setting had a trailing
|
|
119
|
+
slash or a `..` in it. Validating once also left the rest of the dispatch
|
|
120
|
+
trusting the result, so swapping a directory for a link mid-copy redirected
|
|
121
|
+
it.
|
|
122
|
+
|
|
123
|
+
Every directory from the anchor down is now created by this tool with a
|
|
124
|
+
non-recursive `mkdir`, which cannot traverse a link it did not make, and the
|
|
125
|
+
verified path is returned and used instead of being re-derived. Destructive
|
|
126
|
+
operations re-verify immediately before acting. The symlink refusal now
|
|
127
|
+
applies on Windows too, where junctions need no privileges and were being
|
|
128
|
+
followed.
|
|
129
|
+
|
|
130
|
+
Verified as two separate users on Linux across every shape reported: a link
|
|
131
|
+
at the workspace root, at the base, at the base's parent, and with the
|
|
132
|
+
setting spelled with a trailing slash or `..` — all refused, with nothing
|
|
133
|
+
written into the attacker's directory. Legitimate use is unaffected,
|
|
134
|
+
including a symlinked temp directory, which is normal on macOS.
|
|
135
|
+
|
|
136
|
+
- **A delegated "run the tests" task no longer blocks its own route.** The
|
|
137
|
+
rate-limit phrase check ran against the whole transcript with no filter, so
|
|
138
|
+
this project's own test output — which names rate limits in test titles —
|
|
139
|
+
flagged as a real limiter, tripping the breaker for 300 seconds and
|
|
140
|
+
recording a rate limit that never happened. It is now checked per line and
|
|
141
|
+
past the same filter the numeric check uses.
|
|
142
|
+
|
|
143
|
+
- **Real limiter messages that were being missed now register**, including
|
|
144
|
+
Anthropic's `rate_limit_error` and OpenAI's "You exceeded your current
|
|
145
|
+
quota" wording. A missed limit is the worse direction: the router keeps
|
|
146
|
+
hammering a route that has already said stop.
|
|
147
|
+
|
|
148
|
+
- A metered route that declares it cannot bill you is no longer told to allow
|
|
149
|
+
paid usage. The previous release put that advice in a branch such a route
|
|
150
|
+
never reaches, so it worked for local routes only.
|
|
151
|
+
|
|
152
|
+
- The claims checker now also scans MCP resource descriptions, every
|
|
153
|
+
route-refusal message, the rendered status body and the CLI's own help text
|
|
154
|
+
— surfaces a user or agent reads that it could not see.
|
|
155
|
+
|
|
156
|
+
- **SECURITY: the workspace symlink guard covered one of the two isolation
|
|
157
|
+
policies.** The previous release applied the `lstat` refusal, the
|
|
158
|
+
guard-before-prune ordering and the sweep's name check to `copy` only.
|
|
159
|
+
`git_worktree` kept all three defects, so the identical attack still
|
|
160
|
+
destroyed data — reproduced as two real users: `copy` refused, `git_worktree`
|
|
161
|
+
deleted the victim's directory and raised the guard's error afterwards. The
|
|
162
|
+
entry claiming the class was closed was true for one policy and false for the
|
|
163
|
+
other. The test shipped with it passed `policy: "copy"` only, so the suite
|
|
164
|
+
stayed green over a live vulnerability; it now runs against both.
|
|
165
|
+
|
|
166
|
+
- **The same guard only checked the last path segment.** `<tmp>/harness-dispatch`
|
|
167
|
+
and `<tmp>/harness-dispatch/workspaces` are fixed names under a
|
|
168
|
+
world-writable directory and were never examined, so a link planted one or
|
|
169
|
+
two levels up redirected the whole copy into an attacker's tree. Every
|
|
170
|
+
segment from the workspaces base down is now checked for links and ownership.
|
|
171
|
+
|
|
172
|
+
- **A rate-limit regression from the previous release.** The filter added to
|
|
173
|
+
stop false positives discarded any line containing "should", which threw away
|
|
174
|
+
real limiter messages — a missed 429 means the router keeps hammering an
|
|
175
|
+
exhausted route. Two other patterns in that filter could never match at all.
|
|
176
|
+
|
|
177
|
+
- **A free local route is no longer told it can bill you, with a fix that does
|
|
178
|
+
nothing.** A route with a declared kind and `paid_usage_possible: false` is
|
|
179
|
+
blocked when its `billing_confidence` is `unknown` — deliberate — but was
|
|
180
|
+
shown "route can incur paid usage" beside `paid=no` and advised to set a
|
|
181
|
+
field it already had. It now names the real cause and a remedy that works.
|
|
182
|
+
|
|
183
|
+
- The `usage` tool description and README told agents that `service` and
|
|
184
|
+
`models` are unvalidated. Both throw on an unknown route id. The same wrong
|
|
185
|
+
sentence was corrected in the plugin skill one release earlier and missed
|
|
186
|
+
here — `scripts/check-claims.mjs` could not see tool descriptions at all, and
|
|
187
|
+
now does.
|
|
188
|
+
|
|
189
|
+
- `scripts/fetch_benchmarks.py` no longer replaces good benchmark data with its
|
|
190
|
+
bundled fallback when the network fails. It swallowed every exception,
|
|
191
|
+
returned an empty set, wrote it out and exited 0 — turning a transient outage
|
|
192
|
+
into a permanent downgrade of the file that ships in the package.
|
|
193
|
+
|
|
194
|
+
- An env-var reference embedded in a larger value (`base_url:
|
|
195
|
+
https://${HOST}/v1`) is now stripped from spawned CLI environments. The
|
|
196
|
+
sanitizer anchored to whole-string matches, so a key inside a longer string
|
|
197
|
+
stayed visible — the exact leak it was written to close, one string shape
|
|
198
|
+
over.
|
|
199
|
+
|
|
200
|
+
- `OPERATIONS.md` described streamed fanout wrongly (its arms DO create jobs,
|
|
201
|
+
and are not aborted on disconnect), `safety.ts` claimed every shipped harness
|
|
202
|
+
defines all three safety profiles (`cursor_cli` declares one),
|
|
203
|
+
`ux-walkthrough.md` called `discard` the only destructive operation (the
|
|
204
|
+
retention sweep is the one that deleted files), and two module headers said
|
|
205
|
+
three tools where six are registered.
|
|
206
|
+
|
|
207
|
+
- **SECURITY: a `copy` dispatch could delete files outside its workspace.**
|
|
208
|
+
The ownership guard used `stat`, which follows symlinks, so it compared the
|
|
209
|
+
uid of whatever a link POINTED AT rather than the link's own — and on a
|
|
210
|
+
shared machine the workspace path is predictable, so another local user can
|
|
211
|
+
plant one. Worse, the retention sweep that deletes aged workspaces ran
|
|
212
|
+
BEFORE the guard and had no ownership, marker or name check of its own.
|
|
213
|
+
Reproduced end to end in a container: a victim's directory chmod'd, the
|
|
214
|
+
project copied into it, and their files removed. All three now fixed —
|
|
215
|
+
`lstat` with symlinks refused outright, the guard runs before anything
|
|
216
|
+
destructive, and the sweep only deletes directories matching the name shape
|
|
217
|
+
this tool generates. The previous release's entry claiming a foreign-owned
|
|
218
|
+
root was "refused outright" was false against exactly this attack.
|
|
219
|
+
|
|
220
|
+
- **Non-ASCII output was being corrupted.** Every stdout chunk was decoded on
|
|
221
|
+
its own, so a character split across two reads became replacement
|
|
222
|
+
characters — accented text, CJK, emoji, box-drawing. This is the path every
|
|
223
|
+
job uses, so it reached partial output, saved logs and delivered results,
|
|
224
|
+
and was invisible to anyone working in English.
|
|
225
|
+
|
|
226
|
+
- **An aborted streaming request now stops the run.** Nothing connected the
|
|
227
|
+
client disconnect to the dispatch, so an abandoned stream ran to completion,
|
|
228
|
+
spending quota — and being the one path with no job record, it could not be
|
|
229
|
+
cancelled either. On a CLI route that meant an agent still editing files for
|
|
230
|
+
a caller that had gone.
|
|
231
|
+
|
|
232
|
+
- **HTTP fanout with no eligible route now refuses** instead of answering 200
|
|
233
|
+
with an empty list. CI and cron read 200 as success; the MCP surface already
|
|
234
|
+
refused the identical request by name.
|
|
235
|
+
|
|
236
|
+
- A configured `timeout_ms` now covers reading an endpoint's response body,
|
|
237
|
+
not just its headers — the same defect as the leaderboard timeout fixed last
|
|
238
|
+
release, in the path the router uses for its primary route.
|
|
239
|
+
|
|
240
|
+
- Endpoint credentials can no longer leak through streaming error text: undici
|
|
241
|
+
embeds the request URL, and this branch returned it unscrubbed while its
|
|
242
|
+
sibling scrubbed it deliberately.
|
|
243
|
+
|
|
244
|
+
- A rate-limit false positive no longer trips the breaker on text ABOUT a 429
|
|
245
|
+
— a line number, or a test assertion the delegate ran. One flag blocks a
|
|
246
|
+
route for 300 seconds and records a limit that never happened.
|
|
247
|
+
|
|
248
|
+
- Config hot-reload now notices a config whose timestamp moves BACKWARDS
|
|
249
|
+
(restored from a backup, `cp -p`, extracted from an archive), and documents
|
|
250
|
+
the two cases where it does nothing at all.
|
|
251
|
+
|
|
252
|
+
- The MCP snippet `configure` prints is now the entry `connect` actually
|
|
253
|
+
writes. Pasting the documented snippet then running `connect` was answered
|
|
254
|
+
"has an entry we did not write" — the tool called its own output
|
|
255
|
+
hand-edited and refused to update or remove it.
|
|
256
|
+
|
|
257
|
+
- The chained-context omission notice named the wrong jobs when a job id
|
|
258
|
+
repeated, listing ones whose output was directly above it.
|
|
259
|
+
|
|
260
|
+
- **An existing `copy` workspace directory is now actually secured.** The
|
|
261
|
+
previous release set 0700 with `mkdir`'s `mode` option, which applies only to
|
|
262
|
+
directories it CREATES — so everyone who had used `copy` before kept a
|
|
263
|
+
world-readable root holding a full copy of their source, and the entry
|
|
264
|
+
claiming otherwise was wrong. Measured on Linux: 0755 before, 0755 after. An
|
|
265
|
+
explicit chmod fixes them. A root owned by ANOTHER user is now refused
|
|
266
|
+
outright rather than copied into: the path is deterministic inside a shared
|
|
267
|
+
temp directory, so on the multi-user machine this guard exists for, someone
|
|
268
|
+
else can create it first.
|
|
269
|
+
|
|
270
|
+
- **A half-dead leaderboard endpoint no longer hangs every dispatch.** The 8s
|
|
271
|
+
timeout was cancelled as soon as response headers arrived, leaving the body
|
|
272
|
+
read unbounded — measured still pending after 15s. Quality scores are awaited
|
|
273
|
+
per routing candidate, so this stalled routing indefinitely. Off by default,
|
|
274
|
+
which is why it was not worse.
|
|
275
|
+
|
|
276
|
+
- **The HTTP server no longer leaks an MCP server per rejected request.** A
|
|
277
|
+
request carrying an unknown session id is answered 400 without initialising,
|
|
278
|
+
so nothing ever closed the server built for it in advance — five such
|
|
279
|
+
requests left five alive until shutdown. Sessions are also expired after 30
|
|
280
|
+
minutes idle: nothing removed them before except an explicit HTTP DELETE,
|
|
281
|
+
which the standard client never sends, so a clean shutdown left its session
|
|
282
|
+
resident forever.
|
|
283
|
+
|
|
284
|
+
- **`retry_job` on an abandoned job no longer races the original.** Its guard
|
|
285
|
+
read the derived status while the claim path reads the raw status file, so a
|
|
286
|
+
supervisor could pick up the original while the retry ran — the exact outcome
|
|
287
|
+
the command's own error text says it prevents. The original is now marked
|
|
288
|
+
cancelled.
|
|
289
|
+
|
|
290
|
+
- **Chained context says what it left out.** Jobs past the character budget
|
|
291
|
+
were dropped with no header and no note, contradicting the contract stated in
|
|
292
|
+
the same file. The docblock also claimed oldest entries truncate first, which
|
|
293
|
+
was never true — the last ones go.
|
|
294
|
+
|
|
295
|
+
- The `copy` size refusal named the parent of your project rather than the
|
|
296
|
+
workspace holding the work, and the `force` flag's description still promised
|
|
297
|
+
an uncommitted-changes refusal that only exists inside a git repository.
|
|
298
|
+
|
|
299
|
+
- **`configure` no longer deletes `detect: false`.** That key is the only
|
|
300
|
+
setting that isolates a machine from its installed paid CLIs, and it had no
|
|
301
|
+
field on the internal config object, so regenerating a file silently dropped
|
|
302
|
+
it. Measured: `detect: false` plus `max_concurrent_runs: 2` came back as the
|
|
303
|
+
latter alone, and reloading it with all four harness CLIs present produced
|
|
304
|
+
four routes on real subscriptions — under a "Wrote" message. A bare
|
|
305
|
+
`detect: false` regenerated as an empty document, where even the
|
|
306
|
+
"this config defines no routes" warning is suppressed, because its trigger
|
|
307
|
+
requires a non-empty file. `detect` now round-trips, and is written only when
|
|
308
|
+
the file stated it — carrying the resolved value would stamp `detect: true`
|
|
309
|
+
into every config that merely omitted it.
|
|
310
|
+
|
|
311
|
+
- **`connect --remove` now removes the entry.** Without `--clients` it printed
|
|
312
|
+
"our entry is here — will be removed", exited 0, and left the file
|
|
313
|
+
byte-identical. The chooser filtered out plans whose entry already matches —
|
|
314
|
+
correct when registering, and exactly backwards when removing, where a
|
|
315
|
+
matching entry is the one being removed. `--clients` bypassed that function
|
|
316
|
+
and always worked; the broken form is the one documented in README and
|
|
317
|
+
OPERATIONS.md. A hand-edited entry is still refused without `--force`, and
|
|
318
|
+
now exits non-zero rather than reporting success for work it did not do.
|
|
319
|
+
This command had no test coverage at all; it does now.
|
|
320
|
+
|
|
321
|
+
- **The test suite can no longer reach a real route.** Test setup already
|
|
322
|
+
sandboxed the log, state and jobs directories — where the suite WRITES — but
|
|
323
|
+
not config, which is what it DISCOVERS. A test that loaded config without
|
|
324
|
+
naming a file or stubbing detection picked up whatever the developer's
|
|
325
|
+
machine offers. Measured while closing this: on the maintainer's machine
|
|
326
|
+
that is the repo's own `config.yaml`, yielding four API routes with real
|
|
327
|
+
keys; on a machine with the CLIs installed it is the harness fleet, on real
|
|
328
|
+
subscriptions. One boundary test really did dispatch to Claude Code on every
|
|
329
|
+
CI run once. Setup now pins `HARNESS_DISPATCH_CONFIG` at a `detect: false`
|
|
330
|
+
sandbox, so an un-stubbed load gets an empty route table; a test wanting
|
|
331
|
+
routes passes its own path, which still wins.
|
|
332
|
+
|
|
333
|
+
- **An orphaned job can be cancelled again — when it is still able to run.**
|
|
334
|
+
There are two kinds: one written to disk when the server exits before a
|
|
335
|
+
queued job starts (genuinely terminal), and one DERIVED from a stale
|
|
336
|
+
heartbeat while the file still says `queued`. The second is not inert — a
|
|
337
|
+
supervisor reclaims it once the dead owner's claim ages out — and `cancel`
|
|
338
|
+
answered "had already finished; nothing to cancel" about work that could
|
|
339
|
+
still start, leaving no way to stop it. It now settles the status itself,
|
|
340
|
+
since an orphaned job has no runner to notice a marker. The written kind
|
|
341
|
+
still reports nothing to cancel.
|
|
342
|
+
|
|
343
|
+
- **A `copy` workspace of many files can no longer build an unbounded patch.**
|
|
344
|
+
The per-file diff was already bounded; the concatenation of those files was
|
|
345
|
+
not, so files each under the limit still summed past it. Both limits now
|
|
346
|
+
explain themselves — the worktree path used to surface the cap as
|
|
347
|
+
`stdout maxBuffer length exceeded`, which says nothing about patches or
|
|
348
|
+
about the work still being safe on disk.
|
|
349
|
+
|
|
350
|
+
- **Isolated workspaces are created 0700**, like the state and job
|
|
351
|
+
directories. A `copy` workspace holds a full copy of your source and the
|
|
352
|
+
default base is in the shared temp directory, so on a multi-user POSIX
|
|
353
|
+
machine it was readable by everyone. No effect on Windows.
|
|
354
|
+
|
|
355
|
+
- **Chained context says which project it came from.** `contextJobs` inlines
|
|
356
|
+
any job from the machine-wide store with no working-directory scoping, so a
|
|
357
|
+
job from one project could be pulled into a dispatch for another with
|
|
358
|
+
nothing indicating it. Disclosed rather than blocked: chaining across
|
|
359
|
+
projects is legitimate and the caller passes the id explicitly — what was
|
|
360
|
+
missing is that nobody could see it happen.
|
|
361
|
+
|
|
362
|
+
- **Dead supervisor heartbeats are swept.** A killed supervisor's file stopped
|
|
363
|
+
being counted but stayed forever, and the liveness check reads every file on
|
|
364
|
+
every drain. The sweep is limited to heartbeats: the crash log beside them
|
|
365
|
+
exists to explain the very supervisor that died.
|
|
366
|
+
|
|
367
|
+
- `workspace apply`'s description no longer claims a refusal it cannot always
|
|
368
|
+
make: the uncommitted-changes check needs a git repository. Outside one,
|
|
369
|
+
only the per-file check applies — a file the patch touches is still
|
|
370
|
+
refused, unrelated edits cannot be seen.
|
|
371
|
+
|
|
372
|
+
- A `timeout_ms` or token-cap warning no longer claims routing reads the
|
|
373
|
+
field. Routing does not; the range branch was fixed for this and the
|
|
374
|
+
not-a-number branch beside it was not.
|
|
375
|
+
|
|
376
|
+
- **Usage counters are no longer silently lost when two dispatches finish at
|
|
377
|
+
once.** `withFileLock` runs its critical section unlocked after a 2s
|
|
378
|
+
timeout — deliberate, and still right for the circuit breaker, which has
|
|
379
|
+
nothing to fall back on. It was wrong for the quota counters, which keep a
|
|
380
|
+
pending delta and clear it only on success: an unserialised write landed,
|
|
381
|
+
the delta was cleared as though serialised, and the process actually holding
|
|
382
|
+
the lock then overwrote the file with a value computed before that write
|
|
383
|
+
existed. Reproduced against the built artifact: five recorded calls gone,
|
|
384
|
+
no error. The counters now defer instead, and the delta lands on the next
|
|
385
|
+
result. A busy moment is no longer reported as "counters not reaching
|
|
386
|
+
disk" — they are delayed, not lost, and saying otherwise makes a working
|
|
387
|
+
system look broken.
|
|
388
|
+
|
|
389
|
+
- **A route knocked out by a 429 no longer reports `tripped: true,
|
|
390
|
+
failures: 0`.** `trip()` set the cooldown without counting the failure, so
|
|
391
|
+
`usage` — the surface an orchestrator is told to consult before delegating
|
|
392
|
+
— showed a contradiction that reads as a bookkeeping bug rather than a real
|
|
393
|
+
trip.
|
|
394
|
+
|
|
395
|
+
- **Discarding a workspace whose directory is already gone now clears the
|
|
396
|
+
registration git is still holding.** The early return skipped the block that
|
|
397
|
+
removes a worktree through git, so a workspace pruned by retention or
|
|
398
|
+
deleted by hand left `.git/worktrees/<name>` in the user's repo
|
|
399
|
+
permanently — the exact outcome `discardWorkspace`'s own documentation says
|
|
400
|
+
it exists to prevent. Reproduced: `git worktree list` still showing the path
|
|
401
|
+
as `prunable` after discard reported success.
|
|
402
|
+
|
|
403
|
+
- **An orphaned or cancelled job now hands back its partial work through the
|
|
404
|
+
tool an orchestrator actually calls.** The progress was on disk and
|
|
405
|
+
`getAsyncJob` read it, but `job_status` answered `output: ""`: orphaned and
|
|
406
|
+
cancelled count as terminal, and the poll response attached `partialOutput`
|
|
407
|
+
only on the NOT-terminal branch. So a commit titled "an orphaned job hands
|
|
408
|
+
back its progress" was correct in the module it edited and had no effect at
|
|
409
|
+
the surface a caller touches — verified at the function, not at the tool.
|
|
410
|
+
The crash path (a `failed` status with no result) was losing its output the
|
|
411
|
+
same way. The HTTP surface already salvaged this; MCP now matches it, and
|
|
412
|
+
the answer says the output is PARTIAL so salvage is not read as a result.
|
|
413
|
+
|
|
414
|
+
- **A gitignored file the agent wrote is no longer reported as applied while
|
|
415
|
+
being left behind.** Under `git_worktree`, the patch came from `git add -A
|
|
416
|
+
-N`, which obeys `.gitignore`, while the changed-file list came from a
|
|
417
|
+
filesystem fingerprint, which does not. A job that wrote a `.env` or any
|
|
418
|
+
ignored file got `applied: true` naming it, with the file absent from the
|
|
419
|
+
patch and from the project. It compounded: the already-applied guard needs
|
|
420
|
+
every recorded change present, so it never fired, and the next apply refused
|
|
421
|
+
with "changed since dispatch" — blaming the caller for the first apply's own
|
|
422
|
+
writes, the misleading refusal an earlier fix had removed. The paths already
|
|
423
|
+
recorded as changed are now force-added, which also makes `git_worktree`
|
|
424
|
+
agree with `copy`, whose per-file patch always carried them.
|
|
425
|
+
|
|
426
|
+
- **`clis: []` now isolates a config, which is what the previous release said
|
|
427
|
+
it did.** Authoritativeness keyed off a NON-EMPTY list, so the most explicit
|
|
428
|
+
way to write "no CLI routes" still loaded every harness on the machine —
|
|
429
|
+
the exact failure the change was written for, described in its own comment
|
|
430
|
+
in the past tense while remaining true. Presence of `clis:`/`endpoints:` is
|
|
431
|
+
now what makes a config authoritative, empty or not. A config that mentions
|
|
432
|
+
neither still auto-detects, unchanged.
|
|
433
|
+
|
|
434
|
+
- **An infinite or NaN route field is no longer accepted, and the warning
|
|
435
|
+
about it is no longer false.** The new range check covered negative values
|
|
436
|
+
but not `.inf`, `.nan`, or `1e999` (which YAML types as a string, and
|
|
437
|
+
`Number()` overflows to `Infinity`). Two paths were wrong: `1e999` produced
|
|
438
|
+
no warning at all, and `tier: -.inf, weight: .inf` loaded as
|
|
439
|
+
`-Infinity`/`Infinity` — ahead of every tier, above every score —
|
|
440
|
+
underneath a warning reading "IGNORED, and the built-in default applies
|
|
441
|
+
instead". A message asserting the opposite of what happened is worse than
|
|
442
|
+
the silence it replaced. Warnings also now name the value the operator
|
|
443
|
+
actually wrote (`JSON.stringify(Infinity)` is `null`, so they reported
|
|
444
|
+
`tier is null` for a file saying `-.inf`), and explain the right mechanism
|
|
445
|
+
per field rather than describing routing for `timeout_ms`.
|
|
446
|
+
|
|
447
|
+
- **`configure` no longer generates a config that fails its own `doctor`.**
|
|
448
|
+
It carried `disabled:` forward alongside the `clis:` it generates; in an
|
|
449
|
+
authoritative config that control does nothing, so `doctor` reported it had
|
|
450
|
+
no effect and exited 1. The disabled route is already absent from the
|
|
451
|
+
generated list, so the name was saying nothing. It is still emitted for a
|
|
452
|
+
generated config that lists no routes, where it does the work.
|
|
453
|
+
|
|
454
|
+
- **A `clis:` or `endpoints:` written as a mapping instead of a list now
|
|
455
|
+
says so.** The entries vanished, the config counted as defining no routes,
|
|
456
|
+
detection ran, and someone trying to name their own routes silently got
|
|
457
|
+
every installed paid harness instead — under a warning claiming their
|
|
458
|
+
config defined no routes, which contradicted the file in front of them.
|
|
459
|
+
|
|
460
|
+
- Legacy `services:` configs now get the same top-level key warnings as the
|
|
461
|
+
modern shape. That path returned before the check ran, so `policy: copy`
|
|
462
|
+
warned twice in one format and not at all in the other.
|
|
463
|
+
|
|
464
|
+
- **A negative `tier`, `weight` or `cli_capability` no longer hands a route
|
|
465
|
+
every dispatch.** These were type-checked but never range-checked, and
|
|
466
|
+
routing multiplies `weight` and `cli_capability` into the score while
|
|
467
|
+
ordering `tier` ascending — so a negative pair does not demote a route, it
|
|
468
|
+
promotes it past every legitimate one. An acceptance pass measured
|
|
469
|
+
`tier: -5, weight: -100, cli_capability: -3` scoring 299.8 against a normal
|
|
470
|
+
route's 0.88, from a tier sorting ahead of them all, with no warning
|
|
471
|
+
anywhere. Out-of-range values are now reported and ignored, so the built-in
|
|
472
|
+
default applies — the same outcome an unreadable value already got. Upper
|
|
473
|
+
bounds were deliberately not added: `cli_capability: 1.1` ships in this
|
|
474
|
+
repo's own default config as real tuning, and the defect is sign, not
|
|
475
|
+
magnitude.
|
|
476
|
+
|
|
477
|
+
- Top-level `policy:` and `workspace_policy:` now say they have no effect.
|
|
478
|
+
Both were allow-listed and read nowhere — and both ARE valid per-route keys,
|
|
479
|
+
which is what makes the top-level spelling easy to write: it looks like a
|
|
480
|
+
global default for the per-route setting, and there is no such default. An
|
|
481
|
+
isolation control that silently does nothing is the failure the config
|
|
482
|
+
validator exists to prevent. The per-route keys are unaffected.
|
|
483
|
+
|
|
484
|
+
- An `execute` task can no longer be routed to an HTTP endpoint. An endpoint
|
|
485
|
+
has no agent loop, no file access and no shell — PRODUCT.md states this as
|
|
486
|
+
design rather than gap — but an undeclared capability defaults to 1.0 and no
|
|
487
|
+
endpoint example declares any, so endpoints scored PERFECT for execute. An
|
|
488
|
+
acceptance pass measured a `--task-type execute` dispatch routed to an
|
|
489
|
+
endpoint, returning prose with exit 0: execution reported as succeeded when
|
|
490
|
+
none happened. It surfaced exactly when the CLI routes were busy or tripped,
|
|
491
|
+
which is the case a caller is least able to check.
|
|
492
|
+
|
|
493
|
+
Refused outright rather than scored low, on both counts deliberately: a score
|
|
494
|
+
of 0 still leaves a route selectable when it is the only candidate — the
|
|
495
|
+
failing case itself — and a declared capability must not override it, the
|
|
496
|
+
same rule as the safety-flag check. Endpoints remain full members of the mix
|
|
497
|
+
for plan, review and second opinions.
|
|
498
|
+
|
|
499
|
+
- An orphaned job now hands back the progress it saved. When a supervisor dies
|
|
500
|
+
there is no result, but its output is on disk in `stdout.partial.log` — and
|
|
501
|
+
the orphan branch returned above the code that reads it, so the response was
|
|
502
|
+
empty while the trail sat there. Chaining had the same gap, reporting "no
|
|
503
|
+
result available" for a job whose partial output was recoverable; it now
|
|
504
|
+
carries that output, labelled INCOMPLETE. PRODUCT.md names losing this trail
|
|
505
|
+
as the defining failure: "a wasted attempt with no trail".
|
|
506
|
+
|
|
507
|
+
- HTTP fanout arms are now job-backed, like the MCP surface has always been.
|
|
508
|
+
They called `routeTo` directly, so an arm's work existed only inside the
|
|
509
|
+
request — no job directory, no manifest, no partial log. Killing the client
|
|
510
|
+
or the server mid-fanout lost every arm's output with nothing on disk to
|
|
511
|
+
salvage, which is the failure PRODUCT.md names as defining, on one of two
|
|
512
|
+
surfaces.
|
|
513
|
+
|
|
514
|
+
The response shape is unchanged: arms are awaited and the same rows are
|
|
515
|
+
returned, so an OpenAI-compatible client sees exactly what it saw before.
|
|
516
|
+
Each row additionally carries the arm's `jobId`, which is what makes salvage
|
|
517
|
+
possible. This was briefly recorded here as deferred "because it changes what
|
|
518
|
+
a client receives" — that was wrong, and re-reading the MCP path showed why:
|
|
519
|
+
durability and the response contract are independent.
|
|
520
|
+
|
|
9
521
|
## [0.8.0] — 2026-08-31
|
|
10
522
|
|
|
11
523
|
### Added
|
|
@@ -1444,7 +1956,8 @@ the MCP surface to three tools: `dispatch`, `job_status`, `usage`.
|
|
|
1444
1956
|
Known issues in this release, fixed in 0.5.0: `configure` writes resolved API keys into
|
|
1445
1957
|
its output, and `configure --yes --force` can delete user-added harnesses.
|
|
1446
1958
|
|
|
1447
|
-
[Unreleased]: https://github.com/fstubner/harness-dispatch/compare/v0.
|
|
1959
|
+
[Unreleased]: https://github.com/fstubner/harness-dispatch/compare/v0.9.0...HEAD
|
|
1960
|
+
[0.9.0]: https://github.com/fstubner/harness-dispatch/compare/v0.8.0...v0.9.0
|
|
1448
1961
|
[0.8.0]: https://github.com/fstubner/harness-dispatch/compare/v0.7.9...v0.8.0
|
|
1449
1962
|
[0.7.9]: https://github.com/fstubner/harness-dispatch/compare/v0.7.8...v0.7.9
|
|
1450
1963
|
[0.7.8]: https://github.com/fstubner/harness-dispatch/compare/v0.7.7...v0.7.8
|
package/README.md
CHANGED
|
@@ -66,15 +66,23 @@ harness-dispatch configure --yes
|
|
|
66
66
|
harness-dispatch doctor --live
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
-
`configure --yes` detects installed harnesses, writes `config.yaml
|
|
69
|
+
`configure --yes` detects installed harnesses, writes `config.yaml` into the
|
|
70
|
+
tool's own state directory (`~/.harness-dispatch/`, or `HARNESS_DISPATCH_STATE_DIR`),
|
|
71
|
+
and then
|
|
70
72
|
offers to register this server with each MCP client it finds (Claude Code,
|
|
71
73
|
Cursor) — showing you what it would write, and what is already there, before
|
|
72
74
|
changing anything. `--no-clients` skips the offer and prints a snippet to paste
|
|
73
75
|
instead; `harness-dispatch connect` does the same registration later on its own,
|
|
74
76
|
and `connect --remove` undoes it. Without
|
|
75
|
-
`--yes` configure previews and writes nothing.
|
|
77
|
+
`--yes` configure previews and writes nothing. Re-running it regenerates a file it
|
|
78
|
+
wrote and you have not edited, so installing a harness later is just `configure --yes`
|
|
79
|
+
again; a file you have changed is refused without `--force`, and because such a file
|
|
80
|
+
lists its own routes, even `--force` regenerates it from the file rather than from a
|
|
81
|
+
fresh detection (it says so; add `detect: true` to the file to merge new harnesses). `doctor` then checks the whole chain:
|
|
76
82
|
binary, config load, harness detection, auth and billing classification, route
|
|
77
|
-
readiness
|
|
83
|
+
readiness, and for a Codex route asks `codex login status` whether the CLI is
|
|
84
|
+
logged in (the other harnesses have no equivalent this tool has verified, so
|
|
85
|
+
their login state is not checked). `--live` goes further and routes one tiny real prompt, so you see a
|
|
78
86
|
completion before wiring anything into your agent. The live probe never touches paid or
|
|
79
87
|
unknown-billing routes unless you pass `--allow-paid`.
|
|
80
88
|
|
|
@@ -211,6 +219,8 @@ Adding a harness that isn't auto-detected — a second Codex route pinned to a s
|
|
|
211
219
|
model, or a local/hosted OpenAI-compatible endpoint — is a few lines:
|
|
212
220
|
|
|
213
221
|
```yaml
|
|
222
|
+
detect: true # keep auto-detected harnesses as well; see below
|
|
223
|
+
|
|
214
224
|
clis:
|
|
215
225
|
- name: codex_sol
|
|
216
226
|
harness: codex # picks the dispatcher: claude_code | codex | cursor | antigravity_cli | generic
|
|
@@ -224,6 +234,21 @@ endpoints:
|
|
|
224
234
|
tier: 3
|
|
225
235
|
```
|
|
226
236
|
|
|
237
|
+
**`detect: true` is doing real work there.** A config that lists any `clis:` or
|
|
238
|
+
`endpoints:` is authoritative: it gets exactly the routes it names, and nothing is
|
|
239
|
+
auto-detected alongside them. Without that line, the snippet above does not *add*
|
|
240
|
+
`codex_sol` and `ollama` to your installed harnesses — it replaces them, and
|
|
241
|
+
`claude_code_cli`, `codex_cli`, `cursor_cli` and `antigravity_cli` are gone.
|
|
242
|
+
|
|
243
|
+
Which one you want depends on the goal:
|
|
244
|
+
|
|
245
|
+
| You want | Write |
|
|
246
|
+
|---|---|
|
|
247
|
+
| My routes *plus* whatever is installed | `detect: true` alongside your entries |
|
|
248
|
+
| Exactly the routes I list, nothing else | just the entries (the default) |
|
|
249
|
+
| Nothing but auto-detection, minus a route | no `clis:`/`endpoints:`, plus `disabled: [name]` |
|
|
250
|
+
| No routes at all | `detect: false` |
|
|
251
|
+
|
|
227
252
|
See the shipped [`config.default.yaml`](config.default.yaml) for the full field
|
|
228
253
|
reference (capability weights, tiers, escalation, workspace policy, and more) — copy
|
|
229
254
|
it to your own `config.yaml` and edit, or run `harness-dispatch configure` to generate
|
|
@@ -387,7 +412,7 @@ version bump.
|
|
|
387
412
|
| `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. |
|
|
388
413
|
| `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 — the original's model is left behind when the new route does not declare it, reported as `droppedModel`. Returns a new jobId; the original is untouched. |
|
|
389
414
|
| `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. |
|
|
390
|
-
| `usage` | Per-route call counts, quota, billing kind, and breaker state — check this before passing an unfamiliar `hints.model`/`service`/`models` value
|
|
415
|
+
| `usage` | Per-route call counts, quota, billing kind, and breaker state — check this before passing an unfamiliar `hints.model`/`service`/`models` value. `service` and `models` are validated — an unknown route id is rejected, naming the valid ones — while `hints.model` is forwarded to the picked harness as-is, so a wrong model name fails at the harness instead. Pass `listModels: <route id>` to fetch that `openai_compatible` route's live `GET /models` catalog instead of (or alongside) the summary. |
|
|
391
416
|
|
|
392
417
|
`workingDir` is effectively required when starting work: if you omit it, the task runs
|
|
393
418
|
in the router server's own process directory instead of your project, and the response
|
package/config.default.yaml
CHANGED
|
@@ -11,7 +11,15 @@
|
|
|
11
11
|
# actually installed. To customize, copy this file to config.yaml in your
|
|
12
12
|
# own project and edit — or run `harness-dispatch configure` to generate a
|
|
13
13
|
# starting point instead. Same schema, same parser, no special cases either
|
|
14
|
-
# way. Add a new entry and it's a route
|
|
14
|
+
# way. Add a new entry and it's a route.
|
|
15
|
+
#
|
|
16
|
+
# A config that lists any `clis:` or `endpoints:` is AUTHORITATIVE: it gets
|
|
17
|
+
# exactly the routes it names, and nothing is auto-detected alongside them.
|
|
18
|
+
# So deleting an entry from your own config.yaml removes that route, full
|
|
19
|
+
# stop — `disabled:` is not needed for it, and has no effect in such a file.
|
|
20
|
+
# `disabled:` applies only to a config that names no routes of its own and is
|
|
21
|
+
# therefore still relying on auto-detection. Add `detect: true` if you want
|
|
22
|
+
# both: your entries AND whatever is installed.
|
|
15
23
|
#
|
|
16
24
|
# Route selection scores each entry by task fit:
|
|
17
25
|
# capabilities[task_type] x leaderboard ELO x cli_capability x quota x weight
|
|
@@ -197,8 +205,16 @@ clis:
|
|
|
197
205
|
surface: cursor_agent_cli
|
|
198
206
|
auth_source: product_login
|
|
199
207
|
billing_kind: included_usage_then_on_demand
|
|
200
|
-
# Cursor
|
|
201
|
-
#
|
|
208
|
+
# Cursor stops at the plan's included pool unless you have turned on
|
|
209
|
+
# on-demand usage in Settings -> Billing -> Spending. Set true here if you
|
|
210
|
+
# did — and note the second half, which the earlier wording ("hard-stops")
|
|
211
|
+
# missed: once on-demand IS on, overage is billed in ARREARS at the end of
|
|
212
|
+
# the cycle, and Cursor's own docs recommend setting a spend limit because
|
|
213
|
+
# without one there is no cap. So this route is the one subscription in the
|
|
214
|
+
# shipped set whose overage is not prepaid, which is why the `true` here is
|
|
215
|
+
# a bigger statement than it looks. Checked against Cursor's 2026 pricing
|
|
216
|
+
# documentation on 2026-08-31; NOT live-verified, because doing so means
|
|
217
|
+
# exhausting a real included pool.
|
|
202
218
|
paid_usage_possible: false
|
|
203
219
|
# Capability floor, per requested profile. cursor-agent's capability really
|
|
204
220
|
# does differ by mode, so a single value cannot describe it:
|