harness-dispatch 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/README.md +228 -86
  2. package/config.default.yaml +416 -397
  3. package/dist/auth.d.ts.map +1 -1
  4. package/dist/auth.js +2 -2
  5. package/dist/auth.js.map +1 -1
  6. package/dist/billing.d.ts +35 -4
  7. package/dist/billing.d.ts.map +1 -1
  8. package/dist/billing.js +48 -17
  9. package/dist/billing.js.map +1 -1
  10. package/dist/bin.d.ts.map +1 -1
  11. package/dist/bin.js +253 -23
  12. package/dist/bin.js.map +1 -1
  13. package/dist/breaker-store.d.ts +60 -9
  14. package/dist/breaker-store.d.ts.map +1 -1
  15. package/dist/breaker-store.js +246 -50
  16. package/dist/breaker-store.js.map +1 -1
  17. package/dist/circuit-breaker.d.ts +35 -0
  18. package/dist/circuit-breaker.d.ts.map +1 -1
  19. package/dist/circuit-breaker.js +68 -11
  20. package/dist/circuit-breaker.js.map +1 -1
  21. package/dist/config/coercions.d.ts +31 -0
  22. package/dist/config/coercions.d.ts.map +1 -0
  23. package/dist/config/coercions.js +169 -0
  24. package/dist/config/coercions.js.map +1 -0
  25. package/dist/config/env-interpolation.d.ts +36 -0
  26. package/dist/config/env-interpolation.d.ts.map +1 -0
  27. package/dist/config/env-interpolation.js +73 -0
  28. package/dist/config/env-interpolation.js.map +1 -0
  29. package/dist/config/protocol.d.ts +51 -0
  30. package/dist/config/protocol.d.ts.map +1 -0
  31. package/dist/config/protocol.js +300 -0
  32. package/dist/config/protocol.js.map +1 -0
  33. package/dist/config/validation.d.ts +72 -0
  34. package/dist/config/validation.d.ts.map +1 -0
  35. package/dist/config/validation.js +189 -0
  36. package/dist/config/validation.js.map +1 -0
  37. package/dist/config.d.ts +19 -2
  38. package/dist/config.d.ts.map +1 -1
  39. package/dist/config.js +229 -511
  40. package/dist/config.js.map +1 -1
  41. package/dist/dispatch-log.d.ts.map +1 -1
  42. package/dist/dispatch-log.js +2 -3
  43. package/dist/dispatch-log.js.map +1 -1
  44. package/dist/dispatchers/generic-cli.d.ts +15 -1
  45. package/dist/dispatchers/generic-cli.d.ts.map +1 -1
  46. package/dist/dispatchers/generic-cli.js +65 -5
  47. package/dist/dispatchers/generic-cli.js.map +1 -1
  48. package/dist/dispatchers/openai-compatible.d.ts.map +1 -1
  49. package/dist/dispatchers/openai-compatible.js +52 -2
  50. package/dist/dispatchers/openai-compatible.js.map +1 -1
  51. package/dist/dispatchers/shared/kill-tree.d.ts +17 -4
  52. package/dist/dispatchers/shared/kill-tree.d.ts.map +1 -1
  53. package/dist/dispatchers/shared/kill-tree.js +28 -4
  54. package/dist/dispatchers/shared/kill-tree.js.map +1 -1
  55. package/dist/dispatchers/shared/rate-limit-headers.d.ts +17 -0
  56. package/dist/dispatchers/shared/rate-limit-headers.d.ts.map +1 -1
  57. package/dist/dispatchers/shared/rate-limit-headers.js +27 -0
  58. package/dist/dispatchers/shared/rate-limit-headers.js.map +1 -1
  59. package/dist/dispatchers/shared/stream-subprocess.d.ts.map +1 -1
  60. package/dist/dispatchers/shared/stream-subprocess.js +5 -0
  61. package/dist/dispatchers/shared/stream-subprocess.js.map +1 -1
  62. package/dist/dispatchers/shared/subprocess.d.ts.map +1 -1
  63. package/dist/dispatchers/shared/subprocess.js +3 -0
  64. package/dist/dispatchers/shared/subprocess.js.map +1 -1
  65. package/dist/dispatchers/shared/which-available.d.ts.map +1 -1
  66. package/dist/dispatchers/shared/which-available.js +16 -2
  67. package/dist/dispatchers/shared/which-available.js.map +1 -1
  68. package/dist/file-lock.d.ts +17 -0
  69. package/dist/file-lock.d.ts.map +1 -0
  70. package/dist/file-lock.js +143 -0
  71. package/dist/file-lock.js.map +1 -0
  72. package/dist/http/server.d.ts +11 -0
  73. package/dist/http/server.d.ts.map +1 -1
  74. package/dist/http/server.js +331 -162
  75. package/dist/http/server.js.map +1 -1
  76. package/dist/job-runner.js +23 -5
  77. package/dist/job-runner.js.map +1 -1
  78. package/dist/jobs.d.ts +91 -1
  79. package/dist/jobs.d.ts.map +1 -1
  80. package/dist/jobs.js +647 -36
  81. package/dist/jobs.js.map +1 -1
  82. package/dist/leaderboard.d.ts +24 -1
  83. package/dist/leaderboard.d.ts.map +1 -1
  84. package/dist/leaderboard.js +94 -2
  85. package/dist/leaderboard.js.map +1 -1
  86. package/dist/mcp/config-hot-reload.d.ts.map +1 -1
  87. package/dist/mcp/config-hot-reload.js +1 -1
  88. package/dist/mcp/config-hot-reload.js.map +1 -1
  89. package/dist/mcp/dispatcher-factory.d.ts +16 -1
  90. package/dist/mcp/dispatcher-factory.d.ts.map +1 -1
  91. package/dist/mcp/dispatcher-factory.js +41 -3
  92. package/dist/mcp/dispatcher-factory.js.map +1 -1
  93. package/dist/mcp/tools.d.ts +23 -4
  94. package/dist/mcp/tools.d.ts.map +1 -1
  95. package/dist/mcp/tools.js +163 -16
  96. package/dist/mcp/tools.js.map +1 -1
  97. package/dist/observability/index.d.ts +16 -3
  98. package/dist/observability/index.d.ts.map +1 -1
  99. package/dist/observability/index.js +23 -7
  100. package/dist/observability/index.js.map +1 -1
  101. package/dist/observability/spans.d.ts.map +1 -1
  102. package/dist/observability/spans.js +4 -1
  103. package/dist/observability/spans.js.map +1 -1
  104. package/dist/quota.d.ts +88 -22
  105. package/dist/quota.d.ts.map +1 -1
  106. package/dist/quota.js +163 -58
  107. package/dist/quota.js.map +1 -1
  108. package/dist/route-policy.d.ts.map +1 -1
  109. package/dist/route-policy.js +13 -3
  110. package/dist/route-policy.js.map +1 -1
  111. package/dist/router.d.ts +25 -1
  112. package/dist/router.d.ts.map +1 -1
  113. package/dist/router.js +94 -40
  114. package/dist/router.js.map +1 -1
  115. package/dist/safety.d.ts.map +1 -1
  116. package/dist/safety.js +11 -2
  117. package/dist/safety.js.map +1 -1
  118. package/dist/state-dir.d.ts +17 -0
  119. package/dist/state-dir.d.ts.map +1 -0
  120. package/dist/state-dir.js +21 -0
  121. package/dist/state-dir.js.map +1 -0
  122. package/dist/status.d.ts +36 -0
  123. package/dist/status.d.ts.map +1 -1
  124. package/dist/status.js +61 -4
  125. package/dist/status.js.map +1 -1
  126. package/dist/types.d.ts +50 -1
  127. package/dist/types.d.ts.map +1 -1
  128. package/dist/working-dir.d.ts +13 -0
  129. package/dist/working-dir.d.ts.map +1 -1
  130. package/dist/working-dir.js +27 -0
  131. package/dist/working-dir.js.map +1 -1
  132. package/dist/workspace-lock.d.ts +40 -0
  133. package/dist/workspace-lock.d.ts.map +1 -0
  134. package/dist/workspace-lock.js +258 -0
  135. package/dist/workspace-lock.js.map +1 -0
  136. package/dist/workspaces.d.ts +15 -0
  137. package/dist/workspaces.d.ts.map +1 -1
  138. package/dist/workspaces.js +104 -12
  139. package/dist/workspaces.js.map +1 -1
  140. package/package.json +98 -96
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAOA,wBAAgB,OAAO,IAAI,MAAM,CAEhC;AAED,wBAAgB,SAAS,IAAI,MAAM,CAElC;AAED,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C;AAED,wBAAsB,aAAa,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAS5D;AAED,wBAAsB,eAAe,IAAI,OAAO,CAAC,MAAM,CAAC,CAOvD;AAED,wBAAsB,eAAe,IAAI,OAAO,CAAC,MAAM,CAAC,CAKvD;AAmBD,wBAAgB,YAAY,CAC1B,mBAAmB,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,EAClD,KAAK,EAAE,MAAM,GAAG,IAAI,GACnB,OAAO,CAOT;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAG/C"}
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AASA,wBAAgB,OAAO,IAAI,MAAM,CAEhC;AAED,wBAAgB,SAAS,IAAI,MAAM,CAElC;AAED,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C;AAED,wBAAsB,aAAa,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAS5D;AAED,wBAAsB,eAAe,IAAI,OAAO,CAAC,MAAM,CAAC,CAOvD;AAED,wBAAsB,eAAe,IAAI,OAAO,CAAC,MAAM,CAAC,CAKvD;AAmBD,wBAAgB,YAAY,CAC1B,mBAAmB,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,EAClD,KAAK,EAAE,MAAM,GAAG,IAAI,GACnB,OAAO,CAOT;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAG/C"}
package/dist/auth.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import { randomBytes, timingSafeEqual } from "node:crypto";
2
2
  import { promises as fs } from "node:fs";
3
- import os from "node:os";
4
3
  import path from "node:path";
4
+ import { stateRoot } from "./state-dir.js";
5
5
  const TOKEN_ENV = "HARNESS_DISPATCH_HTTP_TOKEN";
6
6
  export function authDir() {
7
- return process.env.HARNESS_DISPATCH_HOME ?? path.join(os.homedir(), ".harness-dispatch");
7
+ return process.env.HARNESS_DISPATCH_HOME ?? stateRoot();
8
8
  }
9
9
  export function tokenPath() {
10
10
  return path.join(authDir(), "http-token");
package/dist/auth.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
package/dist/billing.d.ts CHANGED
@@ -1,7 +1,38 @@
1
- import type { RouteBilling, ServiceConfig } from "./types.js";
2
- export declare function buildRouteBilling(svc: ServiceConfig, opts?: {
3
- now?: Date;
4
- }): RouteBilling;
1
+ import type { BillingKind, RouteBilling, ServiceConfig } from "./types.js";
2
+ /**
3
+ * Whether a route can incur a REAL charge with no further action from the
4
+ * user, by default — i.e. whether harness-dispatch should block it until
5
+ * explicitly allowed.
6
+ *
7
+ * "included_X_then_optional_Y" kinds (Codex flexible credits, Claude usage
8
+ * credits, Cursor on-demand) are NOT blocked by default: researched across
9
+ * Anthropic, OpenAI, and Cursor (2026-07), all three hard-stop at the
10
+ * included cap by default — continuing past it requires the user to have
11
+ * ALREADY completed a separate, deliberate opt-in on the PROVIDER's own
12
+ * side (enabling usage credits/flexible pricing/on-demand billing, usually
13
+ * with its own payment method and spend limit). harness-dispatch blocking
14
+ * these by default would just be re-gating something the provider already
15
+ * gates, and asking every user to prove a negative ("I haven't opted into
16
+ * my provider's overage") for a state that's off by default anyway.
17
+ *
18
+ * A user who HAS enabled provider-side overage can still restore the block
19
+ * by setting `paid_usage_possible: true` explicitly in that route's config
20
+ * — the override in buildRouteBilling() takes precedence over this
21
+ * function. Only kinds with no such provider-side backstop — metered_api
22
+ * (raw API keys bill from the first token, no included pool at all) and
23
+ * unknown (no data to reason about) — are blocked by default here.
24
+ */
25
+ export declare function inferredPaidUsagePossible(kind: BillingKind): boolean;
26
+ /**
27
+ * Classification is date-independent by construction.
28
+ *
29
+ * This took an `opts.now` that was threaded into inferredKind/defaultNotes
30
+ * and used by neither — a leftover from when the Agent SDK credit split was
31
+ * expected to change classification on 2026-06-15. Anthropic paused that on
32
+ * launch day, the date logic was removed, and the parameter stayed behind
33
+ * implying a time dependency that no longer exists.
34
+ */
35
+ export declare function buildRouteBilling(svc: ServiceConfig): RouteBilling;
5
36
  export declare function billingIsUnknown(billing: RouteBilling): boolean;
6
37
  export declare function billingIsBlocked(billing: RouteBilling): boolean;
7
38
  //# sourceMappingURL=billing.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"billing.d.ts","sourceRoot":"","sources":["../src/billing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAMV,YAAY,EACZ,aAAa,EACd,MAAM,YAAY,CAAC;AAmLpB,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,aAAa,EAClB,IAAI,GAAE;IAAE,GAAG,CAAC,EAAE,IAAI,CAAA;CAAO,GACxB,YAAY,CAqBd;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAE/D;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAI/D"}
1
+ {"version":3,"file":"billing.d.ts","sourceRoot":"","sources":["../src/billing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAGV,WAAW,EAGX,YAAY,EACZ,aAAa,EACd,MAAM,YAAY,CAAC;AA0IpB;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAapE;AAuBD;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,aAAa,GAAG,YAAY,CAoBlE;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAE/D;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAI/D"}
package/dist/billing.js CHANGED
@@ -1,17 +1,40 @@
1
- function isLoopback(baseUrl) {
1
+ /**
2
+ * Parse a base_url into (hostname, port), or undefined if it isn't a URL.
3
+ *
4
+ * Both predicates below used `String.includes`, which is not a host check:
5
+ * `https://evil.example.com/proxy?upstream=localhost:11434/v1` contains
6
+ * "localhost:11434" and so classified as free local compute — provider
7
+ * "local", kind "local_compute", paidUsagePossible false. That also exempted
8
+ * it from the caller-supplied `local_only` and `approval_required` policies,
9
+ * because route-policy.ts's isLocalRoute ORs those same four fields.
10
+ *
11
+ * Requires a hostile or mistaken config entry, so it is not remotely
12
+ * triggerable — but "is this host local" is exactly the question a substring
13
+ * cannot answer. config.ts:inferEndpointProvider already did this correctly;
14
+ * this is the same approach applied to the two predicates that did not.
15
+ */
16
+ function hostOf(baseUrl) {
2
17
  if (!baseUrl)
3
- return false;
4
- return (baseUrl.includes("localhost") ||
5
- baseUrl.includes("127.0.0.1") ||
6
- baseUrl.includes("::1"));
18
+ return undefined;
19
+ try {
20
+ const url = new URL(baseUrl);
21
+ return { host: url.hostname.toLowerCase(), port: url.port };
22
+ }
23
+ catch {
24
+ return undefined;
25
+ }
26
+ }
27
+ const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
28
+ function isLoopback(baseUrl) {
29
+ const parsed = hostOf(baseUrl);
30
+ return parsed !== undefined && LOOPBACK_HOSTS.has(parsed.host);
7
31
  }
32
+ /** Ollama (11434) and LM Studio (1234) on loopback — the two runtimes we can name. */
8
33
  function isKnownLocalRuntime(baseUrl) {
9
- if (!baseUrl)
34
+ const parsed = hostOf(baseUrl);
35
+ if (parsed === undefined || !LOOPBACK_HOSTS.has(parsed.host))
10
36
  return false;
11
- return (baseUrl.includes("localhost:11434") ||
12
- baseUrl.includes("127.0.0.1:11434") ||
13
- baseUrl.includes("localhost:1234") ||
14
- baseUrl.includes("127.0.0.1:1234"));
37
+ return parsed.port === "11434" || parsed.port === "1234";
15
38
  }
16
39
  // NOTE: no harness-name special cases here. Built-in harnesses declare
17
40
  // provider/surface/auth_source/billing_kind in the shipped config.default.yaml,
@@ -58,7 +81,7 @@ function authSourceFromService(svc, surface) {
58
81
  }
59
82
  return "unknown";
60
83
  }
61
- function inferredKind(svc, surface, authSource, now) {
84
+ function inferredKind(svc, surface, authSource) {
62
85
  if (svc.billingKind)
63
86
  return svc.billingKind;
64
87
  if (authSource === "api_key")
@@ -136,7 +159,7 @@ function inferredConfidence(svc, surface, kind) {
136
159
  * (raw API keys bill from the first token, no included pool at all) and
137
160
  * unknown (no data to reason about) — are blocked by default here.
138
161
  */
139
- function inferredPaidUsagePossible(kind) {
162
+ export function inferredPaidUsagePossible(kind) {
140
163
  switch (kind) {
141
164
  case "local_compute":
142
165
  case "included_plan_usage":
@@ -150,7 +173,7 @@ function inferredPaidUsagePossible(kind) {
150
173
  return true;
151
174
  }
152
175
  }
153
- function defaultNotes(svc, surface, kind, now) {
176
+ function defaultNotes(svc, surface, kind) {
154
177
  if (svc.billingNotes)
155
178
  return svc.billingNotes;
156
179
  if (surface === "claude_agent_sdk" || surface === "claude_code") {
@@ -165,12 +188,20 @@ function defaultNotes(svc, surface, kind, now) {
165
188
  }
166
189
  return undefined;
167
190
  }
168
- export function buildRouteBilling(svc, opts = {}) {
169
- const now = opts.now ?? new Date();
191
+ /**
192
+ * Classification is date-independent by construction.
193
+ *
194
+ * This took an `opts.now` that was threaded into inferredKind/defaultNotes
195
+ * and used by neither — a leftover from when the Agent SDK credit split was
196
+ * expected to change classification on 2026-06-15. Anthropic paused that on
197
+ * launch day, the date logic was removed, and the parameter stayed behind
198
+ * implying a time dependency that no longer exists.
199
+ */
200
+ export function buildRouteBilling(svc) {
170
201
  const provider = providerFromService(svc);
171
202
  const surface = surfaceFromService(svc);
172
203
  const authSource = authSourceFromService(svc, surface);
173
- const kind = inferredKind(svc, surface, authSource, now);
204
+ const kind = inferredKind(svc, surface, authSource);
174
205
  const confidence = inferredConfidence(svc, surface, kind);
175
206
  const paidUsagePossible = svc.paidUsagePossible ?? inferredPaidUsagePossible(kind);
176
207
  const billing = {
@@ -183,7 +214,7 @@ export function buildRouteBilling(svc, opts = {}) {
183
214
  paidUsageRequiresOptIn: paidUsagePossible ? true : false,
184
215
  confidence,
185
216
  };
186
- const notes = defaultNotes(svc, surface, kind, now);
217
+ const notes = defaultNotes(svc, surface, kind);
187
218
  if (notes !== undefined)
188
219
  billing.notes = notes;
189
220
  return billing;
@@ -1 +1 @@
1
- {"version":3,"file":"billing.js","sourceRoot":"","sources":["../src/billing.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"billing.js","sourceRoot":"","sources":["../src/billing.ts"],"names":[],"mappings":"AAUA;;;;;;;;;;;;;;GAcG;AACH,SAAS,MAAM,CAAC,OAA2B;IACzC,IAAI,CAAC,OAAO;QAAE,OAAO,SAAS,CAAC;IAC/B,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC;QAC7B,OAAO,EAAE,IAAI,EAAE,GAAG,CAAC,QAAQ,CAAC,WAAW,EAAE,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,CAAC;IAC9D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED,MAAM,cAAc,GAAG,IAAI,GAAG,CAAC,CAAC,WAAW,EAAE,WAAW,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC;AAE3E,SAAS,UAAU,CAAC,OAA2B;IAC7C,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC;IAC/B,OAAO,MAAM,KAAK,SAAS,IAAI,cAAc,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;AACjE,CAAC;AAED,sFAAsF;AACtF,SAAS,mBAAmB,CAAC,OAA2B;IACtD,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC;IAC/B,IAAI,MAAM,KAAK,SAAS,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IAC3E,OAAO,MAAM,CAAC,IAAI,KAAK,OAAO,IAAI,MAAM,CAAC,IAAI,KAAK,MAAM,CAAC;AAC3D,CAAC;AAED,uEAAuE;AACvE,gFAAgF;AAChF,8EAA8E;AAC9E,0EAA0E;AAC1E,6EAA6E;AAC7E,4CAA4C;AAE5C,SAAS,mBAAmB,CAAC,GAAkB;IAC7C,IAAI,GAAG,CAAC,QAAQ;QAAE,OAAO,GAAG,CAAC,QAAQ,CAAC;IACtC,IAAI,GAAG,CAAC,IAAI,KAAK,mBAAmB,IAAI,mBAAmB,CAAC,GAAG,CAAC,OAAO,CAAC;QAAE,OAAO,OAAO,CAAC;IACzF,IAAI,GAAG,CAAC,OAAO,EAAE,QAAQ,CAAC,gBAAgB,CAAC;QAAE,OAAO,QAAQ,CAAC;IAC7D,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,SAAS,kBAAkB,CAAC,GAAkB;IAC5C,IAAI,GAAG,CAAC,OAAO;QAAE,OAAO,GAAG,CAAC,OAAO,CAAC;IACpC,IAAI,GAAG,CAAC,IAAI,KAAK,mBAAmB,EAAE,CAAC;QACrC,IAAI,GAAG,CAAC,OAAO,EAAE,QAAQ,CAAC,gBAAgB,CAAC;YAAE,OAAO,YAAY,CAAC;QACjE,IAAI,mBAAmB,CAAC,GAAG,CAAC,OAAO,CAAC;YAAE,OAAO,gBAAgB,CAAC;QAC9D,OAAO,mBAAmB,CAAC;IAC7B,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,SAAS,qBAAqB,CAAC,GAAkB,EAAE,OAAuB;IACxE,IAAI,GAAG,CAAC,UAAU;QAAE,OAAO,GAAG,CAAC,UAAU,CAAC;IAC1C,IAAI,GAAG,CAAC,MAAM;QAAE,OAAO,SAAS,CAAC;IACjC,IAAI,OAAO,KAAK,gBAAgB;QAAE,OAAO,eAAe,CAAC;IACzD,IAAI,GAAG,CAAC,IAAI,KAAK,mBAAmB;QAAE,OAAO,qBAAqB,CAAC;IACnE,IAAI,OAAO,KAAK,kBAAkB;QAAE,OAAO,eAAe,CAAC;IAC3D,IACE,OAAO,KAAK,WAAW;QACvB,OAAO,KAAK,kBAAkB;QAC9B,OAAO,KAAK,iBAAiB,EAC7B,CAAC;QACD,OAAO,eAAe,CAAC;IACzB,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,SAAS,YAAY,CACnB,GAAkB,EAClB,OAAuB,EACvB,UAAsB;IAEtB,IAAI,GAAG,CAAC,WAAW;QAAE,OAAO,GAAG,CAAC,WAAW,CAAC;IAC5C,IAAI,UAAU,KAAK,SAAS;QAAE,OAAO,aAAa,CAAC;IACnD,QAAQ,OAAO,EAAE,CAAC;QAChB,KAAK,kBAAkB,CAAC;QACxB,KAAK,aAAa;YAChB,qEAAqE;YACrE,mEAAmE;YACnE,mEAAmE;YACnE,kEAAkE;YAClE,+DAA+D;YAC/D,2CAA2C;YAC3C,0DAA0D;YAC1D,OAAO,qBAAqB,CAAC;QAC/B,KAAK,WAAW,CAAC;QACjB,KAAK,WAAW;YACd,OAAO,qCAAqC,CAAC;QAC/C,KAAK,kBAAkB;YACrB,OAAO,+BAA+B,CAAC;QACzC,KAAK,iBAAiB;YACpB,OAAO,YAAY,CAAC;QACtB,KAAK,gBAAgB;YACnB,OAAO,eAAe,CAAC;QACzB,KAAK,YAAY,CAAC;QAClB,KAAK,eAAe,CAAC;QACrB,KAAK,YAAY,CAAC;QAClB,KAAK,WAAW;YACd,OAAO,aAAa,CAAC;QACvB,KAAK,mBAAmB;YACtB,OAAO,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,aAAa,CAAC;QAC7D,KAAK,QAAQ;YACX,OAAO,SAAS,CAAC;IACrB,CAAC;AACH,CAAC;AAED,SAAS,kBAAkB,CACzB,GAAkB,EAClB,OAAuB,EACvB,IAAiB;IAEjB,IAAI,GAAG,CAAC,iBAAiB;QAAE,OAAO,GAAG,CAAC,iBAAiB,CAAC;IACxD,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IACzC,wEAAwE;IACxE,oEAAoE;IACpE,sEAAsE;IACtE,iEAAiE;IACjE,IAAI,GAAG,CAAC,WAAW;QAAE,OAAO,YAAY,CAAC;IACzC,IAAI,OAAO,KAAK,mBAAmB,IAAI,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC;QAAE,OAAO,SAAS,CAAC;IACjF,IAAI,OAAO,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC;IAC3C,IAAI,OAAO,KAAK,kBAAkB,IAAI,GAAG,CAAC,UAAU,KAAK,SAAS;QAAE,OAAO,UAAU,CAAC;IACtF,OAAO,YAAY,CAAC;AACtB,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,UAAU,yBAAyB,CAAC,IAAiB;IACzD,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,eAAe,CAAC;QACrB,KAAK,qBAAqB,CAAC;QAC3B,KAAK,YAAY,CAAC;QAClB,KAAK,qCAAqC,CAAC;QAC3C,KAAK,uCAAuC,CAAC;QAC7C,KAAK,+BAA+B;YAClC,OAAO,KAAK,CAAC;QACf,KAAK,aAAa,CAAC;QACnB,KAAK,SAAS;YACZ,OAAO,IAAI,CAAC;IAChB,CAAC;AACH,CAAC;AAED,SAAS,YAAY,CACnB,GAAkB,EAClB,OAAuB,EACvB,IAAiB;IAEjB,IAAI,GAAG,CAAC,YAAY;QAAE,OAAO,GAAG,CAAC,YAAY,CAAC;IAC9C,IAAI,OAAO,KAAK,kBAAkB,IAAI,OAAO,KAAK,aAAa,EAAE,CAAC;QAChE,wEAAwE;QACxE,gEAAgE;QAChE,OAAO,CACL,uEAAuE;YACvE,uEAAuE;YACvE,mCAAmC,CACpC,CAAC;IACJ,CAAC;IACD,IAAI,OAAO,KAAK,mBAAmB,IAAI,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACrF,OAAO,oFAAoF,CAAC;IAC9F,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,iBAAiB,CAAC,GAAkB;IAClD,MAAM,QAAQ,GAAG,mBAAmB,CAAC,GAAG,CAAC,CAAC;IAC1C,MAAM,OAAO,GAAG,kBAAkB,CAAC,GAAG,CAAC,CAAC;IACxC,MAAM,UAAU,GAAG,qBAAqB,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IACvD,MAAM,IAAI,GAAG,YAAY,CAAC,GAAG,EAAE,OAAO,EAAE,UAAU,CAAC,CAAC;IACpD,MAAM,UAAU,GAAG,kBAAkB,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;IAC1D,MAAM,iBAAiB,GAAG,GAAG,CAAC,iBAAiB,IAAI,yBAAyB,CAAC,IAAI,CAAC,CAAC;IACnF,MAAM,OAAO,GAAiB;QAC5B,QAAQ;QACR,OAAO;QACP,UAAU;QACV,IAAI;QACJ,iBAAiB;QACjB,cAAc,EAAE,GAAG,CAAC,cAAc,IAAI,KAAK;QAC3C,sBAAsB,EAAE,iBAAiB,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK;QACxD,UAAU;KACX,CAAC;IACF,MAAM,KAAK,GAAG,YAAY,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;IAC/C,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,CAAC,KAAK,GAAG,KAAK,CAAC;IAC/C,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,OAAqB;IACpD,OAAO,OAAO,CAAC,IAAI,KAAK,SAAS,IAAI,OAAO,CAAC,UAAU,KAAK,SAAS,CAAC;AACxE,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,OAAqB;IACpD,IAAI,gBAAgB,CAAC,OAAO,CAAC;QAAE,OAAO,CAAC,OAAO,CAAC,cAAc,CAAC;IAC9D,IAAI,OAAO,CAAC,iBAAiB;QAAE,OAAO,CAAC,OAAO,CAAC,cAAc,CAAC;IAC9D,OAAO,KAAK,CAAC;AACf,CAAC"}
package/dist/bin.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"bin.d.ts","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":";AACA;;GAEG;AAkfH,wBAAsB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAyF1D"}
1
+ {"version":3,"file":"bin.d.ts","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":";AACA;;GAEG;AA6sBH,wBAAsB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAiH1D"}
package/dist/bin.js CHANGED
@@ -2,13 +2,13 @@
2
2
  /**
3
3
  * harness-dispatch CLI entrypoint.
4
4
  */
5
- import { existsSync } from "node:fs";
5
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
6
6
  import { promises as fs } from "node:fs";
7
7
  import path from "node:path";
8
8
  import { parseArgs } from "node:util";
9
9
  import yaml from "js-yaml";
10
10
  import { ensureHttpToken, maskToken, readHttpToken, rotateHttpToken, tokenPath } from "./auth.js";
11
- import { loadConfig } from "./config.js";
11
+ import { AUTO_DETECT_COMMANDS, loadConfig } from "./config.js";
12
12
  import { LeaderboardCache } from "./leaderboard.js";
13
13
  import { buildDispatchers } from "./mcp/dispatcher-factory.js";
14
14
  import { startMcpServer } from "./mcp/server.js";
@@ -19,11 +19,14 @@ import { buildStatus, buildUsage, renderStatusText, renderUsageText } from "./st
19
19
  import { startHttpServer } from "./http/server.js";
20
20
  import { billingIsBlocked, buildRouteBilling } from "./billing.js";
21
21
  import { effectiveSafetyProfile } from "./safety.js";
22
+ import { stateRoot } from "./state-dir.js";
22
23
  async function buildRuntime(configPath) {
23
24
  const config = await loadConfig(configPath);
24
25
  const dispatchers = await buildDispatchers(config);
25
26
  const quota = new QuotaCache(dispatchers);
26
- const leaderboard = new LeaderboardCache();
27
+ const leaderboard = new LeaderboardCache(undefined, {
28
+ enabled: config.leaderboard?.enabled === true,
29
+ });
27
30
  const router = new Router(config, quota, dispatchers, leaderboard);
28
31
  return { config, dispatchers, quota, leaderboard, router };
29
32
  }
@@ -52,6 +55,7 @@ function printUsage() {
52
55
  " --json Print JSON where supported.",
53
56
  " --print configure: print generated config YAML without writing it.",
54
57
  " --yes configure: write config.yaml instead of only previewing it.",
58
+ " --force configure: overwrite an existing config file.",
55
59
  " --allow-paid Allow doctor --live to probe paid or unknown-paid routes.",
56
60
  " -h, --help Show help.",
57
61
  "",
@@ -86,6 +90,7 @@ function commonEntryFields(svc) {
86
90
  escalate_model: svc.escalateModel,
87
91
  escalate_on: svc.escalateOn.length > 0 ? svc.escalateOn : undefined,
88
92
  capabilities: Object.keys(svc.capabilities).length > 0 ? svc.capabilities : undefined,
93
+ timeout_ms: svc.timeoutMs,
89
94
  max_output_tokens: svc.maxOutputTokens,
90
95
  max_input_tokens: svc.maxInputTokens,
91
96
  allow_paid_usage: svc.allowPaidUsage ? true : undefined,
@@ -99,33 +104,131 @@ function commonEntryFields(svc) {
99
104
  model_hint: svc.modelHint,
100
105
  };
101
106
  }
102
- function cliEntryToYaml(svc) {
107
+ /**
108
+ * Render a route's api_key WITHOUT materialising the secret.
109
+ *
110
+ * `svc.apiKey` is the RESOLVED value — config.ts interpolates `${VAR}` at
111
+ * load time, so by here the reference is gone. Emitting it verbatim wrote
112
+ * live credentials into config.yaml and echoed them to stdout on
113
+ * `configure --print`, which is documented as the safe preview and is exactly
114
+ * what someone pastes into a bug report. It also broke the project's own
115
+ * stated invariant (plugin/commands/setup.md: "API keys MUST be written as
116
+ * ${ENV_VAR} references — never literal").
117
+ *
118
+ * `config.envRefs` maps the resolved value back to the reference that
119
+ * produced it, so a key that came from `${GROQ_API_KEY}` round-trips exactly.
120
+ *
121
+ * A key written as a LITERAL in the source file has no reference to restore.
122
+ * That case splits by destination: `--yes` writes to disk, where the literal
123
+ * already lives and dropping it would break a working config, so it is
124
+ * preserved; `--print` goes to a terminal and a bug report, so it is redacted
125
+ * to the placeholder below.
126
+ */
127
+ function apiKeyForYaml(svc, config, opts) {
128
+ if (svc.apiKey === undefined || svc.apiKey === "")
129
+ return undefined;
130
+ const ref = config.envRefs?.get(svc.apiKey);
131
+ if (ref !== undefined)
132
+ return ref;
133
+ if (!opts.redactLiterals)
134
+ return svc.apiKey;
135
+ // protocol.apiKeyEnvVar is the var the CHILD CLI reads, which is only a
136
+ // suggestion for what to name the config reference — they need not match,
137
+ // and endpoint routes have no protocol at all. Hence the generic fallback.
138
+ const envVar = svc.protocol?.apiKeyEnvVar ?? "YOUR_API_KEY_ENV_VAR";
139
+ return `\${${envVar}}`;
140
+ }
141
+ function cliEntryToYaml(svc, config, opts) {
142
+ // `harness: generic` has NO shipped preset behind it, so anything omitted
143
+ // here is not recoverable on reload — it is gone. Dropping `protocol:` was
144
+ // fatal: config.ts refuses a generic entry without one ("requires a
145
+ // protocol block — entry ignored"), so a round-trip deleted every
146
+ // user-added harness, and `configure --yes --force` wrote that over their
147
+ // file. This is the documented README#adding-a-harness path.
148
+ //
149
+ // Built-in harnesses keep the lean output: their preset supplies protocol
150
+ // and billing, and emitting a copy would freeze a snapshot that stops
151
+ // tracking future default changes — the same reasoning commonEntryFields
152
+ // gives for omitting billing fields generally.
153
+ const isGeneric = svc.harness === "generic";
103
154
  return {
104
155
  name: svc.name,
105
156
  harness: svc.harness,
106
157
  command: svc.command,
107
- api_key: svc.apiKey,
158
+ api_key: apiKeyForYaml(svc, config, opts),
108
159
  ...commonEntryFields(svc),
160
+ ...(isGeneric
161
+ ? {
162
+ provider: svc.provider,
163
+ surface: svc.surface,
164
+ auth_source: svc.authSource,
165
+ billing_kind: svc.billingKind,
166
+ paid_usage_possible: svc.paidUsagePossible,
167
+ billing_notes: svc.billingNotes,
168
+ protocol: svc.protocol,
169
+ }
170
+ : {}),
109
171
  };
110
172
  }
111
- function endpointEntryToYaml(svc) {
173
+ function endpointEntryToYaml(svc, config, opts) {
112
174
  return {
113
175
  name: svc.name,
114
176
  base_url: svc.baseUrl,
115
- api_key: svc.apiKey,
177
+ api_key: apiKeyForYaml(svc, config, opts),
116
178
  ...commonEntryFields(svc),
179
+ // Endpoints have no shipped preset behind them, so an omitted billing
180
+ // field is not recomputed on reload — it is lost. Verified: an endpoint
181
+ // declaring `billing_kind: local_compute` and `paid_usage_possible: false`
182
+ // came back undefined, flipping it to paid=possible and getting it skipped
183
+ // by billing policy.
184
+ //
185
+ // This is the same reasoning already applied to `harness: generic` CLI
186
+ // routes, and it should have been applied here at the same time. Built-in
187
+ // harnesses still keep the lean output because their preset supplies these.
188
+ provider: svc.provider,
189
+ surface: svc.surface,
190
+ auth_source: svc.authSource,
191
+ billing_kind: svc.billingKind,
192
+ paid_usage_possible: svc.paidUsagePossible,
193
+ billing_notes: svc.billingNotes,
117
194
  };
118
195
  }
119
- function configToYaml(config) {
196
+ /**
197
+ * Top-level settings a user set and that have no defaults to track.
198
+ *
199
+ * These were silently dropped on every round-trip. Unlike the billing fields
200
+ * (see commonEntryFields — omitted deliberately so they keep following harness
201
+ * defaults), nothing recomputes these: a dropped `max_concurrent_runs` is
202
+ * simply gone, and `configure --yes --force` wrote the result over the user's
203
+ * file.
204
+ */
205
+ function topLevelToYaml(config) {
206
+ const out = {};
207
+ if (config.disabled && config.disabled.length > 0)
208
+ out.disabled = [...config.disabled];
209
+ if (config.maxConcurrentRuns !== undefined)
210
+ out.max_concurrent_runs = config.maxConcurrentRuns;
211
+ if (config.retention?.jobsDays !== undefined) {
212
+ out.retention = { jobs_days: config.retention.jobsDays };
213
+ }
214
+ if (config.telemetry?.enabled !== undefined) {
215
+ out.telemetry = { enabled: config.telemetry.enabled };
216
+ }
217
+ if (config.leaderboard?.enabled !== undefined) {
218
+ out.leaderboard = { enabled: config.leaderboard.enabled };
219
+ }
220
+ return out;
221
+ }
222
+ function configToYaml(config, opts) {
120
223
  const clis = [];
121
224
  const endpoints = [];
122
225
  for (const svc of Object.values(config.services)) {
123
226
  if (svc.type === "cli")
124
- clis.push(cliEntryToYaml(svc));
227
+ clis.push(cliEntryToYaml(svc, config, opts));
125
228
  else
126
- endpoints.push(endpointEntryToYaml(svc));
229
+ endpoints.push(endpointEntryToYaml(svc, config, opts));
127
230
  }
128
- const doc = {};
231
+ const doc = { ...topLevelToYaml(config) };
129
232
  if (clis.length > 0)
130
233
  doc.clis = clis;
131
234
  if (endpoints.length > 0)
@@ -133,13 +236,27 @@ function configToYaml(config) {
133
236
  return yaml.dump(doc, { noRefs: true, lineWidth: 100 });
134
237
  }
135
238
  async function cmdConfigure(configPath, explicitConfigPath, opts) {
136
- const config = await loadConfig(configPath);
137
- const yamlText = configToYaml(config);
239
+ // configure's --config names where it will WRITE, so a path that does not
240
+ // exist yet is the normal first-run case, not a typo.
241
+ const config = await loadConfig(configPath, { allowMissing: true });
138
242
  const routeCount = Object.keys(config.services).length;
139
243
  if (opts.print) {
140
- process.stdout.write(yamlText);
244
+ // Preview goes to a terminal and, routinely, into a bug report — a
245
+ // literal key with no ${VAR} to restore is redacted rather than echoed.
246
+ const preview = configToYaml(config, { redactLiterals: true });
247
+ process.stdout.write(preview);
248
+ const redacted = Object.values(config.services).some((svc) => svc.apiKey !== undefined &&
249
+ svc.apiKey !== "" &&
250
+ config.envRefs?.get(svc.apiKey) === undefined);
251
+ if (redacted) {
252
+ process.stderr.write("note: one or more api_key values are literals in the source config and were " +
253
+ "replaced with ${ENV_VAR} placeholders in this preview. Move them to " +
254
+ "environment variables — this output is not a drop-in replacement for that file " +
255
+ "until you do.\n");
256
+ }
141
257
  return 0;
142
258
  }
259
+ const yamlText = configToYaml(config, { redactLiterals: false });
143
260
  process.stdout.write(`Detected ${routeCount} harness route${routeCount === 1 ? "" : "s"}.\n`);
144
261
  for (const [name, svc] of Object.entries(config.services)) {
145
262
  process.stdout.write(`- ${name}: harness=${svc.harness ?? name} billing=${buildRouteBilling(svc).kind} safety=${effectiveSafetyProfile(svc)} model=${svc.model ?? svc.leaderboardModel ?? "unknown"}\n`);
@@ -165,8 +282,17 @@ async function cmdConfigure(configPath, explicitConfigPath, opts) {
165
282
  process.stdout.write("After writing config, connect agents by adding the harness-dispatch MCP snippet to the agent you use.\n");
166
283
  return 0;
167
284
  }
168
- if (existsSync(target) && explicitConfigPath === undefined) {
169
- process.stderr.write("configure: config.yaml already exists. Pass --config <path> or --print to avoid overwriting it.\n");
285
+ // Guard ANY existing file, however the path was supplied.
286
+ //
287
+ // This previously read `existsSync(target) && explicitConfigPath === undefined`,
288
+ // so passing --config skipped the protection entirely — and the message told
289
+ // you to pass --config, which is exactly what disabled it. Overwriting a
290
+ // hand-written config is not recoverable, so it now takes an explicit
291
+ // --force rather than an accident of which flag you happened to use.
292
+ if (existsSync(target) && !opts.force) {
293
+ process.stderr.write(`configure: ${target} already exists and would be overwritten.\n` +
294
+ "Use --print to inspect the generated YAML, --config <other-path> to write\n" +
295
+ "elsewhere, or --force to overwrite it deliberately.\n");
170
296
  return 1;
171
297
  }
172
298
  await fs.writeFile(target, yamlText, "utf-8");
@@ -220,6 +346,30 @@ async function cmdUsage(configPath, opts) {
220
346
  process.stdout.write(opts.json ? `${JSON.stringify(usage, null, 2)}\n` : `${renderUsageText(usage)}\n`);
221
347
  return 0;
222
348
  }
349
+ /**
350
+ * Can we actually persist state? Breaker cooldowns, quota counters and job
351
+ * records all live here, and every write path deliberately swallows its own
352
+ * failures so a dispatch is never lost to a bookkeeping problem. The cost of
353
+ * that choice is silence, which is what this check buys back.
354
+ */
355
+ function stateDirWritable() {
356
+ const dir = stateRoot();
357
+ const probe = path.join(dir, `.write-probe-${process.pid}`);
358
+ try {
359
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
360
+ writeFileSync(probe, "", { mode: 0o600 });
361
+ rmSync(probe, { force: true });
362
+ return { ok: true, detail: `writable: ${dir}` };
363
+ }
364
+ catch (err) {
365
+ return {
366
+ ok: false,
367
+ detail: `NOT writable: ${dir} (${err instanceof Error ? err.message : String(err)}). ` +
368
+ `Breaker cooldowns and usage counters will not persist, and jobs may be ` +
369
+ `reported as orphaned after they have actually succeeded.`,
370
+ };
371
+ }
372
+ }
223
373
  async function cmdDoctor(configPath, opts) {
224
374
  const runtime = await buildRuntime(configPath);
225
375
  if (opts.allowPaid) {
@@ -228,12 +378,24 @@ async function cmdDoctor(configPath, opts) {
228
378
  }
229
379
  }
230
380
  const status = await buildStatus(runtime.config, runtime.dispatchers, runtime.quota, runtime.router, runtime.leaderboard);
231
- const nodeMajor = Number(process.versions.node.split(".")[0] ?? "0");
381
+ // Must agree with package.json engines (>=22.22.2) and the README. It said
382
+ // >= 24 while both of those said 22, so a user following the README's own
383
+ // install block hit `fail node` on the second command — on a runtime where
384
+ // dispatch works correctly. Three sources, two answers, and the one the
385
+ // user sees first was the wrong one.
386
+ const [nodeMajor, nodeMinor, nodePatch] = process.versions.node
387
+ .split(".")
388
+ .map((n) => Number(n) || 0);
389
+ const nodeOk = (nodeMajor ?? 0) > 22 ||
390
+ ((nodeMajor ?? 0) === 22 &&
391
+ ((nodeMinor ?? 0) > 22 || ((nodeMinor ?? 0) === 22 && (nodePatch ?? 0) >= 2)));
232
392
  const checks = [
233
393
  {
234
394
  name: "node",
235
- ok: nodeMajor >= 24,
236
- detail: `Node ${process.versions.node}`,
395
+ ok: nodeOk,
396
+ detail: nodeOk
397
+ ? `Node ${process.versions.node}`
398
+ : `Node ${process.versions.node} — harness-dispatch needs >=22.22.2`,
237
399
  },
238
400
  {
239
401
  name: "config",
@@ -250,7 +412,23 @@ async function cmdDoctor(configPath, opts) {
250
412
  {
251
413
  name: "routes",
252
414
  ok: status.ready.length > 0,
253
- detail: `${status.ready.length} ready route(s)`,
415
+ // When nothing is ready, say what was looked for. "0 ready route(s)" on
416
+ // its own leaves a new user with no idea whether the tool is broken or
417
+ // simply has nothing to route to, and no hint what to install.
418
+ detail: status.ready.length > 0
419
+ ? `${status.ready.length} ready route(s)`
420
+ : `0 ready route(s). Looked for these harness CLIs on PATH: ` +
421
+ `${Object.values(AUTO_DETECT_COMMANDS).join(", ")}. ` +
422
+ `Install one, or add a route to config.yaml (endpoints: need no CLI).`,
423
+ },
424
+ {
425
+ // Nothing checked this, so an unwritable state directory surfaced only
426
+ // as jobs mysteriously reported "the dispatch server exited before the
427
+ // run finished" — a false cause, 90s after the work had actually
428
+ // succeeded.
429
+ name: "state-dir",
430
+ ok: stateDirWritable().ok,
431
+ detail: stateDirWritable().detail,
254
432
  },
255
433
  {
256
434
  name: "http-auth",
@@ -385,10 +563,26 @@ function parsePositiveInt(value, fallback) {
385
563
  const parsed = Number(value);
386
564
  return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
387
565
  }
566
+ /**
567
+ * Thrown for a flag value the user typed wrong. main() turns it into a plain
568
+ * one-line message and exit 1 — never a stack trace, which tells a CLI user
569
+ * nothing they can act on.
570
+ */
571
+ class UsageError extends Error {
572
+ }
388
573
  function serveOpts(values) {
389
574
  const out = {};
390
- if (values.port !== undefined)
391
- out.port = parsePositiveInt(values.port, 0);
575
+ if (values.port !== undefined) {
576
+ // `--port abc` used to fall back to 0, which binds a RANDOM free port and
577
+ // prints it as though it were what was asked for. A typo'd port silently
578
+ // serving somewhere else is worse than refusing to start.
579
+ const port = parsePositiveInt(values.port, 0);
580
+ if (port === 0 || !Number.isInteger(port) || port > 65535) {
581
+ throw new UsageError(`--port must be an integer between 1 and 65535 (got ${JSON.stringify(values.port)}). ` +
582
+ `Omit --port to bind a random free one.`);
583
+ }
584
+ out.port = port;
585
+ }
392
586
  if (typeof values.host === "string")
393
587
  out.host = values.host;
394
588
  return out;
@@ -408,17 +602,37 @@ export async function main(argv) {
408
602
  host: { type: "string" },
409
603
  print: { type: "boolean" },
410
604
  yes: { type: "boolean" },
605
+ force: { type: "boolean" },
411
606
  http: { type: "string" },
412
607
  },
413
608
  allowPositionals: true,
414
609
  strict: false,
415
610
  });
611
+ // parseArgs runs with strict:false so positionals and subcommand shapes stay
612
+ // flexible — the cost is that an unknown flag is silently accepted. `status
613
+ // --jsonn` printed human text and exited 0, so a cron job piping it to jq
614
+ // got garbage AND a success code. PRODUCT.md names automation as a user, and
615
+ // a wrong exit code is the one thing automation cannot recover from.
616
+ const knownFlags = new Set([
617
+ "help", "config", "json", "live", "allow-paid", "watch", "interval",
618
+ "port", "host", "print", "yes", "force", "http",
619
+ ]);
620
+ const unknownFlags = Object.keys(values).filter((k) => !knownFlags.has(k));
621
+ if (unknownFlags.length > 0) {
622
+ throw new UsageError(`unknown option${unknownFlags.length > 1 ? "s" : ""}: ` +
623
+ `${unknownFlags.map((f) => `--${f}`).join(", ")}. Run --help for the list.`);
624
+ }
416
625
  if (values.help) {
417
626
  printUsage();
418
627
  return 0;
419
628
  }
420
629
  await initObservability();
421
630
  const [command, ...rest] = positionals;
631
+ // `--config` with no value: parseArgs yields boolean true, which reached
632
+ // path.join and threw ERR_INVALID_ARG_TYPE as a raw Node stack trace.
633
+ if (values.config !== undefined && typeof values.config !== "string") {
634
+ throw new UsageError("--config needs a path, e.g. --config ./config.yaml");
635
+ }
422
636
  const explicitConfigPath = values.config;
423
637
  // If the caller didn't pass --config, fall back to ./config.yaml when it
424
638
  // exists, rather than silently ignoring it and running pure auto-detect.
@@ -445,6 +659,7 @@ export async function main(argv) {
445
659
  return cmdConfigure(configPath, explicitConfigPath, {
446
660
  print: Boolean(values.print),
447
661
  yes: Boolean(values.yes),
662
+ force: Boolean(values.force),
448
663
  });
449
664
  case "doctor":
450
665
  return cmdDoctor(configPath, {
@@ -481,8 +696,23 @@ export async function main(argv) {
481
696
  }
482
697
  const entrypoint = typeof process !== "undefined" && Array.isArray(process.argv) ? process.argv[1] : "";
483
698
  if (entrypoint && (entrypoint.endsWith("bin.ts") || entrypoint.endsWith("bin.js"))) {
484
- void main(process.argv.slice(2)).then((code) => {
699
+ void main(process.argv.slice(2))
700
+ .then((code) => {
485
701
  process.exit(code);
702
+ })
703
+ .catch((err) => {
704
+ // A CLI user gets one actionable line, not a stack trace. Every Error is
705
+ // flattened to its message — UsageError and config-loading failures
706
+ // (missing file, bad YAML) are things the user typed and can fix, and
707
+ // the codebase throws user-facing Errors by convention, so there is no
708
+ // reliable way to tell "bug" from "bad input" by class here. Only a
709
+ // non-Error throw (a genuine programming error) keeps its stack.
710
+ if (err instanceof UsageError || err instanceof Error) {
711
+ process.stderr.write(`harness-dispatch: ${err.message}
712
+ `);
713
+ process.exit(1);
714
+ }
715
+ throw err;
486
716
  });
487
717
  }
488
718
  //# sourceMappingURL=bin.js.map