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.
Files changed (157) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +582 -0
  3. package/config.default.yaml +397 -0
  4. package/data/coding_benchmarks.json +431 -0
  5. package/dist/auth.d.ts +9 -0
  6. package/dist/auth.d.ts.map +1 -0
  7. package/dist/auth.js +73 -0
  8. package/dist/auth.js.map +1 -0
  9. package/dist/billing.d.ts +7 -0
  10. package/dist/billing.d.ts.map +1 -0
  11. package/dist/billing.js +201 -0
  12. package/dist/billing.js.map +1 -0
  13. package/dist/bin.d.ts +6 -0
  14. package/dist/bin.d.ts.map +1 -0
  15. package/dist/bin.js +488 -0
  16. package/dist/bin.js.map +1 -0
  17. package/dist/breaker-store.d.ts +31 -0
  18. package/dist/breaker-store.d.ts.map +1 -0
  19. package/dist/breaker-store.js +103 -0
  20. package/dist/breaker-store.js.map +1 -0
  21. package/dist/circuit-breaker.d.ts +47 -0
  22. package/dist/circuit-breaker.d.ts.map +1 -0
  23. package/dist/circuit-breaker.js +102 -0
  24. package/dist/circuit-breaker.js.map +1 -0
  25. package/dist/config.d.ts +48 -0
  26. package/dist/config.d.ts.map +1 -0
  27. package/dist/config.js +1248 -0
  28. package/dist/config.js.map +1 -0
  29. package/dist/dashboard/live.d.ts +49 -0
  30. package/dist/dashboard/live.d.ts.map +1 -0
  31. package/dist/dashboard/live.js +149 -0
  32. package/dist/dashboard/live.js.map +1 -0
  33. package/dist/dispatch-log.d.ts +46 -0
  34. package/dist/dispatch-log.d.ts.map +1 -0
  35. package/dist/dispatch-log.js +89 -0
  36. package/dist/dispatch-log.js.map +1 -0
  37. package/dist/dispatchers/base.d.ts +55 -0
  38. package/dist/dispatchers/base.d.ts.map +1 -0
  39. package/dist/dispatchers/base.js +80 -0
  40. package/dist/dispatchers/base.js.map +1 -0
  41. package/dist/dispatchers/generic-cli.d.ts +30 -0
  42. package/dist/dispatchers/generic-cli.d.ts.map +1 -0
  43. package/dist/dispatchers/generic-cli.js +471 -0
  44. package/dist/dispatchers/generic-cli.js.map +1 -0
  45. package/dist/dispatchers/openai-compatible.d.ts +57 -0
  46. package/dist/dispatchers/openai-compatible.d.ts.map +1 -0
  47. package/dist/dispatchers/openai-compatible.js +662 -0
  48. package/dist/dispatchers/openai-compatible.js.map +1 -0
  49. package/dist/dispatchers/shared/kill-tree.d.ts +21 -0
  50. package/dist/dispatchers/shared/kill-tree.d.ts.map +1 -0
  51. package/dist/dispatchers/shared/kill-tree.js +35 -0
  52. package/dist/dispatchers/shared/kill-tree.js.map +1 -0
  53. package/dist/dispatchers/shared/rate-limit-headers.d.ts +22 -0
  54. package/dist/dispatchers/shared/rate-limit-headers.d.ts.map +1 -0
  55. package/dist/dispatchers/shared/rate-limit-headers.js +124 -0
  56. package/dist/dispatchers/shared/rate-limit-headers.js.map +1 -0
  57. package/dist/dispatchers/shared/stream-subprocess.d.ts +53 -0
  58. package/dist/dispatchers/shared/stream-subprocess.d.ts.map +1 -0
  59. package/dist/dispatchers/shared/stream-subprocess.js +274 -0
  60. package/dist/dispatchers/shared/stream-subprocess.js.map +1 -0
  61. package/dist/dispatchers/shared/subprocess.d.ts +25 -0
  62. package/dist/dispatchers/shared/subprocess.d.ts.map +1 -0
  63. package/dist/dispatchers/shared/subprocess.js +110 -0
  64. package/dist/dispatchers/shared/subprocess.js.map +1 -0
  65. package/dist/dispatchers/shared/which-available.d.ts +2 -0
  66. package/dist/dispatchers/shared/which-available.d.ts.map +1 -0
  67. package/dist/dispatchers/shared/which-available.js +8 -0
  68. package/dist/dispatchers/shared/which-available.js.map +1 -0
  69. package/dist/dispatchers/shared/windows-cmd.d.ts +38 -0
  70. package/dist/dispatchers/shared/windows-cmd.d.ts.map +1 -0
  71. package/dist/dispatchers/shared/windows-cmd.js +91 -0
  72. package/dist/dispatchers/shared/windows-cmd.js.map +1 -0
  73. package/dist/http/server.d.ts +15 -0
  74. package/dist/http/server.d.ts.map +1 -0
  75. package/dist/http/server.js +495 -0
  76. package/dist/http/server.js.map +1 -0
  77. package/dist/index.d.ts +27 -0
  78. package/dist/index.d.ts.map +1 -0
  79. package/dist/index.js +29 -0
  80. package/dist/index.js.map +1 -0
  81. package/dist/job-runner.d.ts +19 -0
  82. package/dist/job-runner.d.ts.map +1 -0
  83. package/dist/job-runner.js +42 -0
  84. package/dist/job-runner.js.map +1 -0
  85. package/dist/jobs.d.ts +116 -0
  86. package/dist/jobs.d.ts.map +1 -0
  87. package/dist/jobs.js +542 -0
  88. package/dist/jobs.js.map +1 -0
  89. package/dist/leaderboard.d.ts +62 -0
  90. package/dist/leaderboard.d.ts.map +1 -0
  91. package/dist/leaderboard.js +287 -0
  92. package/dist/leaderboard.js.map +1 -0
  93. package/dist/mcp/config-hot-reload.d.ts +64 -0
  94. package/dist/mcp/config-hot-reload.d.ts.map +1 -0
  95. package/dist/mcp/config-hot-reload.js +144 -0
  96. package/dist/mcp/config-hot-reload.js.map +1 -0
  97. package/dist/mcp/dispatcher-factory.d.ts +20 -0
  98. package/dist/mcp/dispatcher-factory.d.ts.map +1 -0
  99. package/dist/mcp/dispatcher-factory.js +30 -0
  100. package/dist/mcp/dispatcher-factory.js.map +1 -0
  101. package/dist/mcp/resources.d.ts +8 -0
  102. package/dist/mcp/resources.d.ts.map +1 -0
  103. package/dist/mcp/resources.js +45 -0
  104. package/dist/mcp/resources.js.map +1 -0
  105. package/dist/mcp/server.d.ts +36 -0
  106. package/dist/mcp/server.d.ts.map +1 -0
  107. package/dist/mcp/server.js +80 -0
  108. package/dist/mcp/server.js.map +1 -0
  109. package/dist/mcp/tools.d.ts +171 -0
  110. package/dist/mcp/tools.d.ts.map +1 -0
  111. package/dist/mcp/tools.js +658 -0
  112. package/dist/mcp/tools.js.map +1 -0
  113. package/dist/observability/index.d.ts +52 -0
  114. package/dist/observability/index.d.ts.map +1 -0
  115. package/dist/observability/index.js +111 -0
  116. package/dist/observability/index.js.map +1 -0
  117. package/dist/observability/spans.d.ts +47 -0
  118. package/dist/observability/spans.d.ts.map +1 -0
  119. package/dist/observability/spans.js +72 -0
  120. package/dist/observability/spans.js.map +1 -0
  121. package/dist/quota.d.ts +91 -0
  122. package/dist/quota.d.ts.map +1 -0
  123. package/dist/quota.js +331 -0
  124. package/dist/quota.js.map +1 -0
  125. package/dist/route-policy.d.ts +24 -0
  126. package/dist/route-policy.d.ts.map +1 -0
  127. package/dist/route-policy.js +92 -0
  128. package/dist/route-policy.js.map +1 -0
  129. package/dist/router.d.ts +162 -0
  130. package/dist/router.d.ts.map +1 -0
  131. package/dist/router.js +878 -0
  132. package/dist/router.js.map +1 -0
  133. package/dist/safety.d.ts +7 -0
  134. package/dist/safety.d.ts.map +1 -0
  135. package/dist/safety.js +33 -0
  136. package/dist/safety.js.map +1 -0
  137. package/dist/status.d.ts +100 -0
  138. package/dist/status.d.ts.map +1 -0
  139. package/dist/status.js +228 -0
  140. package/dist/status.js.map +1 -0
  141. package/dist/types.d.ts +402 -0
  142. package/dist/types.d.ts.map +1 -0
  143. package/dist/types.js +9 -0
  144. package/dist/types.js.map +1 -0
  145. package/dist/version.d.ts +2 -0
  146. package/dist/version.d.ts.map +1 -0
  147. package/dist/version.js +13 -0
  148. package/dist/version.js.map +1 -0
  149. package/dist/working-dir.d.ts +15 -0
  150. package/dist/working-dir.d.ts.map +1 -0
  151. package/dist/working-dir.js +22 -0
  152. package/dist/working-dir.js.map +1 -0
  153. package/dist/workspaces.d.ts +19 -0
  154. package/dist/workspaces.d.ts.map +1 -0
  155. package/dist/workspaces.js +352 -0
  156. package/dist/workspaces.js.map +1 -0
  157. package/package.json +96 -0
