feat(eval): compare three edit contracts on the DeepSeek Harness - #3067

Closed
Astro-Han wants to merge 33 commits into
feat/eval-deepseek-harness-subjectfrom
feat/eval-edit-contract-arms
Closed

feat(eval): compare three edit contracts on the DeepSeek Harness#3067
Astro-Han wants to merge 33 commits into
feat/eval-deepseek-harness-subjectfrom
feat/eval-edit-contract-arms

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Summary

A Terminal-Bench score is attributed to a framework, which leaves the edit contract inside the framework and unmeasured. Comparing Maka's edit/write against the harness's str_replace_editor cannot separate them either — that comparison moves the framework and the contract at once.

These three arms hold everything else and move only the contract:

armcontract
deepseek-harnessstr_replace_editor — one tool, four commands, unique-literal match, all guidance in the tool description
deepseek-harness-fsthe harness's read/write/edit — three tools, snake_case, three prompt sections of their own
deepseek-harness-apply-patchV4A apply_patch — one envelope, changes located by surrounding context

Same one-shot CLI, same toolchain and fingerprint, same dsh-fs-local provider under every editor, byte-identical package.json and cordis.yml. The variable is the whole tool family and not one dimension of it: tool count, argument names, path conventions and where the guidance lives move together, because each family ships that way. A result is attributable to the family as shipped, not to patch syntax as such.

No arm mounts dsh-fs-observation-policy. It was mounted for the fs arm alone on the reasoning that its tools' guidance mentions the policy; of the four presets in @deepseek-ai/dsh, three mount dsh-tool-fs and none mounts the policy, and its own documentation covers str_replace_editor too. Mounting it in one arm gave that arm two failure modes the others cannot have.

Three compositions in three files can drift apart while still producing numbers, so the control is asserted rather than intended. edit-contract-arms.test.ts fails when the compositions differ by any row outside their editors, when they compose in a different order, when the model, context window, reasoning effort, stream idle timeout, sandbox mode, persona or either Bash deadline drifts in one, when the arms stop sharing one toolchain identity object, when the experiment launches them with anything but a different profile argument, or when arms multiplied by concurrency leaves the 128-trial convention.

The patch arm is ours, and that is the thing to review

The harness ships no patch tool, so plugins/tool-apply-patch/ is this repository's while the other two arms run their vendor's. Fidelity to Codex is therefore itself an experimental variable, and it is asymmetric: a tool stricter than the reference costs its arm turns the reference never spends, and a safer one wins recoveries the reference never gets.

One deviation cannot be removed.apply_patch in Codex is a freeform tool — the API is sent a 108-character description plus a Lark grammar the decoder is constrained to, so a model on that path cannot emit a malformed envelope. The harness registers JSON function tools and has no grammar seam. This arm is therefore the unconstrained case, and syntax errors it can make are errors Codex's decoder would have prevented. That cost is charged to this arm and to no other, and it leads the arm's description in the README.

What is not charged to it is the absence of a read tool. Codex registers no handler that reads file text either — its handler set is long, and the only entry taking a filesystem path to read is view_image, which takes images — so an arm that reads with cat is that contract, and every arm has the same bash at the same 16000-character cap.

What the model is told is upstream's own prose for exactly that case: the ## apply_patch section of prompt_with_apply_patch_instructions.md, carried verbatim as upstream-apply-patch-instructions.md with three substitutions applied over it. A test pins the file's digest, requires each substitution to apply exactly once, and requires the built description to differ from the copy nowhere else. That file is upstream's and it is not delivered from where it sits: at the ported commit it is referenced only from a test whose four model cases all declare expects_apply_patch_description: false. Seven of the eight instruction templates in models-manager/models.json carry no ## apply_patch section and leave the format to the grammar; the eighth, gpt-5.2, carries a shorter one with the same envelope and worked example. So the format is still taught in prose upstream and this is the fuller version of it. Either way no shipped configuration is the one this arm is in — gpt-5.2 gets prose and grammar — so the choice was between upstream's own text and text written here.

Three further divergences are known and each is pinned to bytes the reference produces:

  • the tool implements the function-tool path — verify the whole envelope, then re-apply it from the raw text — where the standalone binary applies sequentially with no verification;
  • it runs NormalizeToLf, the shipped default, where PreserveLineEndings is Stage::UnderDevelopment, default_enabled: false upstream;
  • its diagnostics differ from Codex's, which is the one place this arm may be easier on a model. This one is recorded in prose only; the other two are named with their causes in the test files, which also assert that each still diverges.

A third divergence was recorded here and has since been removed rather than documented: the provider's readText strips a leading byte-order mark and rejects a NUL byte, neither of which String::from_utf8 does. Reading through readBytes and decoding in the tool matches the reference exactly.

The grammar is implemented in full. *** Delete File: and *** Move to: are the only operations ctx.fs has no primitive for, so they run against ctx.fs.processPath(target); under a provider that confines, both refuse up front rather than escape. This arm mounts danger-full-access, where the refusal never fires.

Sources and licences are in the plugins' NOTICE. An earlier revision of this branch vendored the OpenAI Agents SDK's applyDiff as the hunk applier; it is a different V4A implementation, not Codex's, and nothing from it remains.

What the adversarial review rounds changed

Two rounds of four fresh-eye passes each — the port against the binary, the harness integration, the three-arm control, and an audit of every factual claim. The second round reviewed the first round's fixes, which is what caught the two entries below that begin "the previous fix". Every finding was re-verified against upstream source or the reference binary before being acted on.

Nine port defects, all now red-checked. The matcher used JavaScript's trim where upstream uses Rust's, so a file whose first line carried a byte-order mark matched a pattern without one and was written back with the mark deleted, and a line terminated by U+0085 was refused where the reference applies it. The verify pass stated a delete target instead of reading it, applying deletes of non-UTF-8 files that the reference refuses outright. It emitted fs/observed {present} off a bare stat, telling an observation policy a file had been seen that nothing read. The duplicate-path key was the provider's realpath-derived target key rather than upstream's lexical join, so an envelope naming both a symlink and its target was refused where the binary applies both. A move destination ran the fs/write-intent waterfall and discarded the answer, turning a decider's refusal to clobber into a silent overwrite. A failed unlink and an abort between passes escaped the seam's error taxonomy.

Five of those were invisible because the test provider was weaker than dsh-fs-local: it decoded lossily, called every non-directory a file, took three parameters so the signal and sandbox policy could not be observed, and never returned a decider's answer.

The previous fix read patch targets through the wrong seam.ctx.fs.readText is not read_file_text: upstream reads bytes and calls String::from_utf8, while dsh-fs-local additionally rejects a NUL in the first 8192 bytes and strips a leading byte-order mark. NUL is valid UTF-8 and the reference patches and deletes straight through it, so making the verify pass read delete targets — itself a correct fix — took *** Delete File: on such a file with it. Reading through readBytes and decoding in the tool is exactly String::from_utf8, and it removes the byte-order-mark divergence too. Separately, *** End of File as the first line of an update hunk was refused where upstream ignores it.

Twelve more mutations the suite could not see. Five dropped the turn signal from a provider call, which a fake that ignored the signal could not notice; two removed or falsified the version on fs/observed; two swapped a removal's announcement with the removal itself, which leaves the event list identical and the trail a lie; one bypassed the create decider, one renamed its event, one made the sandbox-denial remap a no-op. The fake now honours and records the signal on every call, records what the filesystem held when each observation was published, records the version writeText returned, and can be made to deny a write.

Three uncontrolled differences between the arms, of which one fix was itself wrong. streamIdleTimeoutMs was omitted from all three profiles, leaving a five-minute ceiling on the gap between two streamed tokens behind reasoningEffort: max and a 65-minute bash deadline; it now carries upstream's own 172800000. maxConcurrentTaskGroups: 64 made 192 concurrent trials where the saturating specs here land on 128; it is now 42.

readMaxLineLength was raised from its shipped 2000 to the budget to close a cap the baseline has no counterpart to, and that was worse than the asymmetry. The tool appends a ... (line truncated to N chars) marker and then charges the whole line against readMaxBytes, so a cap near the budget makes a long line not fit at all, drops every line after it, and returns a window no offset recovers — 201 lines at 2000 becomes 0 at 16000. No value equalises the two budgets anyway, one counting UTF-16 code units and the other UTF-8 bytes, so the cap is back where the tool ships it and the guard test asserts the invariant rather than the number.

Twenty-one false claims across the two rounds, listed in the two docs commits. The largest was the provenance one, twice: first that the vendored prose is what Codex sends, then that it is retired. The read-budget script had four transcription errors of its own, all inflating the character ratio, so it no longer asks to be trusted — --self-check <dir> lifts the real renderers out of an installed copy and asserts agreement, and the figures are 1.093x lines and 1.001x characters over 2197 files.

Verification

Lint, format and typecheck clean. 47 TypeScript and 200 plugin tests pass.

Fidelity is measured against the binary rather than argued from the Rust:

  • codex-oracle.mjs runs the released codex under argv0 apply_patch over a temporary tree for each of 94 cases and records what it printed and what the tree became; codex-fixtures.test.mjs replays every case through the registered tool and needs no codex to do it.
  • __tests__/upstream/scenarios/ carries Codex's own conformance suite verbatim — the 25 scenario directories upstream publishes as "meant to be easily portable to other languages or platforms" — and upstream-scenarios.test.mjs replays them upstream's way: copy the input tree, apply the patch, ignore the exit status, require the resulting tree to match exactly. 22 match outright; the three that do not are named divergences. These are the only inputs here this repository did not choose.
  • codex-fuzz.mjs generates envelopes from random line soup, including near-miss context, and runs both. It earned its keep on this branch: it caught a regression introduced here, where rebuilding the file forward let a context-free insertion survive inside a range a later chunk replaces. 2300 envelopes across four seeds diverge on nothing.

Red-checked, not just green:

  • Appending a line to the description, renaming the tool, editing one byte of the vendored instructions, or neutering a substitution each fails the contract-surface test.
  • Removing any one of the four context-matching passes fails at least one oracle fixture; restoring String.prototype.trim in the loose passes fails the trailing-U+FEFF fixture; disabling the heredoc strip fails both heredoc fixtures; matching the heredoc close with includes instead of endsWith fails the trailing-text fixture.
  • Each of the twenty-one port defects and unseen mutations above fails the suite when reintroduced. Seventeen of them did not before these two rounds.
  • Both divergence lists assert that each entry still diverges, so an excuse cannot outlive the thing it excused.

End-to-end against the rebuilt linux/amd64 toolchain: loading the plugin from inside the built tree registers exactly one tool named apply_patch, with one required input string and a 2903-byte description carrying no mention of the shell delivery. The instructions file ships, so it is covered by checksums.sha256 and the re-pinned fingerprint.

Not run: an actual benchmark cohort. No dependency moved.

Review focus

One claim this repository does not reproduce, and which should be read as a one-time manual check rather than a standing guarantee: the three profiles were compared against the upstream examples/jsonrpc-agent composition during development — tool names, tool schemas, system prompt and message sequence on the first outbound request — and found identical.

The read budget matched at 16000 equalises the budget's number, not delivered content. scripts/measure-read-budget.mjs makes the residual checkable and --self-check makes the script itself checkable: over 2197 tracked sources the fs arm delivers 1.093x the lines and 1.001x the characters; among the 570 files where a budget binds, the extremes are 1.398x/1.168x and 0.837x/0.720x.

Two things a write-up must carry that this branch cannot fix. The fs arm receives 764 characters of tool-contributed system-prompt guidance where the other two receive none, and two of its three sections reference an fs-observation-policy no arm mounts. And the treatment is the whole tool family plus that guidance, not "the edit contract" — the arm names are shorter than the thing being varied.

experiments/.../edit-contracts.json declares repetitions: 1, so 89 tasks x 3 subjects is 267 cells. Cost, not power, chose that. McNemar at one repetition is weak for the effect sizes this is likely to see: on ~20% discordant pairs a true 5pp difference is about 2 sigma before any correction for three pairwise comparisons, so a write-up has to report discordant counts and intervals rather than three success rates.

Stacked on #2971.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes — described under Summary above
  • No

The harness ships no patch tool, and an Eval arm that gives the model
Codex's patch language needs one composed the same way its other editors
are: `inject: ['tools', 'fs']`, one `ctx.tools.register(defineTool(...))`,
and every file operation through `ctx.fs`. Reaching around that seam would
put a second filesystem authority in one arm.
Applying a single file's hunks — context matching with the fuzz ladder — is
the part that is hard to get right, so it is not written here. It is the
OpenAI Agents SDK's `applyDiff`, vendored rather than depended on:
`@openai/agents-core` unpacks to 9 MB and pulls the whole `openai` SDK into
a tree that is fingerprinted file by file, while the function itself has no
imports at all. The upstream test suite is vendored with it, which is what
turns a mechanical tsc emit from an assertion into a checked fact; its two
import lines are the only edit. Biome's formatter now skips
`packages/eval/harbor/**/vendor/**` for the reason it already skips the
licence artifacts: a rewritten vendor file is no longer the upstream file.
What is written here is the envelope parser and the tool. The grammar is
transcribed from Codex's lark definition, and the model-facing description
is Codex's own instructions text. Two deviations are deliberate and are
stated to the model rather than left to be discovered by failure:
- No `*** Delete File:` and no `*** Move to:`. `ctx.fs` has no delete or
rename primitive and neither does any other harness file tool, so in
every arm the model removes and renames through bash.
- A patch either applies completely or changes nothing. `applyDiff` is
pure, so every file's new content is computed before the first write.
The contracts this will be compared against touch one file per call and
cannot half-apply; a partly-patched tree would be a difference in
failure modes rather than in edit format.
Sources and licences are recorded in the plugins' NOTICE. The three test
files run from the repository through `test:dist`, against the same
`@deepseek-ai/dsh-tools` and `@deepseek-ai/dsh-fs` the toolchain installs —
added as exact-pinned dev dependencies, which the lockfile takes as pure
additions with no existing resolution changed.
No arm composes this tool yet.
Generated-by: Claude Code
Terminal-Bench results attribute a score to a framework, which leaves the
edit contract inside the framework and unmeasured. Comparing Maka's
`edit`/`write` against the harness's `str_replace_editor` cannot separate
them either: that comparison moves the framework and the contract at once.
So this holds everything else and moves only the contract. Three arms run
the same one-shot CLI, the same toolchain and fingerprint, the same
`dsh-fs-local` provider under every editor, and byte-identical
`package.json` and `cordis.yml`:
- `deepseek-harness` keeps `str_replace_editor` — one tool, four
commands, unique-literal match, all guidance in the tool description.
- `deepseek-harness-fs` takes the harness's own `read`/`write`/`edit` —
three tools, snake_case arguments, three prompt sections of their own.
- `deepseek-harness-apply-patch` takes the V4A tool added previously —
one envelope, changes located by surrounding context.
The variable is the whole contract, not one dimension of it. Tool count,
argument names, path conventions, where the guidance lives, and the
read-before-edit policy move together because each family ships that way.
`dsh-fs-observation-policy` is mounted for the `fs` arm alone for that
reason: its tools' own guidance text tells the model the policy is in
force, so omitting it would leave the prompt describing a rule the
deployment does not enforce, while `apply_patch` has no such concept and no
read tool to satisfy one with.
Three compositions in three files can drift apart while still producing
numbers, and the numbers would no longer be about the contract. So the
control is asserted rather than intended: the new test fails when the
compositions differ by any row outside their editors, when the model,
reasoning effort, sandbox mode, persona or either Bash deadline drifts in
one of them, when the arms stop sharing one toolchain identity object, or
when the experiment launches them with anything but a different profile
argument. The arms also share one preparer, so a change to how the harness
is launched cannot reach one and miss another, and one identity constant,
so a re-pinned fingerprint cannot be applied to two arms out of three.
The toolchain build now installs the plugin tree beside the harness's
`node_modules` — a composition copied under `$DSH_HOME` cannot resolve a
plugin by package name from there — and fails if a declared plugin did not
land. Its dependencies are unchanged: every package the new arms compose
was already in the reviewed lockfile.
Re-pinning the fingerprint needs a `linux/amd64` container build and is not
done here, so the arms are not yet runnable end to end.
Generated-by: Claude Code
The toolchain now carries the `apply_patch` plugin beside the harness's
`node_modules`, so its checksum manifest changed and the pin has to follow.
Rebuilt with the same pinned `linux/amd64` image and the same reviewed
lockfile; no dependency moved.
Verified against the rebuilt tree rather than assumed. `dsh --dump-config`
composes all three arm profiles successfully, and the only differences
between the three composed trees are the editor rows and the `cwd` the two
new arms give the filesystem backend — nothing else diverged. Loading the
plugin from inside the toolchain resolves `@deepseek-ai/dsh-tools`,
`dsh-fs`, `dsh-sandbox` and `schemastery` through the upward walk into
`lib/dsh/node_modules`, registers `apply_patch` with the expected schema,
and carries the patch instructions without the operations the tool does not
implement. `dsh-tool-fs` and `dsh-fs-observation-policy` resolve too.
Generated-by: Claude Code
@coderabbitai

coderabbitaiBot commented Aug 15, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1357f048-8ea6-474e-8887-d512c43407ac

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

The tool shipped without them, refusing both with a pointer to the shell.
The stated reason was that `ctx.fs` has no delete or rename primitive, so
reaching them through `node:fs` would put a second filesystem authority in
the one arm being measured. That reason does not survive contact with the
deployment it was written for: this arm mounts `dsh-sandbox-local` at
`danger-full-access`, no observation policy is loaded, and the model's own
bash can already remove any file in the container. The authority being
protected was not enforcing anything.
The cost was real, and it was the point of the arm. `apply_patch` exists
here to measure Codex's contract; a patch tool missing a third of its
grammar is a different contract, and a model trained on the real one pays
for the difference in wasted turns. It also broke this PR's own rule —
every other arm composes what its tool family ships, which is exactly why
the `fs` arm mounts the observation policy. Two standards in one change.
So both operations are implemented, against `ctx.fs.processPath(target)` —
the provider's own answer to where a file is for something outside it to
open, and for `dsh-fs-local` the local absolute path. `ctx.fs` stays the
only thing that resolves paths; only the two syscalls it does not offer
happen outside it. Semantics follow Codex: a rename writes the destination
and then removes the source, overwrites an existing destination rather than
refusing, and creates missing parents.
Where the original concern is real, it is enforced instead of assumed: under
a provider that confines, `processPath` would be the way around the sandbox,
so delete and rename refuse up front. `sandboxMode` is what decides, so the
refusal fires exactly when a provider actually confines.
The parser already implemented the whole grammar, so no change there. The
tool description loses its "not part of this tool" paragraph and returns to
Codex's text, now with one removal rather than three: the trailing shell
invocation example, which is false for a function tool.
Tests move from an in-memory store to a real temporary directory, since
these two operations are syscalls an in-memory store cannot observe.
Red-checked: stubbing out the removal fails five behaviour tests, and
disabling the confinement guard fails the two that cover it.
Generated-by: Claude Code
The plugin gained its delete and rename paths, so the file it contributes to
the toolchain changed and the checksum manifest with it. Rebuilt from the
same pinned image and the same lockfile; no dependency moved.
Verified against the rebuilt tree: all three arm profiles still compose
under `dsh --dump-config`, and loading the plugin from inside the toolchain
registers `apply_patch` carrying the full grammar — `*** Delete File:`,
`*** Move to:`, and both productions — with the shell-invocation example
still absent.
Generated-by: Claude Code
Independent review of this arm found five places where the tool behaved
differently from the reference it exists to measure. The asymmetry matters
more than the count: a tool stricter than Codex costs its arm turns the
reference never spends, and a tool safer than Codex wins its arm recoveries
the reference never gets. Either way the benchmark reads the difference as
an effect of the edit contract.
`*** Add File:` did not terminate the created file with a newline, because
the vendored applier joins its added lines while Codex appends `\n` after
each one. Every file this arm created differed from Codex's byte for byte,
and a test pinned the wrong result. Each added line is now terminated, and
an empty create still writes nothing, as `String::new()` does upstream.
The envelope was planned against one snapshot and written afterwards, so a
failed hunk left the tree untouched. That is the nicer property and it was
still wrong: two sections naming one path made the second fail against a
version the first had replaced, and a delete followed by an update removed
the file and then failed, leaving neither. Operations now apply in order,
each reading what the previous one wrote.
`*** Add File:` over an existing file was refused with an invented
`FS_ALREADY_EXISTS`; Codex records what it overwrote and writes. A bare
`*** Move to:` with no hunks was accepted; Codex rejects an empty chunk list
before it reads the rename. A leading newline, a trailing space on the
opening marker, and an indented envelope were all refused; Codex trims the
patch and each marker line, while comparing only `trim_end()` inside an
update body so an indented context line stays content. Content after
`*** End Patch` was silently dropped, so two envelopes in one call reported
success over operations that never ran.
Three defects in how it reached the filesystem, none of which fire under the
arm's own non-confining provider:
- `mkdir` ran on every write through `processPath`, the same way out of a
sandbox that delete and rename refuse for. A create outside a confining
workspace had its write denied and still left the directory behind.
- Delete and rename used the provider's realpath-derived target key, so
deleting a symlink destroyed the file it pointed at and left the link
dangling. They now use the lexical path, which is what Codex removes.
- Hunk failures reached the model as a bare `Invalid Context 0`, naming
neither the file nor the section, and two error codes were outside
`FsErrorCode`'s closed union.
The description's provenance note claimed one modification from upstream
where there are four, and NOTICE claimed the description dropped the delete
and move operations — true when written, false once they were implemented,
and wrong in the direction that matters for a modification notice.
84 plugin tests pass. Each divergence is pinned by a test naming the
upstream file and line it follows.
Generated-by: Claude Code
Review found two differences between the arms that were not the edit
contract, and a control test that could not have caught either.
Only the `fs` arm mounted `dsh-fs-observation-policy`, justified in the
profile by that family's guidance mentioning it. Checking how the harness
ships does not support this: of the four presets in `@deepseek-ai/dsh`,
three mount `dsh-tool-fs` and none mounts the policy, which is not even a
direct dependency; and the policy's own documentation covers the baseline's
editor too. It is substrate, not part of an editor. Mounting it in one arm
gave that arm two failure modes the others cannot have — an edit without a
prior read, and a file the model's own shell touched in between — on a
benchmark where shell and editor are used together constantly. No arm
mounts it now. The apply-patch profile's stated reason for omitting it was
also wrong, and is corrected rather than quietly dropped: that arm emits
`fs/observed` with the file's version before it writes, so the policy would
have allowed the write.
The two new arms configured `fs-local` with a `cwd` expression the baseline
did not have. It could not change anything — the provider defaults `cwd` to
`process.cwd()`, both tools pass the session's own cwd, and `DSH_CWD` is
never set — but an unreachable setting is still a difference a reviewer has
to rule out.
Read budgets are now matched. The baseline pins its editor to 16000 output
characters while `dsh-tool-fs` shipped a 51200-byte default, a 3.2x window
on the same task. How much of a file one call returns is not part of the
edit contract, so `readMaxBytes` is set to match. The remaining read-side
asymmetry, that the patch arm has no read tool at all, is contract-inherent
and stays.
The control test read each `- id:` row with the `name:` on the line below
and compared those strings, leaving every `config:` block unchecked.
Narrowing one arm's context window, halving an editor's output budget,
rewriting the shared bash description, reordering the rows, or overriding
the patch tool's own description all kept it green. It now parses the YAML
and compares whole entries, asserts composition order, and pins each arm's
editor row by value — the treatment was the one thing no assertion covered.
Verified by making each of those six edits and watching it fail.
The arm-to-directory table moves to `toolchain-verification.ts`, beside the
identity table, and the subject and the test both read it. The test used to
restate it, so an arm pointed at the wrong profile would have been checked
against the composition it was meant to run rather than the one it runs.
The toolchain fingerprint is re-pinned for the plugin changes in the parent
commit. Verified in a `linux/amd64` container: all three profiles compose,
the composed trees differ only in the editor row, and the plugin loads from
inside the toolchain and registers `apply_patch` with the full grammar.
Generated-by: Claude Code
The apply_patch arm applied one file's hunks with the OpenAI Agents SDK's
`applyDiff`, vendored on the assumption that one V4A implementation is like
another. It is not. Running the released `codex` binary over the same trees
shows four disagreements, each of which moves this arm's score: a context-free
`+` hunk lands at the `@@` anchor rather than at end of file, an updated file
keeps whatever trailing newline it had rather than gaining one, CRLF survives,
and near-miss context is refused where Codex's four matching passes accept it.
Replace it with a port of Codex's own applier and parser. The parser now reads
hunks into chunks instead of passing section bodies through, so a syntax error
in the third section is found before the first is written, and the tool no
longer needs a second project's operation shape.
Restore verify-then-write. An earlier round changed the tool to apply
operations sequentially, matching the standalone `apply_patch` binary. That
binary is not what a model calls: the function-tool handler and the intercepted
shell call both go through `try_verify_apply_patch_args`, which collects hunks
into a map keyed by resolved path, refuses a duplicate key, and computes every
change against the pre-patch tree. So an envelope naming one path twice is
refused, and a hunk whose context the model guessed at costs nothing.
Fidelity is now measured rather than argued. `codex-oracle.mjs` runs the real
binary over a temporary tree for each of 72 cases and records what it printed
and what the tree became; `codex-fixtures.test.mjs` replays every case through
the registered tool and compares. The two cases where the function tool
deliberately differs from the standalone binary are named there with what it
does instead. A fuzzer over 1800 random envelopes found no further divergence.
Also tighten the arm-control test, which had three gaps: it skipped composition
steps that override an already-composed entry, so an override of the patch
tool's description was invisible; it named the profile files to compare instead
of reading them; and it exempted the first subject's profile argument while
comparing only the argument vector, not the whole subject definition.
Toolchain fingerprint re-pinned. Test-only files move under `__tests__/` so the
build prunes them by directory rather than by filename.
Generated-by: Claude Code
`readMaxBytes: 16000` was justified as making "how much of a file one call
returns" equal across the two read-capable arms. It does not. The baseline
editor spends eight characters a line on line numbers plus a header; the fs
tool emits neither, so measured over this repository's own sources it returns
10% more lines for the same budget and 22% on the longest files.
The budget is still the right one, for a reason the note did not give: what
should be equal is what a read costs the context window, and the numbering the
baseline adds is part of its contract that the model pays tokens for either
way. Equalising delivered content would mean picking a budget from an assumed
average line length.
Record the measurement, and record the one genuine mismatch that remains — the
budgets are counted in UTF-16 code units against UTF-8 bytes, equal on ASCII
and not otherwise.
Generated-by: Claude Code
The fixtures pin the cases someone thought to write down. This searches for the
rest: it generates envelopes from random line soup, applies each with the
released binary and with the registered tool, and reports every input the two
disagree on. 2100 envelopes across seven seeds currently diverge on nothing.
Red-checked against three deliberate regressions — placing a context-free `+`
hunk at the `@@` anchor, dropping the trailing-newline normalisation, and
removing the punctuation-folding match pass. The third went unnoticed through
400 envelopes at first, because context lines drawn verbatim from the file only
ever exercise the exact-match pass; the generator now half-remembers a line the
way a model does, and catches it.
Envelopes name each path at most once, and a refusal is compared as a refusal
without comparing the tree, because those are exactly where the standalone
binary and the function tool are supposed to differ.
Generated-by: Claude Code
The function-tool handler verifies an envelope and then throws the result
away: `ApplyPatchRuntime::run` hands `req.action.patch` — the raw text the
model sent — back to `apply_patch_with_mode`, which re-parses it and applies
it sequentially against the live filesystem. The verified `changes` map is
carried for approval and telemetry only.
This tool verified and then wrote the plan it had just computed. That is not
a cheaper version of the same contract, and the reference binary shows the
difference in two cases now covered by tests: a rename onto a path a later
section also updates leaves that section failing against the renamed file,
and `*** Move to:` naming its own source writes the destination and then
unlinks it, deleting the file while reporting success. Both were previously
refused with an invented "multiple operations target" error, because the
verification map also claimed the move destination — upstream keys only on
`hunk.resolve_path()` and carries `move_path` as a value.
Also in the write path: `fs/edit-intent` yields `{version}` with no `kind`,
so passing it straight to `writeText` matched neither guard and silently
wrote unconditionally; the abort signal was not checked between operations;
and `rm` ran with `force: true`, reporting `D <path>` for a file that had
already gone.
In the applier: each fuzzy-matching pass now projects both sides once
instead of recomputing the punctuation fold per candidate position, and
replacements are rebuilt in one forward pass rather than spliced, which
threw a RangeError once a hunk added more lines than the argument limit.
The model-facing description is no longer transcribed into this file. It is
read from a byte-for-byte copy of the upstream artifact it came from, with
three named substitutions applied over it, so the claim that it differs in
exactly those places is checkable rather than asserted in a comment. Copying
the file rather than retyping it also removed a fourth difference that had
no reason to exist: upstream's typographic punctuation is now preserved.
Its recorded provenance was stale — the file it named no longer exists at
the pinned commit — and now points at the artifact that does.
One parameter description said the input was "Not JSON-wrapped". It is: the
harness registers JSON function tools and nothing else.
Generated-by: Claude Code
Upstream ships codex-rs/apply-patch/tests/fixtures/scenarios as, in its own
README's words, an end-to-end suite "meant to be easily portable to other
languages or platforms". This is the port: the 25 scenario directories are
copied verbatim from the pinned commit and replayed through the registered
tool, compared the way upstream compares them — copy `input/`, apply
`patch.txt`, ignore the exit status, and require the resulting tree to equal
`expected/` exactly.
It is the only check here with the standing to contradict the code. The
oracle fixtures run the reference binary, but over inputs this repository
chose; the fuzzer perturbs inputs this repository chose how to perturb.
These were chosen by upstream, before and independently of this tool.
22 of 25 match outright. Three do not, and each divergence is recorded with
its cause and pinned to bytes the reference binary produces:
- 015 specifies the standalone binary, which has no verification pass, so an
add preceding a failing update survives. The function tool writes nothing.
- 023 and 024 are specified under CODEX_APPLY_PATCH_PRESERVE_LINE_ENDINGS,
a feature that is `Stage::UnderDevelopment, default_enabled: false`, so a
shipped Codex and this arm both run the `NormalizeToLf` default.
Both divergence lists — here and in the oracle fixtures — now assert that
each entry still diverges, so an excuse cannot outlive the thing it excused.
Generated-by: Claude Code
The treatment in this arm is the tool name, the parameter it takes, and the
description text. None of the three had any coverage, so a build that
registered the tool under another name, dropped the parameter, or shipped a
truncated description would have run a cohort and produced numbers.
The description claim is the one that had to be argued rather than shown:
"three differences from upstream and no others" was a comment. It is now
checked at both ends. The vendored instructions file is pinned by digest,
with the `git show ... | sed -n` that reproduces it from the source recorded
beside the constant, and the built text is recomputed here by a different
mechanism than the code uses and required to match. Each adaptation must
apply exactly once, and no mention of the shell delivery may survive.
Red-checked against four sabotages — appending a line to the description,
renaming the tool, editing one byte of the vendored text, and neutering an
adaptation. Each is caught.
Generated-by: Claude Code
Two differences at the outermost layer of the patch, both pinned by new
oracle fixtures generated from the reference binary.
`parse_patch` selects its mode from `PARSE_IN_STRICT_MODE`, which is `false`:
upstream gave up on gating leniency by model, so every call site including
the function tool unwraps a heredoc-wrapped envelope. A model asked to send
a patch may well send the `<<'EOF' ... EOF` form it was shown, and Codex
applies it where this refused it. The strict boundaries are still tried
first, so an envelope whose own first line reads `<<EOF` is never stripped.
Rust's `str::trim` cuts the Unicode `White_Space` property; JavaScript's also
cuts U+FEFF and does not cut U+0085. Only U+FEFF can realistically occur — a
byte-order mark carried into a JSON string argument — and it made this tool
accept a patch Codex refuses, which is the wrong direction for an arm being
compared against the contract it imitates.
Red-checked: restoring `String.prototype.trim` fails the byte-order-mark
fixture, and disabling the heredoc strip fails both heredoc fixtures.
Generated-by: Claude Code
The comment justifying `readMaxBytes: 16000` said `str_replace_editor`
spends eight characters a line on line numbers and a header while "this tool
emits neither". Both tools number their lines, and this one wraps the result
in a `<path>/<type>/<content>` envelope with a footer.
The setting still holds something equal, but not what was claimed. The
baseline truncates the fully rendered string; this tool charges the budget
against raw line bytes plus separators and adds the gutter, the envelope and
the footer afterwards, outside it. Measured across this repository's 1480
TS/JS/Python sources at 16000, the arm delivers 1.105x the lines and emits
1.027x the characters, worst file 1.34x and 1.19x — a residual in this arm's
favour, now recorded with the mechanism that produces it.
The three profiles also claimed to have been "verified request-for-request"
against the upstream composition. That was a manual comparison made once
during development; nothing here reproduces it, and the headers now say so.
`MAKA_EVAL_DSH_PLUGINS` was exported into all three arms and read by one, so
the two controls carried an environment difference for a mechanism they do
not use. It is now set from the copied composition itself, which keeps the
condition and the file that depends on it from drifting apart.
Generated-by: Claude Code
`apply_patch` in Codex is a freeform tool: the API is sent a short description
and a Lark grammar the decoder is constrained to, so a model on that path
cannot emit a malformed envelope. The harness registers JSON function tools
and has no grammar seam, so this arm is the unconstrained case. That is not a
detail of the port — it is a cost charged to this arm and to no other, and it
now leads the arm's description instead of being absent from it.
Three further divergences are named with their causes: the function-tool path
against the standalone binary's sequential apply, `NormalizeToLf` against a
preserve-line-endings feature that ships disabled, and diagnostics more
specific than Codex's single `Invalid patch:` line, which is the one place
this arm may be easier on a model than the reference.
The README also described a verify-then-write-the-plan design the tool no
longer has, quoted the old fixture count, and repeated the read-budget
mechanism corrected in the previous commit.
The NOTICE opened by saying nothing was carried verbatim as a file; two
things now are. Its description provenance named a file that no longer exists
at the ported commit and a fourth modification that was an artefact of
retyping rather than copying, its fixture path was stale, its account of how
`PreserveLineEndings` is selected covered only the standalone binary, and its
list of ported functions omitted the ones that decide apply order.
The arm-control test read a composition step as an insertion whenever it had
an `insert` key, so `{insert, remove}` would have passed with the removal
unread, and it filtered directories out of each profile's file list rather
than refusing them. It also now names what it does not cover.
Generated-by: Claude Code
`dsh-fs-local` creates a write's parent itself, in `writeFileAtomic`, and it
is the provider every arm mounts. The `ensureParent` here ran `mkdir` on a
`processPath` — the same way around a confining sandbox that delete and
rename refuse for — and existed only because the test harness's provider was
weaker than the real one and failed with ENOENT without it.
That is the more useful half of this change: a fake that cannot do what the
real thing does will not merely miss bugs, it will make production code grow
to cover for it. The fake now creates the parent the way `writeFileAtomic`
does, and the tool has no call that creates a directory at all.
The test that pinned the old behaviour was passing for the wrong reason — it
asserted the tool left no directory behind, and what actually happened was
the fake's write failing before anything could be created.
Generated-by: Claude Code
…sses
`deriveNewContents` reported a context miss as a plain `Error`, and the
caller rewrapped anything it threw as `FS_EDIT_NOT_FOUND` — so a bug in the
applier would have reached the model as "the lines you named are not in the
file", which is advice to rewrite a correct patch. The context miss is now
its own type and the only thing translated; a comment already claimed this
and the code did not.
Two oracle fixtures pin the order of the four matching passes against each
other. The existing cases pinned the exact pass ahead of the loose ones;
between the loose ones, removing a pass does not fail a match, it silently
moves it to a different line, and nothing caught that. Each of the four now
fails at least one fixture when removed.
The line-ending note in codex-apply.mjs described only the standalone
binary's environment variable, not the feature flag the function tool reads.
The toolchain is rebuilt and its fingerprint re-pinned: the model-facing
instructions now ship as a file, so they are covered by `checksums.sha256`
rather than living only inside a JavaScript template. Verified inside the
pinned linux/amd64 container that the built tree registers one tool named
`apply_patch` with one required `input` string and a 2903-byte description
carrying no mention of the shell delivery.
Generated-by: Claude Code
Differential fuzzing against the reference binary found a regression this
branch introduced. `applyReplacements` was rewritten to build the file
forward in one pass, on the reasoning that the replacements are sorted and
disjoint. They are sorted; they are not always disjoint.
`computeReplacements` places a context-free insertion at the end of the file
without consulting the search position — deliberately, because that is what
`insertion_idx` does upstream — so it can land inside a range a later chunk
replaces. Applying descending, the replacement swallows it; applying forward,
the two are treated as separate edits and the inserted line survives into a
file the reference never produces.
Restored to splicing in descending order, with the replacement segment going
in in bounded batches so a hunk larger than the engine's argument limit still
does not throw. The case is now an oracle fixture, so it is caught without a
`codex` on PATH; red-checked by reinstating the forward rebuild.
2300 further envelopes across four seeds diverge on nothing. Toolchain
rebuilt and re-pinned.
Generated-by: Claude Code
An adversarial review pass found nine behaviours that were wrong or untested
in the V4A tool, each verified against the reference binary or upstream source
before being changed. All nine are now covered: sabotaging any one of them
fails the suite, where before five of them left it green.
The matcher used JavaScript's `trim` where upstream uses Rust's. The two
disagree on U+FEFF and U+0085, so a file whose first line carried a byte-order
mark matched a pattern without one and was written back with the mark deleted,
and a line terminated by NEL was refused where the reference applies it. The
charset the parser already spelled out moves to rust-trim.mjs and both use it.
The verify pass now does what `try_verify_apply_patch_args` does: nothing at
all for `*** Add File:`, and a read for `*** Delete File:` as well as for an
update. Statting a delete target instead applied a delete the reference
refuses, since `read_file_text` maps invalid UTF-8 to `InvalidData` and fails
the whole envelope. It also stops emitting `fs/observed {present}` off a bare
stat, which told an observation policy a file had been seen that nothing read.
The duplicate-path key is lexical, as upstream's `PathUri::join` is, not the
provider's realpath-derived target key. An envelope naming both a symlink and
its target was refused here; the binary applies both sections in order.
A move destination no longer runs the `fs/write-intent` waterfall and discards
the answer, which turned a decider's refusal to clobber into a silent
overwrite. A failed unlink and an abort between passes now arrive as `FsError`
with a routable code and the path the model wrote, not a raw errno and a
resolved host path.
The test provider was weaker than `dsh-fs-local` in four ways that made the
above invisible: it decoded lossily where the real one raises `FS_NOT_TEXT`,
called every non-directory a file, took three parameters so the signal and
sandbox policy could not be observed, and never returned a decider's answer.
Two oracle cases are added for the trim charset and one for a trailing U+FEFF,
which is what separates the three loose matching passes from each other; the
BOM-in-content case is recorded as a divergence with its true cause, which is
that the shared provider strips a leading mark before this tool sees the file.
Generated-by: Claude Code
The import landed the 25 scenario directories, their README and their
.gitattributes twice: once at `__tests__/upstream/` and once, byte-identically,
at `__tests__/upstream/scenarios/`. Only the nested copy is read.
Generated-by: Claude Code
…arms
The three arms are meant to differ only in the edit contract. Three settings
outside that contract were not held.
`readMaxLineLength` was left at its shipped 2000 while only `readMaxBytes` was
matched to the baseline. That is a cap the baseline has no counterpart to: the
fs arm could never see the 2001st character of any line, at any offset, while
`str_replace_editor` truncates once over the whole rendered view and can spend
its entire budget on one line. A minified bundle or a single-line payload is
where that lands, and it lands against one arm only. It is raised to the
budget, where it can no longer bind before `readMaxBytes` does.
`streamIdleTimeoutMs` was omitted from all three profiles, leaving the
adapter's 300000 ms default in force — five minutes between two streamed
tokens, behind `reasoningEffort: max` and a 65-minute bash deadline. Upstream's
own jsonrpc-agent composition sets 172800000, which is what the arms now carry;
a turn killed by that timeout would have been scored as a failure of whichever
contract was running.
`maxConcurrentTaskGroups` was 64. A group holds one cell per subject and starts
them together, so three arms made 192 concurrent trials where every other spec
here lands on 128. That does not bias one arm against another, but it puts this
run's `deepseek-harness` score under a load no single-arm run shares. It is now
43, and the test asserts the arithmetic rather than the number.
The read-budget residual quoted in the profile and the README had no
reproducible source and reported only the tail that favoured the fs arm.
`scripts/measure-read-budget.mjs` reimplements both renderers from the shipped
code and reports both tails; the figures are replaced with what it prints.
Generated-by: Claude Code
A claims audit checked every factual assertion in the README, the NOTICE and
the profiles against upstream source at the ported commit and against the
shipped harness packages. Fourteen did not hold.
The largest is provenance. `prompt_with_apply_patch_instructions.md` is not in
any shipped Codex system prompt at the ported commit: it is referenced only
from a test whose four model cases all declare
`expects_apply_patch_description: false`, and none of the eight instruction
templates in models.json carries the section. The file is upstream's own
authored description of the format and it is retired, which is a better reason
to use it than the one previously given, but the prose said Codex sends it.
The freeform description is 108 characters, not 105. The oracle runs 82 cases,
not 76. The fuzz figure is 2300 across four seeds, not 2100. Two of the named
divergences were recorded in the tests, not three, and there are now four with
the byte-order mark the provider strips. `verifyToolchainDirectory` covers ten
registered profiles, not eight. `checksums.sha256` omits itself and
manifest.json. The toolchain build's `__tests__` prune is scoped to the plugins
it copies. The build script runs from the repository root. `ctx.fs` offers
twelve methods, not three. The 0.147.0 delta includes a second behavioural
difference — the duplicate-path check does not exist at that tag — and a larger
restructure than the three items named. The profiles record three deviations
from upstream, not one.
Also stated where it was missing: the arm's lack of a read tool is Codex's
contract rather than a handicap, since Codex registers no file-read tool
either and every arm has the same bash.
Generated-by: Claude Code
A second adversarial round found that the previous round's fix traded one
divergence for another, and that twelve mutations of this tool still left the
whole suite green.
`ctx.fs.readText` is not `read_file_text`. Upstream reads bytes and calls
`String::from_utf8`, so every valid UTF-8 sequence is text; `dsh-fs-local`
adds two rules on top, and both are model-visible. It rejects a NUL in the
first 8192 bytes as binary — but NUL is valid UTF-8, and the reference patches
and deletes straight through it, verified against the binary — so making the
verify pass read delete targets took `*** Delete File:` on such a file with
it. And it decodes with `ignoreBOM` left false, which strips a leading
byte-order mark, so a patch whose context omits the mark applied here, was
refused there, and wrote the file back without the mark. Reading through
`ctx.fs.readBytes` and decoding here is exactly `String::from_utf8`; it fixes
both, and the byte-order-mark divergence is deleted rather than documented.
`*** End of File` as the first line of an update hunk was refused. Upstream
guards with `chunks.last().is_some_and(blank)`, which is false when there is
no chunk, so the marker is ignored; the binary applies the envelope.
Twelve mutations that the suite could not see, now each red-checked: five
dropped the turn signal from a provider call, which a fake that ignored the
signal could not notice; two removed or falsified the version on `fs/observed`;
two swapped a removal's announcement with the removal itself, which leaves the
event list identical and the trail a lie; one bypassed the create decider and
one renamed its event; one reduced the sandbox-denial remap to a no-op. The
fake now honours and records the signal on every call, records what the
filesystem actually held when each observation was published, records the
version `writeText` returned, and can be made to deny a write.
Twelve oracle cases added, generated from the binary: three for the read seam
and nine for shapes that separate this port from a plausible variant of it —
overlapping chunks, a backward `@@` anchor, an indented header after an add
body, a trailing mark on an End of File marker, header paths with a leading
space, a repeated `*** Move to:`, a heredoc close with trailing text, and the
End of File case above. Eleven passed unchanged, which is the point of adding
them; one was the defect.
`requireDirectFilesystem` now raises an `FsError` like every other refusal.
Generated-by: Claude Code
… meant to help
Raising `readMaxLineLength` from its shipped 2000 to the 16000 budget was
wrong, and worse than the asymmetry it was meant to close. `dsh-tool-fs`
truncates a long line and appends a `... (line truncated to N chars)` marker,
then charges the whole thing against `readMaxBytes`. Once the cap approaches
the budget the truncated line no longer fits at all: the window returns zero
lines, drops every line after it, and the footer invites the model to retry at
the offset it just used, so the rest of the file is unreachable. Measured
against the shipped functions, a 20000-character line followed by 200 short
ones delivers 201 lines at 2000 and 0 lines at 16000.
No value equalises the two budgets anyway — one counts UTF-16 code units and
the other UTF-8 bytes — so the cap goes back to the value the tool ships with
and the residual is measured instead of engineered away. At 2000 the direction
is also not the one the earlier reasoning assumed: on that same long-line file
the baseline spends its whole budget on the one line and delivers 1, while
this arm truncates it, says so, and reads on.
The guard test now asserts the invariant rather than the number, so any future
cap has to be one a truncated line can still fit under.
The measurement script had four transcription errors, all inflating the
character ratio: it dropped the baseline's 240-character truncation notice and
the fs arm's per-line marker, counted the baseline's prompt line as content,
and counted a phantom trailing line for the fs arm. It also silently ate the
first positional path when `--budget` was absent. Rather than fix the
transcription and hope, `--self-check <dir>` now lifts the real functions out
of an installed copy and asserts agreement, and the corpus figures are
reported alongside synthetic long-line cases that the repository's own sources
do not contain. Corrected: 2197 files, 1.093x lines, 1.001x characters, tails
reported as what they are — extremes among the 570 files where a budget binds.
`maxConcurrentTaskGroups` moves 43 to 42: three arms at 43 is 129 trials, and
the assertion had been written as `<= 129` to admit it.
Generated-by: Claude Code
The claim that upstream's `## apply_patch` prose is retired was too strong. Of
the eight instruction templates in `models-manager/models.json` at the ported
commit, seven carry no such section — but `gpt-5.2` carries a shorter one with
the same envelope, the same three headers and the same worked example. So the
format is still taught in prose upstream, and the vendored file is the fuller
version of that prose rather than something abandoned. The conclusion it was
supporting survives unchanged and for a cleaner reason: `gpt-5.2` gets the
prose *and* the grammar, so no shipped configuration is the one this arm is in.
Codex's handler set is much longer than the six entries named, and one of them
takes a filesystem path — `view_image`, for images. The load-bearing claim is
that no handler reads file *text*, which is what the prose now says.
The NOTICE gave the wrong cause for the duplicate-path divergence: the
recorded binary composes an envelope naming one path twice because the
standalone applier has no verification pass at either version, not because the
check is absent at rust-v0.147.0 — that is a function-tool difference.
Also: `invocation.rs:246-248` is the delete arm, not the add arm; the
`fs/edit-intent` shape citation named only one of the two declarations; the
end-of-file search has no fallback to the ordinary start position, contrary to
its comment; `__tests__/upstream/` kept a stray `.gitattributes` naming paths
one level down; and the build script's header credited `npm i` for a
non-reproducible fingerprint when it runs `npm ci` and the real cause is native
compilation.
The refusal under a confining provider is described as what it is: dead code
under `dsh-fs-local`, which never sets `sandboxMode` at any sandbox mode, not
something `danger-full-access` switches off.
Generated-by: Claude Code
…t reads it
The comment justifying the conditional export had the causation backwards. The
model's shell inherits this environment, so setting the variable in all three
arms is what makes them identical, and setting it in one arm alone is what
creates a difference the model can read with `env`. It names a directory and
starts nothing, so it is inert in the two arms whose compositions never mention
it.
The remaining environment asymmetry, `$HOME` carrying the profile name, is
recorded rather than removed: the arms need separate homes because each writes
its own composition under `$DSH_HOME/profiles`, and sharing one would have each
overwrite what the others booted from. So the experiment is not blind to its
subject, and that is now stated where the home is built.
Generated-by: Claude Code
The spec declared 42 task groups, or 126 concurrent trials, on the
strength of a 128-trial convention rather than a machine. The account's
CVM quota is 60 vCPU per zone, which caps the host at 56 vCPU and
256 GiB, and the only completed run of this suite put 24 trials on
32 vCPU / 64 GiB — 2.7 GiB per trial. 126 trials on that host is 2.0 GiB
each, close enough to a Terminal Bench build task's peak that an OOM
kill would land as an infra failure. 32 groups is 96 trials at the
density that is known to survive.
Generated-by: Claude Code
The arm named its plugin with `name: !!js process.env.MAKA_EVAL_DSH_PLUGINS
+ '/tool-apply-patch/index.mjs'`. The harness's loader evaluates that tag for
`disabled` alone and hands `name` the unevaluated node, so every trial of
that arm died at boot with `name.startsWith is not a function` before it
reached the model — on a rented host, at the first smoke task.
The row now names `./plugins/tool-apply-patch/index.mjs`, resolved against the
profile directory, and the subject plants `plugins` there as a link into the
toolchain. That keeps what the loader imports inside the fingerprinted
read-only tree, keeps the composition a plain string, and keeps the path
independent of the root the wrapper was given — which naming the mount point
outright would not. The link is planted for all three arms, on the same
reasoning that exported the variable for all three: the model's shell can read
$DSH_HOME, so planting it in one arm alone is the difference, not planting it
everywhere.
The test could not have caught this. It extended its own YAML schema to
construct `!!js <source>` as a string, which taught the assertion to accept a
form the harness cannot load. The tag is real elsewhere in these compositions —
`task: !!js ctx.headlessStartup.task` is upstream's — so the schema now
constructs it as the marker shape the harness's loader produces, and a new
assertion requires every entry name to be a string and every path-named plugin
to be one the toolchain build ships. Reverting the row fails both.
Generated-by: Claude Code
The wrapper counted the subject's stderr bytes and hashed them, then dropped
them. The relay writes `maka-subject.stderr.txt` from the wrapper's own
stderr, which is empty, so a failing arm's own account of what killed it
reached the attempt record as a byte count and a digest and nothing else.
Both failures this run has had were diagnosed by reproducing them by hand on
the host instead: a composition the harness could not load, and a subject that
exits after one model response. The second is still open.
The last 64 KiB now land in `/logs/agent/<profile>.stderr.txt`, which the
relay already collects, as a `stderr-tail` artifact beside the existing
`stderr` count. A tail because a subject that loops printing would otherwise
fill the trial's disk, and because a harness says what killed it on the way
out; the count and digest still describe the whole stream, so truncation is
visible.
Generated-by: Claude Code
Announcing it as an artifact pushed the frame past the relay's 2 KiB payload
cap, so every cell came back `external subject result transport failed`.
`/logs/agent` is collected into the trial directory regardless, so writing the
file is the whole job.
Generated-by: Claude Code
Every failure before the subject starts reached the operator as the string
`external subject setup failed`, because `safeFailure` sanitizes what the
published result frame may say and nothing recorded the error itself. The
process's own stderr is collected into the trial as
`maka-subject.stderr.txt`, which is where a subject's account of its own
failure belongs, so the error, its stack, and its cause chain go there.
Generated-by: Claude Code
This module does its work at the top level, above the function that reads the
subject's stderr, so a `const` declared beside that reader is still in its
temporal dead zone when the reader runs. Every cell died with
`ReferenceError: Cannot access 'STDERR_TAIL_BYTES' before initialization`
before the subject's first model response — reported as
`external subject setup failed`, and legible only because the commit before
this one started writing the error itself.
Generated-by: Claude Code
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat(eval): compare three edit contracts on the DeepSeek Harness - #3067

Closed
Astro-Han wants to merge 33 commits into
feat/eval-deepseek-harness-subjectfrom
feat/eval-edit-contract-arms
Closed

feat(eval): compare three edit contracts on the DeepSeek Harness#3067
Astro-Han wants to merge 33 commits into
feat/eval-deepseek-harness-subjectfrom
feat/eval-edit-contract-arms

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Summary

A Terminal-Bench score is attributed to a framework, which leaves the edit contract inside the framework and unmeasured. Comparing Maka's edit/write against the harness's str_replace_editor cannot separate them either — that comparison moves the framework and the contract at once.

These three arms hold everything else and move only the contract:

armcontract
deepseek-harnessstr_replace_editor — one tool, four commands, unique-literal match, all guidance in the tool description
deepseek-harness-fsthe harness's read/write/edit — three tools, snake_case, three prompt sections of their own
deepseek-harness-apply-patchV4A apply_patch — one envelope, changes located by surrounding context

Same one-shot CLI, same toolchain and fingerprint, same dsh-fs-local provider under every editor, byte-identical package.json and cordis.yml. The variable is the whole tool family and not one dimension of it: tool count, argument names, path conventions and where the guidance lives move together, because each family ships that way. A result is attributable to the family as shipped, not to patch syntax as such.

No arm mounts dsh-fs-observation-policy. It was mounted for the fs arm alone on the reasoning that its tools' guidance mentions the policy; of the four presets in @deepseek-ai/dsh, three mount dsh-tool-fs and none mounts the policy, and its own documentation covers str_replace_editor too. Mounting it in one arm gave that arm two failure modes the others cannot have.

Three compositions in three files can drift apart while still producing numbers, so the control is asserted rather than intended. edit-contract-arms.test.ts fails when the compositions differ by any row outside their editors, when they compose in a different order, when the model, context window, reasoning effort, stream idle timeout, sandbox mode, persona or either Bash deadline drifts in one, when the arms stop sharing one toolchain identity object, when the experiment launches them with anything but a different profile argument, or when arms multiplied by concurrency leaves the 128-trial convention.

The patch arm is ours, and that is the thing to review

The harness ships no patch tool, so plugins/tool-apply-patch/ is this repository's while the other two arms run their vendor's. Fidelity to Codex is therefore itself an experimental variable, and it is asymmetric: a tool stricter than the reference costs its arm turns the reference never spends, and a safer one wins recoveries the reference never gets.

One deviation cannot be removed.apply_patch in Codex is a freeform tool — the API is sent a 108-character description plus a Lark grammar the decoder is constrained to, so a model on that path cannot emit a malformed envelope. The harness registers JSON function tools and has no grammar seam. This arm is therefore the unconstrained case, and syntax errors it can make are errors Codex's decoder would have prevented. That cost is charged to this arm and to no other, and it leads the arm's description in the README.

What is not charged to it is the absence of a read tool. Codex registers no handler that reads file text either — its handler set is long, and the only entry taking a filesystem path to read is view_image, which takes images — so an arm that reads with cat is that contract, and every arm has the same bash at the same 16000-character cap.

What the model is told is upstream's own prose for exactly that case: the ## apply_patch section of prompt_with_apply_patch_instructions.md, carried verbatim as upstream-apply-patch-instructions.md with three substitutions applied over it. A test pins the file's digest, requires each substitution to apply exactly once, and requires the built description to differ from the copy nowhere else. That file is upstream's and it is not delivered from where it sits: at the ported commit it is referenced only from a test whose four model cases all declare expects_apply_patch_description: false. Seven of the eight instruction templates in models-manager/models.json carry no ## apply_patch section and leave the format to the grammar; the eighth, gpt-5.2, carries a shorter one with the same envelope and worked example. So the format is still taught in prose upstream and this is the fuller version of it. Either way no shipped configuration is the one this arm is in — gpt-5.2 gets prose and grammar — so the choice was between upstream's own text and text written here.

Three further divergences are known and each is pinned to bytes the reference produces:

  • the tool implements the function-tool path — verify the whole envelope, then re-apply it from the raw text — where the standalone binary applies sequentially with no verification;
  • it runs NormalizeToLf, the shipped default, where PreserveLineEndings is Stage::UnderDevelopment, default_enabled: false upstream;
  • its diagnostics differ from Codex's, which is the one place this arm may be easier on a model. This one is recorded in prose only; the other two are named with their causes in the test files, which also assert that each still diverges.

A third divergence was recorded here and has since been removed rather than documented: the provider's readText strips a leading byte-order mark and rejects a NUL byte, neither of which String::from_utf8 does. Reading through readBytes and decoding in the tool matches the reference exactly.

The grammar is implemented in full. *** Delete File: and *** Move to: are the only operations ctx.fs has no primitive for, so they run against ctx.fs.processPath(target); under a provider that confines, both refuse up front rather than escape. This arm mounts danger-full-access, where the refusal never fires.

Sources and licences are in the plugins' NOTICE. An earlier revision of this branch vendored the OpenAI Agents SDK's applyDiff as the hunk applier; it is a different V4A implementation, not Codex's, and nothing from it remains.

What the adversarial review rounds changed

Two rounds of four fresh-eye passes each — the port against the binary, the harness integration, the three-arm control, and an audit of every factual claim. The second round reviewed the first round's fixes, which is what caught the two entries below that begin "the previous fix". Every finding was re-verified against upstream source or the reference binary before being acted on.

Nine port defects, all now red-checked. The matcher used JavaScript's trim where upstream uses Rust's, so a file whose first line carried a byte-order mark matched a pattern without one and was written back with the mark deleted, and a line terminated by U+0085 was refused where the reference applies it. The verify pass stated a delete target instead of reading it, applying deletes of non-UTF-8 files that the reference refuses outright. It emitted fs/observed {present} off a bare stat, telling an observation policy a file had been seen that nothing read. The duplicate-path key was the provider's realpath-derived target key rather than upstream's lexical join, so an envelope naming both a symlink and its target was refused where the binary applies both. A move destination ran the fs/write-intent waterfall and discarded the answer, turning a decider's refusal to clobber into a silent overwrite. A failed unlink and an abort between passes escaped the seam's error taxonomy.

Five of those were invisible because the test provider was weaker than dsh-fs-local: it decoded lossily, called every non-directory a file, took three parameters so the signal and sandbox policy could not be observed, and never returned a decider's answer.

The previous fix read patch targets through the wrong seam.ctx.fs.readText is not read_file_text: upstream reads bytes and calls String::from_utf8, while dsh-fs-local additionally rejects a NUL in the first 8192 bytes and strips a leading byte-order mark. NUL is valid UTF-8 and the reference patches and deletes straight through it, so making the verify pass read delete targets — itself a correct fix — took *** Delete File: on such a file with it. Reading through readBytes and decoding in the tool is exactly String::from_utf8, and it removes the byte-order-mark divergence too. Separately, *** End of File as the first line of an update hunk was refused where upstream ignores it.

Twelve more mutations the suite could not see. Five dropped the turn signal from a provider call, which a fake that ignored the signal could not notice; two removed or falsified the version on fs/observed; two swapped a removal's announcement with the removal itself, which leaves the event list identical and the trail a lie; one bypassed the create decider, one renamed its event, one made the sandbox-denial remap a no-op. The fake now honours and records the signal on every call, records what the filesystem held when each observation was published, records the version writeText returned, and can be made to deny a write.

Three uncontrolled differences between the arms, of which one fix was itself wrong. streamIdleTimeoutMs was omitted from all three profiles, leaving a five-minute ceiling on the gap between two streamed tokens behind reasoningEffort: max and a 65-minute bash deadline; it now carries upstream's own 172800000. maxConcurrentTaskGroups: 64 made 192 concurrent trials where the saturating specs here land on 128; it is now 42.

readMaxLineLength was raised from its shipped 2000 to the budget to close a cap the baseline has no counterpart to, and that was worse than the asymmetry. The tool appends a ... (line truncated to N chars) marker and then charges the whole line against readMaxBytes, so a cap near the budget makes a long line not fit at all, drops every line after it, and returns a window no offset recovers — 201 lines at 2000 becomes 0 at 16000. No value equalises the two budgets anyway, one counting UTF-16 code units and the other UTF-8 bytes, so the cap is back where the tool ships it and the guard test asserts the invariant rather than the number.

Twenty-one false claims across the two rounds, listed in the two docs commits. The largest was the provenance one, twice: first that the vendored prose is what Codex sends, then that it is retired. The read-budget script had four transcription errors of its own, all inflating the character ratio, so it no longer asks to be trusted — --self-check <dir> lifts the real renderers out of an installed copy and asserts agreement, and the figures are 1.093x lines and 1.001x characters over 2197 files.

Verification

Lint, format and typecheck clean. 47 TypeScript and 200 plugin tests pass.

Fidelity is measured against the binary rather than argued from the Rust:

  • codex-oracle.mjs runs the released codex under argv0 apply_patch over a temporary tree for each of 94 cases and records what it printed and what the tree became; codex-fixtures.test.mjs replays every case through the registered tool and needs no codex to do it.
  • __tests__/upstream/scenarios/ carries Codex's own conformance suite verbatim — the 25 scenario directories upstream publishes as "meant to be easily portable to other languages or platforms" — and upstream-scenarios.test.mjs replays them upstream's way: copy the input tree, apply the patch, ignore the exit status, require the resulting tree to match exactly. 22 match outright; the three that do not are named divergences. These are the only inputs here this repository did not choose.
  • codex-fuzz.mjs generates envelopes from random line soup, including near-miss context, and runs both. It earned its keep on this branch: it caught a regression introduced here, where rebuilding the file forward let a context-free insertion survive inside a range a later chunk replaces. 2300 envelopes across four seeds diverge on nothing.

Red-checked, not just green:

  • Appending a line to the description, renaming the tool, editing one byte of the vendored instructions, or neutering a substitution each fails the contract-surface test.
  • Removing any one of the four context-matching passes fails at least one oracle fixture; restoring String.prototype.trim in the loose passes fails the trailing-U+FEFF fixture; disabling the heredoc strip fails both heredoc fixtures; matching the heredoc close with includes instead of endsWith fails the trailing-text fixture.
  • Each of the twenty-one port defects and unseen mutations above fails the suite when reintroduced. Seventeen of them did not before these two rounds.
  • Both divergence lists assert that each entry still diverges, so an excuse cannot outlive the thing it excused.

End-to-end against the rebuilt linux/amd64 toolchain: loading the plugin from inside the built tree registers exactly one tool named apply_patch, with one required input string and a 2903-byte description carrying no mention of the shell delivery. The instructions file ships, so it is covered by checksums.sha256 and the re-pinned fingerprint.

Not run: an actual benchmark cohort. No dependency moved.

Review focus

One claim this repository does not reproduce, and which should be read as a one-time manual check rather than a standing guarantee: the three profiles were compared against the upstream examples/jsonrpc-agent composition during development — tool names, tool schemas, system prompt and message sequence on the first outbound request — and found identical.

The read budget matched at 16000 equalises the budget's number, not delivered content. scripts/measure-read-budget.mjs makes the residual checkable and --self-check makes the script itself checkable: over 2197 tracked sources the fs arm delivers 1.093x the lines and 1.001x the characters; among the 570 files where a budget binds, the extremes are 1.398x/1.168x and 0.837x/0.720x.

Two things a write-up must carry that this branch cannot fix. The fs arm receives 764 characters of tool-contributed system-prompt guidance where the other two receive none, and two of its three sections reference an fs-observation-policy no arm mounts. And the treatment is the whole tool family plus that guidance, not "the edit contract" — the arm names are shorter than the thing being varied.

experiments/.../edit-contracts.json declares repetitions: 1, so 89 tasks x 3 subjects is 267 cells. Cost, not power, chose that. McNemar at one repetition is weak for the effect sizes this is likely to see: on ~20% discordant pairs a true 5pp difference is about 2 sigma before any correction for three pairwise comparisons, so a write-up has to report discordant counts and intervals rather than three success rates.

Stacked on #2971.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes — described under Summary above
  • No

The harness ships no patch tool, and an Eval arm that gives the model
Codex's patch language needs one composed the same way its other editors
are: `inject: ['tools', 'fs']`, one `ctx.tools.register(defineTool(...))`,
and every file operation through `ctx.fs`. Reaching around that seam would
put a second filesystem authority in one arm.
Applying a single file's hunks — context matching with the fuzz ladder — is
the part that is hard to get right, so it is not written here. It is the
OpenAI Agents SDK's `applyDiff`, vendored rather than depended on:
`@openai/agents-core` unpacks to 9 MB and pulls the whole `openai` SDK into
a tree that is fingerprinted file by file, while the function itself has no
imports at all. The upstream test suite is vendored with it, which is what
turns a mechanical tsc emit from an assertion into a checked fact; its two
import lines are the only edit. Biome's formatter now skips
`packages/eval/harbor/**/vendor/**` for the reason it already skips the
licence artifacts: a rewritten vendor file is no longer the upstream file.
What is written here is the envelope parser and the tool. The grammar is
transcribed from Codex's lark definition, and the model-facing description
is Codex's own instructions text. Two deviations are deliberate and are
stated to the model rather than left to be discovered by failure:
- No `*** Delete File:` and no `*** Move to:`. `ctx.fs` has no delete or
rename primitive and neither does any other harness file tool, so in
every arm the model removes and renames through bash.
- A patch either applies completely or changes nothing. `applyDiff` is
pure, so every file's new content is computed before the first write.
The contracts this will be compared against touch one file per call and
cannot half-apply; a partly-patched tree would be a difference in
failure modes rather than in edit format.
Sources and licences are recorded in the plugins' NOTICE. The three test
files run from the repository through `test:dist`, against the same
`@deepseek-ai/dsh-tools` and `@deepseek-ai/dsh-fs` the toolchain installs —
added as exact-pinned dev dependencies, which the lockfile takes as pure
additions with no existing resolution changed.
No arm composes this tool yet.
Generated-by: Claude Code
Terminal-Bench results attribute a score to a framework, which leaves the
edit contract inside the framework and unmeasured. Comparing Maka's
`edit`/`write` against the harness's `str_replace_editor` cannot separate
them either: that comparison moves the framework and the contract at once.
So this holds everything else and moves only the contract. Three arms run
the same one-shot CLI, the same toolchain and fingerprint, the same
`dsh-fs-local` provider under every editor, and byte-identical
`package.json` and `cordis.yml`:
- `deepseek-harness` keeps `str_replace_editor` — one tool, four
commands, unique-literal match, all guidance in the tool description.
- `deepseek-harness-fs` takes the harness's own `read`/`write`/`edit` —
three tools, snake_case arguments, three prompt sections of their own.
- `deepseek-harness-apply-patch` takes the V4A tool added previously —
one envelope, changes located by surrounding context.
The variable is the whole contract, not one dimension of it. Tool count,
argument names, path conventions, where the guidance lives, and the
read-before-edit policy move together because each family ships that way.
`dsh-fs-observation-policy` is mounted for the `fs` arm alone for that
reason: its tools' own guidance text tells the model the policy is in
force, so omitting it would leave the prompt describing a rule the
deployment does not enforce, while `apply_patch` has no such concept and no
read tool to satisfy one with.
Three compositions in three files can drift apart while still producing
numbers, and the numbers would no longer be about the contract. So the
control is asserted rather than intended: the new test fails when the
compositions differ by any row outside their editors, when the model,
reasoning effort, sandbox mode, persona or either Bash deadline drifts in
one of them, when the arms stop sharing one toolchain identity object, or
when the experiment launches them with anything but a different profile
argument. The arms also share one preparer, so a change to how the harness
is launched cannot reach one and miss another, and one identity constant,
so a re-pinned fingerprint cannot be applied to two arms out of three.
The toolchain build now installs the plugin tree beside the harness's
`node_modules` — a composition copied under `$DSH_HOME` cannot resolve a
plugin by package name from there — and fails if a declared plugin did not
land. Its dependencies are unchanged: every package the new arms compose
was already in the reviewed lockfile.
Re-pinning the fingerprint needs a `linux/amd64` container build and is not
done here, so the arms are not yet runnable end to end.
Generated-by: Claude Code
The toolchain now carries the `apply_patch` plugin beside the harness's
`node_modules`, so its checksum manifest changed and the pin has to follow.
Rebuilt with the same pinned `linux/amd64` image and the same reviewed
lockfile; no dependency moved.
Verified against the rebuilt tree rather than assumed. `dsh --dump-config`
composes all three arm profiles successfully, and the only differences
between the three composed trees are the editor rows and the `cwd` the two
new arms give the filesystem backend — nothing else diverged. Loading the
plugin from inside the toolchain resolves `@deepseek-ai/dsh-tools`,
`dsh-fs`, `dsh-sandbox` and `schemastery` through the upward walk into
`lib/dsh/node_modules`, registers `apply_patch` with the expected schema,
and carries the patch instructions without the operations the tool does not
implement. `dsh-tool-fs` and `dsh-fs-observation-policy` resolve too.
Generated-by: Claude Code
@coderabbitai

coderabbitaiBot commented Aug 15, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1357f048-8ea6-474e-8887-d512c43407ac

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

The tool shipped without them, refusing both with a pointer to the shell.
The stated reason was that `ctx.fs` has no delete or rename primitive, so
reaching them through `node:fs` would put a second filesystem authority in
the one arm being measured. That reason does not survive contact with the
deployment it was written for: this arm mounts `dsh-sandbox-local` at
`danger-full-access`, no observation policy is loaded, and the model's own
bash can already remove any file in the container. The authority being
protected was not enforcing anything.
The cost was real, and it was the point of the arm. `apply_patch` exists
here to measure Codex's contract; a patch tool missing a third of its
grammar is a different contract, and a model trained on the real one pays
for the difference in wasted turns. It also broke this PR's own rule —
every other arm composes what its tool family ships, which is exactly why
the `fs` arm mounts the observation policy. Two standards in one change.
So both operations are implemented, against `ctx.fs.processPath(target)` —
the provider's own answer to where a file is for something outside it to
open, and for `dsh-fs-local` the local absolute path. `ctx.fs` stays the
only thing that resolves paths; only the two syscalls it does not offer
happen outside it. Semantics follow Codex: a rename writes the destination
and then removes the source, overwrites an existing destination rather than
refusing, and creates missing parents.
Where the original concern is real, it is enforced instead of assumed: under
a provider that confines, `processPath` would be the way around the sandbox,
so delete and rename refuse up front. `sandboxMode` is what decides, so the
refusal fires exactly when a provider actually confines.
The parser already implemented the whole grammar, so no change there. The
tool description loses its "not part of this tool" paragraph and returns to
Codex's text, now with one removal rather than three: the trailing shell
invocation example, which is false for a function tool.
Tests move from an in-memory store to a real temporary directory, since
these two operations are syscalls an in-memory store cannot observe.
Red-checked: stubbing out the removal fails five behaviour tests, and
disabling the confinement guard fails the two that cover it.
Generated-by: Claude Code
The plugin gained its delete and rename paths, so the file it contributes to
the toolchain changed and the checksum manifest with it. Rebuilt from the
same pinned image and the same lockfile; no dependency moved.
Verified against the rebuilt tree: all three arm profiles still compose
under `dsh --dump-config`, and loading the plugin from inside the toolchain
registers `apply_patch` carrying the full grammar — `*** Delete File:`,
`*** Move to:`, and both productions — with the shell-invocation example
still absent.
Generated-by: Claude Code
Independent review of this arm found five places where the tool behaved
differently from the reference it exists to measure. The asymmetry matters
more than the count: a tool stricter than Codex costs its arm turns the
reference never spends, and a tool safer than Codex wins its arm recoveries
the reference never gets. Either way the benchmark reads the difference as
an effect of the edit contract.
`*** Add File:` did not terminate the created file with a newline, because
the vendored applier joins its added lines while Codex appends `\n` after
each one. Every file this arm created differed from Codex's byte for byte,
and a test pinned the wrong result. Each added line is now terminated, and
an empty create still writes nothing, as `String::new()` does upstream.
The envelope was planned against one snapshot and written afterwards, so a
failed hunk left the tree untouched. That is the nicer property and it was
still wrong: two sections naming one path made the second fail against a
version the first had replaced, and a delete followed by an update removed
the file and then failed, leaving neither. Operations now apply in order,
each reading what the previous one wrote.
`*** Add File:` over an existing file was refused with an invented
`FS_ALREADY_EXISTS`; Codex records what it overwrote and writes. A bare
`*** Move to:` with no hunks was accepted; Codex rejects an empty chunk list
before it reads the rename. A leading newline, a trailing space on the
opening marker, and an indented envelope were all refused; Codex trims the
patch and each marker line, while comparing only `trim_end()` inside an
update body so an indented context line stays content. Content after
`*** End Patch` was silently dropped, so two envelopes in one call reported
success over operations that never ran.
Three defects in how it reached the filesystem, none of which fire under the
arm's own non-confining provider:
- `mkdir` ran on every write through `processPath`, the same way out of a
sandbox that delete and rename refuse for. A create outside a confining
workspace had its write denied and still left the directory behind.
- Delete and rename used the provider's realpath-derived target key, so
deleting a symlink destroyed the file it pointed at and left the link
dangling. They now use the lexical path, which is what Codex removes.
- Hunk failures reached the model as a bare `Invalid Context 0`, naming
neither the file nor the section, and two error codes were outside
`FsErrorCode`'s closed union.
The description's provenance note claimed one modification from upstream
where there are four, and NOTICE claimed the description dropped the delete
and move operations — true when written, false once they were implemented,
and wrong in the direction that matters for a modification notice.
84 plugin tests pass. Each divergence is pinned by a test naming the
upstream file and line it follows.
Generated-by: Claude Code
Review found two differences between the arms that were not the edit
contract, and a control test that could not have caught either.
Only the `fs` arm mounted `dsh-fs-observation-policy`, justified in the
profile by that family's guidance mentioning it. Checking how the harness
ships does not support this: of the four presets in `@deepseek-ai/dsh`,
three mount `dsh-tool-fs` and none mounts the policy, which is not even a
direct dependency; and the policy's own documentation covers the baseline's
editor too. It is substrate, not part of an editor. Mounting it in one arm
gave that arm two failure modes the others cannot have — an edit without a
prior read, and a file the model's own shell touched in between — on a
benchmark where shell and editor are used together constantly. No arm
mounts it now. The apply-patch profile's stated reason for omitting it was
also wrong, and is corrected rather than quietly dropped: that arm emits
`fs/observed` with the file's version before it writes, so the policy would
have allowed the write.
The two new arms configured `fs-local` with a `cwd` expression the baseline
did not have. It could not change anything — the provider defaults `cwd` to
`process.cwd()`, both tools pass the session's own cwd, and `DSH_CWD` is
never set — but an unreachable setting is still a difference a reviewer has
to rule out.
Read budgets are now matched. The baseline pins its editor to 16000 output
characters while `dsh-tool-fs` shipped a 51200-byte default, a 3.2x window
on the same task. How much of a file one call returns is not part of the
edit contract, so `readMaxBytes` is set to match. The remaining read-side
asymmetry, that the patch arm has no read tool at all, is contract-inherent
and stays.
The control test read each `- id:` row with the `name:` on the line below
and compared those strings, leaving every `config:` block unchecked.
Narrowing one arm's context window, halving an editor's output budget,
rewriting the shared bash description, reordering the rows, or overriding
the patch tool's own description all kept it green. It now parses the YAML
and compares whole entries, asserts composition order, and pins each arm's
editor row by value — the treatment was the one thing no assertion covered.
Verified by making each of those six edits and watching it fail.
The arm-to-directory table moves to `toolchain-verification.ts`, beside the
identity table, and the subject and the test both read it. The test used to
restate it, so an arm pointed at the wrong profile would have been checked
against the composition it was meant to run rather than the one it runs.
The toolchain fingerprint is re-pinned for the plugin changes in the parent
commit. Verified in a `linux/amd64` container: all three profiles compose,
the composed trees differ only in the editor row, and the plugin loads from
inside the toolchain and registers `apply_patch` with the full grammar.
Generated-by: Claude Code
The apply_patch arm applied one file's hunks with the OpenAI Agents SDK's
`applyDiff`, vendored on the assumption that one V4A implementation is like
another. It is not. Running the released `codex` binary over the same trees
shows four disagreements, each of which moves this arm's score: a context-free
`+` hunk lands at the `@@` anchor rather than at end of file, an updated file
keeps whatever trailing newline it had rather than gaining one, CRLF survives,
and near-miss context is refused where Codex's four matching passes accept it.
Replace it with a port of Codex's own applier and parser. The parser now reads
hunks into chunks instead of passing section bodies through, so a syntax error
in the third section is found before the first is written, and the tool no
longer needs a second project's operation shape.
Restore verify-then-write. An earlier round changed the tool to apply
operations sequentially, matching the standalone `apply_patch` binary. That
binary is not what a model calls: the function-tool handler and the intercepted
shell call both go through `try_verify_apply_patch_args`, which collects hunks
into a map keyed by resolved path, refuses a duplicate key, and computes every
change against the pre-patch tree. So an envelope naming one path twice is
refused, and a hunk whose context the model guessed at costs nothing.
Fidelity is now measured rather than argued. `codex-oracle.mjs` runs the real
binary over a temporary tree for each of 72 cases and records what it printed
and what the tree became; `codex-fixtures.test.mjs` replays every case through
the registered tool and compares. The two cases where the function tool
deliberately differs from the standalone binary are named there with what it
does instead. A fuzzer over 1800 random envelopes found no further divergence.
Also tighten the arm-control test, which had three gaps: it skipped composition
steps that override an already-composed entry, so an override of the patch
tool's description was invisible; it named the profile files to compare instead
of reading them; and it exempted the first subject's profile argument while
comparing only the argument vector, not the whole subject definition.
Toolchain fingerprint re-pinned. Test-only files move under `__tests__/` so the
build prunes them by directory rather than by filename.
Generated-by: Claude Code
`readMaxBytes: 16000` was justified as making "how much of a file one call
returns" equal across the two read-capable arms. It does not. The baseline
editor spends eight characters a line on line numbers plus a header; the fs
tool emits neither, so measured over this repository's own sources it returns
10% more lines for the same budget and 22% on the longest files.
The budget is still the right one, for a reason the note did not give: what
should be equal is what a read costs the context window, and the numbering the
baseline adds is part of its contract that the model pays tokens for either
way. Equalising delivered content would mean picking a budget from an assumed
average line length.
Record the measurement, and record the one genuine mismatch that remains — the
budgets are counted in UTF-16 code units against UTF-8 bytes, equal on ASCII
and not otherwise.
Generated-by: Claude Code
The fixtures pin the cases someone thought to write down. This searches for the
rest: it generates envelopes from random line soup, applies each with the
released binary and with the registered tool, and reports every input the two
disagree on. 2100 envelopes across seven seeds currently diverge on nothing.
Red-checked against three deliberate regressions — placing a context-free `+`
hunk at the `@@` anchor, dropping the trailing-newline normalisation, and
removing the punctuation-folding match pass. The third went unnoticed through
400 envelopes at first, because context lines drawn verbatim from the file only
ever exercise the exact-match pass; the generator now half-remembers a line the
way a model does, and catches it.
Envelopes name each path at most once, and a refusal is compared as a refusal
without comparing the tree, because those are exactly where the standalone
binary and the function tool are supposed to differ.
Generated-by: Claude Code
The function-tool handler verifies an envelope and then throws the result
away: `ApplyPatchRuntime::run` hands `req.action.patch` — the raw text the
model sent — back to `apply_patch_with_mode`, which re-parses it and applies
it sequentially against the live filesystem. The verified `changes` map is
carried for approval and telemetry only.
This tool verified and then wrote the plan it had just computed. That is not
a cheaper version of the same contract, and the reference binary shows the
difference in two cases now covered by tests: a rename onto a path a later
section also updates leaves that section failing against the renamed file,
and `*** Move to:` naming its own source writes the destination and then
unlinks it, deleting the file while reporting success. Both were previously
refused with an invented "multiple operations target" error, because the
verification map also claimed the move destination — upstream keys only on
`hunk.resolve_path()` and carries `move_path` as a value.
Also in the write path: `fs/edit-intent` yields `{version}` with no `kind`,
so passing it straight to `writeText` matched neither guard and silently
wrote unconditionally; the abort signal was not checked between operations;
and `rm` ran with `force: true`, reporting `D <path>` for a file that had
already gone.
In the applier: each fuzzy-matching pass now projects both sides once
instead of recomputing the punctuation fold per candidate position, and
replacements are rebuilt in one forward pass rather than spliced, which
threw a RangeError once a hunk added more lines than the argument limit.
The model-facing description is no longer transcribed into this file. It is
read from a byte-for-byte copy of the upstream artifact it came from, with
three named substitutions applied over it, so the claim that it differs in
exactly those places is checkable rather than asserted in a comment. Copying
the file rather than retyping it also removed a fourth difference that had
no reason to exist: upstream's typographic punctuation is now preserved.
Its recorded provenance was stale — the file it named no longer exists at
the pinned commit — and now points at the artifact that does.
One parameter description said the input was "Not JSON-wrapped". It is: the
harness registers JSON function tools and nothing else.
Generated-by: Claude Code
Upstream ships codex-rs/apply-patch/tests/fixtures/scenarios as, in its own
README's words, an end-to-end suite "meant to be easily portable to other
languages or platforms". This is the port: the 25 scenario directories are
copied verbatim from the pinned commit and replayed through the registered
tool, compared the way upstream compares them — copy `input/`, apply
`patch.txt`, ignore the exit status, and require the resulting tree to equal
`expected/` exactly.
It is the only check here with the standing to contradict the code. The
oracle fixtures run the reference binary, but over inputs this repository
chose; the fuzzer perturbs inputs this repository chose how to perturb.
These were chosen by upstream, before and independently of this tool.
22 of 25 match outright. Three do not, and each divergence is recorded with
its cause and pinned to bytes the reference binary produces:
- 015 specifies the standalone binary, which has no verification pass, so an
add preceding a failing update survives. The function tool writes nothing.
- 023 and 024 are specified under CODEX_APPLY_PATCH_PRESERVE_LINE_ENDINGS,
a feature that is `Stage::UnderDevelopment, default_enabled: false`, so a
shipped Codex and this arm both run the `NormalizeToLf` default.
Both divergence lists — here and in the oracle fixtures — now assert that
each entry still diverges, so an excuse cannot outlive the thing it excused.
Generated-by: Claude Code
The treatment in this arm is the tool name, the parameter it takes, and the
description text. None of the three had any coverage, so a build that
registered the tool under another name, dropped the parameter, or shipped a
truncated description would have run a cohort and produced numbers.
The description claim is the one that had to be argued rather than shown:
"three differences from upstream and no others" was a comment. It is now
checked at both ends. The vendored instructions file is pinned by digest,
with the `git show ... | sed -n` that reproduces it from the source recorded
beside the constant, and the built text is recomputed here by a different
mechanism than the code uses and required to match. Each adaptation must
apply exactly once, and no mention of the shell delivery may survive.
Red-checked against four sabotages — appending a line to the description,
renaming the tool, editing one byte of the vendored text, and neutering an
adaptation. Each is caught.
Generated-by: Claude Code
Two differences at the outermost layer of the patch, both pinned by new
oracle fixtures generated from the reference binary.
`parse_patch` selects its mode from `PARSE_IN_STRICT_MODE`, which is `false`:
upstream gave up on gating leniency by model, so every call site including
the function tool unwraps a heredoc-wrapped envelope. A model asked to send
a patch may well send the `<<'EOF' ... EOF` form it was shown, and Codex
applies it where this refused it. The strict boundaries are still tried
first, so an envelope whose own first line reads `<<EOF` is never stripped.
Rust's `str::trim` cuts the Unicode `White_Space` property; JavaScript's also
cuts U+FEFF and does not cut U+0085. Only U+FEFF can realistically occur — a
byte-order mark carried into a JSON string argument — and it made this tool
accept a patch Codex refuses, which is the wrong direction for an arm being
compared against the contract it imitates.
Red-checked: restoring `String.prototype.trim` fails the byte-order-mark
fixture, and disabling the heredoc strip fails both heredoc fixtures.
Generated-by: Claude Code
The comment justifying `readMaxBytes: 16000` said `str_replace_editor`
spends eight characters a line on line numbers and a header while "this tool
emits neither". Both tools number their lines, and this one wraps the result
in a `<path>/<type>/<content>` envelope with a footer.
The setting still holds something equal, but not what was claimed. The
baseline truncates the fully rendered string; this tool charges the budget
against raw line bytes plus separators and adds the gutter, the envelope and
the footer afterwards, outside it. Measured across this repository's 1480
TS/JS/Python sources at 16000, the arm delivers 1.105x the lines and emits
1.027x the characters, worst file 1.34x and 1.19x — a residual in this arm's
favour, now recorded with the mechanism that produces it.
The three profiles also claimed to have been "verified request-for-request"
against the upstream composition. That was a manual comparison made once
during development; nothing here reproduces it, and the headers now say so.
`MAKA_EVAL_DSH_PLUGINS` was exported into all three arms and read by one, so
the two controls carried an environment difference for a mechanism they do
not use. It is now set from the copied composition itself, which keeps the
condition and the file that depends on it from drifting apart.
Generated-by: Claude Code
`apply_patch` in Codex is a freeform tool: the API is sent a short description
and a Lark grammar the decoder is constrained to, so a model on that path
cannot emit a malformed envelope. The harness registers JSON function tools
and has no grammar seam, so this arm is the unconstrained case. That is not a
detail of the port — it is a cost charged to this arm and to no other, and it
now leads the arm's description instead of being absent from it.
Three further divergences are named with their causes: the function-tool path
against the standalone binary's sequential apply, `NormalizeToLf` against a
preserve-line-endings feature that ships disabled, and diagnostics more
specific than Codex's single `Invalid patch:` line, which is the one place
this arm may be easier on a model than the reference.
The README also described a verify-then-write-the-plan design the tool no
longer has, quoted the old fixture count, and repeated the read-budget
mechanism corrected in the previous commit.
The NOTICE opened by saying nothing was carried verbatim as a file; two
things now are. Its description provenance named a file that no longer exists
at the ported commit and a fourth modification that was an artefact of
retyping rather than copying, its fixture path was stale, its account of how
`PreserveLineEndings` is selected covered only the standalone binary, and its
list of ported functions omitted the ones that decide apply order.
The arm-control test read a composition step as an insertion whenever it had
an `insert` key, so `{insert, remove}` would have passed with the removal
unread, and it filtered directories out of each profile's file list rather
than refusing them. It also now names what it does not cover.
Generated-by: Claude Code
`dsh-fs-local` creates a write's parent itself, in `writeFileAtomic`, and it
is the provider every arm mounts. The `ensureParent` here ran `mkdir` on a
`processPath` — the same way around a confining sandbox that delete and
rename refuse for — and existed only because the test harness's provider was
weaker than the real one and failed with ENOENT without it.
That is the more useful half of this change: a fake that cannot do what the
real thing does will not merely miss bugs, it will make production code grow
to cover for it. The fake now creates the parent the way `writeFileAtomic`
does, and the tool has no call that creates a directory at all.
The test that pinned the old behaviour was passing for the wrong reason — it
asserted the tool left no directory behind, and what actually happened was
the fake's write failing before anything could be created.
Generated-by: Claude Code
…sses
`deriveNewContents` reported a context miss as a plain `Error`, and the
caller rewrapped anything it threw as `FS_EDIT_NOT_FOUND` — so a bug in the
applier would have reached the model as "the lines you named are not in the
file", which is advice to rewrite a correct patch. The context miss is now
its own type and the only thing translated; a comment already claimed this
and the code did not.
Two oracle fixtures pin the order of the four matching passes against each
other. The existing cases pinned the exact pass ahead of the loose ones;
between the loose ones, removing a pass does not fail a match, it silently
moves it to a different line, and nothing caught that. Each of the four now
fails at least one fixture when removed.
The line-ending note in codex-apply.mjs described only the standalone
binary's environment variable, not the feature flag the function tool reads.
The toolchain is rebuilt and its fingerprint re-pinned: the model-facing
instructions now ship as a file, so they are covered by `checksums.sha256`
rather than living only inside a JavaScript template. Verified inside the
pinned linux/amd64 container that the built tree registers one tool named
`apply_patch` with one required `input` string and a 2903-byte description
carrying no mention of the shell delivery.
Generated-by: Claude Code
Differential fuzzing against the reference binary found a regression this
branch introduced. `applyReplacements` was rewritten to build the file
forward in one pass, on the reasoning that the replacements are sorted and
disjoint. They are sorted; they are not always disjoint.
`computeReplacements` places a context-free insertion at the end of the file
without consulting the search position — deliberately, because that is what
`insertion_idx` does upstream — so it can land inside a range a later chunk
replaces. Applying descending, the replacement swallows it; applying forward,
the two are treated as separate edits and the inserted line survives into a
file the reference never produces.
Restored to splicing in descending order, with the replacement segment going
in in bounded batches so a hunk larger than the engine's argument limit still
does not throw. The case is now an oracle fixture, so it is caught without a
`codex` on PATH; red-checked by reinstating the forward rebuild.
2300 further envelopes across four seeds diverge on nothing. Toolchain
rebuilt and re-pinned.
Generated-by: Claude Code
An adversarial review pass found nine behaviours that were wrong or untested
in the V4A tool, each verified against the reference binary or upstream source
before being changed. All nine are now covered: sabotaging any one of them
fails the suite, where before five of them left it green.
The matcher used JavaScript's `trim` where upstream uses Rust's. The two
disagree on U+FEFF and U+0085, so a file whose first line carried a byte-order
mark matched a pattern without one and was written back with the mark deleted,
and a line terminated by NEL was refused where the reference applies it. The
charset the parser already spelled out moves to rust-trim.mjs and both use it.
The verify pass now does what `try_verify_apply_patch_args` does: nothing at
all for `*** Add File:`, and a read for `*** Delete File:` as well as for an
update. Statting a delete target instead applied a delete the reference
refuses, since `read_file_text` maps invalid UTF-8 to `InvalidData` and fails
the whole envelope. It also stops emitting `fs/observed {present}` off a bare
stat, which told an observation policy a file had been seen that nothing read.
The duplicate-path key is lexical, as upstream's `PathUri::join` is, not the
provider's realpath-derived target key. An envelope naming both a symlink and
its target was refused here; the binary applies both sections in order.
A move destination no longer runs the `fs/write-intent` waterfall and discards
the answer, which turned a decider's refusal to clobber into a silent
overwrite. A failed unlink and an abort between passes now arrive as `FsError`
with a routable code and the path the model wrote, not a raw errno and a
resolved host path.
The test provider was weaker than `dsh-fs-local` in four ways that made the
above invisible: it decoded lossily where the real one raises `FS_NOT_TEXT`,
called every non-directory a file, took three parameters so the signal and
sandbox policy could not be observed, and never returned a decider's answer.
Two oracle cases are added for the trim charset and one for a trailing U+FEFF,
which is what separates the three loose matching passes from each other; the
BOM-in-content case is recorded as a divergence with its true cause, which is
that the shared provider strips a leading mark before this tool sees the file.
Generated-by: Claude Code
The import landed the 25 scenario directories, their README and their
.gitattributes twice: once at `__tests__/upstream/` and once, byte-identically,
at `__tests__/upstream/scenarios/`. Only the nested copy is read.
Generated-by: Claude Code
…arms
The three arms are meant to differ only in the edit contract. Three settings
outside that contract were not held.
`readMaxLineLength` was left at its shipped 2000 while only `readMaxBytes` was
matched to the baseline. That is a cap the baseline has no counterpart to: the
fs arm could never see the 2001st character of any line, at any offset, while
`str_replace_editor` truncates once over the whole rendered view and can spend
its entire budget on one line. A minified bundle or a single-line payload is
where that lands, and it lands against one arm only. It is raised to the
budget, where it can no longer bind before `readMaxBytes` does.
`streamIdleTimeoutMs` was omitted from all three profiles, leaving the
adapter's 300000 ms default in force — five minutes between two streamed
tokens, behind `reasoningEffort: max` and a 65-minute bash deadline. Upstream's
own jsonrpc-agent composition sets 172800000, which is what the arms now carry;
a turn killed by that timeout would have been scored as a failure of whichever
contract was running.
`maxConcurrentTaskGroups` was 64. A group holds one cell per subject and starts
them together, so three arms made 192 concurrent trials where every other spec
here lands on 128. That does not bias one arm against another, but it puts this
run's `deepseek-harness` score under a load no single-arm run shares. It is now
43, and the test asserts the arithmetic rather than the number.
The read-budget residual quoted in the profile and the README had no
reproducible source and reported only the tail that favoured the fs arm.
`scripts/measure-read-budget.mjs` reimplements both renderers from the shipped
code and reports both tails; the figures are replaced with what it prints.
Generated-by: Claude Code
A claims audit checked every factual assertion in the README, the NOTICE and
the profiles against upstream source at the ported commit and against the
shipped harness packages. Fourteen did not hold.
The largest is provenance. `prompt_with_apply_patch_instructions.md` is not in
any shipped Codex system prompt at the ported commit: it is referenced only
from a test whose four model cases all declare
`expects_apply_patch_description: false`, and none of the eight instruction
templates in models.json carries the section. The file is upstream's own
authored description of the format and it is retired, which is a better reason
to use it than the one previously given, but the prose said Codex sends it.
The freeform description is 108 characters, not 105. The oracle runs 82 cases,
not 76. The fuzz figure is 2300 across four seeds, not 2100. Two of the named
divergences were recorded in the tests, not three, and there are now four with
the byte-order mark the provider strips. `verifyToolchainDirectory` covers ten
registered profiles, not eight. `checksums.sha256` omits itself and
manifest.json. The toolchain build's `__tests__` prune is scoped to the plugins
it copies. The build script runs from the repository root. `ctx.fs` offers
twelve methods, not three. The 0.147.0 delta includes a second behavioural
difference — the duplicate-path check does not exist at that tag — and a larger
restructure than the three items named. The profiles record three deviations
from upstream, not one.
Also stated where it was missing: the arm's lack of a read tool is Codex's
contract rather than a handicap, since Codex registers no file-read tool
either and every arm has the same bash.
Generated-by: Claude Code
A second adversarial round found that the previous round's fix traded one
divergence for another, and that twelve mutations of this tool still left the
whole suite green.
`ctx.fs.readText` is not `read_file_text`. Upstream reads bytes and calls
`String::from_utf8`, so every valid UTF-8 sequence is text; `dsh-fs-local`
adds two rules on top, and both are model-visible. It rejects a NUL in the
first 8192 bytes as binary — but NUL is valid UTF-8, and the reference patches
and deletes straight through it, verified against the binary — so making the
verify pass read delete targets took `*** Delete File:` on such a file with
it. And it decodes with `ignoreBOM` left false, which strips a leading
byte-order mark, so a patch whose context omits the mark applied here, was
refused there, and wrote the file back without the mark. Reading through
`ctx.fs.readBytes` and decoding here is exactly `String::from_utf8`; it fixes
both, and the byte-order-mark divergence is deleted rather than documented.
`*** End of File` as the first line of an update hunk was refused. Upstream
guards with `chunks.last().is_some_and(blank)`, which is false when there is
no chunk, so the marker is ignored; the binary applies the envelope.
Twelve mutations that the suite could not see, now each red-checked: five
dropped the turn signal from a provider call, which a fake that ignored the
signal could not notice; two removed or falsified the version on `fs/observed`;
two swapped a removal's announcement with the removal itself, which leaves the
event list identical and the trail a lie; one bypassed the create decider and
one renamed its event; one reduced the sandbox-denial remap to a no-op. The
fake now honours and records the signal on every call, records what the
filesystem actually held when each observation was published, records the
version `writeText` returned, and can be made to deny a write.
Twelve oracle cases added, generated from the binary: three for the read seam
and nine for shapes that separate this port from a plausible variant of it —
overlapping chunks, a backward `@@` anchor, an indented header after an add
body, a trailing mark on an End of File marker, header paths with a leading
space, a repeated `*** Move to:`, a heredoc close with trailing text, and the
End of File case above. Eleven passed unchanged, which is the point of adding
them; one was the defect.
`requireDirectFilesystem` now raises an `FsError` like every other refusal.
Generated-by: Claude Code
… meant to help
Raising `readMaxLineLength` from its shipped 2000 to the 16000 budget was
wrong, and worse than the asymmetry it was meant to close. `dsh-tool-fs`
truncates a long line and appends a `... (line truncated to N chars)` marker,
then charges the whole thing against `readMaxBytes`. Once the cap approaches
the budget the truncated line no longer fits at all: the window returns zero
lines, drops every line after it, and the footer invites the model to retry at
the offset it just used, so the rest of the file is unreachable. Measured
against the shipped functions, a 20000-character line followed by 200 short
ones delivers 201 lines at 2000 and 0 lines at 16000.
No value equalises the two budgets anyway — one counts UTF-16 code units and
the other UTF-8 bytes — so the cap goes back to the value the tool ships with
and the residual is measured instead of engineered away. At 2000 the direction
is also not the one the earlier reasoning assumed: on that same long-line file
the baseline spends its whole budget on the one line and delivers 1, while
this arm truncates it, says so, and reads on.
The guard test now asserts the invariant rather than the number, so any future
cap has to be one a truncated line can still fit under.
The measurement script had four transcription errors, all inflating the
character ratio: it dropped the baseline's 240-character truncation notice and
the fs arm's per-line marker, counted the baseline's prompt line as content,
and counted a phantom trailing line for the fs arm. It also silently ate the
first positional path when `--budget` was absent. Rather than fix the
transcription and hope, `--self-check <dir>` now lifts the real functions out
of an installed copy and asserts agreement, and the corpus figures are
reported alongside synthetic long-line cases that the repository's own sources
do not contain. Corrected: 2197 files, 1.093x lines, 1.001x characters, tails
reported as what they are — extremes among the 570 files where a budget binds.
`maxConcurrentTaskGroups` moves 43 to 42: three arms at 43 is 129 trials, and
the assertion had been written as `<= 129` to admit it.
Generated-by: Claude Code
The claim that upstream's `## apply_patch` prose is retired was too strong. Of
the eight instruction templates in `models-manager/models.json` at the ported
commit, seven carry no such section — but `gpt-5.2` carries a shorter one with
the same envelope, the same three headers and the same worked example. So the
format is still taught in prose upstream, and the vendored file is the fuller
version of that prose rather than something abandoned. The conclusion it was
supporting survives unchanged and for a cleaner reason: `gpt-5.2` gets the
prose *and* the grammar, so no shipped configuration is the one this arm is in.
Codex's handler set is much longer than the six entries named, and one of them
takes a filesystem path — `view_image`, for images. The load-bearing claim is
that no handler reads file *text*, which is what the prose now says.
The NOTICE gave the wrong cause for the duplicate-path divergence: the
recorded binary composes an envelope naming one path twice because the
standalone applier has no verification pass at either version, not because the
check is absent at rust-v0.147.0 — that is a function-tool difference.
Also: `invocation.rs:246-248` is the delete arm, not the add arm; the
`fs/edit-intent` shape citation named only one of the two declarations; the
end-of-file search has no fallback to the ordinary start position, contrary to
its comment; `__tests__/upstream/` kept a stray `.gitattributes` naming paths
one level down; and the build script's header credited `npm i` for a
non-reproducible fingerprint when it runs `npm ci` and the real cause is native
compilation.
The refusal under a confining provider is described as what it is: dead code
under `dsh-fs-local`, which never sets `sandboxMode` at any sandbox mode, not
something `danger-full-access` switches off.
Generated-by: Claude Code
…t reads it
The comment justifying the conditional export had the causation backwards. The
model's shell inherits this environment, so setting the variable in all three
arms is what makes them identical, and setting it in one arm alone is what
creates a difference the model can read with `env`. It names a directory and
starts nothing, so it is inert in the two arms whose compositions never mention
it.
The remaining environment asymmetry, `$HOME` carrying the profile name, is
recorded rather than removed: the arms need separate homes because each writes
its own composition under `$DSH_HOME/profiles`, and sharing one would have each
overwrite what the others booted from. So the experiment is not blind to its
subject, and that is now stated where the home is built.
Generated-by: Claude Code
The spec declared 42 task groups, or 126 concurrent trials, on the
strength of a 128-trial convention rather than a machine. The account's
CVM quota is 60 vCPU per zone, which caps the host at 56 vCPU and
256 GiB, and the only completed run of this suite put 24 trials on
32 vCPU / 64 GiB — 2.7 GiB per trial. 126 trials on that host is 2.0 GiB
each, close enough to a Terminal Bench build task's peak that an OOM
kill would land as an infra failure. 32 groups is 96 trials at the
density that is known to survive.
Generated-by: Claude Code
The arm named its plugin with `name: !!js process.env.MAKA_EVAL_DSH_PLUGINS
+ '/tool-apply-patch/index.mjs'`. The harness's loader evaluates that tag for
`disabled` alone and hands `name` the unevaluated node, so every trial of
that arm died at boot with `name.startsWith is not a function` before it
reached the model — on a rented host, at the first smoke task.
The row now names `./plugins/tool-apply-patch/index.mjs`, resolved against the
profile directory, and the subject plants `plugins` there as a link into the
toolchain. That keeps what the loader imports inside the fingerprinted
read-only tree, keeps the composition a plain string, and keeps the path
independent of the root the wrapper was given — which naming the mount point
outright would not. The link is planted for all three arms, on the same
reasoning that exported the variable for all three: the model's shell can read
$DSH_HOME, so planting it in one arm alone is the difference, not planting it
everywhere.
The test could not have caught this. It extended its own YAML schema to
construct `!!js <source>` as a string, which taught the assertion to accept a
form the harness cannot load. The tag is real elsewhere in these compositions —
`task: !!js ctx.headlessStartup.task` is upstream's — so the schema now
constructs it as the marker shape the harness's loader produces, and a new
assertion requires every entry name to be a string and every path-named plugin
to be one the toolchain build ships. Reverting the row fails both.
Generated-by: Claude Code
The wrapper counted the subject's stderr bytes and hashed them, then dropped
them. The relay writes `maka-subject.stderr.txt` from the wrapper's own
stderr, which is empty, so a failing arm's own account of what killed it
reached the attempt record as a byte count and a digest and nothing else.
Both failures this run has had were diagnosed by reproducing them by hand on
the host instead: a composition the harness could not load, and a subject that
exits after one model response. The second is still open.
The last 64 KiB now land in `/logs/agent/<profile>.stderr.txt`, which the
relay already collects, as a `stderr-tail` artifact beside the existing
`stderr` count. A tail because a subject that loops printing would otherwise
fill the trial's disk, and because a harness says what killed it on the way
out; the count and digest still describe the whole stream, so truncation is
visible.
Generated-by: Claude Code
Announcing it as an artifact pushed the frame past the relay's 2 KiB payload
cap, so every cell came back `external subject result transport failed`.
`/logs/agent` is collected into the trial directory regardless, so writing the
file is the whole job.
Generated-by: Claude Code
Every failure before the subject starts reached the operator as the string
`external subject setup failed`, because `safeFailure` sanitizes what the
published result frame may say and nothing recorded the error itself. The
process's own stderr is collected into the trial as
`maka-subject.stderr.txt`, which is where a subject's account of its own
failure belongs, so the error, its stack, and its cause chain go there.
Generated-by: Claude Code
This module does its work at the top level, above the function that reads the
subject's stderr, so a `const` declared beside that reader is still in its
temporal dead zone when the reader runs. Every cell died with
`ReferenceError: Cannot access 'STDERR_TAIL_BYTES' before initialization`
before the subject's first model response — reported as
`external subject setup failed`, and legible only because the commit before
this one started writing the error itself.
Generated-by: Claude Code
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(eval): compare three edit contracts on the DeepSeek Harness - #3067

Closed
Astro-Han wants to merge 33 commits into
feat/eval-deepseek-harness-subjectfrom
feat/eval-edit-contract-arms
Closed

feat(eval): compare three edit contracts on the DeepSeek Harness#3067
Astro-Han wants to merge 33 commits into
feat/eval-deepseek-harness-subjectfrom
feat/eval-edit-contract-arms

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Summary

A Terminal-Bench score is attributed to a framework, which leaves the edit contract inside the framework and unmeasured. Comparing Maka's edit/write against the harness's str_replace_editor cannot separate them either — that comparison moves the framework and the contract at once.

These three arms hold everything else and move only the contract:

armcontract
deepseek-harnessstr_replace_editor — one tool, four commands, unique-literal match, all guidance in the tool description
deepseek-harness-fsthe harness's read/write/edit — three tools, snake_case, three prompt sections of their own
deepseek-harness-apply-patchV4A apply_patch — one envelope, changes located by surrounding context

Same one-shot CLI, same toolchain and fingerprint, same dsh-fs-local provider under every editor, byte-identical package.json and cordis.yml. The variable is the whole tool family and not one dimension of it: tool count, argument names, path conventions and where the guidance lives move together, because each family ships that way. A result is attributable to the family as shipped, not to patch syntax as such.

No arm mounts dsh-fs-observation-policy. It was mounted for the fs arm alone on the reasoning that its tools' guidance mentions the policy; of the four presets in @deepseek-ai/dsh, three mount dsh-tool-fs and none mounts the policy, and its own documentation covers str_replace_editor too. Mounting it in one arm gave that arm two failure modes the others cannot have.

Three compositions in three files can drift apart while still producing numbers, so the control is asserted rather than intended. edit-contract-arms.test.ts fails when the compositions differ by any row outside their editors, when they compose in a different order, when the model, context window, reasoning effort, stream idle timeout, sandbox mode, persona or either Bash deadline drifts in one, when the arms stop sharing one toolchain identity object, when the experiment launches them with anything but a different profile argument, or when arms multiplied by concurrency leaves the 128-trial convention.

The patch arm is ours, and that is the thing to review

The harness ships no patch tool, so plugins/tool-apply-patch/ is this repository's while the other two arms run their vendor's. Fidelity to Codex is therefore itself an experimental variable, and it is asymmetric: a tool stricter than the reference costs its arm turns the reference never spends, and a safer one wins recoveries the reference never gets.

One deviation cannot be removed.apply_patch in Codex is a freeform tool — the API is sent a 108-character description plus a Lark grammar the decoder is constrained to, so a model on that path cannot emit a malformed envelope. The harness registers JSON function tools and has no grammar seam. This arm is therefore the unconstrained case, and syntax errors it can make are errors Codex's decoder would have prevented. That cost is charged to this arm and to no other, and it leads the arm's description in the README.

What is not charged to it is the absence of a read tool. Codex registers no handler that reads file text either — its handler set is long, and the only entry taking a filesystem path to read is view_image, which takes images — so an arm that reads with cat is that contract, and every arm has the same bash at the same 16000-character cap.

What the model is told is upstream's own prose for exactly that case: the ## apply_patch section of prompt_with_apply_patch_instructions.md, carried verbatim as upstream-apply-patch-instructions.md with three substitutions applied over it. A test pins the file's digest, requires each substitution to apply exactly once, and requires the built description to differ from the copy nowhere else. That file is upstream's and it is not delivered from where it sits: at the ported commit it is referenced only from a test whose four model cases all declare expects_apply_patch_description: false. Seven of the eight instruction templates in models-manager/models.json carry no ## apply_patch section and leave the format to the grammar; the eighth, gpt-5.2, carries a shorter one with the same envelope and worked example. So the format is still taught in prose upstream and this is the fuller version of it. Either way no shipped configuration is the one this arm is in — gpt-5.2 gets prose and grammar — so the choice was between upstream's own text and text written here.

Three further divergences are known and each is pinned to bytes the reference produces:

  • the tool implements the function-tool path — verify the whole envelope, then re-apply it from the raw text — where the standalone binary applies sequentially with no verification;
  • it runs NormalizeToLf, the shipped default, where PreserveLineEndings is Stage::UnderDevelopment, default_enabled: false upstream;
  • its diagnostics differ from Codex's, which is the one place this arm may be easier on a model. This one is recorded in prose only; the other two are named with their causes in the test files, which also assert that each still diverges.

A third divergence was recorded here and has since been removed rather than documented: the provider's readText strips a leading byte-order mark and rejects a NUL byte, neither of which String::from_utf8 does. Reading through readBytes and decoding in the tool matches the reference exactly.

The grammar is implemented in full. *** Delete File: and *** Move to: are the only operations ctx.fs has no primitive for, so they run against ctx.fs.processPath(target); under a provider that confines, both refuse up front rather than escape. This arm mounts danger-full-access, where the refusal never fires.

Sources and licences are in the plugins' NOTICE. An earlier revision of this branch vendored the OpenAI Agents SDK's applyDiff as the hunk applier; it is a different V4A implementation, not Codex's, and nothing from it remains.

What the adversarial review rounds changed

Two rounds of four fresh-eye passes each — the port against the binary, the harness integration, the three-arm control, and an audit of every factual claim. The second round reviewed the first round's fixes, which is what caught the two entries below that begin "the previous fix". Every finding was re-verified against upstream source or the reference binary before being acted on.

Nine port defects, all now red-checked. The matcher used JavaScript's trim where upstream uses Rust's, so a file whose first line carried a byte-order mark matched a pattern without one and was written back with the mark deleted, and a line terminated by U+0085 was refused where the reference applies it. The verify pass stated a delete target instead of reading it, applying deletes of non-UTF-8 files that the reference refuses outright. It emitted fs/observed {present} off a bare stat, telling an observation policy a file had been seen that nothing read. The duplicate-path key was the provider's realpath-derived target key rather than upstream's lexical join, so an envelope naming both a symlink and its target was refused where the binary applies both. A move destination ran the fs/write-intent waterfall and discarded the answer, turning a decider's refusal to clobber into a silent overwrite. A failed unlink and an abort between passes escaped the seam's error taxonomy.

Five of those were invisible because the test provider was weaker than dsh-fs-local: it decoded lossily, called every non-directory a file, took three parameters so the signal and sandbox policy could not be observed, and never returned a decider's answer.

The previous fix read patch targets through the wrong seam.ctx.fs.readText is not read_file_text: upstream reads bytes and calls String::from_utf8, while dsh-fs-local additionally rejects a NUL in the first 8192 bytes and strips a leading byte-order mark. NUL is valid UTF-8 and the reference patches and deletes straight through it, so making the verify pass read delete targets — itself a correct fix — took *** Delete File: on such a file with it. Reading through readBytes and decoding in the tool is exactly String::from_utf8, and it removes the byte-order-mark divergence too. Separately, *** End of File as the first line of an update hunk was refused where upstream ignores it.

Twelve more mutations the suite could not see. Five dropped the turn signal from a provider call, which a fake that ignored the signal could not notice; two removed or falsified the version on fs/observed; two swapped a removal's announcement with the removal itself, which leaves the event list identical and the trail a lie; one bypassed the create decider, one renamed its event, one made the sandbox-denial remap a no-op. The fake now honours and records the signal on every call, records what the filesystem held when each observation was published, records the version writeText returned, and can be made to deny a write.

Three uncontrolled differences between the arms, of which one fix was itself wrong. streamIdleTimeoutMs was omitted from all three profiles, leaving a five-minute ceiling on the gap between two streamed tokens behind reasoningEffort: max and a 65-minute bash deadline; it now carries upstream's own 172800000. maxConcurrentTaskGroups: 64 made 192 concurrent trials where the saturating specs here land on 128; it is now 42.

readMaxLineLength was raised from its shipped 2000 to the budget to close a cap the baseline has no counterpart to, and that was worse than the asymmetry. The tool appends a ... (line truncated to N chars) marker and then charges the whole line against readMaxBytes, so a cap near the budget makes a long line not fit at all, drops every line after it, and returns a window no offset recovers — 201 lines at 2000 becomes 0 at 16000. No value equalises the two budgets anyway, one counting UTF-16 code units and the other UTF-8 bytes, so the cap is back where the tool ships it and the guard test asserts the invariant rather than the number.

Twenty-one false claims across the two rounds, listed in the two docs commits. The largest was the provenance one, twice: first that the vendored prose is what Codex sends, then that it is retired. The read-budget script had four transcription errors of its own, all inflating the character ratio, so it no longer asks to be trusted — --self-check <dir> lifts the real renderers out of an installed copy and asserts agreement, and the figures are 1.093x lines and 1.001x characters over 2197 files.

Verification

Lint, format and typecheck clean. 47 TypeScript and 200 plugin tests pass.

Fidelity is measured against the binary rather than argued from the Rust:

  • codex-oracle.mjs runs the released codex under argv0 apply_patch over a temporary tree for each of 94 cases and records what it printed and what the tree became; codex-fixtures.test.mjs replays every case through the registered tool and needs no codex to do it.
  • __tests__/upstream/scenarios/ carries Codex's own conformance suite verbatim — the 25 scenario directories upstream publishes as "meant to be easily portable to other languages or platforms" — and upstream-scenarios.test.mjs replays them upstream's way: copy the input tree, apply the patch, ignore the exit status, require the resulting tree to match exactly. 22 match outright; the three that do not are named divergences. These are the only inputs here this repository did not choose.
  • codex-fuzz.mjs generates envelopes from random line soup, including near-miss context, and runs both. It earned its keep on this branch: it caught a regression introduced here, where rebuilding the file forward let a context-free insertion survive inside a range a later chunk replaces. 2300 envelopes across four seeds diverge on nothing.

Red-checked, not just green:

  • Appending a line to the description, renaming the tool, editing one byte of the vendored instructions, or neutering a substitution each fails the contract-surface test.
  • Removing any one of the four context-matching passes fails at least one oracle fixture; restoring String.prototype.trim in the loose passes fails the trailing-U+FEFF fixture; disabling the heredoc strip fails both heredoc fixtures; matching the heredoc close with includes instead of endsWith fails the trailing-text fixture.
  • Each of the twenty-one port defects and unseen mutations above fails the suite when reintroduced. Seventeen of them did not before these two rounds.
  • Both divergence lists assert that each entry still diverges, so an excuse cannot outlive the thing it excused.

End-to-end against the rebuilt linux/amd64 toolchain: loading the plugin from inside the built tree registers exactly one tool named apply_patch, with one required input string and a 2903-byte description carrying no mention of the shell delivery. The instructions file ships, so it is covered by checksums.sha256 and the re-pinned fingerprint.

Not run: an actual benchmark cohort. No dependency moved.

Review focus

One claim this repository does not reproduce, and which should be read as a one-time manual check rather than a standing guarantee: the three profiles were compared against the upstream examples/jsonrpc-agent composition during development — tool names, tool schemas, system prompt and message sequence on the first outbound request — and found identical.

The read budget matched at 16000 equalises the budget's number, not delivered content. scripts/measure-read-budget.mjs makes the residual checkable and --self-check makes the script itself checkable: over 2197 tracked sources the fs arm delivers 1.093x the lines and 1.001x the characters; among the 570 files where a budget binds, the extremes are 1.398x/1.168x and 0.837x/0.720x.

Two things a write-up must carry that this branch cannot fix. The fs arm receives 764 characters of tool-contributed system-prompt guidance where the other two receive none, and two of its three sections reference an fs-observation-policy no arm mounts. And the treatment is the whole tool family plus that guidance, not "the edit contract" — the arm names are shorter than the thing being varied.

experiments/.../edit-contracts.json declares repetitions: 1, so 89 tasks x 3 subjects is 267 cells. Cost, not power, chose that. McNemar at one repetition is weak for the effect sizes this is likely to see: on ~20% discordant pairs a true 5pp difference is about 2 sigma before any correction for three pairwise comparisons, so a write-up has to report discordant counts and intervals rather than three success rates.

Stacked on #2971.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes — described under Summary above
  • No

The harness ships no patch tool, and an Eval arm that gives the model
Codex's patch language needs one composed the same way its other editors
are: `inject: ['tools', 'fs']`, one `ctx.tools.register(defineTool(...))`,
and every file operation through `ctx.fs`. Reaching around that seam would
put a second filesystem authority in one arm.
Applying a single file's hunks — context matching with the fuzz ladder — is
the part that is hard to get right, so it is not written here. It is the
OpenAI Agents SDK's `applyDiff`, vendored rather than depended on:
`@openai/agents-core` unpacks to 9 MB and pulls the whole `openai` SDK into
a tree that is fingerprinted file by file, while the function itself has no
imports at all. The upstream test suite is vendored with it, which is what
turns a mechanical tsc emit from an assertion into a checked fact; its two
import lines are the only edit. Biome's formatter now skips
`packages/eval/harbor/**/vendor/**` for the reason it already skips the
licence artifacts: a rewritten vendor file is no longer the upstream file.
What is written here is the envelope parser and the tool. The grammar is
transcribed from Codex's lark definition, and the model-facing description
is Codex's own instructions text. Two deviations are deliberate and are
stated to the model rather than left to be discovered by failure:
- No `*** Delete File:` and no `*** Move to:`. `ctx.fs` has no delete or
rename primitive and neither does any other harness file tool, so in
every arm the model removes and renames through bash.
- A patch either applies completely or changes nothing. `applyDiff` is
pure, so every file's new content is computed before the first write.
The contracts this will be compared against touch one file per call and
cannot half-apply; a partly-patched tree would be a difference in
failure modes rather than in edit format.
Sources and licences are recorded in the plugins' NOTICE. The three test
files run from the repository through `test:dist`, against the same
`@deepseek-ai/dsh-tools` and `@deepseek-ai/dsh-fs` the toolchain installs —
added as exact-pinned dev dependencies, which the lockfile takes as pure
additions with no existing resolution changed.
No arm composes this tool yet.
Generated-by: Claude Code
Terminal-Bench results attribute a score to a framework, which leaves the
edit contract inside the framework and unmeasured. Comparing Maka's
`edit`/`write` against the harness's `str_replace_editor` cannot separate
them either: that comparison moves the framework and the contract at once.
So this holds everything else and moves only the contract. Three arms run
the same one-shot CLI, the same toolchain and fingerprint, the same
`dsh-fs-local` provider under every editor, and byte-identical
`package.json` and `cordis.yml`:
- `deepseek-harness` keeps `str_replace_editor` — one tool, four
commands, unique-literal match, all guidance in the tool description.
- `deepseek-harness-fs` takes the harness's own `read`/`write`/`edit` —
three tools, snake_case arguments, three prompt sections of their own.
- `deepseek-harness-apply-patch` takes the V4A tool added previously —
one envelope, changes located by surrounding context.
The variable is the whole contract, not one dimension of it. Tool count,
argument names, path conventions, where the guidance lives, and the
read-before-edit policy move together because each family ships that way.
`dsh-fs-observation-policy` is mounted for the `fs` arm alone for that
reason: its tools' own guidance text tells the model the policy is in
force, so omitting it would leave the prompt describing a rule the
deployment does not enforce, while `apply_patch` has no such concept and no
read tool to satisfy one with.
Three compositions in three files can drift apart while still producing
numbers, and the numbers would no longer be about the contract. So the
control is asserted rather than intended: the new test fails when the
compositions differ by any row outside their editors, when the model,
reasoning effort, sandbox mode, persona or either Bash deadline drifts in
one of them, when the arms stop sharing one toolchain identity object, or
when the experiment launches them with anything but a different profile
argument. The arms also share one preparer, so a change to how the harness
is launched cannot reach one and miss another, and one identity constant,
so a re-pinned fingerprint cannot be applied to two arms out of three.
The toolchain build now installs the plugin tree beside the harness's
`node_modules` — a composition copied under `$DSH_HOME` cannot resolve a
plugin by package name from there — and fails if a declared plugin did not
land. Its dependencies are unchanged: every package the new arms compose
was already in the reviewed lockfile.
Re-pinning the fingerprint needs a `linux/amd64` container build and is not
done here, so the arms are not yet runnable end to end.
Generated-by: Claude Code
The toolchain now carries the `apply_patch` plugin beside the harness's
`node_modules`, so its checksum manifest changed and the pin has to follow.
Rebuilt with the same pinned `linux/amd64` image and the same reviewed
lockfile; no dependency moved.
Verified against the rebuilt tree rather than assumed. `dsh --dump-config`
composes all three arm profiles successfully, and the only differences
between the three composed trees are the editor rows and the `cwd` the two
new arms give the filesystem backend — nothing else diverged. Loading the
plugin from inside the toolchain resolves `@deepseek-ai/dsh-tools`,
`dsh-fs`, `dsh-sandbox` and `schemastery` through the upward walk into
`lib/dsh/node_modules`, registers `apply_patch` with the expected schema,
and carries the patch instructions without the operations the tool does not
implement. `dsh-tool-fs` and `dsh-fs-observation-policy` resolve too.
Generated-by: Claude Code
@coderabbitai

coderabbitaiBot commented Aug 15, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1357f048-8ea6-474e-8887-d512c43407ac

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

The tool shipped without them, refusing both with a pointer to the shell.
The stated reason was that `ctx.fs` has no delete or rename primitive, so
reaching them through `node:fs` would put a second filesystem authority in
the one arm being measured. That reason does not survive contact with the
deployment it was written for: this arm mounts `dsh-sandbox-local` at
`danger-full-access`, no observation policy is loaded, and the model's own
bash can already remove any file in the container. The authority being
protected was not enforcing anything.
The cost was real, and it was the point of the arm. `apply_patch` exists
here to measure Codex's contract; a patch tool missing a third of its
grammar is a different contract, and a model trained on the real one pays
for the difference in wasted turns. It also broke this PR's own rule —
every other arm composes what its tool family ships, which is exactly why
the `fs` arm mounts the observation policy. Two standards in one change.
So both operations are implemented, against `ctx.fs.processPath(target)` —
the provider's own answer to where a file is for something outside it to
open, and for `dsh-fs-local` the local absolute path. `ctx.fs` stays the
only thing that resolves paths; only the two syscalls it does not offer
happen outside it. Semantics follow Codex: a rename writes the destination
and then removes the source, overwrites an existing destination rather than
refusing, and creates missing parents.
Where the original concern is real, it is enforced instead of assumed: under
a provider that confines, `processPath` would be the way around the sandbox,
so delete and rename refuse up front. `sandboxMode` is what decides, so the
refusal fires exactly when a provider actually confines.
The parser already implemented the whole grammar, so no change there. The
tool description loses its "not part of this tool" paragraph and returns to
Codex's text, now with one removal rather than three: the trailing shell
invocation example, which is false for a function tool.
Tests move from an in-memory store to a real temporary directory, since
these two operations are syscalls an in-memory store cannot observe.
Red-checked: stubbing out the removal fails five behaviour tests, and
disabling the confinement guard fails the two that cover it.
Generated-by: Claude Code
The plugin gained its delete and rename paths, so the file it contributes to
the toolchain changed and the checksum manifest with it. Rebuilt from the
same pinned image and the same lockfile; no dependency moved.
Verified against the rebuilt tree: all three arm profiles still compose
under `dsh --dump-config`, and loading the plugin from inside the toolchain
registers `apply_patch` carrying the full grammar — `*** Delete File:`,
`*** Move to:`, and both productions — with the shell-invocation example
still absent.
Generated-by: Claude Code
Independent review of this arm found five places where the tool behaved
differently from the reference it exists to measure. The asymmetry matters
more than the count: a tool stricter than Codex costs its arm turns the
reference never spends, and a tool safer than Codex wins its arm recoveries
the reference never gets. Either way the benchmark reads the difference as
an effect of the edit contract.
`*** Add File:` did not terminate the created file with a newline, because
the vendored applier joins its added lines while Codex appends `\n` after
each one. Every file this arm created differed from Codex's byte for byte,
and a test pinned the wrong result. Each added line is now terminated, and
an empty create still writes nothing, as `String::new()` does upstream.
The envelope was planned against one snapshot and written afterwards, so a
failed hunk left the tree untouched. That is the nicer property and it was
still wrong: two sections naming one path made the second fail against a
version the first had replaced, and a delete followed by an update removed
the file and then failed, leaving neither. Operations now apply in order,
each reading what the previous one wrote.
`*** Add File:` over an existing file was refused with an invented
`FS_ALREADY_EXISTS`; Codex records what it overwrote and writes. A bare
`*** Move to:` with no hunks was accepted; Codex rejects an empty chunk list
before it reads the rename. A leading newline, a trailing space on the
opening marker, and an indented envelope were all refused; Codex trims the
patch and each marker line, while comparing only `trim_end()` inside an
update body so an indented context line stays content. Content after
`*** End Patch` was silently dropped, so two envelopes in one call reported
success over operations that never ran.
Three defects in how it reached the filesystem, none of which fire under the
arm's own non-confining provider:
- `mkdir` ran on every write through `processPath`, the same way out of a
sandbox that delete and rename refuse for. A create outside a confining
workspace had its write denied and still left the directory behind.
- Delete and rename used the provider's realpath-derived target key, so
deleting a symlink destroyed the file it pointed at and left the link
dangling. They now use the lexical path, which is what Codex removes.
- Hunk failures reached the model as a bare `Invalid Context 0`, naming
neither the file nor the section, and two error codes were outside
`FsErrorCode`'s closed union.
The description's provenance note claimed one modification from upstream
where there are four, and NOTICE claimed the description dropped the delete
and move operations — true when written, false once they were implemented,
and wrong in the direction that matters for a modification notice.
84 plugin tests pass. Each divergence is pinned by a test naming the
upstream file and line it follows.
Generated-by: Claude Code
Review found two differences between the arms that were not the edit
contract, and a control test that could not have caught either.
Only the `fs` arm mounted `dsh-fs-observation-policy`, justified in the
profile by that family's guidance mentioning it. Checking how the harness
ships does not support this: of the four presets in `@deepseek-ai/dsh`,
three mount `dsh-tool-fs` and none mounts the policy, which is not even a
direct dependency; and the policy's own documentation covers the baseline's
editor too. It is substrate, not part of an editor. Mounting it in one arm
gave that arm two failure modes the others cannot have — an edit without a
prior read, and a file the model's own shell touched in between — on a
benchmark where shell and editor are used together constantly. No arm
mounts it now. The apply-patch profile's stated reason for omitting it was
also wrong, and is corrected rather than quietly dropped: that arm emits
`fs/observed` with the file's version before it writes, so the policy would
have allowed the write.
The two new arms configured `fs-local` with a `cwd` expression the baseline
did not have. It could not change anything — the provider defaults `cwd` to
`process.cwd()`, both tools pass the session's own cwd, and `DSH_CWD` is
never set — but an unreachable setting is still a difference a reviewer has
to rule out.
Read budgets are now matched. The baseline pins its editor to 16000 output
characters while `dsh-tool-fs` shipped a 51200-byte default, a 3.2x window
on the same task. How much of a file one call returns is not part of the
edit contract, so `readMaxBytes` is set to match. The remaining read-side
asymmetry, that the patch arm has no read tool at all, is contract-inherent
and stays.
The control test read each `- id:` row with the `name:` on the line below
and compared those strings, leaving every `config:` block unchecked.
Narrowing one arm's context window, halving an editor's output budget,
rewriting the shared bash description, reordering the rows, or overriding
the patch tool's own description all kept it green. It now parses the YAML
and compares whole entries, asserts composition order, and pins each arm's
editor row by value — the treatment was the one thing no assertion covered.
Verified by making each of those six edits and watching it fail.
The arm-to-directory table moves to `toolchain-verification.ts`, beside the
identity table, and the subject and the test both read it. The test used to
restate it, so an arm pointed at the wrong profile would have been checked
against the composition it was meant to run rather than the one it runs.
The toolchain fingerprint is re-pinned for the plugin changes in the parent
commit. Verified in a `linux/amd64` container: all three profiles compose,
the composed trees differ only in the editor row, and the plugin loads from
inside the toolchain and registers `apply_patch` with the full grammar.
Generated-by: Claude Code
The apply_patch arm applied one file's hunks with the OpenAI Agents SDK's
`applyDiff`, vendored on the assumption that one V4A implementation is like
another. It is not. Running the released `codex` binary over the same trees
shows four disagreements, each of which moves this arm's score: a context-free
`+` hunk lands at the `@@` anchor rather than at end of file, an updated file
keeps whatever trailing newline it had rather than gaining one, CRLF survives,
and near-miss context is refused where Codex's four matching passes accept it.
Replace it with a port of Codex's own applier and parser. The parser now reads
hunks into chunks instead of passing section bodies through, so a syntax error
in the third section is found before the first is written, and the tool no
longer needs a second project's operation shape.
Restore verify-then-write. An earlier round changed the tool to apply
operations sequentially, matching the standalone `apply_patch` binary. That
binary is not what a model calls: the function-tool handler and the intercepted
shell call both go through `try_verify_apply_patch_args`, which collects hunks
into a map keyed by resolved path, refuses a duplicate key, and computes every
change against the pre-patch tree. So an envelope naming one path twice is
refused, and a hunk whose context the model guessed at costs nothing.
Fidelity is now measured rather than argued. `codex-oracle.mjs` runs the real
binary over a temporary tree for each of 72 cases and records what it printed
and what the tree became; `codex-fixtures.test.mjs` replays every case through
the registered tool and compares. The two cases where the function tool
deliberately differs from the standalone binary are named there with what it
does instead. A fuzzer over 1800 random envelopes found no further divergence.
Also tighten the arm-control test, which had three gaps: it skipped composition
steps that override an already-composed entry, so an override of the patch
tool's description was invisible; it named the profile files to compare instead
of reading them; and it exempted the first subject's profile argument while
comparing only the argument vector, not the whole subject definition.
Toolchain fingerprint re-pinned. Test-only files move under `__tests__/` so the
build prunes them by directory rather than by filename.
Generated-by: Claude Code
`readMaxBytes: 16000` was justified as making "how much of a file one call
returns" equal across the two read-capable arms. It does not. The baseline
editor spends eight characters a line on line numbers plus a header; the fs
tool emits neither, so measured over this repository's own sources it returns
10% more lines for the same budget and 22% on the longest files.
The budget is still the right one, for a reason the note did not give: what
should be equal is what a read costs the context window, and the numbering the
baseline adds is part of its contract that the model pays tokens for either
way. Equalising delivered content would mean picking a budget from an assumed
average line length.
Record the measurement, and record the one genuine mismatch that remains — the
budgets are counted in UTF-16 code units against UTF-8 bytes, equal on ASCII
and not otherwise.
Generated-by: Claude Code
The fixtures pin the cases someone thought to write down. This searches for the
rest: it generates envelopes from random line soup, applies each with the
released binary and with the registered tool, and reports every input the two
disagree on. 2100 envelopes across seven seeds currently diverge on nothing.
Red-checked against three deliberate regressions — placing a context-free `+`
hunk at the `@@` anchor, dropping the trailing-newline normalisation, and
removing the punctuation-folding match pass. The third went unnoticed through
400 envelopes at first, because context lines drawn verbatim from the file only
ever exercise the exact-match pass; the generator now half-remembers a line the
way a model does, and catches it.
Envelopes name each path at most once, and a refusal is compared as a refusal
without comparing the tree, because those are exactly where the standalone
binary and the function tool are supposed to differ.
Generated-by: Claude Code
The function-tool handler verifies an envelope and then throws the result
away: `ApplyPatchRuntime::run` hands `req.action.patch` — the raw text the
model sent — back to `apply_patch_with_mode`, which re-parses it and applies
it sequentially against the live filesystem. The verified `changes` map is
carried for approval and telemetry only.
This tool verified and then wrote the plan it had just computed. That is not
a cheaper version of the same contract, and the reference binary shows the
difference in two cases now covered by tests: a rename onto a path a later
section also updates leaves that section failing against the renamed file,
and `*** Move to:` naming its own source writes the destination and then
unlinks it, deleting the file while reporting success. Both were previously
refused with an invented "multiple operations target" error, because the
verification map also claimed the move destination — upstream keys only on
`hunk.resolve_path()` and carries `move_path` as a value.
Also in the write path: `fs/edit-intent` yields `{version}` with no `kind`,
so passing it straight to `writeText` matched neither guard and silently
wrote unconditionally; the abort signal was not checked between operations;
and `rm` ran with `force: true`, reporting `D <path>` for a file that had
already gone.
In the applier: each fuzzy-matching pass now projects both sides once
instead of recomputing the punctuation fold per candidate position, and
replacements are rebuilt in one forward pass rather than spliced, which
threw a RangeError once a hunk added more lines than the argument limit.
The model-facing description is no longer transcribed into this file. It is
read from a byte-for-byte copy of the upstream artifact it came from, with
three named substitutions applied over it, so the claim that it differs in
exactly those places is checkable rather than asserted in a comment. Copying
the file rather than retyping it also removed a fourth difference that had
no reason to exist: upstream's typographic punctuation is now preserved.
Its recorded provenance was stale — the file it named no longer exists at
the pinned commit — and now points at the artifact that does.
One parameter description said the input was "Not JSON-wrapped". It is: the
harness registers JSON function tools and nothing else.
Generated-by: Claude Code
Upstream ships codex-rs/apply-patch/tests/fixtures/scenarios as, in its own
README's words, an end-to-end suite "meant to be easily portable to other
languages or platforms". This is the port: the 25 scenario directories are
copied verbatim from the pinned commit and replayed through the registered
tool, compared the way upstream compares them — copy `input/`, apply
`patch.txt`, ignore the exit status, and require the resulting tree to equal
`expected/` exactly.
It is the only check here with the standing to contradict the code. The
oracle fixtures run the reference binary, but over inputs this repository
chose; the fuzzer perturbs inputs this repository chose how to perturb.
These were chosen by upstream, before and independently of this tool.
22 of 25 match outright. Three do not, and each divergence is recorded with
its cause and pinned to bytes the reference binary produces:
- 015 specifies the standalone binary, which has no verification pass, so an
add preceding a failing update survives. The function tool writes nothing.
- 023 and 024 are specified under CODEX_APPLY_PATCH_PRESERVE_LINE_ENDINGS,
a feature that is `Stage::UnderDevelopment, default_enabled: false`, so a
shipped Codex and this arm both run the `NormalizeToLf` default.
Both divergence lists — here and in the oracle fixtures — now assert that
each entry still diverges, so an excuse cannot outlive the thing it excused.
Generated-by: Claude Code
The treatment in this arm is the tool name, the parameter it takes, and the
description text. None of the three had any coverage, so a build that
registered the tool under another name, dropped the parameter, or shipped a
truncated description would have run a cohort and produced numbers.
The description claim is the one that had to be argued rather than shown:
"three differences from upstream and no others" was a comment. It is now
checked at both ends. The vendored instructions file is pinned by digest,
with the `git show ... | sed -n` that reproduces it from the source recorded
beside the constant, and the built text is recomputed here by a different
mechanism than the code uses and required to match. Each adaptation must
apply exactly once, and no mention of the shell delivery may survive.
Red-checked against four sabotages — appending a line to the description,
renaming the tool, editing one byte of the vendored text, and neutering an
adaptation. Each is caught.
Generated-by: Claude Code
Two differences at the outermost layer of the patch, both pinned by new
oracle fixtures generated from the reference binary.
`parse_patch` selects its mode from `PARSE_IN_STRICT_MODE`, which is `false`:
upstream gave up on gating leniency by model, so every call site including
the function tool unwraps a heredoc-wrapped envelope. A model asked to send
a patch may well send the `<<'EOF' ... EOF` form it was shown, and Codex
applies it where this refused it. The strict boundaries are still tried
first, so an envelope whose own first line reads `<<EOF` is never stripped.
Rust's `str::trim` cuts the Unicode `White_Space` property; JavaScript's also
cuts U+FEFF and does not cut U+0085. Only U+FEFF can realistically occur — a
byte-order mark carried into a JSON string argument — and it made this tool
accept a patch Codex refuses, which is the wrong direction for an arm being
compared against the contract it imitates.
Red-checked: restoring `String.prototype.trim` fails the byte-order-mark
fixture, and disabling the heredoc strip fails both heredoc fixtures.
Generated-by: Claude Code
The comment justifying `readMaxBytes: 16000` said `str_replace_editor`
spends eight characters a line on line numbers and a header while "this tool
emits neither". Both tools number their lines, and this one wraps the result
in a `<path>/<type>/<content>` envelope with a footer.
The setting still holds something equal, but not what was claimed. The
baseline truncates the fully rendered string; this tool charges the budget
against raw line bytes plus separators and adds the gutter, the envelope and
the footer afterwards, outside it. Measured across this repository's 1480
TS/JS/Python sources at 16000, the arm delivers 1.105x the lines and emits
1.027x the characters, worst file 1.34x and 1.19x — a residual in this arm's
favour, now recorded with the mechanism that produces it.
The three profiles also claimed to have been "verified request-for-request"
against the upstream composition. That was a manual comparison made once
during development; nothing here reproduces it, and the headers now say so.
`MAKA_EVAL_DSH_PLUGINS` was exported into all three arms and read by one, so
the two controls carried an environment difference for a mechanism they do
not use. It is now set from the copied composition itself, which keeps the
condition and the file that depends on it from drifting apart.
Generated-by: Claude Code
`apply_patch` in Codex is a freeform tool: the API is sent a short description
and a Lark grammar the decoder is constrained to, so a model on that path
cannot emit a malformed envelope. The harness registers JSON function tools
and has no grammar seam, so this arm is the unconstrained case. That is not a
detail of the port — it is a cost charged to this arm and to no other, and it
now leads the arm's description instead of being absent from it.
Three further divergences are named with their causes: the function-tool path
against the standalone binary's sequential apply, `NormalizeToLf` against a
preserve-line-endings feature that ships disabled, and diagnostics more
specific than Codex's single `Invalid patch:` line, which is the one place
this arm may be easier on a model than the reference.
The README also described a verify-then-write-the-plan design the tool no
longer has, quoted the old fixture count, and repeated the read-budget
mechanism corrected in the previous commit.
The NOTICE opened by saying nothing was carried verbatim as a file; two
things now are. Its description provenance named a file that no longer exists
at the ported commit and a fourth modification that was an artefact of
retyping rather than copying, its fixture path was stale, its account of how
`PreserveLineEndings` is selected covered only the standalone binary, and its
list of ported functions omitted the ones that decide apply order.
The arm-control test read a composition step as an insertion whenever it had
an `insert` key, so `{insert, remove}` would have passed with the removal
unread, and it filtered directories out of each profile's file list rather
than refusing them. It also now names what it does not cover.
Generated-by: Claude Code
`dsh-fs-local` creates a write's parent itself, in `writeFileAtomic`, and it
is the provider every arm mounts. The `ensureParent` here ran `mkdir` on a
`processPath` — the same way around a confining sandbox that delete and
rename refuse for — and existed only because the test harness's provider was
weaker than the real one and failed with ENOENT without it.
That is the more useful half of this change: a fake that cannot do what the
real thing does will not merely miss bugs, it will make production code grow
to cover for it. The fake now creates the parent the way `writeFileAtomic`
does, and the tool has no call that creates a directory at all.
The test that pinned the old behaviour was passing for the wrong reason — it
asserted the tool left no directory behind, and what actually happened was
the fake's write failing before anything could be created.
Generated-by: Claude Code
…sses
`deriveNewContents` reported a context miss as a plain `Error`, and the
caller rewrapped anything it threw as `FS_EDIT_NOT_FOUND` — so a bug in the
applier would have reached the model as "the lines you named are not in the
file", which is advice to rewrite a correct patch. The context miss is now
its own type and the only thing translated; a comment already claimed this
and the code did not.
Two oracle fixtures pin the order of the four matching passes against each
other. The existing cases pinned the exact pass ahead of the loose ones;
between the loose ones, removing a pass does not fail a match, it silently
moves it to a different line, and nothing caught that. Each of the four now
fails at least one fixture when removed.
The line-ending note in codex-apply.mjs described only the standalone
binary's environment variable, not the feature flag the function tool reads.
The toolchain is rebuilt and its fingerprint re-pinned: the model-facing
instructions now ship as a file, so they are covered by `checksums.sha256`
rather than living only inside a JavaScript template. Verified inside the
pinned linux/amd64 container that the built tree registers one tool named
`apply_patch` with one required `input` string and a 2903-byte description
carrying no mention of the shell delivery.
Generated-by: Claude Code
Differential fuzzing against the reference binary found a regression this
branch introduced. `applyReplacements` was rewritten to build the file
forward in one pass, on the reasoning that the replacements are sorted and
disjoint. They are sorted; they are not always disjoint.
`computeReplacements` places a context-free insertion at the end of the file
without consulting the search position — deliberately, because that is what
`insertion_idx` does upstream — so it can land inside a range a later chunk
replaces. Applying descending, the replacement swallows it; applying forward,
the two are treated as separate edits and the inserted line survives into a
file the reference never produces.
Restored to splicing in descending order, with the replacement segment going
in in bounded batches so a hunk larger than the engine's argument limit still
does not throw. The case is now an oracle fixture, so it is caught without a
`codex` on PATH; red-checked by reinstating the forward rebuild.
2300 further envelopes across four seeds diverge on nothing. Toolchain
rebuilt and re-pinned.
Generated-by: Claude Code
An adversarial review pass found nine behaviours that were wrong or untested
in the V4A tool, each verified against the reference binary or upstream source
before being changed. All nine are now covered: sabotaging any one of them
fails the suite, where before five of them left it green.
The matcher used JavaScript's `trim` where upstream uses Rust's. The two
disagree on U+FEFF and U+0085, so a file whose first line carried a byte-order
mark matched a pattern without one and was written back with the mark deleted,
and a line terminated by NEL was refused where the reference applies it. The
charset the parser already spelled out moves to rust-trim.mjs and both use it.
The verify pass now does what `try_verify_apply_patch_args` does: nothing at
all for `*** Add File:`, and a read for `*** Delete File:` as well as for an
update. Statting a delete target instead applied a delete the reference
refuses, since `read_file_text` maps invalid UTF-8 to `InvalidData` and fails
the whole envelope. It also stops emitting `fs/observed {present}` off a bare
stat, which told an observation policy a file had been seen that nothing read.
The duplicate-path key is lexical, as upstream's `PathUri::join` is, not the
provider's realpath-derived target key. An envelope naming both a symlink and
its target was refused here; the binary applies both sections in order.
A move destination no longer runs the `fs/write-intent` waterfall and discards
the answer, which turned a decider's refusal to clobber into a silent
overwrite. A failed unlink and an abort between passes now arrive as `FsError`
with a routable code and the path the model wrote, not a raw errno and a
resolved host path.
The test provider was weaker than `dsh-fs-local` in four ways that made the
above invisible: it decoded lossily where the real one raises `FS_NOT_TEXT`,
called every non-directory a file, took three parameters so the signal and
sandbox policy could not be observed, and never returned a decider's answer.
Two oracle cases are added for the trim charset and one for a trailing U+FEFF,
which is what separates the three loose matching passes from each other; the
BOM-in-content case is recorded as a divergence with its true cause, which is
that the shared provider strips a leading mark before this tool sees the file.
Generated-by: Claude Code
The import landed the 25 scenario directories, their README and their
.gitattributes twice: once at `__tests__/upstream/` and once, byte-identically,
at `__tests__/upstream/scenarios/`. Only the nested copy is read.
Generated-by: Claude Code
…arms
The three arms are meant to differ only in the edit contract. Three settings
outside that contract were not held.
`readMaxLineLength` was left at its shipped 2000 while only `readMaxBytes` was
matched to the baseline. That is a cap the baseline has no counterpart to: the
fs arm could never see the 2001st character of any line, at any offset, while
`str_replace_editor` truncates once over the whole rendered view and can spend
its entire budget on one line. A minified bundle or a single-line payload is
where that lands, and it lands against one arm only. It is raised to the
budget, where it can no longer bind before `readMaxBytes` does.
`streamIdleTimeoutMs` was omitted from all three profiles, leaving the
adapter's 300000 ms default in force — five minutes between two streamed
tokens, behind `reasoningEffort: max` and a 65-minute bash deadline. Upstream's
own jsonrpc-agent composition sets 172800000, which is what the arms now carry;
a turn killed by that timeout would have been scored as a failure of whichever
contract was running.
`maxConcurrentTaskGroups` was 64. A group holds one cell per subject and starts
them together, so three arms made 192 concurrent trials where every other spec
here lands on 128. That does not bias one arm against another, but it puts this
run's `deepseek-harness` score under a load no single-arm run shares. It is now
43, and the test asserts the arithmetic rather than the number.
The read-budget residual quoted in the profile and the README had no
reproducible source and reported only the tail that favoured the fs arm.
`scripts/measure-read-budget.mjs` reimplements both renderers from the shipped
code and reports both tails; the figures are replaced with what it prints.
Generated-by: Claude Code
A claims audit checked every factual assertion in the README, the NOTICE and
the profiles against upstream source at the ported commit and against the
shipped harness packages. Fourteen did not hold.
The largest is provenance. `prompt_with_apply_patch_instructions.md` is not in
any shipped Codex system prompt at the ported commit: it is referenced only
from a test whose four model cases all declare
`expects_apply_patch_description: false`, and none of the eight instruction
templates in models.json carries the section. The file is upstream's own
authored description of the format and it is retired, which is a better reason
to use it than the one previously given, but the prose said Codex sends it.
The freeform description is 108 characters, not 105. The oracle runs 82 cases,
not 76. The fuzz figure is 2300 across four seeds, not 2100. Two of the named
divergences were recorded in the tests, not three, and there are now four with
the byte-order mark the provider strips. `verifyToolchainDirectory` covers ten
registered profiles, not eight. `checksums.sha256` omits itself and
manifest.json. The toolchain build's `__tests__` prune is scoped to the plugins
it copies. The build script runs from the repository root. `ctx.fs` offers
twelve methods, not three. The 0.147.0 delta includes a second behavioural
difference — the duplicate-path check does not exist at that tag — and a larger
restructure than the three items named. The profiles record three deviations
from upstream, not one.
Also stated where it was missing: the arm's lack of a read tool is Codex's
contract rather than a handicap, since Codex registers no file-read tool
either and every arm has the same bash.
Generated-by: Claude Code
A second adversarial round found that the previous round's fix traded one
divergence for another, and that twelve mutations of this tool still left the
whole suite green.
`ctx.fs.readText` is not `read_file_text`. Upstream reads bytes and calls
`String::from_utf8`, so every valid UTF-8 sequence is text; `dsh-fs-local`
adds two rules on top, and both are model-visible. It rejects a NUL in the
first 8192 bytes as binary — but NUL is valid UTF-8, and the reference patches
and deletes straight through it, verified against the binary — so making the
verify pass read delete targets took `*** Delete File:` on such a file with
it. And it decodes with `ignoreBOM` left false, which strips a leading
byte-order mark, so a patch whose context omits the mark applied here, was
refused there, and wrote the file back without the mark. Reading through
`ctx.fs.readBytes` and decoding here is exactly `String::from_utf8`; it fixes
both, and the byte-order-mark divergence is deleted rather than documented.
`*** End of File` as the first line of an update hunk was refused. Upstream
guards with `chunks.last().is_some_and(blank)`, which is false when there is
no chunk, so the marker is ignored; the binary applies the envelope.
Twelve mutations that the suite could not see, now each red-checked: five
dropped the turn signal from a provider call, which a fake that ignored the
signal could not notice; two removed or falsified the version on `fs/observed`;
two swapped a removal's announcement with the removal itself, which leaves the
event list identical and the trail a lie; one bypassed the create decider and
one renamed its event; one reduced the sandbox-denial remap to a no-op. The
fake now honours and records the signal on every call, records what the
filesystem actually held when each observation was published, records the
version `writeText` returned, and can be made to deny a write.
Twelve oracle cases added, generated from the binary: three for the read seam
and nine for shapes that separate this port from a plausible variant of it —
overlapping chunks, a backward `@@` anchor, an indented header after an add
body, a trailing mark on an End of File marker, header paths with a leading
space, a repeated `*** Move to:`, a heredoc close with trailing text, and the
End of File case above. Eleven passed unchanged, which is the point of adding
them; one was the defect.
`requireDirectFilesystem` now raises an `FsError` like every other refusal.
Generated-by: Claude Code
… meant to help
Raising `readMaxLineLength` from its shipped 2000 to the 16000 budget was
wrong, and worse than the asymmetry it was meant to close. `dsh-tool-fs`
truncates a long line and appends a `... (line truncated to N chars)` marker,
then charges the whole thing against `readMaxBytes`. Once the cap approaches
the budget the truncated line no longer fits at all: the window returns zero
lines, drops every line after it, and the footer invites the model to retry at
the offset it just used, so the rest of the file is unreachable. Measured
against the shipped functions, a 20000-character line followed by 200 short
ones delivers 201 lines at 2000 and 0 lines at 16000.
No value equalises the two budgets anyway — one counts UTF-16 code units and
the other UTF-8 bytes — so the cap goes back to the value the tool ships with
and the residual is measured instead of engineered away. At 2000 the direction
is also not the one the earlier reasoning assumed: on that same long-line file
the baseline spends its whole budget on the one line and delivers 1, while
this arm truncates it, says so, and reads on.
The guard test now asserts the invariant rather than the number, so any future
cap has to be one a truncated line can still fit under.
The measurement script had four transcription errors, all inflating the
character ratio: it dropped the baseline's 240-character truncation notice and
the fs arm's per-line marker, counted the baseline's prompt line as content,
and counted a phantom trailing line for the fs arm. It also silently ate the
first positional path when `--budget` was absent. Rather than fix the
transcription and hope, `--self-check <dir>` now lifts the real functions out
of an installed copy and asserts agreement, and the corpus figures are
reported alongside synthetic long-line cases that the repository's own sources
do not contain. Corrected: 2197 files, 1.093x lines, 1.001x characters, tails
reported as what they are — extremes among the 570 files where a budget binds.
`maxConcurrentTaskGroups` moves 43 to 42: three arms at 43 is 129 trials, and
the assertion had been written as `<= 129` to admit it.
Generated-by: Claude Code
The claim that upstream's `## apply_patch` prose is retired was too strong. Of
the eight instruction templates in `models-manager/models.json` at the ported
commit, seven carry no such section — but `gpt-5.2` carries a shorter one with
the same envelope, the same three headers and the same worked example. So the
format is still taught in prose upstream, and the vendored file is the fuller
version of that prose rather than something abandoned. The conclusion it was
supporting survives unchanged and for a cleaner reason: `gpt-5.2` gets the
prose *and* the grammar, so no shipped configuration is the one this arm is in.
Codex's handler set is much longer than the six entries named, and one of them
takes a filesystem path — `view_image`, for images. The load-bearing claim is
that no handler reads file *text*, which is what the prose now says.
The NOTICE gave the wrong cause for the duplicate-path divergence: the
recorded binary composes an envelope naming one path twice because the
standalone applier has no verification pass at either version, not because the
check is absent at rust-v0.147.0 — that is a function-tool difference.
Also: `invocation.rs:246-248` is the delete arm, not the add arm; the
`fs/edit-intent` shape citation named only one of the two declarations; the
end-of-file search has no fallback to the ordinary start position, contrary to
its comment; `__tests__/upstream/` kept a stray `.gitattributes` naming paths
one level down; and the build script's header credited `npm i` for a
non-reproducible fingerprint when it runs `npm ci` and the real cause is native
compilation.
The refusal under a confining provider is described as what it is: dead code
under `dsh-fs-local`, which never sets `sandboxMode` at any sandbox mode, not
something `danger-full-access` switches off.
Generated-by: Claude Code
…t reads it
The comment justifying the conditional export had the causation backwards. The
model's shell inherits this environment, so setting the variable in all three
arms is what makes them identical, and setting it in one arm alone is what
creates a difference the model can read with `env`. It names a directory and
starts nothing, so it is inert in the two arms whose compositions never mention
it.
The remaining environment asymmetry, `$HOME` carrying the profile name, is
recorded rather than removed: the arms need separate homes because each writes
its own composition under `$DSH_HOME/profiles`, and sharing one would have each
overwrite what the others booted from. So the experiment is not blind to its
subject, and that is now stated where the home is built.
Generated-by: Claude Code
The spec declared 42 task groups, or 126 concurrent trials, on the
strength of a 128-trial convention rather than a machine. The account's
CVM quota is 60 vCPU per zone, which caps the host at 56 vCPU and
256 GiB, and the only completed run of this suite put 24 trials on
32 vCPU / 64 GiB — 2.7 GiB per trial. 126 trials on that host is 2.0 GiB
each, close enough to a Terminal Bench build task's peak that an OOM
kill would land as an infra failure. 32 groups is 96 trials at the
density that is known to survive.
Generated-by: Claude Code
The arm named its plugin with `name: !!js process.env.MAKA_EVAL_DSH_PLUGINS
+ '/tool-apply-patch/index.mjs'`. The harness's loader evaluates that tag for
`disabled` alone and hands `name` the unevaluated node, so every trial of
that arm died at boot with `name.startsWith is not a function` before it
reached the model — on a rented host, at the first smoke task.
The row now names `./plugins/tool-apply-patch/index.mjs`, resolved against the
profile directory, and the subject plants `plugins` there as a link into the
toolchain. That keeps what the loader imports inside the fingerprinted
read-only tree, keeps the composition a plain string, and keeps the path
independent of the root the wrapper was given — which naming the mount point
outright would not. The link is planted for all three arms, on the same
reasoning that exported the variable for all three: the model's shell can read
$DSH_HOME, so planting it in one arm alone is the difference, not planting it
everywhere.
The test could not have caught this. It extended its own YAML schema to
construct `!!js <source>` as a string, which taught the assertion to accept a
form the harness cannot load. The tag is real elsewhere in these compositions —
`task: !!js ctx.headlessStartup.task` is upstream's — so the schema now
constructs it as the marker shape the harness's loader produces, and a new
assertion requires every entry name to be a string and every path-named plugin
to be one the toolchain build ships. Reverting the row fails both.
Generated-by: Claude Code
The wrapper counted the subject's stderr bytes and hashed them, then dropped
them. The relay writes `maka-subject.stderr.txt` from the wrapper's own
stderr, which is empty, so a failing arm's own account of what killed it
reached the attempt record as a byte count and a digest and nothing else.
Both failures this run has had were diagnosed by reproducing them by hand on
the host instead: a composition the harness could not load, and a subject that
exits after one model response. The second is still open.
The last 64 KiB now land in `/logs/agent/<profile>.stderr.txt`, which the
relay already collects, as a `stderr-tail` artifact beside the existing
`stderr` count. A tail because a subject that loops printing would otherwise
fill the trial's disk, and because a harness says what killed it on the way
out; the count and digest still describe the whole stream, so truncation is
visible.
Generated-by: Claude Code
Announcing it as an artifact pushed the frame past the relay's 2 KiB payload
cap, so every cell came back `external subject result transport failed`.
`/logs/agent` is collected into the trial directory regardless, so writing the
file is the whole job.
Generated-by: Claude Code
Every failure before the subject starts reached the operator as the string
`external subject setup failed`, because `safeFailure` sanitizes what the
published result frame may say and nothing recorded the error itself. The
process's own stderr is collected into the trial as
`maka-subject.stderr.txt`, which is where a subject's account of its own
failure belongs, so the error, its stack, and its cause chain go there.
Generated-by: Claude Code
This module does its work at the top level, above the function that reads the
subject's stderr, so a `const` declared beside that reader is still in its
temporal dead zone when the reader runs. Every cell died with
`ReferenceError: Cannot access 'STDERR_TAIL_BYTES' before initialization`
before the subject's first model response — reported as
`external subject setup failed`, and legible only because the commit before
this one started writing the error itself.
Generated-by: Claude Code
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(eval): compare three edit contracts on the DeepSeek Harness - #3067

Closed
Astro-Han wants to merge 33 commits into
feat/eval-deepseek-harness-subjectfrom
feat/eval-edit-contract-arms
Closed

feat(eval): compare three edit contracts on the DeepSeek Harness#3067
Astro-Han wants to merge 33 commits into
feat/eval-deepseek-harness-subjectfrom
feat/eval-edit-contract-arms

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Summary

A Terminal-Bench score is attributed to a framework, which leaves the edit contract inside the framework and unmeasured. Comparing Maka's edit/write against the harness's str_replace_editor cannot separate them either — that comparison moves the framework and the contract at once.

These three arms hold everything else and move only the contract:

armcontract
deepseek-harnessstr_replace_editor — one tool, four commands, unique-literal match, all guidance in the tool description
deepseek-harness-fsthe harness's read/write/edit — three tools, snake_case, three prompt sections of their own
deepseek-harness-apply-patchV4A apply_patch — one envelope, changes located by surrounding context

Same one-shot CLI, same toolchain and fingerprint, same dsh-fs-local provider under every editor, byte-identical package.json and cordis.yml. The variable is the whole tool family and not one dimension of it: tool count, argument names, path conventions and where the guidance lives move together, because each family ships that way. A result is attributable to the family as shipped, not to patch syntax as such.

No arm mounts dsh-fs-observation-policy. It was mounted for the fs arm alone on the reasoning that its tools' guidance mentions the policy; of the four presets in @deepseek-ai/dsh, three mount dsh-tool-fs and none mounts the policy, and its own documentation covers str_replace_editor too. Mounting it in one arm gave that arm two failure modes the others cannot have.

Three compositions in three files can drift apart while still producing numbers, so the control is asserted rather than intended. edit-contract-arms.test.ts fails when the compositions differ by any row outside their editors, when they compose in a different order, when the model, context window, reasoning effort, stream idle timeout, sandbox mode, persona or either Bash deadline drifts in one, when the arms stop sharing one toolchain identity object, when the experiment launches them with anything but a different profile argument, or when arms multiplied by concurrency leaves the 128-trial convention.

The patch arm is ours, and that is the thing to review

The harness ships no patch tool, so plugins/tool-apply-patch/ is this repository's while the other two arms run their vendor's. Fidelity to Codex is therefore itself an experimental variable, and it is asymmetric: a tool stricter than the reference costs its arm turns the reference never spends, and a safer one wins recoveries the reference never gets.

One deviation cannot be removed.apply_patch in Codex is a freeform tool — the API is sent a 108-character description plus a Lark grammar the decoder is constrained to, so a model on that path cannot emit a malformed envelope. The harness registers JSON function tools and has no grammar seam. This arm is therefore the unconstrained case, and syntax errors it can make are errors Codex's decoder would have prevented. That cost is charged to this arm and to no other, and it leads the arm's description in the README.

What is not charged to it is the absence of a read tool. Codex registers no handler that reads file text either — its handler set is long, and the only entry taking a filesystem path to read is view_image, which takes images — so an arm that reads with cat is that contract, and every arm has the same bash at the same 16000-character cap.

What the model is told is upstream's own prose for exactly that case: the ## apply_patch section of prompt_with_apply_patch_instructions.md, carried verbatim as upstream-apply-patch-instructions.md with three substitutions applied over it. A test pins the file's digest, requires each substitution to apply exactly once, and requires the built description to differ from the copy nowhere else. That file is upstream's and it is not delivered from where it sits: at the ported commit it is referenced only from a test whose four model cases all declare expects_apply_patch_description: false. Seven of the eight instruction templates in models-manager/models.json carry no ## apply_patch section and leave the format to the grammar; the eighth, gpt-5.2, carries a shorter one with the same envelope and worked example. So the format is still taught in prose upstream and this is the fuller version of it. Either way no shipped configuration is the one this arm is in — gpt-5.2 gets prose and grammar — so the choice was between upstream's own text and text written here.

Three further divergences are known and each is pinned to bytes the reference produces:

  • the tool implements the function-tool path — verify the whole envelope, then re-apply it from the raw text — where the standalone binary applies sequentially with no verification;
  • it runs NormalizeToLf, the shipped default, where PreserveLineEndings is Stage::UnderDevelopment, default_enabled: false upstream;
  • its diagnostics differ from Codex's, which is the one place this arm may be easier on a model. This one is recorded in prose only; the other two are named with their causes in the test files, which also assert that each still diverges.

A third divergence was recorded here and has since been removed rather than documented: the provider's readText strips a leading byte-order mark and rejects a NUL byte, neither of which String::from_utf8 does. Reading through readBytes and decoding in the tool matches the reference exactly.

The grammar is implemented in full. *** Delete File: and *** Move to: are the only operations ctx.fs has no primitive for, so they run against ctx.fs.processPath(target); under a provider that confines, both refuse up front rather than escape. This arm mounts danger-full-access, where the refusal never fires.

Sources and licences are in the plugins' NOTICE. An earlier revision of this branch vendored the OpenAI Agents SDK's applyDiff as the hunk applier; it is a different V4A implementation, not Codex's, and nothing from it remains.

What the adversarial review rounds changed

Two rounds of four fresh-eye passes each — the port against the binary, the harness integration, the three-arm control, and an audit of every factual claim. The second round reviewed the first round's fixes, which is what caught the two entries below that begin "the previous fix". Every finding was re-verified against upstream source or the reference binary before being acted on.

Nine port defects, all now red-checked. The matcher used JavaScript's trim where upstream uses Rust's, so a file whose first line carried a byte-order mark matched a pattern without one and was written back with the mark deleted, and a line terminated by U+0085 was refused where the reference applies it. The verify pass stated a delete target instead of reading it, applying deletes of non-UTF-8 files that the reference refuses outright. It emitted fs/observed {present} off a bare stat, telling an observation policy a file had been seen that nothing read. The duplicate-path key was the provider's realpath-derived target key rather than upstream's lexical join, so an envelope naming both a symlink and its target was refused where the binary applies both. A move destination ran the fs/write-intent waterfall and discarded the answer, turning a decider's refusal to clobber into a silent overwrite. A failed unlink and an abort between passes escaped the seam's error taxonomy.

Five of those were invisible because the test provider was weaker than dsh-fs-local: it decoded lossily, called every non-directory a file, took three parameters so the signal and sandbox policy could not be observed, and never returned a decider's answer.

The previous fix read patch targets through the wrong seam.ctx.fs.readText is not read_file_text: upstream reads bytes and calls String::from_utf8, while dsh-fs-local additionally rejects a NUL in the first 8192 bytes and strips a leading byte-order mark. NUL is valid UTF-8 and the reference patches and deletes straight through it, so making the verify pass read delete targets — itself a correct fix — took *** Delete File: on such a file with it. Reading through readBytes and decoding in the tool is exactly String::from_utf8, and it removes the byte-order-mark divergence too. Separately, *** End of File as the first line of an update hunk was refused where upstream ignores it.

Twelve more mutations the suite could not see. Five dropped the turn signal from a provider call, which a fake that ignored the signal could not notice; two removed or falsified the version on fs/observed; two swapped a removal's announcement with the removal itself, which leaves the event list identical and the trail a lie; one bypassed the create decider, one renamed its event, one made the sandbox-denial remap a no-op. The fake now honours and records the signal on every call, records what the filesystem held when each observation was published, records the version writeText returned, and can be made to deny a write.

Three uncontrolled differences between the arms, of which one fix was itself wrong. streamIdleTimeoutMs was omitted from all three profiles, leaving a five-minute ceiling on the gap between two streamed tokens behind reasoningEffort: max and a 65-minute bash deadline; it now carries upstream's own 172800000. maxConcurrentTaskGroups: 64 made 192 concurrent trials where the saturating specs here land on 128; it is now 42.

readMaxLineLength was raised from its shipped 2000 to the budget to close a cap the baseline has no counterpart to, and that was worse than the asymmetry. The tool appends a ... (line truncated to N chars) marker and then charges the whole line against readMaxBytes, so a cap near the budget makes a long line not fit at all, drops every line after it, and returns a window no offset recovers — 201 lines at 2000 becomes 0 at 16000. No value equalises the two budgets anyway, one counting UTF-16 code units and the other UTF-8 bytes, so the cap is back where the tool ships it and the guard test asserts the invariant rather than the number.

Twenty-one false claims across the two rounds, listed in the two docs commits. The largest was the provenance one, twice: first that the vendored prose is what Codex sends, then that it is retired. The read-budget script had four transcription errors of its own, all inflating the character ratio, so it no longer asks to be trusted — --self-check <dir> lifts the real renderers out of an installed copy and asserts agreement, and the figures are 1.093x lines and 1.001x characters over 2197 files.

Verification

Lint, format and typecheck clean. 47 TypeScript and 200 plugin tests pass.

Fidelity is measured against the binary rather than argued from the Rust:

  • codex-oracle.mjs runs the released codex under argv0 apply_patch over a temporary tree for each of 94 cases and records what it printed and what the tree became; codex-fixtures.test.mjs replays every case through the registered tool and needs no codex to do it.
  • __tests__/upstream/scenarios/ carries Codex's own conformance suite verbatim — the 25 scenario directories upstream publishes as "meant to be easily portable to other languages or platforms" — and upstream-scenarios.test.mjs replays them upstream's way: copy the input tree, apply the patch, ignore the exit status, require the resulting tree to match exactly. 22 match outright; the three that do not are named divergences. These are the only inputs here this repository did not choose.
  • codex-fuzz.mjs generates envelopes from random line soup, including near-miss context, and runs both. It earned its keep on this branch: it caught a regression introduced here, where rebuilding the file forward let a context-free insertion survive inside a range a later chunk replaces. 2300 envelopes across four seeds diverge on nothing.

Red-checked, not just green:

  • Appending a line to the description, renaming the tool, editing one byte of the vendored instructions, or neutering a substitution each fails the contract-surface test.
  • Removing any one of the four context-matching passes fails at least one oracle fixture; restoring String.prototype.trim in the loose passes fails the trailing-U+FEFF fixture; disabling the heredoc strip fails both heredoc fixtures; matching the heredoc close with includes instead of endsWith fails the trailing-text fixture.
  • Each of the twenty-one port defects and unseen mutations above fails the suite when reintroduced. Seventeen of them did not before these two rounds.
  • Both divergence lists assert that each entry still diverges, so an excuse cannot outlive the thing it excused.

End-to-end against the rebuilt linux/amd64 toolchain: loading the plugin from inside the built tree registers exactly one tool named apply_patch, with one required input string and a 2903-byte description carrying no mention of the shell delivery. The instructions file ships, so it is covered by checksums.sha256 and the re-pinned fingerprint.

Not run: an actual benchmark cohort. No dependency moved.

Review focus

One claim this repository does not reproduce, and which should be read as a one-time manual check rather than a standing guarantee: the three profiles were compared against the upstream examples/jsonrpc-agent composition during development — tool names, tool schemas, system prompt and message sequence on the first outbound request — and found identical.

The read budget matched at 16000 equalises the budget's number, not delivered content. scripts/measure-read-budget.mjs makes the residual checkable and --self-check makes the script itself checkable: over 2197 tracked sources the fs arm delivers 1.093x the lines and 1.001x the characters; among the 570 files where a budget binds, the extremes are 1.398x/1.168x and 0.837x/0.720x.

Two things a write-up must carry that this branch cannot fix. The fs arm receives 764 characters of tool-contributed system-prompt guidance where the other two receive none, and two of its three sections reference an fs-observation-policy no arm mounts. And the treatment is the whole tool family plus that guidance, not "the edit contract" — the arm names are shorter than the thing being varied.

experiments/.../edit-contracts.json declares repetitions: 1, so 89 tasks x 3 subjects is 267 cells. Cost, not power, chose that. McNemar at one repetition is weak for the effect sizes this is likely to see: on ~20% discordant pairs a true 5pp difference is about 2 sigma before any correction for three pairwise comparisons, so a write-up has to report discordant counts and intervals rather than three success rates.

Stacked on #2971.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes — described under Summary above
  • No

The harness ships no patch tool, and an Eval arm that gives the model
Codex's patch language needs one composed the same way its other editors
are: `inject: ['tools', 'fs']`, one `ctx.tools.register(defineTool(...))`,
and every file operation through `ctx.fs`. Reaching around that seam would
put a second filesystem authority in one arm.
Applying a single file's hunks — context matching with the fuzz ladder — is
the part that is hard to get right, so it is not written here. It is the
OpenAI Agents SDK's `applyDiff`, vendored rather than depended on:
`@openai/agents-core` unpacks to 9 MB and pulls the whole `openai` SDK into
a tree that is fingerprinted file by file, while the function itself has no
imports at all. The upstream test suite is vendored with it, which is what
turns a mechanical tsc emit from an assertion into a checked fact; its two
import lines are the only edit. Biome's formatter now skips
`packages/eval/harbor/**/vendor/**` for the reason it already skips the
licence artifacts: a rewritten vendor file is no longer the upstream file.
What is written here is the envelope parser and the tool. The grammar is
transcribed from Codex's lark definition, and the model-facing description
is Codex's own instructions text. Two deviations are deliberate and are
stated to the model rather than left to be discovered by failure:
- No `*** Delete File:` and no `*** Move to:`. `ctx.fs` has no delete or
rename primitive and neither does any other harness file tool, so in
every arm the model removes and renames through bash.
- A patch either applies completely or changes nothing. `applyDiff` is
pure, so every file's new content is computed before the first write.
The contracts this will be compared against touch one file per call and
cannot half-apply; a partly-patched tree would be a difference in
failure modes rather than in edit format.
Sources and licences are recorded in the plugins' NOTICE. The three test
files run from the repository through `test:dist`, against the same
`@deepseek-ai/dsh-tools` and `@deepseek-ai/dsh-fs` the toolchain installs —
added as exact-pinned dev dependencies, which the lockfile takes as pure
additions with no existing resolution changed.
No arm composes this tool yet.
Generated-by: Claude Code
Terminal-Bench results attribute a score to a framework, which leaves the
edit contract inside the framework and unmeasured. Comparing Maka's
`edit`/`write` against the harness's `str_replace_editor` cannot separate
them either: that comparison moves the framework and the contract at once.
So this holds everything else and moves only the contract. Three arms run
the same one-shot CLI, the same toolchain and fingerprint, the same
`dsh-fs-local` provider under every editor, and byte-identical
`package.json` and `cordis.yml`:
- `deepseek-harness` keeps `str_replace_editor` — one tool, four
commands, unique-literal match, all guidance in the tool description.
- `deepseek-harness-fs` takes the harness's own `read`/`write`/`edit` —
three tools, snake_case arguments, three prompt sections of their own.
- `deepseek-harness-apply-patch` takes the V4A tool added previously —
one envelope, changes located by surrounding context.
The variable is the whole contract, not one dimension of it. Tool count,
argument names, path conventions, where the guidance lives, and the
read-before-edit policy move together because each family ships that way.
`dsh-fs-observation-policy` is mounted for the `fs` arm alone for that
reason: its tools' own guidance text tells the model the policy is in
force, so omitting it would leave the prompt describing a rule the
deployment does not enforce, while `apply_patch` has no such concept and no
read tool to satisfy one with.
Three compositions in three files can drift apart while still producing
numbers, and the numbers would no longer be about the contract. So the
control is asserted rather than intended: the new test fails when the
compositions differ by any row outside their editors, when the model,
reasoning effort, sandbox mode, persona or either Bash deadline drifts in
one of them, when the arms stop sharing one toolchain identity object, or
when the experiment launches them with anything but a different profile
argument. The arms also share one preparer, so a change to how the harness
is launched cannot reach one and miss another, and one identity constant,
so a re-pinned fingerprint cannot be applied to two arms out of three.
The toolchain build now installs the plugin tree beside the harness's
`node_modules` — a composition copied under `$DSH_HOME` cannot resolve a
plugin by package name from there — and fails if a declared plugin did not
land. Its dependencies are unchanged: every package the new arms compose
was already in the reviewed lockfile.
Re-pinning the fingerprint needs a `linux/amd64` container build and is not
done here, so the arms are not yet runnable end to end.
Generated-by: Claude Code
The toolchain now carries the `apply_patch` plugin beside the harness's
`node_modules`, so its checksum manifest changed and the pin has to follow.
Rebuilt with the same pinned `linux/amd64` image and the same reviewed
lockfile; no dependency moved.
Verified against the rebuilt tree rather than assumed. `dsh --dump-config`
composes all three arm profiles successfully, and the only differences
between the three composed trees are the editor rows and the `cwd` the two
new arms give the filesystem backend — nothing else diverged. Loading the
plugin from inside the toolchain resolves `@deepseek-ai/dsh-tools`,
`dsh-fs`, `dsh-sandbox` and `schemastery` through the upward walk into
`lib/dsh/node_modules`, registers `apply_patch` with the expected schema,
and carries the patch instructions without the operations the tool does not
implement. `dsh-tool-fs` and `dsh-fs-observation-policy` resolve too.
Generated-by: Claude Code
@coderabbitai

coderabbitaiBot commented Aug 15, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1357f048-8ea6-474e-8887-d512c43407ac

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

The tool shipped without them, refusing both with a pointer to the shell.
The stated reason was that `ctx.fs` has no delete or rename primitive, so
reaching them through `node:fs` would put a second filesystem authority in
the one arm being measured. That reason does not survive contact with the
deployment it was written for: this arm mounts `dsh-sandbox-local` at
`danger-full-access`, no observation policy is loaded, and the model's own
bash can already remove any file in the container. The authority being
protected was not enforcing anything.
The cost was real, and it was the point of the arm. `apply_patch` exists
here to measure Codex's contract; a patch tool missing a third of its
grammar is a different contract, and a model trained on the real one pays
for the difference in wasted turns. It also broke this PR's own rule —
every other arm composes what its tool family ships, which is exactly why
the `fs` arm mounts the observation policy. Two standards in one change.
So both operations are implemented, against `ctx.fs.processPath(target)` —
the provider's own answer to where a file is for something outside it to
open, and for `dsh-fs-local` the local absolute path. `ctx.fs` stays the
only thing that resolves paths; only the two syscalls it does not offer
happen outside it. Semantics follow Codex: a rename writes the destination
and then removes the source, overwrites an existing destination rather than
refusing, and creates missing parents.
Where the original concern is real, it is enforced instead of assumed: under
a provider that confines, `processPath` would be the way around the sandbox,
so delete and rename refuse up front. `sandboxMode` is what decides, so the
refusal fires exactly when a provider actually confines.
The parser already implemented the whole grammar, so no change there. The
tool description loses its "not part of this tool" paragraph and returns to
Codex's text, now with one removal rather than three: the trailing shell
invocation example, which is false for a function tool.
Tests move from an in-memory store to a real temporary directory, since
these two operations are syscalls an in-memory store cannot observe.
Red-checked: stubbing out the removal fails five behaviour tests, and
disabling the confinement guard fails the two that cover it.
Generated-by: Claude Code
The plugin gained its delete and rename paths, so the file it contributes to
the toolchain changed and the checksum manifest with it. Rebuilt from the
same pinned image and the same lockfile; no dependency moved.
Verified against the rebuilt tree: all three arm profiles still compose
under `dsh --dump-config`, and loading the plugin from inside the toolchain
registers `apply_patch` carrying the full grammar — `*** Delete File:`,
`*** Move to:`, and both productions — with the shell-invocation example
still absent.
Generated-by: Claude Code
Independent review of this arm found five places where the tool behaved
differently from the reference it exists to measure. The asymmetry matters
more than the count: a tool stricter than Codex costs its arm turns the
reference never spends, and a tool safer than Codex wins its arm recoveries
the reference never gets. Either way the benchmark reads the difference as
an effect of the edit contract.
`*** Add File:` did not terminate the created file with a newline, because
the vendored applier joins its added lines while Codex appends `\n` after
each one. Every file this arm created differed from Codex's byte for byte,
and a test pinned the wrong result. Each added line is now terminated, and
an empty create still writes nothing, as `String::new()` does upstream.
The envelope was planned against one snapshot and written afterwards, so a
failed hunk left the tree untouched. That is the nicer property and it was
still wrong: two sections naming one path made the second fail against a
version the first had replaced, and a delete followed by an update removed
the file and then failed, leaving neither. Operations now apply in order,
each reading what the previous one wrote.
`*** Add File:` over an existing file was refused with an invented
`FS_ALREADY_EXISTS`; Codex records what it overwrote and writes. A bare
`*** Move to:` with no hunks was accepted; Codex rejects an empty chunk list
before it reads the rename. A leading newline, a trailing space on the
opening marker, and an indented envelope were all refused; Codex trims the
patch and each marker line, while comparing only `trim_end()` inside an
update body so an indented context line stays content. Content after
`*** End Patch` was silently dropped, so two envelopes in one call reported
success over operations that never ran.
Three defects in how it reached the filesystem, none of which fire under the
arm's own non-confining provider:
- `mkdir` ran on every write through `processPath`, the same way out of a
sandbox that delete and rename refuse for. A create outside a confining
workspace had its write denied and still left the directory behind.
- Delete and rename used the provider's realpath-derived target key, so
deleting a symlink destroyed the file it pointed at and left the link
dangling. They now use the lexical path, which is what Codex removes.
- Hunk failures reached the model as a bare `Invalid Context 0`, naming
neither the file nor the section, and two error codes were outside
`FsErrorCode`'s closed union.
The description's provenance note claimed one modification from upstream
where there are four, and NOTICE claimed the description dropped the delete
and move operations — true when written, false once they were implemented,
and wrong in the direction that matters for a modification notice.
84 plugin tests pass. Each divergence is pinned by a test naming the
upstream file and line it follows.
Generated-by: Claude Code
Review found two differences between the arms that were not the edit
contract, and a control test that could not have caught either.
Only the `fs` arm mounted `dsh-fs-observation-policy`, justified in the
profile by that family's guidance mentioning it. Checking how the harness
ships does not support this: of the four presets in `@deepseek-ai/dsh`,
three mount `dsh-tool-fs` and none mounts the policy, which is not even a
direct dependency; and the policy's own documentation covers the baseline's
editor too. It is substrate, not part of an editor. Mounting it in one arm
gave that arm two failure modes the others cannot have — an edit without a
prior read, and a file the model's own shell touched in between — on a
benchmark where shell and editor are used together constantly. No arm
mounts it now. The apply-patch profile's stated reason for omitting it was
also wrong, and is corrected rather than quietly dropped: that arm emits
`fs/observed` with the file's version before it writes, so the policy would
have allowed the write.
The two new arms configured `fs-local` with a `cwd` expression the baseline
did not have. It could not change anything — the provider defaults `cwd` to
`process.cwd()`, both tools pass the session's own cwd, and `DSH_CWD` is
never set — but an unreachable setting is still a difference a reviewer has
to rule out.
Read budgets are now matched. The baseline pins its editor to 16000 output
characters while `dsh-tool-fs` shipped a 51200-byte default, a 3.2x window
on the same task. How much of a file one call returns is not part of the
edit contract, so `readMaxBytes` is set to match. The remaining read-side
asymmetry, that the patch arm has no read tool at all, is contract-inherent
and stays.
The control test read each `- id:` row with the `name:` on the line below
and compared those strings, leaving every `config:` block unchecked.
Narrowing one arm's context window, halving an editor's output budget,
rewriting the shared bash description, reordering the rows, or overriding
the patch tool's own description all kept it green. It now parses the YAML
and compares whole entries, asserts composition order, and pins each arm's
editor row by value — the treatment was the one thing no assertion covered.
Verified by making each of those six edits and watching it fail.
The arm-to-directory table moves to `toolchain-verification.ts`, beside the
identity table, and the subject and the test both read it. The test used to
restate it, so an arm pointed at the wrong profile would have been checked
against the composition it was meant to run rather than the one it runs.
The toolchain fingerprint is re-pinned for the plugin changes in the parent
commit. Verified in a `linux/amd64` container: all three profiles compose,
the composed trees differ only in the editor row, and the plugin loads from
inside the toolchain and registers `apply_patch` with the full grammar.
Generated-by: Claude Code
The apply_patch arm applied one file's hunks with the OpenAI Agents SDK's
`applyDiff`, vendored on the assumption that one V4A implementation is like
another. It is not. Running the released `codex` binary over the same trees
shows four disagreements, each of which moves this arm's score: a context-free
`+` hunk lands at the `@@` anchor rather than at end of file, an updated file
keeps whatever trailing newline it had rather than gaining one, CRLF survives,
and near-miss context is refused where Codex's four matching passes accept it.
Replace it with a port of Codex's own applier and parser. The parser now reads
hunks into chunks instead of passing section bodies through, so a syntax error
in the third section is found before the first is written, and the tool no
longer needs a second project's operation shape.
Restore verify-then-write. An earlier round changed the tool to apply
operations sequentially, matching the standalone `apply_patch` binary. That
binary is not what a model calls: the function-tool handler and the intercepted
shell call both go through `try_verify_apply_patch_args`, which collects hunks
into a map keyed by resolved path, refuses a duplicate key, and computes every
change against the pre-patch tree. So an envelope naming one path twice is
refused, and a hunk whose context the model guessed at costs nothing.
Fidelity is now measured rather than argued. `codex-oracle.mjs` runs the real
binary over a temporary tree for each of 72 cases and records what it printed
and what the tree became; `codex-fixtures.test.mjs` replays every case through
the registered tool and compares. The two cases where the function tool
deliberately differs from the standalone binary are named there with what it
does instead. A fuzzer over 1800 random envelopes found no further divergence.
Also tighten the arm-control test, which had three gaps: it skipped composition
steps that override an already-composed entry, so an override of the patch
tool's description was invisible; it named the profile files to compare instead
of reading them; and it exempted the first subject's profile argument while
comparing only the argument vector, not the whole subject definition.
Toolchain fingerprint re-pinned. Test-only files move under `__tests__/` so the
build prunes them by directory rather than by filename.
Generated-by: Claude Code
`readMaxBytes: 16000` was justified as making "how much of a file one call
returns" equal across the two read-capable arms. It does not. The baseline
editor spends eight characters a line on line numbers plus a header; the fs
tool emits neither, so measured over this repository's own sources it returns
10% more lines for the same budget and 22% on the longest files.
The budget is still the right one, for a reason the note did not give: what
should be equal is what a read costs the context window, and the numbering the
baseline adds is part of its contract that the model pays tokens for either
way. Equalising delivered content would mean picking a budget from an assumed
average line length.
Record the measurement, and record the one genuine mismatch that remains — the
budgets are counted in UTF-16 code units against UTF-8 bytes, equal on ASCII
and not otherwise.
Generated-by: Claude Code
The fixtures pin the cases someone thought to write down. This searches for the
rest: it generates envelopes from random line soup, applies each with the
released binary and with the registered tool, and reports every input the two
disagree on. 2100 envelopes across seven seeds currently diverge on nothing.
Red-checked against three deliberate regressions — placing a context-free `+`
hunk at the `@@` anchor, dropping the trailing-newline normalisation, and
removing the punctuation-folding match pass. The third went unnoticed through
400 envelopes at first, because context lines drawn verbatim from the file only
ever exercise the exact-match pass; the generator now half-remembers a line the
way a model does, and catches it.
Envelopes name each path at most once, and a refusal is compared as a refusal
without comparing the tree, because those are exactly where the standalone
binary and the function tool are supposed to differ.
Generated-by: Claude Code
The function-tool handler verifies an envelope and then throws the result
away: `ApplyPatchRuntime::run` hands `req.action.patch` — the raw text the
model sent — back to `apply_patch_with_mode`, which re-parses it and applies
it sequentially against the live filesystem. The verified `changes` map is
carried for approval and telemetry only.
This tool verified and then wrote the plan it had just computed. That is not
a cheaper version of the same contract, and the reference binary shows the
difference in two cases now covered by tests: a rename onto a path a later
section also updates leaves that section failing against the renamed file,
and `*** Move to:` naming its own source writes the destination and then
unlinks it, deleting the file while reporting success. Both were previously
refused with an invented "multiple operations target" error, because the
verification map also claimed the move destination — upstream keys only on
`hunk.resolve_path()` and carries `move_path` as a value.
Also in the write path: `fs/edit-intent` yields `{version}` with no `kind`,
so passing it straight to `writeText` matched neither guard and silently
wrote unconditionally; the abort signal was not checked between operations;
and `rm` ran with `force: true`, reporting `D <path>` for a file that had
already gone.
In the applier: each fuzzy-matching pass now projects both sides once
instead of recomputing the punctuation fold per candidate position, and
replacements are rebuilt in one forward pass rather than spliced, which
threw a RangeError once a hunk added more lines than the argument limit.
The model-facing description is no longer transcribed into this file. It is
read from a byte-for-byte copy of the upstream artifact it came from, with
three named substitutions applied over it, so the claim that it differs in
exactly those places is checkable rather than asserted in a comment. Copying
the file rather than retyping it also removed a fourth difference that had
no reason to exist: upstream's typographic punctuation is now preserved.
Its recorded provenance was stale — the file it named no longer exists at
the pinned commit — and now points at the artifact that does.
One parameter description said the input was "Not JSON-wrapped". It is: the
harness registers JSON function tools and nothing else.
Generated-by: Claude Code
Upstream ships codex-rs/apply-patch/tests/fixtures/scenarios as, in its own
README's words, an end-to-end suite "meant to be easily portable to other
languages or platforms". This is the port: the 25 scenario directories are
copied verbatim from the pinned commit and replayed through the registered
tool, compared the way upstream compares them — copy `input/`, apply
`patch.txt`, ignore the exit status, and require the resulting tree to equal
`expected/` exactly.
It is the only check here with the standing to contradict the code. The
oracle fixtures run the reference binary, but over inputs this repository
chose; the fuzzer perturbs inputs this repository chose how to perturb.
These were chosen by upstream, before and independently of this tool.
22 of 25 match outright. Three do not, and each divergence is recorded with
its cause and pinned to bytes the reference binary produces:
- 015 specifies the standalone binary, which has no verification pass, so an
add preceding a failing update survives. The function tool writes nothing.
- 023 and 024 are specified under CODEX_APPLY_PATCH_PRESERVE_LINE_ENDINGS,
a feature that is `Stage::UnderDevelopment, default_enabled: false`, so a
shipped Codex and this arm both run the `NormalizeToLf` default.
Both divergence lists — here and in the oracle fixtures — now assert that
each entry still diverges, so an excuse cannot outlive the thing it excused.
Generated-by: Claude Code
The treatment in this arm is the tool name, the parameter it takes, and the
description text. None of the three had any coverage, so a build that
registered the tool under another name, dropped the parameter, or shipped a
truncated description would have run a cohort and produced numbers.
The description claim is the one that had to be argued rather than shown:
"three differences from upstream and no others" was a comment. It is now
checked at both ends. The vendored instructions file is pinned by digest,
with the `git show ... | sed -n` that reproduces it from the source recorded
beside the constant, and the built text is recomputed here by a different
mechanism than the code uses and required to match. Each adaptation must
apply exactly once, and no mention of the shell delivery may survive.
Red-checked against four sabotages — appending a line to the description,
renaming the tool, editing one byte of the vendored text, and neutering an
adaptation. Each is caught.
Generated-by: Claude Code
Two differences at the outermost layer of the patch, both pinned by new
oracle fixtures generated from the reference binary.
`parse_patch` selects its mode from `PARSE_IN_STRICT_MODE`, which is `false`:
upstream gave up on gating leniency by model, so every call site including
the function tool unwraps a heredoc-wrapped envelope. A model asked to send
a patch may well send the `<<'EOF' ... EOF` form it was shown, and Codex
applies it where this refused it. The strict boundaries are still tried
first, so an envelope whose own first line reads `<<EOF` is never stripped.
Rust's `str::trim` cuts the Unicode `White_Space` property; JavaScript's also
cuts U+FEFF and does not cut U+0085. Only U+FEFF can realistically occur — a
byte-order mark carried into a JSON string argument — and it made this tool
accept a patch Codex refuses, which is the wrong direction for an arm being
compared against the contract it imitates.
Red-checked: restoring `String.prototype.trim` fails the byte-order-mark
fixture, and disabling the heredoc strip fails both heredoc fixtures.
Generated-by: Claude Code
The comment justifying `readMaxBytes: 16000` said `str_replace_editor`
spends eight characters a line on line numbers and a header while "this tool
emits neither". Both tools number their lines, and this one wraps the result
in a `<path>/<type>/<content>` envelope with a footer.
The setting still holds something equal, but not what was claimed. The
baseline truncates the fully rendered string; this tool charges the budget
against raw line bytes plus separators and adds the gutter, the envelope and
the footer afterwards, outside it. Measured across this repository's 1480
TS/JS/Python sources at 16000, the arm delivers 1.105x the lines and emits
1.027x the characters, worst file 1.34x and 1.19x — a residual in this arm's
favour, now recorded with the mechanism that produces it.
The three profiles also claimed to have been "verified request-for-request"
against the upstream composition. That was a manual comparison made once
during development; nothing here reproduces it, and the headers now say so.
`MAKA_EVAL_DSH_PLUGINS` was exported into all three arms and read by one, so
the two controls carried an environment difference for a mechanism they do
not use. It is now set from the copied composition itself, which keeps the
condition and the file that depends on it from drifting apart.
Generated-by: Claude Code
`apply_patch` in Codex is a freeform tool: the API is sent a short description
and a Lark grammar the decoder is constrained to, so a model on that path
cannot emit a malformed envelope. The harness registers JSON function tools
and has no grammar seam, so this arm is the unconstrained case. That is not a
detail of the port — it is a cost charged to this arm and to no other, and it
now leads the arm's description instead of being absent from it.
Three further divergences are named with their causes: the function-tool path
against the standalone binary's sequential apply, `NormalizeToLf` against a
preserve-line-endings feature that ships disabled, and diagnostics more
specific than Codex's single `Invalid patch:` line, which is the one place
this arm may be easier on a model than the reference.
The README also described a verify-then-write-the-plan design the tool no
longer has, quoted the old fixture count, and repeated the read-budget
mechanism corrected in the previous commit.
The NOTICE opened by saying nothing was carried verbatim as a file; two
things now are. Its description provenance named a file that no longer exists
at the ported commit and a fourth modification that was an artefact of
retyping rather than copying, its fixture path was stale, its account of how
`PreserveLineEndings` is selected covered only the standalone binary, and its
list of ported functions omitted the ones that decide apply order.
The arm-control test read a composition step as an insertion whenever it had
an `insert` key, so `{insert, remove}` would have passed with the removal
unread, and it filtered directories out of each profile's file list rather
than refusing them. It also now names what it does not cover.
Generated-by: Claude Code
`dsh-fs-local` creates a write's parent itself, in `writeFileAtomic`, and it
is the provider every arm mounts. The `ensureParent` here ran `mkdir` on a
`processPath` — the same way around a confining sandbox that delete and
rename refuse for — and existed only because the test harness's provider was
weaker than the real one and failed with ENOENT without it.
That is the more useful half of this change: a fake that cannot do what the
real thing does will not merely miss bugs, it will make production code grow
to cover for it. The fake now creates the parent the way `writeFileAtomic`
does, and the tool has no call that creates a directory at all.
The test that pinned the old behaviour was passing for the wrong reason — it
asserted the tool left no directory behind, and what actually happened was
the fake's write failing before anything could be created.
Generated-by: Claude Code
…sses
`deriveNewContents` reported a context miss as a plain `Error`, and the
caller rewrapped anything it threw as `FS_EDIT_NOT_FOUND` — so a bug in the
applier would have reached the model as "the lines you named are not in the
file", which is advice to rewrite a correct patch. The context miss is now
its own type and the only thing translated; a comment already claimed this
and the code did not.
Two oracle fixtures pin the order of the four matching passes against each
other. The existing cases pinned the exact pass ahead of the loose ones;
between the loose ones, removing a pass does not fail a match, it silently
moves it to a different line, and nothing caught that. Each of the four now
fails at least one fixture when removed.
The line-ending note in codex-apply.mjs described only the standalone
binary's environment variable, not the feature flag the function tool reads.
The toolchain is rebuilt and its fingerprint re-pinned: the model-facing
instructions now ship as a file, so they are covered by `checksums.sha256`
rather than living only inside a JavaScript template. Verified inside the
pinned linux/amd64 container that the built tree registers one tool named
`apply_patch` with one required `input` string and a 2903-byte description
carrying no mention of the shell delivery.
Generated-by: Claude Code
Differential fuzzing against the reference binary found a regression this
branch introduced. `applyReplacements` was rewritten to build the file
forward in one pass, on the reasoning that the replacements are sorted and
disjoint. They are sorted; they are not always disjoint.
`computeReplacements` places a context-free insertion at the end of the file
without consulting the search position — deliberately, because that is what
`insertion_idx` does upstream — so it can land inside a range a later chunk
replaces. Applying descending, the replacement swallows it; applying forward,
the two are treated as separate edits and the inserted line survives into a
file the reference never produces.
Restored to splicing in descending order, with the replacement segment going
in in bounded batches so a hunk larger than the engine's argument limit still
does not throw. The case is now an oracle fixture, so it is caught without a
`codex` on PATH; red-checked by reinstating the forward rebuild.
2300 further envelopes across four seeds diverge on nothing. Toolchain
rebuilt and re-pinned.
Generated-by: Claude Code
An adversarial review pass found nine behaviours that were wrong or untested
in the V4A tool, each verified against the reference binary or upstream source
before being changed. All nine are now covered: sabotaging any one of them
fails the suite, where before five of them left it green.
The matcher used JavaScript's `trim` where upstream uses Rust's. The two
disagree on U+FEFF and U+0085, so a file whose first line carried a byte-order
mark matched a pattern without one and was written back with the mark deleted,
and a line terminated by NEL was refused where the reference applies it. The
charset the parser already spelled out moves to rust-trim.mjs and both use it.
The verify pass now does what `try_verify_apply_patch_args` does: nothing at
all for `*** Add File:`, and a read for `*** Delete File:` as well as for an
update. Statting a delete target instead applied a delete the reference
refuses, since `read_file_text` maps invalid UTF-8 to `InvalidData` and fails
the whole envelope. It also stops emitting `fs/observed {present}` off a bare
stat, which told an observation policy a file had been seen that nothing read.
The duplicate-path key is lexical, as upstream's `PathUri::join` is, not the
provider's realpath-derived target key. An envelope naming both a symlink and
its target was refused here; the binary applies both sections in order.
A move destination no longer runs the `fs/write-intent` waterfall and discards
the answer, which turned a decider's refusal to clobber into a silent
overwrite. A failed unlink and an abort between passes now arrive as `FsError`
with a routable code and the path the model wrote, not a raw errno and a
resolved host path.
The test provider was weaker than `dsh-fs-local` in four ways that made the
above invisible: it decoded lossily where the real one raises `FS_NOT_TEXT`,
called every non-directory a file, took three parameters so the signal and
sandbox policy could not be observed, and never returned a decider's answer.
Two oracle cases are added for the trim charset and one for a trailing U+FEFF,
which is what separates the three loose matching passes from each other; the
BOM-in-content case is recorded as a divergence with its true cause, which is
that the shared provider strips a leading mark before this tool sees the file.
Generated-by: Claude Code
The import landed the 25 scenario directories, their README and their
.gitattributes twice: once at `__tests__/upstream/` and once, byte-identically,
at `__tests__/upstream/scenarios/`. Only the nested copy is read.
Generated-by: Claude Code
…arms
The three arms are meant to differ only in the edit contract. Three settings
outside that contract were not held.
`readMaxLineLength` was left at its shipped 2000 while only `readMaxBytes` was
matched to the baseline. That is a cap the baseline has no counterpart to: the
fs arm could never see the 2001st character of any line, at any offset, while
`str_replace_editor` truncates once over the whole rendered view and can spend
its entire budget on one line. A minified bundle or a single-line payload is
where that lands, and it lands against one arm only. It is raised to the
budget, where it can no longer bind before `readMaxBytes` does.
`streamIdleTimeoutMs` was omitted from all three profiles, leaving the
adapter's 300000 ms default in force — five minutes between two streamed
tokens, behind `reasoningEffort: max` and a 65-minute bash deadline. Upstream's
own jsonrpc-agent composition sets 172800000, which is what the arms now carry;
a turn killed by that timeout would have been scored as a failure of whichever
contract was running.
`maxConcurrentTaskGroups` was 64. A group holds one cell per subject and starts
them together, so three arms made 192 concurrent trials where every other spec
here lands on 128. That does not bias one arm against another, but it puts this
run's `deepseek-harness` score under a load no single-arm run shares. It is now
43, and the test asserts the arithmetic rather than the number.
The read-budget residual quoted in the profile and the README had no
reproducible source and reported only the tail that favoured the fs arm.
`scripts/measure-read-budget.mjs` reimplements both renderers from the shipped
code and reports both tails; the figures are replaced with what it prints.
Generated-by: Claude Code
A claims audit checked every factual assertion in the README, the NOTICE and
the profiles against upstream source at the ported commit and against the
shipped harness packages. Fourteen did not hold.
The largest is provenance. `prompt_with_apply_patch_instructions.md` is not in
any shipped Codex system prompt at the ported commit: it is referenced only
from a test whose four model cases all declare
`expects_apply_patch_description: false`, and none of the eight instruction
templates in models.json carries the section. The file is upstream's own
authored description of the format and it is retired, which is a better reason
to use it than the one previously given, but the prose said Codex sends it.
The freeform description is 108 characters, not 105. The oracle runs 82 cases,
not 76. The fuzz figure is 2300 across four seeds, not 2100. Two of the named
divergences were recorded in the tests, not three, and there are now four with
the byte-order mark the provider strips. `verifyToolchainDirectory` covers ten
registered profiles, not eight. `checksums.sha256` omits itself and
manifest.json. The toolchain build's `__tests__` prune is scoped to the plugins
it copies. The build script runs from the repository root. `ctx.fs` offers
twelve methods, not three. The 0.147.0 delta includes a second behavioural
difference — the duplicate-path check does not exist at that tag — and a larger
restructure than the three items named. The profiles record three deviations
from upstream, not one.
Also stated where it was missing: the arm's lack of a read tool is Codex's
contract rather than a handicap, since Codex registers no file-read tool
either and every arm has the same bash.
Generated-by: Claude Code
A second adversarial round found that the previous round's fix traded one
divergence for another, and that twelve mutations of this tool still left the
whole suite green.
`ctx.fs.readText` is not `read_file_text`. Upstream reads bytes and calls
`String::from_utf8`, so every valid UTF-8 sequence is text; `dsh-fs-local`
adds two rules on top, and both are model-visible. It rejects a NUL in the
first 8192 bytes as binary — but NUL is valid UTF-8, and the reference patches
and deletes straight through it, verified against the binary — so making the
verify pass read delete targets took `*** Delete File:` on such a file with
it. And it decodes with `ignoreBOM` left false, which strips a leading
byte-order mark, so a patch whose context omits the mark applied here, was
refused there, and wrote the file back without the mark. Reading through
`ctx.fs.readBytes` and decoding here is exactly `String::from_utf8`; it fixes
both, and the byte-order-mark divergence is deleted rather than documented.
`*** End of File` as the first line of an update hunk was refused. Upstream
guards with `chunks.last().is_some_and(blank)`, which is false when there is
no chunk, so the marker is ignored; the binary applies the envelope.
Twelve mutations that the suite could not see, now each red-checked: five
dropped the turn signal from a provider call, which a fake that ignored the
signal could not notice; two removed or falsified the version on `fs/observed`;
two swapped a removal's announcement with the removal itself, which leaves the
event list identical and the trail a lie; one bypassed the create decider and
one renamed its event; one reduced the sandbox-denial remap to a no-op. The
fake now honours and records the signal on every call, records what the
filesystem actually held when each observation was published, records the
version `writeText` returned, and can be made to deny a write.
Twelve oracle cases added, generated from the binary: three for the read seam
and nine for shapes that separate this port from a plausible variant of it —
overlapping chunks, a backward `@@` anchor, an indented header after an add
body, a trailing mark on an End of File marker, header paths with a leading
space, a repeated `*** Move to:`, a heredoc close with trailing text, and the
End of File case above. Eleven passed unchanged, which is the point of adding
them; one was the defect.
`requireDirectFilesystem` now raises an `FsError` like every other refusal.
Generated-by: Claude Code
… meant to help
Raising `readMaxLineLength` from its shipped 2000 to the 16000 budget was
wrong, and worse than the asymmetry it was meant to close. `dsh-tool-fs`
truncates a long line and appends a `... (line truncated to N chars)` marker,
then charges the whole thing against `readMaxBytes`. Once the cap approaches
the budget the truncated line no longer fits at all: the window returns zero
lines, drops every line after it, and the footer invites the model to retry at
the offset it just used, so the rest of the file is unreachable. Measured
against the shipped functions, a 20000-character line followed by 200 short
ones delivers 201 lines at 2000 and 0 lines at 16000.
No value equalises the two budgets anyway — one counts UTF-16 code units and
the other UTF-8 bytes — so the cap goes back to the value the tool ships with
and the residual is measured instead of engineered away. At 2000 the direction
is also not the one the earlier reasoning assumed: on that same long-line file
the baseline spends its whole budget on the one line and delivers 1, while
this arm truncates it, says so, and reads on.
The guard test now asserts the invariant rather than the number, so any future
cap has to be one a truncated line can still fit under.
The measurement script had four transcription errors, all inflating the
character ratio: it dropped the baseline's 240-character truncation notice and
the fs arm's per-line marker, counted the baseline's prompt line as content,
and counted a phantom trailing line for the fs arm. It also silently ate the
first positional path when `--budget` was absent. Rather than fix the
transcription and hope, `--self-check <dir>` now lifts the real functions out
of an installed copy and asserts agreement, and the corpus figures are
reported alongside synthetic long-line cases that the repository's own sources
do not contain. Corrected: 2197 files, 1.093x lines, 1.001x characters, tails
reported as what they are — extremes among the 570 files where a budget binds.
`maxConcurrentTaskGroups` moves 43 to 42: three arms at 43 is 129 trials, and
the assertion had been written as `<= 129` to admit it.
Generated-by: Claude Code
The claim that upstream's `## apply_patch` prose is retired was too strong. Of
the eight instruction templates in `models-manager/models.json` at the ported
commit, seven carry no such section — but `gpt-5.2` carries a shorter one with
the same envelope, the same three headers and the same worked example. So the
format is still taught in prose upstream, and the vendored file is the fuller
version of that prose rather than something abandoned. The conclusion it was
supporting survives unchanged and for a cleaner reason: `gpt-5.2` gets the
prose *and* the grammar, so no shipped configuration is the one this arm is in.
Codex's handler set is much longer than the six entries named, and one of them
takes a filesystem path — `view_image`, for images. The load-bearing claim is
that no handler reads file *text*, which is what the prose now says.
The NOTICE gave the wrong cause for the duplicate-path divergence: the
recorded binary composes an envelope naming one path twice because the
standalone applier has no verification pass at either version, not because the
check is absent at rust-v0.147.0 — that is a function-tool difference.
Also: `invocation.rs:246-248` is the delete arm, not the add arm; the
`fs/edit-intent` shape citation named only one of the two declarations; the
end-of-file search has no fallback to the ordinary start position, contrary to
its comment; `__tests__/upstream/` kept a stray `.gitattributes` naming paths
one level down; and the build script's header credited `npm i` for a
non-reproducible fingerprint when it runs `npm ci` and the real cause is native
compilation.
The refusal under a confining provider is described as what it is: dead code
under `dsh-fs-local`, which never sets `sandboxMode` at any sandbox mode, not
something `danger-full-access` switches off.
Generated-by: Claude Code
…t reads it
The comment justifying the conditional export had the causation backwards. The
model's shell inherits this environment, so setting the variable in all three
arms is what makes them identical, and setting it in one arm alone is what
creates a difference the model can read with `env`. It names a directory and
starts nothing, so it is inert in the two arms whose compositions never mention
it.
The remaining environment asymmetry, `$HOME` carrying the profile name, is
recorded rather than removed: the arms need separate homes because each writes
its own composition under `$DSH_HOME/profiles`, and sharing one would have each
overwrite what the others booted from. So the experiment is not blind to its
subject, and that is now stated where the home is built.
Generated-by: Claude Code
The spec declared 42 task groups, or 126 concurrent trials, on the
strength of a 128-trial convention rather than a machine. The account's
CVM quota is 60 vCPU per zone, which caps the host at 56 vCPU and
256 GiB, and the only completed run of this suite put 24 trials on
32 vCPU / 64 GiB — 2.7 GiB per trial. 126 trials on that host is 2.0 GiB
each, close enough to a Terminal Bench build task's peak that an OOM
kill would land as an infra failure. 32 groups is 96 trials at the
density that is known to survive.
Generated-by: Claude Code
The arm named its plugin with `name: !!js process.env.MAKA_EVAL_DSH_PLUGINS
+ '/tool-apply-patch/index.mjs'`. The harness's loader evaluates that tag for
`disabled` alone and hands `name` the unevaluated node, so every trial of
that arm died at boot with `name.startsWith is not a function` before it
reached the model — on a rented host, at the first smoke task.
The row now names `./plugins/tool-apply-patch/index.mjs`, resolved against the
profile directory, and the subject plants `plugins` there as a link into the
toolchain. That keeps what the loader imports inside the fingerprinted
read-only tree, keeps the composition a plain string, and keeps the path
independent of the root the wrapper was given — which naming the mount point
outright would not. The link is planted for all three arms, on the same
reasoning that exported the variable for all three: the model's shell can read
$DSH_HOME, so planting it in one arm alone is the difference, not planting it
everywhere.
The test could not have caught this. It extended its own YAML schema to
construct `!!js <source>` as a string, which taught the assertion to accept a
form the harness cannot load. The tag is real elsewhere in these compositions —
`task: !!js ctx.headlessStartup.task` is upstream's — so the schema now
constructs it as the marker shape the harness's loader produces, and a new
assertion requires every entry name to be a string and every path-named plugin
to be one the toolchain build ships. Reverting the row fails both.
Generated-by: Claude Code
The wrapper counted the subject's stderr bytes and hashed them, then dropped
them. The relay writes `maka-subject.stderr.txt` from the wrapper's own
stderr, which is empty, so a failing arm's own account of what killed it
reached the attempt record as a byte count and a digest and nothing else.
Both failures this run has had were diagnosed by reproducing them by hand on
the host instead: a composition the harness could not load, and a subject that
exits after one model response. The second is still open.
The last 64 KiB now land in `/logs/agent/<profile>.stderr.txt`, which the
relay already collects, as a `stderr-tail` artifact beside the existing
`stderr` count. A tail because a subject that loops printing would otherwise
fill the trial's disk, and because a harness says what killed it on the way
out; the count and digest still describe the whole stream, so truncation is
visible.
Generated-by: Claude Code
Announcing it as an artifact pushed the frame past the relay's 2 KiB payload
cap, so every cell came back `external subject result transport failed`.
`/logs/agent` is collected into the trial directory regardless, so writing the
file is the whole job.
Generated-by: Claude Code
Every failure before the subject starts reached the operator as the string
`external subject setup failed`, because `safeFailure` sanitizes what the
published result frame may say and nothing recorded the error itself. The
process's own stderr is collected into the trial as
`maka-subject.stderr.txt`, which is where a subject's account of its own
failure belongs, so the error, its stack, and its cause chain go there.
Generated-by: Claude Code
This module does its work at the top level, above the function that reads the
subject's stderr, so a `const` declared beside that reader is still in its
temporal dead zone when the reader runs. Every cell died with
`ReferenceError: Cannot access 'STDERR_TAIL_BYTES' before initialization`
before the subject's first model response — reported as
`external subject setup failed`, and legible only because the commit before
this one started writing the error itself.
Generated-by: Claude Code
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat(eval): compare three edit contracts on the DeepSeek Harness - #3067

Closed
Astro-Han wants to merge 33 commits into
feat/eval-deepseek-harness-subjectfrom
feat/eval-edit-contract-arms
Closed

feat(eval): compare three edit contracts on the DeepSeek Harness#3067
Astro-Han wants to merge 33 commits into
feat/eval-deepseek-harness-subjectfrom
feat/eval-edit-contract-arms

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Summary

A Terminal-Bench score is attributed to a framework, which leaves the edit contract inside the framework and unmeasured. Comparing Maka's edit/write against the harness's str_replace_editor cannot separate them either — that comparison moves the framework and the contract at once.

These three arms hold everything else and move only the contract:

armcontract
deepseek-harnessstr_replace_editor — one tool, four commands, unique-literal match, all guidance in the tool description
deepseek-harness-fsthe harness's read/write/edit — three tools, snake_case, three prompt sections of their own
deepseek-harness-apply-patchV4A apply_patch — one envelope, changes located by surrounding context

Same one-shot CLI, same toolchain and fingerprint, same dsh-fs-local provider under every editor, byte-identical package.json and cordis.yml. The variable is the whole tool family and not one dimension of it: tool count, argument names, path conventions and where the guidance lives move together, because each family ships that way. A result is attributable to the family as shipped, not to patch syntax as such.

No arm mounts dsh-fs-observation-policy. It was mounted for the fs arm alone on the reasoning that its tools' guidance mentions the policy; of the four presets in @deepseek-ai/dsh, three mount dsh-tool-fs and none mounts the policy, and its own documentation covers str_replace_editor too. Mounting it in one arm gave that arm two failure modes the others cannot have.

Three compositions in three files can drift apart while still producing numbers, so the control is asserted rather than intended. edit-contract-arms.test.ts fails when the compositions differ by any row outside their editors, when they compose in a different order, when the model, context window, reasoning effort, stream idle timeout, sandbox mode, persona or either Bash deadline drifts in one, when the arms stop sharing one toolchain identity object, when the experiment launches them with anything but a different profile argument, or when arms multiplied by concurrency leaves the 128-trial convention.

The patch arm is ours, and that is the thing to review

The harness ships no patch tool, so plugins/tool-apply-patch/ is this repository's while the other two arms run their vendor's. Fidelity to Codex is therefore itself an experimental variable, and it is asymmetric: a tool stricter than the reference costs its arm turns the reference never spends, and a safer one wins recoveries the reference never gets.

One deviation cannot be removed.apply_patch in Codex is a freeform tool — the API is sent a 108-character description plus a Lark grammar the decoder is constrained to, so a model on that path cannot emit a malformed envelope. The harness registers JSON function tools and has no grammar seam. This arm is therefore the unconstrained case, and syntax errors it can make are errors Codex's decoder would have prevented. That cost is charged to this arm and to no other, and it leads the arm's description in the README.

What is not charged to it is the absence of a read tool. Codex registers no handler that reads file text either — its handler set is long, and the only entry taking a filesystem path to read is view_image, which takes images — so an arm that reads with cat is that contract, and every arm has the same bash at the same 16000-character cap.

What the model is told is upstream's own prose for exactly that case: the ## apply_patch section of prompt_with_apply_patch_instructions.md, carried verbatim as upstream-apply-patch-instructions.md with three substitutions applied over it. A test pins the file's digest, requires each substitution to apply exactly once, and requires the built description to differ from the copy nowhere else. That file is upstream's and it is not delivered from where it sits: at the ported commit it is referenced only from a test whose four model cases all declare expects_apply_patch_description: false. Seven of the eight instruction templates in models-manager/models.json carry no ## apply_patch section and leave the format to the grammar; the eighth, gpt-5.2, carries a shorter one with the same envelope and worked example. So the format is still taught in prose upstream and this is the fuller version of it. Either way no shipped configuration is the one this arm is in — gpt-5.2 gets prose and grammar — so the choice was between upstream's own text and text written here.

Three further divergences are known and each is pinned to bytes the reference produces:

  • the tool implements the function-tool path — verify the whole envelope, then re-apply it from the raw text — where the standalone binary applies sequentially with no verification;
  • it runs NormalizeToLf, the shipped default, where PreserveLineEndings is Stage::UnderDevelopment, default_enabled: false upstream;
  • its diagnostics differ from Codex's, which is the one place this arm may be easier on a model. This one is recorded in prose only; the other two are named with their causes in the test files, which also assert that each still diverges.

A third divergence was recorded here and has since been removed rather than documented: the provider's readText strips a leading byte-order mark and rejects a NUL byte, neither of which String::from_utf8 does. Reading through readBytes and decoding in the tool matches the reference exactly.

The grammar is implemented in full. *** Delete File: and *** Move to: are the only operations ctx.fs has no primitive for, so they run against ctx.fs.processPath(target); under a provider that confines, both refuse up front rather than escape. This arm mounts danger-full-access, where the refusal never fires.

Sources and licences are in the plugins' NOTICE. An earlier revision of this branch vendored the OpenAI Agents SDK's applyDiff as the hunk applier; it is a different V4A implementation, not Codex's, and nothing from it remains.

What the adversarial review rounds changed

Two rounds of four fresh-eye passes each — the port against the binary, the harness integration, the three-arm control, and an audit of every factual claim. The second round reviewed the first round's fixes, which is what caught the two entries below that begin "the previous fix". Every finding was re-verified against upstream source or the reference binary before being acted on.

Nine port defects, all now red-checked. The matcher used JavaScript's trim where upstream uses Rust's, so a file whose first line carried a byte-order mark matched a pattern without one and was written back with the mark deleted, and a line terminated by U+0085 was refused where the reference applies it. The verify pass stated a delete target instead of reading it, applying deletes of non-UTF-8 files that the reference refuses outright. It emitted fs/observed {present} off a bare stat, telling an observation policy a file had been seen that nothing read. The duplicate-path key was the provider's realpath-derived target key rather than upstream's lexical join, so an envelope naming both a symlink and its target was refused where the binary applies both. A move destination ran the fs/write-intent waterfall and discarded the answer, turning a decider's refusal to clobber into a silent overwrite. A failed unlink and an abort between passes escaped the seam's error taxonomy.

Five of those were invisible because the test provider was weaker than dsh-fs-local: it decoded lossily, called every non-directory a file, took three parameters so the signal and sandbox policy could not be observed, and never returned a decider's answer.

The previous fix read patch targets through the wrong seam.ctx.fs.readText is not read_file_text: upstream reads bytes and calls String::from_utf8, while dsh-fs-local additionally rejects a NUL in the first 8192 bytes and strips a leading byte-order mark. NUL is valid UTF-8 and the reference patches and deletes straight through it, so making the verify pass read delete targets — itself a correct fix — took *** Delete File: on such a file with it. Reading through readBytes and decoding in the tool is exactly String::from_utf8, and it removes the byte-order-mark divergence too. Separately, *** End of File as the first line of an update hunk was refused where upstream ignores it.

Twelve more mutations the suite could not see. Five dropped the turn signal from a provider call, which a fake that ignored the signal could not notice; two removed or falsified the version on fs/observed; two swapped a removal's announcement with the removal itself, which leaves the event list identical and the trail a lie; one bypassed the create decider, one renamed its event, one made the sandbox-denial remap a no-op. The fake now honours and records the signal on every call, records what the filesystem held when each observation was published, records the version writeText returned, and can be made to deny a write.

Three uncontrolled differences between the arms, of which one fix was itself wrong. streamIdleTimeoutMs was omitted from all three profiles, leaving a five-minute ceiling on the gap between two streamed tokens behind reasoningEffort: max and a 65-minute bash deadline; it now carries upstream's own 172800000. maxConcurrentTaskGroups: 64 made 192 concurrent trials where the saturating specs here land on 128; it is now 42.

readMaxLineLength was raised from its shipped 2000 to the budget to close a cap the baseline has no counterpart to, and that was worse than the asymmetry. The tool appends a ... (line truncated to N chars) marker and then charges the whole line against readMaxBytes, so a cap near the budget makes a long line not fit at all, drops every line after it, and returns a window no offset recovers — 201 lines at 2000 becomes 0 at 16000. No value equalises the two budgets anyway, one counting UTF-16 code units and the other UTF-8 bytes, so the cap is back where the tool ships it and the guard test asserts the invariant rather than the number.

Twenty-one false claims across the two rounds, listed in the two docs commits. The largest was the provenance one, twice: first that the vendored prose is what Codex sends, then that it is retired. The read-budget script had four transcription errors of its own, all inflating the character ratio, so it no longer asks to be trusted — --self-check <dir> lifts the real renderers out of an installed copy and asserts agreement, and the figures are 1.093x lines and 1.001x characters over 2197 files.

Verification

Lint, format and typecheck clean. 47 TypeScript and 200 plugin tests pass.

Fidelity is measured against the binary rather than argued from the Rust:

  • codex-oracle.mjs runs the released codex under argv0 apply_patch over a temporary tree for each of 94 cases and records what it printed and what the tree became; codex-fixtures.test.mjs replays every case through the registered tool and needs no codex to do it.
  • __tests__/upstream/scenarios/ carries Codex's own conformance suite verbatim — the 25 scenario directories upstream publishes as "meant to be easily portable to other languages or platforms" — and upstream-scenarios.test.mjs replays them upstream's way: copy the input tree, apply the patch, ignore the exit status, require the resulting tree to match exactly. 22 match outright; the three that do not are named divergences. These are the only inputs here this repository did not choose.
  • codex-fuzz.mjs generates envelopes from random line soup, including near-miss context, and runs both. It earned its keep on this branch: it caught a regression introduced here, where rebuilding the file forward let a context-free insertion survive inside a range a later chunk replaces. 2300 envelopes across four seeds diverge on nothing.

Red-checked, not just green:

  • Appending a line to the description, renaming the tool, editing one byte of the vendored instructions, or neutering a substitution each fails the contract-surface test.
  • Removing any one of the four context-matching passes fails at least one oracle fixture; restoring String.prototype.trim in the loose passes fails the trailing-U+FEFF fixture; disabling the heredoc strip fails both heredoc fixtures; matching the heredoc close with includes instead of endsWith fails the trailing-text fixture.
  • Each of the twenty-one port defects and unseen mutations above fails the suite when reintroduced. Seventeen of them did not before these two rounds.
  • Both divergence lists assert that each entry still diverges, so an excuse cannot outlive the thing it excused.

End-to-end against the rebuilt linux/amd64 toolchain: loading the plugin from inside the built tree registers exactly one tool named apply_patch, with one required input string and a 2903-byte description carrying no mention of the shell delivery. The instructions file ships, so it is covered by checksums.sha256 and the re-pinned fingerprint.

Not run: an actual benchmark cohort. No dependency moved.

Review focus

One claim this repository does not reproduce, and which should be read as a one-time manual check rather than a standing guarantee: the three profiles were compared against the upstream examples/jsonrpc-agent composition during development — tool names, tool schemas, system prompt and message sequence on the first outbound request — and found identical.

The read budget matched at 16000 equalises the budget's number, not delivered content. scripts/measure-read-budget.mjs makes the residual checkable and --self-check makes the script itself checkable: over 2197 tracked sources the fs arm delivers 1.093x the lines and 1.001x the characters; among the 570 files where a budget binds, the extremes are 1.398x/1.168x and 0.837x/0.720x.

Two things a write-up must carry that this branch cannot fix. The fs arm receives 764 characters of tool-contributed system-prompt guidance where the other two receive none, and two of its three sections reference an fs-observation-policy no arm mounts. And the treatment is the whole tool family plus that guidance, not "the edit contract" — the arm names are shorter than the thing being varied.

experiments/.../edit-contracts.json declares repetitions: 1, so 89 tasks x 3 subjects is 267 cells. Cost, not power, chose that. McNemar at one repetition is weak for the effect sizes this is likely to see: on ~20% discordant pairs a true 5pp difference is about 2 sigma before any correction for three pairwise comparisons, so a write-up has to report discordant counts and intervals rather than three success rates.

Stacked on #2971.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes — described under Summary above
  • No

The harness ships no patch tool, and an Eval arm that gives the model
Codex's patch language needs one composed the same way its other editors
are: `inject: ['tools', 'fs']`, one `ctx.tools.register(defineTool(...))`,
and every file operation through `ctx.fs`. Reaching around that seam would
put a second filesystem authority in one arm.
Applying a single file's hunks — context matching with the fuzz ladder — is
the part that is hard to get right, so it is not written here. It is the
OpenAI Agents SDK's `applyDiff`, vendored rather than depended on:
`@openai/agents-core` unpacks to 9 MB and pulls the whole `openai` SDK into
a tree that is fingerprinted file by file, while the function itself has no
imports at all. The upstream test suite is vendored with it, which is what
turns a mechanical tsc emit from an assertion into a checked fact; its two
import lines are the only edit. Biome's formatter now skips
`packages/eval/harbor/**/vendor/**` for the reason it already skips the
licence artifacts: a rewritten vendor file is no longer the upstream file.
What is written here is the envelope parser and the tool. The grammar is
transcribed from Codex's lark definition, and the model-facing description
is Codex's own instructions text. Two deviations are deliberate and are
stated to the model rather than left to be discovered by failure:
- No `*** Delete File:` and no `*** Move to:`. `ctx.fs` has no delete or
rename primitive and neither does any other harness file tool, so in
every arm the model removes and renames through bash.
- A patch either applies completely or changes nothing. `applyDiff` is
pure, so every file's new content is computed before the first write.
The contracts this will be compared against touch one file per call and
cannot half-apply; a partly-patched tree would be a difference in
failure modes rather than in edit format.
Sources and licences are recorded in the plugins' NOTICE. The three test
files run from the repository through `test:dist`, against the same
`@deepseek-ai/dsh-tools` and `@deepseek-ai/dsh-fs` the toolchain installs —
added as exact-pinned dev dependencies, which the lockfile takes as pure
additions with no existing resolution changed.
No arm composes this tool yet.
Generated-by: Claude Code
Terminal-Bench results attribute a score to a framework, which leaves the
edit contract inside the framework and unmeasured. Comparing Maka's
`edit`/`write` against the harness's `str_replace_editor` cannot separate
them either: that comparison moves the framework and the contract at once.
So this holds everything else and moves only the contract. Three arms run
the same one-shot CLI, the same toolchain and fingerprint, the same
`dsh-fs-local` provider under every editor, and byte-identical
`package.json` and `cordis.yml`:
- `deepseek-harness` keeps `str_replace_editor` — one tool, four
commands, unique-literal match, all guidance in the tool description.
- `deepseek-harness-fs` takes the harness's own `read`/`write`/`edit` —
three tools, snake_case arguments, three prompt sections of their own.
- `deepseek-harness-apply-patch` takes the V4A tool added previously —
one envelope, changes located by surrounding context.
The variable is the whole contract, not one dimension of it. Tool count,
argument names, path conventions, where the guidance lives, and the
read-before-edit policy move together because each family ships that way.
`dsh-fs-observation-policy` is mounted for the `fs` arm alone for that
reason: its tools' own guidance text tells the model the policy is in
force, so omitting it would leave the prompt describing a rule the
deployment does not enforce, while `apply_patch` has no such concept and no
read tool to satisfy one with.
Three compositions in three files can drift apart while still producing
numbers, and the numbers would no longer be about the contract. So the
control is asserted rather than intended: the new test fails when the
compositions differ by any row outside their editors, when the model,
reasoning effort, sandbox mode, persona or either Bash deadline drifts in
one of them, when the arms stop sharing one toolchain identity object, or
when the experiment launches them with anything but a different profile
argument. The arms also share one preparer, so a change to how the harness
is launched cannot reach one and miss another, and one identity constant,
so a re-pinned fingerprint cannot be applied to two arms out of three.
The toolchain build now installs the plugin tree beside the harness's
`node_modules` — a composition copied under `$DSH_HOME` cannot resolve a
plugin by package name from there — and fails if a declared plugin did not
land. Its dependencies are unchanged: every package the new arms compose
was already in the reviewed lockfile.
Re-pinning the fingerprint needs a `linux/amd64` container build and is not
done here, so the arms are not yet runnable end to end.
Generated-by: Claude Code
The toolchain now carries the `apply_patch` plugin beside the harness's
`node_modules`, so its checksum manifest changed and the pin has to follow.
Rebuilt with the same pinned `linux/amd64` image and the same reviewed
lockfile; no dependency moved.
Verified against the rebuilt tree rather than assumed. `dsh --dump-config`
composes all three arm profiles successfully, and the only differences
between the three composed trees are the editor rows and the `cwd` the two
new arms give the filesystem backend — nothing else diverged. Loading the
plugin from inside the toolchain resolves `@deepseek-ai/dsh-tools`,
`dsh-fs`, `dsh-sandbox` and `schemastery` through the upward walk into
`lib/dsh/node_modules`, registers `apply_patch` with the expected schema,
and carries the patch instructions without the operations the tool does not
implement. `dsh-tool-fs` and `dsh-fs-observation-policy` resolve too.
Generated-by: Claude Code
@coderabbitai

coderabbitaiBot commented Aug 15, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1357f048-8ea6-474e-8887-d512c43407ac

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

The tool shipped without them, refusing both with a pointer to the shell.
The stated reason was that `ctx.fs` has no delete or rename primitive, so
reaching them through `node:fs` would put a second filesystem authority in
the one arm being measured. That reason does not survive contact with the
deployment it was written for: this arm mounts `dsh-sandbox-local` at
`danger-full-access`, no observation policy is loaded, and the model's own
bash can already remove any file in the container. The authority being
protected was not enforcing anything.
The cost was real, and it was the point of the arm. `apply_patch` exists
here to measure Codex's contract; a patch tool missing a third of its
grammar is a different contract, and a model trained on the real one pays
for the difference in wasted turns. It also broke this PR's own rule —
every other arm composes what its tool family ships, which is exactly why
the `fs` arm mounts the observation policy. Two standards in one change.
So both operations are implemented, against `ctx.fs.processPath(target)` —
the provider's own answer to where a file is for something outside it to
open, and for `dsh-fs-local` the local absolute path. `ctx.fs` stays the
only thing that resolves paths; only the two syscalls it does not offer
happen outside it. Semantics follow Codex: a rename writes the destination
and then removes the source, overwrites an existing destination rather than
refusing, and creates missing parents.
Where the original concern is real, it is enforced instead of assumed: under
a provider that confines, `processPath` would be the way around the sandbox,
so delete and rename refuse up front. `sandboxMode` is what decides, so the
refusal fires exactly when a provider actually confines.
The parser already implemented the whole grammar, so no change there. The
tool description loses its "not part of this tool" paragraph and returns to
Codex's text, now with one removal rather than three: the trailing shell
invocation example, which is false for a function tool.
Tests move from an in-memory store to a real temporary directory, since
these two operations are syscalls an in-memory store cannot observe.
Red-checked: stubbing out the removal fails five behaviour tests, and
disabling the confinement guard fails the two that cover it.
Generated-by: Claude Code
The plugin gained its delete and rename paths, so the file it contributes to
the toolchain changed and the checksum manifest with it. Rebuilt from the
same pinned image and the same lockfile; no dependency moved.
Verified against the rebuilt tree: all three arm profiles still compose
under `dsh --dump-config`, and loading the plugin from inside the toolchain
registers `apply_patch` carrying the full grammar — `*** Delete File:`,
`*** Move to:`, and both productions — with the shell-invocation example
still absent.
Generated-by: Claude Code
Independent review of this arm found five places where the tool behaved
differently from the reference it exists to measure. The asymmetry matters
more than the count: a tool stricter than Codex costs its arm turns the
reference never spends, and a tool safer than Codex wins its arm recoveries
the reference never gets. Either way the benchmark reads the difference as
an effect of the edit contract.
`*** Add File:` did not terminate the created file with a newline, because
the vendored applier joins its added lines while Codex appends `\n` after
each one. Every file this arm created differed from Codex's byte for byte,
and a test pinned the wrong result. Each added line is now terminated, and
an empty create still writes nothing, as `String::new()` does upstream.
The envelope was planned against one snapshot and written afterwards, so a
failed hunk left the tree untouched. That is the nicer property and it was
still wrong: two sections naming one path made the second fail against a
version the first had replaced, and a delete followed by an update removed
the file and then failed, leaving neither. Operations now apply in order,
each reading what the previous one wrote.
`*** Add File:` over an existing file was refused with an invented
`FS_ALREADY_EXISTS`; Codex records what it overwrote and writes. A bare
`*** Move to:` with no hunks was accepted; Codex rejects an empty chunk list
before it reads the rename. A leading newline, a trailing space on the
opening marker, and an indented envelope were all refused; Codex trims the
patch and each marker line, while comparing only `trim_end()` inside an
update body so an indented context line stays content. Content after
`*** End Patch` was silently dropped, so two envelopes in one call reported
success over operations that never ran.
Three defects in how it reached the filesystem, none of which fire under the
arm's own non-confining provider:
- `mkdir` ran on every write through `processPath`, the same way out of a
sandbox that delete and rename refuse for. A create outside a confining
workspace had its write denied and still left the directory behind.
- Delete and rename used the provider's realpath-derived target key, so
deleting a symlink destroyed the file it pointed at and left the link
dangling. They now use the lexical path, which is what Codex removes.
- Hunk failures reached the model as a bare `Invalid Context 0`, naming
neither the file nor the section, and two error codes were outside
`FsErrorCode`'s closed union.
The description's provenance note claimed one modification from upstream
where there are four, and NOTICE claimed the description dropped the delete
and move operations — true when written, false once they were implemented,
and wrong in the direction that matters for a modification notice.
84 plugin tests pass. Each divergence is pinned by a test naming the
upstream file and line it follows.
Generated-by: Claude Code
Review found two differences between the arms that were not the edit
contract, and a control test that could not have caught either.
Only the `fs` arm mounted `dsh-fs-observation-policy`, justified in the
profile by that family's guidance mentioning it. Checking how the harness
ships does not support this: of the four presets in `@deepseek-ai/dsh`,
three mount `dsh-tool-fs` and none mounts the policy, which is not even a
direct dependency; and the policy's own documentation covers the baseline's
editor too. It is substrate, not part of an editor. Mounting it in one arm
gave that arm two failure modes the others cannot have — an edit without a
prior read, and a file the model's own shell touched in between — on a
benchmark where shell and editor are used together constantly. No arm
mounts it now. The apply-patch profile's stated reason for omitting it was
also wrong, and is corrected rather than quietly dropped: that arm emits
`fs/observed` with the file's version before it writes, so the policy would
have allowed the write.
The two new arms configured `fs-local` with a `cwd` expression the baseline
did not have. It could not change anything — the provider defaults `cwd` to
`process.cwd()`, both tools pass the session's own cwd, and `DSH_CWD` is
never set — but an unreachable setting is still a difference a reviewer has
to rule out.
Read budgets are now matched. The baseline pins its editor to 16000 output
characters while `dsh-tool-fs` shipped a 51200-byte default, a 3.2x window
on the same task. How much of a file one call returns is not part of the
edit contract, so `readMaxBytes` is set to match. The remaining read-side
asymmetry, that the patch arm has no read tool at all, is contract-inherent
and stays.
The control test read each `- id:` row with the `name:` on the line below
and compared those strings, leaving every `config:` block unchecked.
Narrowing one arm's context window, halving an editor's output budget,
rewriting the shared bash description, reordering the rows, or overriding
the patch tool's own description all kept it green. It now parses the YAML
and compares whole entries, asserts composition order, and pins each arm's
editor row by value — the treatment was the one thing no assertion covered.
Verified by making each of those six edits and watching it fail.
The arm-to-directory table moves to `toolchain-verification.ts`, beside the
identity table, and the subject and the test both read it. The test used to
restate it, so an arm pointed at the wrong profile would have been checked
against the composition it was meant to run rather than the one it runs.
The toolchain fingerprint is re-pinned for the plugin changes in the parent
commit. Verified in a `linux/amd64` container: all three profiles compose,
the composed trees differ only in the editor row, and the plugin loads from
inside the toolchain and registers `apply_patch` with the full grammar.
Generated-by: Claude Code
The apply_patch arm applied one file's hunks with the OpenAI Agents SDK's
`applyDiff`, vendored on the assumption that one V4A implementation is like
another. It is not. Running the released `codex` binary over the same trees
shows four disagreements, each of which moves this arm's score: a context-free
`+` hunk lands at the `@@` anchor rather than at end of file, an updated file
keeps whatever trailing newline it had rather than gaining one, CRLF survives,
and near-miss context is refused where Codex's four matching passes accept it.
Replace it with a port of Codex's own applier and parser. The parser now reads
hunks into chunks instead of passing section bodies through, so a syntax error
in the third section is found before the first is written, and the tool no
longer needs a second project's operation shape.
Restore verify-then-write. An earlier round changed the tool to apply
operations sequentially, matching the standalone `apply_patch` binary. That
binary is not what a model calls: the function-tool handler and the intercepted
shell call both go through `try_verify_apply_patch_args`, which collects hunks
into a map keyed by resolved path, refuses a duplicate key, and computes every
change against the pre-patch tree. So an envelope naming one path twice is
refused, and a hunk whose context the model guessed at costs nothing.
Fidelity is now measured rather than argued. `codex-oracle.mjs` runs the real
binary over a temporary tree for each of 72 cases and records what it printed
and what the tree became; `codex-fixtures.test.mjs` replays every case through
the registered tool and compares. The two cases where the function tool
deliberately differs from the standalone binary are named there with what it
does instead. A fuzzer over 1800 random envelopes found no further divergence.
Also tighten the arm-control test, which had three gaps: it skipped composition
steps that override an already-composed entry, so an override of the patch
tool's description was invisible; it named the profile files to compare instead
of reading them; and it exempted the first subject's profile argument while
comparing only the argument vector, not the whole subject definition.
Toolchain fingerprint re-pinned. Test-only files move under `__tests__/` so the
build prunes them by directory rather than by filename.
Generated-by: Claude Code
`readMaxBytes: 16000` was justified as making "how much of a file one call
returns" equal across the two read-capable arms. It does not. The baseline
editor spends eight characters a line on line numbers plus a header; the fs
tool emits neither, so measured over this repository's own sources it returns
10% more lines for the same budget and 22% on the longest files.
The budget is still the right one, for a reason the note did not give: what
should be equal is what a read costs the context window, and the numbering the
baseline adds is part of its contract that the model pays tokens for either
way. Equalising delivered content would mean picking a budget from an assumed
average line length.
Record the measurement, and record the one genuine mismatch that remains — the
budgets are counted in UTF-16 code units against UTF-8 bytes, equal on ASCII
and not otherwise.
Generated-by: Claude Code
The fixtures pin the cases someone thought to write down. This searches for the
rest: it generates envelopes from random line soup, applies each with the
released binary and with the registered tool, and reports every input the two
disagree on. 2100 envelopes across seven seeds currently diverge on nothing.
Red-checked against three deliberate regressions — placing a context-free `+`
hunk at the `@@` anchor, dropping the trailing-newline normalisation, and
removing the punctuation-folding match pass. The third went unnoticed through
400 envelopes at first, because context lines drawn verbatim from the file only
ever exercise the exact-match pass; the generator now half-remembers a line the
way a model does, and catches it.
Envelopes name each path at most once, and a refusal is compared as a refusal
without comparing the tree, because those are exactly where the standalone
binary and the function tool are supposed to differ.
Generated-by: Claude Code
The function-tool handler verifies an envelope and then throws the result
away: `ApplyPatchRuntime::run` hands `req.action.patch` — the raw text the
model sent — back to `apply_patch_with_mode`, which re-parses it and applies
it sequentially against the live filesystem. The verified `changes` map is
carried for approval and telemetry only.
This tool verified and then wrote the plan it had just computed. That is not
a cheaper version of the same contract, and the reference binary shows the
difference in two cases now covered by tests: a rename onto a path a later
section also updates leaves that section failing against the renamed file,
and `*** Move to:` naming its own source writes the destination and then
unlinks it, deleting the file while reporting success. Both were previously
refused with an invented "multiple operations target" error, because the
verification map also claimed the move destination — upstream keys only on
`hunk.resolve_path()` and carries `move_path` as a value.
Also in the write path: `fs/edit-intent` yields `{version}` with no `kind`,
so passing it straight to `writeText` matched neither guard and silently
wrote unconditionally; the abort signal was not checked between operations;
and `rm` ran with `force: true`, reporting `D <path>` for a file that had
already gone.
In the applier: each fuzzy-matching pass now projects both sides once
instead of recomputing the punctuation fold per candidate position, and
replacements are rebuilt in one forward pass rather than spliced, which
threw a RangeError once a hunk added more lines than the argument limit.
The model-facing description is no longer transcribed into this file. It is
read from a byte-for-byte copy of the upstream artifact it came from, with
three named substitutions applied over it, so the claim that it differs in
exactly those places is checkable rather than asserted in a comment. Copying
the file rather than retyping it also removed a fourth difference that had
no reason to exist: upstream's typographic punctuation is now preserved.
Its recorded provenance was stale — the file it named no longer exists at
the pinned commit — and now points at the artifact that does.
One parameter description said the input was "Not JSON-wrapped". It is: the
harness registers JSON function tools and nothing else.
Generated-by: Claude Code
Upstream ships codex-rs/apply-patch/tests/fixtures/scenarios as, in its own
README's words, an end-to-end suite "meant to be easily portable to other
languages or platforms". This is the port: the 25 scenario directories are
copied verbatim from the pinned commit and replayed through the registered
tool, compared the way upstream compares them — copy `input/`, apply
`patch.txt`, ignore the exit status, and require the resulting tree to equal
`expected/` exactly.
It is the only check here with the standing to contradict the code. The
oracle fixtures run the reference binary, but over inputs this repository
chose; the fuzzer perturbs inputs this repository chose how to perturb.
These were chosen by upstream, before and independently of this tool.
22 of 25 match outright. Three do not, and each divergence is recorded with
its cause and pinned to bytes the reference binary produces:
- 015 specifies the standalone binary, which has no verification pass, so an
add preceding a failing update survives. The function tool writes nothing.
- 023 and 024 are specified under CODEX_APPLY_PATCH_PRESERVE_LINE_ENDINGS,
a feature that is `Stage::UnderDevelopment, default_enabled: false`, so a
shipped Codex and this arm both run the `NormalizeToLf` default.
Both divergence lists — here and in the oracle fixtures — now assert that
each entry still diverges, so an excuse cannot outlive the thing it excused.
Generated-by: Claude Code
The treatment in this arm is the tool name, the parameter it takes, and the
description text. None of the three had any coverage, so a build that
registered the tool under another name, dropped the parameter, or shipped a
truncated description would have run a cohort and produced numbers.
The description claim is the one that had to be argued rather than shown:
"three differences from upstream and no others" was a comment. It is now
checked at both ends. The vendored instructions file is pinned by digest,
with the `git show ... | sed -n` that reproduces it from the source recorded
beside the constant, and the built text is recomputed here by a different
mechanism than the code uses and required to match. Each adaptation must
apply exactly once, and no mention of the shell delivery may survive.
Red-checked against four sabotages — appending a line to the description,
renaming the tool, editing one byte of the vendored text, and neutering an
adaptation. Each is caught.
Generated-by: Claude Code
Two differences at the outermost layer of the patch, both pinned by new
oracle fixtures generated from the reference binary.
`parse_patch` selects its mode from `PARSE_IN_STRICT_MODE`, which is `false`:
upstream gave up on gating leniency by model, so every call site including
the function tool unwraps a heredoc-wrapped envelope. A model asked to send
a patch may well send the `<<'EOF' ... EOF` form it was shown, and Codex
applies it where this refused it. The strict boundaries are still tried
first, so an envelope whose own first line reads `<<EOF` is never stripped.
Rust's `str::trim` cuts the Unicode `White_Space` property; JavaScript's also
cuts U+FEFF and does not cut U+0085. Only U+FEFF can realistically occur — a
byte-order mark carried into a JSON string argument — and it made this tool
accept a patch Codex refuses, which is the wrong direction for an arm being
compared against the contract it imitates.
Red-checked: restoring `String.prototype.trim` fails the byte-order-mark
fixture, and disabling the heredoc strip fails both heredoc fixtures.
Generated-by: Claude Code
The comment justifying `readMaxBytes: 16000` said `str_replace_editor`
spends eight characters a line on line numbers and a header while "this tool
emits neither". Both tools number their lines, and this one wraps the result
in a `<path>/<type>/<content>` envelope with a footer.
The setting still holds something equal, but not what was claimed. The
baseline truncates the fully rendered string; this tool charges the budget
against raw line bytes plus separators and adds the gutter, the envelope and
the footer afterwards, outside it. Measured across this repository's 1480
TS/JS/Python sources at 16000, the arm delivers 1.105x the lines and emits
1.027x the characters, worst file 1.34x and 1.19x — a residual in this arm's
favour, now recorded with the mechanism that produces it.
The three profiles also claimed to have been "verified request-for-request"
against the upstream composition. That was a manual comparison made once
during development; nothing here reproduces it, and the headers now say so.
`MAKA_EVAL_DSH_PLUGINS` was exported into all three arms and read by one, so
the two controls carried an environment difference for a mechanism they do
not use. It is now set from the copied composition itself, which keeps the
condition and the file that depends on it from drifting apart.
Generated-by: Claude Code
`apply_patch` in Codex is a freeform tool: the API is sent a short description
and a Lark grammar the decoder is constrained to, so a model on that path
cannot emit a malformed envelope. The harness registers JSON function tools
and has no grammar seam, so this arm is the unconstrained case. That is not a
detail of the port — it is a cost charged to this arm and to no other, and it
now leads the arm's description instead of being absent from it.
Three further divergences are named with their causes: the function-tool path
against the standalone binary's sequential apply, `NormalizeToLf` against a
preserve-line-endings feature that ships disabled, and diagnostics more
specific than Codex's single `Invalid patch:` line, which is the one place
this arm may be easier on a model than the reference.
The README also described a verify-then-write-the-plan design the tool no
longer has, quoted the old fixture count, and repeated the read-budget
mechanism corrected in the previous commit.
The NOTICE opened by saying nothing was carried verbatim as a file; two
things now are. Its description provenance named a file that no longer exists
at the ported commit and a fourth modification that was an artefact of
retyping rather than copying, its fixture path was stale, its account of how
`PreserveLineEndings` is selected covered only the standalone binary, and its
list of ported functions omitted the ones that decide apply order.
The arm-control test read a composition step as an insertion whenever it had
an `insert` key, so `{insert, remove}` would have passed with the removal
unread, and it filtered directories out of each profile's file list rather
than refusing them. It also now names what it does not cover.
Generated-by: Claude Code
`dsh-fs-local` creates a write's parent itself, in `writeFileAtomic`, and it
is the provider every arm mounts. The `ensureParent` here ran `mkdir` on a
`processPath` — the same way around a confining sandbox that delete and
rename refuse for — and existed only because the test harness's provider was
weaker than the real one and failed with ENOENT without it.
That is the more useful half of this change: a fake that cannot do what the
real thing does will not merely miss bugs, it will make production code grow
to cover for it. The fake now creates the parent the way `writeFileAtomic`
does, and the tool has no call that creates a directory at all.
The test that pinned the old behaviour was passing for the wrong reason — it
asserted the tool left no directory behind, and what actually happened was
the fake's write failing before anything could be created.
Generated-by: Claude Code
…sses
`deriveNewContents` reported a context miss as a plain `Error`, and the
caller rewrapped anything it threw as `FS_EDIT_NOT_FOUND` — so a bug in the
applier would have reached the model as "the lines you named are not in the
file", which is advice to rewrite a correct patch. The context miss is now
its own type and the only thing translated; a comment already claimed this
and the code did not.
Two oracle fixtures pin the order of the four matching passes against each
other. The existing cases pinned the exact pass ahead of the loose ones;
between the loose ones, removing a pass does not fail a match, it silently
moves it to a different line, and nothing caught that. Each of the four now
fails at least one fixture when removed.
The line-ending note in codex-apply.mjs described only the standalone
binary's environment variable, not the feature flag the function tool reads.
The toolchain is rebuilt and its fingerprint re-pinned: the model-facing
instructions now ship as a file, so they are covered by `checksums.sha256`
rather than living only inside a JavaScript template. Verified inside the
pinned linux/amd64 container that the built tree registers one tool named
`apply_patch` with one required `input` string and a 2903-byte description
carrying no mention of the shell delivery.
Generated-by: Claude Code
Differential fuzzing against the reference binary found a regression this
branch introduced. `applyReplacements` was rewritten to build the file
forward in one pass, on the reasoning that the replacements are sorted and
disjoint. They are sorted; they are not always disjoint.
`computeReplacements` places a context-free insertion at the end of the file
without consulting the search position — deliberately, because that is what
`insertion_idx` does upstream — so it can land inside a range a later chunk
replaces. Applying descending, the replacement swallows it; applying forward,
the two are treated as separate edits and the inserted line survives into a
file the reference never produces.
Restored to splicing in descending order, with the replacement segment going
in in bounded batches so a hunk larger than the engine's argument limit still
does not throw. The case is now an oracle fixture, so it is caught without a
`codex` on PATH; red-checked by reinstating the forward rebuild.
2300 further envelopes across four seeds diverge on nothing. Toolchain
rebuilt and re-pinned.
Generated-by: Claude Code
An adversarial review pass found nine behaviours that were wrong or untested
in the V4A tool, each verified against the reference binary or upstream source
before being changed. All nine are now covered: sabotaging any one of them
fails the suite, where before five of them left it green.
The matcher used JavaScript's `trim` where upstream uses Rust's. The two
disagree on U+FEFF and U+0085, so a file whose first line carried a byte-order
mark matched a pattern without one and was written back with the mark deleted,
and a line terminated by NEL was refused where the reference applies it. The
charset the parser already spelled out moves to rust-trim.mjs and both use it.
The verify pass now does what `try_verify_apply_patch_args` does: nothing at
all for `*** Add File:`, and a read for `*** Delete File:` as well as for an
update. Statting a delete target instead applied a delete the reference
refuses, since `read_file_text` maps invalid UTF-8 to `InvalidData` and fails
the whole envelope. It also stops emitting `fs/observed {present}` off a bare
stat, which told an observation policy a file had been seen that nothing read.
The duplicate-path key is lexical, as upstream's `PathUri::join` is, not the
provider's realpath-derived target key. An envelope naming both a symlink and
its target was refused here; the binary applies both sections in order.
A move destination no longer runs the `fs/write-intent` waterfall and discards
the answer, which turned a decider's refusal to clobber into a silent
overwrite. A failed unlink and an abort between passes now arrive as `FsError`
with a routable code and the path the model wrote, not a raw errno and a
resolved host path.
The test provider was weaker than `dsh-fs-local` in four ways that made the
above invisible: it decoded lossily where the real one raises `FS_NOT_TEXT`,
called every non-directory a file, took three parameters so the signal and
sandbox policy could not be observed, and never returned a decider's answer.
Two oracle cases are added for the trim charset and one for a trailing U+FEFF,
which is what separates the three loose matching passes from each other; the
BOM-in-content case is recorded as a divergence with its true cause, which is
that the shared provider strips a leading mark before this tool sees the file.
Generated-by: Claude Code
The import landed the 25 scenario directories, their README and their
.gitattributes twice: once at `__tests__/upstream/` and once, byte-identically,
at `__tests__/upstream/scenarios/`. Only the nested copy is read.
Generated-by: Claude Code
…arms
The three arms are meant to differ only in the edit contract. Three settings
outside that contract were not held.
`readMaxLineLength` was left at its shipped 2000 while only `readMaxBytes` was
matched to the baseline. That is a cap the baseline has no counterpart to: the
fs arm could never see the 2001st character of any line, at any offset, while
`str_replace_editor` truncates once over the whole rendered view and can spend
its entire budget on one line. A minified bundle or a single-line payload is
where that lands, and it lands against one arm only. It is raised to the
budget, where it can no longer bind before `readMaxBytes` does.
`streamIdleTimeoutMs` was omitted from all three profiles, leaving the
adapter's 300000 ms default in force — five minutes between two streamed
tokens, behind `reasoningEffort: max` and a 65-minute bash deadline. Upstream's
own jsonrpc-agent composition sets 172800000, which is what the arms now carry;
a turn killed by that timeout would have been scored as a failure of whichever
contract was running.
`maxConcurrentTaskGroups` was 64. A group holds one cell per subject and starts
them together, so three arms made 192 concurrent trials where every other spec
here lands on 128. That does not bias one arm against another, but it puts this
run's `deepseek-harness` score under a load no single-arm run shares. It is now
43, and the test asserts the arithmetic rather than the number.
The read-budget residual quoted in the profile and the README had no
reproducible source and reported only the tail that favoured the fs arm.
`scripts/measure-read-budget.mjs` reimplements both renderers from the shipped
code and reports both tails; the figures are replaced with what it prints.
Generated-by: Claude Code
A claims audit checked every factual assertion in the README, the NOTICE and
the profiles against upstream source at the ported commit and against the
shipped harness packages. Fourteen did not hold.
The largest is provenance. `prompt_with_apply_patch_instructions.md` is not in
any shipped Codex system prompt at the ported commit: it is referenced only
from a test whose four model cases all declare
`expects_apply_patch_description: false`, and none of the eight instruction
templates in models.json carries the section. The file is upstream's own
authored description of the format and it is retired, which is a better reason
to use it than the one previously given, but the prose said Codex sends it.
The freeform description is 108 characters, not 105. The oracle runs 82 cases,
not 76. The fuzz figure is 2300 across four seeds, not 2100. Two of the named
divergences were recorded in the tests, not three, and there are now four with
the byte-order mark the provider strips. `verifyToolchainDirectory` covers ten
registered profiles, not eight. `checksums.sha256` omits itself and
manifest.json. The toolchain build's `__tests__` prune is scoped to the plugins
it copies. The build script runs from the repository root. `ctx.fs` offers
twelve methods, not three. The 0.147.0 delta includes a second behavioural
difference — the duplicate-path check does not exist at that tag — and a larger
restructure than the three items named. The profiles record three deviations
from upstream, not one.
Also stated where it was missing: the arm's lack of a read tool is Codex's
contract rather than a handicap, since Codex registers no file-read tool
either and every arm has the same bash.
Generated-by: Claude Code
A second adversarial round found that the previous round's fix traded one
divergence for another, and that twelve mutations of this tool still left the
whole suite green.
`ctx.fs.readText` is not `read_file_text`. Upstream reads bytes and calls
`String::from_utf8`, so every valid UTF-8 sequence is text; `dsh-fs-local`
adds two rules on top, and both are model-visible. It rejects a NUL in the
first 8192 bytes as binary — but NUL is valid UTF-8, and the reference patches
and deletes straight through it, verified against the binary — so making the
verify pass read delete targets took `*** Delete File:` on such a file with
it. And it decodes with `ignoreBOM` left false, which strips a leading
byte-order mark, so a patch whose context omits the mark applied here, was
refused there, and wrote the file back without the mark. Reading through
`ctx.fs.readBytes` and decoding here is exactly `String::from_utf8`; it fixes
both, and the byte-order-mark divergence is deleted rather than documented.
`*** End of File` as the first line of an update hunk was refused. Upstream
guards with `chunks.last().is_some_and(blank)`, which is false when there is
no chunk, so the marker is ignored; the binary applies the envelope.
Twelve mutations that the suite could not see, now each red-checked: five
dropped the turn signal from a provider call, which a fake that ignored the
signal could not notice; two removed or falsified the version on `fs/observed`;
two swapped a removal's announcement with the removal itself, which leaves the
event list identical and the trail a lie; one bypassed the create decider and
one renamed its event; one reduced the sandbox-denial remap to a no-op. The
fake now honours and records the signal on every call, records what the
filesystem actually held when each observation was published, records the
version `writeText` returned, and can be made to deny a write.
Twelve oracle cases added, generated from the binary: three for the read seam
and nine for shapes that separate this port from a plausible variant of it —
overlapping chunks, a backward `@@` anchor, an indented header after an add
body, a trailing mark on an End of File marker, header paths with a leading
space, a repeated `*** Move to:`, a heredoc close with trailing text, and the
End of File case above. Eleven passed unchanged, which is the point of adding
them; one was the defect.
`requireDirectFilesystem` now raises an `FsError` like every other refusal.
Generated-by: Claude Code
… meant to help
Raising `readMaxLineLength` from its shipped 2000 to the 16000 budget was
wrong, and worse than the asymmetry it was meant to close. `dsh-tool-fs`
truncates a long line and appends a `... (line truncated to N chars)` marker,
then charges the whole thing against `readMaxBytes`. Once the cap approaches
the budget the truncated line no longer fits at all: the window returns zero
lines, drops every line after it, and the footer invites the model to retry at
the offset it just used, so the rest of the file is unreachable. Measured
against the shipped functions, a 20000-character line followed by 200 short
ones delivers 201 lines at 2000 and 0 lines at 16000.
No value equalises the two budgets anyway — one counts UTF-16 code units and
the other UTF-8 bytes — so the cap goes back to the value the tool ships with
and the residual is measured instead of engineered away. At 2000 the direction
is also not the one the earlier reasoning assumed: on that same long-line file
the baseline spends its whole budget on the one line and delivers 1, while
this arm truncates it, says so, and reads on.
The guard test now asserts the invariant rather than the number, so any future
cap has to be one a truncated line can still fit under.
The measurement script had four transcription errors, all inflating the
character ratio: it dropped the baseline's 240-character truncation notice and
the fs arm's per-line marker, counted the baseline's prompt line as content,
and counted a phantom trailing line for the fs arm. It also silently ate the
first positional path when `--budget` was absent. Rather than fix the
transcription and hope, `--self-check <dir>` now lifts the real functions out
of an installed copy and asserts agreement, and the corpus figures are
reported alongside synthetic long-line cases that the repository's own sources
do not contain. Corrected: 2197 files, 1.093x lines, 1.001x characters, tails
reported as what they are — extremes among the 570 files where a budget binds.
`maxConcurrentTaskGroups` moves 43 to 42: three arms at 43 is 129 trials, and
the assertion had been written as `<= 129` to admit it.
Generated-by: Claude Code
The claim that upstream's `## apply_patch` prose is retired was too strong. Of
the eight instruction templates in `models-manager/models.json` at the ported
commit, seven carry no such section — but `gpt-5.2` carries a shorter one with
the same envelope, the same three headers and the same worked example. So the
format is still taught in prose upstream, and the vendored file is the fuller
version of that prose rather than something abandoned. The conclusion it was
supporting survives unchanged and for a cleaner reason: `gpt-5.2` gets the
prose *and* the grammar, so no shipped configuration is the one this arm is in.
Codex's handler set is much longer than the six entries named, and one of them
takes a filesystem path — `view_image`, for images. The load-bearing claim is
that no handler reads file *text*, which is what the prose now says.
The NOTICE gave the wrong cause for the duplicate-path divergence: the
recorded binary composes an envelope naming one path twice because the
standalone applier has no verification pass at either version, not because the
check is absent at rust-v0.147.0 — that is a function-tool difference.
Also: `invocation.rs:246-248` is the delete arm, not the add arm; the
`fs/edit-intent` shape citation named only one of the two declarations; the
end-of-file search has no fallback to the ordinary start position, contrary to
its comment; `__tests__/upstream/` kept a stray `.gitattributes` naming paths
one level down; and the build script's header credited `npm i` for a
non-reproducible fingerprint when it runs `npm ci` and the real cause is native
compilation.
The refusal under a confining provider is described as what it is: dead code
under `dsh-fs-local`, which never sets `sandboxMode` at any sandbox mode, not
something `danger-full-access` switches off.
Generated-by: Claude Code
…t reads it
The comment justifying the conditional export had the causation backwards. The
model's shell inherits this environment, so setting the variable in all three
arms is what makes them identical, and setting it in one arm alone is what
creates a difference the model can read with `env`. It names a directory and
starts nothing, so it is inert in the two arms whose compositions never mention
it.
The remaining environment asymmetry, `$HOME` carrying the profile name, is
recorded rather than removed: the arms need separate homes because each writes
its own composition under `$DSH_HOME/profiles`, and sharing one would have each
overwrite what the others booted from. So the experiment is not blind to its
subject, and that is now stated where the home is built.
Generated-by: Claude Code
The spec declared 42 task groups, or 126 concurrent trials, on the
strength of a 128-trial convention rather than a machine. The account's
CVM quota is 60 vCPU per zone, which caps the host at 56 vCPU and
256 GiB, and the only completed run of this suite put 24 trials on
32 vCPU / 64 GiB — 2.7 GiB per trial. 126 trials on that host is 2.0 GiB
each, close enough to a Terminal Bench build task's peak that an OOM
kill would land as an infra failure. 32 groups is 96 trials at the
density that is known to survive.
Generated-by: Claude Code
The arm named its plugin with `name: !!js process.env.MAKA_EVAL_DSH_PLUGINS
+ '/tool-apply-patch/index.mjs'`. The harness's loader evaluates that tag for
`disabled` alone and hands `name` the unevaluated node, so every trial of
that arm died at boot with `name.startsWith is not a function` before it
reached the model — on a rented host, at the first smoke task.
The row now names `./plugins/tool-apply-patch/index.mjs`, resolved against the
profile directory, and the subject plants `plugins` there as a link into the
toolchain. That keeps what the loader imports inside the fingerprinted
read-only tree, keeps the composition a plain string, and keeps the path
independent of the root the wrapper was given — which naming the mount point
outright would not. The link is planted for all three arms, on the same
reasoning that exported the variable for all three: the model's shell can read
$DSH_HOME, so planting it in one arm alone is the difference, not planting it
everywhere.
The test could not have caught this. It extended its own YAML schema to
construct `!!js <source>` as a string, which taught the assertion to accept a
form the harness cannot load. The tag is real elsewhere in these compositions —
`task: !!js ctx.headlessStartup.task` is upstream's — so the schema now
constructs it as the marker shape the harness's loader produces, and a new
assertion requires every entry name to be a string and every path-named plugin
to be one the toolchain build ships. Reverting the row fails both.
Generated-by: Claude Code
The wrapper counted the subject's stderr bytes and hashed them, then dropped
them. The relay writes `maka-subject.stderr.txt` from the wrapper's own
stderr, which is empty, so a failing arm's own account of what killed it
reached the attempt record as a byte count and a digest and nothing else.
Both failures this run has had were diagnosed by reproducing them by hand on
the host instead: a composition the harness could not load, and a subject that
exits after one model response. The second is still open.
The last 64 KiB now land in `/logs/agent/<profile>.stderr.txt`, which the
relay already collects, as a `stderr-tail` artifact beside the existing
`stderr` count. A tail because a subject that loops printing would otherwise
fill the trial's disk, and because a harness says what killed it on the way
out; the count and digest still describe the whole stream, so truncation is
visible.
Generated-by: Claude Code
Announcing it as an artifact pushed the frame past the relay's 2 KiB payload
cap, so every cell came back `external subject result transport failed`.
`/logs/agent` is collected into the trial directory regardless, so writing the
file is the whole job.
Generated-by: Claude Code
Every failure before the subject starts reached the operator as the string
`external subject setup failed`, because `safeFailure` sanitizes what the
published result frame may say and nothing recorded the error itself. The
process's own stderr is collected into the trial as
`maka-subject.stderr.txt`, which is where a subject's account of its own
failure belongs, so the error, its stack, and its cause chain go there.
Generated-by: Claude Code
This module does its work at the top level, above the function that reads the
subject's stderr, so a `const` declared beside that reader is still in its
temporal dead zone when the reader runs. Every cell died with
`ReferenceError: Cannot access 'STDERR_TAIL_BYTES' before initialization`
before the subject's first model response — reported as
`external subject setup failed`, and legible only because the commit before
this one started writing the error itself.
Generated-by: Claude Code
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(eval): compare three edit contracts on the DeepSeek Harness - #3067

Closed
Astro-Han wants to merge 33 commits into
feat/eval-deepseek-harness-subjectfrom
feat/eval-edit-contract-arms
Closed

feat(eval): compare three edit contracts on the DeepSeek Harness#3067
Astro-Han wants to merge 33 commits into
feat/eval-deepseek-harness-subjectfrom
feat/eval-edit-contract-arms

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Summary

A Terminal-Bench score is attributed to a framework, which leaves the edit contract inside the framework and unmeasured. Comparing Maka's edit/write against the harness's str_replace_editor cannot separate them either — that comparison moves the framework and the contract at once.

These three arms hold everything else and move only the contract:

armcontract
deepseek-harnessstr_replace_editor — one tool, four commands, unique-literal match, all guidance in the tool description
deepseek-harness-fsthe harness's read/write/edit — three tools, snake_case, three prompt sections of their own
deepseek-harness-apply-patchV4A apply_patch — one envelope, changes located by surrounding context

Same one-shot CLI, same toolchain and fingerprint, same dsh-fs-local provider under every editor, byte-identical package.json and cordis.yml. The variable is the whole tool family and not one dimension of it: tool count, argument names, path conventions and where the guidance lives move together, because each family ships that way. A result is attributable to the family as shipped, not to patch syntax as such.

No arm mounts dsh-fs-observation-policy. It was mounted for the fs arm alone on the reasoning that its tools' guidance mentions the policy; of the four presets in @deepseek-ai/dsh, three mount dsh-tool-fs and none mounts the policy, and its own documentation covers str_replace_editor too. Mounting it in one arm gave that arm two failure modes the others cannot have.

Three compositions in three files can drift apart while still producing numbers, so the control is asserted rather than intended. edit-contract-arms.test.ts fails when the compositions differ by any row outside their editors, when they compose in a different order, when the model, context window, reasoning effort, stream idle timeout, sandbox mode, persona or either Bash deadline drifts in one, when the arms stop sharing one toolchain identity object, when the experiment launches them with anything but a different profile argument, or when arms multiplied by concurrency leaves the 128-trial convention.

The patch arm is ours, and that is the thing to review

The harness ships no patch tool, so plugins/tool-apply-patch/ is this repository's while the other two arms run their vendor's. Fidelity to Codex is therefore itself an experimental variable, and it is asymmetric: a tool stricter than the reference costs its arm turns the reference never spends, and a safer one wins recoveries the reference never gets.

One deviation cannot be removed.apply_patch in Codex is a freeform tool — the API is sent a 108-character description plus a Lark grammar the decoder is constrained to, so a model on that path cannot emit a malformed envelope. The harness registers JSON function tools and has no grammar seam. This arm is therefore the unconstrained case, and syntax errors it can make are errors Codex's decoder would have prevented. That cost is charged to this arm and to no other, and it leads the arm's description in the README.

What is not charged to it is the absence of a read tool. Codex registers no handler that reads file text either — its handler set is long, and the only entry taking a filesystem path to read is view_image, which takes images — so an arm that reads with cat is that contract, and every arm has the same bash at the same 16000-character cap.

What the model is told is upstream's own prose for exactly that case: the ## apply_patch section of prompt_with_apply_patch_instructions.md, carried verbatim as upstream-apply-patch-instructions.md with three substitutions applied over it. A test pins the file's digest, requires each substitution to apply exactly once, and requires the built description to differ from the copy nowhere else. That file is upstream's and it is not delivered from where it sits: at the ported commit it is referenced only from a test whose four model cases all declare expects_apply_patch_description: false. Seven of the eight instruction templates in models-manager/models.json carry no ## apply_patch section and leave the format to the grammar; the eighth, gpt-5.2, carries a shorter one with the same envelope and worked example. So the format is still taught in prose upstream and this is the fuller version of it. Either way no shipped configuration is the one this arm is in — gpt-5.2 gets prose and grammar — so the choice was between upstream's own text and text written here.

Three further divergences are known and each is pinned to bytes the reference produces:

  • the tool implements the function-tool path — verify the whole envelope, then re-apply it from the raw text — where the standalone binary applies sequentially with no verification;
  • it runs NormalizeToLf, the shipped default, where PreserveLineEndings is Stage::UnderDevelopment, default_enabled: false upstream;
  • its diagnostics differ from Codex's, which is the one place this arm may be easier on a model. This one is recorded in prose only; the other two are named with their causes in the test files, which also assert that each still diverges.

A third divergence was recorded here and has since been removed rather than documented: the provider's readText strips a leading byte-order mark and rejects a NUL byte, neither of which String::from_utf8 does. Reading through readBytes and decoding in the tool matches the reference exactly.

The grammar is implemented in full. *** Delete File: and *** Move to: are the only operations ctx.fs has no primitive for, so they run against ctx.fs.processPath(target); under a provider that confines, both refuse up front rather than escape. This arm mounts danger-full-access, where the refusal never fires.

Sources and licences are in the plugins' NOTICE. An earlier revision of this branch vendored the OpenAI Agents SDK's applyDiff as the hunk applier; it is a different V4A implementation, not Codex's, and nothing from it remains.

What the adversarial review rounds changed

Two rounds of four fresh-eye passes each — the port against the binary, the harness integration, the three-arm control, and an audit of every factual claim. The second round reviewed the first round's fixes, which is what caught the two entries below that begin "the previous fix". Every finding was re-verified against upstream source or the reference binary before being acted on.

Nine port defects, all now red-checked. The matcher used JavaScript's trim where upstream uses Rust's, so a file whose first line carried a byte-order mark matched a pattern without one and was written back with the mark deleted, and a line terminated by U+0085 was refused where the reference applies it. The verify pass stated a delete target instead of reading it, applying deletes of non-UTF-8 files that the reference refuses outright. It emitted fs/observed {present} off a bare stat, telling an observation policy a file had been seen that nothing read. The duplicate-path key was the provider's realpath-derived target key rather than upstream's lexical join, so an envelope naming both a symlink and its target was refused where the binary applies both. A move destination ran the fs/write-intent waterfall and discarded the answer, turning a decider's refusal to clobber into a silent overwrite. A failed unlink and an abort between passes escaped the seam's error taxonomy.

Five of those were invisible because the test provider was weaker than dsh-fs-local: it decoded lossily, called every non-directory a file, took three parameters so the signal and sandbox policy could not be observed, and never returned a decider's answer.

The previous fix read patch targets through the wrong seam.ctx.fs.readText is not read_file_text: upstream reads bytes and calls String::from_utf8, while dsh-fs-local additionally rejects a NUL in the first 8192 bytes and strips a leading byte-order mark. NUL is valid UTF-8 and the reference patches and deletes straight through it, so making the verify pass read delete targets — itself a correct fix — took *** Delete File: on such a file with it. Reading through readBytes and decoding in the tool is exactly String::from_utf8, and it removes the byte-order-mark divergence too. Separately, *** End of File as the first line of an update hunk was refused where upstream ignores it.

Twelve more mutations the suite could not see. Five dropped the turn signal from a provider call, which a fake that ignored the signal could not notice; two removed or falsified the version on fs/observed; two swapped a removal's announcement with the removal itself, which leaves the event list identical and the trail a lie; one bypassed the create decider, one renamed its event, one made the sandbox-denial remap a no-op. The fake now honours and records the signal on every call, records what the filesystem held when each observation was published, records the version writeText returned, and can be made to deny a write.

Three uncontrolled differences between the arms, of which one fix was itself wrong. streamIdleTimeoutMs was omitted from all three profiles, leaving a five-minute ceiling on the gap between two streamed tokens behind reasoningEffort: max and a 65-minute bash deadline; it now carries upstream's own 172800000. maxConcurrentTaskGroups: 64 made 192 concurrent trials where the saturating specs here land on 128; it is now 42.

readMaxLineLength was raised from its shipped 2000 to the budget to close a cap the baseline has no counterpart to, and that was worse than the asymmetry. The tool appends a ... (line truncated to N chars) marker and then charges the whole line against readMaxBytes, so a cap near the budget makes a long line not fit at all, drops every line after it, and returns a window no offset recovers — 201 lines at 2000 becomes 0 at 16000. No value equalises the two budgets anyway, one counting UTF-16 code units and the other UTF-8 bytes, so the cap is back where the tool ships it and the guard test asserts the invariant rather than the number.

Twenty-one false claims across the two rounds, listed in the two docs commits. The largest was the provenance one, twice: first that the vendored prose is what Codex sends, then that it is retired. The read-budget script had four transcription errors of its own, all inflating the character ratio, so it no longer asks to be trusted — --self-check <dir> lifts the real renderers out of an installed copy and asserts agreement, and the figures are 1.093x lines and 1.001x characters over 2197 files.

Verification

Lint, format and typecheck clean. 47 TypeScript and 200 plugin tests pass.

Fidelity is measured against the binary rather than argued from the Rust:

  • codex-oracle.mjs runs the released codex under argv0 apply_patch over a temporary tree for each of 94 cases and records what it printed and what the tree became; codex-fixtures.test.mjs replays every case through the registered tool and needs no codex to do it.
  • __tests__/upstream/scenarios/ carries Codex's own conformance suite verbatim — the 25 scenario directories upstream publishes as "meant to be easily portable to other languages or platforms" — and upstream-scenarios.test.mjs replays them upstream's way: copy the input tree, apply the patch, ignore the exit status, require the resulting tree to match exactly. 22 match outright; the three that do not are named divergences. These are the only inputs here this repository did not choose.
  • codex-fuzz.mjs generates envelopes from random line soup, including near-miss context, and runs both. It earned its keep on this branch: it caught a regression introduced here, where rebuilding the file forward let a context-free insertion survive inside a range a later chunk replaces. 2300 envelopes across four seeds diverge on nothing.

Red-checked, not just green:

  • Appending a line to the description, renaming the tool, editing one byte of the vendored instructions, or neutering a substitution each fails the contract-surface test.
  • Removing any one of the four context-matching passes fails at least one oracle fixture; restoring String.prototype.trim in the loose passes fails the trailing-U+FEFF fixture; disabling the heredoc strip fails both heredoc fixtures; matching the heredoc close with includes instead of endsWith fails the trailing-text fixture.
  • Each of the twenty-one port defects and unseen mutations above fails the suite when reintroduced. Seventeen of them did not before these two rounds.
  • Both divergence lists assert that each entry still diverges, so an excuse cannot outlive the thing it excused.

End-to-end against the rebuilt linux/amd64 toolchain: loading the plugin from inside the built tree registers exactly one tool named apply_patch, with one required input string and a 2903-byte description carrying no mention of the shell delivery. The instructions file ships, so it is covered by checksums.sha256 and the re-pinned fingerprint.

Not run: an actual benchmark cohort. No dependency moved.

Review focus

One claim this repository does not reproduce, and which should be read as a one-time manual check rather than a standing guarantee: the three profiles were compared against the upstream examples/jsonrpc-agent composition during development — tool names, tool schemas, system prompt and message sequence on the first outbound request — and found identical.

The read budget matched at 16000 equalises the budget's number, not delivered content. scripts/measure-read-budget.mjs makes the residual checkable and --self-check makes the script itself checkable: over 2197 tracked sources the fs arm delivers 1.093x the lines and 1.001x the characters; among the 570 files where a budget binds, the extremes are 1.398x/1.168x and 0.837x/0.720x.

Two things a write-up must carry that this branch cannot fix. The fs arm receives 764 characters of tool-contributed system-prompt guidance where the other two receive none, and two of its three sections reference an fs-observation-policy no arm mounts. And the treatment is the whole tool family plus that guidance, not "the edit contract" — the arm names are shorter than the thing being varied.

experiments/.../edit-contracts.json declares repetitions: 1, so 89 tasks x 3 subjects is 267 cells. Cost, not power, chose that. McNemar at one repetition is weak for the effect sizes this is likely to see: on ~20% discordant pairs a true 5pp difference is about 2 sigma before any correction for three pairwise comparisons, so a write-up has to report discordant counts and intervals rather than three success rates.

Stacked on #2971.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes — described under Summary above
  • No

The harness ships no patch tool, and an Eval arm that gives the model
Codex's patch language needs one composed the same way its other editors
are: `inject: ['tools', 'fs']`, one `ctx.tools.register(defineTool(...))`,
and every file operation through `ctx.fs`. Reaching around that seam would
put a second filesystem authority in one arm.
Applying a single file's hunks — context matching with the fuzz ladder — is
the part that is hard to get right, so it is not written here. It is the
OpenAI Agents SDK's `applyDiff`, vendored rather than depended on:
`@openai/agents-core` unpacks to 9 MB and pulls the whole `openai` SDK into
a tree that is fingerprinted file by file, while the function itself has no
imports at all. The upstream test suite is vendored with it, which is what
turns a mechanical tsc emit from an assertion into a checked fact; its two
import lines are the only edit. Biome's formatter now skips
`packages/eval/harbor/**/vendor/**` for the reason it already skips the
licence artifacts: a rewritten vendor file is no longer the upstream file.
What is written here is the envelope parser and the tool. The grammar is
transcribed from Codex's lark definition, and the model-facing description
is Codex's own instructions text. Two deviations are deliberate and are
stated to the model rather than left to be discovered by failure:
- No `*** Delete File:` and no `*** Move to:`. `ctx.fs` has no delete or
rename primitive and neither does any other harness file tool, so in
every arm the model removes and renames through bash.
- A patch either applies completely or changes nothing. `applyDiff` is
pure, so every file's new content is computed before the first write.
The contracts this will be compared against touch one file per call and
cannot half-apply; a partly-patched tree would be a difference in
failure modes rather than in edit format.
Sources and licences are recorded in the plugins' NOTICE. The three test
files run from the repository through `test:dist`, against the same
`@deepseek-ai/dsh-tools` and `@deepseek-ai/dsh-fs` the toolchain installs —
added as exact-pinned dev dependencies, which the lockfile takes as pure
additions with no existing resolution changed.
No arm composes this tool yet.
Generated-by: Claude Code
Terminal-Bench results attribute a score to a framework, which leaves the
edit contract inside the framework and unmeasured. Comparing Maka's
`edit`/`write` against the harness's `str_replace_editor` cannot separate
them either: that comparison moves the framework and the contract at once.
So this holds everything else and moves only the contract. Three arms run
the same one-shot CLI, the same toolchain and fingerprint, the same
`dsh-fs-local` provider under every editor, and byte-identical
`package.json` and `cordis.yml`:
- `deepseek-harness` keeps `str_replace_editor` — one tool, four
commands, unique-literal match, all guidance in the tool description.
- `deepseek-harness-fs` takes the harness's own `read`/`write`/`edit` —
three tools, snake_case arguments, three prompt sections of their own.
- `deepseek-harness-apply-patch` takes the V4A tool added previously —
one envelope, changes located by surrounding context.
The variable is the whole contract, not one dimension of it. Tool count,
argument names, path conventions, where the guidance lives, and the
read-before-edit policy move together because each family ships that way.
`dsh-fs-observation-policy` is mounted for the `fs` arm alone for that
reason: its tools' own guidance text tells the model the policy is in
force, so omitting it would leave the prompt describing a rule the
deployment does not enforce, while `apply_patch` has no such concept and no
read tool to satisfy one with.
Three compositions in three files can drift apart while still producing
numbers, and the numbers would no longer be about the contract. So the
control is asserted rather than intended: the new test fails when the
compositions differ by any row outside their editors, when the model,
reasoning effort, sandbox mode, persona or either Bash deadline drifts in
one of them, when the arms stop sharing one toolchain identity object, or
when the experiment launches them with anything but a different profile
argument. The arms also share one preparer, so a change to how the harness
is launched cannot reach one and miss another, and one identity constant,
so a re-pinned fingerprint cannot be applied to two arms out of three.
The toolchain build now installs the plugin tree beside the harness's
`node_modules` — a composition copied under `$DSH_HOME` cannot resolve a
plugin by package name from there — and fails if a declared plugin did not
land. Its dependencies are unchanged: every package the new arms compose
was already in the reviewed lockfile.
Re-pinning the fingerprint needs a `linux/amd64` container build and is not
done here, so the arms are not yet runnable end to end.
Generated-by: Claude Code
The toolchain now carries the `apply_patch` plugin beside the harness's
`node_modules`, so its checksum manifest changed and the pin has to follow.
Rebuilt with the same pinned `linux/amd64` image and the same reviewed
lockfile; no dependency moved.
Verified against the rebuilt tree rather than assumed. `dsh --dump-config`
composes all three arm profiles successfully, and the only differences
between the three composed trees are the editor rows and the `cwd` the two
new arms give the filesystem backend — nothing else diverged. Loading the
plugin from inside the toolchain resolves `@deepseek-ai/dsh-tools`,
`dsh-fs`, `dsh-sandbox` and `schemastery` through the upward walk into
`lib/dsh/node_modules`, registers `apply_patch` with the expected schema,
and carries the patch instructions without the operations the tool does not
implement. `dsh-tool-fs` and `dsh-fs-observation-policy` resolve too.
Generated-by: Claude Code
@coderabbitai

coderabbitaiBot commented Aug 15, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1357f048-8ea6-474e-8887-d512c43407ac

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

The tool shipped without them, refusing both with a pointer to the shell.
The stated reason was that `ctx.fs` has no delete or rename primitive, so
reaching them through `node:fs` would put a second filesystem authority in
the one arm being measured. That reason does not survive contact with the
deployment it was written for: this arm mounts `dsh-sandbox-local` at
`danger-full-access`, no observation policy is loaded, and the model's own
bash can already remove any file in the container. The authority being
protected was not enforcing anything.
The cost was real, and it was the point of the arm. `apply_patch` exists
here to measure Codex's contract; a patch tool missing a third of its
grammar is a different contract, and a model trained on the real one pays
for the difference in wasted turns. It also broke this PR's own rule —
every other arm composes what its tool family ships, which is exactly why
the `fs` arm mounts the observation policy. Two standards in one change.
So both operations are implemented, against `ctx.fs.processPath(target)` —
the provider's own answer to where a file is for something outside it to
open, and for `dsh-fs-local` the local absolute path. `ctx.fs` stays the
only thing that resolves paths; only the two syscalls it does not offer
happen outside it. Semantics follow Codex: a rename writes the destination
and then removes the source, overwrites an existing destination rather than
refusing, and creates missing parents.
Where the original concern is real, it is enforced instead of assumed: under
a provider that confines, `processPath` would be the way around the sandbox,
so delete and rename refuse up front. `sandboxMode` is what decides, so the
refusal fires exactly when a provider actually confines.
The parser already implemented the whole grammar, so no change there. The
tool description loses its "not part of this tool" paragraph and returns to
Codex's text, now with one removal rather than three: the trailing shell
invocation example, which is false for a function tool.
Tests move from an in-memory store to a real temporary directory, since
these two operations are syscalls an in-memory store cannot observe.
Red-checked: stubbing out the removal fails five behaviour tests, and
disabling the confinement guard fails the two that cover it.
Generated-by: Claude Code
The plugin gained its delete and rename paths, so the file it contributes to
the toolchain changed and the checksum manifest with it. Rebuilt from the
same pinned image and the same lockfile; no dependency moved.
Verified against the rebuilt tree: all three arm profiles still compose
under `dsh --dump-config`, and loading the plugin from inside the toolchain
registers `apply_patch` carrying the full grammar — `*** Delete File:`,
`*** Move to:`, and both productions — with the shell-invocation example
still absent.
Generated-by: Claude Code
Independent review of this arm found five places where the tool behaved
differently from the reference it exists to measure. The asymmetry matters
more than the count: a tool stricter than Codex costs its arm turns the
reference never spends, and a tool safer than Codex wins its arm recoveries
the reference never gets. Either way the benchmark reads the difference as
an effect of the edit contract.
`*** Add File:` did not terminate the created file with a newline, because
the vendored applier joins its added lines while Codex appends `\n` after
each one. Every file this arm created differed from Codex's byte for byte,
and a test pinned the wrong result. Each added line is now terminated, and
an empty create still writes nothing, as `String::new()` does upstream.
The envelope was planned against one snapshot and written afterwards, so a
failed hunk left the tree untouched. That is the nicer property and it was
still wrong: two sections naming one path made the second fail against a
version the first had replaced, and a delete followed by an update removed
the file and then failed, leaving neither. Operations now apply in order,
each reading what the previous one wrote.
`*** Add File:` over an existing file was refused with an invented
`FS_ALREADY_EXISTS`; Codex records what it overwrote and writes. A bare
`*** Move to:` with no hunks was accepted; Codex rejects an empty chunk list
before it reads the rename. A leading newline, a trailing space on the
opening marker, and an indented envelope were all refused; Codex trims the
patch and each marker line, while comparing only `trim_end()` inside an
update body so an indented context line stays content. Content after
`*** End Patch` was silently dropped, so two envelopes in one call reported
success over operations that never ran.
Three defects in how it reached the filesystem, none of which fire under the
arm's own non-confining provider:
- `mkdir` ran on every write through `processPath`, the same way out of a
sandbox that delete and rename refuse for. A create outside a confining
workspace had its write denied and still left the directory behind.
- Delete and rename used the provider's realpath-derived target key, so
deleting a symlink destroyed the file it pointed at and left the link
dangling. They now use the lexical path, which is what Codex removes.
- Hunk failures reached the model as a bare `Invalid Context 0`, naming
neither the file nor the section, and two error codes were outside
`FsErrorCode`'s closed union.
The description's provenance note claimed one modification from upstream
where there are four, and NOTICE claimed the description dropped the delete
and move operations — true when written, false once they were implemented,
and wrong in the direction that matters for a modification notice.
84 plugin tests pass. Each divergence is pinned by a test naming the
upstream file and line it follows.
Generated-by: Claude Code
Review found two differences between the arms that were not the edit
contract, and a control test that could not have caught either.
Only the `fs` arm mounted `dsh-fs-observation-policy`, justified in the
profile by that family's guidance mentioning it. Checking how the harness
ships does not support this: of the four presets in `@deepseek-ai/dsh`,
three mount `dsh-tool-fs` and none mounts the policy, which is not even a
direct dependency; and the policy's own documentation covers the baseline's
editor too. It is substrate, not part of an editor. Mounting it in one arm
gave that arm two failure modes the others cannot have — an edit without a
prior read, and a file the model's own shell touched in between — on a
benchmark where shell and editor are used together constantly. No arm
mounts it now. The apply-patch profile's stated reason for omitting it was
also wrong, and is corrected rather than quietly dropped: that arm emits
`fs/observed` with the file's version before it writes, so the policy would
have allowed the write.
The two new arms configured `fs-local` with a `cwd` expression the baseline
did not have. It could not change anything — the provider defaults `cwd` to
`process.cwd()`, both tools pass the session's own cwd, and `DSH_CWD` is
never set — but an unreachable setting is still a difference a reviewer has
to rule out.
Read budgets are now matched. The baseline pins its editor to 16000 output
characters while `dsh-tool-fs` shipped a 51200-byte default, a 3.2x window
on the same task. How much of a file one call returns is not part of the
edit contract, so `readMaxBytes` is set to match. The remaining read-side
asymmetry, that the patch arm has no read tool at all, is contract-inherent
and stays.
The control test read each `- id:` row with the `name:` on the line below
and compared those strings, leaving every `config:` block unchecked.
Narrowing one arm's context window, halving an editor's output budget,
rewriting the shared bash description, reordering the rows, or overriding
the patch tool's own description all kept it green. It now parses the YAML
and compares whole entries, asserts composition order, and pins each arm's
editor row by value — the treatment was the one thing no assertion covered.
Verified by making each of those six edits and watching it fail.
The arm-to-directory table moves to `toolchain-verification.ts`, beside the
identity table, and the subject and the test both read it. The test used to
restate it, so an arm pointed at the wrong profile would have been checked
against the composition it was meant to run rather than the one it runs.
The toolchain fingerprint is re-pinned for the plugin changes in the parent
commit. Verified in a `linux/amd64` container: all three profiles compose,
the composed trees differ only in the editor row, and the plugin loads from
inside the toolchain and registers `apply_patch` with the full grammar.
Generated-by: Claude Code
The apply_patch arm applied one file's hunks with the OpenAI Agents SDK's
`applyDiff`, vendored on the assumption that one V4A implementation is like
another. It is not. Running the released `codex` binary over the same trees
shows four disagreements, each of which moves this arm's score: a context-free
`+` hunk lands at the `@@` anchor rather than at end of file, an updated file
keeps whatever trailing newline it had rather than gaining one, CRLF survives,
and near-miss context is refused where Codex's four matching passes accept it.
Replace it with a port of Codex's own applier and parser. The parser now reads
hunks into chunks instead of passing section bodies through, so a syntax error
in the third section is found before the first is written, and the tool no
longer needs a second project's operation shape.
Restore verify-then-write. An earlier round changed the tool to apply
operations sequentially, matching the standalone `apply_patch` binary. That
binary is not what a model calls: the function-tool handler and the intercepted
shell call both go through `try_verify_apply_patch_args`, which collects hunks
into a map keyed by resolved path, refuses a duplicate key, and computes every
change against the pre-patch tree. So an envelope naming one path twice is
refused, and a hunk whose context the model guessed at costs nothing.
Fidelity is now measured rather than argued. `codex-oracle.mjs` runs the real
binary over a temporary tree for each of 72 cases and records what it printed
and what the tree became; `codex-fixtures.test.mjs` replays every case through
the registered tool and compares. The two cases where the function tool
deliberately differs from the standalone binary are named there with what it
does instead. A fuzzer over 1800 random envelopes found no further divergence.
Also tighten the arm-control test, which had three gaps: it skipped composition
steps that override an already-composed entry, so an override of the patch
tool's description was invisible; it named the profile files to compare instead
of reading them; and it exempted the first subject's profile argument while
comparing only the argument vector, not the whole subject definition.
Toolchain fingerprint re-pinned. Test-only files move under `__tests__/` so the
build prunes them by directory rather than by filename.
Generated-by: Claude Code
`readMaxBytes: 16000` was justified as making "how much of a file one call
returns" equal across the two read-capable arms. It does not. The baseline
editor spends eight characters a line on line numbers plus a header; the fs
tool emits neither, so measured over this repository's own sources it returns
10% more lines for the same budget and 22% on the longest files.
The budget is still the right one, for a reason the note did not give: what
should be equal is what a read costs the context window, and the numbering the
baseline adds is part of its contract that the model pays tokens for either
way. Equalising delivered content would mean picking a budget from an assumed
average line length.
Record the measurement, and record the one genuine mismatch that remains — the
budgets are counted in UTF-16 code units against UTF-8 bytes, equal on ASCII
and not otherwise.
Generated-by: Claude Code
The fixtures pin the cases someone thought to write down. This searches for the
rest: it generates envelopes from random line soup, applies each with the
released binary and with the registered tool, and reports every input the two
disagree on. 2100 envelopes across seven seeds currently diverge on nothing.
Red-checked against three deliberate regressions — placing a context-free `+`
hunk at the `@@` anchor, dropping the trailing-newline normalisation, and
removing the punctuation-folding match pass. The third went unnoticed through
400 envelopes at first, because context lines drawn verbatim from the file only
ever exercise the exact-match pass; the generator now half-remembers a line the
way a model does, and catches it.
Envelopes name each path at most once, and a refusal is compared as a refusal
without comparing the tree, because those are exactly where the standalone
binary and the function tool are supposed to differ.
Generated-by: Claude Code
The function-tool handler verifies an envelope and then throws the result
away: `ApplyPatchRuntime::run` hands `req.action.patch` — the raw text the
model sent — back to `apply_patch_with_mode`, which re-parses it and applies
it sequentially against the live filesystem. The verified `changes` map is
carried for approval and telemetry only.
This tool verified and then wrote the plan it had just computed. That is not
a cheaper version of the same contract, and the reference binary shows the
difference in two cases now covered by tests: a rename onto a path a later
section also updates leaves that section failing against the renamed file,
and `*** Move to:` naming its own source writes the destination and then
unlinks it, deleting the file while reporting success. Both were previously
refused with an invented "multiple operations target" error, because the
verification map also claimed the move destination — upstream keys only on
`hunk.resolve_path()` and carries `move_path` as a value.
Also in the write path: `fs/edit-intent` yields `{version}` with no `kind`,
so passing it straight to `writeText` matched neither guard and silently
wrote unconditionally; the abort signal was not checked between operations;
and `rm` ran with `force: true`, reporting `D <path>` for a file that had
already gone.
In the applier: each fuzzy-matching pass now projects both sides once
instead of recomputing the punctuation fold per candidate position, and
replacements are rebuilt in one forward pass rather than spliced, which
threw a RangeError once a hunk added more lines than the argument limit.
The model-facing description is no longer transcribed into this file. It is
read from a byte-for-byte copy of the upstream artifact it came from, with
three named substitutions applied over it, so the claim that it differs in
exactly those places is checkable rather than asserted in a comment. Copying
the file rather than retyping it also removed a fourth difference that had
no reason to exist: upstream's typographic punctuation is now preserved.
Its recorded provenance was stale — the file it named no longer exists at
the pinned commit — and now points at the artifact that does.
One parameter description said the input was "Not JSON-wrapped". It is: the
harness registers JSON function tools and nothing else.
Generated-by: Claude Code
Upstream ships codex-rs/apply-patch/tests/fixtures/scenarios as, in its own
README's words, an end-to-end suite "meant to be easily portable to other
languages or platforms". This is the port: the 25 scenario directories are
copied verbatim from the pinned commit and replayed through the registered
tool, compared the way upstream compares them — copy `input/`, apply
`patch.txt`, ignore the exit status, and require the resulting tree to equal
`expected/` exactly.
It is the only check here with the standing to contradict the code. The
oracle fixtures run the reference binary, but over inputs this repository
chose; the fuzzer perturbs inputs this repository chose how to perturb.
These were chosen by upstream, before and independently of this tool.
22 of 25 match outright. Three do not, and each divergence is recorded with
its cause and pinned to bytes the reference binary produces:
- 015 specifies the standalone binary, which has no verification pass, so an
add preceding a failing update survives. The function tool writes nothing.
- 023 and 024 are specified under CODEX_APPLY_PATCH_PRESERVE_LINE_ENDINGS,
a feature that is `Stage::UnderDevelopment, default_enabled: false`, so a
shipped Codex and this arm both run the `NormalizeToLf` default.
Both divergence lists — here and in the oracle fixtures — now assert that
each entry still diverges, so an excuse cannot outlive the thing it excused.
Generated-by: Claude Code
The treatment in this arm is the tool name, the parameter it takes, and the
description text. None of the three had any coverage, so a build that
registered the tool under another name, dropped the parameter, or shipped a
truncated description would have run a cohort and produced numbers.
The description claim is the one that had to be argued rather than shown:
"three differences from upstream and no others" was a comment. It is now
checked at both ends. The vendored instructions file is pinned by digest,
with the `git show ... | sed -n` that reproduces it from the source recorded
beside the constant, and the built text is recomputed here by a different
mechanism than the code uses and required to match. Each adaptation must
apply exactly once, and no mention of the shell delivery may survive.
Red-checked against four sabotages — appending a line to the description,
renaming the tool, editing one byte of the vendored text, and neutering an
adaptation. Each is caught.
Generated-by: Claude Code
Two differences at the outermost layer of the patch, both pinned by new
oracle fixtures generated from the reference binary.
`parse_patch` selects its mode from `PARSE_IN_STRICT_MODE`, which is `false`:
upstream gave up on gating leniency by model, so every call site including
the function tool unwraps a heredoc-wrapped envelope. A model asked to send
a patch may well send the `<<'EOF' ... EOF` form it was shown, and Codex
applies it where this refused it. The strict boundaries are still tried
first, so an envelope whose own first line reads `<<EOF` is never stripped.
Rust's `str::trim` cuts the Unicode `White_Space` property; JavaScript's also
cuts U+FEFF and does not cut U+0085. Only U+FEFF can realistically occur — a
byte-order mark carried into a JSON string argument — and it made this tool
accept a patch Codex refuses, which is the wrong direction for an arm being
compared against the contract it imitates.
Red-checked: restoring `String.prototype.trim` fails the byte-order-mark
fixture, and disabling the heredoc strip fails both heredoc fixtures.
Generated-by: Claude Code
The comment justifying `readMaxBytes: 16000` said `str_replace_editor`
spends eight characters a line on line numbers and a header while "this tool
emits neither". Both tools number their lines, and this one wraps the result
in a `<path>/<type>/<content>` envelope with a footer.
The setting still holds something equal, but not what was claimed. The
baseline truncates the fully rendered string; this tool charges the budget
against raw line bytes plus separators and adds the gutter, the envelope and
the footer afterwards, outside it. Measured across this repository's 1480
TS/JS/Python sources at 16000, the arm delivers 1.105x the lines and emits
1.027x the characters, worst file 1.34x and 1.19x — a residual in this arm's
favour, now recorded with the mechanism that produces it.
The three profiles also claimed to have been "verified request-for-request"
against the upstream composition. That was a manual comparison made once
during development; nothing here reproduces it, and the headers now say so.
`MAKA_EVAL_DSH_PLUGINS` was exported into all three arms and read by one, so
the two controls carried an environment difference for a mechanism they do
not use. It is now set from the copied composition itself, which keeps the
condition and the file that depends on it from drifting apart.
Generated-by: Claude Code
`apply_patch` in Codex is a freeform tool: the API is sent a short description
and a Lark grammar the decoder is constrained to, so a model on that path
cannot emit a malformed envelope. The harness registers JSON function tools
and has no grammar seam, so this arm is the unconstrained case. That is not a
detail of the port — it is a cost charged to this arm and to no other, and it
now leads the arm's description instead of being absent from it.
Three further divergences are named with their causes: the function-tool path
against the standalone binary's sequential apply, `NormalizeToLf` against a
preserve-line-endings feature that ships disabled, and diagnostics more
specific than Codex's single `Invalid patch:` line, which is the one place
this arm may be easier on a model than the reference.
The README also described a verify-then-write-the-plan design the tool no
longer has, quoted the old fixture count, and repeated the read-budget
mechanism corrected in the previous commit.
The NOTICE opened by saying nothing was carried verbatim as a file; two
things now are. Its description provenance named a file that no longer exists
at the ported commit and a fourth modification that was an artefact of
retyping rather than copying, its fixture path was stale, its account of how
`PreserveLineEndings` is selected covered only the standalone binary, and its
list of ported functions omitted the ones that decide apply order.
The arm-control test read a composition step as an insertion whenever it had
an `insert` key, so `{insert, remove}` would have passed with the removal
unread, and it filtered directories out of each profile's file list rather
than refusing them. It also now names what it does not cover.
Generated-by: Claude Code
`dsh-fs-local` creates a write's parent itself, in `writeFileAtomic`, and it
is the provider every arm mounts. The `ensureParent` here ran `mkdir` on a
`processPath` — the same way around a confining sandbox that delete and
rename refuse for — and existed only because the test harness's provider was
weaker than the real one and failed with ENOENT without it.
That is the more useful half of this change: a fake that cannot do what the
real thing does will not merely miss bugs, it will make production code grow
to cover for it. The fake now creates the parent the way `writeFileAtomic`
does, and the tool has no call that creates a directory at all.
The test that pinned the old behaviour was passing for the wrong reason — it
asserted the tool left no directory behind, and what actually happened was
the fake's write failing before anything could be created.
Generated-by: Claude Code
…sses
`deriveNewContents` reported a context miss as a plain `Error`, and the
caller rewrapped anything it threw as `FS_EDIT_NOT_FOUND` — so a bug in the
applier would have reached the model as "the lines you named are not in the
file", which is advice to rewrite a correct patch. The context miss is now
its own type and the only thing translated; a comment already claimed this
and the code did not.
Two oracle fixtures pin the order of the four matching passes against each
other. The existing cases pinned the exact pass ahead of the loose ones;
between the loose ones, removing a pass does not fail a match, it silently
moves it to a different line, and nothing caught that. Each of the four now
fails at least one fixture when removed.
The line-ending note in codex-apply.mjs described only the standalone
binary's environment variable, not the feature flag the function tool reads.
The toolchain is rebuilt and its fingerprint re-pinned: the model-facing
instructions now ship as a file, so they are covered by `checksums.sha256`
rather than living only inside a JavaScript template. Verified inside the
pinned linux/amd64 container that the built tree registers one tool named
`apply_patch` with one required `input` string and a 2903-byte description
carrying no mention of the shell delivery.
Generated-by: Claude Code
Differential fuzzing against the reference binary found a regression this
branch introduced. `applyReplacements` was rewritten to build the file
forward in one pass, on the reasoning that the replacements are sorted and
disjoint. They are sorted; they are not always disjoint.
`computeReplacements` places a context-free insertion at the end of the file
without consulting the search position — deliberately, because that is what
`insertion_idx` does upstream — so it can land inside a range a later chunk
replaces. Applying descending, the replacement swallows it; applying forward,
the two are treated as separate edits and the inserted line survives into a
file the reference never produces.
Restored to splicing in descending order, with the replacement segment going
in in bounded batches so a hunk larger than the engine's argument limit still
does not throw. The case is now an oracle fixture, so it is caught without a
`codex` on PATH; red-checked by reinstating the forward rebuild.
2300 further envelopes across four seeds diverge on nothing. Toolchain
rebuilt and re-pinned.
Generated-by: Claude Code
An adversarial review pass found nine behaviours that were wrong or untested
in the V4A tool, each verified against the reference binary or upstream source
before being changed. All nine are now covered: sabotaging any one of them
fails the suite, where before five of them left it green.
The matcher used JavaScript's `trim` where upstream uses Rust's. The two
disagree on U+FEFF and U+0085, so a file whose first line carried a byte-order
mark matched a pattern without one and was written back with the mark deleted,
and a line terminated by NEL was refused where the reference applies it. The
charset the parser already spelled out moves to rust-trim.mjs and both use it.
The verify pass now does what `try_verify_apply_patch_args` does: nothing at
all for `*** Add File:`, and a read for `*** Delete File:` as well as for an
update. Statting a delete target instead applied a delete the reference
refuses, since `read_file_text` maps invalid UTF-8 to `InvalidData` and fails
the whole envelope. It also stops emitting `fs/observed {present}` off a bare
stat, which told an observation policy a file had been seen that nothing read.
The duplicate-path key is lexical, as upstream's `PathUri::join` is, not the
provider's realpath-derived target key. An envelope naming both a symlink and
its target was refused here; the binary applies both sections in order.
A move destination no longer runs the `fs/write-intent` waterfall and discards
the answer, which turned a decider's refusal to clobber into a silent
overwrite. A failed unlink and an abort between passes now arrive as `FsError`
with a routable code and the path the model wrote, not a raw errno and a
resolved host path.
The test provider was weaker than `dsh-fs-local` in four ways that made the
above invisible: it decoded lossily where the real one raises `FS_NOT_TEXT`,
called every non-directory a file, took three parameters so the signal and
sandbox policy could not be observed, and never returned a decider's answer.
Two oracle cases are added for the trim charset and one for a trailing U+FEFF,
which is what separates the three loose matching passes from each other; the
BOM-in-content case is recorded as a divergence with its true cause, which is
that the shared provider strips a leading mark before this tool sees the file.
Generated-by: Claude Code
The import landed the 25 scenario directories, their README and their
.gitattributes twice: once at `__tests__/upstream/` and once, byte-identically,
at `__tests__/upstream/scenarios/`. Only the nested copy is read.
Generated-by: Claude Code
…arms
The three arms are meant to differ only in the edit contract. Three settings
outside that contract were not held.
`readMaxLineLength` was left at its shipped 2000 while only `readMaxBytes` was
matched to the baseline. That is a cap the baseline has no counterpart to: the
fs arm could never see the 2001st character of any line, at any offset, while
`str_replace_editor` truncates once over the whole rendered view and can spend
its entire budget on one line. A minified bundle or a single-line payload is
where that lands, and it lands against one arm only. It is raised to the
budget, where it can no longer bind before `readMaxBytes` does.
`streamIdleTimeoutMs` was omitted from all three profiles, leaving the
adapter's 300000 ms default in force — five minutes between two streamed
tokens, behind `reasoningEffort: max` and a 65-minute bash deadline. Upstream's
own jsonrpc-agent composition sets 172800000, which is what the arms now carry;
a turn killed by that timeout would have been scored as a failure of whichever
contract was running.
`maxConcurrentTaskGroups` was 64. A group holds one cell per subject and starts
them together, so three arms made 192 concurrent trials where every other spec
here lands on 128. That does not bias one arm against another, but it puts this
run's `deepseek-harness` score under a load no single-arm run shares. It is now
43, and the test asserts the arithmetic rather than the number.
The read-budget residual quoted in the profile and the README had no
reproducible source and reported only the tail that favoured the fs arm.
`scripts/measure-read-budget.mjs` reimplements both renderers from the shipped
code and reports both tails; the figures are replaced with what it prints.
Generated-by: Claude Code
A claims audit checked every factual assertion in the README, the NOTICE and
the profiles against upstream source at the ported commit and against the
shipped harness packages. Fourteen did not hold.
The largest is provenance. `prompt_with_apply_patch_instructions.md` is not in
any shipped Codex system prompt at the ported commit: it is referenced only
from a test whose four model cases all declare
`expects_apply_patch_description: false`, and none of the eight instruction
templates in models.json carries the section. The file is upstream's own
authored description of the format and it is retired, which is a better reason
to use it than the one previously given, but the prose said Codex sends it.
The freeform description is 108 characters, not 105. The oracle runs 82 cases,
not 76. The fuzz figure is 2300 across four seeds, not 2100. Two of the named
divergences were recorded in the tests, not three, and there are now four with
the byte-order mark the provider strips. `verifyToolchainDirectory` covers ten
registered profiles, not eight. `checksums.sha256` omits itself and
manifest.json. The toolchain build's `__tests__` prune is scoped to the plugins
it copies. The build script runs from the repository root. `ctx.fs` offers
twelve methods, not three. The 0.147.0 delta includes a second behavioural
difference — the duplicate-path check does not exist at that tag — and a larger
restructure than the three items named. The profiles record three deviations
from upstream, not one.
Also stated where it was missing: the arm's lack of a read tool is Codex's
contract rather than a handicap, since Codex registers no file-read tool
either and every arm has the same bash.
Generated-by: Claude Code
A second adversarial round found that the previous round's fix traded one
divergence for another, and that twelve mutations of this tool still left the
whole suite green.
`ctx.fs.readText` is not `read_file_text`. Upstream reads bytes and calls
`String::from_utf8`, so every valid UTF-8 sequence is text; `dsh-fs-local`
adds two rules on top, and both are model-visible. It rejects a NUL in the
first 8192 bytes as binary — but NUL is valid UTF-8, and the reference patches
and deletes straight through it, verified against the binary — so making the
verify pass read delete targets took `*** Delete File:` on such a file with
it. And it decodes with `ignoreBOM` left false, which strips a leading
byte-order mark, so a patch whose context omits the mark applied here, was
refused there, and wrote the file back without the mark. Reading through
`ctx.fs.readBytes` and decoding here is exactly `String::from_utf8`; it fixes
both, and the byte-order-mark divergence is deleted rather than documented.
`*** End of File` as the first line of an update hunk was refused. Upstream
guards with `chunks.last().is_some_and(blank)`, which is false when there is
no chunk, so the marker is ignored; the binary applies the envelope.
Twelve mutations that the suite could not see, now each red-checked: five
dropped the turn signal from a provider call, which a fake that ignored the
signal could not notice; two removed or falsified the version on `fs/observed`;
two swapped a removal's announcement with the removal itself, which leaves the
event list identical and the trail a lie; one bypassed the create decider and
one renamed its event; one reduced the sandbox-denial remap to a no-op. The
fake now honours and records the signal on every call, records what the
filesystem actually held when each observation was published, records the
version `writeText` returned, and can be made to deny a write.
Twelve oracle cases added, generated from the binary: three for the read seam
and nine for shapes that separate this port from a plausible variant of it —
overlapping chunks, a backward `@@` anchor, an indented header after an add
body, a trailing mark on an End of File marker, header paths with a leading
space, a repeated `*** Move to:`, a heredoc close with trailing text, and the
End of File case above. Eleven passed unchanged, which is the point of adding
them; one was the defect.
`requireDirectFilesystem` now raises an `FsError` like every other refusal.
Generated-by: Claude Code
… meant to help
Raising `readMaxLineLength` from its shipped 2000 to the 16000 budget was
wrong, and worse than the asymmetry it was meant to close. `dsh-tool-fs`
truncates a long line and appends a `... (line truncated to N chars)` marker,
then charges the whole thing against `readMaxBytes`. Once the cap approaches
the budget the truncated line no longer fits at all: the window returns zero
lines, drops every line after it, and the footer invites the model to retry at
the offset it just used, so the rest of the file is unreachable. Measured
against the shipped functions, a 20000-character line followed by 200 short
ones delivers 201 lines at 2000 and 0 lines at 16000.
No value equalises the two budgets anyway — one counts UTF-16 code units and
the other UTF-8 bytes — so the cap goes back to the value the tool ships with
and the residual is measured instead of engineered away. At 2000 the direction
is also not the one the earlier reasoning assumed: on that same long-line file
the baseline spends its whole budget on the one line and delivers 1, while
this arm truncates it, says so, and reads on.
The guard test now asserts the invariant rather than the number, so any future
cap has to be one a truncated line can still fit under.
The measurement script had four transcription errors, all inflating the
character ratio: it dropped the baseline's 240-character truncation notice and
the fs arm's per-line marker, counted the baseline's prompt line as content,
and counted a phantom trailing line for the fs arm. It also silently ate the
first positional path when `--budget` was absent. Rather than fix the
transcription and hope, `--self-check <dir>` now lifts the real functions out
of an installed copy and asserts agreement, and the corpus figures are
reported alongside synthetic long-line cases that the repository's own sources
do not contain. Corrected: 2197 files, 1.093x lines, 1.001x characters, tails
reported as what they are — extremes among the 570 files where a budget binds.
`maxConcurrentTaskGroups` moves 43 to 42: three arms at 43 is 129 trials, and
the assertion had been written as `<= 129` to admit it.
Generated-by: Claude Code
The claim that upstream's `## apply_patch` prose is retired was too strong. Of
the eight instruction templates in `models-manager/models.json` at the ported
commit, seven carry no such section — but `gpt-5.2` carries a shorter one with
the same envelope, the same three headers and the same worked example. So the
format is still taught in prose upstream, and the vendored file is the fuller
version of that prose rather than something abandoned. The conclusion it was
supporting survives unchanged and for a cleaner reason: `gpt-5.2` gets the
prose *and* the grammar, so no shipped configuration is the one this arm is in.
Codex's handler set is much longer than the six entries named, and one of them
takes a filesystem path — `view_image`, for images. The load-bearing claim is
that no handler reads file *text*, which is what the prose now says.
The NOTICE gave the wrong cause for the duplicate-path divergence: the
recorded binary composes an envelope naming one path twice because the
standalone applier has no verification pass at either version, not because the
check is absent at rust-v0.147.0 — that is a function-tool difference.
Also: `invocation.rs:246-248` is the delete arm, not the add arm; the
`fs/edit-intent` shape citation named only one of the two declarations; the
end-of-file search has no fallback to the ordinary start position, contrary to
its comment; `__tests__/upstream/` kept a stray `.gitattributes` naming paths
one level down; and the build script's header credited `npm i` for a
non-reproducible fingerprint when it runs `npm ci` and the real cause is native
compilation.
The refusal under a confining provider is described as what it is: dead code
under `dsh-fs-local`, which never sets `sandboxMode` at any sandbox mode, not
something `danger-full-access` switches off.
Generated-by: Claude Code
…t reads it
The comment justifying the conditional export had the causation backwards. The
model's shell inherits this environment, so setting the variable in all three
arms is what makes them identical, and setting it in one arm alone is what
creates a difference the model can read with `env`. It names a directory and
starts nothing, so it is inert in the two arms whose compositions never mention
it.
The remaining environment asymmetry, `$HOME` carrying the profile name, is
recorded rather than removed: the arms need separate homes because each writes
its own composition under `$DSH_HOME/profiles`, and sharing one would have each
overwrite what the others booted from. So the experiment is not blind to its
subject, and that is now stated where the home is built.
Generated-by: Claude Code
The spec declared 42 task groups, or 126 concurrent trials, on the
strength of a 128-trial convention rather than a machine. The account's
CVM quota is 60 vCPU per zone, which caps the host at 56 vCPU and
256 GiB, and the only completed run of this suite put 24 trials on
32 vCPU / 64 GiB — 2.7 GiB per trial. 126 trials on that host is 2.0 GiB
each, close enough to a Terminal Bench build task's peak that an OOM
kill would land as an infra failure. 32 groups is 96 trials at the
density that is known to survive.
Generated-by: Claude Code
The arm named its plugin with `name: !!js process.env.MAKA_EVAL_DSH_PLUGINS
+ '/tool-apply-patch/index.mjs'`. The harness's loader evaluates that tag for
`disabled` alone and hands `name` the unevaluated node, so every trial of
that arm died at boot with `name.startsWith is not a function` before it
reached the model — on a rented host, at the first smoke task.
The row now names `./plugins/tool-apply-patch/index.mjs`, resolved against the
profile directory, and the subject plants `plugins` there as a link into the
toolchain. That keeps what the loader imports inside the fingerprinted
read-only tree, keeps the composition a plain string, and keeps the path
independent of the root the wrapper was given — which naming the mount point
outright would not. The link is planted for all three arms, on the same
reasoning that exported the variable for all three: the model's shell can read
$DSH_HOME, so planting it in one arm alone is the difference, not planting it
everywhere.
The test could not have caught this. It extended its own YAML schema to
construct `!!js <source>` as a string, which taught the assertion to accept a
form the harness cannot load. The tag is real elsewhere in these compositions —
`task: !!js ctx.headlessStartup.task` is upstream's — so the schema now
constructs it as the marker shape the harness's loader produces, and a new
assertion requires every entry name to be a string and every path-named plugin
to be one the toolchain build ships. Reverting the row fails both.
Generated-by: Claude Code
The wrapper counted the subject's stderr bytes and hashed them, then dropped
them. The relay writes `maka-subject.stderr.txt` from the wrapper's own
stderr, which is empty, so a failing arm's own account of what killed it
reached the attempt record as a byte count and a digest and nothing else.
Both failures this run has had were diagnosed by reproducing them by hand on
the host instead: a composition the harness could not load, and a subject that
exits after one model response. The second is still open.
The last 64 KiB now land in `/logs/agent/<profile>.stderr.txt`, which the
relay already collects, as a `stderr-tail` artifact beside the existing
`stderr` count. A tail because a subject that loops printing would otherwise
fill the trial's disk, and because a harness says what killed it on the way
out; the count and digest still describe the whole stream, so truncation is
visible.
Generated-by: Claude Code
Announcing it as an artifact pushed the frame past the relay's 2 KiB payload
cap, so every cell came back `external subject result transport failed`.
`/logs/agent` is collected into the trial directory regardless, so writing the
file is the whole job.
Generated-by: Claude Code
Every failure before the subject starts reached the operator as the string
`external subject setup failed`, because `safeFailure` sanitizes what the
published result frame may say and nothing recorded the error itself. The
process's own stderr is collected into the trial as
`maka-subject.stderr.txt`, which is where a subject's account of its own
failure belongs, so the error, its stack, and its cause chain go there.
Generated-by: Claude Code
This module does its work at the top level, above the function that reads the
subject's stderr, so a `const` declared beside that reader is still in its
temporal dead zone when the reader runs. Every cell died with
`ReferenceError: Cannot access 'STDERR_TAIL_BYTES' before initialization`
before the subject's first model response — reported as
`external subject setup failed`, and legible only because the commit before
this one started writing the error itself.
Generated-by: Claude Code
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(eval): compare three edit contracts on the DeepSeek Harness - #3067

Closed
Astro-Han wants to merge 33 commits into
feat/eval-deepseek-harness-subjectfrom
feat/eval-edit-contract-arms
Closed

feat(eval): compare three edit contracts on the DeepSeek Harness#3067
Astro-Han wants to merge 33 commits into
feat/eval-deepseek-harness-subjectfrom
feat/eval-edit-contract-arms

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Summary

A Terminal-Bench score is attributed to a framework, which leaves the edit contract inside the framework and unmeasured. Comparing Maka's edit/write against the harness's str_replace_editor cannot separate them either — that comparison moves the framework and the contract at once.

These three arms hold everything else and move only the contract:

armcontract
deepseek-harnessstr_replace_editor — one tool, four commands, unique-literal match, all guidance in the tool description
deepseek-harness-fsthe harness's read/write/edit — three tools, snake_case, three prompt sections of their own
deepseek-harness-apply-patchV4A apply_patch — one envelope, changes located by surrounding context

Same one-shot CLI, same toolchain and fingerprint, same dsh-fs-local provider under every editor, byte-identical package.json and cordis.yml. The variable is the whole tool family and not one dimension of it: tool count, argument names, path conventions and where the guidance lives move together, because each family ships that way. A result is attributable to the family as shipped, not to patch syntax as such.

No arm mounts dsh-fs-observation-policy. It was mounted for the fs arm alone on the reasoning that its tools' guidance mentions the policy; of the four presets in @deepseek-ai/dsh, three mount dsh-tool-fs and none mounts the policy, and its own documentation covers str_replace_editor too. Mounting it in one arm gave that arm two failure modes the others cannot have.

Three compositions in three files can drift apart while still producing numbers, so the control is asserted rather than intended. edit-contract-arms.test.ts fails when the compositions differ by any row outside their editors, when they compose in a different order, when the model, context window, reasoning effort, stream idle timeout, sandbox mode, persona or either Bash deadline drifts in one, when the arms stop sharing one toolchain identity object, when the experiment launches them with anything but a different profile argument, or when arms multiplied by concurrency leaves the 128-trial convention.

The patch arm is ours, and that is the thing to review

The harness ships no patch tool, so plugins/tool-apply-patch/ is this repository's while the other two arms run their vendor's. Fidelity to Codex is therefore itself an experimental variable, and it is asymmetric: a tool stricter than the reference costs its arm turns the reference never spends, and a safer one wins recoveries the reference never gets.

One deviation cannot be removed.apply_patch in Codex is a freeform tool — the API is sent a 108-character description plus a Lark grammar the decoder is constrained to, so a model on that path cannot emit a malformed envelope. The harness registers JSON function tools and has no grammar seam. This arm is therefore the unconstrained case, and syntax errors it can make are errors Codex's decoder would have prevented. That cost is charged to this arm and to no other, and it leads the arm's description in the README.

What is not charged to it is the absence of a read tool. Codex registers no handler that reads file text either — its handler set is long, and the only entry taking a filesystem path to read is view_image, which takes images — so an arm that reads with cat is that contract, and every arm has the same bash at the same 16000-character cap.

What the model is told is upstream's own prose for exactly that case: the ## apply_patch section of prompt_with_apply_patch_instructions.md, carried verbatim as upstream-apply-patch-instructions.md with three substitutions applied over it. A test pins the file's digest, requires each substitution to apply exactly once, and requires the built description to differ from the copy nowhere else. That file is upstream's and it is not delivered from where it sits: at the ported commit it is referenced only from a test whose four model cases all declare expects_apply_patch_description: false. Seven of the eight instruction templates in models-manager/models.json carry no ## apply_patch section and leave the format to the grammar; the eighth, gpt-5.2, carries a shorter one with the same envelope and worked example. So the format is still taught in prose upstream and this is the fuller version of it. Either way no shipped configuration is the one this arm is in — gpt-5.2 gets prose and grammar — so the choice was between upstream's own text and text written here.

Three further divergences are known and each is pinned to bytes the reference produces:

  • the tool implements the function-tool path — verify the whole envelope, then re-apply it from the raw text — where the standalone binary applies sequentially with no verification;
  • it runs NormalizeToLf, the shipped default, where PreserveLineEndings is Stage::UnderDevelopment, default_enabled: false upstream;
  • its diagnostics differ from Codex's, which is the one place this arm may be easier on a model. This one is recorded in prose only; the other two are named with their causes in the test files, which also assert that each still diverges.

A third divergence was recorded here and has since been removed rather than documented: the provider's readText strips a leading byte-order mark and rejects a NUL byte, neither of which String::from_utf8 does. Reading through readBytes and decoding in the tool matches the reference exactly.

The grammar is implemented in full. *** Delete File: and *** Move to: are the only operations ctx.fs has no primitive for, so they run against ctx.fs.processPath(target); under a provider that confines, both refuse up front rather than escape. This arm mounts danger-full-access, where the refusal never fires.

Sources and licences are in the plugins' NOTICE. An earlier revision of this branch vendored the OpenAI Agents SDK's applyDiff as the hunk applier; it is a different V4A implementation, not Codex's, and nothing from it remains.

What the adversarial review rounds changed

Two rounds of four fresh-eye passes each — the port against the binary, the harness integration, the three-arm control, and an audit of every factual claim. The second round reviewed the first round's fixes, which is what caught the two entries below that begin "the previous fix". Every finding was re-verified against upstream source or the reference binary before being acted on.

Nine port defects, all now red-checked. The matcher used JavaScript's trim where upstream uses Rust's, so a file whose first line carried a byte-order mark matched a pattern without one and was written back with the mark deleted, and a line terminated by U+0085 was refused where the reference applies it. The verify pass stated a delete target instead of reading it, applying deletes of non-UTF-8 files that the reference refuses outright. It emitted fs/observed {present} off a bare stat, telling an observation policy a file had been seen that nothing read. The duplicate-path key was the provider's realpath-derived target key rather than upstream's lexical join, so an envelope naming both a symlink and its target was refused where the binary applies both. A move destination ran the fs/write-intent waterfall and discarded the answer, turning a decider's refusal to clobber into a silent overwrite. A failed unlink and an abort between passes escaped the seam's error taxonomy.

Five of those were invisible because the test provider was weaker than dsh-fs-local: it decoded lossily, called every non-directory a file, took three parameters so the signal and sandbox policy could not be observed, and never returned a decider's answer.

The previous fix read patch targets through the wrong seam.ctx.fs.readText is not read_file_text: upstream reads bytes and calls String::from_utf8, while dsh-fs-local additionally rejects a NUL in the first 8192 bytes and strips a leading byte-order mark. NUL is valid UTF-8 and the reference patches and deletes straight through it, so making the verify pass read delete targets — itself a correct fix — took *** Delete File: on such a file with it. Reading through readBytes and decoding in the tool is exactly String::from_utf8, and it removes the byte-order-mark divergence too. Separately, *** End of File as the first line of an update hunk was refused where upstream ignores it.

Twelve more mutations the suite could not see. Five dropped the turn signal from a provider call, which a fake that ignored the signal could not notice; two removed or falsified the version on fs/observed; two swapped a removal's announcement with the removal itself, which leaves the event list identical and the trail a lie; one bypassed the create decider, one renamed its event, one made the sandbox-denial remap a no-op. The fake now honours and records the signal on every call, records what the filesystem held when each observation was published, records the version writeText returned, and can be made to deny a write.

Three uncontrolled differences between the arms, of which one fix was itself wrong. streamIdleTimeoutMs was omitted from all three profiles, leaving a five-minute ceiling on the gap between two streamed tokens behind reasoningEffort: max and a 65-minute bash deadline; it now carries upstream's own 172800000. maxConcurrentTaskGroups: 64 made 192 concurrent trials where the saturating specs here land on 128; it is now 42.

readMaxLineLength was raised from its shipped 2000 to the budget to close a cap the baseline has no counterpart to, and that was worse than the asymmetry. The tool appends a ... (line truncated to N chars) marker and then charges the whole line against readMaxBytes, so a cap near the budget makes a long line not fit at all, drops every line after it, and returns a window no offset recovers — 201 lines at 2000 becomes 0 at 16000. No value equalises the two budgets anyway, one counting UTF-16 code units and the other UTF-8 bytes, so the cap is back where the tool ships it and the guard test asserts the invariant rather than the number.

Twenty-one false claims across the two rounds, listed in the two docs commits. The largest was the provenance one, twice: first that the vendored prose is what Codex sends, then that it is retired. The read-budget script had four transcription errors of its own, all inflating the character ratio, so it no longer asks to be trusted — --self-check <dir> lifts the real renderers out of an installed copy and asserts agreement, and the figures are 1.093x lines and 1.001x characters over 2197 files.

Verification

Lint, format and typecheck clean. 47 TypeScript and 200 plugin tests pass.

Fidelity is measured against the binary rather than argued from the Rust:

  • codex-oracle.mjs runs the released codex under argv0 apply_patch over a temporary tree for each of 94 cases and records what it printed and what the tree became; codex-fixtures.test.mjs replays every case through the registered tool and needs no codex to do it.
  • __tests__/upstream/scenarios/ carries Codex's own conformance suite verbatim — the 25 scenario directories upstream publishes as "meant to be easily portable to other languages or platforms" — and upstream-scenarios.test.mjs replays them upstream's way: copy the input tree, apply the patch, ignore the exit status, require the resulting tree to match exactly. 22 match outright; the three that do not are named divergences. These are the only inputs here this repository did not choose.
  • codex-fuzz.mjs generates envelopes from random line soup, including near-miss context, and runs both. It earned its keep on this branch: it caught a regression introduced here, where rebuilding the file forward let a context-free insertion survive inside a range a later chunk replaces. 2300 envelopes across four seeds diverge on nothing.

Red-checked, not just green:

  • Appending a line to the description, renaming the tool, editing one byte of the vendored instructions, or neutering a substitution each fails the contract-surface test.
  • Removing any one of the four context-matching passes fails at least one oracle fixture; restoring String.prototype.trim in the loose passes fails the trailing-U+FEFF fixture; disabling the heredoc strip fails both heredoc fixtures; matching the heredoc close with includes instead of endsWith fails the trailing-text fixture.
  • Each of the twenty-one port defects and unseen mutations above fails the suite when reintroduced. Seventeen of them did not before these two rounds.
  • Both divergence lists assert that each entry still diverges, so an excuse cannot outlive the thing it excused.

End-to-end against the rebuilt linux/amd64 toolchain: loading the plugin from inside the built tree registers exactly one tool named apply_patch, with one required input string and a 2903-byte description carrying no mention of the shell delivery. The instructions file ships, so it is covered by checksums.sha256 and the re-pinned fingerprint.

Not run: an actual benchmark cohort. No dependency moved.

Review focus

One claim this repository does not reproduce, and which should be read as a one-time manual check rather than a standing guarantee: the three profiles were compared against the upstream examples/jsonrpc-agent composition during development — tool names, tool schemas, system prompt and message sequence on the first outbound request — and found identical.

The read budget matched at 16000 equalises the budget's number, not delivered content. scripts/measure-read-budget.mjs makes the residual checkable and --self-check makes the script itself checkable: over 2197 tracked sources the fs arm delivers 1.093x the lines and 1.001x the characters; among the 570 files where a budget binds, the extremes are 1.398x/1.168x and 0.837x/0.720x.

Two things a write-up must carry that this branch cannot fix. The fs arm receives 764 characters of tool-contributed system-prompt guidance where the other two receive none, and two of its three sections reference an fs-observation-policy no arm mounts. And the treatment is the whole tool family plus that guidance, not "the edit contract" — the arm names are shorter than the thing being varied.

experiments/.../edit-contracts.json declares repetitions: 1, so 89 tasks x 3 subjects is 267 cells. Cost, not power, chose that. McNemar at one repetition is weak for the effect sizes this is likely to see: on ~20% discordant pairs a true 5pp difference is about 2 sigma before any correction for three pairwise comparisons, so a write-up has to report discordant counts and intervals rather than three success rates.

Stacked on #2971.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes — described under Summary above
  • No

The harness ships no patch tool, and an Eval arm that gives the model
Codex's patch language needs one composed the same way its other editors
are: `inject: ['tools', 'fs']`, one `ctx.tools.register(defineTool(...))`,
and every file operation through `ctx.fs`. Reaching around that seam would
put a second filesystem authority in one arm.
Applying a single file's hunks — context matching with the fuzz ladder — is
the part that is hard to get right, so it is not written here. It is the
OpenAI Agents SDK's `applyDiff`, vendored rather than depended on:
`@openai/agents-core` unpacks to 9 MB and pulls the whole `openai` SDK into
a tree that is fingerprinted file by file, while the function itself has no
imports at all. The upstream test suite is vendored with it, which is what
turns a mechanical tsc emit from an assertion into a checked fact; its two
import lines are the only edit. Biome's formatter now skips
`packages/eval/harbor/**/vendor/**` for the reason it already skips the
licence artifacts: a rewritten vendor file is no longer the upstream file.
What is written here is the envelope parser and the tool. The grammar is
transcribed from Codex's lark definition, and the model-facing description
is Codex's own instructions text. Two deviations are deliberate and are
stated to the model rather than left to be discovered by failure:
- No `*** Delete File:` and no `*** Move to:`. `ctx.fs` has no delete or
rename primitive and neither does any other harness file tool, so in
every arm the model removes and renames through bash.
- A patch either applies completely or changes nothing. `applyDiff` is
pure, so every file's new content is computed before the first write.
The contracts this will be compared against touch one file per call and
cannot half-apply; a partly-patched tree would be a difference in
failure modes rather than in edit format.
Sources and licences are recorded in the plugins' NOTICE. The three test
files run from the repository through `test:dist`, against the same
`@deepseek-ai/dsh-tools` and `@deepseek-ai/dsh-fs` the toolchain installs —
added as exact-pinned dev dependencies, which the lockfile takes as pure
additions with no existing resolution changed.
No arm composes this tool yet.
Generated-by: Claude Code
Terminal-Bench results attribute a score to a framework, which leaves the
edit contract inside the framework and unmeasured. Comparing Maka's
`edit`/`write` against the harness's `str_replace_editor` cannot separate
them either: that comparison moves the framework and the contract at once.
So this holds everything else and moves only the contract. Three arms run
the same one-shot CLI, the same toolchain and fingerprint, the same
`dsh-fs-local` provider under every editor, and byte-identical
`package.json` and `cordis.yml`:
- `deepseek-harness` keeps `str_replace_editor` — one tool, four
commands, unique-literal match, all guidance in the tool description.
- `deepseek-harness-fs` takes the harness's own `read`/`write`/`edit` —
three tools, snake_case arguments, three prompt sections of their own.
- `deepseek-harness-apply-patch` takes the V4A tool added previously —
one envelope, changes located by surrounding context.
The variable is the whole contract, not one dimension of it. Tool count,
argument names, path conventions, where the guidance lives, and the
read-before-edit policy move together because each family ships that way.
`dsh-fs-observation-policy` is mounted for the `fs` arm alone for that
reason: its tools' own guidance text tells the model the policy is in
force, so omitting it would leave the prompt describing a rule the
deployment does not enforce, while `apply_patch` has no such concept and no
read tool to satisfy one with.
Three compositions in three files can drift apart while still producing
numbers, and the numbers would no longer be about the contract. So the
control is asserted rather than intended: the new test fails when the
compositions differ by any row outside their editors, when the model,
reasoning effort, sandbox mode, persona or either Bash deadline drifts in
one of them, when the arms stop sharing one toolchain identity object, or
when the experiment launches them with anything but a different profile
argument. The arms also share one preparer, so a change to how the harness
is launched cannot reach one and miss another, and one identity constant,
so a re-pinned fingerprint cannot be applied to two arms out of three.
The toolchain build now installs the plugin tree beside the harness's
`node_modules` — a composition copied under `$DSH_HOME` cannot resolve a
plugin by package name from there — and fails if a declared plugin did not
land. Its dependencies are unchanged: every package the new arms compose
was already in the reviewed lockfile.
Re-pinning the fingerprint needs a `linux/amd64` container build and is not
done here, so the arms are not yet runnable end to end.
Generated-by: Claude Code
The toolchain now carries the `apply_patch` plugin beside the harness's
`node_modules`, so its checksum manifest changed and the pin has to follow.
Rebuilt with the same pinned `linux/amd64` image and the same reviewed
lockfile; no dependency moved.
Verified against the rebuilt tree rather than assumed. `dsh --dump-config`
composes all three arm profiles successfully, and the only differences
between the three composed trees are the editor rows and the `cwd` the two
new arms give the filesystem backend — nothing else diverged. Loading the
plugin from inside the toolchain resolves `@deepseek-ai/dsh-tools`,
`dsh-fs`, `dsh-sandbox` and `schemastery` through the upward walk into
`lib/dsh/node_modules`, registers `apply_patch` with the expected schema,
and carries the patch instructions without the operations the tool does not
implement. `dsh-tool-fs` and `dsh-fs-observation-policy` resolve too.
Generated-by: Claude Code
@coderabbitai

coderabbitaiBot commented Aug 15, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1357f048-8ea6-474e-8887-d512c43407ac

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

The tool shipped without them, refusing both with a pointer to the shell.
The stated reason was that `ctx.fs` has no delete or rename primitive, so
reaching them through `node:fs` would put a second filesystem authority in
the one arm being measured. That reason does not survive contact with the
deployment it was written for: this arm mounts `dsh-sandbox-local` at
`danger-full-access`, no observation policy is loaded, and the model's own
bash can already remove any file in the container. The authority being
protected was not enforcing anything.
The cost was real, and it was the point of the arm. `apply_patch` exists
here to measure Codex's contract; a patch tool missing a third of its
grammar is a different contract, and a model trained on the real one pays
for the difference in wasted turns. It also broke this PR's own rule —
every other arm composes what its tool family ships, which is exactly why
the `fs` arm mounts the observation policy. Two standards in one change.
So both operations are implemented, against `ctx.fs.processPath(target)` —
the provider's own answer to where a file is for something outside it to
open, and for `dsh-fs-local` the local absolute path. `ctx.fs` stays the
only thing that resolves paths; only the two syscalls it does not offer
happen outside it. Semantics follow Codex: a rename writes the destination
and then removes the source, overwrites an existing destination rather than
refusing, and creates missing parents.
Where the original concern is real, it is enforced instead of assumed: under
a provider that confines, `processPath` would be the way around the sandbox,
so delete and rename refuse up front. `sandboxMode` is what decides, so the
refusal fires exactly when a provider actually confines.
The parser already implemented the whole grammar, so no change there. The
tool description loses its "not part of this tool" paragraph and returns to
Codex's text, now with one removal rather than three: the trailing shell
invocation example, which is false for a function tool.
Tests move from an in-memory store to a real temporary directory, since
these two operations are syscalls an in-memory store cannot observe.
Red-checked: stubbing out the removal fails five behaviour tests, and
disabling the confinement guard fails the two that cover it.
Generated-by: Claude Code
The plugin gained its delete and rename paths, so the file it contributes to
the toolchain changed and the checksum manifest with it. Rebuilt from the
same pinned image and the same lockfile; no dependency moved.
Verified against the rebuilt tree: all three arm profiles still compose
under `dsh --dump-config`, and loading the plugin from inside the toolchain
registers `apply_patch` carrying the full grammar — `*** Delete File:`,
`*** Move to:`, and both productions — with the shell-invocation example
still absent.
Generated-by: Claude Code
Independent review of this arm found five places where the tool behaved
differently from the reference it exists to measure. The asymmetry matters
more than the count: a tool stricter than Codex costs its arm turns the
reference never spends, and a tool safer than Codex wins its arm recoveries
the reference never gets. Either way the benchmark reads the difference as
an effect of the edit contract.
`*** Add File:` did not terminate the created file with a newline, because
the vendored applier joins its added lines while Codex appends `\n` after
each one. Every file this arm created differed from Codex's byte for byte,
and a test pinned the wrong result. Each added line is now terminated, and
an empty create still writes nothing, as `String::new()` does upstream.
The envelope was planned against one snapshot and written afterwards, so a
failed hunk left the tree untouched. That is the nicer property and it was
still wrong: two sections naming one path made the second fail against a
version the first had replaced, and a delete followed by an update removed
the file and then failed, leaving neither. Operations now apply in order,
each reading what the previous one wrote.
`*** Add File:` over an existing file was refused with an invented
`FS_ALREADY_EXISTS`; Codex records what it overwrote and writes. A bare
`*** Move to:` with no hunks was accepted; Codex rejects an empty chunk list
before it reads the rename. A leading newline, a trailing space on the
opening marker, and an indented envelope were all refused; Codex trims the
patch and each marker line, while comparing only `trim_end()` inside an
update body so an indented context line stays content. Content after
`*** End Patch` was silently dropped, so two envelopes in one call reported
success over operations that never ran.
Three defects in how it reached the filesystem, none of which fire under the
arm's own non-confining provider:
- `mkdir` ran on every write through `processPath`, the same way out of a
sandbox that delete and rename refuse for. A create outside a confining
workspace had its write denied and still left the directory behind.
- Delete and rename used the provider's realpath-derived target key, so
deleting a symlink destroyed the file it pointed at and left the link
dangling. They now use the lexical path, which is what Codex removes.
- Hunk failures reached the model as a bare `Invalid Context 0`, naming
neither the file nor the section, and two error codes were outside
`FsErrorCode`'s closed union.
The description's provenance note claimed one modification from upstream
where there are four, and NOTICE claimed the description dropped the delete
and move operations — true when written, false once they were implemented,
and wrong in the direction that matters for a modification notice.
84 plugin tests pass. Each divergence is pinned by a test naming the
upstream file and line it follows.
Generated-by: Claude Code
Review found two differences between the arms that were not the edit
contract, and a control test that could not have caught either.
Only the `fs` arm mounted `dsh-fs-observation-policy`, justified in the
profile by that family's guidance mentioning it. Checking how the harness
ships does not support this: of the four presets in `@deepseek-ai/dsh`,
three mount `dsh-tool-fs` and none mounts the policy, which is not even a
direct dependency; and the policy's own documentation covers the baseline's
editor too. It is substrate, not part of an editor. Mounting it in one arm
gave that arm two failure modes the others cannot have — an edit without a
prior read, and a file the model's own shell touched in between — on a
benchmark where shell and editor are used together constantly. No arm
mounts it now. The apply-patch profile's stated reason for omitting it was
also wrong, and is corrected rather than quietly dropped: that arm emits
`fs/observed` with the file's version before it writes, so the policy would
have allowed the write.
The two new arms configured `fs-local` with a `cwd` expression the baseline
did not have. It could not change anything — the provider defaults `cwd` to
`process.cwd()`, both tools pass the session's own cwd, and `DSH_CWD` is
never set — but an unreachable setting is still a difference a reviewer has
to rule out.
Read budgets are now matched. The baseline pins its editor to 16000 output
characters while `dsh-tool-fs` shipped a 51200-byte default, a 3.2x window
on the same task. How much of a file one call returns is not part of the
edit contract, so `readMaxBytes` is set to match. The remaining read-side
asymmetry, that the patch arm has no read tool at all, is contract-inherent
and stays.
The control test read each `- id:` row with the `name:` on the line below
and compared those strings, leaving every `config:` block unchecked.
Narrowing one arm's context window, halving an editor's output budget,
rewriting the shared bash description, reordering the rows, or overriding
the patch tool's own description all kept it green. It now parses the YAML
and compares whole entries, asserts composition order, and pins each arm's
editor row by value — the treatment was the one thing no assertion covered.
Verified by making each of those six edits and watching it fail.
The arm-to-directory table moves to `toolchain-verification.ts`, beside the
identity table, and the subject and the test both read it. The test used to
restate it, so an arm pointed at the wrong profile would have been checked
against the composition it was meant to run rather than the one it runs.
The toolchain fingerprint is re-pinned for the plugin changes in the parent
commit. Verified in a `linux/amd64` container: all three profiles compose,
the composed trees differ only in the editor row, and the plugin loads from
inside the toolchain and registers `apply_patch` with the full grammar.
Generated-by: Claude Code
The apply_patch arm applied one file's hunks with the OpenAI Agents SDK's
`applyDiff`, vendored on the assumption that one V4A implementation is like
another. It is not. Running the released `codex` binary over the same trees
shows four disagreements, each of which moves this arm's score: a context-free
`+` hunk lands at the `@@` anchor rather than at end of file, an updated file
keeps whatever trailing newline it had rather than gaining one, CRLF survives,
and near-miss context is refused where Codex's four matching passes accept it.
Replace it with a port of Codex's own applier and parser. The parser now reads
hunks into chunks instead of passing section bodies through, so a syntax error
in the third section is found before the first is written, and the tool no
longer needs a second project's operation shape.
Restore verify-then-write. An earlier round changed the tool to apply
operations sequentially, matching the standalone `apply_patch` binary. That
binary is not what a model calls: the function-tool handler and the intercepted
shell call both go through `try_verify_apply_patch_args`, which collects hunks
into a map keyed by resolved path, refuses a duplicate key, and computes every
change against the pre-patch tree. So an envelope naming one path twice is
refused, and a hunk whose context the model guessed at costs nothing.
Fidelity is now measured rather than argued. `codex-oracle.mjs` runs the real
binary over a temporary tree for each of 72 cases and records what it printed
and what the tree became; `codex-fixtures.test.mjs` replays every case through
the registered tool and compares. The two cases where the function tool
deliberately differs from the standalone binary are named there with what it
does instead. A fuzzer over 1800 random envelopes found no further divergence.
Also tighten the arm-control test, which had three gaps: it skipped composition
steps that override an already-composed entry, so an override of the patch
tool's description was invisible; it named the profile files to compare instead
of reading them; and it exempted the first subject's profile argument while
comparing only the argument vector, not the whole subject definition.
Toolchain fingerprint re-pinned. Test-only files move under `__tests__/` so the
build prunes them by directory rather than by filename.
Generated-by: Claude Code
`readMaxBytes: 16000` was justified as making "how much of a file one call
returns" equal across the two read-capable arms. It does not. The baseline
editor spends eight characters a line on line numbers plus a header; the fs
tool emits neither, so measured over this repository's own sources it returns
10% more lines for the same budget and 22% on the longest files.
The budget is still the right one, for a reason the note did not give: what
should be equal is what a read costs the context window, and the numbering the
baseline adds is part of its contract that the model pays tokens for either
way. Equalising delivered content would mean picking a budget from an assumed
average line length.
Record the measurement, and record the one genuine mismatch that remains — the
budgets are counted in UTF-16 code units against UTF-8 bytes, equal on ASCII
and not otherwise.
Generated-by: Claude Code
The fixtures pin the cases someone thought to write down. This searches for the
rest: it generates envelopes from random line soup, applies each with the
released binary and with the registered tool, and reports every input the two
disagree on. 2100 envelopes across seven seeds currently diverge on nothing.
Red-checked against three deliberate regressions — placing a context-free `+`
hunk at the `@@` anchor, dropping the trailing-newline normalisation, and
removing the punctuation-folding match pass. The third went unnoticed through
400 envelopes at first, because context lines drawn verbatim from the file only
ever exercise the exact-match pass; the generator now half-remembers a line the
way a model does, and catches it.
Envelopes name each path at most once, and a refusal is compared as a refusal
without comparing the tree, because those are exactly where the standalone
binary and the function tool are supposed to differ.
Generated-by: Claude Code
The function-tool handler verifies an envelope and then throws the result
away: `ApplyPatchRuntime::run` hands `req.action.patch` — the raw text the
model sent — back to `apply_patch_with_mode`, which re-parses it and applies
it sequentially against the live filesystem. The verified `changes` map is
carried for approval and telemetry only.
This tool verified and then wrote the plan it had just computed. That is not
a cheaper version of the same contract, and the reference binary shows the
difference in two cases now covered by tests: a rename onto a path a later
section also updates leaves that section failing against the renamed file,
and `*** Move to:` naming its own source writes the destination and then
unlinks it, deleting the file while reporting success. Both were previously
refused with an invented "multiple operations target" error, because the
verification map also claimed the move destination — upstream keys only on
`hunk.resolve_path()` and carries `move_path` as a value.
Also in the write path: `fs/edit-intent` yields `{version}` with no `kind`,
so passing it straight to `writeText` matched neither guard and silently
wrote unconditionally; the abort signal was not checked between operations;
and `rm` ran with `force: true`, reporting `D <path>` for a file that had
already gone.
In the applier: each fuzzy-matching pass now projects both sides once
instead of recomputing the punctuation fold per candidate position, and
replacements are rebuilt in one forward pass rather than spliced, which
threw a RangeError once a hunk added more lines than the argument limit.
The model-facing description is no longer transcribed into this file. It is
read from a byte-for-byte copy of the upstream artifact it came from, with
three named substitutions applied over it, so the claim that it differs in
exactly those places is checkable rather than asserted in a comment. Copying
the file rather than retyping it also removed a fourth difference that had
no reason to exist: upstream's typographic punctuation is now preserved.
Its recorded provenance was stale — the file it named no longer exists at
the pinned commit — and now points at the artifact that does.
One parameter description said the input was "Not JSON-wrapped". It is: the
harness registers JSON function tools and nothing else.
Generated-by: Claude Code
Upstream ships codex-rs/apply-patch/tests/fixtures/scenarios as, in its own
README's words, an end-to-end suite "meant to be easily portable to other
languages or platforms". This is the port: the 25 scenario directories are
copied verbatim from the pinned commit and replayed through the registered
tool, compared the way upstream compares them — copy `input/`, apply
`patch.txt`, ignore the exit status, and require the resulting tree to equal
`expected/` exactly.
It is the only check here with the standing to contradict the code. The
oracle fixtures run the reference binary, but over inputs this repository
chose; the fuzzer perturbs inputs this repository chose how to perturb.
These were chosen by upstream, before and independently of this tool.
22 of 25 match outright. Three do not, and each divergence is recorded with
its cause and pinned to bytes the reference binary produces:
- 015 specifies the standalone binary, which has no verification pass, so an
add preceding a failing update survives. The function tool writes nothing.
- 023 and 024 are specified under CODEX_APPLY_PATCH_PRESERVE_LINE_ENDINGS,
a feature that is `Stage::UnderDevelopment, default_enabled: false`, so a
shipped Codex and this arm both run the `NormalizeToLf` default.
Both divergence lists — here and in the oracle fixtures — now assert that
each entry still diverges, so an excuse cannot outlive the thing it excused.
Generated-by: Claude Code
The treatment in this arm is the tool name, the parameter it takes, and the
description text. None of the three had any coverage, so a build that
registered the tool under another name, dropped the parameter, or shipped a
truncated description would have run a cohort and produced numbers.
The description claim is the one that had to be argued rather than shown:
"three differences from upstream and no others" was a comment. It is now
checked at both ends. The vendored instructions file is pinned by digest,
with the `git show ... | sed -n` that reproduces it from the source recorded
beside the constant, and the built text is recomputed here by a different
mechanism than the code uses and required to match. Each adaptation must
apply exactly once, and no mention of the shell delivery may survive.
Red-checked against four sabotages — appending a line to the description,
renaming the tool, editing one byte of the vendored text, and neutering an
adaptation. Each is caught.
Generated-by: Claude Code
Two differences at the outermost layer of the patch, both pinned by new
oracle fixtures generated from the reference binary.
`parse_patch` selects its mode from `PARSE_IN_STRICT_MODE`, which is `false`:
upstream gave up on gating leniency by model, so every call site including
the function tool unwraps a heredoc-wrapped envelope. A model asked to send
a patch may well send the `<<'EOF' ... EOF` form it was shown, and Codex
applies it where this refused it. The strict boundaries are still tried
first, so an envelope whose own first line reads `<<EOF` is never stripped.
Rust's `str::trim` cuts the Unicode `White_Space` property; JavaScript's also
cuts U+FEFF and does not cut U+0085. Only U+FEFF can realistically occur — a
byte-order mark carried into a JSON string argument — and it made this tool
accept a patch Codex refuses, which is the wrong direction for an arm being
compared against the contract it imitates.
Red-checked: restoring `String.prototype.trim` fails the byte-order-mark
fixture, and disabling the heredoc strip fails both heredoc fixtures.
Generated-by: Claude Code
The comment justifying `readMaxBytes: 16000` said `str_replace_editor`
spends eight characters a line on line numbers and a header while "this tool
emits neither". Both tools number their lines, and this one wraps the result
in a `<path>/<type>/<content>` envelope with a footer.
The setting still holds something equal, but not what was claimed. The
baseline truncates the fully rendered string; this tool charges the budget
against raw line bytes plus separators and adds the gutter, the envelope and
the footer afterwards, outside it. Measured across this repository's 1480
TS/JS/Python sources at 16000, the arm delivers 1.105x the lines and emits
1.027x the characters, worst file 1.34x and 1.19x — a residual in this arm's
favour, now recorded with the mechanism that produces it.
The three profiles also claimed to have been "verified request-for-request"
against the upstream composition. That was a manual comparison made once
during development; nothing here reproduces it, and the headers now say so.
`MAKA_EVAL_DSH_PLUGINS` was exported into all three arms and read by one, so
the two controls carried an environment difference for a mechanism they do
not use. It is now set from the copied composition itself, which keeps the
condition and the file that depends on it from drifting apart.
Generated-by: Claude Code
`apply_patch` in Codex is a freeform tool: the API is sent a short description
and a Lark grammar the decoder is constrained to, so a model on that path
cannot emit a malformed envelope. The harness registers JSON function tools
and has no grammar seam, so this arm is the unconstrained case. That is not a
detail of the port — it is a cost charged to this arm and to no other, and it
now leads the arm's description instead of being absent from it.
Three further divergences are named with their causes: the function-tool path
against the standalone binary's sequential apply, `NormalizeToLf` against a
preserve-line-endings feature that ships disabled, and diagnostics more
specific than Codex's single `Invalid patch:` line, which is the one place
this arm may be easier on a model than the reference.
The README also described a verify-then-write-the-plan design the tool no
longer has, quoted the old fixture count, and repeated the read-budget
mechanism corrected in the previous commit.
The NOTICE opened by saying nothing was carried verbatim as a file; two
things now are. Its description provenance named a file that no longer exists
at the ported commit and a fourth modification that was an artefact of
retyping rather than copying, its fixture path was stale, its account of how
`PreserveLineEndings` is selected covered only the standalone binary, and its
list of ported functions omitted the ones that decide apply order.
The arm-control test read a composition step as an insertion whenever it had
an `insert` key, so `{insert, remove}` would have passed with the removal
unread, and it filtered directories out of each profile's file list rather
than refusing them. It also now names what it does not cover.
Generated-by: Claude Code
`dsh-fs-local` creates a write's parent itself, in `writeFileAtomic`, and it
is the provider every arm mounts. The `ensureParent` here ran `mkdir` on a
`processPath` — the same way around a confining sandbox that delete and
rename refuse for — and existed only because the test harness's provider was
weaker than the real one and failed with ENOENT without it.
That is the more useful half of this change: a fake that cannot do what the
real thing does will not merely miss bugs, it will make production code grow
to cover for it. The fake now creates the parent the way `writeFileAtomic`
does, and the tool has no call that creates a directory at all.
The test that pinned the old behaviour was passing for the wrong reason — it
asserted the tool left no directory behind, and what actually happened was
the fake's write failing before anything could be created.
Generated-by: Claude Code
…sses
`deriveNewContents` reported a context miss as a plain `Error`, and the
caller rewrapped anything it threw as `FS_EDIT_NOT_FOUND` — so a bug in the
applier would have reached the model as "the lines you named are not in the
file", which is advice to rewrite a correct patch. The context miss is now
its own type and the only thing translated; a comment already claimed this
and the code did not.
Two oracle fixtures pin the order of the four matching passes against each
other. The existing cases pinned the exact pass ahead of the loose ones;
between the loose ones, removing a pass does not fail a match, it silently
moves it to a different line, and nothing caught that. Each of the four now
fails at least one fixture when removed.
The line-ending note in codex-apply.mjs described only the standalone
binary's environment variable, not the feature flag the function tool reads.
The toolchain is rebuilt and its fingerprint re-pinned: the model-facing
instructions now ship as a file, so they are covered by `checksums.sha256`
rather than living only inside a JavaScript template. Verified inside the
pinned linux/amd64 container that the built tree registers one tool named
`apply_patch` with one required `input` string and a 2903-byte description
carrying no mention of the shell delivery.
Generated-by: Claude Code
Differential fuzzing against the reference binary found a regression this
branch introduced. `applyReplacements` was rewritten to build the file
forward in one pass, on the reasoning that the replacements are sorted and
disjoint. They are sorted; they are not always disjoint.
`computeReplacements` places a context-free insertion at the end of the file
without consulting the search position — deliberately, because that is what
`insertion_idx` does upstream — so it can land inside a range a later chunk
replaces. Applying descending, the replacement swallows it; applying forward,
the two are treated as separate edits and the inserted line survives into a
file the reference never produces.
Restored to splicing in descending order, with the replacement segment going
in in bounded batches so a hunk larger than the engine's argument limit still
does not throw. The case is now an oracle fixture, so it is caught without a
`codex` on PATH; red-checked by reinstating the forward rebuild.
2300 further envelopes across four seeds diverge on nothing. Toolchain
rebuilt and re-pinned.
Generated-by: Claude Code
An adversarial review pass found nine behaviours that were wrong or untested
in the V4A tool, each verified against the reference binary or upstream source
before being changed. All nine are now covered: sabotaging any one of them
fails the suite, where before five of them left it green.
The matcher used JavaScript's `trim` where upstream uses Rust's. The two
disagree on U+FEFF and U+0085, so a file whose first line carried a byte-order
mark matched a pattern without one and was written back with the mark deleted,
and a line terminated by NEL was refused where the reference applies it. The
charset the parser already spelled out moves to rust-trim.mjs and both use it.
The verify pass now does what `try_verify_apply_patch_args` does: nothing at
all for `*** Add File:`, and a read for `*** Delete File:` as well as for an
update. Statting a delete target instead applied a delete the reference
refuses, since `read_file_text` maps invalid UTF-8 to `InvalidData` and fails
the whole envelope. It also stops emitting `fs/observed {present}` off a bare
stat, which told an observation policy a file had been seen that nothing read.
The duplicate-path key is lexical, as upstream's `PathUri::join` is, not the
provider's realpath-derived target key. An envelope naming both a symlink and
its target was refused here; the binary applies both sections in order.
A move destination no longer runs the `fs/write-intent` waterfall and discards
the answer, which turned a decider's refusal to clobber into a silent
overwrite. A failed unlink and an abort between passes now arrive as `FsError`
with a routable code and the path the model wrote, not a raw errno and a
resolved host path.
The test provider was weaker than `dsh-fs-local` in four ways that made the
above invisible: it decoded lossily where the real one raises `FS_NOT_TEXT`,
called every non-directory a file, took three parameters so the signal and
sandbox policy could not be observed, and never returned a decider's answer.
Two oracle cases are added for the trim charset and one for a trailing U+FEFF,
which is what separates the three loose matching passes from each other; the
BOM-in-content case is recorded as a divergence with its true cause, which is
that the shared provider strips a leading mark before this tool sees the file.
Generated-by: Claude Code
The import landed the 25 scenario directories, their README and their
.gitattributes twice: once at `__tests__/upstream/` and once, byte-identically,
at `__tests__/upstream/scenarios/`. Only the nested copy is read.
Generated-by: Claude Code
…arms
The three arms are meant to differ only in the edit contract. Three settings
outside that contract were not held.
`readMaxLineLength` was left at its shipped 2000 while only `readMaxBytes` was
matched to the baseline. That is a cap the baseline has no counterpart to: the
fs arm could never see the 2001st character of any line, at any offset, while
`str_replace_editor` truncates once over the whole rendered view and can spend
its entire budget on one line. A minified bundle or a single-line payload is
where that lands, and it lands against one arm only. It is raised to the
budget, where it can no longer bind before `readMaxBytes` does.
`streamIdleTimeoutMs` was omitted from all three profiles, leaving the
adapter's 300000 ms default in force — five minutes between two streamed
tokens, behind `reasoningEffort: max` and a 65-minute bash deadline. Upstream's
own jsonrpc-agent composition sets 172800000, which is what the arms now carry;
a turn killed by that timeout would have been scored as a failure of whichever
contract was running.
`maxConcurrentTaskGroups` was 64. A group holds one cell per subject and starts
them together, so three arms made 192 concurrent trials where every other spec
here lands on 128. That does not bias one arm against another, but it puts this
run's `deepseek-harness` score under a load no single-arm run shares. It is now
43, and the test asserts the arithmetic rather than the number.
The read-budget residual quoted in the profile and the README had no
reproducible source and reported only the tail that favoured the fs arm.
`scripts/measure-read-budget.mjs` reimplements both renderers from the shipped
code and reports both tails; the figures are replaced with what it prints.
Generated-by: Claude Code
A claims audit checked every factual assertion in the README, the NOTICE and
the profiles against upstream source at the ported commit and against the
shipped harness packages. Fourteen did not hold.
The largest is provenance. `prompt_with_apply_patch_instructions.md` is not in
any shipped Codex system prompt at the ported commit: it is referenced only
from a test whose four model cases all declare
`expects_apply_patch_description: false`, and none of the eight instruction
templates in models.json carries the section. The file is upstream's own
authored description of the format and it is retired, which is a better reason
to use it than the one previously given, but the prose said Codex sends it.
The freeform description is 108 characters, not 105. The oracle runs 82 cases,
not 76. The fuzz figure is 2300 across four seeds, not 2100. Two of the named
divergences were recorded in the tests, not three, and there are now four with
the byte-order mark the provider strips. `verifyToolchainDirectory` covers ten
registered profiles, not eight. `checksums.sha256` omits itself and
manifest.json. The toolchain build's `__tests__` prune is scoped to the plugins
it copies. The build script runs from the repository root. `ctx.fs` offers
twelve methods, not three. The 0.147.0 delta includes a second behavioural
difference — the duplicate-path check does not exist at that tag — and a larger
restructure than the three items named. The profiles record three deviations
from upstream, not one.
Also stated where it was missing: the arm's lack of a read tool is Codex's
contract rather than a handicap, since Codex registers no file-read tool
either and every arm has the same bash.
Generated-by: Claude Code
A second adversarial round found that the previous round's fix traded one
divergence for another, and that twelve mutations of this tool still left the
whole suite green.
`ctx.fs.readText` is not `read_file_text`. Upstream reads bytes and calls
`String::from_utf8`, so every valid UTF-8 sequence is text; `dsh-fs-local`
adds two rules on top, and both are model-visible. It rejects a NUL in the
first 8192 bytes as binary — but NUL is valid UTF-8, and the reference patches
and deletes straight through it, verified against the binary — so making the
verify pass read delete targets took `*** Delete File:` on such a file with
it. And it decodes with `ignoreBOM` left false, which strips a leading
byte-order mark, so a patch whose context omits the mark applied here, was
refused there, and wrote the file back without the mark. Reading through
`ctx.fs.readBytes` and decoding here is exactly `String::from_utf8`; it fixes
both, and the byte-order-mark divergence is deleted rather than documented.
`*** End of File` as the first line of an update hunk was refused. Upstream
guards with `chunks.last().is_some_and(blank)`, which is false when there is
no chunk, so the marker is ignored; the binary applies the envelope.
Twelve mutations that the suite could not see, now each red-checked: five
dropped the turn signal from a provider call, which a fake that ignored the
signal could not notice; two removed or falsified the version on `fs/observed`;
two swapped a removal's announcement with the removal itself, which leaves the
event list identical and the trail a lie; one bypassed the create decider and
one renamed its event; one reduced the sandbox-denial remap to a no-op. The
fake now honours and records the signal on every call, records what the
filesystem actually held when each observation was published, records the
version `writeText` returned, and can be made to deny a write.
Twelve oracle cases added, generated from the binary: three for the read seam
and nine for shapes that separate this port from a plausible variant of it —
overlapping chunks, a backward `@@` anchor, an indented header after an add
body, a trailing mark on an End of File marker, header paths with a leading
space, a repeated `*** Move to:`, a heredoc close with trailing text, and the
End of File case above. Eleven passed unchanged, which is the point of adding
them; one was the defect.
`requireDirectFilesystem` now raises an `FsError` like every other refusal.
Generated-by: Claude Code
… meant to help
Raising `readMaxLineLength` from its shipped 2000 to the 16000 budget was
wrong, and worse than the asymmetry it was meant to close. `dsh-tool-fs`
truncates a long line and appends a `... (line truncated to N chars)` marker,
then charges the whole thing against `readMaxBytes`. Once the cap approaches
the budget the truncated line no longer fits at all: the window returns zero
lines, drops every line after it, and the footer invites the model to retry at
the offset it just used, so the rest of the file is unreachable. Measured
against the shipped functions, a 20000-character line followed by 200 short
ones delivers 201 lines at 2000 and 0 lines at 16000.
No value equalises the two budgets anyway — one counts UTF-16 code units and
the other UTF-8 bytes — so the cap goes back to the value the tool ships with
and the residual is measured instead of engineered away. At 2000 the direction
is also not the one the earlier reasoning assumed: on that same long-line file
the baseline spends its whole budget on the one line and delivers 1, while
this arm truncates it, says so, and reads on.
The guard test now asserts the invariant rather than the number, so any future
cap has to be one a truncated line can still fit under.
The measurement script had four transcription errors, all inflating the
character ratio: it dropped the baseline's 240-character truncation notice and
the fs arm's per-line marker, counted the baseline's prompt line as content,
and counted a phantom trailing line for the fs arm. It also silently ate the
first positional path when `--budget` was absent. Rather than fix the
transcription and hope, `--self-check <dir>` now lifts the real functions out
of an installed copy and asserts agreement, and the corpus figures are
reported alongside synthetic long-line cases that the repository's own sources
do not contain. Corrected: 2197 files, 1.093x lines, 1.001x characters, tails
reported as what they are — extremes among the 570 files where a budget binds.
`maxConcurrentTaskGroups` moves 43 to 42: three arms at 43 is 129 trials, and
the assertion had been written as `<= 129` to admit it.
Generated-by: Claude Code
The claim that upstream's `## apply_patch` prose is retired was too strong. Of
the eight instruction templates in `models-manager/models.json` at the ported
commit, seven carry no such section — but `gpt-5.2` carries a shorter one with
the same envelope, the same three headers and the same worked example. So the
format is still taught in prose upstream, and the vendored file is the fuller
version of that prose rather than something abandoned. The conclusion it was
supporting survives unchanged and for a cleaner reason: `gpt-5.2` gets the
prose *and* the grammar, so no shipped configuration is the one this arm is in.
Codex's handler set is much longer than the six entries named, and one of them
takes a filesystem path — `view_image`, for images. The load-bearing claim is
that no handler reads file *text*, which is what the prose now says.
The NOTICE gave the wrong cause for the duplicate-path divergence: the
recorded binary composes an envelope naming one path twice because the
standalone applier has no verification pass at either version, not because the
check is absent at rust-v0.147.0 — that is a function-tool difference.
Also: `invocation.rs:246-248` is the delete arm, not the add arm; the
`fs/edit-intent` shape citation named only one of the two declarations; the
end-of-file search has no fallback to the ordinary start position, contrary to
its comment; `__tests__/upstream/` kept a stray `.gitattributes` naming paths
one level down; and the build script's header credited `npm i` for a
non-reproducible fingerprint when it runs `npm ci` and the real cause is native
compilation.
The refusal under a confining provider is described as what it is: dead code
under `dsh-fs-local`, which never sets `sandboxMode` at any sandbox mode, not
something `danger-full-access` switches off.
Generated-by: Claude Code
…t reads it
The comment justifying the conditional export had the causation backwards. The
model's shell inherits this environment, so setting the variable in all three
arms is what makes them identical, and setting it in one arm alone is what
creates a difference the model can read with `env`. It names a directory and
starts nothing, so it is inert in the two arms whose compositions never mention
it.
The remaining environment asymmetry, `$HOME` carrying the profile name, is
recorded rather than removed: the arms need separate homes because each writes
its own composition under `$DSH_HOME/profiles`, and sharing one would have each
overwrite what the others booted from. So the experiment is not blind to its
subject, and that is now stated where the home is built.
Generated-by: Claude Code
The spec declared 42 task groups, or 126 concurrent trials, on the
strength of a 128-trial convention rather than a machine. The account's
CVM quota is 60 vCPU per zone, which caps the host at 56 vCPU and
256 GiB, and the only completed run of this suite put 24 trials on
32 vCPU / 64 GiB — 2.7 GiB per trial. 126 trials on that host is 2.0 GiB
each, close enough to a Terminal Bench build task's peak that an OOM
kill would land as an infra failure. 32 groups is 96 trials at the
density that is known to survive.
Generated-by: Claude Code
The arm named its plugin with `name: !!js process.env.MAKA_EVAL_DSH_PLUGINS
+ '/tool-apply-patch/index.mjs'`. The harness's loader evaluates that tag for
`disabled` alone and hands `name` the unevaluated node, so every trial of
that arm died at boot with `name.startsWith is not a function` before it
reached the model — on a rented host, at the first smoke task.
The row now names `./plugins/tool-apply-patch/index.mjs`, resolved against the
profile directory, and the subject plants `plugins` there as a link into the
toolchain. That keeps what the loader imports inside the fingerprinted
read-only tree, keeps the composition a plain string, and keeps the path
independent of the root the wrapper was given — which naming the mount point
outright would not. The link is planted for all three arms, on the same
reasoning that exported the variable for all three: the model's shell can read
$DSH_HOME, so planting it in one arm alone is the difference, not planting it
everywhere.
The test could not have caught this. It extended its own YAML schema to
construct `!!js <source>` as a string, which taught the assertion to accept a
form the harness cannot load. The tag is real elsewhere in these compositions —
`task: !!js ctx.headlessStartup.task` is upstream's — so the schema now
constructs it as the marker shape the harness's loader produces, and a new
assertion requires every entry name to be a string and every path-named plugin
to be one the toolchain build ships. Reverting the row fails both.
Generated-by: Claude Code
The wrapper counted the subject's stderr bytes and hashed them, then dropped
them. The relay writes `maka-subject.stderr.txt` from the wrapper's own
stderr, which is empty, so a failing arm's own account of what killed it
reached the attempt record as a byte count and a digest and nothing else.
Both failures this run has had were diagnosed by reproducing them by hand on
the host instead: a composition the harness could not load, and a subject that
exits after one model response. The second is still open.
The last 64 KiB now land in `/logs/agent/<profile>.stderr.txt`, which the
relay already collects, as a `stderr-tail` artifact beside the existing
`stderr` count. A tail because a subject that loops printing would otherwise
fill the trial's disk, and because a harness says what killed it on the way
out; the count and digest still describe the whole stream, so truncation is
visible.
Generated-by: Claude Code
Announcing it as an artifact pushed the frame past the relay's 2 KiB payload
cap, so every cell came back `external subject result transport failed`.
`/logs/agent` is collected into the trial directory regardless, so writing the
file is the whole job.
Generated-by: Claude Code
Every failure before the subject starts reached the operator as the string
`external subject setup failed`, because `safeFailure` sanitizes what the
published result frame may say and nothing recorded the error itself. The
process's own stderr is collected into the trial as
`maka-subject.stderr.txt`, which is where a subject's account of its own
failure belongs, so the error, its stack, and its cause chain go there.
Generated-by: Claude Code
This module does its work at the top level, above the function that reads the
subject's stderr, so a `const` declared beside that reader is still in its
temporal dead zone when the reader runs. Every cell died with
`ReferenceError: Cannot access 'STDERR_TAIL_BYTES' before initialization`
before the subject's first model response — reported as
`external subject setup failed`, and legible only because the commit before
this one started writing the error itself.
Generated-by: Claude Code
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat(eval): compare three edit contracts on the DeepSeek Harness - #3067

Closed
Astro-Han wants to merge 33 commits into
feat/eval-deepseek-harness-subjectfrom
feat/eval-edit-contract-arms
Closed

feat(eval): compare three edit contracts on the DeepSeek Harness#3067
Astro-Han wants to merge 33 commits into
feat/eval-deepseek-harness-subjectfrom
feat/eval-edit-contract-arms

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Summary

A Terminal-Bench score is attributed to a framework, which leaves the edit contract inside the framework and unmeasured. Comparing Maka's edit/write against the harness's str_replace_editor cannot separate them either — that comparison moves the framework and the contract at once.

These three arms hold everything else and move only the contract:

armcontract
deepseek-harnessstr_replace_editor — one tool, four commands, unique-literal match, all guidance in the tool description
deepseek-harness-fsthe harness's read/write/edit — three tools, snake_case, three prompt sections of their own
deepseek-harness-apply-patchV4A apply_patch — one envelope, changes located by surrounding context

Same one-shot CLI, same toolchain and fingerprint, same dsh-fs-local provider under every editor, byte-identical package.json and cordis.yml. The variable is the whole tool family and not one dimension of it: tool count, argument names, path conventions and where the guidance lives move together, because each family ships that way. A result is attributable to the family as shipped, not to patch syntax as such.

No arm mounts dsh-fs-observation-policy. It was mounted for the fs arm alone on the reasoning that its tools' guidance mentions the policy; of the four presets in @deepseek-ai/dsh, three mount dsh-tool-fs and none mounts the policy, and its own documentation covers str_replace_editor too. Mounting it in one arm gave that arm two failure modes the others cannot have.

Three compositions in three files can drift apart while still producing numbers, so the control is asserted rather than intended. edit-contract-arms.test.ts fails when the compositions differ by any row outside their editors, when they compose in a different order, when the model, context window, reasoning effort, stream idle timeout, sandbox mode, persona or either Bash deadline drifts in one, when the arms stop sharing one toolchain identity object, when the experiment launches them with anything but a different profile argument, or when arms multiplied by concurrency leaves the 128-trial convention.

The patch arm is ours, and that is the thing to review

The harness ships no patch tool, so plugins/tool-apply-patch/ is this repository's while the other two arms run their vendor's. Fidelity to Codex is therefore itself an experimental variable, and it is asymmetric: a tool stricter than the reference costs its arm turns the reference never spends, and a safer one wins recoveries the reference never gets.

One deviation cannot be removed.apply_patch in Codex is a freeform tool — the API is sent a 108-character description plus a Lark grammar the decoder is constrained to, so a model on that path cannot emit a malformed envelope. The harness registers JSON function tools and has no grammar seam. This arm is therefore the unconstrained case, and syntax errors it can make are errors Codex's decoder would have prevented. That cost is charged to this arm and to no other, and it leads the arm's description in the README.

What is not charged to it is the absence of a read tool. Codex registers no handler that reads file text either — its handler set is long, and the only entry taking a filesystem path to read is view_image, which takes images — so an arm that reads with cat is that contract, and every arm has the same bash at the same 16000-character cap.

What the model is told is upstream's own prose for exactly that case: the ## apply_patch section of prompt_with_apply_patch_instructions.md, carried verbatim as upstream-apply-patch-instructions.md with three substitutions applied over it. A test pins the file's digest, requires each substitution to apply exactly once, and requires the built description to differ from the copy nowhere else. That file is upstream's and it is not delivered from where it sits: at the ported commit it is referenced only from a test whose four model cases all declare expects_apply_patch_description: false. Seven of the eight instruction templates in models-manager/models.json carry no ## apply_patch section and leave the format to the grammar; the eighth, gpt-5.2, carries a shorter one with the same envelope and worked example. So the format is still taught in prose upstream and this is the fuller version of it. Either way no shipped configuration is the one this arm is in — gpt-5.2 gets prose and grammar — so the choice was between upstream's own text and text written here.

Three further divergences are known and each is pinned to bytes the reference produces:

  • the tool implements the function-tool path — verify the whole envelope, then re-apply it from the raw text — where the standalone binary applies sequentially with no verification;
  • it runs NormalizeToLf, the shipped default, where PreserveLineEndings is Stage::UnderDevelopment, default_enabled: false upstream;
  • its diagnostics differ from Codex's, which is the one place this arm may be easier on a model. This one is recorded in prose only; the other two are named with their causes in the test files, which also assert that each still diverges.

A third divergence was recorded here and has since been removed rather than documented: the provider's readText strips a leading byte-order mark and rejects a NUL byte, neither of which String::from_utf8 does. Reading through readBytes and decoding in the tool matches the reference exactly.

The grammar is implemented in full. *** Delete File: and *** Move to: are the only operations ctx.fs has no primitive for, so they run against ctx.fs.processPath(target); under a provider that confines, both refuse up front rather than escape. This arm mounts danger-full-access, where the refusal never fires.

Sources and licences are in the plugins' NOTICE. An earlier revision of this branch vendored the OpenAI Agents SDK's applyDiff as the hunk applier; it is a different V4A implementation, not Codex's, and nothing from it remains.

What the adversarial review rounds changed

Two rounds of four fresh-eye passes each — the port against the binary, the harness integration, the three-arm control, and an audit of every factual claim. The second round reviewed the first round's fixes, which is what caught the two entries below that begin "the previous fix". Every finding was re-verified against upstream source or the reference binary before being acted on.

Nine port defects, all now red-checked. The matcher used JavaScript's trim where upstream uses Rust's, so a file whose first line carried a byte-order mark matched a pattern without one and was written back with the mark deleted, and a line terminated by U+0085 was refused where the reference applies it. The verify pass stated a delete target instead of reading it, applying deletes of non-UTF-8 files that the reference refuses outright. It emitted fs/observed {present} off a bare stat, telling an observation policy a file had been seen that nothing read. The duplicate-path key was the provider's realpath-derived target key rather than upstream's lexical join, so an envelope naming both a symlink and its target was refused where the binary applies both. A move destination ran the fs/write-intent waterfall and discarded the answer, turning a decider's refusal to clobber into a silent overwrite. A failed unlink and an abort between passes escaped the seam's error taxonomy.

Five of those were invisible because the test provider was weaker than dsh-fs-local: it decoded lossily, called every non-directory a file, took three parameters so the signal and sandbox policy could not be observed, and never returned a decider's answer.

The previous fix read patch targets through the wrong seam.ctx.fs.readText is not read_file_text: upstream reads bytes and calls String::from_utf8, while dsh-fs-local additionally rejects a NUL in the first 8192 bytes and strips a leading byte-order mark. NUL is valid UTF-8 and the reference patches and deletes straight through it, so making the verify pass read delete targets — itself a correct fix — took *** Delete File: on such a file with it. Reading through readBytes and decoding in the tool is exactly String::from_utf8, and it removes the byte-order-mark divergence too. Separately, *** End of File as the first line of an update hunk was refused where upstream ignores it.

Twelve more mutations the suite could not see. Five dropped the turn signal from a provider call, which a fake that ignored the signal could not notice; two removed or falsified the version on fs/observed; two swapped a removal's announcement with the removal itself, which leaves the event list identical and the trail a lie; one bypassed the create decider, one renamed its event, one made the sandbox-denial remap a no-op. The fake now honours and records the signal on every call, records what the filesystem held when each observation was published, records the version writeText returned, and can be made to deny a write.

Three uncontrolled differences between the arms, of which one fix was itself wrong. streamIdleTimeoutMs was omitted from all three profiles, leaving a five-minute ceiling on the gap between two streamed tokens behind reasoningEffort: max and a 65-minute bash deadline; it now carries upstream's own 172800000. maxConcurrentTaskGroups: 64 made 192 concurrent trials where the saturating specs here land on 128; it is now 42.

readMaxLineLength was raised from its shipped 2000 to the budget to close a cap the baseline has no counterpart to, and that was worse than the asymmetry. The tool appends a ... (line truncated to N chars) marker and then charges the whole line against readMaxBytes, so a cap near the budget makes a long line not fit at all, drops every line after it, and returns a window no offset recovers — 201 lines at 2000 becomes 0 at 16000. No value equalises the two budgets anyway, one counting UTF-16 code units and the other UTF-8 bytes, so the cap is back where the tool ships it and the guard test asserts the invariant rather than the number.

Twenty-one false claims across the two rounds, listed in the two docs commits. The largest was the provenance one, twice: first that the vendored prose is what Codex sends, then that it is retired. The read-budget script had four transcription errors of its own, all inflating the character ratio, so it no longer asks to be trusted — --self-check <dir> lifts the real renderers out of an installed copy and asserts agreement, and the figures are 1.093x lines and 1.001x characters over 2197 files.

Verification

Lint, format and typecheck clean. 47 TypeScript and 200 plugin tests pass.

Fidelity is measured against the binary rather than argued from the Rust:

  • codex-oracle.mjs runs the released codex under argv0 apply_patch over a temporary tree for each of 94 cases and records what it printed and what the tree became; codex-fixtures.test.mjs replays every case through the registered tool and needs no codex to do it.
  • __tests__/upstream/scenarios/ carries Codex's own conformance suite verbatim — the 25 scenario directories upstream publishes as "meant to be easily portable to other languages or platforms" — and upstream-scenarios.test.mjs replays them upstream's way: copy the input tree, apply the patch, ignore the exit status, require the resulting tree to match exactly. 22 match outright; the three that do not are named divergences. These are the only inputs here this repository did not choose.
  • codex-fuzz.mjs generates envelopes from random line soup, including near-miss context, and runs both. It earned its keep on this branch: it caught a regression introduced here, where rebuilding the file forward let a context-free insertion survive inside a range a later chunk replaces. 2300 envelopes across four seeds diverge on nothing.

Red-checked, not just green:

  • Appending a line to the description, renaming the tool, editing one byte of the vendored instructions, or neutering a substitution each fails the contract-surface test.
  • Removing any one of the four context-matching passes fails at least one oracle fixture; restoring String.prototype.trim in the loose passes fails the trailing-U+FEFF fixture; disabling the heredoc strip fails both heredoc fixtures; matching the heredoc close with includes instead of endsWith fails the trailing-text fixture.
  • Each of the twenty-one port defects and unseen mutations above fails the suite when reintroduced. Seventeen of them did not before these two rounds.
  • Both divergence lists assert that each entry still diverges, so an excuse cannot outlive the thing it excused.

End-to-end against the rebuilt linux/amd64 toolchain: loading the plugin from inside the built tree registers exactly one tool named apply_patch, with one required input string and a 2903-byte description carrying no mention of the shell delivery. The instructions file ships, so it is covered by checksums.sha256 and the re-pinned fingerprint.

Not run: an actual benchmark cohort. No dependency moved.

Review focus

One claim this repository does not reproduce, and which should be read as a one-time manual check rather than a standing guarantee: the three profiles were compared against the upstream examples/jsonrpc-agent composition during development — tool names, tool schemas, system prompt and message sequence on the first outbound request — and found identical.

The read budget matched at 16000 equalises the budget's number, not delivered content. scripts/measure-read-budget.mjs makes the residual checkable and --self-check makes the script itself checkable: over 2197 tracked sources the fs arm delivers 1.093x the lines and 1.001x the characters; among the 570 files where a budget binds, the extremes are 1.398x/1.168x and 0.837x/0.720x.

Two things a write-up must carry that this branch cannot fix. The fs arm receives 764 characters of tool-contributed system-prompt guidance where the other two receive none, and two of its three sections reference an fs-observation-policy no arm mounts. And the treatment is the whole tool family plus that guidance, not "the edit contract" — the arm names are shorter than the thing being varied.

experiments/.../edit-contracts.json declares repetitions: 1, so 89 tasks x 3 subjects is 267 cells. Cost, not power, chose that. McNemar at one repetition is weak for the effect sizes this is likely to see: on ~20% discordant pairs a true 5pp difference is about 2 sigma before any correction for three pairwise comparisons, so a write-up has to report discordant counts and intervals rather than three success rates.

Stacked on #2971.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes — described under Summary above
  • No

The harness ships no patch tool, and an Eval arm that gives the model
Codex's patch language needs one composed the same way its other editors
are: `inject: ['tools', 'fs']`, one `ctx.tools.register(defineTool(...))`,
and every file operation through `ctx.fs`. Reaching around that seam would
put a second filesystem authority in one arm.
Applying a single file's hunks — context matching with the fuzz ladder — is
the part that is hard to get right, so it is not written here. It is the
OpenAI Agents SDK's `applyDiff`, vendored rather than depended on:
`@openai/agents-core` unpacks to 9 MB and pulls the whole `openai` SDK into
a tree that is fingerprinted file by file, while the function itself has no
imports at all. The upstream test suite is vendored with it, which is what
turns a mechanical tsc emit from an assertion into a checked fact; its two
import lines are the only edit. Biome's formatter now skips
`packages/eval/harbor/**/vendor/**` for the reason it already skips the
licence artifacts: a rewritten vendor file is no longer the upstream file.
What is written here is the envelope parser and the tool. The grammar is
transcribed from Codex's lark definition, and the model-facing description
is Codex's own instructions text. Two deviations are deliberate and are
stated to the model rather than left to be discovered by failure:
- No `*** Delete File:` and no `*** Move to:`. `ctx.fs` has no delete or
rename primitive and neither does any other harness file tool, so in
every arm the model removes and renames through bash.
- A patch either applies completely or changes nothing. `applyDiff` is
pure, so every file's new content is computed before the first write.
The contracts this will be compared against touch one file per call and
cannot half-apply; a partly-patched tree would be a difference in
failure modes rather than in edit format.
Sources and licences are recorded in the plugins' NOTICE. The three test
files run from the repository through `test:dist`, against the same
`@deepseek-ai/dsh-tools` and `@deepseek-ai/dsh-fs` the toolchain installs —
added as exact-pinned dev dependencies, which the lockfile takes as pure
additions with no existing resolution changed.
No arm composes this tool yet.
Generated-by: Claude Code
Terminal-Bench results attribute a score to a framework, which leaves the
edit contract inside the framework and unmeasured. Comparing Maka's
`edit`/`write` against the harness's `str_replace_editor` cannot separate
them either: that comparison moves the framework and the contract at once.
So this holds everything else and moves only the contract. Three arms run
the same one-shot CLI, the same toolchain and fingerprint, the same
`dsh-fs-local` provider under every editor, and byte-identical
`package.json` and `cordis.yml`:
- `deepseek-harness` keeps `str_replace_editor` — one tool, four
commands, unique-literal match, all guidance in the tool description.
- `deepseek-harness-fs` takes the harness's own `read`/`write`/`edit` —
three tools, snake_case arguments, three prompt sections of their own.
- `deepseek-harness-apply-patch` takes the V4A tool added previously —
one envelope, changes located by surrounding context.
The variable is the whole contract, not one dimension of it. Tool count,
argument names, path conventions, where the guidance lives, and the
read-before-edit policy move together because each family ships that way.
`dsh-fs-observation-policy` is mounted for the `fs` arm alone for that
reason: its tools' own guidance text tells the model the policy is in
force, so omitting it would leave the prompt describing a rule the
deployment does not enforce, while `apply_patch` has no such concept and no
read tool to satisfy one with.
Three compositions in three files can drift apart while still producing
numbers, and the numbers would no longer be about the contract. So the
control is asserted rather than intended: the new test fails when the
compositions differ by any row outside their editors, when the model,
reasoning effort, sandbox mode, persona or either Bash deadline drifts in
one of them, when the arms stop sharing one toolchain identity object, or
when the experiment launches them with anything but a different profile
argument. The arms also share one preparer, so a change to how the harness
is launched cannot reach one and miss another, and one identity constant,
so a re-pinned fingerprint cannot be applied to two arms out of three.
The toolchain build now installs the plugin tree beside the harness's
`node_modules` — a composition copied under `$DSH_HOME` cannot resolve a
plugin by package name from there — and fails if a declared plugin did not
land. Its dependencies are unchanged: every package the new arms compose
was already in the reviewed lockfile.
Re-pinning the fingerprint needs a `linux/amd64` container build and is not
done here, so the arms are not yet runnable end to end.
Generated-by: Claude Code
The toolchain now carries the `apply_patch` plugin beside the harness's
`node_modules`, so its checksum manifest changed and the pin has to follow.
Rebuilt with the same pinned `linux/amd64` image and the same reviewed
lockfile; no dependency moved.
Verified against the rebuilt tree rather than assumed. `dsh --dump-config`
composes all three arm profiles successfully, and the only differences
between the three composed trees are the editor rows and the `cwd` the two
new arms give the filesystem backend — nothing else diverged. Loading the
plugin from inside the toolchain resolves `@deepseek-ai/dsh-tools`,
`dsh-fs`, `dsh-sandbox` and `schemastery` through the upward walk into
`lib/dsh/node_modules`, registers `apply_patch` with the expected schema,
and carries the patch instructions without the operations the tool does not
implement. `dsh-tool-fs` and `dsh-fs-observation-policy` resolve too.
Generated-by: Claude Code
@coderabbitai

coderabbitaiBot commented Aug 15, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1357f048-8ea6-474e-8887-d512c43407ac

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

The tool shipped without them, refusing both with a pointer to the shell.
The stated reason was that `ctx.fs` has no delete or rename primitive, so
reaching them through `node:fs` would put a second filesystem authority in
the one arm being measured. That reason does not survive contact with the
deployment it was written for: this arm mounts `dsh-sandbox-local` at
`danger-full-access`, no observation policy is loaded, and the model's own
bash can already remove any file in the container. The authority being
protected was not enforcing anything.
The cost was real, and it was the point of the arm. `apply_patch` exists
here to measure Codex's contract; a patch tool missing a third of its
grammar is a different contract, and a model trained on the real one pays
for the difference in wasted turns. It also broke this PR's own rule —
every other arm composes what its tool family ships, which is exactly why
the `fs` arm mounts the observation policy. Two standards in one change.
So both operations are implemented, against `ctx.fs.processPath(target)` —
the provider's own answer to where a file is for something outside it to
open, and for `dsh-fs-local` the local absolute path. `ctx.fs` stays the
only thing that resolves paths; only the two syscalls it does not offer
happen outside it. Semantics follow Codex: a rename writes the destination
and then removes the source, overwrites an existing destination rather than
refusing, and creates missing parents.
Where the original concern is real, it is enforced instead of assumed: under
a provider that confines, `processPath` would be the way around the sandbox,
so delete and rename refuse up front. `sandboxMode` is what decides, so the
refusal fires exactly when a provider actually confines.
The parser already implemented the whole grammar, so no change there. The
tool description loses its "not part of this tool" paragraph and returns to
Codex's text, now with one removal rather than three: the trailing shell
invocation example, which is false for a function tool.
Tests move from an in-memory store to a real temporary directory, since
these two operations are syscalls an in-memory store cannot observe.
Red-checked: stubbing out the removal fails five behaviour tests, and
disabling the confinement guard fails the two that cover it.
Generated-by: Claude Code
The plugin gained its delete and rename paths, so the file it contributes to
the toolchain changed and the checksum manifest with it. Rebuilt from the
same pinned image and the same lockfile; no dependency moved.
Verified against the rebuilt tree: all three arm profiles still compose
under `dsh --dump-config`, and loading the plugin from inside the toolchain
registers `apply_patch` carrying the full grammar — `*** Delete File:`,
`*** Move to:`, and both productions — with the shell-invocation example
still absent.
Generated-by: Claude Code
Independent review of this arm found five places where the tool behaved
differently from the reference it exists to measure. The asymmetry matters
more than the count: a tool stricter than Codex costs its arm turns the
reference never spends, and a tool safer than Codex wins its arm recoveries
the reference never gets. Either way the benchmark reads the difference as
an effect of the edit contract.
`*** Add File:` did not terminate the created file with a newline, because
the vendored applier joins its added lines while Codex appends `\n` after
each one. Every file this arm created differed from Codex's byte for byte,
and a test pinned the wrong result. Each added line is now terminated, and
an empty create still writes nothing, as `String::new()` does upstream.
The envelope was planned against one snapshot and written afterwards, so a
failed hunk left the tree untouched. That is the nicer property and it was
still wrong: two sections naming one path made the second fail against a
version the first had replaced, and a delete followed by an update removed
the file and then failed, leaving neither. Operations now apply in order,
each reading what the previous one wrote.
`*** Add File:` over an existing file was refused with an invented
`FS_ALREADY_EXISTS`; Codex records what it overwrote and writes. A bare
`*** Move to:` with no hunks was accepted; Codex rejects an empty chunk list
before it reads the rename. A leading newline, a trailing space on the
opening marker, and an indented envelope were all refused; Codex trims the
patch and each marker line, while comparing only `trim_end()` inside an
update body so an indented context line stays content. Content after
`*** End Patch` was silently dropped, so two envelopes in one call reported
success over operations that never ran.
Three defects in how it reached the filesystem, none of which fire under the
arm's own non-confining provider:
- `mkdir` ran on every write through `processPath`, the same way out of a
sandbox that delete and rename refuse for. A create outside a confining
workspace had its write denied and still left the directory behind.
- Delete and rename used the provider's realpath-derived target key, so
deleting a symlink destroyed the file it pointed at and left the link
dangling. They now use the lexical path, which is what Codex removes.
- Hunk failures reached the model as a bare `Invalid Context 0`, naming
neither the file nor the section, and two error codes were outside
`FsErrorCode`'s closed union.
The description's provenance note claimed one modification from upstream
where there are four, and NOTICE claimed the description dropped the delete
and move operations — true when written, false once they were implemented,
and wrong in the direction that matters for a modification notice.
84 plugin tests pass. Each divergence is pinned by a test naming the
upstream file and line it follows.
Generated-by: Claude Code
Review found two differences between the arms that were not the edit
contract, and a control test that could not have caught either.
Only the `fs` arm mounted `dsh-fs-observation-policy`, justified in the
profile by that family's guidance mentioning it. Checking how the harness
ships does not support this: of the four presets in `@deepseek-ai/dsh`,
three mount `dsh-tool-fs` and none mounts the policy, which is not even a
direct dependency; and the policy's own documentation covers the baseline's
editor too. It is substrate, not part of an editor. Mounting it in one arm
gave that arm two failure modes the others cannot have — an edit without a
prior read, and a file the model's own shell touched in between — on a
benchmark where shell and editor are used together constantly. No arm
mounts it now. The apply-patch profile's stated reason for omitting it was
also wrong, and is corrected rather than quietly dropped: that arm emits
`fs/observed` with the file's version before it writes, so the policy would
have allowed the write.
The two new arms configured `fs-local` with a `cwd` expression the baseline
did not have. It could not change anything — the provider defaults `cwd` to
`process.cwd()`, both tools pass the session's own cwd, and `DSH_CWD` is
never set — but an unreachable setting is still a difference a reviewer has
to rule out.
Read budgets are now matched. The baseline pins its editor to 16000 output
characters while `dsh-tool-fs` shipped a 51200-byte default, a 3.2x window
on the same task. How much of a file one call returns is not part of the
edit contract, so `readMaxBytes` is set to match. The remaining read-side
asymmetry, that the patch arm has no read tool at all, is contract-inherent
and stays.
The control test read each `- id:` row with the `name:` on the line below
and compared those strings, leaving every `config:` block unchecked.
Narrowing one arm's context window, halving an editor's output budget,
rewriting the shared bash description, reordering the rows, or overriding
the patch tool's own description all kept it green. It now parses the YAML
and compares whole entries, asserts composition order, and pins each arm's
editor row by value — the treatment was the one thing no assertion covered.
Verified by making each of those six edits and watching it fail.
The arm-to-directory table moves to `toolchain-verification.ts`, beside the
identity table, and the subject and the test both read it. The test used to
restate it, so an arm pointed at the wrong profile would have been checked
against the composition it was meant to run rather than the one it runs.
The toolchain fingerprint is re-pinned for the plugin changes in the parent
commit. Verified in a `linux/amd64` container: all three profiles compose,
the composed trees differ only in the editor row, and the plugin loads from
inside the toolchain and registers `apply_patch` with the full grammar.
Generated-by: Claude Code
The apply_patch arm applied one file's hunks with the OpenAI Agents SDK's
`applyDiff`, vendored on the assumption that one V4A implementation is like
another. It is not. Running the released `codex` binary over the same trees
shows four disagreements, each of which moves this arm's score: a context-free
`+` hunk lands at the `@@` anchor rather than at end of file, an updated file
keeps whatever trailing newline it had rather than gaining one, CRLF survives,
and near-miss context is refused where Codex's four matching passes accept it.
Replace it with a port of Codex's own applier and parser. The parser now reads
hunks into chunks instead of passing section bodies through, so a syntax error
in the third section is found before the first is written, and the tool no
longer needs a second project's operation shape.
Restore verify-then-write. An earlier round changed the tool to apply
operations sequentially, matching the standalone `apply_patch` binary. That
binary is not what a model calls: the function-tool handler and the intercepted
shell call both go through `try_verify_apply_patch_args`, which collects hunks
into a map keyed by resolved path, refuses a duplicate key, and computes every
change against the pre-patch tree. So an envelope naming one path twice is
refused, and a hunk whose context the model guessed at costs nothing.
Fidelity is now measured rather than argued. `codex-oracle.mjs` runs the real
binary over a temporary tree for each of 72 cases and records what it printed
and what the tree became; `codex-fixtures.test.mjs` replays every case through
the registered tool and compares. The two cases where the function tool
deliberately differs from the standalone binary are named there with what it
does instead. A fuzzer over 1800 random envelopes found no further divergence.
Also tighten the arm-control test, which had three gaps: it skipped composition
steps that override an already-composed entry, so an override of the patch
tool's description was invisible; it named the profile files to compare instead
of reading them; and it exempted the first subject's profile argument while
comparing only the argument vector, not the whole subject definition.
Toolchain fingerprint re-pinned. Test-only files move under `__tests__/` so the
build prunes them by directory rather than by filename.
Generated-by: Claude Code
`readMaxBytes: 16000` was justified as making "how much of a file one call
returns" equal across the two read-capable arms. It does not. The baseline
editor spends eight characters a line on line numbers plus a header; the fs
tool emits neither, so measured over this repository's own sources it returns
10% more lines for the same budget and 22% on the longest files.
The budget is still the right one, for a reason the note did not give: what
should be equal is what a read costs the context window, and the numbering the
baseline adds is part of its contract that the model pays tokens for either
way. Equalising delivered content would mean picking a budget from an assumed
average line length.
Record the measurement, and record the one genuine mismatch that remains — the
budgets are counted in UTF-16 code units against UTF-8 bytes, equal on ASCII
and not otherwise.
Generated-by: Claude Code
The fixtures pin the cases someone thought to write down. This searches for the
rest: it generates envelopes from random line soup, applies each with the
released binary and with the registered tool, and reports every input the two
disagree on. 2100 envelopes across seven seeds currently diverge on nothing.
Red-checked against three deliberate regressions — placing a context-free `+`
hunk at the `@@` anchor, dropping the trailing-newline normalisation, and
removing the punctuation-folding match pass. The third went unnoticed through
400 envelopes at first, because context lines drawn verbatim from the file only
ever exercise the exact-match pass; the generator now half-remembers a line the
way a model does, and catches it.
Envelopes name each path at most once, and a refusal is compared as a refusal
without comparing the tree, because those are exactly where the standalone
binary and the function tool are supposed to differ.
Generated-by: Claude Code
The function-tool handler verifies an envelope and then throws the result
away: `ApplyPatchRuntime::run` hands `req.action.patch` — the raw text the
model sent — back to `apply_patch_with_mode`, which re-parses it and applies
it sequentially against the live filesystem. The verified `changes` map is
carried for approval and telemetry only.
This tool verified and then wrote the plan it had just computed. That is not
a cheaper version of the same contract, and the reference binary shows the
difference in two cases now covered by tests: a rename onto a path a later
section also updates leaves that section failing against the renamed file,
and `*** Move to:` naming its own source writes the destination and then
unlinks it, deleting the file while reporting success. Both were previously
refused with an invented "multiple operations target" error, because the
verification map also claimed the move destination — upstream keys only on
`hunk.resolve_path()` and carries `move_path` as a value.
Also in the write path: `fs/edit-intent` yields `{version}` with no `kind`,
so passing it straight to `writeText` matched neither guard and silently
wrote unconditionally; the abort signal was not checked between operations;
and `rm` ran with `force: true`, reporting `D <path>` for a file that had
already gone.
In the applier: each fuzzy-matching pass now projects both sides once
instead of recomputing the punctuation fold per candidate position, and
replacements are rebuilt in one forward pass rather than spliced, which
threw a RangeError once a hunk added more lines than the argument limit.
The model-facing description is no longer transcribed into this file. It is
read from a byte-for-byte copy of the upstream artifact it came from, with
three named substitutions applied over it, so the claim that it differs in
exactly those places is checkable rather than asserted in a comment. Copying
the file rather than retyping it also removed a fourth difference that had
no reason to exist: upstream's typographic punctuation is now preserved.
Its recorded provenance was stale — the file it named no longer exists at
the pinned commit — and now points at the artifact that does.
One parameter description said the input was "Not JSON-wrapped". It is: the
harness registers JSON function tools and nothing else.
Generated-by: Claude Code
Upstream ships codex-rs/apply-patch/tests/fixtures/scenarios as, in its own
README's words, an end-to-end suite "meant to be easily portable to other
languages or platforms". This is the port: the 25 scenario directories are
copied verbatim from the pinned commit and replayed through the registered
tool, compared the way upstream compares them — copy `input/`, apply
`patch.txt`, ignore the exit status, and require the resulting tree to equal
`expected/` exactly.
It is the only check here with the standing to contradict the code. The
oracle fixtures run the reference binary, but over inputs this repository
chose; the fuzzer perturbs inputs this repository chose how to perturb.
These were chosen by upstream, before and independently of this tool.
22 of 25 match outright. Three do not, and each divergence is recorded with
its cause and pinned to bytes the reference binary produces:
- 015 specifies the standalone binary, which has no verification pass, so an
add preceding a failing update survives. The function tool writes nothing.
- 023 and 024 are specified under CODEX_APPLY_PATCH_PRESERVE_LINE_ENDINGS,
a feature that is `Stage::UnderDevelopment, default_enabled: false`, so a
shipped Codex and this arm both run the `NormalizeToLf` default.
Both divergence lists — here and in the oracle fixtures — now assert that
each entry still diverges, so an excuse cannot outlive the thing it excused.
Generated-by: Claude Code
The treatment in this arm is the tool name, the parameter it takes, and the
description text. None of the three had any coverage, so a build that
registered the tool under another name, dropped the parameter, or shipped a
truncated description would have run a cohort and produced numbers.
The description claim is the one that had to be argued rather than shown:
"three differences from upstream and no others" was a comment. It is now
checked at both ends. The vendored instructions file is pinned by digest,
with the `git show ... | sed -n` that reproduces it from the source recorded
beside the constant, and the built text is recomputed here by a different
mechanism than the code uses and required to match. Each adaptation must
apply exactly once, and no mention of the shell delivery may survive.
Red-checked against four sabotages — appending a line to the description,
renaming the tool, editing one byte of the vendored text, and neutering an
adaptation. Each is caught.
Generated-by: Claude Code
Two differences at the outermost layer of the patch, both pinned by new
oracle fixtures generated from the reference binary.
`parse_patch` selects its mode from `PARSE_IN_STRICT_MODE`, which is `false`:
upstream gave up on gating leniency by model, so every call site including
the function tool unwraps a heredoc-wrapped envelope. A model asked to send
a patch may well send the `<<'EOF' ... EOF` form it was shown, and Codex
applies it where this refused it. The strict boundaries are still tried
first, so an envelope whose own first line reads `<<EOF` is never stripped.
Rust's `str::trim` cuts the Unicode `White_Space` property; JavaScript's also
cuts U+FEFF and does not cut U+0085. Only U+FEFF can realistically occur — a
byte-order mark carried into a JSON string argument — and it made this tool
accept a patch Codex refuses, which is the wrong direction for an arm being
compared against the contract it imitates.
Red-checked: restoring `String.prototype.trim` fails the byte-order-mark
fixture, and disabling the heredoc strip fails both heredoc fixtures.
Generated-by: Claude Code
The comment justifying `readMaxBytes: 16000` said `str_replace_editor`
spends eight characters a line on line numbers and a header while "this tool
emits neither". Both tools number their lines, and this one wraps the result
in a `<path>/<type>/<content>` envelope with a footer.
The setting still holds something equal, but not what was claimed. The
baseline truncates the fully rendered string; this tool charges the budget
against raw line bytes plus separators and adds the gutter, the envelope and
the footer afterwards, outside it. Measured across this repository's 1480
TS/JS/Python sources at 16000, the arm delivers 1.105x the lines and emits
1.027x the characters, worst file 1.34x and 1.19x — a residual in this arm's
favour, now recorded with the mechanism that produces it.
The three profiles also claimed to have been "verified request-for-request"
against the upstream composition. That was a manual comparison made once
during development; nothing here reproduces it, and the headers now say so.
`MAKA_EVAL_DSH_PLUGINS` was exported into all three arms and read by one, so
the two controls carried an environment difference for a mechanism they do
not use. It is now set from the copied composition itself, which keeps the
condition and the file that depends on it from drifting apart.
Generated-by: Claude Code
`apply_patch` in Codex is a freeform tool: the API is sent a short description
and a Lark grammar the decoder is constrained to, so a model on that path
cannot emit a malformed envelope. The harness registers JSON function tools
and has no grammar seam, so this arm is the unconstrained case. That is not a
detail of the port — it is a cost charged to this arm and to no other, and it
now leads the arm's description instead of being absent from it.
Three further divergences are named with their causes: the function-tool path
against the standalone binary's sequential apply, `NormalizeToLf` against a
preserve-line-endings feature that ships disabled, and diagnostics more
specific than Codex's single `Invalid patch:` line, which is the one place
this arm may be easier on a model than the reference.
The README also described a verify-then-write-the-plan design the tool no
longer has, quoted the old fixture count, and repeated the read-budget
mechanism corrected in the previous commit.
The NOTICE opened by saying nothing was carried verbatim as a file; two
things now are. Its description provenance named a file that no longer exists
at the ported commit and a fourth modification that was an artefact of
retyping rather than copying, its fixture path was stale, its account of how
`PreserveLineEndings` is selected covered only the standalone binary, and its
list of ported functions omitted the ones that decide apply order.
The arm-control test read a composition step as an insertion whenever it had
an `insert` key, so `{insert, remove}` would have passed with the removal
unread, and it filtered directories out of each profile's file list rather
than refusing them. It also now names what it does not cover.
Generated-by: Claude Code
`dsh-fs-local` creates a write's parent itself, in `writeFileAtomic`, and it
is the provider every arm mounts. The `ensureParent` here ran `mkdir` on a
`processPath` — the same way around a confining sandbox that delete and
rename refuse for — and existed only because the test harness's provider was
weaker than the real one and failed with ENOENT without it.
That is the more useful half of this change: a fake that cannot do what the
real thing does will not merely miss bugs, it will make production code grow
to cover for it. The fake now creates the parent the way `writeFileAtomic`
does, and the tool has no call that creates a directory at all.
The test that pinned the old behaviour was passing for the wrong reason — it
asserted the tool left no directory behind, and what actually happened was
the fake's write failing before anything could be created.
Generated-by: Claude Code
…sses
`deriveNewContents` reported a context miss as a plain `Error`, and the
caller rewrapped anything it threw as `FS_EDIT_NOT_FOUND` — so a bug in the
applier would have reached the model as "the lines you named are not in the
file", which is advice to rewrite a correct patch. The context miss is now
its own type and the only thing translated; a comment already claimed this
and the code did not.
Two oracle fixtures pin the order of the four matching passes against each
other. The existing cases pinned the exact pass ahead of the loose ones;
between the loose ones, removing a pass does not fail a match, it silently
moves it to a different line, and nothing caught that. Each of the four now
fails at least one fixture when removed.
The line-ending note in codex-apply.mjs described only the standalone
binary's environment variable, not the feature flag the function tool reads.
The toolchain is rebuilt and its fingerprint re-pinned: the model-facing
instructions now ship as a file, so they are covered by `checksums.sha256`
rather than living only inside a JavaScript template. Verified inside the
pinned linux/amd64 container that the built tree registers one tool named
`apply_patch` with one required `input` string and a 2903-byte description
carrying no mention of the shell delivery.
Generated-by: Claude Code
Differential fuzzing against the reference binary found a regression this
branch introduced. `applyReplacements` was rewritten to build the file
forward in one pass, on the reasoning that the replacements are sorted and
disjoint. They are sorted; they are not always disjoint.
`computeReplacements` places a context-free insertion at the end of the file
without consulting the search position — deliberately, because that is what
`insertion_idx` does upstream — so it can land inside a range a later chunk
replaces. Applying descending, the replacement swallows it; applying forward,
the two are treated as separate edits and the inserted line survives into a
file the reference never produces.
Restored to splicing in descending order, with the replacement segment going
in in bounded batches so a hunk larger than the engine's argument limit still
does not throw. The case is now an oracle fixture, so it is caught without a
`codex` on PATH; red-checked by reinstating the forward rebuild.
2300 further envelopes across four seeds diverge on nothing. Toolchain
rebuilt and re-pinned.
Generated-by: Claude Code
An adversarial review pass found nine behaviours that were wrong or untested
in the V4A tool, each verified against the reference binary or upstream source
before being changed. All nine are now covered: sabotaging any one of them
fails the suite, where before five of them left it green.
The matcher used JavaScript's `trim` where upstream uses Rust's. The two
disagree on U+FEFF and U+0085, so a file whose first line carried a byte-order
mark matched a pattern without one and was written back with the mark deleted,
and a line terminated by NEL was refused where the reference applies it. The
charset the parser already spelled out moves to rust-trim.mjs and both use it.
The verify pass now does what `try_verify_apply_patch_args` does: nothing at
all for `*** Add File:`, and a read for `*** Delete File:` as well as for an
update. Statting a delete target instead applied a delete the reference
refuses, since `read_file_text` maps invalid UTF-8 to `InvalidData` and fails
the whole envelope. It also stops emitting `fs/observed {present}` off a bare
stat, which told an observation policy a file had been seen that nothing read.
The duplicate-path key is lexical, as upstream's `PathUri::join` is, not the
provider's realpath-derived target key. An envelope naming both a symlink and
its target was refused here; the binary applies both sections in order.
A move destination no longer runs the `fs/write-intent` waterfall and discards
the answer, which turned a decider's refusal to clobber into a silent
overwrite. A failed unlink and an abort between passes now arrive as `FsError`
with a routable code and the path the model wrote, not a raw errno and a
resolved host path.
The test provider was weaker than `dsh-fs-local` in four ways that made the
above invisible: it decoded lossily where the real one raises `FS_NOT_TEXT`,
called every non-directory a file, took three parameters so the signal and
sandbox policy could not be observed, and never returned a decider's answer.
Two oracle cases are added for the trim charset and one for a trailing U+FEFF,
which is what separates the three loose matching passes from each other; the
BOM-in-content case is recorded as a divergence with its true cause, which is
that the shared provider strips a leading mark before this tool sees the file.
Generated-by: Claude Code
The import landed the 25 scenario directories, their README and their
.gitattributes twice: once at `__tests__/upstream/` and once, byte-identically,
at `__tests__/upstream/scenarios/`. Only the nested copy is read.
Generated-by: Claude Code
…arms
The three arms are meant to differ only in the edit contract. Three settings
outside that contract were not held.
`readMaxLineLength` was left at its shipped 2000 while only `readMaxBytes` was
matched to the baseline. That is a cap the baseline has no counterpart to: the
fs arm could never see the 2001st character of any line, at any offset, while
`str_replace_editor` truncates once over the whole rendered view and can spend
its entire budget on one line. A minified bundle or a single-line payload is
where that lands, and it lands against one arm only. It is raised to the
budget, where it can no longer bind before `readMaxBytes` does.
`streamIdleTimeoutMs` was omitted from all three profiles, leaving the
adapter's 300000 ms default in force — five minutes between two streamed
tokens, behind `reasoningEffort: max` and a 65-minute bash deadline. Upstream's
own jsonrpc-agent composition sets 172800000, which is what the arms now carry;
a turn killed by that timeout would have been scored as a failure of whichever
contract was running.
`maxConcurrentTaskGroups` was 64. A group holds one cell per subject and starts
them together, so three arms made 192 concurrent trials where every other spec
here lands on 128. That does not bias one arm against another, but it puts this
run's `deepseek-harness` score under a load no single-arm run shares. It is now
43, and the test asserts the arithmetic rather than the number.
The read-budget residual quoted in the profile and the README had no
reproducible source and reported only the tail that favoured the fs arm.
`scripts/measure-read-budget.mjs` reimplements both renderers from the shipped
code and reports both tails; the figures are replaced with what it prints.
Generated-by: Claude Code
A claims audit checked every factual assertion in the README, the NOTICE and
the profiles against upstream source at the ported commit and against the
shipped harness packages. Fourteen did not hold.
The largest is provenance. `prompt_with_apply_patch_instructions.md` is not in
any shipped Codex system prompt at the ported commit: it is referenced only
from a test whose four model cases all declare
`expects_apply_patch_description: false`, and none of the eight instruction
templates in models.json carries the section. The file is upstream's own
authored description of the format and it is retired, which is a better reason
to use it than the one previously given, but the prose said Codex sends it.
The freeform description is 108 characters, not 105. The oracle runs 82 cases,
not 76. The fuzz figure is 2300 across four seeds, not 2100. Two of the named
divergences were recorded in the tests, not three, and there are now four with
the byte-order mark the provider strips. `verifyToolchainDirectory` covers ten
registered profiles, not eight. `checksums.sha256` omits itself and
manifest.json. The toolchain build's `__tests__` prune is scoped to the plugins
it copies. The build script runs from the repository root. `ctx.fs` offers
twelve methods, not three. The 0.147.0 delta includes a second behavioural
difference — the duplicate-path check does not exist at that tag — and a larger
restructure than the three items named. The profiles record three deviations
from upstream, not one.
Also stated where it was missing: the arm's lack of a read tool is Codex's
contract rather than a handicap, since Codex registers no file-read tool
either and every arm has the same bash.
Generated-by: Claude Code
A second adversarial round found that the previous round's fix traded one
divergence for another, and that twelve mutations of this tool still left the
whole suite green.
`ctx.fs.readText` is not `read_file_text`. Upstream reads bytes and calls
`String::from_utf8`, so every valid UTF-8 sequence is text; `dsh-fs-local`
adds two rules on top, and both are model-visible. It rejects a NUL in the
first 8192 bytes as binary — but NUL is valid UTF-8, and the reference patches
and deletes straight through it, verified against the binary — so making the
verify pass read delete targets took `*** Delete File:` on such a file with
it. And it decodes with `ignoreBOM` left false, which strips a leading
byte-order mark, so a patch whose context omits the mark applied here, was
refused there, and wrote the file back without the mark. Reading through
`ctx.fs.readBytes` and decoding here is exactly `String::from_utf8`; it fixes
both, and the byte-order-mark divergence is deleted rather than documented.
`*** End of File` as the first line of an update hunk was refused. Upstream
guards with `chunks.last().is_some_and(blank)`, which is false when there is
no chunk, so the marker is ignored; the binary applies the envelope.
Twelve mutations that the suite could not see, now each red-checked: five
dropped the turn signal from a provider call, which a fake that ignored the
signal could not notice; two removed or falsified the version on `fs/observed`;
two swapped a removal's announcement with the removal itself, which leaves the
event list identical and the trail a lie; one bypassed the create decider and
one renamed its event; one reduced the sandbox-denial remap to a no-op. The
fake now honours and records the signal on every call, records what the
filesystem actually held when each observation was published, records the
version `writeText` returned, and can be made to deny a write.
Twelve oracle cases added, generated from the binary: three for the read seam
and nine for shapes that separate this port from a plausible variant of it —
overlapping chunks, a backward `@@` anchor, an indented header after an add
body, a trailing mark on an End of File marker, header paths with a leading
space, a repeated `*** Move to:`, a heredoc close with trailing text, and the
End of File case above. Eleven passed unchanged, which is the point of adding
them; one was the defect.
`requireDirectFilesystem` now raises an `FsError` like every other refusal.
Generated-by: Claude Code
… meant to help
Raising `readMaxLineLength` from its shipped 2000 to the 16000 budget was
wrong, and worse than the asymmetry it was meant to close. `dsh-tool-fs`
truncates a long line and appends a `... (line truncated to N chars)` marker,
then charges the whole thing against `readMaxBytes`. Once the cap approaches
the budget the truncated line no longer fits at all: the window returns zero
lines, drops every line after it, and the footer invites the model to retry at
the offset it just used, so the rest of the file is unreachable. Measured
against the shipped functions, a 20000-character line followed by 200 short
ones delivers 201 lines at 2000 and 0 lines at 16000.
No value equalises the two budgets anyway — one counts UTF-16 code units and
the other UTF-8 bytes — so the cap goes back to the value the tool ships with
and the residual is measured instead of engineered away. At 2000 the direction
is also not the one the earlier reasoning assumed: on that same long-line file
the baseline spends its whole budget on the one line and delivers 1, while
this arm truncates it, says so, and reads on.
The guard test now asserts the invariant rather than the number, so any future
cap has to be one a truncated line can still fit under.
The measurement script had four transcription errors, all inflating the
character ratio: it dropped the baseline's 240-character truncation notice and
the fs arm's per-line marker, counted the baseline's prompt line as content,
and counted a phantom trailing line for the fs arm. It also silently ate the
first positional path when `--budget` was absent. Rather than fix the
transcription and hope, `--self-check <dir>` now lifts the real functions out
of an installed copy and asserts agreement, and the corpus figures are
reported alongside synthetic long-line cases that the repository's own sources
do not contain. Corrected: 2197 files, 1.093x lines, 1.001x characters, tails
reported as what they are — extremes among the 570 files where a budget binds.
`maxConcurrentTaskGroups` moves 43 to 42: three arms at 43 is 129 trials, and
the assertion had been written as `<= 129` to admit it.
Generated-by: Claude Code
The claim that upstream's `## apply_patch` prose is retired was too strong. Of
the eight instruction templates in `models-manager/models.json` at the ported
commit, seven carry no such section — but `gpt-5.2` carries a shorter one with
the same envelope, the same three headers and the same worked example. So the
format is still taught in prose upstream, and the vendored file is the fuller
version of that prose rather than something abandoned. The conclusion it was
supporting survives unchanged and for a cleaner reason: `gpt-5.2` gets the
prose *and* the grammar, so no shipped configuration is the one this arm is in.
Codex's handler set is much longer than the six entries named, and one of them
takes a filesystem path — `view_image`, for images. The load-bearing claim is
that no handler reads file *text*, which is what the prose now says.
The NOTICE gave the wrong cause for the duplicate-path divergence: the
recorded binary composes an envelope naming one path twice because the
standalone applier has no verification pass at either version, not because the
check is absent at rust-v0.147.0 — that is a function-tool difference.
Also: `invocation.rs:246-248` is the delete arm, not the add arm; the
`fs/edit-intent` shape citation named only one of the two declarations; the
end-of-file search has no fallback to the ordinary start position, contrary to
its comment; `__tests__/upstream/` kept a stray `.gitattributes` naming paths
one level down; and the build script's header credited `npm i` for a
non-reproducible fingerprint when it runs `npm ci` and the real cause is native
compilation.
The refusal under a confining provider is described as what it is: dead code
under `dsh-fs-local`, which never sets `sandboxMode` at any sandbox mode, not
something `danger-full-access` switches off.
Generated-by: Claude Code
…t reads it
The comment justifying the conditional export had the causation backwards. The
model's shell inherits this environment, so setting the variable in all three
arms is what makes them identical, and setting it in one arm alone is what
creates a difference the model can read with `env`. It names a directory and
starts nothing, so it is inert in the two arms whose compositions never mention
it.
The remaining environment asymmetry, `$HOME` carrying the profile name, is
recorded rather than removed: the arms need separate homes because each writes
its own composition under `$DSH_HOME/profiles`, and sharing one would have each
overwrite what the others booted from. So the experiment is not blind to its
subject, and that is now stated where the home is built.
Generated-by: Claude Code
The spec declared 42 task groups, or 126 concurrent trials, on the
strength of a 128-trial convention rather than a machine. The account's
CVM quota is 60 vCPU per zone, which caps the host at 56 vCPU and
256 GiB, and the only completed run of this suite put 24 trials on
32 vCPU / 64 GiB — 2.7 GiB per trial. 126 trials on that host is 2.0 GiB
each, close enough to a Terminal Bench build task's peak that an OOM
kill would land as an infra failure. 32 groups is 96 trials at the
density that is known to survive.
Generated-by: Claude Code
The arm named its plugin with `name: !!js process.env.MAKA_EVAL_DSH_PLUGINS
+ '/tool-apply-patch/index.mjs'`. The harness's loader evaluates that tag for
`disabled` alone and hands `name` the unevaluated node, so every trial of
that arm died at boot with `name.startsWith is not a function` before it
reached the model — on a rented host, at the first smoke task.
The row now names `./plugins/tool-apply-patch/index.mjs`, resolved against the
profile directory, and the subject plants `plugins` there as a link into the
toolchain. That keeps what the loader imports inside the fingerprinted
read-only tree, keeps the composition a plain string, and keeps the path
independent of the root the wrapper was given — which naming the mount point
outright would not. The link is planted for all three arms, on the same
reasoning that exported the variable for all three: the model's shell can read
$DSH_HOME, so planting it in one arm alone is the difference, not planting it
everywhere.
The test could not have caught this. It extended its own YAML schema to
construct `!!js <source>` as a string, which taught the assertion to accept a
form the harness cannot load. The tag is real elsewhere in these compositions —
`task: !!js ctx.headlessStartup.task` is upstream's — so the schema now
constructs it as the marker shape the harness's loader produces, and a new
assertion requires every entry name to be a string and every path-named plugin
to be one the toolchain build ships. Reverting the row fails both.
Generated-by: Claude Code
The wrapper counted the subject's stderr bytes and hashed them, then dropped
them. The relay writes `maka-subject.stderr.txt` from the wrapper's own
stderr, which is empty, so a failing arm's own account of what killed it
reached the attempt record as a byte count and a digest and nothing else.
Both failures this run has had were diagnosed by reproducing them by hand on
the host instead: a composition the harness could not load, and a subject that
exits after one model response. The second is still open.
The last 64 KiB now land in `/logs/agent/<profile>.stderr.txt`, which the
relay already collects, as a `stderr-tail` artifact beside the existing
`stderr` count. A tail because a subject that loops printing would otherwise
fill the trial's disk, and because a harness says what killed it on the way
out; the count and digest still describe the whole stream, so truncation is
visible.
Generated-by: Claude Code
Announcing it as an artifact pushed the frame past the relay's 2 KiB payload
cap, so every cell came back `external subject result transport failed`.
`/logs/agent` is collected into the trial directory regardless, so writing the
file is the whole job.
Generated-by: Claude Code
Every failure before the subject starts reached the operator as the string
`external subject setup failed`, because `safeFailure` sanitizes what the
published result frame may say and nothing recorded the error itself. The
process's own stderr is collected into the trial as
`maka-subject.stderr.txt`, which is where a subject's account of its own
failure belongs, so the error, its stack, and its cause chain go there.
Generated-by: Claude Code
This module does its work at the top level, above the function that reads the
subject's stderr, so a `const` declared beside that reader is still in its
temporal dead zone when the reader runs. Every cell died with
`ReferenceError: Cannot access 'STDERR_TAIL_BYTES' before initialization`
before the subject's first model response — reported as
`external subject setup failed`, and legible only because the commit before
this one started writing the error itself.
Generated-by: Claude Code
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Astro-Han