Repository navigation
fix(harnesses): grade Harbor cells from Harbor's result.json - #111
Conversation
Harbor exits 0 for a trial that raised (agent install, environment start) and prints a Reward table on stdout, so a Harbor-backed cell with no reward fell back to `returncode == 0` or scraped "Reward ... 1.0" off the table, and an errored trial read as a pass. For kind="harbor" (codex, claude-code, terminal-bench:<agent>) the verdict now comes only from <jobs-dir>/<job>/<trial>/result.json, with the rules of intel/skills' tools/check_harbor_job.py: exception_info, no numeric verifier_result.rewards.reward, no trial file, or more than one trial make the cell errored (metrics.error), out of pass_rate. Tokens and cost are imported from agent_result under the keys docker-claude and BenchFlow use. command: templates keep their reward.txt / stdout / exit-code fallback. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
A Harbor cell is now graded from its trial result.json, so a fake harbor that writes none makes the cell errored; these tests check that the directory is not refused, not the verdict. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #111 +/- ##
==========================================
+ Coverage 91.28% 91.38% +0.10%
==========================================
Files 216 216
Lines 26565 26722 +157
Branches 4337 4373 +36
==========================================
+ Hits 24250 24421 +171
+ Misses 1646 1636 -10
+ Partials 669 665 -4
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
🟢 Approval recommended
The isolated grading changes have focused regression coverage and no identified blocking defects.
No open issues 🎉
What changed in this PR
Corrects Harbor-backed task evaluation by using trial results instead of process exit codes or stdout, preventing failed installations from counting as passes.
Changes:
- Grades Harbor cells from
result.jsonand records token usage and cost. - Adds regression tests and sanitized Harbor fixtures; preserves
command:behavior. - Documents grading rules and the need to rerun affected historical evaluations.
| File | Description |
|---|---|
dokimi/tests/test_task_formats.py |
Makes fake Harbor write a passing trial. |
dokimi/tests/test_harbor_results.py |
Tests grading, errors, usage, and aggregation. |
dokimi/tests/fixtures/harbor/pass/2026-10-06__14-39-30/result.json |
Passing job summary fixture. |
dokimi/tests/fixtures/harbor/pass/2026-10-06__14-39-30/onetbb-transpose__4LraMVa/result.json |
Passing trial with usage metrics. |
dokimi/tests/fixtures/harbor/fail/2026-10-06__14-39-30/result.json |
Failing job summary fixture. |
dokimi/tests/fixtures/harbor/fail/2026-10-06__14-39-30/onetbb-transpose__kaHJy7X/result.json |
Zero-reward trial fixture. |
dokimi/tests/fixtures/harbor/exception/2026-10-06__14-39-30/result.json |
Errored job summary fixture. |
dokimi/tests/fixtures/harbor/exception/2026-10-06__14-39-30/onetbb-transpose__SizQxqz/result.json |
Agent-install exception fixture. |
dokimi/dokimi/harnesses/command.py |
Reads Harbor verdicts and usage metrics. |
dokimi/docs/coding-harnesses.md |
Explains grading and telemetry contracts. |
dokimi/changelog.d/20261018_120000_napetrov_harbor-result-json.md |
Records corrected grading and added metrics. |
Review effort: Balanced
antonwolfy
left a comment
There was a problem hiding this comment.
Review at d7b8cc7. Grading from the trial result.json is the right fix, and the core rules hold: an exception, a missing reward, no trial file or several trials all come out errored, and a stray reward.txt or Harbor's stdout table no longer decides anything. Three things are worth fixing before merge, all inline.
What I ran, on a Linux review host at d7b8cc7:
make ci-localwith docker: 23 passed, 0 failed, 3 skipped (test-latest, docs deploy, semgrep).- Reproductions for the code findings with the transport mocked; a 31-mutant run over
read_harbor_job,harbor_usage,_harbor_rewardand the Harbor branch ofrun, 7 of which pass the four affected test files.
One more, outside this diff:
- The artifact-fields text still describes the old fallback.
docs/coding-harnesses.md:840says "If no reward is found, the adapter's exit code is used as the fallback", and:841lists the token and cost metrics fordocker-claude*only. Neither is true for Harbor-backed cells any more: with no reward they are errored, and they now carryprompt_tokens,completion_tokens,cache_read_tokens,total_tokensandcost_usd. Suggestion: add "except Harbor-backed cells, which never fall back to the exit code" and name Harbor on:841.
Checked, no issues
- Exit-code isolation: a stray
reward: 1.0on stdout plusreward.txt, with rc 0 and rc 1, still gives an errored cell. Swapping in_find_rewardis caught by the tests. - Errored cells in the stats:
metrics.errorwithreward=Nonetakes the cell out ofpass_rate,avg_rewardand McNemar throughis_errored. - Bool and string rewards, a non-object top level, a BOM: all errored, none a pass.
- Stale results:
clear_run_dirruns before the command, so a previous run's file is not read back. - Docs and changelog: metric keys match
runner.pyand BenchFlow; the fragment name and categories follow the repo's pattern;coding-harnesses.mdis in the nav; fixtures hold no hosts or paths.
…finite and negative values - _harbor_reward() now rejects non-finite (inf/nan) and out-of-range rewards (outside [0,1]); they trigger the 'no verifier result' error like missing rewards. - _number() rejects non-finite and negative values; null/missing counts are skipped (unknown, not counted), preserving sums from corrupted data. - Add parametrized tests for reward validation (inf/nan/2.0/-1 cases) and unit tests for _number() validation logic. - Verify result.json path recording. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Copilot review overview
4 open issues
The error message \"trial result.json is not a JSON object\" is also used when the file is… · New Casting token metrics withint(v)will silently truncate non-integer numeric values (e.g., `0.5… · New This test's docstring is inconsistent with the implemented validation:_harbor_rewardrejects… · NewPASS_TRIALis defined but not used anywhere in this test module (based on the full file in this… · New
Review effort: Lite
antonwolfy
left a comment
There was a problem hiding this comment.
Review at 340b1a2. The reward and count validation fixes the NaN, Infinity and negative cases from my earlier review. The same validation still lets a huge integer crash the cell, and the relative-path test is still missing. Two inline comments and one doc fix I'd do before merge.
What I ran, on a Linux review host at 340b1a2:
make ci-localwith docker: 23 passed, 0 failed, 3 skipped. pytest: 5209 passed, 1 skipped.- Reproductions for the code findings, with the LLM transport mocked, driven through
CommandHarness.runwith a fakeharborthat leaves an editedpassfixture. - A mutation run over
read_harbor_job,_harbor_reward,_numberand the Harbor branch ofrun.
One more, outside this diff:
- The artifact-fields text and the grading table still do not match the code. This is the point from my earlier review, not fixed at this head.
docs/coding-harnesses.md:840still says "If no reward is found, the adapter's exit code is used as the fallback", and:841still lists the token and cost metrics fordocker-claude*only. Harbor-backed cells never fall back to the exit code, and they carryprompt_tokens,completion_tokens,cache_read_tokens,total_tokensandcost_usd. The grading table (:419-422) also does not say that a reward must be finite and in [0, 1]: a verifier that writes2.0gets the error "has no verifier_result.rewards.reward", and nothing in the docs explains it. Suggestion: add "except Harbor-backed cells, which never fall back to the exit code" on:840, name Harbor on:841, and add the [0, 1] rule to the table.
I agree with @copilot-pull-request-reviewer's four points (int(v) truncation, the "not a JSON object" message, the no-op assertion, unused PASS_TRIAL). New evidence: n_input_tokens: 0.5, n_output_tokens: 1.7 gives prompt_tokens=0, completion_tokens=1, total_tokens=2, so the parts do not sum to the total.
Still open from my earlier review:
command.py:151-153: theresult_jsonrewrite back to the caller's relative dir is still not tested.test_result_json_path_is_properly_recordeduses an absolutetmp_path, sogiven_dir == output_dirand the rewrite does nothing; it asserts only"result.json" in pathand that the path exists. Replacinggiven_dirwithoutput_dir, and forcing the condition toif False:, both still pass all 235 tests in the four affected files. The smaller gaps I named also still survive:or "unknown", the[:200]cut and theisinstance(exception, dict)guard at:269-273.
Checked, no issues
- Reward range:
inf,NaN,2.0and-1now come out errored withreward=None;<= 1.0changed to<= 2.0andvalue < 0changed tovalue < -1are both caught by the suite. - Hostile
exception_info: a list, a string, and a dict with non-string type or message do not crash; the cell is errored. - Errored cells in the stats:
metrics.errorwithreward=Nonestill takes the cell out ofpass_ratethroughis_errored. - Fixtures and changelog: no hosts or paths in the fixtures; the fragment follows the repo's pattern;
task_runs.v1metrics is open, so no schema bump.
Huge integers no longer raise OverflowError out of run(); oversized JSON ints and bad UTF-8 are errored, not raised; fractional token counts are dropped instead of truncated; the unreadable-file and not-an-object errors are distinct; a relative output dir is recorded as the caller gave it. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
Harbor puts a step's exception on step_results[i] only and averages the top-level reward over the steps that reached their verifier, so a failed step-2 setup read as a 1.0 pass. Read step exceptions too, and sum usage over step_results[i].agent_result, where multi-step trials keep it. Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
Nothing calls it: read_harbor_job reads the trial file inline, and benchflow.py keeps its own _load_json. Its five lines were the only lines of the patch codecov reported as never run. Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
The results[].passed note still said a missing reward falls back to the exit code, and the metrics note named usage keys for docker-claude* only. Harbor cells are errored instead and carry Harbor's usage. The grading table now states the [0, 1] reward range the code enforces. Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
Merge order with #104: each passes alone, but on main + #104 + #111 |
Harbor raises VerifierTimeoutError (with no verifier_result) when the verifier overruns its timeout. As for docker-* and BenchFlow, and as the report footnote says, that is a failure, not missing data, so the cell stays in the pass-rate denominator with reward 0. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
|
Thanks, Alexander. Point by point:
Tests: |
json.loads raises a plain ValueError past Python's int-string limit, not JSONDecodeError, so BenchFlow's _load_json let it escape run(). Catch ValueError here as the Harbor reader does. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
antonwolfy
left a comment
There was a problem hiding this comment.
Review at 04e17d2. The new multi-step, verifier-timeout and ValueError changes hold up on the cases I ran, and the grading still never reads the exit code or stdout. Four things are worth fixing before merge: three inline and one outside the diff. One nit inline.
What I ran, on a Linux review host at 04e17d2:
make ci-localwith docker: 23 passed, 0 failed, 3 skipped (test-latest, docs deploy, semgrep). Changed-line coverage gate passed.- Reproductions through
read_harbor_jobwith hand-written trial files, no model calls. - A 61-mutant run over
read_harbor_job,harbor_usage,_step_usage,_harbor_reward,_number,_countand the Harbor branch ofrun, 16 of which pass the five affected test files. Most survivors are equivalent or cosmetic; the ones worth a test are in the inline comment on the tests.
One more, outside this diff:
docs/coding-harnesses.md:912-914contradicts the new Harbor rule. It says "An agent that exceeds its timeout, or a verifier that does, is a failure (reward 0, returncode 124), not an error: otherwise an arm that hangs would score above one that fails fast." For a Harbor cell,AgentTimeoutErroris now errored and out ofpass_rate(the PR description names this as a follow-up), and the list ofmetrics.errorcauses at:908-911does not mention Harbor. A user comparing a Harbor arm withdocker-*reads a hanging Harbor arm as missing data, and it ranks above an arm that fails fast, which is the effect that paragraph warns against. Suggestion: say there that a Harbor trial that raised, an agent timeout included, is errored, while a lone verifier timeout is a 0.
Checked, no issues
- Verifier timeout: a lone
VerifierTimeoutErrorat the top or on every step gives reward 0.0 withverifier_timeout, and stays in the denominator. - Odd shapes: non-dict
step_resultsentries, a dict-valuedstep_results, a list or stringexception_info, a bool reward and a 10**400 reward or count do not crash and never come out as a pass. - Schema:
task_runs.v1metricsis an open object, so the new keys need no bump. No fixture holds a host, path or secret (gitleaks clean). - Prior threads: the relative output dir, the
[0, 1]rule, the artifact-field text and the overlong-integer reads are fixed at this head.
…eout A verifier timeout beside a real exception was skipped only when it came first, so `harbor_exception` could name VerifierTimeoutError for a trial that really raised AgentTimeoutError. The label is now the first exception that is not a verifier timeout. Pin the mixed-trial cases and the step-only verifier timeout, plus the usage and exception-read edge cases the review found unpinned: steps with no agent_result, a non-object step, a top-level agent_result beside steps, a result.json that is a directory, non-string exception fields, and no reward_after_exception without a reward. The changelog fragment now states that a lone VerifierTimeoutError is a scored 0 and that the reward must be in [0, 1]. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
antonwolfy
left a comment
There was a problem hiding this comment.
Review at 6fb20af. The three fixes from my last review hold: the label now skips a verifier timeout, the edge cases are mostly pinned, and the fragment states the verifier-timeout and [0, 1] rules. Two things are worth fixing before merge: one doc paragraph outside the diff, and one test gap inline. Two earlier threads stay open.
What I ran, on a Linux review host at 6fb20af:
make ci-localwith docker: 23 passed, 0 failed, 3 skipped. pytest: 5230 passed, 1 skipped. Changed-line coverage 100%.- Reproductions through
read_harbor_jobwith hand-written trial files, no model calls. - A 39-mutant run over
read_harbor_job,_is_verifier_timeout,_step_usage,harbor_usageand_harbor_rewardagainsttest_harbor_results.py: the survivors that matter are the two named under the open threads and the label order inline.
One more, outside this diff:
docs/coding-harnesses.md:912-914still contradicts the Harbor rule. This is the point from my earlier review, not fixed at this head. It says "An agent that exceeds its timeout, or a verifier that does, is a failure (reward 0, returncode 124), not an error". For a Harbor cell, anAgentTimeoutErrorinexception_infois errored and out ofpass_rate(read_harbor_jobexempts only a loneVerifierTimeoutError). A trial withexception_infoof typeAgentTimeoutErrorand reward 0.0 returnsNonewitherror='harbor: AgentTimeoutError: x'. The errored list at:908-911does not name Harbor either. Suggestion: say there that a Harbor trial that raised, an agent timeout included, is errored, a lone verifier timeout is a 0, and only dokimi's own kill ofharborgives reward 0 and returncode 124.
Still open from my earlier review:
tests/test_harbor_results.py:450: two of the listed mutants still pass the file:str(...)removed aroundexception_type(command.py:288; theexception_type: 42case checks only the f-string inmetrics.error, so addassert metrics["harbor_exception"] == "42"), and theisinstance(s.get("agent_result"), dict)filter removed (command.py:334; add steps[{"agent_result": {...tokens...}}, {"agent_result": None}]and expect the first step's numbers).changelog.d/20261018_120000_napetrov_harbor-result-json.md:15: this line is still 96 columns, the others wrap at 80.
Checked, no issues
- Label: a verifier timeout beside a real exception, at the top or on a step, in either order, gives the real exception's type and message.
all(toany(and dropping the step scan fromraisedare caught. - Lone verifier timeout: top-level or on steps only gives 0.0 with
verifier_timeout, noerror. - Edge cases now pinned: the
if not usagesguard, the stepisinstance(s, dict)filter, the top-levelagent_resultbeside steps,OSErroron a directory,str(...)onexception_messageand thereward is not Noneguard each fail a test when removed. - Fixed threads: the extra blank line in
test_benchflow_harness.pyis gone.
|
Thanks, Alexander. Point by point, at 2f75c52:
Tests: |
…ut usage Address review of intel#111 (6fb20af): a trial exception now has a test beside a step's, a non-string exception_type is kept as text, and a step with a null agent_result does not erase the other steps' usage. Also document that a Harbor trial that raised, an agent timeout included, is errored, and rewrap a long changelog line. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
5826097 to
3515190
Compare
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
|
Re-checked at 3515190 with real Harbor 0.20.0 through |
Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
antonwolfy
left a comment
There was a problem hiding this comment.
LGTM. The three open threads are fixed at this head, and the grading still never reads the exit code or stdout. One nit inline.
What I ran, on a Linux review host at fb60a08:
make ci-localwith docker: 28 passed, 3 skipped, 2 failed. Both failures are the gitleaks job failing to download its binary;gitleaks detectover the PR's commits finds no leaks. pytest: 6405 passed, 1 skipped. Changed-line coverage gate passed.- A mutation run over the three newest tests plus three more mutants (reward upper bound,
all(toany(, thereward_after_exceptionassignment): all six are killed. - Hand-written trial files through
read_harbor_job, no model calls: mixed exceptions, steps withoutagent_result, a second directory holding aresult.json, a symlinked trial directory. None scores an errored trial as a pass or raises out ofrun().
Checked, no issues
- Label order: swapping the trial and step exceptions in
raised(command.py:277) failstest_a_trial_exception_is_labelled_before_a_step_exception. - Text and filter mutants: removing
str(...)aroundexception_typeand theisinstance(s.get("agent_result"), dict)filter each fail one new test. - Docs and changelog: the fragment wraps at 80 columns, and the errored-cell paragraph now scopes the timeout rule and names the Harbor case.
Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
Resolves the conflict in docs/coding-harnesses.md: main's timeout paragraph, now covering docker-* too, with this branch's Harbor sentence after the general rule. Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
Harbor cells are graded from the trial's result.json, and the DockerCLI fake answered harbor with exit 0 and no file, so with main merged test_migrated_dir_with_task_toml_and_task_md_runs_everywhere failed with "no Harbor result.json". The fake now writes a passing trial result.json under --jobs-dir, with its reward, as _no_harbor does. Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
antonwolfy
left a comment
There was a problem hiding this comment.
LGTM. I pushed three commits on top of fb60a08:
4edeb4b:dokimi/docs/coding-harnesses.md:438: "their sum" now reads "input + output", matchingprompt + completionin the code.82c64ed: mergesmain. #104 landed, and the errored-cell timeout paragraph indocs/coding-harnesses.mdconflicted. The resolution keeps main's paragraph, which now coversdocker-*too, and puts this branch's Harbor sentence right after the general rule ("A Harbor trial that raised, an agent timeout included, is errored (metrics.error); only a lone verifier timeout is scored 0."), dropping "Under the command harness".6732835: theDockerCLIfake intests/docker_cli_fake.pyanswersharbor ... --jobs-dir <d>with a passing trialresult.json, as_no_harbordoes. Without it,test_task_formats.py::test_migrated_dir_with_task_toml_and_task_md_runs_everywherefailed on the merge withno Harbor result.json, the order issue @xaleryb noted.
make ci-local at 6732835 with docker: 28 passed, 0 failed by the change (the 2 gitleaks rows could not download the tool), 4 skipped. CI here is green.


Summary
Harbor-backed cells (
codex,claude-code,terminal-bench:<agent>) are now graded from the trialresult.jsonHarbor writes.harbor's exit code and stdout no longer decide the verdict. Harbor 0.24.0 exits 0 when a trial raised, so an agent-install failure was recorded as PASS 1/1.The rules are the same as intel/skills'
tools/check_harbor_job.py. An errored cell is excluded frompass_rateand from McNemar, the same waydocker-*and BenchFlow cells are (metrics.error→is_errored). The token keys are the ones BenchFlow uses. As in BenchFlow, Harbor'sn_input_tokensincludes the cached prompt. A null count, such as from the oracle or a trial that never ran its agent, is left out rather than recorded as 0.Also in this PR:
docs/coding-harnesses.md: the Harbor section now gives the grading table and the usage mapping. The old text said to readresult.jsonyourself; that paragraph is replaced.tests/fixtures/harbor/{pass,fail,exception}/: trimmed real Harbor 0.24.0 job and trial files with paths and hosts removed.tests/test_task_formats.py: the fakeharbornow writes a passing trial file. Those tests check that a task directory is not refused. Without the file the cell now comes out errored, which is the intended behavior.command:is unchanged. It has the same weakness: when no reward file is found, rc 0 counts as a pass, and the stdout regexreward\D+([01]…)matches Harbor's┃ Reward ┃ Count ┃ … │ 1.0 │table. So a--command-templatethat wrapsharborcan still read an errored trial as a pass. I left it alone because custom templates depend on that fallback. The docs now say this.Evidence
Before (intel/main
6e695ce2). A fakeharborexits 0 and leaves each fixture under--jobs-dir, as Harbor 0.24.0 did:(The fixtures contain only the
result.jsonfiles. A real failing trial also writesverifier/reward.txt, which main reads, so main'sfailrow is right in practice. Theexceptionand no-file rows are the bug as measured.)After:
New tests,
tests/test_harbor_results.py(21): pass with tokens and cost, scored fail, exception (codex / claude-code / terminal-bench), a reward next to an exception is kept aside rather than scored, missing or non-numeric reward ×5, noresult.json(rc 0 and rc 1, with a strayreward.txtand Harbor's table on stdout), job summary without a trial file, two trials, unreadable file, usage mapping, aTaskSuiteRunnersweep where the errored cell is out ofpass_rateand the cost is summed, andcommand:keeping its rc fallback.make ci-local:0 failed, 19 passed, 4 skipped. pytest5199 passed, 2 skipped; ruff, mypy, pre-commit, bandit, zizmor, mkdocs, schema_compat and package-smoke all pass. Diff coverage againstintel/main:command.py (100%), 67 lines, 0 missing.Live:
dokimi tasks run --harness claude-codeon intel/skillsonetbb-transposewith Harbor 0.24.0 and Claude Haiku 4.5. The agent install failed inside the task container, and Harbor exited 0. The cell is now0/0 pass (n/a), 1 errored(harbor: NonZeroAgentExitCodeError …). The same setup recorded1/1 pass (100.00%)before this change.Merge Danger
Door: two-way
The change is limited to the
kind == "harbor"branch ofCommandHarness.runplus two helpers. Reverting is one commit, and no schema or artifact format changes; the newmetrics.*keys are additive, andtask_runs.v1metrics is an open object.Blast Radius: Harbor-arms
harborwrites no trial file for is now errored. That covers a version that changes the jobs-dir layout, or a-k/-noverride inside--command-template. The--command-templatecase is still graded ascommand:, which is unaffected.AgentTimeoutErrorwith a verifier reward is now errored, with the reward stored asmetrics.reward_after_exception, which followscheck_harbor_job.py.docker-*and BenchFlow score an agent timeout as 0. If Harbor arms should match that, it is a follow-up, not part of this PR.docker-*, BenchFlow andcommand:are untouched.🤖 Generated with Claude Code