Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
5afd021
feat(repo): Codex and Copilot CLI harness profiles, native review mode
napetrov Oct 19, 2026
8f74647
fix: handle Windows paths in reach profile config, shell quotes in se…
napetrov Oct 19, 2026
be51812
fix(mypy): make tests a proper package to resolve import conflicts
napetrov Oct 19, 2026
191a7f1
fix: normalize all test module imports to use explicit tests. prefix
napetrov Oct 19, 2026
10c94f3
fix: normalize test_novelty_gate import to use tests. prefix
napetrov Oct 19, 2026
89a8822
Revert "fix: normalize test_novelty_gate import to use tests. prefix"
napetrov Oct 19, 2026
0b814b9
Revert "fix: normalize all test module imports to use explicit tests.…
napetrov Oct 19, 2026
799546a
Revert "fix(mypy): make tests a proper package to resolve import conf…
napetrov Oct 19, 2026
27911d6
fix(mypy): key Codex command events by string in cli_traces; drop str…
napetrov Oct 19, 2026
cb59414
fix(cli_traces): overlapping commands, cd path handling, sed options,…
napetrov Oct 19, 2026
3264d74
fix(cli_traces, answers): cwd-aware shell reads, sed/&/$ parsing, fin…
napetrov Oct 19, 2026
92d2efb
fix(tests): update test module imports to use tests. prefix
napetrov Oct 19, 2026
b4457f9
fix(cli_traces): remove redundant event annotation
napetrov Oct 19, 2026
44608a0
fix(repo): harden shell read parsing, empty-answer handling and revie…
napetrov Oct 19, 2026
ce78a9c
refactor(repo): move trace run record out of events to break import c…
napetrov Oct 19, 2026
c512af6
fix(repo): pass codex-review tool flags to exec, not to review
xaleryb Oct 20, 2026
dcd5039
fix(repo): pass the copilot prompt as one --prompt= element
xaleryb Oct 20, 2026
72405e7
fix(repo): stop forwarding GH_TOKEN and GITHUB_TOKEN to Copilot
xaleryb Oct 20, 2026
53ff957
fix(repo): keep a failed native review without a findings block
xaleryb Oct 20, 2026
4442f40
test(repo): pin bin_companions names and multi-word unwrap_shell
xaleryb Oct 20, 2026
1f8d127
fix(repo): cover brace groups, tail -s, nl long options, /dev/null wo…
napetrov Oct 20, 2026
b55ccad
fix(repo): review of #116: shell reads, codex usage and findings parsing
napetrov Oct 20, 2026
0012e8f
Merge intel/dokimi main into feat/harness-profiles-codex-copilot
napetrov Oct 21, 2026
c566250
docs(profiles): copilot-review is a prompt variant; explain codex OPE…
napetrov Oct 21, 2026
80ff6f3
test(repo): pin the turn.failed half of a failed native review
xaleryb Oct 21, 2026
57559b5
test(repo): pin the companion lookup for a linked --bin
xaleryb Oct 21, 2026
ba0b4a3
test(repo): pin the starting directory of a reach probe's shell reads
xaleryb Oct 21, 2026
f038189
docs(changelog): describe copilot-review as a prompt variant
antonwolfy Oct 21, 2026
31719ab
docs(repo): fix codex endpoint comments; test: pin harness_mounts rea…
napetrov Oct 21, 2026
f829c4e
Merge remote-tracking branch 'intel/main' into feat/harness-profiles-…
napetrov Oct 21, 2026
bc66cd0
fix(repo): a failed codex shell command is not a read; test format li…
napetrov Oct 21, 2026
f2146f7
fix(repo): keep the reads of a failed codex command that names severa…
xaleryb Oct 22, 2026
c1c400e
docs(repo): a Codex provider in config_files names its key variable w…
napetrov Oct 22, 2026
a22d109
docs(repo): list the built-in Codex and Copilot profiles and traces
antonwolfy Oct 22, 2026
9667bb4
test(repo): a codex command with a missing or null exit_code keeps it…
napetrov Oct 22, 2026
adfe925
refactor(repo): extract helpers from the Codex and Copilot trace pars…
napetrov Oct 22, 2026
80b222e
Merge remote-tracking branch 'intel/main' into feat/harness-profiles-…
napetrov Oct 22, 2026
0100221
fix(repos): type the copilot transcript start as datetime | None
napetrov Oct 22, 2026
25dd17d
docs: list the built-in harness profiles in the repo reach checklist
antonwolfy Oct 23, 2026
d479a0f
test(repo): pin the Codex failed-read scope and the Copilot exit-code…
napetrov Oct 23, 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,7 @@
### Added

