Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 17 additions & 11 deletions .agents/scripts/nel-next.sh
Original file line number Diff line number Diff line change
Expand Up @@ -32,21 +32,27 @@
# using the config's export_config.mlflow (resolves ${MLFLOW_TRACKING_URI}, forces
# emit_traces=false to avoid the per-sample hang). Run after `source .env`.
#
# Install source (env overrides): NEL_NEXT_SPEC (PyPI default, "nemo-evaluator[harbor,export]==0.3.*"
# — [export] pulls mlflow for mlflow-push; pin an exact 0.3.x here for reproducibility), or
# NEL_NEXT_ORIGIN [+ NEL_NEXT_REF] for the internal git build. uv caches the resolved env and
# refreshes it when the spec changes.
# Install source: NEL_NEXT_ORIGIN + NEL_NEXT_REF git build (default), or NEL_NEXT_SPEC
# to force a PyPI release. Upstream `main` is 0.4.0 and ships the vendored TB 2.1 registry
# override; PyPI stops at 0.3.0, so a 0.4.x pin there resolves to nothing. No v0.4.0 tag
# exists, so NEL_NEXT_REF defaults to a commit SHA — an unpinned branch HEAD would install
# a different harness for the baseline and the candidate run, folding a harness change into
# the pass@1 delta. NEL_NEXT_REF=main tracks HEAD (dev/canary only); bump the default below
# after re-validating a canary. Override NEL_NEXT_ORIGIN in `.env` to build from a mirror.
# `--version` prints the resolved spec — record it alongside scored results.
set -euo pipefail

# [harbor] = agentic/sandbox deps; [export] pulls mlflow for `mlflow-push`.
NEL_NEXT_SPEC="${NEL_NEXT_SPEC:-nemo-evaluator[harbor,export]==0.3.*}"
NEL_NEXT_ORIGIN="${NEL_NEXT_ORIGIN:-}"
NEL_NEXT_REF="${NEL_NEXT_REF:-}"
NEL_NEXT_SPEC="${NEL_NEXT_SPEC:-}"
NEL_NEXT_ORIGIN="${NEL_NEXT_ORIGIN:-git+https://github.com/NVIDIA-NeMo/Evaluator.git}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[IMPORTANT Compatibility] The new default install is an unpinned git branch, which is weaker pinning than what it replaces and undercuts this PR's own goal.

Before: NEL_NEXT_SPEC defaulted to nemo-evaluator[harbor,export]==0.3.* — a version-constrained resolve that a user could tighten to an exact 0.3.x.
After: NEL_NEXT_ORIGIN defaults to git+https://github.com/NVIDIA-NeMo/Evaluator.git with NEL_NEXT_REF empty, so INSTALL_SPEC resolves to whatever the default branch HEAD happens to be.

Why it matters for this specific script:

  1. INSTALL_SPEC is now a constant string across upstream commits. The header's premise — "uv caches the resolved env and refreshes it when the spec changes" — no longer holds: the spec never changes while HEAD moves. uv caches the resolved git commit, so machine A (first run in June) and machine B (first run in August) silently run different toolchains under an identical spec, and neither run needs --refresh to notice.
  2. --version cannot disambiguate. The PR body notes there is no v0.4.0 tag, so nemo_evaluator.__version__ prints 0.4.0 for every commit on the branch. There is no way for a user (or an agent following the skill) to answer "which build produced this score."
  3. The validated toolchain is a specific SHA. The PR's own testing section cites Evaluator.git@9dcca2ae as the build that matches golden, and the header tells the user to "set NEL_NEXT_REF to a commit SHA to pin" — but the shipped default does not do that, so the default path is the unreproducible one.

For a script whose entire purpose is producing benchmark numbers that are comparable across runs and across the BF16/NVFP4 sides of a comparison, the default should be the reproducible build.

Suggested fix — default NEL_NEXT_REF to the validated SHA, keeping both override paths intact:

NEL_NEXT_SPEC="${NEL_NEXT_SPEC:-}"
NEL_NEXT_ORIGIN="${NEL_NEXT_ORIGIN:-git+https://github.com/NVIDIA-NeMo/Evaluator.git}"
# Pinned to the build golden runs on (0.4.0, no tag exists). Set to a branch name
# (e.g. main) to track upstream, at the cost of reproducibility.
NEL_NEXT_REF="${NEL_NEXT_REF:-9dcca2ae}"

and update the header + references/nel-next.md to state the default is pinned rather than instructing the user to pin it themselves. If tracking HEAD is genuinely intended, then --version / --which should surface the resolved commit so a run is at least attributable after the fact.

Secondary, smaller point in the same hunk: precedence between the two variables inverted (NEL_NEXT_ORIGIN used to win when both were set; now NEL_NEXT_SPEC does). That is intentional per the comment, but anyone with both already in their .env flips from a git build to a PyPI build with no signal — worth a line in references/nel-next.md.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bot comment.

The default git origin has no ref, so the default install tracks upstream main. Two problems for a scoring toolchain:

  1. Reproducibility — the docs you added in this PR say "No v0.4.0 tag exists — pin NEL_NEXT_REF to a commit SHA for reproducibility", but the shipped default doesn't pin. INSTALL_SPEC is also the uv cache key, and it never changes as upstream moves, so the resolved environment depends on when the cache was populated — different people can score the same benchmark on different toolchains without noticing.
  2. Supply chain--setup-only now builds and runs whatever is currently on a public default branch by default.

You already validated Evaluator.git@9dcca2ae; suggest making that the default (NEL_NEXT_REF="${NEL_NEXT_REF:-9dcca2ae...}") so the tested toolchain is what users get, with the env var still available to move forward.

# Reproducibility pin — NVIDIA-NeMo/Evaluator main @ 2026-08-04 (nemo-evaluator 0.4.0).
NEL_NEXT_REF="${NEL_NEXT_REF:-4d081325170aababd0c8f27c58bed31a81ce82ac}"

