Skip to content

test(agentic-use): add evaluator agent benchmark matrix - #3

Merged
SandyChapman merged 1 commit into
mainfrom
schapman/evaluator-agent-more-mvp-tasks
May 21, 2026
Merged

test(agentic-use): add evaluator agent benchmark matrix#3
SandyChapman merged 1 commit into
mainfrom
schapman/evaluator-agent-more-mvp-tasks

Conversation

@SandyChapman

Copy link
Copy Markdown
Contributor

Summary

  • add Evaluator agentic-use benchmark matrix/reporting support
  • add standalone SDK agent-target and surface-adherence tasks
  • update direct agent backend handling and task metric reporting

Validation

  • uv run --frozen pytest tests/agentic-use/tests/evaluator_agent_eval/test_task_metrics.py tests/agentic-use/tests/test_nat_runner_config.py tests/agentic-use/tests/test_agent_matrix_benchmark.py -q
  • bash tools/lint/lint-python-style.sh

@SandyChapman
SandyChapman requested review from a team as code owners May 21, 2026 12:34
@github-actions

github-actions Bot commented May 21, 2026

Copy link
Copy Markdown
Contributor

All contributors have signed the DCO ✍️ ✅
Posted by the DCO Assistant Lite bot.

@SandyChapman

Copy link
Copy Markdown
Contributor Author

I have read the DCO document, and I hereby sign the DCO.

@SandyChapman

Copy link
Copy Markdown
Contributor Author

recheck

@SandyChapman

Copy link
Copy Markdown
Contributor Author

I have read the DCO document, and I hereby sign the DCO.

@github-actions

Copy link
Copy Markdown
Contributor
Suite Lines Covered Line Rate Branch Rate
Unit Tests 18001/23916 75.3% 61.6%
Integration Tests 11427/22698 50.3% 25.5%

@SandyChapman
SandyChapman enabled auto-merge May 21, 2026 13:08
@SandyChapman
SandyChapman added this pull request to the merge queue May 21, 2026
Merged via the queue into main with commit b9b5933 May 21, 2026
11 of 12 checks passed
github-merge-queue Bot pushed a commit that referenced this pull request May 28, 2026
* fix: address open CodeQL alerts in TypeScript code

Close 21 open CodeQL alerts on main:

Security
- LargeFileWorker: remove dead `download` (untrusted-URL fetch) and `upload`
  actions; only `downloadAsFile` (SDK path-based) is used by callers. Closes
  #4 (client-side-request-forgery) and #17 (missing-origin-check).
- orval/generate.ts: use `fs.mkdtempSync` for the OpenAPI spec temp file
  instead of a predictable `os.tmpdir()` path. Closes #5
  (insecure-temporary-file).

Code-quality
- Drop redundant `this.page = page` / `this.request = request` in 11
  e2e-tests classes — TS parameter properties (`public readonly page: Page`,
  `private request: APIRequestContext`) already assign the field. Closes
  #22-#32 (useless-assignment-to-property).
- Drop redundant null/undefined checks after narrowing in
  ReportTraceModal/utils, BenchmarkDetailsPanel, api/intake/utils,
  ActionMenu, useSubmitICLsFile. Closes #33-#37.
- SafeSynthesizerJobReportRoute/util: drop unreachable `else if (score >= 8)`
  branches and the dead `UNAVAILABLE` fallback; add explicit
  `Number.isNaN` guard at the top of each grading helper. Closes #20, #21.
- WorkspaceDashboardRoute: drop inner `MODEL_COMPARE_ENABLED ? a : b`
  ternary that always picked `a` (lives inside an outer
  `MODEL_COMPARE_ENABLED &&` guard); drop now-unused
  `getWorkspaceBaseModelsRoute` import. Closes #19.

Signed-off-by: mschwab <mschwab@nvidia.com>

* fix: refactor remaining CodeQL-flagged build scripts to argv form

Drop shell interpolation in dev/build scripts so user-supplied branch names,
commit hashes, paths, and env values cannot be parsed as shell syntax. Also
plug a TOCTOU and add origin allowlists for the two http-to-file fetches.

- scripts/cherry-pick.ts: route every git call through execFileSync('git',
  [...]). Closes #6-#10 (indirect-cmd-line-injection).
- scripts/git-utils.ts: openBrowser uses execFile + argv array; status/branch
  helpers use execFileSync with argv. Removes the brittle " → \" escape and
  the shell-interpolated browser command. Closes #1
  (incomplete-sanitization) and #11 (indirect-cmd-line-injection).
- sdk/orval/format-generated.ts: prettier runs via execFileSync. Closes #2
  (shell-cmd-injection-from-env) and #13 (indirect-cmd-line-injection).
- sdk/orval/generate.ts: orval runs via execFileSync, with its parameters
  passed in env instead of interpolated into a shell string; remote spec
  fetches are restricted to an allowlist of github/gitlab hosts; the
  existsSync+readFileSync TOCTOU in postProcessZodFiles is collapsed into a
  single try/catch on ENOENT. Closes #3 (file-system-race), #12
  (indirect-cmd-line-injection), and #14 (http-to-file-access).
- studio/scripts/fetch-styles.ts: validate that the fetch URL hostname
  matches the configured Kaizen CDN before fetching. Closes #15
  (http-to-file-access).

Signed-off-by: mschwab <mschwab@nvidia.com>

* fix: close remaining CodeQL alerts re-emitted on PR scan

- scripts/git-utils.ts openBrowser: parse URL with `new URL()` and require
  http/https before spawning. Replace the Windows `cmd /c start` shell
  invocation with `rundll32 url.dll,FileProtocolHandler` so no branch goes
  through a shell. Pass `--` separator on darwin/linux so a URL starting
  with `-` cannot be parsed as an option. Closes #3951.
