Skip to content

Commit 024eedc

Browse files
Add pi_env: Pi coding-agent environment (HF sandbox + transparent-proxy logprobs) (#999)
* Add Pi coding-agent harness (pi_env) reusing opencode_env sandbox + proxy PiSessionFactory/PiSession/PiConfig mirror the OpenCode primitive: install the pi CLI (Node bootstrap + npm), write models.json pointing at an OpenAI-compatible endpoint, run pi --print --mode json headless, and (transparent_proxy mode) capture per-token logprobs via the shared interception proxy for GRPO. The sandbox backend (HFSandboxBackend) and interception proxy are imported from opencode_env for now; the plan is to consolidate both into openenv.core in a follow-up. Includes builder + factory-lifecycle unit tests. * Add deployable HTTP env layer to pi_env (parity with opencode_env) Mirrors opencode_env's second layer so pi_env is both an in-process harness primitive and a deployable OpenEnv environment: PiEnv MCP client + FastAPI server (PiEnvironment with the run_rollout tool, task catalog, Gradio UI) + Dockerfile + openenv.yaml. HF sandbox (image, HF_TOKEN) replaces E2B (template, E2B_API_KEY); in-sandbox paths are /root; the sandbox backend and interception proxy are imported from opencode_env. Adds uv.lock and the pi-env server-image entry to docker-build.yml. * Add pi_env to the docs (env stub + catalog card + toctree nav) sync_env_docs.py --fix generates the docs/source/environments/pi.md stub from the README; the HTML catalog card in environments.md and the _toctree.yml nav entry are added manually (the script does not manage those). check-env-docs now passes. * Format test_pi_runtime.py (usort import order + ruff line wrap) * Address review: fix docs links, node version gate, black-box model id, pipefail - README: correct the HF sandbox docs URL, drop redundant/circular links - .dockerignore: HF_TOKEN, not E2B_API_KEY - pi_runtime: gate Node >=22.19 (not just major 22); use the full model id and select by --provider so a slashed id isn't split; set -o pipefail so the piped tee doesn't mask Pi's exit code * Keep server/client separation in pi_env server; correct setup timing docs Import the primitive from its defining modules instead of the top-level pi_env package (which re-exports the client), per the server/client-separation invariant. Reword the setup docs: it runs at rollout start, not strictly before the agent. * Address review: cover cold-bootstrap in timeouts, doc peer-dep, drop dead field - harness/server: size the sandbox lifetime and MCP-tool timeout to the full Node+Pi cold install, not just the agent - README: note the opencode_env install prerequisite for the primitive path; correct the server Dockerfile comment - config: drop unused request_timeout_ms - client: document the endpoint shorthand * Add pre-baked HF sandbox image for pi_env; fix mode docstring Mirror opencode_env's pre-baked image: bake Node + Pi + proxy deps so rollouts skip the cold install (the proxy source is uploaded at runtime since it lives in opencode_env). Also drop the stale '(default)' on transparent_proxy in the harness docstring (the factory default is black_box). * Address review: skip verify/reward on failed setup; pipefail proxy-deps; guard UI stderr - server: only run verify + compute reward when the run is clean (a failed setup/agent leaves a half-prepared sandbox); dedupe the endpoint error paren - harness: set -o pipefail on the proxy-deps install so a failed pip isn't masked by tail - gradio: don't index an empty stderr split (IndexError when exit!=0, no stderr) * Give setup commands a longer timeout than verify (pip install / downloads) * Use get_token() for the HF credential check; gate proxy deps vs source separately - token check via huggingface_hub.get_token() (HF_TOKEN, legacy var, or a cached login) instead of guessing env var names - _start_proxy: skip the pip install when the deps import, independent of the proxy source upload, so the pre-baked image (deps baked, source not) skips it * Drop unused os import in pi_environment (token check now uses get_token) * Default PiEnv client message timeout to cover a full rollout (server caps at 1500s) * Retry PiSessionFactory.create() with backoff Mirror of the OpenCodeSessionFactory retry: session creation (sandbox + Pi install + proxy + agent) is the flakiest step, and a single transient failure dropped the rollout as unscorable. create() now retries create_attempts times with exponential backoff; the per-attempt logic moved to _create_once, which already tears its own sandbox down on failure, so a retry never leaks. * Run setup before the agent and skip verify on a non-zero agent exit Addresses review: run_rollout ran setup after create() had already started the agent, so setup raced the agent over the workspace. create() now takes start_agent (default True, so the loop-owning training path is unchanged), the server passes start_agent=False, runs setup, then calls session.start_agent(), giving the correct setup then agent then verify order. A non-zero agent exit is now treated like a timeout, so a crashed agent no longer runs verify or earns a reward. AI-assisted. * Add pi_env demo and training tutorial - examples/pi_env_simple.py: inference demo hitting the deployed Space - docs tutorial pi-agent-grpo.md: black-box AsyncGRPO training recipe, links to TRL
1 parent 5298e0d commit 024eedc

29 files changed

Lines changed: 6991 additions & 0 deletions

.github/workflows/docker-build.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,12 @@ jobs:
112112
- name: opencode-sandbox
113113
dockerfile: envs/opencode_env/sandbox/hf_image/Dockerfile
114114
context: envs/opencode_env
115+
- name: pi-env
116+
dockerfile: envs/pi_env/server/Dockerfile
117+
context: envs/pi_env
118+
- name: pi-sandbox
119+
dockerfile: envs/pi_env/hf_image/Dockerfile
120+
context: envs/pi_env
115121
- name: openapp-env
116122
dockerfile: envs/openapp_env/server/Dockerfile
117123
context: envs/openapp_env

docs/source/_toctree.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@
5454
title: RL Training with an Agentic Harness
5555
- local: tutorials/sft-warmup
5656
title: SFT Training with Environments
57+
- local: tutorials/pi-agent-grpo
58+
title: Training a Real Coding Agent (Pi)
5759
title: Tutorials
5860
- isExpanded: false
5961
sections:
@@ -131,6 +133,8 @@
131133
title: OpenCode
132134
- local: environments/pelican_svg
133135
title: Pelican SVG
136+
- local: environments/pi
137+
title: Pi
134138
- local: environments/sophistry_bench_sprint
135139
title: Sophistry Bench Sprint
136140
title: Environments

docs/source/environments.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,13 @@ The OpenEnv community has built a catalog of ready-to-run environments that cove
266266
<a href="https://huggingface.co/spaces/sergiopaniego/pelican-svg-env" class="!no-underline border dark:border-gray-700 px-3 py-1 rounded text-sm hover:shadow">🤗 HF</a>
267267
</div>
268268
</div>
269+
<div class="border dark:border-gray-700 p-5 rounded-lg shadow">
270+
<div class="font-bold mb-2">Pi</div>
271+
<p class="text-sm"><code>pi_env</code> runs the Pi coding agent inside an isolated Hugging Face sandbox against any OpenAI-compatible LLM endpoint, optionally capturing per-token logprobs.</p>
272+
<div class="flex gap-2 mt-3">
273+
<a href="environments/pi" class="!no-underline border dark:border-gray-700 px-3 py-1 rounded text-sm hover:shadow">📄 Docs</a>
274+
</div>
275+
</div>
269276
<div class="border dark:border-gray-700 p-5 rounded-lg shadow">
270277
<div class="font-bold mb-2">Sophistry Bench Sprint</div>
271278
<p class="text-sm"><code>sophistry_bench_sprint_env</code> is a single-turn advocacy reward-hacking environment on QuALITY passages: the policy defends an assigned answer and the reward proxy peaks at 8 <code>&lt;claim&gt;</code> tags, with four weight-0 canaries that detect format hacking.</p>

docs/source/environments/pi.md

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
<!-- openenv-source: pi_env -->
2+
# Pi Environment for OpenEnv
3+
4+
`pi_env` runs the [Pi](https://github.com/badlogic/pi-mono) coding agent
5+
inside an isolated [Hugging Face sandbox](https://huggingface.co/docs/huggingface_hub/package_reference/sandbox)
6+
against any OpenAI-compatible LLM endpoint, optionally capturing per-token
7+
logprobs for GRPO training.
8+
9+
It mirrors `opencode_env`: same two-layer design (an
10+
in-process harness primitive + a deployable HTTP env), same transparent-proxy
11+
logprob capture, same uniform `(instruction, setup, verify)` Task shape. The
12+
agent is Pi instead of OpenCode, and the default sandbox backend is Hugging
13+
Face instead of E2B.
14+
15+
The env is **task-agnostic** — every rollout is configured at call-time
16+
with a uniform Task shape:
17+
18+
- **`instruction`** — prompt for the agent
19+
- **`setup`** — list of bash commands run *before* the agent (pip install,
20+
git clone, file downloads — anything you need staged in the sandbox)
21+
- **`verify`** — list of bash commands run *after* the agent (asserts,
22+
pytest invocations, score-file writes)
23+
24+
Reward = `passed_verify / total_verify` unless any `verify` command writes
25+
a float to `/root/logs/verifier/reward.txt` (override).
26+
27+
## In-process primitive (no HTTP)
28+
29+
For trainers that drive a sandbox directly without an HTTP boundary — this is
30+
what loop-owning GRPO training uses. The primitive reuses the sandbox backend +
31+
proxy from `opencode_env`, so install it alongside `pi_env`:
32+
33+
```bash
34+
pip install "openenv-opencode-env @ git+https://github.com/huggingface/OpenEnv.git#subdirectory=envs/opencode_env"
35+
```
36+
37+
```python
38+
import os
39+
from pi_env import PiConfig, PiSessionFactory, PiTask, HFSandboxBackend
40+
41+
factory = PiSessionFactory(
42+
config=PiConfig(
43+
base_url="https://api.openai.com/v1",
44+
api_key=os.environ["OPENAI_API_KEY"],
45+
model="gpt-4o-mini",
46+
sandbox_home="/root", # HF sandbox execs as root
47+
),
48+
sandbox_backend=HFSandboxBackend(image="python:3.12"),
49+
mode="transparent_proxy", # captures per-token logprobs
50+
)
51+
session = factory.create(task=PiTask(instruction="..."))
52+
session.wait_for_completion()
53+
turns = session.fetch_proxy_trace() # per-turn (tokens, logprobs)
54+
session.close()
55+
```
56+
57+
Pi is pointed at the endpoint via a `models.json` provider block written under
58+
`PI_CODING_AGENT_DIR` (`api: openai-completions`), then launched headless with
59+
`pi --print --no-session --mode json`. In `transparent_proxy` mode the
60+
in-sandbox proxy fronts `base_url`, injects `logprobs=true`, and writes each
61+
turn's `(messages, completion_token_ids, per_token_logps)` to
62+
`proxy_trace.jsonl`.
63+
64+
### Sandbox backend
65+
66+
`HFSandboxBackend` (from `opencode_env.sandbox`, shared with `opencode_env`)
67+
runs the agent in a Hugging Face sandbox. `image="python:3.12"` cold-installs
68+
Node 22 (bootstrapped) + the Pi CLI (`npm install -g @mariozechner/pi-coding-agent`)
69+
+ the proxy's Python deps on every rollout. For faster rollouts use the
70+
pre-baked image (Node + Pi + proxy deps already installed), built by CI from
71+
`hf_image/Dockerfile`:
72+
73+
```python
74+
sandbox_backend=HFSandboxBackend(image="ghcr.io/huggingface/openenv-pi-sandbox:latest")
75+
```
76+
77+
Any backend satisfying the `SandboxBackend` / `SandboxHandle` / `BgJob`
78+
protocols in `opencode_env.sandbox.base` can be plugged in the same way.
79+
80+
> The sandbox backend and interception proxy live in `opencode_env` for now;
81+
> the plan is to consolidate both into `openenv.core` so `pi_env` and
82+
> `opencode_env` share them without a cross-package import.
83+
84+
## Deployed env (HTTP)
85+
86+
The deployed Space exposes:
87+
88+
- **Web UI** at `/web` — pick endpoint, write task, hit Run, watch live phase
89+
log + reward + logprobs.
90+
- **MCP tool API** at `/mcp` — programmatic `run_rollout` calls.
91+
- **OpenAPI docs** at `/docs`, **health** at `/health`.
92+
93+
```python
94+
import os
95+
from pi_env import PiEnv
96+
97+
with PiEnv(base_url="https://<user>-pi-env.hf.space") as env:
98+
env.reset()
99+
result = env.run_rollout(
100+
endpoint="openai", # vllm | openai | hf_router
101+
api_key=os.environ["OPENAI_API_KEY"], # or set as a Space secret
102+
instruction=(
103+
"Create binary_search.py exposing def binary_search(arr, target) -> int "
104+
"that returns the index of target in arr, or -1 if absent."
105+
),
106+
setup=[],
107+
verify=[
108+
"test -f /root/workdir/binary_search.py",
109+
"python -c \"import sys; sys.path.insert(0, '/root/workdir'); "
110+
"import binary_search; "
111+
"assert binary_search.binary_search([1,2,3], 2) == 1; print('OK')\"",
112+
],
113+
task_id="binary_search_v1",
114+
)
115+
print("reward:", result.reward)
116+
print("turns:", len(result.proxy_turns))
117+
```
118+
119+
## The MCP Tool: `run_rollout`
120+
121+
Single tool, two ways to specify the LLM endpoint:
122+
123+
**Option A — endpoint shorthand (recommended)**: pass `endpoint="vllm"` (or
124+
`"openai"` / `"hf_router"`). The server resolves `base_url`, `api_key`, and
125+
`model` from env vars + catalog defaults. Any explicit field overrides.
126+
127+
**Option B — fully explicit**: pass `base_url` + `api_key` + `model` directly.
128+
129+
| Arg | Type | Default | Notes |
130+
|---|---|---|---|
131+
| `endpoint` | `str` | `""` | One of `"vllm"` / `"openai"` / `"hf_router"`. |
132+
| `base_url` / `api_key` / `model` | `str` | `""` | Override / supply explicitly. |
133+
| `instruction` | `str` | required | Prompt passed to `pi`. |
134+
| `setup` | `list[str]` | `[]` | Bash commands run **before** the agent. |
135+
| `verify` | `list[str]` | `[]` | Bash commands run **after** the agent. |
136+
| `task_id` | `str` | `""` | Echoed back in result. |
137+
| `mode` | `str` | `"transparent_proxy"` | Or `"black_box"` (no logprobs). |
138+
| `disable_thinking` | `bool \| None` | `None` (catalog default) | Inject `chat_template_kwargs.enable_thinking=false`. |
139+
| `max_tokens_cap` | `int` | `4096` | Per-turn `max_tokens` clamp. |
140+
| `top_logprobs` | `int` | `5` | HF Router cap is 5; OpenAI 0–20; vLLM unbounded. |
141+
| `agent_timeout_s` | `float` | `600.0` | Hard wall budget for one `pi` run. |
142+
| `image` | `str` | `""` | HF sandbox image; blank → `python:3.12` (cold-installs Node + Pi). |
143+
144+
Returns `RolloutResult` JSON with: `reward`, `setup_results[]`,
145+
`verify_results[]`, `proxy_turns[]`, `files{}`, `agent_log_tail`,
146+
`proxy_log_tail`, `wall_s`, `agent_exit_code`, `sandbox_id`, `error`.
147+
148+
## Two Operating Modes
149+
150+
| Mode | What it does | Best for |
151+
|---|---|---|
152+
| **`transparent_proxy`** (default) | In-sandbox proxy at `localhost:7000` forwards Pi's LLM calls to `base_url`, injects `logprobs=true`, captures per-turn `(messages, completion_tokens, logprobs)` to `proxy_trace.jsonl`. | GRPO / RL training, observability, top-k distillation. |
153+
| **`black_box`** | No proxy. Pi talks straight to `base_url`. | Smoke tests, eval, SFT data collection. |
154+
155+
## Building the Docker Image
156+
157+
```bash
158+
cd envs/pi_env
159+
160+
openenv validate # check pyproject.toml + openenv.yaml + server/app.py + uv.lock
161+
openenv build -t pi-env # builds the image (uses server/Dockerfile)
162+
163+
# run locally with an HF token (Sandbox + Jobs access)
164+
docker run -p 8000:8000 -e HF_TOKEN=hf_... pi-env
165+
```
166+
167+
Or build directly:
168+
169+
```bash
170+
docker build -t pi-env -f envs/pi_env/server/Dockerfile envs/pi_env
171+
```
172+
173+
## Environment Variables
174+
175+
| Variable | Required | Purpose |
176+
|---|---|---|
177+
| `HF_TOKEN` | **yes** for any rollout | Hugging Face sandbox credentials. |
178+
| `MAX_CONCURRENT_ENVS` | no | Env-instance pool size. Default `4`. |
179+
| `ENABLE_WEB_INTERFACE` | no | Set `false` to disable the `/web` Gradio mount. Default `true`. |
180+
| `VLLM_URL` / `VLLM_API_KEY` / `VLLM_MODEL` | for `endpoint="vllm"` | OAI-compatible base URL (key defaults to `intercepted`). |
181+
| `OPENAI_API_KEY` / `OPENAI_BASE_URL` / `OPENAI_MODEL` | for `endpoint="openai"` | Standard OpenAI. |
182+
| `HF_ROUTER_API_KEY` / `HF_ROUTER_BASE_URL` / `HF_ROUTER_MODEL` | for `endpoint="hf_router"` | HF Router. |
183+
184+
Pick `provider:` suffixes that actually return logprobs:
185+
**Together / Nscale / Scaleway / SambaNova / Cerebras**. Avoid Novita /
186+
Hyperbolic / Featherless (silent drop) and Groq (HTTP 400).
187+
188+
## Project Structure
189+
190+
```
191+
pi_env/
192+
├── README.md # this file
193+
├── openenv.yaml # OpenEnv space spec
194+
├── pyproject.toml # deps + ``server`` entrypoint
195+
├── __init__.py # re-exports primitive + client + models
196+
197+
├── client.py # PiEnv(MCPToolClient)
198+
├── models.py # RolloutResult / RolloutTurn / PiState
199+
200+
├── config.py # PiConfig (primitive)
201+
├── harness.py # PiSession / PiSessionFactory (CLI-only)
202+
├── pi_runtime.py # models.json builder + install/run cmds
203+
├── task.py # PiTask
204+
205+
└── server/
206+
├── __init__.py
207+
├── app.py # FastAPI factory; mounts Gradio at /web
208+
├── pi_environment.py # MCPEnvironment with single ``run_rollout`` tool
209+
├── gradio_ui.py # the /web Gradio Blocks UI
210+
├── catalog.py # endpoint shorthand resolver
211+
└── Dockerfile # multi-stage uv build (used by ``openenv build``)
212+
```
213+
214+
The sandbox backend + interception proxy are imported from
215+
`opencode_env.sandbox`; `pi_env` ships no `sandbox/` of its own.

docs/source/tutorials/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,5 @@ Already familiar with the basics? These tutorials cover specific workflows in de
2727
| [Evaluating agents with Inspect AI](evaluation-inspect.md) | Wrap an OpenEnv environment in an Inspect AI `Task`, run it via `InspectAIHarness`, and get a structured `EvalResult`. | No | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/OpenEnv/blob/main/examples/evaluation_inspect.ipynb) |
2828
| [BrowserGym Harness Rollouts](browsergym-harness.md) | Drive BrowserGym through the OpenEnv harness runtime when a trainer needs token sampling, logprobs, and reward assignment inside the training loop. | Yes ||
2929
| [Collecting rollouts for supervised training](sft-warmup.md) | Run a teacher model to collect reward-labeled rollouts, filter them, and fine-tune a student with TRL's `SFTTrainer` as a warm-start for GRPO. | Yes | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/OpenEnv/blob/main/examples/sft_warmup.ipynb) |
30+
| [Training a Real Coding Agent (Pi)](pi-agent-grpo.md) | Train the actual Pi agent (black-box, loop-owning) with TRL's AsyncGRPOTrainer: a transparent proxy captures each turn's token ids and logprobs while the agent runs its own tool loop. | Yes ||
3031

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Coding Agent Training with TRL (Pi)
2+
3+
This tutorial covers the black-box training path: training the actual
4+
[`pi`](https://github.com/badlogic/pi-mono) coding agent, with its own planner,
5+
tools, context management, and stop condition, using TRL's experimental
6+
`AsyncGRPOTrainer`. The agent owns its loop, and OpenEnv captures what it did.
7+
8+
> [!NOTE]
9+
> Three GRPO patterns, three tutorials. For a standard `reset()` / `step()`
10+
> flow where TRL drives the episode, see the
11+
> [Wordle GRPO tutorial](wordle-grpo.md). For harness rollouts where the
12+
> trainer still generates each turn (white-box), see the
13+
> [BrowserGym harness tutorial](browsergym-harness.md). Use this page when you
14+
> want to train a production agent as-is, without reimplementing its loop.
15+
16+
## How It Works
17+
18+
The full recipe lives in TRL. The moving pieces:
19+
20+
1. Each rollout runs the agent inside an OpenEnv session created by
21+
`PiSessionFactory` from
22+
[`pi_env`](https://github.com/huggingface/OpenEnv/tree/main/envs/pi_env),
23+
in `transparent_proxy` mode. A small proxy inside the sandbox forwards the
24+
agent's `/v1/chat/completions` calls to your vLLM server and records each
25+
turn's token ids and logprobs to a trace.
26+
2. When the agent stops, TRL's `HarnessRolloutWorker` reads the trace, rebuilds
27+
the per-turn training rows from the recorded ids, and scores the final
28+
workspace with the session's `verify()` method (a held-out verifier the
29+
agent never sees).
30+
3. `AsyncGRPOTrainer` trains on those rows, propagating the rollout reward to
31+
every trained token through the group-relative advantage. NCCL weight sync
32+
keeps the vLLM server on the current policy, so the agent always samples
33+
from the model being trained.
34+
35+
Each rollout gets its own isolated session: one sandbox, one proxy port, one
36+
agent process. Three small functions adapt the recipe to your task:
37+
`rollout_reward_fn` (outcome to scalar reward), `train_turn_fn` (which turns
38+
receive gradient), and `agent_turn_fn` (which trace entries are real agent
39+
turns rather than auxiliary calls like title generation).
40+
41+
## Full Recipe
42+
43+
The reference script trains on competitive-coding problems from
44+
`agentica-org/DeepCoder-Preview-Dataset`: the agent writes `solution.py`, and
45+
the verifier runs it against held-out tests, returning the fraction passed. It
46+
is self-contained, runs the agent in a local subprocess sandbox (no container
47+
setup needed), needs two GPUs (one serving the policy with vLLM, one
48+
training).
49+
50+
- [`examples/scripts/openenv/pi.py`](https://github.com/huggingface/trl/blob/main/examples/scripts/openenv/pi.py)
51+
in TRL: the complete, runnable script.
52+
- [`envs/pi_env`](https://github.com/huggingface/OpenEnv/tree/main/envs/pi_env):
53+
the OpenEnv side, including the session factory, sandbox backends, and the
54+
transparent interception proxy.

envs/pi_env/.dockerignore

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Don't bloat the build context. Copy only what the runtime needs.
2+
.venv
3+
__pycache__
4+
**/__pycache__
5+
*.pyc
6+
*.pyo
7+
.pytest_cache
8+
.mypy_cache
9+
.ruff_cache
10+
11+
# NEVER ship secrets into the image. The container reads HF_TOKEN etc.
12+
# from runtime env vars (HF Space secrets, ``docker run -e``).
13+
.env
14+
.env.*
15+
16+
# Test artifacts and local debug output.
17+
tests/_artifacts/
18+
*.log
19+
.coverage
20+
htmlcov/
21+
22+
# Editor / IDE noise.
23+
.idea
24+
.vscode
25+
.DS_Store
26+
27+
# Git metadata is unused at runtime.
28+
.git
29+
.gitignore

envs/pi_env/.gitignore

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Tests are dev-only — never ship into the deployable env.
2+
tests/
3+
4+
# Local debug artifacts (covered above, kept for clarity).
5+
*.log
6+
7+
# Local secrets / venv (also covered at repo root, kept here for safety).
8+
.env
9+
.env.*
10+
.venv
11+
__pycache__/
12+
*.pyc

0 commit comments

Comments
 (0)