if [[ -n "$NEL_NEXT_ORIGIN" ]]; then
INSTALL_SPEC="nemo-evaluator[harbor,export] @ ${NEL_NEXT_ORIGIN}${NEL_NEXT_REF:+@${NEL_NEXT_REF}}"
else
# NEL_NEXT_SPEC wins when explicitly set (PyPI escape hatch); otherwise use the git origin.
if [[ -n "$NEL_NEXT_SPEC" ]]; then
INSTALL_SPEC="$NEL_NEXT_SPEC"
else
INSTALL_SPEC="nemo-evaluator[harbor,export] @ ${NEL_NEXT_ORIGIN}${NEL_NEXT_REF:+@${NEL_NEXT_REF}}"
fi

_log() { printf '\033[2m %s\033[0m\n' "$*" >&2; }
Expand Down Expand Up @@ -128,7 +134,7 @@ command -v uvx >/dev/null 2>&1 || { echo "ERROR: 'uvx' not found (curl -LsSf htt
case "${1:-}" in
--setup-only) _uvx nel --version >/dev/null 2>&1 && _log "nel-next ready — ${INSTALL_SPEC}"; exit 0 ;;
--which) echo "uvx --python 3.12 --from \"${INSTALL_SPEC}\" nel"; exit 0 ;;
--version) _uvx python -c 'import nemo_evaluator; print(nemo_evaluator.__version__)'; exit 0 ;;
--version) _uvx python -c 'import nemo_evaluator; print(nemo_evaluator.__version__)'; _log "from ${INSTALL_SPEC}"; exit 0 ;;
mlflow-push) _mlflow_push "${@:2}"; exit $? ;;
"") echo "ERROR: no args. Try: nel-next.sh eval run <config.yaml> [--dry-run] (or --help)" >&2; exit 2 ;;
esac
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,18 @@ services:
- ???:/cache/huggingface
generation: {temperature: 1.0, top_p: 0.95} # from model card (reasoning mode); adjust per card — mandatory lookup (references/model-card-research.md), same as 0.2.6
proxy:
request_timeout: 1800
request_timeout: 3600 # canonical; MUST be >= benchmarks[].solver.agent_kwargs.llm_kwargs.timeout
extra_body: {skip_special_tokens: false} # add model-card sampling extras here if the card specifies them; mirror them in the export tags below
interceptors:
- name: drop_params # agents send max_tokens; many servers reject it
config: {params: [max_tokens, max_completion_tokens]}
# last two are sent by the 0.5.x harbor eval image; vLLM 400s on them unless stripped
config: {params: [max_tokens, max_completion_tokens, max_input_tokens_per_task, no_rebuild]}
# SWE-bench (OpenHands, multi-turn) adds turn_counter + consolidate_system + a system_message — see swebench_verified.md
# FEP-1104/1120 diagnostics — uncomment for a CANARY/debug run, drop it for the scored run:
# first_n caps only 200s, so every error pair (full req+res bodies) is retained in memory for
# the whole run and re-serialized on each write — unbounded growth exactly when the server errors.
# - name: http_pairs_dump # canonical LAST in the chain (SWE-bench: first)
# config: {dump_path: "$${NEL_OUTPUT_DIR}/http_pairs_metrics.json", first_n: 50} # $$ defers expansion to run time
node_pool: gpu

benchmarks:
Expand All @@ -72,7 +78,7 @@ cluster:
account: ???
walltime: "04:00:00" # auto_resume chains across windows
shards: 1 # N = N nodes (each redeploys vLLM)
eval_image: ${NEL_NEXT_EVAL_IMAGE} # from eval-config (TB2.1 needs ≥0.3.1.1-harbor, multi-arch; enroot creds per SKILL Step 7.5)
eval_image: ${NEL_NEXT_EVAL_IMAGE} # from eval-config (0.5.0.1-harbor, multi-arch; enroot creds per SKILL Step 7.5)
sbatch_comment: '{"OccupiedIdleGPUsJobReaper":{"exemptIdleTimeMins":"480","reason":"benchmarking","description":"nel-next agentic eval"}}'
sbatch_extra_flags: {switches: 1, exclusive: true}
container_env: # AWS creds reach the eval container ONLY via here
Expand Down Expand Up @@ -100,7 +106,7 @@ output:
experiment_name: ??? # <user>/<model>-<benchmark> (hardcode; ${USER}=root in-container)
log_config_params: true
copy_logs: true
exclude_patterns: ["shard*"]
exclude_patterns: ["shard*", "model_traffic.jsonl"] # captured request bodies (FEA-224) stay in the run dir
description: ??? # '<model> | T=1.0 top_p=0.95 | <benchmark> (timeout_strategy=…) | r8'
# model/checkpoint_path/benchmark drive dashboard attribution (engine logs only a generic metric key); temperature/top_p mirror generation above.
tags: {framework: vllm, model: "???", checkpoint_path: "???", benchmark: "???", temperature: '1.0', top_p: '0.95'}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ run flow). Same harbor/ECS-Fargate flow as Terminal-Bench; the deltas are the
**OpenHands agent**, a larger problem set, longer timeouts, and a different
ECR/region. Start from `recipes/examples/example_eval_next.yaml`.

> **Source of truth:** `configs/benchmarks/nel_next/swebench_verified/bench.yaml`
> in nvidia-eval-factory-benchmarking — match its values for a reference run.
> **Source of truth:** `configs/benchmarks/swe-bench-verified/bench.yaml` in
> nvidia-eval-factory-benchmarking (`dl/JoC/competitive_evaluation/…`), with the eval-image
> pin in `configs/shared/nel_next_containers.yaml` — match its values for a reference run.

## Task-specific values (canonical `bench.yaml`)