- sdk/orval/generate.ts: delete the unused HTTP-fetch branch from
  `getFile()`. All current `serviceConfigs` reference local YAML paths, so
  the network->file write CodeQL flagged on line 131 (#14) no longer
  exists. Throws a clear error if a remote URL is configured.

Signed-off-by: mschwab <mschwab@nvidia.com>

* fix: drop -- separator for xdg-open

xdg-open does not honor -- as an option terminator; passing it as an arg
caused openBrowser to fail on Linux. URL is already validated to http(s),
so the separator wasn't load-bearing — just drop it on the Linux branch.

Codex review on PR #75.

Signed-off-by: mschwab <mschwab@nvidia.com>

* fix: address CodeRabbit findings on PR #75

- scripts/git-utils.ts: drop `--` from macOS `open` argv too. `open`'s man
  page does not document `--` as an end-of-options separator. URL is
  already validated to http(s), so the separator wasn't load-bearing.
- sdk/orval/format-generated.ts: on Windows, run prettier through
  `cmd.exe /c` so the `prettier.cmd` shim resolves. `execFileSync` on
  Windows cannot launch .cmd shims directly.
- sdk/orval/generate.ts: same Windows wrap for `pnpm exec orval`.

Signed-off-by: mschwab <mschwab@nvidia.com>

* fix: validate format-generated.ts servicePath argv

The Windows cmd.exe /c wrap added in ec7aa93 re-opened a CodeQL
data-flow finding (#3961, #3962) because generatedPath traces back to
process.argv[2]. Validate the argv against a safe-char regex at entry
so CodeQL sees it as sanitized before it flows into argv or paths.

Signed-off-by: mschwab <mschwab@nvidia.com>

* fix: replace regex with hardcoded Set allowlist for servicePath

CodeQL did not recognize the regex check as a sanitizer; switching to a
hardcoded Set lookup against known serviceConfigs paths so the data flow
is reducible to a finite set of literal values.

Signed-off-by: mschwab <mschwab@nvidia.com>

* fix: use prettier Node API instead of subprocess

Replace the prettier CLI invocation with prettier's programmatic
format/resolveConfig/getFileInfo API. No subprocess means no
cmd.exe wrap, no command-line argument flow, and the CodeQL
indirect-command-line-injection / shell-cmd-injection-from-env
alerts on format-generated.ts can resolve. Also fixes the Windows
.cmd shim resolution problem CR raised, since prettier now runs
in-process.

The servicePath argv is still validated against a hardcoded Set
of known serviceConfigs paths to prevent directory traversal via
path.join.

Signed-off-by: mschwab <mschwab@nvidia.com>

* fix: use readdirSync withFileTypes to avoid statSync TOCTOU

CodeQL flagged the statSync -> readFileSync / writeFileSync pair in
formatWithPrettier as a file-system-race. Getting Dirent entries from
readdirSync(dir, { withFileTypes: true }) lets us check isDirectory /
isFile inline without a separate stat round-trip, closing the alert.

Signed-off-by: mschwab <mschwab@nvidia.com>

* fix: drop remaining statSync usages in format-generated.ts

Codex flagged that getTsFiles and splitZodTagFilesIn still used the
readdir-string + statSync pattern, leaving two more file-system-race
sinks even after formatWithPrettier was converted. Switch both to
readdirSync(dir, { withFileTypes: true }) and use Dirent.isFile() /
isDirectory() inline. Removes the last statSync from this script.

Signed-off-by: mschwab <mschwab@nvidia.com>

---------

Signed-off-by: mschwab <mschwab@nvidia.com>
aray12 pushed a commit that referenced this pull request May 28, 2026
* fix: address open CodeQL alerts in TypeScript code

Close 21 open CodeQL alerts on main:

Security
- LargeFileWorker: remove dead `download` (untrusted-URL fetch) and `upload`
  actions; only `downloadAsFile` (SDK path-based) is used by callers. Closes
  #4 (client-side-request-forgery) and #17 (missing-origin-check).
- orval/generate.ts: use `fs.mkdtempSync` for the OpenAPI spec temp file
  instead of a predictable `os.tmpdir()` path. Closes #5
  (insecure-temporary-file).

Code-quality
- Drop redundant `this.page = page` / `this.request = request` in 11
  e2e-tests classes — TS parameter properties (`public readonly page: Page`,
  `private request: APIRequestContext`) already assign the field. Closes
  #22-#32 (useless-assignment-to-property).
- Drop redundant null/undefined checks after narrowing in
  ReportTraceModal/utils, BenchmarkDetailsPanel, api/intake/utils,
  ActionMenu, useSubmitICLsFile. Closes #33-#37.
- SafeSynthesizerJobReportRoute/util: drop unreachable `else if (score >= 8)`
  branches and the dead `UNAVAILABLE` fallback; add explicit
  `Number.isNaN` guard at the top of each grading helper. Closes #20, #21.
- WorkspaceDashboardRoute: drop inner `MODEL_COMPARE_ENABLED ? a : b`
  ternary that always picked `a` (lives inside an outer
  `MODEL_COMPARE_ENABLED &&` guard); drop now-unused
  `getWorkspaceBaseModelsRoute` import. Closes #19.

Signed-off-by: mschwab <mschwab@nvidia.com>

* fix: refactor remaining CodeQL-flagged build scripts to argv form

Drop shell interpolation in dev/build scripts so user-supplied branch names,
commit hashes, paths, and env values cannot be parsed as shell syntax. Also
plug a TOCTOU and add origin allowlists for the two http-to-file fetches.

- scripts/cherry-pick.ts: route every git call through execFileSync('git',
  [...]). Closes #6-#10 (indirect-cmd-line-injection).
- scripts/git-utils.ts: openBrowser uses execFile + argv array; status/branch
  helpers use execFileSync with argv. Removes the brittle " → \" escape and
  the shell-interpolated browser command. Closes #1
  (incomplete-sanitization) and #11 (indirect-cmd-line-injection).
- sdk/orval/format-generated.ts: prettier runs via execFileSync. Closes #2
  (shell-cmd-injection-from-env) and #13 (indirect-cmd-line-injection).
- sdk/orval/generate.ts: orval runs via execFileSync, with its parameters
  passed in env instead of interpolated into a shell string; remote spec
  fetches are restricted to an allowlist of github/gitlab hosts; the
  existsSync+readFileSync TOCTOU in postProcessZodFiles is collapsed into a
  single try/catch on ENOENT. Closes #3 (file-system-race), #12
  (indirect-cmd-line-injection), and #14 (http-to-file-access).
- studio/scripts/fetch-styles.ts: validate that the fetch URL hostname
  matches the configured Kaizen CDN before fetching. Closes #15
  (http-to-file-access).

Signed-off-by: mschwab <mschwab@nvidia.com>

* fix: close remaining CodeQL alerts re-emitted on PR scan

- scripts/git-utils.ts openBrowser: parse URL with `new URL()` and require
  http/https before spawning. Replace the Windows `cmd /c start` shell
  invocation with `rundll32 url.dll,FileProtocolHandler` so no branch goes
  through a shell. Pass `--` separator on darwin/linux so a URL starting
  with `-` cannot be parsed as an option. Closes #3951.
- sdk/orval/generate.ts: delete the unused HTTP-fetch branch from
  `getFile()`. All current `serviceConfigs` reference local YAML paths, so
  the network->file write CodeQL flagged on line 131 (#14) no longer
  exists. Throws a clear error if a remote URL is configured.

Signed-off-by: mschwab <mschwab@nvidia.com>

* fix: drop -- separator for xdg-open

xdg-open does not honor -- as an option terminator; passing it as an arg
caused openBrowser to fail on Linux. URL is already validated to http(s),
so the separator wasn't load-bearing — just drop it on the Linux branch.

Codex review on PR #75.

Signed-off-by: mschwab <mschwab@nvidia.com>

* fix: address CodeRabbit findings on PR #75

- scripts/git-utils.ts: drop `--` from macOS `open` argv too. `open`'s man
  page does not document `--` as an end-of-options separator. URL is
  already validated to http(s), so the separator wasn't load-bearing.
- sdk/orval/format-generated.ts: on Windows, run prettier through
  `cmd.exe /c` so the `prettier.cmd` shim resolves. `execFileSync` on
  Windows cannot launch .cmd shims directly.
- sdk/orval/generate.ts: same Windows wrap for `pnpm exec orval`.

Signed-off-by: mschwab <mschwab@nvidia.com>

* fix: validate format-generated.ts servicePath argv

The Windows cmd.exe /c wrap added in ec7aa93 re-opened a CodeQL
data-flow finding (#3961, #3962) because generatedPath traces back to
process.argv[2]. Validate the argv against a safe-char regex at entry
so CodeQL sees it as sanitized before it flows into argv or paths.

Signed-off-by: mschwab <mschwab@nvidia.com>

* fix: replace regex with hardcoded Set allowlist for servicePath

CodeQL did not recognize the regex check as a sanitizer; switching to a
hardcoded Set lookup against known serviceConfigs paths so the data flow
is reducible to a finite set of literal values.

Signed-off-by: mschwab <mschwab@nvidia.com>

* fix: use prettier Node API instead of subprocess

Replace the prettier CLI invocation with prettier's programmatic
format/resolveConfig/getFileInfo API. No subprocess means no
cmd.exe wrap, no command-line argument flow, and the CodeQL
indirect-command-line-injection / shell-cmd-injection-from-env
alerts on format-generated.ts can resolve. Also fixes the Windows
.cmd shim resolution problem CR raised, since prettier now runs
in-process.

The servicePath argv is still validated against a hardcoded Set
of known serviceConfigs paths to prevent directory traversal via
path.join.

Signed-off-by: mschwab <mschwab@nvidia.com>

* fix: use readdirSync withFileTypes to avoid statSync TOCTOU

CodeQL flagged the statSync -> readFileSync / writeFileSync pair in
formatWithPrettier as a file-system-race. Getting Dirent entries from
readdirSync(dir, { withFileTypes: true }) lets us check isDirectory /
isFile inline without a separate stat round-trip, closing the alert.

Signed-off-by: mschwab <mschwab@nvidia.com>

* fix: drop remaining statSync usages in format-generated.ts

Codex flagged that getTsFiles and splitZodTagFilesIn still used the
readdir-string + statSync pattern, leaving two more file-system-race
sinks even after formatWithPrettier was converted. Switch both to
readdirSync(dir, { withFileTypes: true }) and use Dirent.isFile() /
isDirectory() inline. Removes the last statSync from this script.

Signed-off-by: mschwab <mschwab@nvidia.com>

---------

Signed-off-by: mschwab <mschwab@nvidia.com>
Signed-off-by: Alex Ray <alray@nvidia.com>
@SandyChapman
SandyChapman deleted the schapman/evaluator-agent-more-mvp-tasks branch June 2, 2026 11:58
benmccown added a commit that referenced this pull request Jun 23, 2026
Composition over inheritance for the k8s reconcilers (review #2/#3):
* Extract StatusProjector (pod-status projection, crash-loop/pending-timeout
  error builders, host URL) and ResourceDeleter (idempotent 404-tolerant
  delete) as standalone collaborators.
* Reconciler is now a pure interface (the 5 verbs); NimOperatorReconciler and
  K8sReconciler compose the projector + deleter instead of inheriting them.
  The backend builds both collaborators in init() and injects them.

Thread the reconcile context through the backend interface (review #19):
* create/update/get_model_deployment_status now take a single
  ctx: ModelContext instead of (deployment, config, model_entity); applied
  across the ServiceBackend ABC and the docker / none / k8s backends, the
  deployment reconciler call sites, and the test mocks. delete stays
  (workspace, name).

Fixes + nits:
* Harden NIMService status read against a null status/state (review #15):
  (nim_status.get("state") or "").lower() can no longer raise.
* Convert nim_operator logging to structured extra={} (review #13); avoid the
  reserved LogRecord 'name' key (use resource_name / deployment_name).
* Flatten the Files-service create/update branches into a guard-clause helper
  (review #14).
* compile_puller_job: rename args -> container_args (review #17).
* Reconciler nits: import the vllm_k8s_compiler module under its full name
  (review #7), reflow the P3 (a)/(b) comment (review #8), quote values in the
  model-source error (review #9), drop the _ = image_pull_secrets dance
  (review #12), name the event-message cap MAX_EVENT_MESSAGE_CHARS (review #6),
  and document the _select_reconciler None contract (review #16).

Signed-off-by: Ben McCown <bmccown@nvidia.com>
benmccown added a commit that referenced this pull request Jun 24, 2026
Composition over inheritance for the k8s reconcilers (review #2/#3):
* Extract StatusProjector (pod-status projection, crash-loop/pending-timeout
  error builders, host URL) and ResourceDeleter (idempotent 404-tolerant
  delete) as standalone collaborators.
* Reconciler is now a pure interface (the 5 verbs); NimOperatorReconciler and
  K8sReconciler compose the projector + deleter instead of inheriting them.
  The backend builds both collaborators in init() and injects them.

Thread the reconcile context through the backend interface (review #19):
* create/update/get_model_deployment_status now take a single
  ctx: ModelContext instead of (deployment, config, model_entity); applied
  across the ServiceBackend ABC and the docker / none / k8s backends, the
  deployment reconciler call sites, and the test mocks. delete stays
  (workspace, name).

Fixes + nits:
* Harden NIMService status read against a null status/state (review #15):
  (nim_status.get("state") or "").lower() can no longer raise.
* Convert nim_operator logging to structured extra={} (review #13); avoid the
  reserved LogRecord 'name' key (use resource_name / deployment_name).
* Flatten the Files-service create/update branches into a guard-clause helper
  (review #14).
* compile_puller_job: rename args -> container_args (review #17).
* Reconciler nits: import the vllm_k8s_compiler module under its full name
  (review #7), reflow the P3 (a)/(b) comment (review #8), quote values in the
  model-source error (review #9), drop the _ = image_pull_secrets dance
  (review #12), name the event-message cap MAX_EVENT_MESSAGE_CHARS (review #6),
  and document the _select_reconciler None contract (review #16).

Signed-off-by: Ben McCown <bmccown@nvidia.com>
marcusds added a commit that referenced this pull request Jul 17, 2026
#3: add is_external_agent(agent) (backend, mirrors is_container_deployment_mode)
and isExternalAgent() (frontend helper); replace the 4 backend + 6 frontend
inline `source == "external"` / `source === 'external'` checks so the next
AgentSource value only touches one predicate.

#6: extract the external-agent logs notice into a reusable ExternalAgentNotice
component + a shared EXTERNAL_AGENT_HEADING constant, so the "runs outside NeMo
Platform" phrasing lives in one place.

Signed-off-by: mschwab <mschwab@nvidia.com>
sklinglernv added a commit that referenced this pull request Aug 4, 2026
Nine of sixteen review comments were valid. The two that were real defects:

`log_model_config` guarded the tiers but resolved the endpoint and key unguarded,
so the banner raised on exactly the unconfigured install it exists to diagnose.
Every field is optional there now; callers that need a value still fail in
`api_base()` / `api_key()`.

`asyncio.gather(..., return_exceptions=True)` returns `CancelledError`, which
derives from BaseException, so the `isinstance(r, Exception)` filter classified a
cancelled Coder as neither failed nor succeeded and let the candidate through to
evaluation and ranking as though its source had been written. Cancellation now
re-raises and unwinds the round. Pre-existing on main; fixed here because this
branch already rewrote that block.

Both have regression tests, each verified to fail with its fix reverted.

Also: `candidate_metric_keys` coerced non-strings, so malformed cached metadata
(`[1]`) passed as a valid measurement and skipped a fresh Insight evaluation --
it now reads as "not recorded"; the mirror test asserted only on the entity and
would have passed if `project_candidate` reverted to a fixed channel allowlist,
so it projects through a real mirror now; Eval Author's autouse fixture clears
`Configuration` alongside the client caches, since bridged endpoints outlive
`monkeypatch`; the README example imports `Dataset` from `entities`; a duplicated
env prefix in AGENTS.md; a prompt referring to `insight_reward` where its own
code block defines `insight_rewards`; the nooa rev in
`third_party/requirements-main.txt` and the framework skill; and the api_base
doc row, which claimed "required" while the CLI defaults it to the gateway.

Declined: requiring HTTPS for the API base (#3, #8) would break local endpoints,
and the actual credential-forwarding risk is already scheme-guarded in
`_is_gateway_base`. The dotenv-linter ordering finding (#7) cites a hook this
repo does not configure. The conftest credential-namespace overlap (#4) is real
but pre-existing and identical on main.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Severin Klingler <sklingler@nvidia.com>
ryana pushed a commit to ryana/nemo-platform that referenced this pull request Aug 12, 2026
…erimentalist (NVIDIA-NeMo#1038)

* doc: fix readme steps

Signed-off-by: Severin Klingler <sklingler@nvidia.com>

* refactor(experimentalist): delete the remote backend and dead ABC surface

M1 groundwork for the microkernel refactor: remove seams that never had an
implementation, so the real ones are easier to see.

- Delete `RemoteExperimentalistBackend`. It delegated 100% to the local backend,
  raised `NotImplementedError` for five methods, and was unreachable — the CLI
  rejected `--mode remote` before ever constructing it.
- Drop `--mode` and its plumbing through `cli.py`, `run.py`, and
  `make_experimentalist_backend`, which now always builds the local backend.
- Delete `list_traces`, `get_trace`, `list_scores` and `get_agent` from
  `ExperimentalistBackend`. All four were abstract with no implementation
  anywhere and no caller; ~22% of the ABC was fiction.
- Replace the `LocalExperimentalistBackend.__new__` hack in `WorkspaceTool` with
  a module-level `load_candidate()`. The hack existed only to reach an 8-line
  private deserializer without triggering `__init__`'s directory creation.

No behavior change: every deleted path was unreachable or unimplemented.

Signed-off-by: Severin Klingler <sklingler@nvidia.com>

* refactor(experimentalist): resolve model tiers at construction, not import

Nine agent classes bound an LLM in the class body
(``class X(Agent, llm=get_smart_model())``), and the getters call
``_required_env``. Importing any component module therefore required live
credentials — which blocks registry discovery (it must import component modules
to find them), forced `cli.py` to lazy-import so `doctor` could run, and is the
sole reason Eval Author ships an `_env_bridge` side-effect module.

- Agent classes take ``Agent`` plainly and resolve their tier in ``__init__``,
  via ``kwargs.pop("llm", None) or get_<tier>_model()``. The tier stays the
  default; an explicit ``llm=`` still wins, so components are now injectable.
- Three ``@strategy(..., llm=...)`` method overrides are decorator arguments and
  evaluate at import regardless. They take a `LazyModel` proxy that builds its
  client on first attribute access. nooa accepts the override as ``Any``, stores
  it with ``setattr`` and reads it back with ``getattr`` — there is no
  ``isinstance`` check — so the proxy is transparent; `lazy_model()` carries the
  one documented cast.
- Cache on ``(name, api_base, api_key)`` rather than per-tier with no key, so two
  tiers naming the same model share a client and a changed key is not ignored.
- Same change for `EvalAuthor`, whose class-body binding blocked `loop.py`, which
  imports it at module scope.

Tiers and defaults are unchanged. Covered by a subprocess test asserting that
importing loop, coder and EvalAuthor builds no client — a subprocess because the
client cache is process-global and an earlier test would mask a regression.

Signed-off-by: Severin Klingler <sklingler@nvidia.com>

* refactor(eval-author): delete the _env_bridge side-effect module

`_env_bridge.py` existed for one reason: the Experimentalist agents Eval Author
borrows (TraceAnalyzer, TraceExplorer) built their LLM in the class body, so they
read `EXPERIMENTALIST_*` the moment their module was imported. Bridging therefore
had to happen before an import, which no function call inside the module can do —
hence a module imported purely for its side effect, plus an isort-ordering
constraint documented in three places.

Those agents now resolve their tier when constructed, so `EvalAuthor.__init__`
calls `bridge_author_env_to_experimentalist()` directly and the module is gone.

The `AUTHOR_*` → `EXPERIMENTALIST_*` credential fallback stays. It is a separate
mechanism serving Eval Author's own clients, and per the boundary ratchet it is
removable only once the allowlist is empty — not by this change.

The ordering test becomes a construction test, and clears `EXPERIMENTALIST_*`
inside the subprocess before constructing: importing nooa loads a `.env`, which
would otherwise pre-fill the slots and mask a broken bridge.

Signed-off-by: Severin Klingler <sklingler@nvidia.com>

* refactor(experimentalist): one config tree, and a models: block

`resolve.py` carried a second declaration of six component configs — `CoderConfig`,
`AnalyzerConfig`, `ProposerConfig`, `GoalTreeConfig`, `RationalizerConfig`,
`TraceAnalyzerConfig` — byte-equivalent to the components' own, plus four
`model_validate(x.model_dump())` round trips to convert between them at each use
site. They existed because importing a component module used to require
credentials, so `resolve.py` could not import the real classes. Lazy binding
removed that constraint.

- New `config.py` owns the run-config tree and imports each component slice from
  the component that consumes it. `resolve.py` keeps input resolution only, 131
  lines lighter.
- The four round trips are gone: the tree already holds the right classes.
- Every importer points at `config.py`; no re-export shim.
- Add a `models:` block (`smart`/`mid`/`fast`), mirroring the key the benchmark
  configs already use. `run.py` applies it before any agent is constructed, so a
  config-file choice takes effect; unset tiers keep the environment's value.
  Credentials stay environment-only and are not accepted in config.

Signed-off-by: Severin Klingler <sklingler@nvidia.com>

* refactor(experimentalist): move Layer A entities out of components/evaluator/

`Dataset`, `Task`, `TrialResult`, `EvaluationResult`, `DatasetRef`, `ResourceRef`
and the metric/dependency models lived in
`experimentalist/components/evaluator/models.py` — Layer A filed inside a Layer B
component. They are the contract every plugin speaks, and the evaluator is one
consumer among several, so they now sit in `entities.py` beside `ExperimentRun`
and `Candidate`.

Pure relocation: no field, validator or behaviour changes. 43 files repointed,
no re-export shim.

This also reclassifies the Eval Author boundary ratchet, which previously counted
imports without distinguishing what they were for. It now pins two lists:

- `_SHARED_LAYER_A` — the entity contract. Permanent by design. Duplicating it
  would fork the contract and break Studio comparability, which is the one thing
  the design cannot trade away.
- `_BORROWED_BEHAVIOUR` — Harbor, tools, trace analysis, the backend factory.
  Still debt, still shrink-only, still "duplicate rather than add a row".

Five of the twelve original rows were Layer A and collapse into one shared entry;
the reverse assertion now guards only the debt list, so removing a borrowed module
still fails loudly while sharing entities does not.

Signed-off-by: Severin Klingler <sklingler@nvidia.com>

* refactor(experimentalist): reward channels as an open map on Candidate

`Candidate` carried four reward channels across ten fields — `train_reward`,
`validation_reward`, `insight_reward`, `validation_trajectory_reward`, their
`_details` twins, and two insight-only sidecars — and the channel list was
re-hardcoded in five places: `slim()`, `__repr__`, `ExperimentMirror.SPLITS`,
`_split_reward`, and `insight_promotion`. A strategy with a new reward channel
could not reach Studio without editing our entity, which contradicts the
microkernel's universal-`record_reward` claim.

    rewards: dict[str, RewardRecord]   # channel -> {metrics, summary, trials, metadata}

Keyed by *channel*, not by split: trajectory scoring is a second measurement of
the validation split, so a split-keyed map cannot hold both. Channels today are
`train`, `validation`, `insight` and `validation-trajectory`; adding one now costs
no entity change, and `ExperimentMirror` iterates what was measured instead of an
allowlist.

`RewardRecord.summary` separates a scalar rollup from the dimensions. The
trajectory aggregate previously shared a namespace with goal-tree node ids, so a
node named `aggregate` silently overwrote it — and any selector doing Pareto over
those metrics would have treated the rollup as a dimension dominating every real
one.

Also here: `optimization_params` (the genotype HPO breeds from, absent until now),
the insight sidecars move to the channel's `metadata`, `trajectory_detail` keeps
its own field because its shape is per-node-per-task rather than per-trial, and
the untyped `artifacts` dict is gone.

This changes the on-disk `metadata.json` shape. Alpha software, no migration:
existing experiment directories will not load.

Signed-off-by: Severin Klingler <sklingler@nvidia.com>

* fix(experimentalist): model names are required, with no built-in default

`_DEFAULTS` named `openai/openai/openai/gpt-5.5` and friends. Those are NVIDIA
Inference Gateway routing paths, not portable model ids, so against any other
endpoint they name nothing — and the failure surfaced at the first LLM call as an
opaque provider error rather than a configuration problem.

Gating them on the gateway hostname was the first fix I tried. It worked, but it
added a third copy of `_is_gateway_base` to library code to defend a default that
serves almost nobody: the getting-started guide exports all three tiers
explicitly, `benchmarks/run.py` sets them from its config, and the README table
lists them. Deleting the defaults removes both the wrong values and the branch.

`model_name(tier)` now reads its variable and raises when unset. A model name is
only meaningful against a specific endpoint, so there is no portable value to fall
back to — name them as your endpoint does (`openai/openai/openai/gpt-5-mini` on
the gateway, `gpt-5-mini` against OpenAI directly). The error arrives when the
agent is constructed, before any Docker work.

`log_model_config` shows "(unset)" rather than raising; a display helper should
not be the thing that fails.

The `INFERENCE_API_KEY` → `EXPERIMENTALIST_API_KEY` copy in `cli.py` is untouched.
That one is a scoping check on a secret, not a convenience, and removing it is a
separate decision — tracked as an open question in the plan.

Signed-off-by: Severin Klingler <sklingler@nvidia.com>

* fix(experimentalist): EvolutionNode is not a Candidate

`7493877ff` replaced the `Candidate` reward fields with accessors via a bulk
rename, which also rewrote three call sites in `models.py` whose receiver is an
`EvolutionNode`. The node wraps a candidate and exposes `train_reward` /
`val_reward` / `trajectory_reward` properties that delegate; it has no `metrics()`.
`EvolutionTree.to_markdown_table` and `EvolutionNode.reward_str` therefore raised
`AttributeError` at runtime.

Nothing caught it: neither method had a test, so the whole suite stayed green and
a live run crashed while writing the round report.

Restores the property access and adds coverage for both rendering paths.

Signed-off-by: Severin Klingler <sklingler@nvidia.com>

* refactor(experimentalist): one reward accessor, and channel-agnostic rendering

Two follow-ups to the reward-channel map.

`Candidate.metrics(channel)` / `Candidate.trials(channel)` made `metrics` name two
different levels of the structure — the accessor keyed by *channel*, and
`RewardRecord.metrics` keyed by *dimension* — so `candidate.metrics("train")`
read like an index into a dict that does not exist. One accessor now returns the
record itself:

    candidate.reward("train").metrics    # dimensions
    candidate.reward("train").trials
    "train" in candidate.rewards         # was it measured at all

`EvolutionNode` was the sixth place hardcoding the channel list, and the one the
previous commit's message wrongly claimed to have fixed. It exposed exactly three
named reward properties and `reward_str` / `to_markdown_table` iterated them, so a
new channel reached Studio but stayed invisible in `OPTIMIZATION.md`. The tell was
already there: `insight` has existed for weeks and had no property.

Both now derive from `candidate.rewards`: `reward_str` renders whatever was
measured, and the table's columns are the union of (channel, dimension) across
nodes. The three named properties stay — the terminator and winner selection mean
`validation` specifically, and should say so.

Covered by a test that adds an unknown channel and asserts it appears in both.

Signed-off-by: Severin Klingler <sklingler@nvidia.com>

* refactor(experimentalist): drop two dead Candidate fields

Neither has a reader, and the plan removes both.

`optimization_params` was added by an earlier commit in this branch as HPO's
genotype slot. §3.1 now puts HPO parameters in `parameters.json` inside the
candidate artifact instead, so Layer A never interprets them and `Candidate`
gains no component-specific field.

`artifacts: dict[str, Any]` was the untyped `exclude=True` dict used to pass
directory paths around at runtime. It has no readers left; the three
`model_dump(exclude={"artifacts"})` call sites go with it.

Corrects the record: the commit that introduced the reward map claimed the
`artifacts` dict was "deleted outright". It was not — it survived until now.

Signed-off-by: Severin Klingler <sklingler@nvidia.com>

* refactor(experimentalist): delete _with_reward, use Candidate.set_reward

`_with_reward()` was a second implementation of the channel merge that
`Candidate.set_reward` already performs — same "take the existing record, apply
the parts that were supplied, put it back" logic, written twice.

It existed only because `_update_candidate(candidate, updates={...})` wants a
*value* for the field, while `set_reward` mutates. But `_update_candidate` merely
`setattr`s the updates onto the candidate before persisting, so mutating first is
equivalent:

    candidate.set_reward("validation", metrics=..., trials=...)
    await self._update_candidate(candidate, workspace=..., backend=..., run_id=...)

One merge implementation, on the entity that owns the field, and the five call
sites say what they do instead of assembling a dict to hand to a setattr loop.

Signed-off-by: Severin Klingler <sklingler@nvidia.com>

* refactor(experimentalist): one home for the insight-suite readers

`_suite_identity` and `_metric_keys` were defined identically in both
`loop.py` and `insight_promotion.py` — a duplication I introduced when the
insight sidecars moved from dedicated `Candidate` fields into the `insight`
channel's `metadata`.

They now live once, in `insight_promotion.py`, as `candidate_suite_identity` and
`candidate_metric_keys`, and `loop.py` imports them alongside the other
insight-suite helpers it already takes from there.

Deliberately not methods on `Candidate`. `suite_identity` is the content hash of
the Eval Author's generated suite, used to decide whether a cached insight reward
was measured against the suite currently in play; `metric_keys` are that suite's
validated metric names. Both are insight-suite semantics, and Layer A stores a
channel's `metadata` without interpreting it — the same rule §3.1 applies to HPO's
payload. Putting them on the entity would re-add the component-specific coupling
the channel map removed.

Signed-off-by: Severin Klingler <sklingler@nvidia.com>

* review: address MR feedback on docstrings, config fields, and a duplicate

- `entities.py` docstring drops the "Layer A"/"Layer B" vocabulary and the note
  about where these models used to live. Both only make sense with the design doc
  open, and the file should read on its own.
- `EvolutionaryOptimizerConfig` and `ModelsConfig` fields carry descriptions, so
  the run config is self-documenting.
- `_evaluate_insight_candidates` had `"insight" not in candidate.rewards` twice.
  It came from translating `insight_reward is None or insight_reward_details is
  None` — two fields that became one channel. Removed the duplicate and left a
  note on why one check is now correct: a RewardRecord holds metrics and trials
  together, and empty trials are valid cached state rather than a missing
  measurement.
- The evolution-tree table test asserts the whole rendered table instead of
  scattered substrings.

Signed-off-by: Severin Klingler <sklingler@nvidia.com>

* refactor(experimentalist): adopt NemoConfig for deployment settings

The Experimentalist was the only plugin in the repo configuring itself with a
bare BaseModel and an ad-hoc EXPERIMENTALIST_* prefix; ten others subclass
NemoConfig. Split the configuration into the two kinds it always was, and give
the deployment half the platform's treatment.

ExperimentalistConfig (new, settings.py) holds the endpoint, its credential and
the model tiers. As a NemoConfig it gets the NEMO_EXPERIMENTALIST_ env prefix,
an `experimentalist:` section in the platform config file, and the platform's
precedence: env, then file, then defaults. It lives in its own module rather
than config.py because config.py imports each component's config slice, and
those components resolve models through components/model_config.py -- so the
settings have to sit below the components, not above them.

EvolutionaryOptimizerConfig keeps holding run parameters and stays a plain
BaseModel with no env binding at all. A stale NEMO_EXPERIMENTALIST_MAX_ROUNDS
truncating a run whose --config says 15 would be a bad failure, and it would
make config_snapshot a dishonest record of what ran. Its `models:` key is now
rejected outright rather than silently ignored.

This deletes ModelsConfig.apply_to_env(), which wrote config values into
os.environ so module-level getters could see them -- inverting precedence so
that config silently beat anything the operator had exported.

EXPERIMENTALIST_* becomes NEMO_EXPERIMENTALIST_*, and the tier names become
NEMO_EXPERIMENTALIST_MODELS_{SMART,MID,FAST}. No compatibility shim: this is
pre-1.0, and two accepted names for one variable is worse than one rename.
Eval Author's bridge retargets to the new destinations and now clears the
settings cache after writing, so it is no longer order-dependent.

The test conftest gains placeholder tier names; the suite had been relying on a
developer .env being loaded by the nooa import to supply them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Severin Klingler <sklingler@nvidia.com>

* fix(experimentalist): rename the env vars the onboarding path actually uses

The .env.example the getting-started guide tells you to copy still exported the
old names, so a fresh run of docs/get-started/example-agent.mdx would have found
no model tiers. Dotfiles were missed by the first sweep.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Severin Klingler <sklingler@nvidia.com>

* test(eval-author): pin that bridging survives an already-resolved settings cache

Verified to fail without the Configuration.clear_cache() in the bridge: this is
the ordering that used to silently no-op in a real run, where something resolves
a model before EvalAuthor.__init__ gets to bridge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Severin Klingler <sklingler@nvidia.com>

* docs(experimentalist): add the plugin config to the config reference

Registers ExperimentalistConfig in the generator and regenerates
docs/set-up/config-reference.mdx, so the endpoint and model tiers are
discoverable alongside every other service's settings rather than only in the
plugin README.

Field descriptions are written for that audience: which models these are (the
ones doing the optimizing, not the agent under test) and a pointer to set the
credential through the environment rather than the config file.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Severin Klingler <sklingler@nvidia.com>

* docs(experimentalist): give the tiers different models in the README example

The example exported the same model for smart, mid and fast, which makes the
tiering look pointless — the reviewer reasonably asked whether smart was meant
to be smarter. Use the same spread the example agent's .env.example already
uses, and say what the tiers are buying.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Severin Klingler <sklingler@nvidia.com>

* fix(experimentalist): log why a candidate build failed, not just that it did

`return_exceptions=True` turns a build failure into a value rather than a
raise, so the exception was discarded and the log said only "Impl failed:
agent-1". That is not diagnosable after the fact, and a killed candidate is the
one thing a run cannot cheaply reproduce. Keep the exception and log it with a
traceback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Severin Klingler <sklingler@nvidia.com>

* fix(eval-author): drop the mode argument left dangling by the backend removal

Deleting the remote backend removed `mode` from `make_experimentalist_backend`,
but `run_eval_author` still passed `mode=mode` — a TypeError on every insight-mode
run. `mode` had no other use, so the parameter goes too.

The unit test hid this: its fake backend factory declared `mode` and accepted the
argument, so 646 tests stayed green over a call that could not work. The fake now
mirrors the real signature. CI's `lint-python-types` caught it because
`unknown-argument` is not in its suppression list, while the pre-commit `ty` hook
runs without the insights/experimentalist dependency groups and never resolved it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Severin Klingler <sklingler@nvidia.com>

* fix(experimentalist): repoint the example trace recorder at the moved entities

Moving Layer A out of components/evaluator/ orphaned the import in
record_tau_airline_traces.py, which is step 3 of the getting-started guide — a
new user following the guide hit ModuleNotFoundError before recording anything.
The same stale path appeared in Eval Author's README.

Nothing caught this: plugins/nemo-experimentalist/examples/ is in ty's exclude
list and no test imports the script, so a green test suite and a green `Lint all`
said nothing about it. Found by actually running the guide.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Severin Klingler <sklingler@nvidia.com>

* refactor(experimentalist): a method's tier reads off the instance, not the env

Three `@strategy` methods pick a non-default tier. They did it with `LazyModel`,
a proxy resolving from the environment at first use, because a decorator argument
is evaluated at import time and there is no instance to read config from then.
That is the same import-time binding this milestone set out to remove -- deferred
rather than eliminated -- and it is why the environment is still a transport.

nooa now accepts a callable for `@strategy(llm=...)`, resolved on each generation
call with the agent in scope, so the tier reads off the instance like every other
model these components use.

The dependency bump is not separable from the removal. nooa duck-type-probes the
`llm=` value for `acall`; on a forwarding proxy that fires `__getattr__` and
builds a client at import, breaking the guarantee that importing a component
needs no credentials -- which registry discovery and `doctor` depend on, and
which this milestone's own test asserts. That test catches it immediately.

The pin moves to the current nooa main. It stays a commit rather than a tag
because v0.0.8 predates the support; the comment above it now says so instead of
citing the MCP fix it was originally written for. Both lock files move with it.

Also pins what the same upstream change fixed in passing: `llm=` was stripped
from call kwargs only for `_session_locals`, so the per-call precedence
`actor.py` documents was unreachable for a direct call. The tier choice does not
use it, but a regression there would be silent.

The ty override that exempts nooa's ellipsis-bodied methods gains this one test
module, which defines throwaway agents for the same reason.

Signed-off-by: Severin Klingler <sklingler@nvidia.com>

* review: address bot findings on the banner, cancellation, and stale pins

Nine of sixteen review comments were valid. The two that were real defects:

`log_model_config` guarded the tiers but resolved the endpoint and key unguarded,
so the banner raised on exactly the unconfigured install it exists to diagnose.
Every field is optional there now; callers that need a value still fail in
`api_base()` / `api_key()`.

`asyncio.gather(..., return_exceptions=True)` returns `CancelledError`, which
derives from BaseException, so the `isinstance(r, Exception)` filter classified a
cancelled Coder as neither failed nor succeeded and let the candidate through to
evaluation and ranking as though its source had been written. Cancellation now
re-raises and unwinds the round. Pre-existing on main; fixed here because this
branch already rewrote that block.

Both have regression tests, each verified to fail with its fix reverted.

Also: `candidate_metric_keys` coerced non-strings, so malformed cached metadata
(`[1]`) passed as a valid measurement and skipped a fresh Insight evaluation --
it now reads as "not recorded"; the mirror test asserted only on the entity and
would have passed if `project_candidate` reverted to a fixed channel allowlist,
so it projects through a real mirror now; Eval Author's autouse fixture clears
`Configuration` alongside the client caches, since bridged endpoints outlive
`monkeypatch`; the README example imports `Dataset` from `entities`; a duplicated
env prefix in AGENTS.md; a prompt referring to `insight_reward` where its own
code block defines `insight_rewards`; the nooa rev in
`third_party/requirements-main.txt` and the framework skill; and the api_base
doc row, which claimed "required" while the CLI defaults it to the gateway.

Declined: requiring HTTPS for the API base (NVIDIA-NeMo#3, NVIDIA-NeMo#8) would break local endpoints,
and the actual credential-forwarding risk is already scheme-guarded in
`_is_gateway_base`. The dotenv-linter ordering finding (NVIDIA-NeMo#7) cites a hook this
repo does not configure. The conftest credential-namespace overlap (NVIDIA-NeMo#4) is real
but pre-existing and identical on main.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Severin Klingler <sklingler@nvidia.com>

* docs(experimentalist): drop the stale import-time credential claim

The comment said importing experimentalist.run needs NEMO_EXPERIMENTALIST_API_*
at import time. That stopped being true in this branch -- components resolve
their tier when constructed, not imported, which is what
test_importing_components_resolves_no_model asserts. Verified: with every
credential and tier variable unset, the module imports and builds zero clients.

It survived because the rename rewrote the variable names in it instead of
noticing the sentence was obsolete.

The lazy import stays, but for import cost rather than credentials, and it only
defers ~85 ms since cli.py already pulls in most of the same chain. One line is
enough; the mechanism goes away in M1 when the runner collapses the _flow
closure.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Severin Klingler <sklingler@nvidia.com>

* refactor(experimentalist): one way to read a channel, and an honest name for the writer

Two spellings of the same read existed: `reward(ch)` and, twice in
insight_promotion.py, `rewards.get(ch, RewardRecord())`. Collapsed to the former;
its `RewardRecord` import is now dead and goes too.

`set_reward` merges — an argument left as None keeps what the channel already
holds — so `set_` was the wrong verb. It is `record_reward` now. Keeping the
merge rather than dropping it to a replace: every caller writes a channel exactly
once today, so nothing exercises it, but the channel set is open by design and a
second writer adding metadata to a channel another path measured would silently
lose its metrics. That is the kind of loss no test catches, so the defensive
default is worth the unused branch. Two tests pin it, plus the unmeasured-versus-
measured-empty distinction that eight evaluation gates depend on.

`rewards` stays public: it is the serialized key in metadata.json and the entity
store, `channel in rewards` is load-bearing at those eight sites and cannot be
expressed through the accessor, and there is no cross-channel invariant to
protect.

Collapsing the two readers into one mapping whose `__missing__` returns without
storing is the better shape, but it needs a Pydantic core schema (a bare dict
subclass is rejected) and touches ~55 sites, so it is noted in M1 alongside the
lifecycle change that removes the merge's reason to exist. Explicitly not a
defaultdict, whose `__missing__` inserts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Severin Klingler <sklingler@nvidia.com>

* test(experimentalist): drop the nooa contract tests and their ty override

test_method_llm_injection.py defined throwaway agents to re-derive nooa's own
`@strategy(llm=...)` resolution. Three of its four cases pinned behaviour this
plugin does not use -- the default fallback, the baked-on decorator client the
callable replaced, and a call-level override its own docstring called "not what
the tier choice will use". Only the callable case matched a production site, and
pinning a dependency's API surface is not this repo's job. The docstrings also
cited an external PR number and another repo's `actor.py`, which rot on the next
pin bump.

Its pyproject ty override goes with it: the `empty-body` exemption is back to the
two source trees, since the entry existed only for that module's ellipsis-bodied
throwaways.

The gap it half-covered is real -- test_experimentalist_analyzer.py builds its
analyzer with object.__new__ and assigns over the strategy methods, so nothing
proves a method-level tier reaches the model it names. Recorded in M1, where
config injection makes it testable against a real component instead of a
stand-in.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Severin Klingler <sklingler@nvidia.com>

* test(experimentalist): inject settings instead of driving them through env

Most of test_model_config.py set NEMO_EXPERIMENTALIST_* to get a value in place.
Only two tests are actually about *where* a value comes from -- the config-file
one and the env-overrides-file one -- so the rest now install an
ExperimentalistConfig with Configuration.set_override and stop caring about the
environment entirely. That also settles the isolation concern raised on the
config-file test: an override bypasses file loading, so those tests no longer
depend on whether /etc/nmp/config.yaml happens to exist on the machine.

The helper clears the plugin's env first, and that is load-bearing rather than
tidiness: these settings are env-first by design, so constructing
ExperimentalistConfig(models=...) would otherwise read conftest's import-time
placeholders over the values the test passes. The autouse fixture snapshots and
restores os.environ, and now clears Configuration overrides too, since an
override outlives the test that set it exactly as a cached config does.

Also drops _CHANNEL_ABBREV. It shortened two of three channel names -- "train"
mapped to itself -- and was a hardcoded closed list of channels in a change whose
point is that the channel set is open. Since it fell back to the raw name for
anything it did not know, tables already mixed abbreviated and full names as soon
as a third party added a channel. Rendering every channel by its own name is
wider and consistent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Severin Klingler <sklingler@nvidia.com>

---------

Signed-off-by: Severin Klingler <sklingler@nvidia.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants