Repository navigation
Harbor foundation: every CLI in a task's container, and eval, split and stream runs - #105
Open
iamziyuzhao wants to merge 5 commits into
Open
iamziyuzhao wants to merge 5 commits into
iamziyuzhao wants to merge 5 commits into
Conversation
…as Hermit's own sessions are What the Harbor runner needs of the layers every method shares, so that a trial in a task's container is one more session of the CLI's Base Agent: - The hosts deploy for a caller that serves the adapter's tools itself (`--no-serve-tools` on every host; Pi's and dsh's `prepare(serve_tools=False)` names no server, and the acting agents' workspace now passes the flag on, so a container session of Hermit's own runs gets no server row it cannot start) and take one task's context (`--context-file`). - Every CLI Base Agent reads a stream its caller read elsewhere: `outcome()` with no stream file reads the stream's text as it reads the file, the reply included; a missing or empty stream is a session not served, never a TypeError. - A Codex session whose sign-in or key the provider refused is `login_failed` (the run stops) if its turn did not complete, read from its own notices: never one it recovered from, what its log quotes of the session's tool calls, or an event's text (U+0085, U+2028 and U+2029 end no line of a stream); `unavailable` comes from its last failure. - hermit.agent.base.trajectory, the AHE Debugger, Harness²'s bench and RRSI's render read a Harbor trial of any CLI (`agent/agent-<backend>.jsonl`, then Harbor's own output files, then its ATIF trajectory, which the Debugger takes before those files), and AHE's texts (EVOLVER.md's too) name that trial's files (the stream, hook audit and state under agent/; the record's instruction, as the Debugger's `task0i.md`; START.md's tree). Harness²'s bench takes a Claude Code trial's final message as the Base Agent reads its reply. - A Harbor acting agent's model is bound and checked in the acting CLI's dialect (`agent (harbor, <cli>)`, an endpoint's too), the whole Binding spec kept, developer_role included, and its login is a contained session's (logins_of) unless `claude_bin` names this machine's Claude Code, whose version is then checked; the task set's benchmark file counts for both, as the runner merges it (relative to `normalize_config`'s `base`, else the working directory). hermit.harbor.clis is the table of those CLIs. tests/selection.py counts the recorded OpenCode and dsh streams as shared: the methods' tests read them too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FcJCJanVqQuJyChDevJNYP
…dict, orders and checkpoint tests
How a run is done is the config's `run:` block, so that a benchmark plugs into each protocol by its split files and
nothing of its own:
- `mode: eval` is a config for `hermit eval` alone: no train split, Evolver, task-info or budget, and `hermit train`
refuses it; `root` there is the template's own adapter. `split` trains, then tests the final adapter on
`data.test`. `stream` scores each row once, by the adapter the earlier rows taught, before the learner sees it:
one epoch, no shuffle, the final adapter the last step's, run_agent only on rows already reached (no validation
row may be a train row); its prequential score (overall, by halves, by step) goes to stream_summary.json and the
summary. An Evolver whose protocol scores its own steps gives that score (Evolver.step_scores, None for a row it
leaves unscored, left out and counted; Harness²: the deliverable it chose without the labels), the rollouts' mean
kept beside it.
- `feedback: full | score | none` is what the learner is shown of each verdict, in the learning material (a README
per policy), run_agent's results and the logs handed to it (the verifier's output and result files gone under
score and none); a stream must name it, and `hermit train` refuses (`hermit check` reports) one below what the
Evolver reads: `Evolver.min_feedback`, `score` by default (`full` for AHE Adapter, `none` for the CLI Evolvers
and Harness²), or `min_feedback_for(params)`. An Evolver that keeps the labels from its sessions itself
(`hides_labels`: Harness²) gets the full records. The CLI Evolvers', Meta-Harness Adapter's and ASPIRE's prompts
promise no more than the run shows.
- `orders` runs a stream once per seed or order file (a task's rows kept together), combined in streams.json, each
order to train checked before the first trains; `checkpoints` test the commit after the step a row ends on a split
of their own, with run.state's environment and run.score's scores, after training or as each is reached (outside
the training budget's hours); one inside a step is refused before training. `hermit eval --checkpoints` runs those
without results.
- A multi-step Harbor task's steps run in order: `data.shuffle` moves its rows as one, an order or split taken as
given or an Evolver's `next_tasks` that names a step ahead of an earlier one is refused before any run takes it,
and run_agent runs no training row that is a step.
- A run the pipeline says is not the agent's result (`RunOutput.extra["unscored"]`: a Harbor infra trial) has status
infra: never judged, null in steps.jsonl, left out of the Trainer's and hermit eval's means and counted (a 0 to an
Evolver that averages the records itself, as GEPA does); `eval.infra_retries` runs those rows again, a multi-step
task's steps excepted, until a halt. `hermit eval --split` tests another split; each repeat of `eval.repeats` > 1
hands the pipeline rows that name it (`repeat`); pass@k counts a Harbor task's rows as its attempts and a
multi-step task's step (`<task>:<step>`) as a task; "solved" is the judge's `passed` when it is a bool (unknown
when it is None), else a score of at least 1.
- An acting agent of Harbor's own takes no adapter: `hermit train` refuses it, `hermit check` reports it, and
`hermit eval` runs it with `--adapter none` only. A `run:` or `eval:` left empty takes its defaults, and so does a
key left empty or left out of a whole-section override (`eval={repeats: 3}`).
- The run's secrets include order files, checkpoint splits, the score hook and a Harbor runner's task and job
directories.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FcJCJanVqQuJyChDevJNYP
…the adapter's tools, context and notes A Harbor trial ran only Codex well (and Claude Code through code of its own); the adapter's tools, a per-task context and an adapter's notes never reached the container, and infra trials counted as zeros. Now: - One HermitXXX class per CLI (hermit.harbor.clis: codex, claude, pi, opencode, dsh, mini-swe-agent) over one executor (harborside/hermit.py). Hermit's side renders each trial's session with the CLI's own Base Agent (hermit.harbor.launch: the home, the command, the environment; host paths made the container's) against the adapter deployed as `hermit deploy` lays it out, and harborside uploads and runs it as the task's agent user: the prompt on stdin, the stream in agent/agent-<backend>.jsonl, the deployment record always there (so an adapter's hooks run on Codex too, HERMIT_HOOK_LOGS set), a session still running at Harbor's timeout killed with its process group. The steps after the session still run past that timeout; a copy that failed is dropped, never kept in part. A `<cli>_bin` serves only at the asked-for version and when the agent user may execute it. The runner reads the stream back with the same Base Agent: reply, served, the plan's or the provider's refusal, the usage at `agent.params.price`, else at the backend's price. - The adapter's tools are served from this machine on the Docker bridge's gateway (hermit.harbor.tools, every call logged); a per-task context is computed in the adapter sandbox (hermit.harbor.context); an adapter's notes come back after the session (memory/ in the log). - A key or a token reaches the container by its variable's name only (`docker compose exec -e NAME`; Claude Code's subscription token on a descriptor), never in a command line, a file, the plan or a log; Codex's ChatGPT sign-in is a file of its home there (mode 600). Codex and Claude Code run on the user's subscription (`login: chatgpt | claude`) in the plan's slots and holds, each trial noting the plan's usage (so `MAX_USED` holds trials back): a refused session again after the window, one the provider could not serve after a pause unless it ran out of the agent's time, a lost sign-in never, none once another trial halted the run; the trial's usage is every run's. Codex's refreshed sign-in comes back and becomes the shared copy. A task whose agent network is not public gets the model's and tools' hosts allowlisted and web search off; `web_search` must be set. - The answer-only agent (`cli: answer`) makes one model call through an endpoint the runner serves and writes the answer into the container (LAB-Bench's questions); a request still in flight when the trial ends is given up, its cost unknown, and on a model a CLI reaches on a login the trial waits for that plan first (a request the plan held past the agent's time is infra, `usage_limit`). A question or attachments it cannot read fail the launch. - The judge scores the verifier's reward whenever the verifier gave one, a timeout or an agent error included (Harbor's own mean), says "passed" at `pass_reward` (null for a score scale) and leaves out what `verifier_redact` matches. Not the agent's result, so unscored (infra): a verifier whose tools never downloaded or that hit a full disk, harborside's launch failing (HermitLaunchError, the CLI's install included), a session that never started or whose tools never came up, a lost sign-in, a Claude Code, Pi or OpenCode session or an answer request the provider refused for the run's configuration before serving any (its key, model, bill or a setting it does not take: `model_rejected`, no halt). A trial out of the agent's time is the agent's even after a provider notice, and so is a request refused for its input; the adapter's context over the CLI's limit beside the image's own is the adapter's 0, and so is a session the model's safeguards ended. - Startup refuses what every trial would: a login for another CLI, a login and a binding at once, a loopback model, a missing key or sign-in, a non-Docker environment for secrets by name, a plan the Base Agent cannot render, a `harbor` that is no executable file, mini-swe-agent without its package for a run that deploys adapters, the answer agent's attachments on a model a CLI reaches. - `python -m hermit.harbor.dataset prepare` makes an example's data from its benchmark file's `dataset:` block (the registry's tasks, rows, splits, smoke rows, a patch, the images' sizes with `image_sizes: true`, a lock). A download is moved into place only once whole, and what is in its way is refused first; a relative `data:` is the benchmark file's folder's. The lock holds no path of this machine and hashes only a mode's executable bit (one written under a umask other than 022 needs writing again); an unreadable lock is refused unless `--force`. - Also, from the Terminal-Bench 2.1 runs: a host failure (Docker's, the host's /tmp full) is infra and no trial starts while /tmp is short; the trial of a task whose name is longer than 32 characters is found; a `harbor run` past its wait budget is killed and its trial's compose projects, the verifier's too, taken down by name alone (no compose file or `COMPOSE_*` variable read). A stream's step runs its own plan, its verifier read as a trial's; each `hermit eval` repeat and each run gets streams of its own, stopped when the run ends unless a `hermit train` may resume them. The trial cache keys the launch, the whole row, the task's files, the model's price and the adapter as deployed; a cached trial costs nothing again. - OfficeQA's Harbor judge reads the final message from HermitCodex's stream (`agent-codex.jsonl`): a task set built before needs building again. Checked in real containers on this machine: Codex on the ChatGPT login (tasks with an open and a closed network, the adapter's MCP tool, AHE's hooks, Continual Harness's notes, ASPIRE's template) and Claude Code on a subscription token (the MCP tool, AHE's hooks); Pi, OpenCode, dsh and mini-swe-agent on a scripted model; the answer agent; a three-step stream. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FcJCJanVqQuJyChDevJNYP
…ings Round 7's findings, listed as known limitations when the PR was opened: - A client of a CLI's chat endpoint counts what a session that failed or that the provider refused used, which the endpoint reports in its error body, each failed attempt's too (ChatClient count_error_usage, set by endpoint_spec; the answer agent's relay takes it). - harborside keeps the session's own error when Harbor's timeout lands in the steps after it (a launch failure or a non-zero exit stays infra, not the agent's timeout). - CODEX_RECORD takes a record with span prefixes (RUST_LOG=codex_core=info). - A split, order, checkpoint split or --split file that lists a step but not every earlier step of its stream is refused before any run, and so is a next_tasks request naming a step whose earlier step has neither run in the epoch nor come earlier in the request. - The benchmark file's price applies to its model however the config writes it. - Attempts are rollouts x repeats (CHANGELOG, evaluate.py); the troubleshooting row of hermit_login_failed covers Codex. - The trial cache's adapter digest hashes whether a file is executable, not the bits a umask sets. - hermit check's probe neither takes a trial from the trial cache nor keeps one. - CHANGELOG (Breaking): the runner's codex_login is gone (login: chatgpt), a table CLI's trial needs web_search, secrets by name run on local Docker only. The post-PR audit's findings (on 0483ae0): - An eval config's tree (trainer.tree_dir, with no template) is a run secret. - dataset prepare records the tasks' source (the ref with repo, registry_url and registry_path) in the lock and in the download, never reuses another source's download, compares the lock's source, and keys the default data directory by the source. - A task Harbor runs for none of Hermit's agents (MCP servers, a skills folder, a Windows container) is an infra rollout (unsupported_task), never handed to Harbor; the task set's other tasks run. - Harbor's own claude-code keeps Harbor's per-task web rule unless web_search is false; CLAUDE_FORCE_OAUTH, or the subscription's token as its only sign-in, is refused at startup (CHANGELOG, Breaking). - Harness²'s stream score is 0 for a composed harness that delivered nothing (None when every run of it was infra), never H0's. - A checkpoint test a halt cut short is not kept as done: its summary goes to halted.json, and hermit eval --checkpoints runs it again. - dsh's context limit counts the image's own file as dsh escapes it. - Docs: Claude Code's own cost is kept on Anthropic's API. CHANGELOG: hermit eval --adapter root starting a tree that does not exist yet, the hosts' --no-serve-tools and --context-file, and which locks written before need writing again. The review of those fixes: - A model the config gives as an endpoint mapping keeps its own price (or none) beside a benchmark file that prices its own model. - Harbor's own claude-code is given a bound model's Messages API root (Claude Code appends /v1/messages; it was sent to /v1/v1/messages), and a model at Anthropic's own API by name with its key's variable, so the subscription-only refusal also holds for a loaded config's model; a bill in acting_agent.env or the model's own key variable counts. - Harbor's mini-swe-agent trials refuse what MiniSweAgent.run refuses (web search, a model class Hermit does not read): CliAgent.check_settings, called by run and by the launch plan's render, so at startup too. - dataset.lock names a registry file of this machine by its file name; the download's own record keeps the path. - eval.infra_retries does not rerun an unsupported_task row. Each code fix has a regression test that fails with the fix undone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FcJCJanVqQuJyChDevJNYP
test_an_endpoint_models_price_is_its_own_never_the_benchmark_files_model builds the Harbor runner, whose startup checks that the harbor command is an executable file. It now gives the runner a stand-in, as the runner's own tests do (fake_harbor), so it passes on CI, which installs no Harbor venv. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FcJCJanVqQuJyChDevJNYP
9 of 10 tasks
This branch has not been deployed
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Change
This PR is the groundwork for running Harbor's benchmarks with Hermit's methods. After it, a benchmark from Harbor's
registry plugs in as an example folder: a
utils/benchmark.yaml, from whichpython -m hermit.harbor.dataset preparemakes the data and the split files. The folder's only code, when it needs any, is a patch of the downloaded tasks
(
dataset.patch; LAB-Bench FigQA's removes the shuffle's seed, which gives the answer away). The config'srun:block then picks one of three ways to run it. This PR holds no benchmark folder. It covers what Terminal-Bench 2.0,
SWE-bench Verified, AlgoTune, Terminal-Bench Science and LAB-Bench FigQA need; a follow-up PR adds only their
folders. It builds on #56 (Harbor's task format as Hermit's one task protocol, a stream as a multi-step task). It also
covers the CLIs
mainadded since: mini-swe-agent (#49), Pi (#89; every method on Pi, #92), OpenCode (#94) and theDeepSeek Harness (
dsh, #90) each run in a Harbor trial, as Codex and Claude Code now do.The decisions it implements:
task's container. A Harbor trial is then one more session of that CLI: the same home, command, stream and reading
of the outcome as Hermit's own sessions. The answer agent (
cli: answer) makes its one model call from Harbor'sprocess; Harbor's own agents run as Harbor installs them.
there (mode 600).
hermit eval's means leave it out and count it. An Evolver that averages the records itself, as GEPA does, still takes itas a 0, as on
main.Harbor's own job mean does.
eval,splitorstream. Separately,run.feedbacksets what the learner is shown of each verdict:full,scoreornone.The first three commits follow these parts. The fourth holds the fixes of the reviews since the PR was opened (its
message lists them), and the fifth makes one of its tests run where Harbor's venv is not installed (CI); each part
below describes the code as it is with them.
1. Shared layers
What the Harbor runner needs of the layers every method shares, so that a trial in a task's container is one more
session of the CLI's Base Agent:
--no-serve-toolson every host; Pi's anddsh's
prepare(serve_tools=False)names no server, and the acting agents' workspace now passes the flag on, so acontainer session of Hermit's own runs gets no server row it cannot start) and take one task's context
(
--context-file).outcome()with no stream file reads the stream'stext as it reads the file, the reply included; a missing or empty stream is a session not served, never a TypeError.
login_failed(the run stops) if its turn did notcomplete, read from its own notices: never one it recovered from, what its log quotes of the session's tool calls,
or an event's text (U+0085, U+2028 and U+2029 end no line of a stream);
unavailablecomes from its last failure.(
agent/agent-<backend>.jsonl, then Harbor's own output files, then its ATIF trajectory, which the Debugger takesbefore those files), and AHE's texts (EVOLVER.md's too) name that trial's files (the stream, hook audit and state
under agent/; the record's instruction, as the Debugger's
task0i.md; START.md's tree). Harness²'s bench takes aClaude Code trial's final message as the Base Agent reads its reply.
agent (harbor, <cli>), anendpoint's too), the whole Binding spec kept, developer_role included, and its login is a contained session's
(logins_of) unless
claude_binnames this machine's Claude Code, whose version is then checked; the task set'sbenchmark file counts for both, as the runner merges it (relative to
normalize_config'sbase, else the workingdirectory). hermit.harbor.clis is the table of those CLIs.
tests/selection.py counts the recorded OpenCode and dsh streams as shared: the methods' tests read them too.
2. Run modes
How a run is done is the config's
run:block, so that a benchmark plugs into each protocol by its split files andnothing of its own:
mode: evalis a config forhermit evalalone: no train split, Evolver, task-info or budget, andhermit trainrefuses it;
rootthere is the template's own adapter, and the tree it tests (trainer.tree_dir, with no template)is a run secret as a training run's is.
splittrains, then tests the final adapter ondata.test.streamscores each row once, by the adapter the earlier rows taught, before the learner sees it:one epoch, no shuffle, the final adapter the last step's, run_agent only on rows already reached (no validation
row may be a train row); its prequential score (overall, by halves, by step) goes to stream_summary.json and the
summary. An Evolver whose protocol scores its own steps gives that score (Evolver.step_scores, None for a row it
leaves unscored, left out and counted; Harness²: the deliverable it chose without the labels, 0 for a composed
harness that delivered nothing, never the base harness's score), the rollouts' mean kept beside it.
feedback: full | score | noneis what the learner is shown of each verdict, in the learning material (a READMEper policy), run_agent's results and the logs handed to it (the verifier's output and result files gone under
score and none); a stream must name it, and
hermit trainrefuses (hermit checkreports) one below what theEvolver reads:
Evolver.min_feedback,scoreby default (fullfor AHE Adapter and EvoSafeHarness;noneforthe CLI Evolvers, Harness², MWM, ReHarness and Continual Harness), or
min_feedback_for(params). An Evolver thatkeeps the labels from its sessions itself (
hides_labels: Harness²) gets the full records. The CLI Evolvers',Meta-Harness Adapter's and ASPIRE's prompts promise no more than the run shows.
ordersruns a stream once per seed or order file (a task's rows kept together), combined in streams.json, eachorder to train checked before the first trains;
checkpointstest the commit after the step a row ends on a splitof their own, with run.state's environment and run.score's scores, after training or as each is reached (outside
the training budget's hours); one inside a step is refused before training.
hermit eval --checkpointsruns thosewithout results, and one a halt cut short (a lost login, a usage limit), which is not kept as done.
data.shufflemoves its rows as one, an order or split taken asgiven or an Evolver's
next_tasksthat names a step ahead of an earlier one is refused before any run takes it,and run_agent runs no training row that is a step.
RunOutput.extra["unscored"]: a Harbor infra trial) has statusinfra: never judged, null in steps.jsonl, left out of the Trainer's and hermit eval's means and counted (a 0 to an
Evolver that averages the records itself, as GEPA does);
eval.infra_retriesruns those rows again, a multi-steptask's steps and an
unsupported_taskexcepted, until a halt.hermit eval --splittests another split; each repeatof
eval.repeats> 1 hands the pipeline rows that name it (repeat); pass@k counts a Harbor task's rows as itsattempts and a multi-step task's step (
<task>:<step>) as a task; "solved" is the judge'spassedwhen it is a bool(unknown when it is None), else a score of at least 1.
hermit trainrefuses it,hermit checkreports it, andhermit evalruns it with--adapter noneonly. Arun:oreval:left empty takes its defaults, and so does akey left empty or left out of a whole-section override (
eval={repeats: 3}).directories.
3. Harbor
A Harbor trial ran only Codex well (Claude Code only as Harbor's own agent, which takes no adapter); the adapter's
tools, a per-task context and an adapter's notes never reached the container, and infra trials counted as zeros. Now:
(harborside/hermit.py). Hermit's side renders each trial's session with the CLI's own Base Agent
(hermit.harbor.launch: the home, the command, the environment; host paths made the container's) against the adapter
deployed as
hermit deploylays it out, and harborside uploads and runs it as the task's agent user: the prompt onstdin, the stream in agent/agent-.jsonl, the deployment record always there (so an adapter's hooks run on
Codex too, HERMIT_HOOK_LOGS set), a session still running at Harbor's timeout killed with its process group. The steps
after the session still run past that timeout; a copy that failed is dropped, never kept in part. A
<cli>_binservesonly at the asked-for version and when the agent user may execute it. The runner reads the stream back with the same
Base Agent: reply, served, the plan's or the provider's refusal, the usage at
agent.params.price, else at thebackend's price (Claude Code's own cost on Anthropic's API, as in Hermit's own sessions).
logged); a per-task context is computed in the adapter sandbox (hermit.harbor.context); an adapter's notes come back
after the session (memory/ in the log).
docker compose exec -e NAME; Claude Code'ssubscription token on a descriptor), never in a command line, a file, the plan or a log; Codex's ChatGPT sign-in is
a file of its home there (mode 600). Codex and Claude Code run on the user's subscription
(
login: chatgpt | claude) in the plan's slots and holds, each trial noting the plan's usage (soMAX_USEDholdstrials back): a refused session again after the window, one the provider could not serve after a pause unless it
ran out of the agent's time, a lost sign-in never, none once another trial halted the run; the trial's usage is
every run's. Codex's refreshed sign-in comes back and becomes the shared copy. A task whose agent network is not
public gets the model's and tools' hosts allowlisted and web search off; a CLI's trial (the answer agent's aside)
needs
web_searchset.Harbor's own
claude-codekeeps Harbor's rule for its web tools unlessweb_search: falseturns them off on everytask, and bills the key this process has, never the subscription. On a bound model it is given the backend's
Messages API root, and a model at Anthropic's own API by name (
anthropic/<model>).cli: answer) makes one model call through an endpoint the runner serves and writes theanswer into the container (LAB-Bench's questions); a request still in flight when the trial ends is given up, its
cost unknown, and on a model a CLI reaches on a login the trial waits for that plan first (a request the plan held
past the agent's time is infra,
usage_limit). A question or attachments it cannot read fail the launch. Itsrelay counts what a failed session used, which a CLI's endpoint reports in its error body.
(Harbor's own mean), says "passed" at
pass_reward(null for a score scale) and leaves out whatverifier_redactmatches. Not the agent's result, so unscored (infra): a verifier whose tools never downloaded or that hit a full
disk, harborside's launch failing (HermitLaunchError, the CLI's install included), a session that never started or
whose tools never came up, a lost sign-in, a Claude Code, Pi or OpenCode session or an answer request the provider
refused for the run's configuration before serving any (its key, model, bill or a setting it does not take:
model_rejected, no halt). A trial out of the agent's time is the agent's even after a provider notice, and so is arequest refused for its input; the adapter's context over the CLI's limit beside the image's own is the adapter's 0,
and so is a session the model's safeguards ended.
missing key or sign-in, a non-Docker environment for secrets by name, a plan the Base Agent cannot render or would
refuse to run (mini-swe-agent's web search, a model class Hermit does not read), a
harborthat is no executablefile, mini-swe-agent without its package for a run that deploys adapters, the answer agent's attachments on a model a
CLI reaches, and for Harbor's own
claude-codeCLAUDE_FORCE_OAUTHor the subscription's token as its only sign-in.A task Harbor runs for none of Hermit's agents (one with MCP servers, a skills folder or a Windows container) is
infra,
unsupported_task, and never handed to Harbor.python -m hermit.harbor.dataset preparemakes an example's data from its benchmark file'sdataset:block (theregistry's tasks, rows, splits, smoke rows, a patch, the images' sizes with
image_sizes: true, a lock). A downloadis moved into place only once whole, and what is in its way is refused first; a relative
data:is the benchmarkfile's folder's. The tasks' source (the ref with
repo,registry_urlandregistry_path) is recorded in the lockand in the download: another source's download is never reused, and the default data directory is the source's own.
The lock holds no path of this machine (a registry file of this machine by its file name) and hashes only a mode's
executable bit (one written before needs writing again when a file's mode was other than 644 or 755); an unreadable
lock is refused unless
--force.while /tmp is short; the trial of a task whose name is longer than 32 characters is found; a
harbor runpast itswait budget is killed and its trial's compose projects, the verifier's too, taken down by name alone (no compose
file or
COMPOSE_*variable read). A stream's step runs its own plan, its verifier read as a trial's; eachhermit evalrepeat and each run gets streams of its own, stopped when the run ends unless ahermit trainmayresume them. The trial cache keys the launch, the whole row, the task's files, the model's price and the adapter
as deployed (its bytes and executable bits, not a umask's); a cached trial costs nothing again, and
hermit check'sprobe never takes or keeps one. dsh's context limit counts the image's own file as dsh escapes it.
agent-codex.jsonl): a task set builtbefore needs building again.
Core changes. All the parts above change shared code:
hermit/agent/base/:cli.py,endpoint.py,login.py,records.py,trajectory.py), thehosts' command lines (
hermit/hosts/), the model binding (hermit/agent/model/backends.py), the chat client(
hermit/agent/model/llm.py: a failed session's usage from a CLI endpoint's error body) and the acting agents'workspace.
hermit/config.py,hermit/defaults.yaml: therun:block,eval.infra_retries,eval.infra_wait_s),the CLI,
hermit/evaluate.py,hermit/settings_check.pyandhermit/task.py.hermit/trainer/:trainer.py,material.py,rollout.py,budget.py,card.py).hermit/agent/evolver/base.py):min_feedback,min_feedback_for(params)andstep_scores.AgentEvolverandCommandEvolver(hermit/agent/evolver/agent.py) setmin_feedbacktonone, and the Trainerreads
hides_labelsfrom an Evolver that sets it (Harness²'s).hermit/harbor/): the runner, the launch plan, the CLI table, the tool server, the per-taskcontext, the dataset module and harborside.
hermit/isolate.py:a Harbor task set's tasks and jobs directories, and the tree an eval config names (
trainer.tree_dir, with notemplate).
agent_pipelinegives a Harbor agent what its streams and startup checks need: the run's identity, whether itdeploys adapters, and the model's CLI and login. It closes that agent with the pipeline, and
hermit check'sagent gets no trial cache.
conftest.pycollects no benchmark's own tests from a prepared task set, and.gitignoreignores the task set'slink (
examples/*/utils/tasks).hermit/sandbox.py,hermit/sandbox_relay.pyand the sandbox's defaults are unchanged.The
run:block's defaults keep the old behavior: with norun:block,run.mode: nulltrains only andrun.feedback: nullisfull.Changes an existing config meets:
codexacting agent and the agentEvolvers on Codex end the run, which
hermit trainresumes once the sign-in or key is restored. Before, only thatsession failed, and the run went on. So does a method's model call through Codex (a learner's
llm: {cli: codex},as in OfficeQA's
run_codex_luna.yamlconfigs: the endpoint answers 401hermit_login_failed, and the call raisesLoginFailed, which the methods' error handlers do not catch), and so do the Codex sessions of Harness², MWM, RRSIAdapter, the AHE Debugger and EvoSafeHarness. Before, such a call failed after the client's retries, and GEPA, OPRO,
ACE, SkillOpt, ReHarness and Continual Harness skipped it and went on.
run_agentreturns a result with statuserrorfor an id it does not run. Before, the whole call raised.hermit eval'ssolved_onceandpass_at_ktake the judge'spassedwhen it is a bool, before a score of 1.login: chatgpt, with the CLI's model name. The runner'scodex_login(default true) is gone: a HermitCodex trial with a plain model name and no login is refused at startup, and Harbor's
own
codexagent is no longer given that sign-in.claude-codeis no longer given the subscription's token norCLAUDE_FORCE_OAUTH(Harborwould put the token on a command line). It bills the key this process has; a run that asks it for the subscription
(
CLAUDE_FORCE_OAUTH) or has only the token is refused at startup. The subscription runs withacting_agent.cli: claudeandlogin: claude.claude-codeon a<backend>/<model>reference was given the backend's chat base URL, to whichClaude Code appends
/v1/messages(its requests went to/v1/v1/messages). It is now given the Messages API root,and a model at Anthropic's own API by name (
anthropic/<model>, with its key's variable).web_searchset.model: {cli: ...}) counts what a session that failed or that the providerrefused used, which the endpoint reports in its error body: a run's usage and cost take those sessions in.
eval.repeatsabove 1, each repeat hands the pipeline copies of the test rows that name it (repeat): apipeline that records the whole row records another row in each repeat (AgentDyn's
case_sha256differs byrepeat).
it recovered from: a session that recovered and then failed for its own reason is no longer run again as infra.
hermit eval --adapter rooton a run whose tree does not exist yet starts it from the template; before, it failed.writing again when any task file's mode was other than 644 or 755 (a umask other than 022 at download, or a file
of a mode of its own).
Documentation:
docs/framework/configuration.md,trainer.md,evolver.md,commands.md,custom-evolver.md, and the guidesevaluation.md,runs.mdandtraining.md. The docs' exampleEvolvers that read the judge's feedback (
docs/_examples/) setmin_feedback.docs/framework/task.md#benchmarks,examples/README.md,docs/guides/claude-subscription.md,model-backends.md,docs/get-started/installation.md, and OfficeQA's Harbor route (docs/recipes/officeqa.md,examples/officeqa/README.md).docs/framework/evolver.mdandconfiguration.mdlist therun.feedbackeach Evolver needs.docs/reference/compatibility.mdsays whatrun_agent's refusal of a multi-step task's steps does to each method.SkillOpt say which
run.feedbackthey need. Those of ACE, GEPA, OPRO, SkillOpt, Harness² and RRSI Adapter saywhat they do with a multi-step task, and Continual Harness's that a verifier's timeout is shown as one.
docs/reference/config-fields.md,docs/reference/compatibility.md, two tutorials(
docs/tutorials/custom-evolver.md,your-own-task.md) andCHANGELOG.md.Validation
On this PR's head
2ca476a, onmainat9c45a65. Linux x86_64 (RHEL 9) as a non-root user, Python 3.12.13, avenv with
.[test], mini-swe-agent 2.4.6 (themini-swe-agentextra) and thegepa,skillopt,harness2andrrsidependency groups (notace's sentence-transformers).tests/and the examples' own tests), aspython -m pytestcollects it, run in 12 parallel shardsof its files (each its own
pytestprocess and temporary directory): 1881 passed, 84 skipped, 4 failed, in 176 s. Thechecks after this one, and the real-container runs, ran on
7f02e95, the head before the last commit, which changesone test only (it gives the runner a stand-in
harbor, as the runner's own tests do, so that it runs on CI); therethe whole suite gave the same.
mainhere:tests/core/test_sandbox.py::test_a_linked_venv_runs_by_its_own_name_with_its_own_packages, and three tests inexamples/clbench_database/utils/tests/test_parity_golden.py(ContractParity,SessionPromptParity), whichcompare against a local MetaWorld checkout.
--integration17, the pi CLI 15, the dshCLI 11, MetaWorld 5, CL-Bench 4, the opencode CLI 4, a Docker opt-in 4, OfficeQA's data 2, the Harbor smoke 2 (run
below), torch 1.
2ca476a: 1817 passed, 152 skipped. On7f02e95it failed the one test the last commit changes, whichneeded Harbor's venv to build the runner; 1816 others passed.
0483ae0(the head as the PR was opened), the whole suite gave 1857 passed, 84 skipped and the same 4failures.
8a7710e, the whole suite fails the same 4 tests and nothing else.70340e6, the same holds. The whole suite ran on a first build of this commit and failed those 4, plus onetest in each of
tests/core/test_backends.pyandtests/core/test_deploy.pythat needs the third commit's code.Eleven of those two files' cases, those two among them, then moved to the third commit. The two files pass at
70340e6(71 tests), and the moved cases are the only difference between the two builds.HERMIT_HARBOR_SMOKE=1 python -m pytest tests/core/test_harbor_smoke.py(Harbor's venv and Docker): 2 passed.bash docs/build.sh(warnings are errors) andpre-commit run --all-filespass.hermit check,hermit trainandhermit eval --adapter finalondocs/_examples/first_training/run.yaml, passes;finalscores 1.0 on its 2 test tasks.test_run_modes.py70 tests in 101 s,test_harbor_launch.py18 in 0.3 s,test_harbor_dataset.py30 in 0.7 s,test_trajectory.py22 in 0.2 s,test_harbor_judge.py8 in 0.1 s. The PR's 25test files together take 11.1 min (909 passed, 4 skipped). These ran at the same time as the other checks above.
In real containers, on
7f02e95.hermit evalwithagent.name: harbor, on this machine's Docker, on smalltasks made for the check: write 42 to
/app/answer.txt(with an open network, and withnetwork_mode: "no-network"), the same with an instruction that begins with/, call the adapter's MCP tool and write what itreturned, answer a question (the answer agent), and a three-step stream that appends a line per step.
gpt-5.6-luna, effort lowprompt-onlyrootmagictool (one call, intool-calls.jsonl)agent/hook-calls.jsonl)memory/NOTES.mdin the trial's log)codex_bin)prompt-onlyrootclaude-haiku-5-5, effort low/shape: one shell call that writes the answer, then the answer. No real model was used for Pi, dsh, OpenCode,
mini-swe-agent or the answer agent: this machine has no DeepSeek, Fireworks or other API key.
session_failed): OpenCode's own server (opencode serve --stdio) sent no request in the agent's 600 s and was stopped at Harbor's timeout. Run again on its own, it scored1.0 in 24 s. The same happened once on
0483ae0, and on the fourth commit's first build it scored 1.0 at the firstrun.
key, and not the ChatGPT sign-in's tokens. Codex's own session files name the ChatGPT account's ids. A trial on that
login brings those files back into its log, and the learner is never shown them.
To reproduce: Harbor 0.25.0 in its own venv (
~/.cache/hermit/harbor-venv), Docker with the default bridge, and foreach acting agent its sign-in: Codex's ChatGPT login (
codex login) and its 0.164.0 build (the musl binary, mountedwith
codex_bin; one run without it, installed by Harbor), a Claude Code token (claude setup-token, exported asCLAUDE_CODE_OAUTH_TOKEN), and for the scripted model an OpenAI-compatible server on the bridge's gateway thatanswers one shell call writing 42, then
42. The configs arehermit evalconfigs of this shape (Codex's; theothers change
acting_agent.cli,model,loginand the sign-in's settings):each run as
hermit eval --config <file> --adapter <root, an adapter folder or a template> --split <one split>.Not verified:
acting_agent: {name}) in a container. For Harbor's ownclaude-codeon a bound model, thepath its requests take was checked with this machine's Claude Code (2.1.322) and a local listener: a base URL that
ends in
/v1givesPOST /v1/v1/messages, the Messages API root givesPOST /v1/messages. The rest is the tests'.hermit trainthrough Harbor: onlyhermit evalran in containers. The Trainer's Harbor path (the curated trialdirectories, what
run.feedbackstrips from them,run_agent) is covered by the tests' stand-in trials.examples/officeqa/utils/tests/).Review
run:block (mode,feedback,orders,checkpoints,checkpoints_during,state,score);eval.infra_retriesandeval.infra_wait_s;hermit eval --splitand--checkpoints;--no-serve-toolsand--context-file;python -m hermit.harbor.dataset prepareand a benchmark file'sdataset:block;acting_agent.clifor each CLI, and its sign-in, network, judge, cache and disk-gatesettings (
docs/framework/task.md#benchmarks);min_feedback,min_feedback_for(params),hides_labelsandstep_scores, and a pipeline'sRunOutput.extra["unscored"].run:block's defaults keep the old behavior (Change).verifier's reward, a timeout or an agent error included. Infra is unscored, counted and run again with
eval.infra_retries: a host or Docker failure, a launch that failed, a session that never started, or aClaude Code, Pi, OpenCode or answer-agent request the provider refused for the run's configuration. A task Harbor
runs for none of Hermit's agents is infra too, never started and not run again (
unsupported_task).and
hermit evalruns none of them again.hermit trainstill runs its remaining steps on those rows, as onmain. A lost sign-in in Hermit's own CLI sessions ends the run (LoginFailed).hermit evaland Harbor.test_run_modes.pygoes throughConfig.load, the Trainer,hermit.evaluate,run_checkand the CLI.tests/core/(new:test_run_modes.py,test_trajectory.py,test_harbor_launch.py,test_harbor_dataset.py); a method's are intests/methods/<name>/; the OfficeQA Harborjudge's are the example's own (
examples/officeqa/utils/tests/test_harbor_judge.py). The recorded OpenCode and dshstreams, which the methods' tests now read too, are shared fixtures in
tests/selection.py;.gitignoreselectsthe core group (
test_harbor_dataset.pychecks what it ignores).tests/README.mdsays which tests read therecorded streams and how the Harbor tests skip.
conftest.pycollects no benchmark's own tests from a preparedtask set (
examples/*/utils/tasks).it covers; all but
test_harbor_launch.py's also name its stand-ins.test_run_modes.pygoes throughConfig.load, the Trainer,hermit.evaluate,run_checkand theCLI; the harborside tests run the HermitXXX agents under Harbor's own interpreter (they skip without Harbor's
venv); the trajectory, Base Agent and method tests replay streams the real CLIs printed (
tests/fixtures/).tmp_path, use no sign-in, token or network, and start no container unlessHERMIT_HARBOR_SMOKE=1(tests/core/test_harbor_smoke.py, run in Validation).maingives 28 times:tests/methods/ahe/test_ahe_hosts.pyimports its owntmp_pathfixture (one outside/tmp), so each testthat takes it is a "redefinition of unused 'tmp_path'", the PR's four new ones too. Five warnings
maingivesin those files are gone.
the core team, and none was opened for this PR's
hermit/isolate.pychanges (last item). The other items:That container is the boundary Harbor's tasks define, so the session does not go through
hermit.sandbox.hermit.harbor.tools,hermit.harbor.context): never in the runner's process, nor with its keys or network.harbor runruns as a subprocess in Harbor's own venv, unconfined, as onmainsince Benchmarks: one task protocol (Harbor's format) for single tasks and streams #56. It drives Docker.and under
run.feedback: scoreornonethe learner gets no verdict files.sandbox/<role>-<n>.json(a model only a CLI reaches would get one), so the real-container runs left none to link.hermit/isolate.pychanges, as described under Core changes. No issue was opened for this,so the box is left unchecked; the change is listed for the core team's review.
hermit/sandbox.pyandhermit/sandbox_relay.pyare unchanged.gpt-6.1-sol, high effort,read-only): on
0483ae0once the PR was open, and on the fourth commit's first build (2389a10). The last round'sfixes (in the fourth commit as pushed) were reviewed by the author's session only, so the box is left unchecked.
reviewers: a fresh Codex session (
gpt-6.1-sol, high effort, read-only), and this session's own review, inwhich six reviewers (five in round 1) each took one area. From round 2 on, a verifier tried to reproduce each
finding and a second rated it (a low finding its reviewer was not sure of had the first only); this session
checked round 1's findings itself. Each reviewer's findings were written down before the other's were read.
resolved in the documentation only: a few that predate the PR, and three it introduced (an answer agent on a login
whose request waits for one of the plan's session slots waits inside the agent's time; RRSI Adapter's smoke check
cannot see a Harbor trial's status under
run.feedback: score; verdict fields a pipeline carries under names of itsown are not removed under
scoreornone). Minor findings, and a few that predate the PR outside its scope, wererecorded and left.
checked each fix; round 1's fixes and this session's own follow-up fixes were checked by the next round's review.
Rounds 1 to 6's fixes are folded into the first three commits; round 7's and later ones are the fourth.
fb1f678), 1 high and 25medium; round 2, 8 medium; round 3, 4 medium; round 4, 1 high and 2 medium; round 5, 3 medium; round 6, 1 medium;
round 7 (on
0483ae0's content), 1 medium. The fixes' verifiers and this session's own checks found 6 moremedium ones in round 2 and 1 in round 6, all fixed.
0483ae0: Codex found 2 medium problems (an eval config's tree was not kept fromthe learner; a download of the ref from another registry was taken for the benchmark's tasks) and 1 minor one.
This session's own audit found 13, five of them rated medium by at least one verifier: a task set with a task
that declares MCP servers, skills or a Windows container; Harbor's own
claude-code's web rule; its sign-in;Harness²'s stream score for a composed harness that delivered nothing; a checkpoint test a halt cut short, kept as
done. All of them, round 7's too, were fixed in the fourth commit.
2389a10): Codex found 3 medium problems (an endpoint model took the benchmark file'sprice; a loaded config's Anthropic model passed the subscription-only refusal of Harbor's own
claude-code;Harbor's mini-swe-agent trials skipped mini-swe-agent's own checks) and 2 low ones (a registry file's path in the
lock; this description). This session's review found 2 medium (the startup refusal took a whole task set for one
task Harbor cannot run on Hermit's agents; Harbor's own
claude-codeon a bound model was sent to/v1/v1/messages, whichmaindoes too) and 2 low ones. All are fixed in the fourth commit.claude-codereads, counted as no bill), fixed. A last read of every change since2389a10, the fifth commit'stoo, found nothing new. No high or medium finding is open.
🤖 Generated with Claude Code
https://claude.ai/code/session_01FcJCJanVqQuJyChDevJNYP