Expand All @@ -15,20 +16,24 @@ ECR/region. Start from `recipes/examples/example_eval_next.yaml`.
| `playbook` | `swebench_verified` (`harbor://swebench-verified@1.0`) |
| agent | `openhands-sdk` (playbook; `agent_kwargs: {max_iterations: 200, version: "1.17.0"}`) |
| scope | 500 Python tasks × `repeats: 5` |
| `max_concurrent` / `sandbox.concurrency` | `15` |
| `max_concurrent` / `sandbox.concurrency` | `15` in `bench.yaml`; per-model configs override it (MiniMax-M2.7 uses `20`) |
| `solver` | `timeout_strategy: max`, `run_timeout: 10800` (3h), `agent_kwargs.llm_kwargs.timeout: 3600` |
| `sandbox.region` | `us-east-2` |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION] Two small consistency issues in this table row and the block below it.

  1. The row now says 15 in bench.yaml but "per-model configs override it (MiniMax-M2.7 uses 20)", while the YAML at line 33 still hardcodes max_concurrent: 15 with no note. Since sandbox.concurrency must track max_concurrent (the TB2.1 example flags keep == sandbox.concurrency), a reader who bumps one per the table may not bump the other. A trailing comment on line 33 would close it.

  2. The new Sharding section recommends shards: 10 at concurrency: 15 → 150 live Fargate sandboxes, whereas TB2.1's parallel section recommends shards: 4 at concurrency: 50 → 200. Both then say "check N × concurrency against the Fargate quota" without giving the quota. Naming the actual limit once (in references/nel-next.md, since it's account-level rather than per-benchmark) would make both checks performable instead of advisory.

| `sandbox.ecr_repository` | `${HARBOR_SWEBENCH_ECR_REPOSITORY}` (dedicated `harbor-swebench` repo, **us-west-2**, regardless of sandbox region) |
| `cluster.eval_image` | `${NEL_NEXT_EVAL_IMAGE}` (needs **`0.3.1.1-harbor`** — FEP-1085 reasoning fix) |
| `cluster.eval_image` | `${NEL_NEXT_EVAL_IMAGE}` **`0.5.0.1-harbor`** (same pin as TB2.1: `configs/shared/nel_next_containers.yaml`) |
| `cluster.container_env.AWS_DEFAULT_REGION` | `us-east-2` (match `sandbox.region`) |
| `instruction_template` | **must be MOUNTED** — the harbor image doesn't bundle the built-in (gotcha below) |
| `instruction_template` | `/configs/prompts/swebench_instruction.md`, **must be MOUNTED**; content is scoring-relevant (gotcha below) |
| `proxy.request_timeout` | `3600` (FEP-1104 paired HTTP timeout; leaves mirror it on the service proxy) |
| `drop_params` | `max_tokens`, `max_completion_tokens`, `max_input_tokens_per_task`, `no_rebuild` |
| `output.export_config.mlflow.exclude_patterns` | `["shard*", "model_traffic.jsonl"]` |
| `system_message` | `strategy: replace` + the OpenHands prompt from `bench.yaml` (verbatim) — scoring-relevant |

```yaml
benchmarks:
- playbook: swebench_verified
repeats: 5
max_concurrent: 15
instruction_template: /configs/swebench-instruction.md # mounted (see gotcha)
instruction_template: /configs/prompts/swebench_instruction.md # mounted (see gotcha)
solver:
service: <svc-name>
timeout_strategy: max # canonical; "task" = leaderboard-comparable
Expand All @@ -45,41 +50,65 @@ benchmarks:

The playbook defaults `instruction_template: swebench-instruction.md`, but the
harbor image doesn't ship that built-in → run dies at finalize with
`FileNotFoundError: instruction_template not found`. Mount it (the canonical
config uses the compeval OpenHands prompt; the public built-in from the host venv
works too):
`FileNotFoundError: instruction_template not found`. So it must be mounted.

**Which file you mount changes the score.** Mount the canonical `swebench_instruction.md`
(underscore) at `/configs/prompts/swebench_instruction.md`, taken from the reference config or
run dir. The built-in in the `nemo_evaluator/templates/` venv directory is a **different
prompt** (`swebench-instruction.md`, hyphen) — it runs, but results are not comparable. Keep
whichever you use fixed across both sides of a comparison.

```bash
VENV="${NEL_NEXT_VENV:-$HOME/.local/share/nel/venvs/nel-next}" # same default as nel-next.sh (NEL_NEXT_VENV may be unset)
cp "$VENV/lib/python3.12/site-packages/nemo_evaluator/templates/swebench-instruction.md" /tmp/
ssh <login> 'mkdir -p <lustre>/<user>/prompts' && scp /tmp/swebench-instruction.md <login>:<lustre>/<user>/prompts/
ssh <login> 'mkdir -p <lustre>/<user>/prompts'
Comment on lines +54 to +62

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION] The rewritten gotcha loses the one thing that made the old version actionable: where to get the file.

The previous text gave a concrete cp from the venv ($VENV/lib/python3.12/site-packages/nemo_evaluator/templates/swebench-instruction.md). The new text correctly explains that the built-in is a different prompt and not comparable — good, that's a real scoring trap — but then the scp on line 64 sources a bare swebench_instruction.md from the caller's cwd, with the provenance only as "taken from the reference config or run dir." An agent following this recipe has no path to fetch, and the instruction "verify sha256 against the source" names no source to verify against.

The two other source-of-truth pointers in this file are precise (configs/benchmarks/swe-bench-verified/bench.yaml, configs/shared/nel_next_containers.yaml), so this one stands out. Suggest naming the repo-relative path the same way, e.g.:

# canonical prompt: configs/prompts/swebench_instruction.md in nvidia-eval-factory-benchmarking
# (dl/JoC/competitive_evaluation/…) — record its sha256 alongside the score
ssh <login> 'mkdir -p <lustre>/<user>/prompts'
scp swebench_instruction.md <login>:<lustre>/<user>/prompts/

Also worth stating explicitly that both sides of a BF16-vs-quantized comparison must mount the same file — the current "Keep whichever you use fixed across both sides of a comparison" implies it, but this is the highest-leverage sentence in the section.