- **Codex CLI and Copilot CLI harness profiles for `repo run` and `repo reach`** — `--harness codex`, `codex-review`, `copilot` and `copilot-review` run oneDAL-style repository tasks under `codex exec`, Codex's native review mode (`codex exec review --commit HEAD`), `copilot -p` and `copilot -p` with a `/review`-prefixed prompt, so guidance arms can be compared per harness, not only under Claude Code. Their JSON traces convert to `events.jsonl` (`trace: codex-json`, `copilot-json`). A profile can now run the harness's own prompt (`native_prompt`) and convert its answer before grading (`answer_format: codex-review` appends a findings block parsed from Codex's review text); `repo reach` refuses a `native_prompt` profile. `bin_companions` mounts the helper files an executable needs beside it (Codex's `codex-code-mode-host`).

### Fixed

- **`repo reach` writes the profile's `config_files`** — only `repo run` wrote them, so a profile whose endpoint or hooks live in a config file (a Codex `config.toml` naming a provider) probed a different setup than it ran.
10 changes: 7 additions & 3 deletions dokimi/docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,8 +241,10 @@ next release.
it can see. A file comes back `loaded`, `truncated` (one token — the file was
cut), `fetched` (seen only after the agent read it), `not_loaded`, or `error`
(no usable answer). Harness behaviour is data, not code:
`dokimi/repos/profiles/claude-code.yaml` is a `harness_profile.v1`
file and any other harness is another YAML.
`dokimi/repos/profiles/` holds `harness_profile.v1` files for Claude
Code, the Codex CLI (`codex`, and its native review mode `codex-review`) and
the Copilot CLI (`copilot`, and `copilot-review`, a `/review`-prefixed prompt variant of it); any
other harness is another YAML.
- `dokimi/repos/budget.py` — `repo budget`: what the tree puts into an
agent's context when it starts at the root, in characters, with no LLM call.
Root CLAUDE.md files and their `@` imports, the root AGENTS.md files and their `@` imports, and
Expand All @@ -269,7 +271,9 @@ next release.
- `dokimi/repos/bases.py` — checks that need every task base: each
`base` must resolve, and the guidance globs are evaluated at each base.
- `dokimi/repos/events.py` — converts a harness trace to the
`events.jsonl` run trace.
`events.jsonl` run trace; `dokimi/repos/cli_traces.py` holds the Codex and
Copilot JSON converters, and `dokimi/repos/answers.py` the `answer_format`
conversions that turn a native review answer into a findings block.
- `dokimi/repos/l0/` — `repo scan` (L0): checks the links, paths,
identifiers, commands and build labels the guidance names against the same
tree (`repo_readiness.v1`), with no LLM call.
Expand Down
9 changes: 5 additions & 4 deletions dokimi/docs/preparing-a-repository.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,10 +204,11 @@ effect. Targets skipped because a `CLAUDE.md` already exists are listed as
`bridge_skipped` in `meta.json`; `repo report` does not show them.

Loading changes between versions. Run `dokimi repo reach` for the harness and
version you use instead of relying on this table. Only the `claude-code` profile is
built in; for Codex or Copilot, pass a `harness_profile.v1` file with `--harness`
version you use instead of relying on this table. The `claude-code`, `codex`,
`codex-review`, `copilot` and `copilot-review` profiles are built in; for another
harness, pass a `harness_profile.v1` file with `--harness`
([contract](repo-evaluation-contract.md)). Tool-use counts need a trace the profile
can convert, which today means `claude-stream-json`; a `text` trace gives no
can convert, which today means `claude-stream-json`, `codex-json` or `copilot-json`; a `text` trace gives no
events, turns or cost, so for such a harness `repo reach` can report a file as
loaded but never as fetched by the agent. For Claude Code it requires the `claude`
CLI, credentials and a mandatory `--model` flag; `repo --help` notes it costs one
Expand Down Expand Up @@ -266,5 +267,5 @@ Treat each item above as a hypothesis for your repository.
- C/C++: `compile_commands.json` covers every build variant; indexes are prebuilt if you offer navigation tools.
- Recurring review rules, curated, are in the review section of `AGENTS.md`.
- A `CLAUDE.md` containing `@AGENTS.md` sits next to each `AGENTS.md` (§7).
- `dokimi repo reach` (only `claude-code` is built in; others need a `harness_profile.v1` file) confirms each harness you use loads the files you rely on.
- `dokimi repo reach` (`claude-code`, `codex`, `copilot` and `copilot-review` are built in; `codex-review` runs its own prompt and is refused; others need a `harness_profile.v1` file) confirms each harness you use loads the files you rely on.
- Each change was compared against the baseline on current models.
66 changes: 60 additions & 6 deletions dokimi/docs/repo-evaluation-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ SPDX-License-Identifier: Apache-2.0
> Slice S3 adds harness profiles (§6) and the reach probe (§7):
>
> ```bash
> python cli.py repo reach <source> --manifest <path> --model <id> [--harness claude-code]
> python cli.py repo reach <source> --manifest <path> --model <id> [--harness claude-code|codex|copilot|copilot-review|<profile.yaml>]
> [--bin <cli>] [--scenarios root,cwd,read] [--only <scenario>] [--dry-run]
> ```
>
Expand Down Expand Up @@ -608,7 +608,7 @@ env_allow: [PATH, HOME, AWS_REGION, AWS_BEARER_TOKEN_BEDROCK, ANTHROPIC_API_KEY,
env_set: {CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1", DISABLE_AUTOUPDATER: "1"}
egress: ["bedrock-runtime.*.amazonaws.com", "api.anthropic.com"] # the only hosts the agent container reaches (§2)
endpoint_env: ANTHROPIC_BASE_URL # used by payload capture, if set
trace: claude-stream-json # converter to events.jsonl; or: text
trace: claude-stream-json # converter to events.jsonl; or: codex-json, copilot-json, text
guidance: # DECLARED; verified by the reach probe, never trusted
names: [CLAUDE.md, CLAUDE.local.md, AGENTS.md]
resolution: ancestors-of-cwd + lazy-on-subtree-access
Expand All @@ -631,8 +631,56 @@ bridge: {for: AGENTS.md, write: CLAUDE.md, content: "@AGENTS.md"}
scripts) as the agent's container sees them. The built-in `claude-code`
profile uses them for a hook that refuses backgrounded shell commands.

Profiles for other harnesses (Codex, Gemini CLI, Copilot CLI, Cursor agent,
Aider, OpenCode, in-house agents) follow the same shape. Their `guidance`
- `trace` names the converter of the harness's stdout to `events.jsonl`:
`claude-stream-json`, `codex-json` (`codex exec --json`), `copilot-json`
(`copilot -p --output-format json`) or `text` (stdout is the answer, no
events). Codex has no read tool, so its `read` events come from shell
commands that only print files (`cat`, `head`, `tail`, `sed -n`, `nl`).
Neither Codex nor Copilot reports a cost: `cost_usd` is `null` and a batch
budget counts each cell's cap.
- `bin_companions` names files the executable starts from its own directory.
`repo run` mounts the executable alone at `/eval/harness/<name>`, so each
companion is mounted read-only beside it, from beside `--bin`; a missing one
stops the run before any agent starts. Codex 0.160 needs
`codex-code-mode-host`: every tool call goes through it.
- `repo reach` writes `config_files` into each scenario's config directory as
`repo run` does, with `{config_dir}` and `{support_dir}` as host paths.
- `native_prompt: true` marks a harness that runs its own built-in prompt, such
as a native review mode. `invoke` then passes no `{prompt}` (and there is no
`prompt_suffix`), the agent never sees the task prompt, and `repo reach`
refuses the profile. Use such a profile only on tasks whose grader reads what
it answers, which `answer_format` provides.
- `answer_format` converts the final answer before grading. `codex-review`
keeps Codex's review text and appends a ```` ```json ```` block of findings
`{file, line, end_line, severity, priority, rule, message}` parsed from its
`- [P1] title — path:12-14` lines: `file` relative to the workspace,
`severity` `blocker` (P0), `major` (P1) or `minor` (P2, P3), `rule` the title
and `message` the body.

Built-in profiles besides `claude-code`:

| profile | runs | trace | notes |
|---|---|---|---|
| `codex` | `codex exec --json … -- {prompt}` | `codex-json` | `CODEX_HOME` is the per-run config dir; no turn or spend flag, the timeout bounds a run; `none`/`read` get Codex's read-only sandbox, which needs unprivileged user namespaces |
| `codex-review` | `codex exec review --commit HEAD --json` | `codex-json` | `native_prompt`, `answer_format: codex-review`; review tasks only (the change under review is HEAD) |
| `copilot` | `copilot --prompt={prompt} --output-format json --allow-all-tools …` | `copilot-json` | `COPILOT_HOME` per run; a GitHub token as `COPILOT_GITHUB_TOKEN` (`GH_TOKEN` and `GITHUB_TOKEN` are not forwarded: Copilot leaves them readable by the agent's commands), or BYOK through the `COPILOT_PROVIDER_*` variables; `none` names a tool that does not exist, because an empty `--available-tools` leaves every tool on |
| `copilot-review` | as `copilot`, with the prompt `/review {prompt}` | `copilot-json` | a prompt variant of `copilot`, not Copilot's review agent: under `-p`, `/review` reaches the model as plain text, with the same system prompt and tools; the task prompt still asks for its answer format |

npm installs `codex` and `copilot` as Node launchers. A run mounts one file as
the harness, so `--bin` names the native binary the launcher starts
(`@openai/codex-<platform>/vendor/<triple>/bin/codex`,
`@github/copilot-<platform>/copilot`). To route a CLI to another endpoint,
copy its profile: for Copilot, set the `COPILOT_PROVIDER_*` variables (and
`COPILOT_OFFLINE=true` to skip GitHub sign-in); for Codex, add a `config.toml`
under `config_files` that defines a `model_providers` entry with the endpoint's
`base_url`, `wire_api = "responses"` and `env_key = "CODEX_API_KEY"` (the variable that
holds its key; without it Codex sends no `Authorization` header) and selects it with `model_provider`.
Add the endpoint's host to `egress` in both cases. The hosted pull-request
reviewers (Copilot code review, `@codex review`, CodeRabbit) are not CLIs and
have no profile.

Profiles for other harnesses (Gemini CLI, Cursor agent, Aider, OpenCode,
in-house agents) follow the same shape. Their `guidance`
blocks start as the vendor's documented behaviour and are corrected by the
reach probe. Loading changes between versions of one harness, so
reach results are keyed by version.
Expand Down Expand Up @@ -702,7 +750,12 @@ per scenario).
- `class` is one of:
- `loaded`: both tokens reported;
- `truncated`: one token reported;
- `fetched`: reported only after the agent read the file with a tool;
- `fetched`: reported only after the agent read the file with a tool. For the
Codex and Copilot profiles a shell read counts only as a bare `cat`, `head`,
`tail`, `nl` or `sed -n`; for Codex, a command that is one such read of one
file and exits non-zero is not a read (the exit code of `cat a missing` does
not say which file failed, so both stay reads). A read through a pipe, `grep`
or `rg` is not seen, so such a file can be classed `loaded`;
- `not_loaded`;
- `error`: the scenario produced no usable answer: no answer, a non-zero
exit, or an error result.
Expand Down Expand Up @@ -962,7 +1015,8 @@ python cli.py repo compare repo-runs/pilot --baseline guidance:none --metric roo
names a `dokimi:` grader (or a task directory that cannot be read) also stays on the built-in graders' sources.
- The agent's container mounts the workspace, the per-run `home`, `config` and
scratch directories (each `image.env` value under `{run_dir}/`), the caches,
the harness executable (`--bin`, read-only at `/eval/harness`) and the
the harness executable (`--bin`, read-only at `/eval/harness`, with its
`bin_companions` beside it, §6) and the
framework's helper scripts (read-only at `/eval/support`). Before a batch,
the harness must report its version inside the image, and a probe container
with the same mounts must see neither `/eval/content` nor `/eval/source`, no
Expand Down
82 changes: 82 additions & 0 deletions dokimi/dokimi/repos/answers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Copyright (C) 2026 Intel Corporation
# SPDX-License-Identifier: Apache-2.0

"""Final-answer conversions a harness profile names in ``answer_format`` (contract §6).

A native review mode answers in its own format, not the ```` ```json ```` block
a review task asks for. The conversion keeps the harness's text and appends
that block, so a grader reading the last ```` ```json ```` block of the answer
reads the native findings; nothing is dropped from what the agent said.

Findings are ``{file, line, end_line, severity, priority, rule, message}``:
``file`` relative to the workspace, ``line`` the first line of the range,
``severity`` ``blocker`` (P0), ``major`` (P1) or ``minor`` (P2, P3) and ``None`` for a
priority this module does not know, ``rule`` the finding's title and ``message`` its body.
"""

from __future__ import annotations

import json
import logging
import re
from collections.abc import Callable
from typing import Any

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nit (plausible, maintainability): Answer-format list is repeated in the schema and in code

ANSWER_FORMATS, _CONVERTERS and the answer_format enum in harness_profile.v1.json each list the formats. Adding a format to one but not the others fails late, after the agent has run, as a ValueError that run_cell records as a host-error failure. A small test that the three lists agree would catch this.

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.

Added in bc66cd0: test_answer_format_lists_agree asserts ANSWER_FORMATS, _CONVERTERS and the schema enum match, so drift fails in CI instead of after an agent run.

from .transcript import relative_path

log = logging.getLogger(__name__)

ANSWER_FORMATS = ("codex-review",)
_SEVERITY = {"P0": "blocker", "P1": "major", "P2": "minor", "P3": "minor"}
Comment thread
antonwolfy marked this conversation as resolved.
# the title may hold " — ": the path part is what follows the last one and holds none
_CODEX_FINDING = re.compile(r"^- \[(P\d)\] (.+) — ([^\s—][^—]*?):(\d+)(?:-(\d+))?[ \t]*$", re.M)
_FINDING_MARK = re.compile(r"^\s*[-*]?\s*\[P\d\]", re.M | re.I)


def codex_review_findings(text: str, workdir: str | None = None) -> list[dict[str, Any]]:
"""Findings of Codex's native review text: ``- [P1] title — /abs/path:12-14`` then an indented body."""
found = list(_CODEX_FINDING.finditer(text))
out = []
for i, m in enumerate(found):
end = found[i + 1].start() if i + 1 < len(found) else len(text)
body_lines = []
for line in text[m.end():end].splitlines()[1:]: # [0] is the rest of the header line
if not line.strip(): # a blank line may separate paragraphs of the body
continue
if not line[0].isspace(): # the body is indented; a flush line is prose after the findings
break
body_lines.append(line.strip())
body = " ".join(body_lines)
first = int(m.group(4))
if m.group(1) not in _SEVERITY:
log.warning("unknown priority %s in a review finding; its severity is left unset", m.group(1))
out.append({"file": relative_path(m.group(3), workdir), "line": first,
"end_line": int(m.group(5)) if m.group(5) else first,
"severity": _SEVERITY.get(m.group(1)), "priority": m.group(1),
"rule": m.group(2).strip(), "message": body})
return out


_CONVERTERS: dict[str, Callable[[str, str | None], list[dict[str, Any]]]] = {
"codex-review": codex_review_findings,
}


def convert_answer(fmt: str | None, answer: str, workdir: str | None = None, *, failed: bool = False) -> str:
"""``answer`` with the findings block of ``fmt`` appended; unchanged when ``fmt`` is ``None``.

A ``failed`` run or an empty answer stays as it is: a missing review, not a clean one. So does
an answer with more finding markers than findings parsed: no block is appended, so a grader
does not read a review that reported a problem as a clean one.
"""
if fmt is None or failed or not answer.strip():
return answer
if fmt not in _CONVERTERS:
raise ValueError(f"unknown answer format {fmt!r} (known: {', '.join(ANSWER_FORMATS)})")
findings = _CONVERTERS[fmt](answer, workdir)
marked = len(_FINDING_MARK.findall(answer))
if marked > len(findings): # format drift must not read as a clean review
log.warning("%s: %d finding marker(s) in the answer but %d parsed; no findings block", fmt, marked,
len(findings))
return answer
return f"{answer}\n\n```json\n{json.dumps(findings, indent=1)}\n```\n"
Comment thread
napetrov marked this conversation as resolved.
Comment on lines +78 to +82

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

When the answer has more [Pn] markers than parsed findings, this only logs a warning and still appends the block. An answer with "- [P1] Leak - a.c:2" (hyphen, not em dash) ends with json [] , so the grader scores a review that did report a bug as a clean one. Copilot raised this in its review at b4457f9. Please return the answer without a block here, as for failed=True (or raise, so run_cell records a failed outcome), and change the test at test_repo_cli_harnesses.py:353 to expect that.

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 b55ccad: an answer with more finding markers than parsed findings gets no findings block and is returned as is (with the warning), as for a failed run. The caplog test now asserts the unchanged answer.

Loading