-
Notifications
You must be signed in to change notification settings - Fork 432
Harbor integration: serve Harbor task datasets through OpenEnv as trainable environments #1036
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
adithya-s-k
wants to merge
78
commits into
huggingface:main
Choose a base branch
from
adithya-s-k:harbor-integration
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 57 commits
Commits
Show all changes
78 commits
Select commit
Hold shift + click to select a range
eee41a7
capture: dialect-agnostic token capture for agent rollouts
adithya-s-k 5976ac9
harbor: run Harbor tasks as an OpenEnv environment
adithya-s-k f1ae0e4
cli: openenv harbor info / rollout / serve / push
adithya-s-k 341eebe
harbor_env: deployment packaging for a Space
adithya-s-k 1b9b324
tests: port ownership, request normalisation, hosted serving
adithya-s-k d44f42c
docs: add the harbor_env stub and link it
adithya-s-k c684348
tests: cover the graph, rewards, seams, discovery and rendering
adithya-s-k af14859
harbor: fix the process-env lock breaking all concurrent rollouts
adithya-s-k 17e080c
harbor: a FATAL capture finding must make the rollout unusable
adithya-s-k 173acd8
harbor: cross-check pi against its own session log
adithya-s-k 49dbdfa
harbor: recover the ATIF trajectory for hermes and openclaw
adithya-s-k 559b30d
capture: name the upstream project in the dialect provenance
adithya-s-k 33516cd
docs: rewrite the harbor_env guide, and make its example importable
adithya-s-k af2459e
docs: lead on multi-harness training, and document the CLI properly
adithya-s-k 0d5c1de
harbor: stop calling asyncio.run on server-reachable paths
adithya-s-k 24e7bcc
capture: fix the cloudflare forwarder, which never worked
adithya-s-k 6ef13b5
capture: set the served-model marker before transforming, not after
adithya-s-k f924b40
capture: export every agent root, not only the first
adithya-s-k ecfb90a
harbor: restore the process env after a rollout, and fix the install …
adithya-s-k eb6d09e
harbor: give every turn its own prompt ids, and stop deriving turn bo…
adithya-s-k a18b592
capture: leave the caller's messages alone, and accept an engine URL …
adithya-s-k 628059d
harbor: export a forked prefix once, and never leave a capture server…
adithya-s-k b592acc
harbor: sort imports the way usort does
adithya-s-k b2145d3
capture: accept SGLang as an engine, not just vLLM
adithya-s-k ad059f5
harbor: stop the tool-output box from eating the page scroll
adithya-s-k 902699f
capture: authenticate to the upstream, and negotiate what it accepts
adithya-s-k 84ac60c
capture: probe what an endpoint returns, instead of demanding token ids
adithya-s-k bde46be
capture: an eval rollout keeps its trace, and never a training contract
adithya-s-k 59a09f5
harbor: carry the key and the capture level through a rollout
adithya-s-k 125288b
harbor: fix the harnesses a compatibility matrix caught
adithya-s-k 4d10afd
harbor ui: an API key field, and say plainly what you are about to get
adithya-s-k 67597d8
tests: cover the negotiation, the eval path, and whether the logprobs…
adithya-s-k 1a5014c
docs: two kinds of rollout, and what each endpoint gives you
adithya-s-k 95ceb5c
Merge remote-tracking branch 'upstream/main' into harbor-integration
adithya-s-k e69e935
capture: link turns symmetrically, and catch retries of the first call
adithya-s-k 846d805
capture: refuse to present an untrainable turn as a trainable one
adithya-s-k 1a15811
harbor: keep a bad reward or a bad sequence from reaching the trainer
adithya-s-k 14fc268
capture: gate the session routes, and survive odd upstream replies
adithya-s-k 9838206
capture: dialect and transport fixes from review
adithya-s-k 77bcd15
harbor: assume the weaker capture tier until the probe says otherwise
adithya-s-k 7f0e26a
tests: cover the review findings
adithya-s-k 3357767
harbor: one upstream rename should degrade one agent, not eight
adithya-s-k 31d0939
capture: record the sampling parameters each turn was drawn with
adithya-s-k ad14f55
harbor: mask the aux node's own tokens, not its neighbours'
adithya-s-k 3c71f5e
capture: count tokens in Google's request shape too
adithya-s-k 1c8479c
harbor: don't strand the capture proxy when the tunnel fails
adithya-s-k 4b857b6
capture: sample from the policy when the rollout is trainable
adithya-s-k 5986b79
scripts: measure the top_p truncation bias in logprob_parity
adithya-s-k d2fc5f5
capture: the engine belongs to the rollout, not to the server
adithya-s-k 5e82fe2
harbor: name the engine per rollout, alongside the harness and the sa…
adithya-s-k 7a2056a
harbor: carry each turn's request on the wire
adithya-s-k 9c209af
harbor: serve datasets without an engine, and cap agent output budgets
adithya-s-k 1483c5d
Merge remote-tracking branch 'upstream/main' into harbor-integration
adithya-s-k 9dcde4b
harbor: a served deployment does not need an engine to be useful
adithya-s-k c8535e5
capture: rewrite the outgoing model from the session's engine, not th…
adithya-s-k 3924da8
capture: formatting (usort + ruff), as CI runs it
adithya-s-k c03e50e
harbor: let the caller bound a rollout
adithya-s-k 33b57cc
harbor_env: loop-owning sessions, so a stock TRL worker can train on …
adithya-s-k e5d15fb
harbor_env: four review findings on the session factory
adithya-s-k 6c0ad30
core: close() on an MCP client must actually close it
adithya-s-k 33e2df1
harbor: n_trainable_tokens must agree with the turns beside it
adithya-s-k c2dac3b
harbor ui: run against the endpoint the user validated
adithya-s-k 770ea8d
harbor: compute the token total after the turns exist, and let Run re…
adithya-s-k acd3c78
harbor_env: measure_prompt_skew has to decode tool-call arguments, li…
adithya-s-k b8b90e9
harbor: remove hermes support
adithya-s-k 7e4688e
harbor harness: one client per rollout, not one per factory
adithya-s-k 50e21cb
harbor: release both halves on teardown, and do not leak the proc-env…
adithya-s-k 2854581
harbor tests: drop the asyncio import the hermes deletion orphaned
adithya-s-k ef11f95
harbor: let a caller bound how many steps an agent takes
adithya-s-k 0ef184a
harbor harness: say when a rollout's packed row is big enough to OOM …
adithya-s-k 5be8cb1
harbor harness: refuse an empty indices list instead of silently sele…
adithya-s-k 9bf5e7f
harbor: one E2B template per distinct environment, not per task
adithya-s-k ca4d2e5
harbor: stop credential-by-env harnesses serialising, with context-lo…
adithya-s-k 0308ac0
Merge upstream/main into harbor-integration
adithya-s-k 77e6920
harbor: run the repo's own usort+ruff pipeline over three stragglers
adithya-s-k 962ffcb
harbor: attribute a re-rendered system prompt to its parent, and let …
adithya-s-k c8bc449
harbor: 10 validated harnesses, and say why the other five are not
adithya-s-k 664a93c
Merge upstream/main into harbor-integration (brings the RFC 008 rever…
adithya-s-k File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| """harbor_env: run Harbor tasks with token-level capture. | ||
|
|
||
| The implementation lives in `openenv.harbor` and `openenv.core.harness.capture`; this package is | ||
| deployment packaging only (manifest, Dockerfile, ASGI entry point). The client and result models are | ||
| re-exported here so `from harbor_env import HarborEnv` works, matching every other environment. | ||
|
|
||
| Examples: | ||
|
|
||
| ```python | ||
| from harbor_env import HarborEnv | ||
|
|
||
| with HarborEnv(base_url="http://localhost:8000") as env: | ||
| split = env.splits()[0]["name"] | ||
| result = env.run_rollout(split=split, task_index=0, harness="opencode", sandbox="e2b") | ||
| print(result.reward, result.n_turns) | ||
| ``` | ||
| """ | ||
|
|
||
| from openenv.harbor.client import HarborEnv | ||
| from openenv.harbor.models import ( | ||
| HarborConversation, | ||
| HarborRolloutResult, | ||
| HarborTaskRef, | ||
| HarborTurn, | ||
| ) | ||
|
|
||
| __all__ = [ | ||
| "HarborEnv", | ||
| "HarborConversation", | ||
| "HarborRolloutResult", | ||
| "HarborTaskRef", | ||
| "HarborTurn", | ||
| ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| """Typed client for a deployed harbor_env.""" | ||
|
|
||
| from openenv.harbor.client import HarborEnv | ||
|
|
||
| __all__ = ["HarborEnv"] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| """Wire types, re-exported so `from harbor_env.models import ...` works like other envs.""" | ||
|
|
||
| from openenv.harbor.models import ( | ||
| HarborRolloutResult, | ||
| HarborState, | ||
| HarborStepResult, | ||
| HarborTaskRef, | ||
| HarborTurn, | ||
| ) | ||
|
|
||
| __all__ = [ | ||
| "HarborRolloutResult", | ||
| "HarborState", | ||
| "HarborStepResult", | ||
| "HarborTaskRef", | ||
| "HarborTurn", | ||
| ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| spec_version: 1 | ||
| name: harbor_env | ||
| type: space | ||
| runtime: fastapi | ||
| app: server.app:app | ||
| port: 8000 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| [project] | ||
| name = "openenv-harbor-env" | ||
| version = "0.1.0" | ||
| description = "Run Harbor tasks with a coding agent and capture token-level training data" | ||
| requires-python = ">=3.12" | ||
| dependencies = [ | ||
| "openenv", | ||
| # Every sandbox backend, listed individually rather than via `harbor[cloud]`. | ||
| # `harbor[cloud]` cannot be installed at all: it pulls both `langsmith[sandbox]`, which | ||
| # requires `websockets>=15`, and `tensorlake`, which requires `websockets>=13,<14`. uv | ||
| # reports the pair as unsatisfiable and the image build fails. Neither is a sandbox backend | ||
| # we offer, so both are dropped and everything else kept. Re-check on a Harbor upgrade. | ||
| "harbor[e2b,modal,daytona,gke,ec2,runloop,novita,blaxel,beam,islo,opensandbox,cwsandbox,use-computer,cua]>=0.20.0", | ||
| "huggingface_hub>=1.12", | ||
| "fastapi>=0.104", | ||
| "uvicorn[standard]>=0.24", | ||
| "httpx>=0.27", | ||
| "gradio>=5", | ||
| ] | ||
|
|
||
| [project.scripts] | ||
| server = "server.app:main" | ||
|
|
||
| [build-system] | ||
| requires = ["setuptools>=61"] | ||
| build-backend = "setuptools.build_meta" | ||
|
|
||
| [tool.setuptools] | ||
| packages = ["server"] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| ARG BASE_IMAGE=ghcr.io/huggingface/openenv-base:latest | ||
| FROM ${BASE_IMAGE} AS builder | ||
|
|
||
| # Harbor requires Python >= 3.12 while openenv-base ships 3.11, so `uv sync` downloads its own | ||
| # interpreter and the venv's bin/python becomes a symlink into uv's install dir. Pinning that dir | ||
| # (and creating it up front, so the COPY below cannot fail when uv reuses a system interpreter) | ||
| # is what lets the runtime stage carry the interpreter the venv actually points at. Without it the | ||
| # venv arrives with a dangling bin/python and the container dies with "not found". | ||
| ENV UV_PYTHON_INSTALL_DIR=/opt/uv-python | ||
| RUN mkdir -p /opt/uv-python | ||
|
|
||
| WORKDIR /app/env | ||
| COPY . /app/env | ||
| RUN --mount=type=cache,target=/root/.cache/uv \ | ||
| if [ -f uv.lock ]; then uv sync --frozen --no-editable; else uv sync --no-editable; fi | ||
|
|
||
| FROM ${BASE_IMAGE} | ||
| COPY --from=builder /opt/uv-python /opt/uv-python | ||
| COPY --from=builder /app/env/.venv /app/.venv | ||
| COPY --from=builder /app/env /app/env | ||
|
|
||
| # Fail at build time rather than at startup if the interpreter did not survive the stage boundary. | ||
| RUN /app/.venv/bin/python -c "import sys; print('venv python', sys.version)" | ||
|
|
||
| ENV PATH="/app/.venv/bin:$PATH" | ||
| # `harbor push` bundles the working tree's openenv/ into /app/env when pushing from a source | ||
| # checkout; PYTHONPATH puts it ahead of the released wheel in site-packages, which has no | ||
| # `openenv.harbor` until this lands upstream. | ||
| ENV PYTHONPATH="/app/env:$PYTHONPATH" | ||
| ENV ENABLE_WEB_INTERFACE=true | ||
|
|
||
| HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ | ||
| CMD /app/.venv/bin/python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1 | ||
|
|
||
| EXPOSE 8000 | ||
| CMD ["sh", "-c", "cd /app/env && exec /app/.venv/bin/python -m uvicorn server.app:app --host 0.0.0.0 --port 8000"] |
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,144 @@ | ||
| """ASGI entry point for a deployed harbor_env. | ||
|
|
||
| Everything is read from the environment so the same image serves any dataset and any engine without | ||
| a rebuild — which is what makes this deployable to a Space: | ||
|
|
||
| OPENENV_DATASETS comma-separated dataset specs (HF repo id, local dir, harbor name@version) | ||
| OPENENV_LLM_URL DEFAULT OpenAI-spec endpoint; optional, since rollouts may name their own | ||
| OPENENV_MAX_OUTPUT_TOKENS cap on what an agent may request per turn (default 8192) | ||
| OPENENV_MODEL served model id; read from the engine when it serves exactly one | ||
| OPENENV_LLM_API_KEY credential for a hosted endpoint; a Space SECRET, never a variable | ||
| OPENENV_LLM_AUTH_HEADER header to send it under, when not `Authorization` | ||
| E2B_API_KEY / MODAL_TOKEN_ID+MODAL_TOKEN_SECRET whichever sandboxes you want offered | ||
|
|
||
| OPENENV_LLM_URL is OPTIONAL. With no engine the server still comes up serving its datasets, and each | ||
| rollout names the engine it wants (`run_rollout(llm_url=...)`), which is probed once and cached. That | ||
| is the useful way round: a dataset tree is thousands of files and prebuilt sandbox templates, while an | ||
| engine restarts every training run — and a train-tier engine and an eval-tier one are usually both | ||
| wanted against the same task suite. | ||
|
|
||
| Naming an engine here still works and makes it the default for rollouts that name none. | ||
|
|
||
| An endpoint that cannot return token ids is not a boot failure either: the Space comes up as an EVAL | ||
| deployment, which is what a hosted provider can honestly offer. `capture_level` says which it is, and | ||
| the UI shows it. | ||
|
|
||
| The capture proxy rides on this same app rather than on a second port. A Space publishes exactly one | ||
| port and one URL, so the proxy is mounted at `/capture` and the sandbox reaches it at | ||
| `https://<space>.hf.space/capture`. Nothing is forwarded and no second listener is opened. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import os | ||
|
|
||
| from openenv.harbor.serving import HarborService, build_app | ||
|
|
||
| _DATASETS = [ | ||
| d.strip() for d in os.environ.get("OPENENV_DATASETS", "").split(",") if d.strip() | ||
| ] | ||
| _LLM_URL = os.environ.get("OPENENV_LLM_URL", "") | ||
| _MODEL = os.environ.get("OPENENV_MODEL", "") | ||
| _API_KEY = os.environ.get("OPENENV_LLM_API_KEY", "") or None | ||
| _AUTH_HEADER = os.environ.get("OPENENV_LLM_AUTH_HEADER", "") or "Authorization" | ||
| _LLM: dict = {} | ||
| # "text", not "tokens". This is the value used when the probe never ran or never finished — an | ||
| # ambiguous model list, an unset model, an endpoint that raised — and defaulting it optimistically | ||
| # meant a Space in exactly that state built its proxy at token level and stamped every rollout it | ||
| # produced as trainable. Which is the one failure this whole capture level exists to prevent, so the | ||
| # unknown case has to assume the weaker tier and be corrected upward only by evidence. | ||
| _CAPTURE_LEVEL = "text" | ||
|
|
||
| # Ask the endpoint what it serves when `OPENENV_MODEL` was not set, the same way `harbor serve` does. | ||
| # Without this the proxy has no served model id and stops rewriting `model` on the way upstream, so | ||
| # whatever name the harness happened to use is forwarded verbatim and the engine rejects it. The | ||
| # report is kept so `capabilities()` can state whether capture is actually supported here. | ||
| if _LLM_URL: | ||
| try: | ||
| from openenv.core.harness.capture.validate_llm import list_models, validate_llm | ||
|
|
||
| if not _MODEL: | ||
| served = list_models(_LLM_URL, api_key=_API_KEY, auth_header=_AUTH_HEADER) | ||
| _MODEL = served[0] if len(served) == 1 else "" | ||
| if _MODEL: | ||
| _report = validate_llm( | ||
| _LLM_URL, _MODEL, api_key=_API_KEY, auth_header=_AUTH_HEADER | ||
| ) | ||
| _CAPTURE_LEVEL = _report.capture_level or "text" | ||
| _LLM = { | ||
| "url": _LLM_URL, | ||
| "model": _report.model, | ||
| "ok": _report.ok, | ||
| "findings": _report.findings, | ||
| "served_models": _report.served_models, | ||
| "capture_level": _report.capture_level, | ||
| "rollout_type": _report.rollout_type, | ||
| "trainable": _report.trainable, | ||
| "reachable": _report.reachable, | ||
| "param_fixes": _report.param_fixes, | ||
| "authenticated": bool(_API_KEY), | ||
| } | ||
| except Exception as exc: # noqa: BLE001 - a Space must still boot so the UI can show the fault | ||
| _LLM = { | ||
| "url": _LLM_URL, | ||
| "model": _MODEL, | ||
| "ok": False, | ||
| "reachable": False, | ||
| "capture_level": _CAPTURE_LEVEL, | ||
| "findings": [ | ||
| f"could not reach the LLM at startup: {type(exc).__name__}: {exc}" | ||
| ], | ||
| } | ||
|
|
||
| if not _MODEL: | ||
| # Reached when the endpoint serves several models and none was named. The proxy then cannot | ||
| # rewrite `model` upstream, so nothing will work anyway — but it must not claim to be | ||
| # trainable while failing. | ||
| _LLM.setdefault("url", _LLM_URL) | ||
| _LLM.setdefault("ok", False) | ||
| _LLM.setdefault("reachable", False) | ||
| _LLM.setdefault( | ||
| "findings", | ||
| [ | ||
| "no model resolved: set OPENENV_MODEL, or point at an endpoint that serves " | ||
| "exactly one model" | ||
| ], | ||
| ) | ||
| _LLM["capture_level"] = _CAPTURE_LEVEL | ||
|
|
||
| # Resolve capture before the app is built. A Space gives no separate boot hook, the UI needs the | ||
| # proxy's public URL to exist by the time anyone presses Run, and `build_app` has to see the service | ||
| # in order to mount it. | ||
| # | ||
| # Started unconditionally: the proxy has to be listening and publicly reachable before any rollout | ||
| # can name an engine, and it is the SESSION that carries the engine. Gating this on OPENENV_LLM_URL | ||
| # was what made an engineless server useless — every rollout answered "server not initialised". | ||
| _service = HarborService( | ||
| llm_url=_LLM_URL, | ||
| model=_MODEL, | ||
| datasets=_DATASETS, | ||
| capture_port=int(os.environ.get("OPENENV_CAPTURE_PORT", "8100")), | ||
| expose=os.environ.get("OPENENV_EXPOSE", "gradio"), | ||
| api_key=_API_KEY, | ||
| auth_header=_AUTH_HEADER, | ||
| capture_level=_CAPTURE_LEVEL, | ||
| max_output_tokens=int(os.environ.get("OPENENV_MAX_OUTPUT_TOKENS", "8192")) or None, | ||
| ) | ||
| # On a Space this only computes the public URL and flags the app for mounting; off one it | ||
| # publishes the capture port the usual way. | ||
| _service.start() | ||
| HarborService.set_current(_service) | ||
|
|
||
| os.environ.setdefault("ENABLE_WEB_INTERFACE", "true") | ||
|
|
||
| app = build_app(datasets=_DATASETS, llm_url=_LLM_URL, model=_MODEL, llm=_LLM) | ||
|
|
||
|
|
||
| def main() -> None: | ||
| import uvicorn | ||
|
|
||
| uvicorn.run(app, host="0.0.0.0", port=int(os.environ.get("PORT", "8000"))) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.