scp swebench_instruction.md <login>:<lustre>/<user>/prompts/ # canonical file; verify sha256 against the source
```

```yaml
benchmarks: [{playbook: swebench_verified, instruction_template: /configs/swebench-instruction.md}]
benchmarks: [{playbook: swebench_verified, instruction_template: /configs/prompts/swebench_instruction.md}]
cluster:
container_mounts: ["<lustre>/<user>/prompts/swebench-instruction.md:/configs/swebench-instruction.md:ro"]
container_mounts: ["<lustre>/<user>/prompts/swebench_instruction.md:/configs/prompts/swebench_instruction.md:ro"]
```
Comment on lines 66 to 70

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 8 \
  'container_mounts|^services:|^cluster:' \
  .agents/skills/evaluation/references/nel-next.md \
  .agents/skills/evaluation/recipes/examples/example_eval_next.yaml \
  .agents/skills/evaluation/recipes/tasks/aa_next/swebench_verified.md

Repository: NVIDIA/Model-Optimizer

Length of output: 13010


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- relevant files ---'
git ls-files '.agents/skills/evaluation' | sed -n '1,160p'

printf '%s\n' '--- mount and eval-image references ---'
rg -n -C 4 \
  'container_mounts|eval_image|instruction_template|eval.image|mount' \
  .agents/skills/evaluation \
  --glob '*.md' --glob '*.yaml' --glob '*.yml' --glob '*.py' --glob '*.sh' \
  | sed -n '1,260p'

printf '%s\n' '--- recipe context ---'
cat -n .agents/skills/evaluation/recipes/tasks/aa_next/swebench_verified.md | sed -n '45,90p'

Repository: NVIDIA/Model-Optimizer

Length of output: 34115


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- NEL schema reference ---'
cat -n .agents/skills/evaluation/references/nel-next.md | sed -n '58,125p'

printf '%s\n' '--- all repository references to cluster.container_mounts and evaluation mounts ---'
rg -n -C 5 \
  'cluster\.container_mounts|container_mounts|mounts:|eval_image|evaluation:|eval-image|eval image' \
  . \
  --glob '!*.lock' --glob '!*.jsonl' \
  | sed -n '1,360p'

printf '%s\n' '--- possible schema/parser sources ---'
rg -n -C 3 \
  'extra="forbid"|extra.*forbid|class .*Config|container_mounts|eval_image' \
  . \
  --glob '*.py' --glob '*.pyi' --glob '*.yaml' --glob '*.yml' --glob '*.toml' \
  | sed -n '1,360p'

Repository: NVIDIA/Model-Optimizer

Length of output: 50380


🌐 Web query:

NEL next configuration schema eval_image container_mounts evaluation container mounts nemo evaluator

💡 Result:

In the NVIDIA NeMo Evaluator (including configurations used with NeMo Evaluator Launcher/NEL), the eval_image and container_mounts parameters are primary configuration options used when deploying evaluation jobs to containerized environments, such as SLURM clusters or Docker-based executors [1][2]. 1. eval_image: This parameter specifies the container image (e.g., Apptainer, Enroot, or Docker) used to execute the evaluation harness [1][2]. It provides the isolated environment where the evaluation code runs [3]. In some advanced workflows, this can be dynamically set via environment variables (e.g., ${NEL_NEXT_EVAL_IMAGE}) to ensure the correct architecture-matched or version-pinned image is used [4]. 2. container_mounts: This parameter accepts a list of host paths to mount into the evaluation container [1][2]. It is essential for providing the containerized environment access to required data on the host machine, such as: - Datasets (if they are not automatically managed by the task's dataset directory mounting feature) [5][6] - Caching directories (e.g., for vLLM or HuggingFace) [4] - Configuration files [4] - Results output directories [7] Usage Contexts: - SLURM Deployment: Both parameters are typically defined under the cluster configuration block (e.g., cluster.eval_image and cluster.container_mounts) [1][2]. - NeMo Run: When using NeMo Run, these are set as properties on the executor object (e.g., executor.container_image and executor.container_mounts) [8]. - Dataset-Specific Mounting: Beyond general container mounts, tasks that require specific local datasets can also use the dataset_dir and optional dataset_mount_path fields within the evaluation task configuration [5][6]. The system automatically handles these mounts and sets the corresponding NEMO_EVALUATOR_DATASET_DIR environment variable [5][6]. For further details on specific schema structures, users should refer to the official documentation for the respective version of the NeMo Evaluator SDK, as configurations may vary between stable releases and development branches [5][9][10].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- official SLURM documentation ---'
for url in \
  'https://docs.nvidia.com/nemo/evaluator/deployment/slurm' \
  'https://docs.nvidia.com/nemo/evaluator/nightly/deployment/slurm' \
  'https://docs.nvidia.com/nemo/evaluator/latest/libraries/nemo-evaluator-launcher/configuration/executors/slurm.html'
do
  printf '\nURL: %s\n' "$url"
  curl -L --fail --silent --show-error "$url" |
    tr '\n' ' ' |
    sed 's/<[^>]*>/ /g' |
    grep -oE '.{0,180}(container_mounts|eval_image|eval image|mounts).{0,260}' |
    head -n 12 || true
done

printf '%s\n' '--- referenced repository commit ---'
if git cat-file -e a05850bffad6f254f265d7e30b46823a6064d8f7^{commit} 2>/dev/null; then
  git show --stat --oneline a05850bffad6f254f265d7e30b46823a6064d8f7
  git show a05850bffad6f254f265d7e30b46823a6064d8f7 -- \
    '.agents/skills/evaluation/references/nel-next.md' \
    '.agents/skills/evaluation/recipes/tasks/aa_next/swebench_verified.md' \
    | sed -n '1,260p'
else
  echo 'commit not present locally'
fi

Repository: NVIDIA/Model-Optimizer

Length of output: 18435


Document cluster.container_mounts in the NEL schema reference.

cluster.container_mounts is supported for NEL SLURM jobs and mounts files into the evaluator container. The recipe does not need a service-level mount. Update references/nel-next.md to list this field under cluster and clarify its target.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.agents/skills/evaluation/recipes/tasks/aa_next/swebench_verified.md around
lines 66 - 70, Update the NEL schema reference in references/nel-next.md to
document cluster.container_mounts under the cluster configuration, noting that
it mounts files into the evaluator container for NEL SLURM jobs. Do not add a
service-level mount or alter the recipe configuration.


### Deployment proxy (multi-turn agentic)

OpenHands runs ~200 turns/task. The canonical config adds a `system_message`
interceptor (a large OpenHands system prompt — copy it verbatim from `bench.yaml`)
plus `turn_counter`. Full stack on `services.<svc>.proxy.interceptors`:
plus `turn_counter`.

**Order differs from TB2.1**: `http_pairs_dump` is **first** (not last) and `drop_params`
comes **before** `consolidate_system`. `http_pairs_dump` is canary/diagnostic-only — it
retains every error pair in memory for the whole run (`references/nel-next.md`); drop it
from the scored config.

```yaml
proxy:
request_timeout: 3600
extra_body: {skip_special_tokens: false} # add model-card sampling extras if the card sets them
model_traffic: {capture_request_body: true} # FEA-224; pair with the exclude_patterns entry

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "pair with the exclude_patterns entry" framing reads as if the exclude is only needed because of this SWE-bench opt-in, but the capture is on by default.

In 0.4.0 ModelTrafficCaptureConfig (config/services.py:64-78) defaults:

capture_tool_calls: bool = True
capture_reasoning: bool = True
capture_messages: bool = True
capture_request_body: bool = False   # <- the only opt-in
max_content_chars: int = 0           # 0 = no truncation

So model_traffic.jsonl — assistant messages, reasoning content and full tool-call payloads — is written on every nel-next run; capture_request_body: true only adds the upstream request body on top. A TB2.1 user who isn't setting capture_request_body can read this note and conclude the exclude_patterns entry is unnecessary for them, which pushes captured model traffic to MLflow.

Suggest making the exclude unconditional in references/nel-next.md ("model traffic is captured by default, so the exclude is always required") and rewording this to "adds the upstream request body to the traffic capture that is already on by default." max_content_chars is also worth a mention as the truncation knob, since there is currently no documented way to turn the capture down.

interceptors:
# - {name: http_pairs_dump, config: {dump_path: "$${NEL_OUTPUT_DIR}/http_pairs_metrics.json", first_n: 50}} # canary only
- {name: system_message, config: {strategy: replace, system_message: "<the OpenHands prompt from bench.yaml>"}}
- {name: turn_counter, config: {max_turns: 200}}
- {name: turn_counter, config: {max_turns: 200, position: system_message}}
- {name: drop_params, config: {params: [max_tokens, max_completion_tokens, max_input_tokens_per_task, no_rebuild]}}
- {name: consolidate_system}
- {name: drop_params, config: {params: [max_tokens, max_completion_tokens]}}
- {name: reasoning} # reasoning models: normalize reasoning field …
- {name: reasoning_replay} # … and replay it across turns (drop both for instruct)
- {name: reasoning_replay} # … and replay across turns. Drop both for instruct models.
Comment on lines 94 to +95

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

file='.agents/skills/evaluation/recipes/tasks/aa_next/swebench_verified.md'
printf '%s\n' '--- target excerpt ---'
sed -n '70,115p' "$file"

printf '%s\n' '--- related reasoning terms ---'
rg -n -C 3 'reasoning_replay|think_tags|MiniMax|Qwen|GLM|instruct|native' .agents/skills/evaluation/recipes/tasks/aa_next .agents/skills/evaluation 2>/dev/null | head -n 240

Repository: NVIDIA/Model-Optimizer

Length of output: 33100


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- interceptor definitions and usage ---'
rg -n -C 4 'reasoning_replay|name:\s*reasoning|think_tags|reasoning.*mode|native.*GLM' . --glob '!*.md' --glob '!*.lock' --glob '!package-lock.json' 2>/dev/null | head -n 320

printf '%s\n' '--- all likely proxy/config files ---'
git ls-files | rg '(^|/)(bench\.yaml|.*(proxy|interceptor|nel.*config|config).*\.(ya?ml|json|py|ts|js|md))$' | head -n 240

Repository: NVIDIA/Model-Optimizer

Length of output: 18764


🌐 Web query:

NeMo Evaluator Launcher reasoning_replay interceptor mode think_tags native MiniMax

💡 Result:

In the NVIDIA NeMo Evaluator, reasoning_replay is a specialized interceptor designed to manage chain-of-thought (CoT) reasoning across multi-turn agentic interactions [1][2]. It addresses the issue where certain agent SDKs drop reasoning_content during replay, which can lead to a loss of CoT context and increased turn counts in agentic rollouts [1][3]. The reasoning_replay interceptor caches reasoning_content from upstream model responses (keyed by tool call ID or content hash) and re-injects it into subsequent outbound requests [1]. Regarding the requested modes and features: 1. Interceptor Mode (Native): The reasoning_replay interceptor supports a native mode [3]. In this mode, it ensures compatibility with backends (such as vLLM versions >=0.19) that may require specific reasoning field representations [3]. Recent updates ensure that in native (and both) modes, the interceptor preserves and writes both reasoning and reasoning_content fields to accommodate differences in how various backends parse chat input [3]. 2. Think Tags: The interceptor integrates with the configured reasoning pipeline to wrap cached hidden chains of thought with configured think tokens (e.g., ...) [3][4]. While NeMo Evaluator generally allows customization of these tokens (e.g., in reasoning interceptor configuration) [4], the reasoning_replay interceptor uses these settings to maintain consistent reasoning formatting during re-injection [3]. 3. MiniMax: There is no direct mention of native MiniMax integration specifically within the NeMo Evaluator reasoning_replay interceptor documentation or commit history [1][3]. NeMo Evaluator interceptors are modular and typically built to be backend-agnostic by using standard OpenAI-compatible interfaces [4][5], meaning they would process reasoning content based on standard fields (like reasoning_content) regardless of the specific underlying model provider, provided the provider follows that protocol. In summary, the reasoning_replay interceptor is a tool to ensure continuity of reasoning in long-running agentic evaluations, with native modes specifically tuned to handle backend-specific field requirements (like vLLM) and standard think-tag wrapping [1][3].

Citations:


🌐 Web query:

site:github.com/NVIDIA/NeMo-Evaluator-Launcher reasoning_replay think_tags

💡 Result:

I couldn’t find an indexed match for reasoning_replay or think_tags in NVIDIA/NeMo-Evaluator-Launcher.

The relevant repository appears to be NVIDIA-NeMo/Evaluator, whose launcher package is located under packages/nemo-evaluator-launcher/. (github.com)

Note: GitHub’s current repository path is likely:

NVIDIA-NeMo/Evaluator

Citations:


🏁 Script executed:

#!/bin/bash
set -eu

for sha in 751d1f7cc2b078ac90a161e49db361da487d741a fce9256690e99c5905e74ba03fea482f2e9f9a75 e4cc20c94e4dd9dbdcd3c5a2d05b878bbfc22907; do
  printf '\n--- commit %s ---\n' "$sha"
  curl -fsSL "https://api.github.com/repos/NVIDIA-NeMo/Evaluator/commits/$sha" |
    jq -r '.commit.message, (.files[]? | "FILE: \(.filename)\n\(.patch // "")")' |
    rg -n -C 5 'reasoning_replay|think_tags|native|MiniMax|mode|reasoning_content' | head -n 240 || true
done

printf '\n--- current upstream interceptor docs ---\n'
curl -fsSL 'https://raw.githubusercontent.com/NVIDIA-NeMo/Evaluator/main/docs/about/concepts/interceptors.md' |
  rg -n -C 5 'reasoning_replay|think_tags|native|MiniMax|reasoning_content' | head -n 240 || true

Repository: NVIDIA/Model-Optimizer

Length of output: 570


🌐 Web query:

site:docs.nvidia.com/nemo/evaluator reasoning_replay mode think_tags

💡 Result:

In the context of LLM evaluation frameworks, specifically the NVIDIA NeMo Evaluator SDK, the concepts of reasoning traces and think tags are utilized to process and analyze the step-by-step thinking processes of reasoning-capable models [1][2]. Think tags (e.g., and ) are special tokens used by many modern LLMs to encapsulate their chain-of-thought reasoning process—the intermediate logical steps, calculations, and deliberations performed before arriving at a final answer [1][2]. Reasoning replay or reasoning interception is a functional component within the NeMo Evaluator that manages these traces [2][3]. Because many benchmarks and evaluation pipelines expect only the final answer from a model, the Reasoning Interceptor serves several key roles: - Extraction and Removal: It identifies the reasoning trace within the model's output (using the specified start and end tokens), removes it from the content to ensure the evaluation receives only the final answer, and optionally isolates it for further analysis [1][2][4]. - Statistics and Metrics Tracking: The interceptor tracks metrics related to the reasoning process, such as the number of responses that included reasoning, whether the reasoning completed (i.e., reached the end token), and the average length of the reasoning trace (in tokens or words) [2][4][5]. This data is typically saved to an evaluation metrics file (e.g., eval_factory_metrics.json), allowing developers to analyze model behavior, compute costs, or evaluate the quality of the chain-of-thought [1][4][5]. While "reasoning_replay" is not a singular standard term, it appears in this technical ecosystem as part of the operational workflow for handling, logging, and re-evaluating the reasoning traces generated during model inference [2][6]. Configuration of these interceptors allows users to specify custom tokens if their model uses different markers than the standard tags [2][7].

Citations:


🌐 Web query:

site:github.com/NVIDIA-NeMo/Evaluator "reasoning_replay" "think_tags"

💡 Result:

In the context of the NVIDIA NeMo Evaluator framework, reasoning_replay and think_tags are components designed to manage and preserve Chain-of-Thought (CoT) reasoning processes across multi-turn agent interactions [1][2]. reasoning_replay is an interceptor designed to solve the problem where certain agent SDKs (such as OpenHands or pi-ai) drop reasoning_content during conversation replays, causing the model to lose its reasoning context in subsequent turns [2]. The interceptor functions by caching the reasoning_content from upstream model responses and automatically re-injecting it into the next outbound request [3][2]. This ensures that the model's intermediate "thinking" remains available throughout the session [2]. think_tags refer to the configuration used to wrap this cached reasoning content [3]. Because different model backends and APIs may use different fields or formats for reasoning (e.g., specific tags or normalized fields like reasoning_content), the system uses think_tags to standardize how the hidden chain of thought is presented when re-injected [3]. The interceptor can be configured to operate in different modes (such as native or both), ensuring compatibility across various backends like vLLM and SGLang by reading and writing the appropriate reasoning fields [3].

Citations:


🌐 Web query:

site:github.com/NVIDIA-NeMo/Evaluator "reasoning_replay" "MiniMax"

💡 Result:

The term reasoning_replay refers to a specific interceptor component within the NVIDIA NeMo Evaluator framework, not a standalone model or feature from MiniMax [1][2]. It was developed to solve the issue where certain agent SDKs (such as OpenHands or older implementations) would drop an LLM's reasoning content—its Chain-of-Thought (CoT)—during multi-turn interactions [1][2]. When agentic workflows replay previous turns, the loss of this reasoning can cause models to lose context, leading to redundant or inefficient rollouts [2]. Key technical aspects of the reasoning_replay interceptor include: Caching and Re-injection: It intercepts model responses to cache reasoning content (typically identified by fields like reasoning_content or reasoning) and re-injects that content into subsequent outbound requests [1][2]. Cross-Backend Compatibility: The interceptor manages differences in how various inference backends (such as vLLM versions or SGLang) represent reasoning fields [2]. For example, it ensures compatibility with vLLM ≥0.19 by writing both reasoning and reasoning_content fields, as different backends rely on different keys [2]. Deterministic Operation: It is designed to compose deterministically with other framework components, such as reasoning normalizers and caching interceptors, ensuring the CoT is preserved consistently across long agentic benchmark runs [1]. While NeMo Evaluator is used to evaluate various models, including those with reasoning capabilities, reasoning_replay is a utility within the evaluation infrastructure designed to maintain data integrity during testing, rather than a feature of any specific model provider like MiniMax [1][3][4].

