@hue-run/sdk 0.10.0 → 0.11.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/CLI.md CHANGED
@@ -42,7 +42,7 @@ project manifest are refused because managers can update ancestor locks; Python
42
42
  The generated `hue.setup.mjs` or `hue_setup.py` always selects `captureContent: false` /
43
43
  `capture_content=False`. For a supported application, setup installs the dependency and adds the
44
44
  managed import and middleware registration to the existing entrypoint; an unreferenced helper is
45
- not a completed integration. TypeScript uses `@hue-run/sdk@0.10.0`, `@opentelemetry/api@1.9.1` and
45
+ not a completed integration. TypeScript uses `@hue-run/sdk@0.11.0`, `@opentelemetry/api@1.9.1` and
46
46
  `@opentelemetry/context-async-hooks@2.11.0`; Python setup uses its separately tested package pin.
47
47
  Content capture requires an ordinary account-managed key and a later explicit application decision.
48
48
 
@@ -113,6 +113,7 @@ hue setup --format human # explicit append-only terminal rendering
113
113
  hue setup --origin http://127.0.0.1:PORT # isolated loopback tests only
114
114
  hue login # validate keys created in Hue and store them in ./.env.hue
115
115
  hue mcp install --client claude-code # write the Hue MCP configuration for a coding agent
116
+ hue listen --subscription <id> --forward-to http://localhost:3000/slack/events # deliver events to a local bot
116
117
  ```
117
118
 
118
119
  Hosted setup accepts HTTPS origins only. HTTP is accepted solely for `localhost`, `127.0.0.1` and
@@ -176,30 +177,130 @@ warns; `--gitignore` appends the file name to the `.gitignore` next to it. Exit
176
177
 
177
178
  ## Install the MCP for your coding agent
178
179
 
179
- `hue mcp install --client <name>` writes, runs or prints the configuration for Hue's MCP server.
180
- Every shape matches the snippet Hue shows in Settings: server name `hue`, the endpoint (default
181
- `https://mcp.hue.run/mcp`; `--url https://mcp.staging.hue.run/mcp` for staging) and a reference to
182
- the `HUE_MCP_KEY` environment variable. A key value is never written.
183
-
184
- | Client | Result |
185
- | --- | --- |
186
- | `claude-code` | Merges `mcpServers.hue` into `./.mcp.json`. `--scope user` runs `claude mcp add --transport http --scope user hue URL --header 'Authorization: Bearer ${HUE_MCP_KEY}'` when `claude` is on `PATH`, otherwise prints it. |
187
- | `cursor` | Merges `mcpServers.hue` into `./.cursor/mcp.json` with `${env:HUE_MCP_KEY}`. |
188
- | `codex` | Runs `codex mcp add hue --url URL --bearer-token-env-var HUE_MCP_KEY`, or prints the `[mcp_servers.hue]` TOML block for `~/.codex/config.toml`. |
189
- | `vscode` | Merges `servers.hue` and the `hue-mcp-key` password input into `./.vscode/mcp.json`. |
190
- | `windsurf` | Prints the `serverUrl` snippet for `~/.codeium/windsurf/mcp_config.json`; nothing is written to the home directory. |
191
- | `gemini` | Runs `gemini mcp add --transport http hue URL -H 'Authorization: Bearer $HUE_MCP_KEY'`, or prints it. |
180
+ `hue mcp install --client <name>` writes, runs or prints the configuration for Hue's MCP server,
181
+ in the shapes of the [connection guide](https://docs.hue.run/agents/mcp-server): server name
182
+ `hue` and the endpoint (default `https://mcp.hue.run/mcp`; `--url https://mcp.staging.hue.run/mcp`
183
+ for staging). Hue's Settings page does not show these snippets; this command keeps its own copy.
184
+
185
+ `--auth` chooses how the client authenticates:
186
+
187
+ - `key` (the default, except for `conductor`) references the `HUE_MCP_KEY` environment variable,
188
+ which `hue login` stores in `.env.hue`. A key value is never written.
189
+ - `oauth` configures the URL only. The client opens Hue in a browser, where you sign in and
190
+ approve the connection; no key or header is involved. A new sign-in connection has **Read and
191
+ write** access to every active project in the organization you choose, within your role, so an
192
+ agent first calls `list_projects` and passes the chosen project's id as `project_id`. It is available
193
+ for `claude-code`, `codex` and `conductor`. Hue does not yet accept Cursor's sign-in callback, so
194
+ `cursor` and the other clients use a key.
195
+
196
+ `--read-only` adds `?read_only=true` to a key configuration's URL, so Hue hides and rejects write
197
+ tools whatever the key allows. A project key, with or without it, keeps reaching its one project.
198
+ With `--auth oauth` it is refused: a sign-in connection has **Read and write** access, so use a
199
+ **Read** project key for read-only access.
200
+
201
+ `--toolsets <names>` lists only the named tools, for clients with a tool-count limit or agents that
202
+ only read production: `observe` (the production reads: projects, traces, spans and their recorded
203
+ content, sessions, trace checks, intents and documentation), `all`, or the groups `project`, `traces`, `evals`, `environments`,
204
+ `intents` and `docs`, comma-separated. A key configuration adds `?toolsets=<names>` to the URL. A
205
+ sign-in configuration keeps the URL bare and sends the `X-Hue-MCP-Toolsets` header instead,
206
+ because toolsets are not part of the connection's identity: Claude Code stores the header, and for
207
+ Codex the command prints the `http_headers` line to add to `~/.codex/config.toml`, since
208
+ `codex mcp add` stores none. `--toolsets` replaces a selection already in `--url`, and a selection in `--url` is kept without
209
+ it; otherwise `cursor` selects `observe`. `--toolsets all` lists every tool. Unknown names are refused rather than passed on, because Hue
210
+ ignores them and would list every tool. `get_project_context` is always listed and reports the
211
+ selection.
212
+
213
+ | Client | Key (`--auth key`) | Sign-in (`--auth oauth`) |
214
+ | --- | --- | --- |
215
+ | `claude-code` | Merges `mcpServers.hue` into `./.mcp.json`. `--scope user` runs `claude mcp add --transport http --scope user hue URL --header 'Authorization: Bearer ${HUE_MCP_KEY}'` when `claude` is on `PATH`, otherwise prints it. | Merges `mcpServers.hue` with only `type` and `url` into `./.mcp.json`; `--scope user` runs `claude mcp add --transport http --scope user hue URL`. Then run `/mcp` in Claude Code, select `hue` and choose **Authenticate**. |
216
+ | `codex` | Runs `codex mcp add hue --url URL --bearer-token-env-var HUE_MCP_KEY`, or prints the `[mcp_servers.hue]` TOML block for `~/.codex/config.toml`. | Runs `codex mcp add hue --url URL`, which can start sign-in right away; `codex mcp login hue` starts or repeats it. |
217
+ | `conductor` | Runs the Claude Code user-scope and Codex key commands above. | The default: runs `claude mcp add --transport http --scope user hue URL` and `codex mcp add hue --url URL`. Then use `hue`'s authentication action in Conductor's MCP status (the plug icon or `/mcp-status`). |
218
+ | `cursor` | Merges `mcpServers.hue` into `./.cursor/mcp.json` with `${env:HUE_MCP_KEY}`. | Refused. |
219
+ | `vscode` | Merges `servers.hue` and the `hue-mcp-key` password input into `./.vscode/mcp.json`. | Refused. |
220
+ | `windsurf` | Prints the `serverUrl` snippet for `~/.codeium/windsurf/mcp_config.json`; nothing is written to the home directory. | Refused. |
221
+ | `gemini` | Runs `gemini mcp add --scope user --transport http hue URL --header 'Authorization: Bearer ${HUE_MCP_KEY}'`, or prints it. | Refused. |
222
+
223
+ Conductor has no MCP configuration of its own: its Claude Code and Codex agents read their user
224
+ configuration in every workspace, so `conductor` registers the server at user scope with each CLI
225
+ on `PATH` and prints the command for a CLI that is missing. A failing CLI does not stop the other;
226
+ the command then exits `1`. Hue has not yet verified the complete signed-in Conductor flow (consent
227
+ through a trace read) end to end; if Hue's tools do not load after signing in, run the command
228
+ again with `--auth key`.
192
229
 
193
230
  JSON files are parsed and merged: other servers, inputs and top-level fields are kept, only the
194
231
  `hue` entry is replaced, and invalid JSON (including comments) is refused together with the snippet
195
232
  to add by hand. Files are written with mode `0644` through a temporary file and an atomic rename;
196
- symlinks are refused. `--dry-run` prints the resulting file content or command without writing or
233
+ symlinks are refused. `--dry-run` prints the resulting file content or commands without writing or
197
234
  running; `--print` prints only the snippet. Client CLIs run without a shell, so the
198
- `${HUE_MCP_KEY}` and `$HUE_MCP_KEY` references reach them literally; the printed commands use
199
- single quotes for the same reason. After installation the command reminds you to export
200
- `HUE_MCP_KEY` in the shell that starts the client (VS Code prompts for the key instead) and prints
201
- the verification prompt: `Use the Hue MCP: call get_project_context, then show my 5 most recent
202
- error traces with links.` Exit codes: `0` done or printed, `1` failed, `2` usage error.
235
+ `${HUE_MCP_KEY}` reference reaches them literally; the printed commands use single quotes for the
236
+ same reason, and quote a URL with a query such as `?read_only=true`, whose `?` is a zsh glob.
237
+
238
+ After a key installation the command reminds you to export `HUE_MCP_KEY` in the shell that starts
239
+ the client (VS Code prompts for the key instead). A desktop app started from the Dock or a launcher
240
+ does not see that shell's variables, and Conductor's agents read the login-shell environment that
241
+ Conductor captures, so sign-in is the simpler choice there. After a sign-in installation it names
242
+ the client's authentication action. Both end with the prompt that verifies the connection:
243
+ `Use the Hue MCP: call list_projects and confirm which project to inspect. Then call get_project_context and show that project's traces from the last 24 hours that need attention or have errors, with links; if there are none, show its 5 most recent traces. For an organization connection, pass the project's id as project_id on each call after list_projects.` `list_projects` returns a project key's one project too. Exit codes:
244
+ `0` done or printed, `1` failed, `2` usage error.
245
+
246
+ ## Deliver simulated events to a local bot
247
+
248
+ `hue listen` delivers a simulated world's events to a receiver on your machine, so an event-driven
249
+ agent such as a Slack bot can be tested without a public URL, the way `stripe listen --forward-to`
250
+ delivers webhooks. Hue signs each request as the provider does (Slack Events API requests today);
251
+ the command pulls them, forwards each one unchanged to `--forward-to` and reports the bot's answer
252
+ to Hue, which settles or retries the event on the provider's schedule. It needs a **listen** event
253
+ subscription on the world or on a connection key; the subscription's id and signing secret are shown
254
+ once when it is created. Configure the bot with that signing secret where `SLACK_SIGNING_SECRET`
255
+ went, then start the command: Hue offers the URL verification first, and events follow once the bot
256
+ has answered it. Event subscriptions are not yet available on `https://app.hue.run`; until the
257
+ origin offers them, the command stops with Hue's refusal.
258
+
259
+ ```sh
260
+ hue listen --subscription <id> --forward-to http://localhost:3000/slack/events --env-path .env.world
261
+ ```
262
+ The credential is the subscription's own, read from the environment (or `--env-path`, loaded first;
263
+ a variable already set keeps its value): `HUE_WORLD_TOKEN` for a world's subscription or
264
+ `HUE_CONNECTION_KEY` for a connection key's, chosen with `--credential world-token|connection-key`
265
+ when both are set. It is never taken from the command line, where other local processes and shell
266
+ history can read it. A project key is refused: a value equal to `HUE_API_KEY` or `HUE_MCP_KEY` exits
267
+ 2 before any request, and Hue answers any credential that is not the subscription's with `401`.
268
+ `--origin` (default `HUE_BASE_URL`, then `https://app.hue.run`) must be HTTPS, or plain HTTP on a
269
+ loopback test server.
270
+
271
+ Each request reaches the bot with its body bytes and headers unchanged, so the provider's signature
272
+ verifies as it would in production; only `Host`, `Content-Length` and connection headers are the
273
+ local request's own, and Hue's credential is never forwarded. No redirect is followed: a `3xx` is
274
+ reported as the answer. An answer counts only within three seconds, Slack's acknowledgement window;
275
+ a later one is a timeout. A failing answer with `x-slack-no-retry: 1` is reported so Hue stops
276
+ retrying it. Only a successful URL verification answer's body (at most 4 KiB, for its challenge) is
277
+ sent to Hue; any other answer body stays on this machine. Each pull waits for at most 20 seconds and
278
+ leases up to `--max` deliveries (1 to 10, default 10), which are forwarded concurrently; the next
279
+ pull starts once they are acknowledged, and a delivery beyond `--max` is never forwarded. A provider
280
+ retry of an event is a new delivery and is forwarded again with its `X-Slack-Retry-Num` and
281
+ `X-Slack-Retry-Reason`, so the bot deduplicates by `event_id` as it does with Slack. A delivery Hue
282
+ hands out a second time is not sent again; its recorded answer is repeated. An acknowledgement Hue
283
+ did not answer is repeated until the delivery's lease ends, as Hue stated it (30 seconds from the
284
+ lease).
285
+
286
+ `--forward-to` must name this machine: `localhost`, a `.localhost` name or a loopback address, and a
287
+ name is refused unless every address it resolves to is a loopback address. `--allow-remote-forward`
288
+ permits another host over HTTPS only, because every request carries the subscription's verification
289
+ token and a valid signature; plain HTTP still reaches only loopback addresses. Credentials and
290
+ fragments in the URL are refused. Output is one line per delivery (time, event id, retry number, the
291
+ bot's status and duration, and the state Hue recorded), never a body, a signature or a credential;
292
+ credentials in error messages are replaced with `[redacted]`.
293
+
294
+ Ctrl+C or `SIGTERM` stops pulling (a waiting pull is cancelled), finishes the deliveries in flight
295
+ and acknowledges them, then exits `0`; a second Ctrl+C abandons the forwards and acknowledgements
296
+ still in flight and exits `130`. Nothing is acknowledged before the bot answered or its window
297
+ closed, and a lease left unacknowledged lapses into a timeout that Hue retries on the provider's
298
+ schedule, so no event is lost and none is marked delivered unanswered. Network errors, `429`, `5xx`
299
+ and another open pull for the same subscription (a second `hue listen`) are retried with backoff up
300
+ to 30 seconds; a `Retry-After` can lengthen a wait to at most 60 seconds, never shorten it. Exit
301
+ codes: `0` stopped, `1` Hue refused the credential, the subscription (unknown, revoked, or one that
302
+ delivers to a request URL) or the pull itself (a redirect, another refusal or an unreadable answer),
303
+ `2` usage error, `130` interrupted twice.
203
304
 
204
305
  ## Local state and conflicts
205
306
 
@@ -305,7 +406,7 @@ node packages/sdk-typescript/scripts/verify-package.mjs --artifacts-dir .artifac
305
406
  # Set project to an existing supported fixture; use the same directory on resume.
306
407
  project=/absolute/path/to/supported-fixture
307
408
  node packages/sdk-typescript/scripts/verify-setup-live.mjs \
308
- --archive .artifacts/typescript/hue-run-sdk-0.10.0.tgz \
409
+ --archive .artifacts/typescript/hue-run-sdk-0.11.0.tgz \
309
410
  --origin https://STAGING_ORIGIN \
310
411
  --project "$project" --command setup \
311
412
  --evidence .context/setup-staging-before-claim.json
@@ -316,7 +417,7 @@ the private local handoff and finish the real browser claim, then reconcile the
316
417
 
317
418
  ```sh
318
419
  node packages/sdk-typescript/scripts/verify-setup-live.mjs \
319
- --archive .artifacts/typescript/hue-run-sdk-0.10.0.tgz \
420
+ --archive .artifacts/typescript/hue-run-sdk-0.11.0.tgz \
320
421
  --origin https://STAGING_ORIGIN \
321
422
  --project "$project" --command claim \
322
423
  --evidence .context/setup-staging-after-claim.json
@@ -393,11 +494,19 @@ Failing cases print the scorer explanation. An evaluator Hue records as not appl
393
494
  (its outcome scoring has no outcome criteria or conversion rubric to grade there) shows `n/a` in
394
495
  that case's columns and neither passes nor fails it, so in a mixed eval set a case is decided by
395
496
  the evaluators that apply to it; the pass line counts the not-applicable results. A case that no
396
- pinned evaluator applies to is an error that names what they need. `--baseline <experiment id|url>` adds improvement,
497
+ pinned evaluator applies to is an error that names what they need. A result of a pinned Hue judge
498
+ (an evaluator of kind `world_judge`) that Hue marks advisory, as it does every judge's today, is
499
+ shown in its column, marked `(advisory)`, but never decides a case or the exit code; the pass line
500
+ counts advisory failures as not counted, and a case only advisory results ran for is an error. Any
501
+ other evaluator's result, an error, or a metric that carries `passed` is never advisory, whatever
502
+ its evidence says. A metric name that
503
+ several evaluators report, such as two judges' `verdict`, gets a column per evaluator, labelled
504
+ with the version's first eight characters. `--baseline <experiment id|url>` adds improvement,
397
505
  regression and unchanged counts with per-case deltas. `--json` prints one JSON document
398
506
  (`experimentId`, `runId`, `runUrl`, `complete`, `cases`, `totals`, optional `baseline`) on stdout
399
507
  and sends progress to stderr; each case lists its not-applicable evaluator versions in
400
- `notApplicable`, and `totals.notApplicable` counts them; a case failed for its telemetry has `state: "error"`,
508
+ `notApplicable` and its advisory ones in `advisory`, and `totals.notApplicable` counts the
509
+ former; a case failed for its telemetry has `state: "error"`,
401
510
  `passed: false` and
402
511
  `telemetry: { code: "telemetry_not_accepted", issues: [{ signal, kind, status?, count }] }`. `--wait <seconds>` (default 300) bounds the verdict wait because
403
512
  Hue-owned `world_outcome` checks are graded after the world seals. An experiment always covers
@@ -406,7 +515,9 @@ every case of the saved version; there is no case subset.
406
515
  `--command "<shell command>"` spawns the command once per case with the world's environment
407
516
  (`HUE_WORLD_ID`, `HUE_WORLD_TOKEN`, one `HUE_SIM_<SURFACE ID>_URL` per provider mirror,
408
517
  `HUE_MCP_CONFIG` naming an owner-only `mcpServers` file that is removed after the case, and
409
- `HUE_MCP_URL`, `HUE_MCP_TOKEN`, `HUE_MCP_EXPIRES_AT` for the first MCP mirror), plus
518
+ `HUE_MCP_URL`, `HUE_MCP_TOKEN`, `HUE_MCP_EXPIRES_AT` for the first MCP mirror; these name the
519
+ world's simulated MCP server and replace a `HUE_MCP_URL` that `hue login` stored for Hue's project
520
+ MCP server), plus
410
521
  `HUE_EXECUTION_ID`, `HUE_ENVIRONMENT_RUN_ID`, `HUE_CASE_ID` and `HUE_CASE_KEY`, and
411
522
  `{"inputs": ..., "config": ...}` on stdin. The child does not receive `HUE_API_KEY`, `HUE_MCP_KEY`
412
523
  or any other Hue control-plane credential unless `--allow-hue-credentials` is passed; the rest of
@@ -475,7 +586,10 @@ case also keeps the agent's raw `output/` files on disk under its checkpoint dir
475
586
  Trace evidence is required for every case: when Hue does not accept a case's traces or logs, the
476
587
  case is completed as failed (error `TelemetryNotAccepted`, evidence omitted as
477
588
  `telemetry_not_accepted`, no output or generated files attached) instead of being left started,
478
- and the run goes on. Stderr names the case as it completes, with the export
589
+ and the run goes on. Each export of the CLI's own telemetry may take 30 seconds, retries
590
+ included: a slow acknowledgement is waited for, and a 429, 502, 503, 504 or network error is
591
+ retried within that time, honouring `Retry-After`; any other response, or rejected records, fails
592
+ the case at once. Stderr names the case as it completes, with the export
479
593
  issue counts, for example
480
594
  `[refund] telemetry not accepted, case failed: telemetry_not_accepted: traces failed 1 (HTTP 400)`;
481
595
  the case counts as an error in the table and JSON whatever its scores, and the command exits 1.
@@ -485,8 +599,9 @@ inside `.hue/eval/`); `--checkpoint-dir` overrides the root. Rerunning the same
485
599
  an interrupted run without invoking the agent again; a different selection is refused until the
486
600
  unfinished one is resumed or its directory is removed.
487
601
 
488
- Exit codes: `0` every case passed the evaluators that apply to it, `1` a case failed, errored
489
- (including one no pinned evaluator applies to), was skipped or Hue's checks were
602
+ Exit codes: `0` every case passed the evaluators that apply to it (advisory ones never count),
603
+ `1` a case failed, errored (including one no pinned evaluator applies to or decides), was skipped
604
+ or Hue's checks were
490
605
  still pending at `--wait`, `2` usage or configuration error (including a missing key), `130`
491
606
  interrupted. On Node.js 22, load TypeScript adapters with `NODE_OPTIONS=--experimental-strip-types`;
492
607
  non-erasable syntax (enums, parameter properties, namespaces) needs a loader such as `--import tsx`
@@ -519,6 +634,11 @@ For each case the command is spawned once **inside a private case directory** wi
519
634
  <case dir>/output/ write the generated documents here
520
635
  ```
521
636
 
637
+ Hue verifies each generated document before the case completes. Once a document is reserved and
638
+ its bytes are uploaded, a verification that outlasts a request is waited out for up to three
639
+ minutes, so a completion that cannot reach Hue takes that long to fail; a reservation or upload
640
+ that cannot reach Hue still fails at once.
641
+
522
642
  Every regular file the command leaves under `output/` is uploaded as a generated document
523
643
  (accepted: `.pdf .docx .pptx .xlsx .json .txt .csv .png .jpg .jpeg .webp`; another extension or an
524
644
  empty file is the case's error). Optional helpers: `manifest.json` (`{"primary": "<filename>",
package/ENVIRONMENTS.md CHANGED
@@ -79,7 +79,11 @@ until every item has a terminal result for every pin or the budget elapses (`com
79
79
  `summarizeVerdicts` turns results into per-case rows with `passed` and totals; a result Hue marks
80
80
  `notApplicable` (the evaluator has no outcome criteria or conversion rubric to grade in that case)
81
81
  neither passes nor fails its case and is listed in the row's `notApplicable`, and a case no pinned
82
- evaluator applies to is an error. `compareVerdicts` diffs two summaries by case key, and
82
+ evaluator applies to is an error. A result of a Hue judge listed in `waitForResults`'
83
+ `judgeScorerVersionIds` (`collectExperimentVerdicts` passes the experiment's `world_judge` pins)
84
+ whose evidence says `advisory: true` keeps its metrics in the row and is listed in its
85
+ `advisory`, but never decides the case; a case only advisory results ran for is an error. Any
86
+ other result, an error, or one with a metric carrying `passed` is never advisory. `compareVerdicts` diffs two summaries by case key, and
83
87
  `collectExperimentVerdicts` combines those reads for one experiment. The `hue eval` command uses the same path; see
84
88
  [Evaluate an agent against a case](CLI.md#evaluate-an-agent-against-a-case).
85
89
 
package/EVALUATIONS.md CHANGED
@@ -159,7 +159,7 @@ built-ins keep their usual missing-output behavior. See
159
159
 
160
160
  Scorer deferral shipped in TypeScript `0.3.1`: the local runner executes only the three known built-in entries and bound `local_code` scorers. It leaves every other pin pending and reports its ID in `deferredScorerVersionIds`, including kinds and built-in entries introduced by a newer server. It never uploads a placeholder result that would occupy the immutable result slot, including placeholders already saved in an older SDK's checkpoint. Direct `scoreLocally()` calls reject pins that require another executor.
161
161
 
162
- `world_outcome` pins run inside Hue and need no local callback or executable source digest. A supporting server owns their execution from saved world evidence. Legacy `local_code` pins still require the exact registered callback; changing the worker cannot convert those immutable pins into hosted ones.
162
+ `world_outcome` pins (`hue.conversion_outcome.v1` and `.v2`, and the `hue.outcome_assertions.v2` and `.v3` case-outcome evaluators, `.v3` with its pinned judge in `config.judge`) run inside Hue and need no local callback or executable source digest. A supporting server owns their execution from saved world evidence. Legacy `local_code` pins still require the exact registered callback; changing the worker cannot convert those immutable pins into hosted ones.
163
163
 
164
164
  Manual results require a human session. Hosted model-judge dispatch is an explicit separate API operation: inspect `getJudgeBudget()`, then call `createJudgeJobs(runId,{idempotencyKey,jobs:[{evaluationItemId,scorerVersionId}]})`. `listJudgeJobs`, `getJudgeJob` and `cancelJudgeJob` expose job progress and cancellation requests. These methods never claim that local execution has hosted provenance. Hosted job endpoints are covered by HTTP contract tests here; live hosted model execution is a separate platform acceptance phase. `listResults` and `getResult` read recorded local or hosted results.
165
165
 
@@ -183,7 +183,7 @@ An exclusive `.lock` prevents two processes from invoking targets through the sa
183
183
 
184
184
  The runner saves a starting marker before `start`, and a running marker before invoking a target. If an outcome is not durably saved, resume throws `UncertainExecutionError` and never reruns the target. Replaying an original start key only recovers its execution ID. The low-level `startExecution` API requires an explicit `previousExecutionId`; replacing a still-started attempt additionally requires `allowUncertainRetry:true`. The convenience runner does not automatically adopt externally created attempts. Create a fresh experiment for a fresh target run after investigating side effects.
185
185
 
186
- After a target completes and local scoring finishes, the runner saves the allowed completion/result payloads before uploading. Network/API errors leave those payloads and stable keys available for another call to `runExperiment` with the same directory. Saved receipt IDs prevent duplicate result writes. Serialization failure raises `OutcomeSerializationError` with the execution ID; it does not relabel the target as failed or invoke it again. Scoring/upload failures never change target state.
186
+ After a target completes and local scoring finishes, the runner saves the allowed completion/result payloads before uploading. Network/API errors leave those payloads and stable keys available for another call to `runExperiment` with the same directory. Saved receipt IDs prevent duplicate result writes. An output beyond what Hue stores for one case (200,000 bytes of JSON, 20,000 values or 32 levels of nesting) is that case's own failure: the case completes as `error` with the error type `OutputTooLarge` (and, when result content is persisted, a message naming the bound), no output is stored and the other cases keep running; return a large result as a generated file instead. Output that is not JSON (a cycle, a class instance, a non-finite number) raises `OutcomeSerializationError` with the execution ID. The output is checked as it is read, and the first value that is not JSON or past the value or nesting bound decides which; the byte bound is checked last, so an output past it that also holds a value that is not JSON raises `OutcomeSerializationError`, and one too long for the runtime to serialize is refused before it is serialized. `OutcomeSerializationError` never relabels the target as failed or invokes it again. Scoring/upload failures never change target state.
187
187
 
188
188
  A crash between target completion and saving its permitted result still leaves an uncertain outcome; metadata-only mode intentionally cannot reconstruct discarded output. Export acknowledgement is saved separately. If required telemetry export was not acknowledged, a fresh empty exporter is not evidence of prior receipt: automatic completion is refused. Inspect/export the original trace or use the low-level completion API with an explicit omission policy; do not rerun a known completed target to manufacture telemetry. API completion/results retain their own idempotency guarantees for explicit recovery.
189
189
 
package/README.md CHANGED
@@ -83,8 +83,9 @@ claim. Setup never enables content capture or creates a simulation, Hue Run, eva
83
83
  or remote execution. See the [setup CLI contract](./CLI.md) for the supported shapes and release gates.
84
84
 
85
85
  For an existing account, `hue login` validates keys created in Hue Settings and stores them in
86
- `.env.hue` without printing them, and `hue mcp install --client claude-code` (or `cursor`, `codex`,
87
- `vscode`, `windsurf`, `gemini`) writes the Hue MCP configuration that references `HUE_MCP_KEY`. See
86
+ `.env.hue` without printing them, and `hue mcp install --client claude-code` (or `codex`,
87
+ `conductor`, `cursor`, `vscode`, `windsurf`, `gemini`) writes the Hue MCP configuration that
88
+ references `HUE_MCP_KEY`, or with `--auth oauth` only the URL, for signing in with Hue. See
88
89
  [Sign in and store keys](./CLI.md#sign-in-and-store-keys) and
89
90
  [Install the MCP for your coding agent](./CLI.md#install-the-mcp-for-your-coding-agent).
90
91
 
@@ -155,12 +156,22 @@ await hue.model(
155
156
 
156
157
  Each OpenAI Responses `mcp_call`, `web_search_call`, `file_search_call` or `code_interpreter_call`
157
158
  item, and each Anthropic `mcp_tool_use` or `server_tool_use` block with its result, becomes an
158
- `execute_tool {name}` child span with `gen_ai.tool.type` `extension`, `gen_ai.tool.call.id` and,
159
- for MCP calls, `mcp.server.name` (the provider's label). Arguments and results follow
160
- `captureContent`; a failed call carries `error.type` (`mcp_error`, the provider's status or error
161
- code) and ERROR status. An `mcp_list_tools` item becomes a `tools/list` child span with that
162
- server's `gen_ai.tool.definitions`. Pass the request so each server's host is recorded as
163
- `server.address` (only server URLs are read, never credentials), and `servers` to record a
159
+ `execute_tool {name}` child span with `gen_ai.tool.type` `extension`, `gen_ai.tool.call.id`,
160
+ `hue.tool.call.position` (the 0-based position of its item in the response: the OpenAI `output`
161
+ index or the Anthropic `content` block index, in both capture modes) and, for MCP calls,
162
+ `mcp.server.name` (the provider's label). Arguments and results follow `captureContent`; a failed
163
+ call carries `error.type` (`mcp_error`, the provider's status or error code) and ERROR status. With
164
+ `captureContent: true`, a failed OpenAI MCP call's status description is the provider's error
165
+ text, credentials scrubbed (URL userinfo, query values and fragments, or the whole URL for a scheme
166
+ other than `http(s)`, `ws(s)` and `ftp`; tokens with a known credential prefix such as `hue_sk_`,
167
+ `sk-` or `xoxb-`; authorization-scheme credentials and an `Authorization` header's whole value;
168
+ the value of a credential-named `key=value` or `key: value` pair, quoted or not) and cut to 1,024
169
+ characters. An `mcp_list_tools`
170
+ item becomes a `tools/list` child span with that server's `gen_ai.tool.definitions`; with
171
+ `captureContent: false` it carries only `hue.tool.names` and `hue.tool.definitions.sha256`, the
172
+ summary described below. Pass the request so each server's host is recorded as `server.address`
173
+ (only server URLs are read, never credentials; a host name keeps its underscores, as WHATWG URL
174
+ parsing does), and `servers` to record a
164
175
  server's real `name`, `version`, Hue `provider` and `surface` under its label. `provider` defaults
165
176
  to the enclosing `model()` call's provider (`openai` or `anthropic`). The spans have no duration of
166
177
  their own: the provider ran the tools inside the model request.
package/dist/cli/eval.js CHANGED
@@ -446,6 +446,10 @@ function parseAnswer(text) {
446
446
  }
447
447
  }
448
448
  const MIN_SECRET_LENGTH = 16;
449
+ /** Each export of the CLI's own telemetry, retries included, may take this long. A case's trace
450
+ * is required evidence, and Hue can acknowledge it slowly or ask for a retry after a few seconds,
451
+ * which the 10-second default leaves no room for. A refusal still fails at once. */
452
+ const CASE_TRACE_EXPORT_MILLIS = 30_000;
449
453
  function caseSecrets(context) {
450
454
  const values = new Set();
451
455
  // Hue issues long credentials; a short value would redact ordinary text around it.
@@ -696,27 +700,38 @@ const STATE_LABEL = {
696
700
  pending: "PENDING",
697
701
  };
698
702
  export function renderTable(verdicts, output) {
699
- const names = [];
700
- // The evaluators that report each metric: a case none of them applies to shows n/a there, and
701
- // one an evaluator applies to without reporting the metric shows "-".
702
- const reportedBy = new Map();
703
- for (const item of verdicts.summary.cases)
703
+ const cases = verdicts.summary.cases;
704
+ // A column per metric name; a name several evaluators report, such as two judges' `verdict`,
705
+ // gets a column per evaluator so each one's value shows.
706
+ const reporters = new Map();
707
+ for (const item of cases)
704
708
  for (const metric of item.metrics) {
705
- if (!names.includes(metric.name))
706
- names.push(metric.name);
707
- reportedBy.set(metric.name, (reportedBy.get(metric.name) ?? new Set()).add(metric.scorerVersionId));
709
+ const versions = reporters.get(metric.name) ?? [];
710
+ if (!versions.includes(metric.scorerVersionId))
711
+ versions.push(metric.scorerVersionId);
712
+ reporters.set(metric.name, versions);
708
713
  }
709
- const header = ["Case", ...names, "Result"];
710
- const rows = verdicts.summary.cases.map((item) => [
714
+ const columns = [...reporters].flatMap(([name, versions]) => versions.map((version) => ({
715
+ name,
716
+ version,
717
+ label: versions.length === 1 ? name : `${name} ${version.slice(0, 8)}`,
718
+ })));
719
+ // An advisory evaluator's values are shown, marked, and never decide a case.
720
+ const advisory = new Set(cases.flatMap((item) => item.advisory ?? []));
721
+ const header = [
722
+ "Case",
723
+ ...columns.map((column) => advisory.has(column.version) ? `${column.label} (advisory)` : column.label),
724
+ "Result",
725
+ ];
726
+ const rows = cases.map((item) => [
711
727
  item.externalKey,
712
- ...names.map((name) => {
713
- const metric = item.metrics.find((candidate) => candidate.name === name);
728
+ ...columns.map((column) => {
729
+ const metric = item.metrics.find((candidate) => candidate.name === column.name && candidate.scorerVersionId === column.version);
714
730
  if (metric)
715
731
  return metricText(metric);
716
- const notApplicable = new Set(item.notApplicable);
717
- return [...reportedBy.get(name)].every((version) => notApplicable.has(version))
718
- ? "n/a"
719
- : "-";
732
+ // A case the column's evaluator does not apply to shows n/a; one it applies to without
733
+ // reporting the metric shows "-".
734
+ return item.notApplicable?.includes(column.version) ? "n/a" : "-";
720
735
  }),
721
736
  STATE_LABEL[item.state],
722
737
  ]);
@@ -737,6 +752,8 @@ export function renderTable(verdicts, output) {
737
752
  output.log(` ${item.externalKey}: ${detail}`);
738
753
  }
739
754
  const totals = verdicts.summary.totals;
755
+ const advisoryFailures = cases.reduce((count, item) => count +
756
+ item.metrics.filter((metric) => item.advisory?.includes(metric.scorerVersionId) && !metricPassed(metric)).length, 0);
740
757
  const extra = [
741
758
  totals.error ? `${totals.error} error` : "",
742
759
  totals.skipped ? `${totals.skipped} skipped` : "",
@@ -744,6 +761,9 @@ export function renderTable(verdicts, output) {
744
761
  totals.notApplicable
745
762
  ? `${totals.notApplicable} evaluator result${totals.notApplicable === 1 ? "" : "s"} not applicable`
746
763
  : "",
764
+ advisoryFailures
765
+ ? `${advisoryFailures} advisory failure${advisoryFailures === 1 ? "" : "s"} not counted`
766
+ : "",
747
767
  ].filter(Boolean);
748
768
  output.log(`${totals.passed} of ${totals.cases} case${totals.cases === 1 ? "" : "s"} passed${extra.length ? ` (${extra.join(", ")})` : ""}`);
749
769
  if (!verdicts.results.complete)
@@ -1426,7 +1446,13 @@ export async function runEvalCommand(argv) {
1426
1446
  allowHueCredentials: values["allow-hue-credentials"],
1427
1447
  })),
1428
1448
  };
1429
- hue = createHue({ apiKey, baseUrl, serviceName: key, captureContent: values.content });
1449
+ hue = createHue({
1450
+ apiKey,
1451
+ baseUrl,
1452
+ serviceName: key,
1453
+ captureContent: values.content,
1454
+ timeoutMillis: CASE_TRACE_EXPORT_MILLIS,
1455
+ });
1430
1456
  return values.worker
1431
1457
  ? await runWorker(values, connection, agents.simulation, agent, hue, output, controller.signal)
1432
1458
  : await runOnce(values, connection, agents, agent, hue, output, controller.signal);
@@ -0,0 +1,76 @@
1
+ import { lookup as dnsLookup } from "node:dns";
2
+ /**
3
+ * `hue listen`: delivers a simulated world's events to a receiver on this machine, the way a
4
+ * provider would post them to a public request URL. The client pulls leased deliveries from Hue
5
+ * with the subscription's own credential (a world token or a connection key, never a project
6
+ * key), forwards each stored request unchanged to the local URL, and acknowledges it with the
7
+ * local answer. Every pull is one bounded request; nothing holds a connection open. A delivery is
8
+ * acknowledged only after the receiver answered or its window closed, so stopping never marks an
9
+ * event delivered that was not: an unacknowledged lease lapses and Hue retries it on the
10
+ * provider's schedule.
11
+ */
12
+ /** Streams, environment, network and signal seams for {@link runListenCommand}; tests inject these. */
13
+ export interface ListenCommandIo {
14
+ stdout?: NodeJS.WritableStream;
15
+ stderr?: NodeJS.WritableStream;
16
+ env?: NodeJS.ProcessEnv;
17
+ cwd?: string;
18
+ fetch?: typeof fetch;
19
+ /** Registers the stop request (SIGINT and SIGTERM by default) and returns its removal. */
20
+ onInterrupt?: (listener: () => void) => () => void;
21
+ /** The acknowledgement window a forward waits for; the provider's three seconds by default. */
22
+ forwardTimeoutMs?: number;
23
+ /** Delays between retries of a failed pull. */
24
+ retryDelaysMs?: readonly number[];
25
+ }
26
+ export declare const LISTEN_USAGE = "Usage: hue listen --subscription ID --forward-to URL [--origin URL] [--env-path PATH]\n [--credential world-token|connection-key] [--max N] [--allow-remote-forward]\n\nDeliver a simulated world's events to a receiver on this machine. Hue's signed requests (Slack\nEvents API requests today) are pulled with the subscription's own credential, forwarded unchanged\nto --forward-to, and acknowledged with the receiver's answer. Press Ctrl+C to stop: pulls stop,\ndeliveries in flight are finished and acknowledged, and nothing unanswered is acknowledged.\n\nThe credential is read from the environment, never from the command line:\n HUE_WORLD_TOKEN the world token, for a subscription on one world\n HUE_CONNECTION_KEY the connection key, for a subscription on a connection key\nA project key (HUE_API_KEY, HUE_MCP_KEY) is refused.\n\nOptions:\n --subscription ID The listen subscription to deliver (its id from Hue)\n --forward-to URL The local receiver, for example http://localhost:3000/slack/events\n --origin URL Hue origin (default HUE_BASE_URL, then https://app.hue.run)\n --env-path PATH Load a dotenv file first (--env-file also works when it exists)\n --credential KIND world-token or connection-key, when both variables are set\n --max N Deliveries leased per pull and forwarded at once, 1 to 10 (default 10)\n --allow-remote-forward Allow a --forward-to host other than this machine, over https only\n -h, --help Show this help";
27
+ /** A leased delivery: the exact request the provider would send. */
28
+ interface Delivery {
29
+ deliveryId: string;
30
+ eventId: string | null;
31
+ kind: "url_verification" | "event_callback";
32
+ retryNum: number;
33
+ /** The lease's end as the server stated it, or null when absent. */
34
+ leaseExpiresAt: number | null;
35
+ request: {
36
+ headers: Record<string, string>;
37
+ body: string;
38
+ };
39
+ }
40
+ /** The local answer, in the acknowledgement's own shape. */
41
+ interface LocalAnswer {
42
+ outcome: "response" | "timeout" | "connection_failed" | "tls_error";
43
+ status?: number;
44
+ durationMs: number;
45
+ noRetry?: boolean;
46
+ body?: string;
47
+ }
48
+ /** A loopback address: 127.0.0.0/8, ::1, or 127.0.0.0/8 carried in IPv4-mapped IPv6. */
49
+ export declare function isLoopbackAddress(address: string): boolean;
50
+ /**
51
+ * The receiver URL, or why it is refused: HTTP or HTTPS without credentials or fragment, on this
52
+ * machine (`localhost`, a `.localhost` name or a loopback address) unless `allowRemote`, and then
53
+ * HTTPS only off this machine.
54
+ */
55
+ export declare function parseForwardTarget(value: string, allowRemote: boolean): URL | string;
56
+ /**
57
+ * Sends one delivery to the receiver and reports its answer, or null when `signal` abandoned it.
58
+ * The body bytes and every header go out unchanged (the provider's signature covers them), no
59
+ * redirect is followed, and the answer counts only inside `timeoutMs`. Only a URL verification's
60
+ * successful answer body is read.
61
+ */
62
+ export declare function forwardDelivery(target: URL, delivery: Delivery, options?: {
63
+ timeoutMs?: number;
64
+ allowRemote?: boolean;
65
+ signal?: AbortSignal;
66
+ /** Resolves `--forward-to` names; tests stand in for DNS. */
67
+ resolver?: typeof dnsLookup;
68
+ }): Promise<LocalAnswer | null>;
69
+ /** One delivery from a pull answer, or null when it is not one this client can forward safely. */
70
+ export declare function parseDelivery(value: unknown, subscriptionId: string): Delivery | null;
71
+ /**
72
+ * Runs `hue listen` and returns the process exit code: 0 stopped, 1 failed, 2 usage error, 130
73
+ * interrupted twice (acknowledgements abandoned). `argv` may start with the `listen` command word.
74
+ */
75
+ export declare function runListenCommand(argv: string[], io?: ListenCommandIo): Promise<number>;
76
+ export {};