@@ -0,0 +1,397 @@
1
+ # harness-dispatch configuration — shipped defaults
2
+ #
3
+ # This is the config harness-dispatch ships with — not a separate "defaults
4
+ # registry" in some other format, just an ordinary config.yaml shape. A
5
+ # DIFFERENT file from your own config.yaml on purpose: this one is tracked
6
+ # and updated by the package itself, yours is gitignored/untouched by
7
+ # updates and may hold secrets/local overrides — keeping them separate means
8
+ # neither ever clobbers the other. The entries below are both the built-in
9
+ # defaults and the worked examples of the pattern. With no config.yaml of
10
+ # your own, the router uses this file directly, filtered by which CLIs are
11
+ # actually installed. To customize, copy this file to config.yaml in your
12
+ # own project and edit — or run `harness-dispatch configure` to generate a
13
+ # starting point instead. Same schema, same parser, no special cases either
14
+ # way. Add a new entry and it's a route; delete one and it's gone.
15
+ #
16
+ # Route selection scores each entry by task fit:
17
+ # capabilities[task_type] x leaderboard ELO x cli_capability x quota x weight
18
+ # Billing safety: routes that can cost money (metered api_key, unknown billing)
19
+ # stay blocked until you set allow_paid_usage: true on them. Product-login CLIs
20
+ # (Claude Code, Codex, Cursor) run by default — their providers hard-stop at
21
+ # your plan's included usage.
22
+ #
23
+ # ${ENV_VAR} anywhere in a string value is replaced from the environment.
24
+
25
+ clis:
26
+ # ── Claude Code ─────────────────────────────────────────────────────────────
27
+ # Best for plan + review (extended thinking). Subscription auth via the CLI's
28
+ # own login; add api_key: ${ANTHROPIC_API_KEY} only for API billing.
29
+ - name: claude_code_cli
30
+ harness: claude_code
31
+ command: claude
32
+ model_hint: >-
33
+ Anthropic model family only. Current model ids:
34
+ https://platform.claude.com/docs/en/docs/about-claude/models/overview
35
+ (e.g. claude-opus-4-8, claude-sonnet-5) — the installed claude CLI may
36
+ lag behind what's newly listed there.
37
+ leaderboard_model: claude-opus-4-6
38
+ cli_capability: 1.1
39
+ tier: 1
40
+ capabilities: { execute: 0.95, plan: 1.0, review: 1.0 }
41
+ max_output_tokens: 64000
42
+ max_input_tokens: 1000000 # Opus + Sonnet 1M context
43
+ provider: anthropic
44
+ surface: claude_agent_sdk
45
+ auth_source: oauth_session
46
+ # protocol: is the full invocation template — args is a literal command
47
+ # line; {{tokens}} are substituted at dispatch time (see the reference at
48
+ # the bottom of this file).
49
+ protocol:
50
+ args: ["-p", "{{prompt}}", "--output-format", "json", "{{safety}}", "{{model}}"]
51
+ model: { flag: "--model" }
52
+ safety:
53
+ read_only: ["--allowedTools", "Read", "--permission-mode", "plan"]
54
+ workspace_edit: ["--allowedTools", "Read,Edit,Write", "--permission-mode", "acceptEdits"]
55
+ full_auto: ["--allowedTools", "Bash,Read,Edit,Write", "--permission-mode", "acceptEdits"]
56
+ # Claude Code reads ANTHROPIC_API_KEY itself to switch from
57
+ # subscription/OAuth session auth to API-key billing — without this,
58
+ # setting api_key: on the route reclassified billing to metered_api
59
+ # (blocking it) while the CLI kept silently using subscription auth.
60
+ api_key_env_var: ANTHROPIC_API_KEY
61
+ file_list_header: "Files to work with:"
62
+ file_list_bullet: " "
63
+ output:
64
+ mode: json_field
65
+ fields: [result, response, text]
66
+ usage: { input: [usage.input_tokens], output: [usage.output_tokens] }
67
+ # Confirmed live 2026-07-24: a successful response carries a sibling
68
+ # "is_error": false (and "api_error_status": null) alongside "result"
69
+ # — without this, an API-level failure that still exits 0 would be
70
+ # reported as success (success_requires_output: false takes ANY
71
+ # exit-0 output as a win, including an error's own JSON text).
72
+ # message_fields omitted deliberately: the error text lands in the
73
+ # same "result" field as a successful answer, already covered above.
74
+ error: { field: is_error }
75
+ success_requires_output: false
76
+
77
+ # ── Codex ───────────────────────────────────────────────────────────────────
78
+ # Best for execute — full-auto drives code + tests in a loop. Streams NDJSON
79
+ # events mid-run; event_rules below surface tool_use/thinking/usage live.
80
+ - name: codex_cli
81
+ harness: codex
82
+ command: codex
83
+ model_hint: >-
84
+ OpenAI/Codex model family only. Current model ids:
85
+ https://developers.openai.com/api/docs/models (e.g. gpt-5.6-sol,
86
+ gpt-5.6-terra) — the installed codex CLI may lag behind what's newly
87
+ listed there.
88
+ leaderboard_model: gpt-5.4
89
+ cli_capability: 1.08
90
+ tier: 1
91
+ capabilities: { execute: 1.0, plan: 0.83, review: 0.82 }
92
+ max_output_tokens: 128000
93
+ max_input_tokens: 400000
94
+ provider: openai
95
+ surface: codex_cli
96
+ auth_source: product_login
97
+ billing_kind: included_plan_then_flexible_credits
98
+ # OpenAI hard-stops at the plan's included usage unless you enabled
99
+ # flexible-credit overage on your account — set true here if you did.
100
+ paid_usage_possible: false
101
+ protocol:
102
+ # "exec" is Codex's required subcommand; trailing "-" = read prompt from
103
+ # stdin (stdin: true below).
104
+ args: ["exec", "{{native_args}}", "{{safety}}", "{{model}}", "{{working_dir}}", "--json", "-"]
105
+ stdin: true
106
+ working_dir: { flag: "--cd", extra_args_when_set: ["--skip-git-repo-check"] }
107
+ model: { flag: "--model" }
108
+ safety:
109
+ read_only: ["--sandbox", "read-only"]
110
+ workspace_edit: ["--sandbox", "workspace-write"]
111
+ full_auto: ["--full-auto"]
112
+ api_key_env_var: OPENAI_API_KEY
113
+ file_list_header: "Files to work with:"
114
+ file_list_bullet: " "
115
+ output:
116
+ mode: jsonl_stream
117
+ event_rules:
118
+ - when: { type: "item.completed", "item.type": "agent_message" }
119
+ emit: text
120
+ text_field: item.text
121
+ - when: { type: "message" }
122
+ emit: text
123
+ text_field: message.content
124
+ - when: { "item.type": "tool_use" }
125
+ emit: tool_use
126
+ name_field: item.name
127
+ input_field: item.input
128
+ - when: { type: "thinking" }
129
+ emit: thinking
130
+ chunk_field: item.text
131
+ - when: {} # unconditional — fires on every line carrying usage
132
+ emit: usage
133
+ input_token_fields: [usage.input_tokens, usage.prompt_tokens]
134
+ output_token_fields: [usage.output_tokens, usage.completion_tokens]
135
+ # Real shapes confirmed live 2026-07-24 against an actually-exhausted
136
+ # account: {"type":"error","message":"You've hit your usage
137
+ # limit..."} followed by {"type":"turn.failed","error":{"message":
138
+ # "..."}}. Without these, the dispatcher never sees either frame —
139
+ # it falls back to stderr text (just "Reading additional input from
140
+ # stdin..." noise) and reports a useless, unclassifiable failure.
141
+ - when: { type: "error" }
142
+ emit: error
143
+ message_field: message
144
+ - when: { type: "turn.failed" }
145
+ emit: error
146
+ message_field: error.message
147
+ success_requires_output: false
148
+ # Used only when a route runs endpoint_mode: harness_native_endpoint
149
+ # against a matching endpoint_provider (see codex_ollama example below).
150
+ endpoint_native_args:
151
+ ollama: ["--oss", "--local-provider", "ollama"]
152
+ lmstudio: ["--oss", "--local-provider", "lmstudio"]
153
+
154
+ # ── Cursor ──────────────────────────────────────────────────────────────────
155
+ # Best for execute with editor-aware codebase indexing. Note: Cursor print
156
+ # mode has write+shell capability, so these routes run under full_auto —
157
+ # they're skipped for read_only/workspace_edit requests.
158
+ - name: cursor_cli
159
+ harness: cursor
160
+ command: cursor-agent
161
+ model_hint: >-
162
+ Wide multi-vendor catalog. Current model ids and pricing:
163
+ https://cursor.com/docs/models — or run `cursor-agent --list-models`
164
+ for what's actually available on this install.
165
+ leaderboard_model: claude-sonnet-4-6
166
+ cli_capability: 1.05
167
+ tier: 1
168
+ capabilities: { execute: 1.0, plan: 0.82, review: 0.9 }
169
+ max_output_tokens: 64000
170
+ max_input_tokens: 1000000
171
+ provider: cursor
172
+ surface: cursor_agent_cli
173
+ auth_source: product_login
174
+ billing_kind: included_usage_then_on_demand
175
+ # Cursor hard-stops at the plan's included pool unless on-demand billing is
176
+ # enabled in Settings -> Billing -> Spending — set true here if you did.
177
+ paid_usage_possible: false
178
+ # Capability floor: cursor-agent print mode always has write+shell
179
+ # capability, so this route only serves requests that allow full_auto —
180
+ # it is skipped (safety_incompatible) for read_only/workspace_edit calls.
181
+ # Override here if a future cursor-agent grows restricted modes.
182
+ effective_safety: full_auto
183
+ protocol:
184
+ # -p is Cursor's boolean "print mode" flag; the prompt itself is the
185
+ # trailing positional {{prompt}}.
186
+ args: ["-p", "--trust", "{{working_dir}}", "--output-format", "json", "{{model}}", "{{prompt}}"]
187
+ # cursor-agent needs SOME --workspace value even with no workingDir —
188
+ # fall back to the user's home directory.
189
+ working_dir: { flag: "--workspace", fallback: home }
190
+ model: { flag: "--model" }
191
+ api_key_env_var: CURSOR_API_KEY
192
+ file_list_header: "Focus on these files:"
193
+ file_list_bullet: " - "
194
+ output:
195
+ mode: json_field
196
+ fields: [result, output, text]
197
+ usage: { input: [usage.input_tokens, usage.prompt_tokens], output: [usage.output_tokens, usage.completion_tokens] }
198
+ # success_requires_output omitted -> strict: exit 0 with no parseable
199
+ # output still counts as failure.
200
+ # No protocol.output.error entry: unlike Codex/Claude Code above (both
201
+ # verified live 2026-07-24 against real exhausted-quota responses),
202
+ # cursor-agent isn't installed on this machine to capture its actual
203
+ # on-quota-exhaustion JSON shape — deliberately not guessing one. A
204
+ # real Cursor exhaustion still falls through to detectRateLimit's raw
205
+ # text scan (rate limit/quota exceeded/usage limit/429), just without
206
+ # a structured message or forced-failure-past-exit-0 override.
207
+
208
+ # ── Antigravity ─────────────────────────────────────────────────────────────
209
+ # Google's successor to Gemini CLI (shut down 2026-06-18). Best for plan +
210
+ # review — 1M+ context holds whole codebases; free_quota billing. Model
211
+ # selection lives in the agy CLI's own config, not per route.
212
+ - name: antigravity_cli
213
+ harness: antigravity_cli
214
+ command: agy
215
+ model_hint: >-
216
+ Cross-vendor catalog (Gemini plus some Claude/GPT-OSS models). Gemini's
217
+ portion: https://ai.google.dev/gemini-api/docs/models; check the agy
218
+ CLI's own docs for the rest.
219
+ leaderboard_model: gemini-3.1-pro-preview # closest benchmarked model, for scoring
220
+ cli_capability: 1.0
221
+ tier: 1
222
+ capabilities: { execute: 0.87, plan: 0.97, review: 0.95 }
223
+ max_output_tokens: 65536
224
+ max_input_tokens: 2000000
225
+ provider: google
226
+ surface: antigravity_cli
227
+ auth_source: product_login
228
+ billing_kind: free_quota
229
+ paid_usage_possible: false
230
+ protocol:
231
+ args: ["{{model}}", "{{safety}}", "{{working_dir}}", "{{file_dirs}}", "--print", "{{prompt}}"]
232
+ # --print is headless: agy cannot prompt for per-tool permissions, so
233
+ # every profile needs auto-approval or tools are silently auto-denied
234
+ # ("a tool required the read_file permission that headless mode cannot
235
+ # prompt for"). read_only stays safe via --mode plan --sandbox (blocks
236
+ # writes/shell); the skip-permissions flag just lets reads proceed.
237
+ working_dir: { flag: "--add-dir" }
238
+ file_dirs: { flag: "--add-dir" }
239
+ model: { flag: "--model" }
240
+ safety:
241
+ read_only: ["--mode", "plan", "--sandbox", "--dangerously-skip-permissions"]
242
+ workspace_edit: ["--mode", "accept-edits", "--dangerously-skip-permissions"]
243
+ full_auto: ["--dangerously-skip-permissions"]
244
+ file_list_header: "Files to work with:"
245
+ file_list_bullet: " "
246
+ output:
247
+ mode: text
248
+ success_requires_output: false
249
+
250
+ # ── More routes from the same harnesses ─────────────────────────────────────
251
+ # A route is (harness, model) — declare as many as you want. `harness:` picks
252
+ # up everything from the matching entry above (including protocol), so a
253
+ # variant is a few lines:
254
+ #
255
+ # - name: codex_sol
256
+ # harness: codex
257
+ # model: gpt-5.6-sol
258
+ # tier: 1
259
+ # weight: 0.9
260
+ #
261
+ # - name: codex_ollama # Codex harness driving a local Ollama model
262
+ # harness: codex
263
+ # model: qwen3-coder:latest
264
+ # endpoint_mode: harness_native_endpoint
265
+ # endpoint_provider: ollama
266
+ # billing_kind: local_compute
267
+ # tier: 3
268
+
269
+ # ── A wholly new CLI harness ────────────────────────────────────────────────
270
+ # harness: generic + your own protocol: block — no code changes. protocol.args
271
+ # is written the way you'd type the command line; reserved {{tokens}}:
272
+ #
273
+ # {{prompt}} the prompt text (omitted from args if stdin: true)
274
+ # {{model}} [model.flag, value] if a model is set, else nothing
275
+ # {{safety}} safety[requested profile], zero or more tokens
276
+ # {{working_dir}} [working_dir.flag, dir, ...extra_args_when_set] if set
277
+ # {{file_dirs}} [file_dirs.flag, dir] per unique file directory
278
+ # {{native_args}} endpoint_native_args[endpoint_provider] under
279
+ # endpoint_mode: harness_native_endpoint
280
+ #
281
+ # Everything else in args passes through literally. You can also reuse an
282
+ # entry above by name (protocol: cursor) or extend one (protocol: { extends:
283
+ # codex, model: { flag: "--llm-model" } }) — safety merges per-profile.
284
+ # See README.md#adding-a-harness for the full reference.
285
+ #
286
+ # - name: my_cli
287
+ # harness: generic
288
+ # command: my-cli
289
+ # tier: 3
290
+ # protocol:
291
+ # args: ["-p", "{{prompt}}", "{{model}}", "--json"]
292
+ # model: { flag: "--model" }
293
+ # output: { mode: json_field, fields: [result] }
294
+
295
+ endpoints:
296
+ # OpenAI-compatible HTTP endpoints — local or hosted. Same idea: an entry is
297
+ # a route. Endpoints are structurally read_only (no agent loop or file
298
+ # access), so they serve plan/review calls, never execute.
299
+ #
300
+ # - name: ollama
301
+ # base_url: http://localhost:11434/v1
302
+ # model: llama3.2
303
+ # billing_kind: local_compute
304
+ # tier: 3
305
+ #
306
+ # - name: lmstudio
307
+ # base_url: http://localhost:1234/v1
308
+ # model: mistral-7b-instruct
309
+ # api_key: lm-studio
310
+ # billing_kind: local_compute
311
+ # tier: 3
312
+ #
313
+ # - name: gemini_api # AI Studio direct (free tier)
314
+ # base_url: https://generativelanguage.googleapis.com/v1beta/openai
315
+ # model: gemini-2.5-flash
316
+ # api_key: ${GEMINI_API_KEY}
317
+ # billing_kind: free_quota
318
+ # tier: 3
319
+ #
320
+ # - name: openrouter # metered — blocked until you opt in
321
+ # base_url: https://openrouter.ai/api/v1
322
+ # model: openai/gpt-5.4
323
+ # api_key: ${OPENROUTER_API_KEY}
324
+ # allow_paid_usage: true
325
+ # tier: 1
326
+ #
327
+ # - name: nvidia_nim # NVIDIA-hosted models (build.nvidia.com)
328
+ # base_url: https://integrate.api.nvidia.com/v1
329
+ # model: qwen/qwen3-coder-480b-a35b-instruct # any model id from build.nvidia.com
330
+ # api_key: ${NVIDIA_API_KEY}
331
+ # allow_paid_usage: true # metered API key -> blocked until you opt in
332
+ # tier: 2
333
+ # # Optional, both surfaced to calling agents via the `usage` tool. `usage`
334
+ # # with listModels: nvidia_nim returns this models: list VERBATIM, no
335
+ # # network call — declaring it overrides live discovery, e.g. to pin
336
+ # # specific ids or because the endpoint's live /models is noisy/untrusted.
337
+ # # Omit models: and listModels instead does a live GET {base_url}/models
338
+ # # (key stays server-side either way). model_hint points humans/agents at
339
+ # # the full catalog for picking new ids to add here.
340
+ # models: [qwen/qwen3-coder-480b-a35b-instruct]
341
+ # model_hint: "Full catalog: https://build.nvidia.com/models"
342
+ #
343
+ # - name: anthropic_api # Claude via the direct Anthropic API
344
+ # base_url: https://api.anthropic.com/v1
345
+ # model: claude-opus-4-6
346
+ # api_key: ${ANTHROPIC_API_KEY}
347
+ # wire_protocol: anthropic_messages # see below — different from the default
348
+ # allow_paid_usage: true # metered API key -> blocked until you opt in
349
+ # max_output_tokens: 8192 # required by the Messages API; defaults to 8192 if omitted
350
+ # tier: 1
351
+ #
352
+ # wire_protocol picks which request/response shape this endpoint speaks:
353
+ #
354
+ # openai_chat_completions (default) POST {base_url}/chat/completions,
355
+ # Authorization: Bearer <api_key>, OpenAI's messages[]/choices[] shape.
356
+ # Covers Ollama, LM Studio, OpenRouter, NVIDIA NIM, Gemini's OpenAI-
357
+ # compatible endpoint, and most other hosted/local model servers.
358
+ #
359
+ # anthropic_messages POST {base_url}/messages, x-api-key + anthropic-
360
+ # version headers (NOT Bearer), system prompt at the top level (not a
361
+ # messages[] entry), max_tokens required, content: [{type,text}] +
362
+ # usage: {input_tokens,output_tokens} response shape. Use this for
363
+ # Anthropic's own API or any third-party host that mirrors its
364
+ # Messages API. thinking_level isn't translated to Anthropic's
365
+ # extended-thinking parameter yet.
366
+ #
367
+ # - name: claude_compatible_host # a third party serving the Anthropic shape
368
+ # base_url: https://your-host.example.com/v1
369
+ # model: claude-opus-4-6
370
+ # api_key: ${CLAUDE_COMPATIBLE_API_KEY}
371
+ # wire_protocol: anthropic_messages
372
+ # allow_paid_usage: true
373
+ # tier: 2
374
+
375
+ # ── Global settings ───────────────────────────────────────────────────────────
376
+
377
+ # OpenTelemetry traces — OFF by default. Purely local operator observability:
378
+ # only useful if you run an OTLP collector (Jaeger, Tempo, ...) on
379
+ # localhost:4318 (or point OTEL_EXPORTER_OTLP_ENDPOINT elsewhere). Nothing is
380
+ # ever sent anywhere unless you enable this AND run a collector.
381
+ # telemetry:
382
+ # enabled: true
383
+
384
+ # How long job artifacts (~/.harness-dispatch/jobs: manifests, stdout/stderr
385
+ # logs, workspace snapshots) are kept before being pruned. Default: 7 days.
386
+ # The dispatch log (~/.harness-dispatch/logs/dispatches.jsonl) is size-capped
387
+ # at ~5MB with one rotation instead of time-based.
388
+ # retention:
389
+ # jobs_days: 7
390
+
391
+ # Per-route knobs worth knowing (all optional, all shown in the entries above
392
+ # or README.md): enabled, weight, timeout_ms, safety_profile, effective_safety,
393
+ # workspace_policy (shared | shared_locked | copy | git_worktree),
394
+ # thinking_level, escalate_on, allow_paid_usage, billing_kind,
395
+ # paid_usage_possible.
396
+ #
397
+ # disabled: [route_name, ...] hides an entry without deleting it.