Citations:


Make the reasoning interceptor configuration model-specific.

Reasoning models require reasoning and reasoning_replay; instruct models require neither. Set reasoning_replay.mode per model: think_tags for Qwen-style models, native for GLM models, and omit it for MiniMax. Update the examples at lines 92–93 and 96–100.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.agents/skills/evaluation/recipes/tasks/aa_next/swebench_verified.md around
lines 92 - 93, Update the reasoning interceptor examples in the model
configuration section so they are model-specific: retain both reasoning and
reasoning_replay for reasoning models, use reasoning_replay.mode "think_tags"
for Qwen-style models, "native" for GLM models, and omit reasoning_replay for
MiniMax; ensure instruct-model examples include neither interceptor.

```

**`reasoning_replay.mode` is per model, not per benchmark.** `think_tags` (Qwen-style),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

omitted isn't a third behavior — it selects think_tags.

reasoning_replay.Interceptor.__init__ (adapters/interceptors/reasoning_replay.py:120) is mode: str = "think_tags", so omitting the key gives exactly the Qwen setting. Since this same sentence warns that the wrong mode is "a silent output-parsing bug", listing MiniMax as omitted invites the reading that replay is neutral or off for it, when it actually gets <think>-tag re-injection.

If think_tags genuinely is right for MiniMax, saying so explicitly ("MiniMax: leave mode unset — the default is think_tags") removes the ambiguity. The valid set is think_tags / native / both.

`native` (GLM), omitted (MiniMax). Copying another model's mode is a silent output-parsing bug.

**Omitting `system_message` is a scoring change**: without it the agent runs the
openhands-sdk default prompt instead of the canonical one.

### Sharding

`max_concurrent`/`sandbox.concurrency` are **per shard**, and each shard redeploys the model
on its own node — `shards: N` multiplies serving capacity *and* live Fargate sandboxes
(`N × concurrency`). 500 tasks × `repeats: 5` = 2500 trials; `shards: 10` at `concurrency: 15`
suits it. Score is unaffected — purely a wall-clock lever. Check `N × concurrency` against the
Fargate quota and `N × gpus_per_node` against your allocation.

## Score Extraction

Report **`pass@1`** only — benchmark `swebench-verified@1.0`, scorer `pass@1` (0–1):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,18 @@ pinned via a vendored registry override in nemo-evaluator-next.
| `cluster.container_env.AWS_DEFAULT_REGION` | match `sandbox.region` |
| `max_concurrent` / `sandbox.concurrency` | `50` (canonical bench.yaml) |
| timeout_strategy | `max` (canonical bench.yaml) + `agent_kwargs.llm_kwargs.timeout: 3600`; use `task` for leaderboard-comparable |
| `cluster.eval_image` requirement | **≥ `0.3.1.1-harbor`** — TB 2.1's task set is pinned via a vendored registry override in that image (`${NEL_NEXT_EVAL_IMAGE}`, multi-arch) |
| `cluster.eval_image` | **`0.5.0.1-harbor`** (`${NEL_NEXT_EVAL_IMAGE}`, multi-arch) |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bot comment.

This table now has two cluster.eval_image rows: line 23 (${NEL_NEXT_EVAL_IMAGE} — set by modelopttools:eval-config) and this one (0.5.0.1-harbor). The diff replaced the old "cluster.eval_image requirement" row but left the earlier plain one, so the table has a duplicated key with two different values. Please merge into a single row.

| `proxy.request_timeout` | `3600` — must be **≥** `agent_kwargs.llm_kwargs.timeout` |
| `drop_params` | `max_tokens`, `max_completion_tokens`, `max_input_tokens_per_task`, `no_rebuild` |
| `output.export_config.mlflow.exclude_patterns` | `["shard*", "model_traffic.jsonl"]` |
| `http_pairs_dump` | **last** in the interceptor chain — canary/diagnostic only, drop it for a scored run (unbounded error-pair retention) |
| scope | 89 tasks × `repeats: 8` |

These values mirror the canonical TB2.1 config — re-check it before a scored run:
`configs/benchmarks/nel_next/terminal_bench_21/bench.yaml` in
nvidia-eval-factory-benchmarking (see `references/nel-next.md` + the eval-config
"source of truth" note). The `benchmarks:` block (drop into the example template):
`configs/benchmarks/terminal-bench-2.1/bench.yaml` (+ `manifest.yaml`) in
nvidia-eval-factory-benchmarking (`dl/JoC/competitive_evaluation/…`), with the image pin in
`configs/shared/nel_next_containers.yaml`. See `references/nel-next.md` + the eval-config
"source of truth" note. The `benchmarks:` block (drop into the example template):

```yaml
benchmarks:
Expand All @@ -50,9 +56,15 @@ benchmarks:
log_stream_prefix: terminalbench21-<model>-<cluster>
```

`cluster.eval_image: ${NEL_NEXT_EVAL_IMAGE}` (`0.3.1.1-harbor`) and the AWS creds
`cluster.eval_image: ${NEL_NEXT_EVAL_IMAGE}` (`0.5.0.1-harbor`) and the AWS creds
come from `modelopttools:eval-config` (run it first) + the workspace `.env`.

**Sharding.** `max_concurrent`/`sandbox.concurrency` are **per shard**, and each shard runs
its own vLLM on its own node — `shards: N` multiplies both serving capacity and live Fargate
sandboxes (`N × concurrency`). Trials are partitioned and merged, so the score is unaffected;
it is purely a wall-clock lever. `shards: 4` suits 89 × r8 = 712 trials. Check
`N × concurrency` against the Fargate quota and `N × gpus_per_node` against your allocation.

## Score Extraction

Report **`pass@1`** only — benchmark `terminal-bench@2.1`, scorer `pass@1` (0–1):
Expand Down
32 changes: 27 additions & 5 deletions .agents/skills/evaluation/references/nel-next.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,14 @@ Installing 0.3.x into the 0.2.6 env clobbers `nel`, so it lives in its own venv:
.agents/scripts/nel-next.sh eval run <cfg> --dry-run | --submit | …
```

