Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
f20d4dd
feat(repo): time to first useful result per run
napetrov Oct 31, 2026
00be2ab
fix(repo): name the first-useful result first_useful, issued_s and me…
napetrov Oct 31, 2026
c0773d6
feat(repo): first_useful_rate in repo compare, per-arm counts
napetrov Oct 31, 2026
a221219
fix(repo): a failed edit is no first useful result
napetrov Oct 31, 2026
4f38f1e
fix(repo): a copilot edit takes its completion's success; a deep usef…
napetrov Nov 1, 2026
4586da3
fix(repo): a huge useful repeat count is refused, not an OverflowError
napetrov Nov 1, 2026
c36bc80
fix(repo): count useful results per task and per model, keep kinds an…
napetrov Nov 1, 2026
2909150
fix(repo): useful hits refuse Windows paths; failed Codex patches and…
napetrov Nov 1, 2026
4a816d9
fix(cli-traces): type the codex edit event so mypy accepts its rc
napetrov Nov 1, 2026
3104341
fix(useful): a hit needs a successful result; tests for the hit rules…
napetrov Nov 1, 2026
07c495a
fix(useful): time out a catastrophic command pattern; drop malformed …
napetrov Nov 1, 2026
5f404c8
Treat a falsy non-list Codex file_change changes as malformed
antonwolfy Nov 1, 2026
bce5fc6
Fix the contract's unobserved, edit hit and command regex text
antonwolfy Nov 1, 2026
24ab1d1
Count an assistant record without content as a dropped line
antonwolfy Nov 1, 2026
8170f8f
ci(static): install regex in the static job's tools lock
napetrov Nov 1, 2026
30ee48d
Merge remote-tracking branch 'intel/main' into pr332-land1031
napetrov Nov 1, 2026
65af232
fix(repo): dropped tool calls, untimed wording, timings of observed h…
napetrov Nov 2, 2026
0f29657
refactor(repo): split events._apply by record kind, drop its baseline…
napetrov Nov 2, 2026
48df4a5
fix(repo): a Codex file_change path must be a non-empty string
napetrov Nov 2, 2026
5481ef0
fix(repo): malformed Copilot tool arguments and assistant string cont…
napetrov Nov 2, 2026
47c0880
fix(repo): narrow Copilot tool arguments for mypy
napetrov Nov 2, 2026
c6ff529
test(repo): pin empty tool_use id and name and empty Codex path
antonwolfy Nov 2, 2026
4a131a7
docs(repo): say what drops a line of the trace
antonwolfy Nov 2, 2026
81484ad
test(repo): cover write and str_replace_editor in Copilot arguments test
antonwolfy Nov 2, 2026
2b0e173
fix(repo): a tool result with no usable id is a dropped line; mixed f…
napetrov Nov 2, 2026
dea1e88
fix(repo): an unobserved or timeout cell is no miss in first_useful_rate
napetrov Nov 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
### Added

- **Time to first useful result, per repo-eval run** — a `task.json` can declare
`"useful": {"command": "<regex>"}` for a task graded by its target tests, or
`"useful": {"edit": "<glob>"}` (or a list of globs) for one graded by an edit to a target
file. The first non-denied event with `rc` 0 that hits it (a command whose text matches, or an
edit whose path matches) is recorded in `result.json` `first_useful` with its `issued_s` (the
event's issue time) and `steps` (events up to and including it). A run with no hit says
why (`never`, `unobserved`, or `timeout`: a `command` pattern, searched with the `regex` module, ran past
2 s on one event, so the run has no verdict); a task with no declaration is `not_declared`. A hit in a trace
without timestamps is `untimed`: it keeps its `steps` and counts as observed, with no seconds.
`repo report` shows a row's observed count, medians and reasons under the row, and
`repo compare` pairs the per-task medians as `first_useful_s` and `first_useful_steps`, and
the share of the cells with a verdict that observed a hit as `first_useful_rate` (`unobserved` and `timeout`
cells are unmeasured: neither a hit nor a miss); its header counts observed of measured cells per arm and
how many are unmeasured.
A malformed `useful` is refused by `repo validate`, `repo tasks check` and `repo run`.
Batches run before this change have no `first_useful` and no such rows.
- A Codex `file_change` record whose `changes` has a malformed entry (not an object, or no path), or is a
non-list value other than `null`, is one dropped line, as the other malformed records are. The whole record is dropped: its valid
entries are not read either, so no edit from it counts.
37 changes: 35 additions & 2 deletions dokimi/docs/repo-evaluation-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,7 @@ belongs to the repository's grader and is passed through unread, except `grader`
| `host` | no | framework | `{"min_cpus": <int>, "cpu_vendors": [<str>], "cpu_flags": [<str>]}`, every key optional: what the hidden test needs to show the bug (a thread race that needs 3+ CPUs, an ISA path). Vendor and flag names are `/proc/cpuinfo`'s (`GenuineIntel`, `avx512f`); both match in any case, and a vendor other than `GenuineIntel`, `AuthenticAMD`, `HygonGenuine`, `CentaurHauls`, `Shanghai` is refused (`repo validate` too). A probe that read neither vendor nor flags from `/proc/cpuinfo` is a probe failure, not a miss They are checked against a probe container started like the grading one: CPUs are its affinity mask capped by its cgroup CPU quota (`docker --cpus`). On a host that misses them the task is unsupported (§4.3), and the probed facts go to `meta.json` `host` of a cell that runs, or to its `unsupported.json` `host` when it does not |
| `egress` | no | framework | extra hosts (DNS names, optional `:port`; `*` stands for one whole label and the last two labels are literal, so `*.pythonhosted.org` passes and `*` or `*.org` does not) this task's agent and entry points reach through the proxy (§2), for example `["pypi.org", "files.pythonhosted.org"]` |
| `expected_skill` | no | framework | the name (directory name) of the skill the agent should use; a `skill-catalog:` arm must install it, or the arm is refused before anything runs. `result.json` `skills` records whether the agent used it and whether it used it first |
| `useful` | no | framework | the definition of the run's first useful result, reported as `first_useful` in `result.json` (§4.1): `{"command": "<regex>"}` for a task whose grade runs the target tests, or `{"edit": "<glob>"}` (or a list of globs) for one that grades an edit to a target file. Omit it when no event is the useful result. A malformed value is refused by `repo validate`, `tasks check` and `repo run` |
| anything else | — | repository | for example `hidden_files`, `targets`, `regression_targets`, `manifest` |

`setup_patch` (a single string) is accepted as a synonym for a one-element
Expand Down Expand Up @@ -471,6 +472,36 @@ parent's and subagents' together, so a grader need not:

`process` is `null` when the trace could not be read, the agent left none (no event and no result record), or computing the metrics failed (the error is in `agent.trace_errors`).

`result.json` `first_useful` is the run's first useful result: the first event that
hits the task's `useful` key (§3). The definition depends on the grader:

| Grader | Declare | Hit |
|---|---|---|
| test: the grade runs the target tests | `{"command": "<regex>"}` | the first `command` event whose text matches the regex (`regex.search`) and whose `rc` is `0`. `rc` 0 says that command passed, not that every target test did, so the regex should name the target tests |
| edit or diff: the grade reads a target file | `{"edit": "<glob>"}` or a list | the first `edit` event whose path matches a glob (`match_glob`, as the guidance globs) |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

An edit made by a shell command is never an edit event, so such a run reads never even when its grade passes.

Only edit tools emit edit events (Claude Edit/Write, the Copilot edit tools, Codex file_change). A run that fixes src/a.c with sed -i 's/x/y/' src/a.c (rc 0) has only a command event, so first_useful is never and the cell counts as a miss in first_useful_rate. An arm that leads the agent to scripts then shows a lower rate for a difference only in what the trace can see.

Suggestion: say in this row that an edit made through a shell command is not seen.

@napetrov napetrov Nov 2, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Documented in dea1e88: the §4.1 text after the table now says that an edit made through a shell command (sed -i) is not an edit event, so it is not seen.

| review (`dokimi:review_*`) | none | none, so `not_declared`: the answer is the product, so no event is the useful result. A declared key is measured all the same |
| any other repository grader | either form, or none | none declared, `not_declared` |

A refused event (`denied`) never hits, nor does an `edit` without a success result (`rc` 0): an error result (`rc` 1) means the file was not changed, and no result at all (a killed run) proves nothing. `steps` counts every event up to and
including the hit, the parent's and subagents' (as `process.tool_calls` does).
`issued_s` is the hit's `t`: the time the call was issued, since a call's result
is not timed. `first_useful` is `{"kind": "command"|"edit"|null, "reason": ...,
"issued_s": number|null, "steps": int|null}`. `reason` is `null` when the hit was
found and timed; otherwise it says why `issued_s` (and `steps`, with no hit) is
null: `not_declared` (no definition), `unobserved` (the trace was not read: the
harness never started, it did not parse, or a line of the trace was dropped: an unreadable record or block, such as a tool call without an id, a name or an object of input, a content block without a string type, a Codex file change without a path, or Copilot shell, read or edit arguments that are not an object), `never` (no event hit), `timeout` (a `command` pattern ran past
2 s on one event: no verdict, and the rest of the trace is not searched) or `untimed`
(the trace format has no timestamps, so `steps` is set and `issued_s` is null).
A cell of an older batch has no `first_useful`. Each cell that declares a definition
is counted in `repo report`: the row says how many observed a hit, and the medians
cover only those. `repo compare` pairs `first_useful_rate` per task (the share of its
declared cells with a verdict that observed a hit, so a task where most runs never hit it is not read
through its medians alone; a cell `unobserved` or `timeout` has no verdict, so it is neither a hit nor
a miss and is left out of the share) and its header counts observed of measured cells per arm, with
the unmeasured ones. An edit made by a shell command (`sed -i`) is not an `edit` event, so it is not seen. A
compared task has one kind, but the mean delta pools the tasks of both kinds: a per-task
row shows which.

A cell of an arm that installs skills also has a `skills` object in
`result.json`: `measured`, `skills_installed` (the installed skill names),
`skills_used` (the installed skills the agent called with the `Skill` tool or
Expand Down Expand Up @@ -1482,13 +1513,15 @@ python cli.py repo compare repo-runs/replay --baseline guidance:none --metric re
- `repo compare` reduces cells to one value per (task, arm, model) first:
repetitions share a task's difficulty and are not paired across arms, so the
task is the unit. Per treatment arm, model and measure (gate share, score,
cost, turns, `first_prompt_tokens`, each `--metric` from `grade.json`, and each `process` count a
cost, turns, `first_prompt_tokens`, `first_useful_s` and `first_useful_steps` (the median over a
task's cells that observed the first useful result, §4.1), `first_useful_rate` (the share of a task's
declared cells with a verdict that observed one), each `--metric` from `grade.json`, and each `process` count a
compared cell reports as `process:<key>` or `counter:<name>`) it gives the mean per-task
delta against the baseline with a 95% interval over tasks (a bootstrap for
gate and score, a t interval for the descriptive measures, `-` for a single
task), the tasks up, down and tied, and a two-sided sign-flip permutation p (exact up to 16
changed tasks; at one cell per task and arm it equals McNemar's exact test).
A `process` count or `first_prompt_tokens` only one arm reports for the shared tasks of a
A `process` count, `first_prompt_tokens` or `first_useful_*` only one arm reports for the shared tasks of a
(treatment, model) is not compared; a warning names it. When neither arm
reports it (a batch run before `repo run` recorded it), the row is absent.
Holm's correction runs over the gate and score comparisons of one call; the
Expand Down
33 changes: 27 additions & 6 deletions dokimi/dokimi/repos/cli_traces.py
Original file line number Diff line number Diff line change
Expand Up @@ -531,13 +531,16 @@ def codex_json(raw: str, workdir: str | None, cwd: str = "") -> Transcript:
try:
rec = json.loads(line)
except json.JSONDecodeError:
out.dropped_lines += 1
out.errors.append(f"line {n}: not JSON")
continue
if not isinstance(rec, dict):
out.dropped_lines += 1
continue
try:
_codex_apply(out, rec, messages, workdir, by_id, cwd, event_reads)
except (AttributeError, TypeError, ValueError) as exc:
out.dropped_lines += 1
out.errors.append(f"line {n}: unexpected shape ({exc})")
Comment on lines +543 to 544

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

bug · medium
Must-fix: malformed Copilot tool arguments can bypass this completeness accounting because _copilot_tool() silently normalizes them instead of raising. For example, tool.execution_start with {"toolCallId":"e","toolName":"edit","arguments":42}, followed by its successful completion and a result record, produces only a generic tool event and leaves dropped_lines == 0. run_cell() therefore reports first_useful.reason == "never" for an edit definition, although the edit's path was unreadable and the correct verdict is unobserved. Validate the arguments of recognized tools before normalizing them (preserving the supported string form for apply_patch), count malformed calls as dropped, and add a clean-JSONL regression test. The same silent normalization occurs for malformed shell arguments.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Real, fixed in 5481ef0. _copilot_tool now raises on non-object arguments for bash/powershell/view/edit-family calls (incl. null), so copilot_json counts a dropped line. apply_patch keeps its string form. Tests: edit/create/view/bash/powershell with 42 and null drop; clean-JSONL edit with 42 gives unobserved, not never. Red before, green after.

out.answer = messages[-1].strip() if messages else ""
out.agent_messages = messages
Expand Down Expand Up @@ -617,9 +620,18 @@ def _codex_item_completed(out: Transcript, item: dict[str, Any], messages: list[
else:
_codex_command_unstarted(out, item, script, workdir, cwd)
elif itype == "file_change":
for change in item.get("changes") or []:
if isinstance(change, dict) and change.get("path"):
out.events.append({"t": None, "kind": "edit", "path": relative_path(str(change["path"]), workdir)})
status = item.get("status") # a failed patch changed nothing
rc = {"completed": 0, "failed": 1}.get(status) if isinstance(status, str) else None

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

bug · medium
Must-fix: malformed Codex file-change entries are silently skipped without incrementing dropped_lines. For example, {"type":"item.completed","item":{"type":"file_change","status":"completed","changes":[42]}} followed by {"type":"turn.completed","usage":{}} produces no events and dropped_lines == 0. With a declared edit target, run_cell therefore records never rather than unobserved; if a valid edit follows, it reports a first hit and step count despite the missing event. Validate the changes collection and its entries, counting a malformed record as dropped, and add a regression test analogous to the Claude malformed-content test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 07c495a: a Codex file_change entry that is not an object, or has no path, raises and counts as one dropped line, with no partial edit list.
Test: test_a_codex_file_change_with_a_malformed_entry_is_one_dropped_line_and_no_edits (red before the fix).

changes = [] if item.get("changes") is None else item["changes"]
# an entry that is not a change makes the record malformed: it is dropped whole (counted), not read in part
if not isinstance(changes, list) or not all(
isinstance(c, dict) and isinstance(c.get("path"), str) and c["path"] for c in changes):
raise ValueError("file_change with a malformed changes entry")
for change in changes:
event: dict[str, Any] = {"t": None, "kind": "edit", "path": relative_path(str(change["path"]), workdir)}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Deleting a target file counts as its first useful edit.

Every Codex file_change entry becomes an edit event whatever its kind, and Copilot's apply_patch lists *** Delete File: paths the same way (_PATCH_FILE). With useful: {"edit": "src/a.c"}, an agent that deletes src/a.c and writes it again later gets the deletion as its first useful result, with its steps and seconds.

Suggestion: mark a deletion on the event ("change": "delete") and have hits() skip it, or say in §4.1 that a deletion counts.

if rc is not None:
event["rc"] = rc
out.events.append(event)
elif itype in ("mcp_tool_call", "web_search"):
name = f"{item.get('server')}.{item.get('tool')}" if itype == "mcp_tool_call" else "web_search"
out.events.append({"t": None, "kind": "tool", "name": name})
Expand Down Expand Up @@ -712,14 +724,17 @@ def copilot_json(raw: str, workdir: str | None, cwd: str = "") -> Transcript:
try:
rec = json.loads(line)
except json.JSONDecodeError:
out.dropped_lines += 1
out.errors.append(f"line {n}: not JSON")
continue
if not isinstance(rec, dict):
out.dropped_lines += 1
continue
t = run.offset(rec)
try:
run.apply(out, rec, t, workdir, cwd)
except (AttributeError, TypeError, ValueError) as exc:
out.dropped_lines += 1
out.errors.append(f"line {n}: unexpected shape ({exc})")
out.answer = (run.final or run.last).strip()
out.num_turns = run.turns or None
Expand All @@ -730,14 +745,17 @@ def copilot_json(raw: str, workdir: str | None, cwd: str = "") -> Transcript:
def _copilot_tool(data: dict[str, Any], t: float | None, workdir: str | None, cwd: str = "") -> list[dict[str, Any]]:
name = str(data.get("toolName") or "")
args = data.get("arguments")
if name in _COPILOT_SHELL | _COPILOT_READ | _COPILOT_EDIT and not isinstance(args, dict):
raise TypeError(f"{name} arguments are not an object") # counted as a dropped line by the caller
obj: dict[str, Any] = args if isinstance(args, dict) else {}
if name in _COPILOT_SHELL:
command = str((args or {}).get("command", "")) if isinstance(args, dict) else ""
command = str(obj.get("command", ""))
# only bash is a POSIX script; a PowerShell command is not parsed for the files it reads
scripts = shell_reads(command, cwd) if name == "bash" else []
reads = [{"t": t, "kind": "read", "path": relative_path(p, workdir)} for p in scripts]
return [*reads, {"t": t, "kind": "command", "text": command, "rc": None}]
if name in _COPILOT_READ | _COPILOT_EDIT and isinstance(args, dict):
path = str(args.get("path") or args.get("file_path") or "")
if name in _COPILOT_READ | _COPILOT_EDIT:
path = str(obj.get("path") or obj.get("file_path") or "")
if not path: # a call without a path names no file
return []
return [{"t": t, "kind": "read" if name in _COPILOT_READ else "edit", "path": relative_path(path, workdir)}]
Expand All @@ -752,6 +770,9 @@ def _copilot_result(out: Transcript, events: list[dict[str, Any]], data: dict[st
reads = [e for e in events if e["kind"] == "read"]
failed = data.get("success") is False
for event in events:
if event["kind"] == "edit" and data.get("success") is not None:
# a failed edit changed nothing: rc 1, so it is no useful result
event["rc"] = 0 if data.get("success") else 1
if event["kind"] != "command":
continue
shell = data.get("shellExecution") or {}
Expand Down
Loading
Loading