Default install is public PyPI `nemo-evaluator[harbor]==0.3.*`; set
`NEL_NEXT_ORIGIN`/`NEL_NEXT_REF` for the internal git build (see script header).
Default install is a git build from `github.com/NVIDIA-NeMo/Evaluator` via `NEL_NEXT_ORIGIN`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION] This paragraph now correctly says the default resolves to 0.4.0, but the version labels elsewhere still say 0.3.x and were not updated — so the doc contradicts itself a few lines apart:

  • references/nel-next.md:14 — comparison table: package | `nemo-evaluator-launcher` 0.2.6 | `nemo-evaluator[harbor]` 0.3.x
  • references/nel-next.md:23 — "Installing 0.3.x into the 0.2.6 env clobbers nel"
  • SKILL.md:39 — "(nemo-evaluator[harbor] 0.3.x)"
  • scripts/nel-next.sh:17,20,23,55 — header and _uvx comment all say "nel 0.3.x" / "0.3.x environment"

The 0.2.6-vs-next separation argument holds regardless of which minor the "next" side is, so nothing is functionally wrong — but "0.3.x" is the value a reader will quote when sanity-checking --version output against the docs, and it now prints 0.4.0. Since the point of this PR is removing exactly this kind of drift, worth relabeling these to 0.3.x/0.4.x (or just "next") in the same pass.

(`main` → **0.4.0**). PyPI `nemo-evaluator` stops at **0.3.0**, so a version pin can't reach
0.4.x. No `v0.4.0` tag exists, so `NEL_NEXT_REF` **defaults to a pinned commit SHA** in
`nel-next.sh` — a floating branch HEAD installs a different harness for the baseline and the
candidate, so the pass@1 delta no longer isolates the model. `NEL_NEXT_REF=main` tracks HEAD
for dev/canary work only; to move the pin, canary against the new SHA and bump the default in
`nel-next.sh`. Record `nel-next.sh --version` (prints version + resolved spec) with scored
results. Set `NEL_NEXT_ORIGIN` in `.env` to build from a mirror.

## Credentials + internal infra (`.env`)

Expand Down Expand Up @@ -75,7 +81,7 @@ services:
extra_env: {...} # VLLM_* backend env (e.g. NVFP4 MoE flags)
container_mounts: [<lustre>/.cache/vllm:/cache/vllm, ...]
generation: {temperature: 1.0, top_p: 0.95}
proxy: {request_timeout: 1800, extra_body: {...}, interceptors: [...]}
proxy: {request_timeout: 3600, extra_body: {...}, interceptors: [...]} # >= llm_kwargs.timeout
node_pool: gpu
benchmarks: # EXACTLY ONE entry — one benchmark per config (see "One benchmark per config")
- playbook: <benchmark> # per recipe
Expand Down Expand Up @@ -131,9 +137,25 @@ with its own `run_id`, copying the shared `services:` block.

## Rules & gotchas

- **`eval_image`** = `${NEL_NEXT_EVAL_IMAGE}`. `0.3.1.1-harbor` is multi-arch and is
the minimum for **TB 2.1**; older `0.17.x/0.18.x-harbor-<arch>` are arch-suffixed.
- **`eval_image`** = `${NEL_NEXT_EVAL_IMAGE}` → `0.5.0.1-harbor` (multi-arch). Re-check
against `configs/shared/nel_next_containers.yaml` in the eval-factory repo, which is the
pin and does move. Arch-suffixed `0.17.x/0.18.x-harbor-<arch>` are too old for TB 2.1.
Private gitlab-master image → cluster needs enroot creds (SKILL Step 7.5).
- **`proxy.request_timeout` must be >= `agent_kwargs.llm_kwargs.timeout`** (both 3600). A
smaller proxy timeout silently truncates long agent turns.
- **`drop_params`** for harbor agentic benchmarks: `max_tokens`, `max_completion_tokens`,
`max_input_tokens_per_task`, `no_rebuild`. The last two are sent by the 0.5.x eval image;
vLLM 400s on them if they aren't stripped.
- **`exclude_patterns`** = `["shard*", "model_traffic.jsonl"]` — captured request bodies
stay in the run dir, never MLflow.
- **`http_pairs_dump` — canary/diagnostic runs only, leave it OUT of scored runs.**
`config: {dump_path: "$${NEL_OUTPUT_DIR}/http_pairs_metrics.json", first_n: 50}` (the `$$`
defers expansion to run time). `first_n` caps only the *successful* pairs: the keep rule is
`(total_seen <= first_n) or (status != 200)`, so **every** non-200 pair is held in memory
for the life of the run, full request + response bodies, and the whole list is re-serialized
on each write. A long agentic run that is 400ing or rate-limiting (the failure this dumps
diagnose) grows the proxy without bound — exactly the run you can least afford to lose.
Chain position is per benchmark (last for TB2.1, first for SWE-bench).
- **Mount sources must pre-exist** — pyxis won't create the host side of a bind
mount (invisible to `--dry-run`, fails at canary). `ssh <login> 'mkdir -p
<lustre>/<user>/.cache/{vllm,huggingface}'`.
Expand Down
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,7 @@ docs/source/reference/generated
**/.ipynb_checkpoints

# Environments
.env
.env-*
.env*

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION] .env* is broader than the stated goal ("secret backups such as .env.bak-tb21 cannot be staged") and will also swallow any future .env.example / .env.template at the repo root — the exact files you want tracked. The skill already relies on a committed template (.agents/skills/evaluation/recipes/env.example, referenced from SKILL.md:84 and four task recipes); that path is safe today only because it lacks the leading dot.

Cheap insurance, since a silently-untracked template is a confusing failure mode:

.env*
!.env.example

.venv
env/
venv/
Expand Down
Loading