Conversation
Merge pull request #242 from slowdini/dev
Two platform assumptions kept `cargo test` from running at all on Windows. `tests/run/conversation.rs` imported `std::os::unix::fs::PermissionsExt` unconditionally. Because the file is part of the `run` test target, the resulting compile error aborted `cargo test` and `cargo clippy --all-targets` before any test executed. The scripted-turn test that needs it drives a `#!/bin/sh` stub it has to mark executable, so it and its imports are now gated `#[cfg(unix)]`; the other two tests in the file run everywhere. `.gitattributes` pinned only `tests/golden/**` against EOL translation, so a Windows checkout with the default `core.autocrlf=true` produced a CRLF working tree everywhere else. Harness descriptors and profiles are embedded verbatim in generated artifacts, and `tests/fixtures/**` is compared byte for byte, so CRLF silently changed program output. Checking every text file out as LF keeps generated bytes identical across platforms. Verified on Windows 11 with rustc 1.97.1: - `cargo build`, `cargo fmt --check`, and `cargo clippy --all-targets -- -D warnings` all pass; clippy previously could not compile. - `cargo test --no-fail-fast` reaches 1009 passing, up from 886 with the 123-test `run` target unable to build. The remaining failures are path-separator and POSIX-spawning assumptions, tracked separately. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fix(tests): let the suite build and compare correctly on Windows
`Path::join` plus `Display` emits the host's separator, so on Windows a POSIX-rooted base yields `/work/cond\run.json` — malformed for every reader of an artifact that is, by design, a wire format shared across platforms. `artifact_path` renders a path into that wire format: forward slashes, with a verbatim `\?\` prefix stripped (verbatim UNC collapsed back to `\server\share` rather than left as a bare `UNC\` component). The rewrite is Windows-only, because a POSIX filename may legally contain a literal backslash and rewriting it there would name a different file — which also keeps Unix output, and so every golden fixture, byte-identical. `normalize_separators` is the comparison-side counterpart and is unconditional: its job is matching a path spelled by a *different* host, not preserving the local spelling. No call sites yet; those land with the boundaries they fix. Refs #246 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The guard classifies paths that come out of agent tool calls, and agents spell them POSIX-style on every host. Windows has no root without a drive, so `std::path::absolute` was grafting the process's current one: `/dev/null` became `C:\dev\null`, which stopped reading as a device and started reading as a file the guard had to block — `cmd >/dev/null` denied, and `git fetch origin >/dev/null 2>&1` reclassified from "git remote operation" to "output redirection to a file". `/etc/passwd` still denied, but the evidence recorded a path that never existed. `lexically_absolute` now leaves a rooted-but-prefixless path exactly as given, and `resolve_path` applies it to the *joined* path so a relative target under a POSIX-rooted root resolves the same way its allowed roots do. The stray-write scanner shares the helper for the same reason: resolving one side with a drive and the other without silently stops the comparison matching. `is_non_file_device` now matches by path component, since a resolved `/dev/fd/1` renders as `fd\1` and a `"fd/"` string prefix would miss it. Lexical `..` normalization still runs first, so `/dev/../etc/passwd` cannot launder a write past the device check. Fixture validation gets the matching treatment: `Path::is_absolute` answers for the host only, so `/etc/passwd` slipped past it on Windows and `Path::join`'s root-replacing behavior would have landed the fixture outside the env. Eval configs are committed and run on every platform, so `is_absolute_on_any_platform` gives one verdict everywhere — `\etc\passwd` included, which is absolute on Windows and a legal filename elsewhere. Windows lib failures: 45 -> 36. Refs #246 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rmat `dispatch.json`, `judge-tasks.json`, the manifest, the runbook, the guard's deny reasons, and the shadow report are all read by agents and by later pipeline stages, so their path fields are a wire format — but they were built with `Path::join` and `Display`, which emit the host separator. On Windows a POSIX-rooted base produced `/work/cond\run.json`, and a descriptor-declared dir produced genuinely mixed output like `...\home\.agents/skills\different-folder`. Every such field now goes through `artifact_path`. In `build_dispatch_task` the rendering happens once up front, so a task's serialized fields and the prompt text quoting them cannot disagree. Guard deny reasons render their allowed roots the same way as the scratch hint beside them, so one sentence never shows the agent a root in one spelling and a directory under it in another. `canonical_path` loses the verbatim `\?\` prefix `canonicalize` returns on Windows. Deliberately left native: the guard hook command line in `sandbox::install` and harness `exec_template` arguments, which are handed to a process rather than to a reader. No golden fixture changed — `artifact_path` is a no-op on Unix, and on Windows it now produces the bytes the committed fixtures already held. The five golden tests and the whole `sandbox::decide` deny-verdict group go green. Windows lib failures: 36 -> 19. Refs #246 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three comparisons matched a path against a differently-spelled copy of the same path and silently answered "no". Each failure is quiet by design — the code reports nothing rather than erroring — so on Windows a real signal simply stopped appearing. `prompt_read_failed` searched `args.to_string()`, the *serialized* JSON, for a raw path. Serializing escapes every Windows separator to `\`, so the text never contained the path as written: `skipped_prompt_unread` stayed 0 and a dispatch that never received its instructions was recorded as data. It now walks the args' string leaves — which also reaches nested shapes like cline's `files[].path`, the reason the serialized form was searched to begin with — and compares with separators normalized. This matters beyond Windows now: `dispatch_prompt_path` is forward-slash wire format while the harness transcript echoes the agent's own spelling. `detect_live_source_reads` compared the recorded live directory against the raw command text, so an arm could read the live skill source and still produce a clean stray-write report — a contaminated arm presented as comparable data. `colliding_staged_source` split `discovery_path` on `MAIN_SEPARATOR` to take a basename. Now that the field is forward-slash wire format, that found no basename at all on Windows and let a refutation through on exactly the collision the check exists to block; it splits on either separator. Windows lib failures: 19 -> 15. Refs #246 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Twelve failures were the tests' own assumptions rather than the code's. The guard suites built their marker and manifest JSON with `format!`, interpolating a raw path. On Windows that embeds `\U`, `\A`, `\T` — none of them valid JSON escapes — so the marker was malformed, the guard read it as absent, and every assertion passed through the fail-open path. Ten tests reported nothing about the guard while looking like they covered it. They now serialize with `serde_json`, which is also what the production installer does. The `sandbox::install` byte pins had the mirror-image bug: the *expectation* interpolated the marker raw into a JSON string the real file escapes. `canonical_root` returned a verbatim (`\?\`) path, but a child process reports the plain form as its cwd, so no path the CLI emitted could ever match one a test joined onto that root. The `tests/run` counterpart is now a shared `resolved()` helper — it keeps the symlink resolution macOS needs for its temp dirs and drops only the prefix. The rest compare against generated artifacts that are now forward-slash wire format (`discovery_path`, `response_path`, `eval_root`) or against `cargo package --list`, which prints forward slashes everywhere. Where a choice existed the comparison was normalized rather than the POSIX literal rewritten: those spellings are the behavior under test, since agents emit POSIX paths on any host. Building the env manifest's `envs[].dir` through `artifact_path` as well — readers join it against a task's `eval_root`, and the two had drifted into different spellings. Windows failures: 68 -> 17, all of them #247's POSIX-executable cluster (`harness_lint_probe_*` x6, `command_check` x4, `judge_recipe_*` x3, `execute_with_timeout_*` x2, `run_git_spawn_error_surfaced`, `execute_round_creates_the_round_output_directory_before_shell_redirection`). `tests/run` is fully green. No golden fixture changed. Refs #246 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…erage Review pass over the branch. `record_runs.rs` crossed 500 lines, so the prompt-read guard moves to `record_runs/prompt_read.rs` — a self-contained concern (does the transcript show the agent's read of its dispatch prompt failing?) beside the existing `conversation.rs`, matching how this module already splits. 501 -> 411 lines. Driving a real `eval-magic run` on Windows found two path fields the tests did not cover, both leaving one logical value spelled two ways in a single document: - `conditions.json`'s `skill_path` is echoed into `dispatch.json` beside the tasks, which carry the same path — one native, one wire format. - The runbook's `ingest` line and the judge recipe's `cd` embedded native paths in POSIX shell commands, where a backslash is an escape character. (The recipes remain POSIX-only; that is #248.) Generated artifacts now contain no Windows-spelled path at all — verified against a real run's `dispatch.json`, `conditions.json`, `dispatch-manifest.md`, and `RUNBOOK.md`. The guard marker stays native by design: it is read back through `resolve_path`, which compares by component. Also tightened comments that narrated the fix rather than the code, and moved a misplaced `crate::` import in `skill_shadow.rs` below the external block. Refs #246 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fix(windows): make path handling platform-independent
…the OS Closes #247, whose diagnosis was wrong in two places. The issue reports the failures as confined to the test suite, and the spawning code as already portable. Neither held. `/bin/sh` was hardcoded in two production paths — `harness lint --probe` and multi-turn `dispatch-task` — so both were broken on every released Windows binary, not merely under test. `posix_shell()` now resolves a real `sh`: `EVAL_MAGIC_SH`, then `PATH`, then a Git for Windows install, then `/bin/sh`. It never accepts `bash`, because `System32\bash.exe` is the WSL launcher and resolves a different filesystem namespace. That alone fixed 9 of the 17 failures, with no descriptor changes: the shipped `exec_template`s are POSIX command lines and run unmodified. The `command_check` cluster was not a POSIX-executable problem either — that path already chose `cmd /C` on Windows. Two separate bugs hid behind it: - `Command::arg` escapes a command's embedded quotes as `\"`, which `cmd.exe` does not understand, so any eval author's quoted `command_check` silently arrived split at its spaces. Now `/S /C` plus `raw_arg`, which hands `cmd` the string verbatim. This is user-facing and independent of the tests. - The batch fixture strings were not equivalent to their POSIX counterparts (`echo x>>f` appends CRLF; `echo|set /p=` cannot round-trip a value). Both dialects are gone. A hidden `__fixture` subcommand now supplies the predictable child process the suite needs — a chosen exit code, chosen bytes, a chosen file — in one invocation that `sh -c` and `cmd /C` parse identically. Tests state the capability they need instead of the OS they tolerate. An `#[cfg(unix)]` attribute hides a test from compilation and clippy on the other host and hides the coverage gap with it; `report_skip` prints why a test was skipped, and `EVAL_MAGIC_REQUIRE_POSIX_TOOLS=1` (set in CI) turns every skip into a failure so coverage cannot rot. Two capabilities are gated: the POSIX toolchain the shipped recipes need, and symlink creation, which Windows permits only under Developer Mode. Where a per-OS difference is genuinely the behavior under test — signals, path separators — both arms now compile everywhere behind a runtime `cfg!(windows)`. `run_git_spawn_error_surfaced` was a third category again: it pinned the POSIX errno spelling while its own doc comment said the contract was a readable reason, not a particular one. `#[cfg(unix)]`/`#[cfg(windows)]` now appears 6 times, all in production code and none in tests: the per-OS symlink API, the shell selection, and reading a signal Windows does not have. Verified on Windows 11, rustc 1.97.1: - `cargo test --no-fail-fast` — 1122 passing, 0 failing, up from 1069 passing and 17 failing. - `cargo fmt --check` and `cargo clippy --all-targets -- -D warnings` clean. - `harness lint codex --probe` reaches the shell and reports exit 127 (`codex` absent) rather than failing to spawn. Not executed on this host: the three `judge_recipe_*` tests skip without `jq`, and the three symlink round-trips skip without Developer Mode. Both run on the Linux job, which sets the enforcement variable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
RENDER_STAND_INS backed only {cwd} and {model_arg}, so render_only_check
reported {guard_args} as unresolved in codex's parallel_command_template and
judge_command_template. The real dispatch path resolves it from
dispatch.guard_args, so both were false failures.
Add the stand-in and cover it two ways: a focused check on the concatenation
shape guarded harnesses use, and a sweep over every embedded descriptor so a
future built-in placeholder without a stand-in fails cargo test rather than
surfacing as a spurious probe failure.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fix(windows): resolve a POSIX shell; gate tests on capabilities, not the OS
fix(probe): supply a {guard_args} stand-in to the render-only checks
`run` prepared a correct workspace on a host with no POSIX shell, then printed — and wrote into RUNBOOK.md and dispatch-manifest.md — commands only a POSIX shell can execute, with nothing to say a different shell was expected. State the requirement instead, once, and reuse it: POSIX_TOOLING_REQUIREMENT feeds the shell-discovery errors, the new `run` preflight, RUNBOOK.md, and dispatch-manifest.md. It names `jq` alongside the shell because the parallel-dispatch and judge recipes are `jq` pipelines, and Git for Windows bundles `sh`, `xargs`, `tr`, and `wc` but not `jq` — so naming only the shell would point an operator at a setup that still walls out at the judge step. The preflight warns and continues rather than failing. `run` never dispatches, so preparing on Windows and dispatching from WSL stays a valid split. Development carries the same requirement. The scripted-turn tests already spawn a `#!/bin/sh` stub through the resolved shell with no capability skip, so a POSIX shell was required in fact but recorded nowhere; the two tolerant discovery tests now assert it. `jq` and symlink creation remain capability-gated skips, since Git for Windows supplies neither. Closes #248 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fix(run): declare a POSIX shell + jq requirement and preflight for it
A staged `.claude/skills/<slug>/SKILL.md` under a deep workspace crosses Windows' 260-character MAX_PATH, and the task repository's deliberate configuration isolation (GIT_CONFIG_NOSYSTEM plus an empty GIT_CONFIG_GLOBAL) discards the core.longpaths an operator set globally. The runner now writes its own. The reported failure was the loud band — `git add` aborting the run with `fatal: unable to stat '...SKILL.md': Filename too long`. Reproducing it end to end surfaced a quieter one a few characters deeper: git cannot open the staged directory to enumerate it, so `git add` warns, exits zero, and commits a baseline that does not contain the skill under test. The cleanliness check cannot catch that, since git reports nothing about a file it could not read, so every later diff would be measured against a baseline missing its subject. The setting is written twice: to the repository, so the agent under test and the pipeline's own `run_git` calls inherit it, and transiently on each invocation, so `git init` is covered before that local config exists. Initialization failures under a deep root now also name the path budget, keyed on the measured root length rather than on git's localizable `Filename too long`. Fixes #270 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Drop the issue references, which date and point away from the code, and cut each comment down to the line or item it sits on so none of them needs a neighbour to make sense. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fix(run): set core.longpaths on runner-owned task repositories
jq's native Windows build opens stdout in text mode, so every `\n` it writes arrives as `\r\n`. Both shipped recipes read that output as paths, and neither reader drops the CR: `read -r` keeps it by definition, and `tr '\n' '\0'` converts only the newline. The carriage return then rides on the end of every path. The judge recipe reports `0/N verdicts present` because `[ -s "$response_path" ]` matches nothing, and the parallel-dispatch recipe hands every task a corrupted `eval_root`, `dispatch_prompt_path`, and `outputs_dir`. Git Bash with `jq` is the setup the tooling requirement documents, so this is the supported Windows path rather than an exotic one. Pipe each jq call through `tr -d '\r'`: a no-op wherever jq already writes LF, and `tr` is a declared requirement alongside `jq` itself. The regression test defines a CRLF-emitting `jq` as a shell function in front of the recipe rather than shimming `PATH`, so the failure mode is covered on every host instead of only where a Windows jq is installed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Releases attach a Windows binary and the README documents a PowerShell installer, but both jobs ran on ubuntu-latest, so every Windows fix so far was found by hand rather than by the pipeline. Matrix the test job across ubuntu-latest and windows-latest with `fail-fast: false`, since a Linux failure cancelling the Windows leg would lose the result exactly when it is worth having. Clippy earns its place on both: the `#[cfg(windows)]` arms in `core::fs` and `command_check` are lint-invisible on Ubuntu. `EVAL_MAGIC_REQUIRE_POSIX_TOOLS` applies to both runners, so the Windows host is provisioned for the gated capabilities rather than exempted from them: `jq`, which Git for Windows does not bundle, and Developer Mode for symlink creation. Long paths need nothing — task repositories carry their own `core.longpaths`, and `LongPathsEnabled` stays unset deliberately so those tests keep proving what a default box does. `EVAL_MAGIC_SH` stays unset for the same reason: discovering the shell from the Git install root is what a Windows user hits. The guard test pins all of it together, because a matrix entry whose enforcement variable went missing would report green while covering six fewer tests than it appears to. Also corrects both contributor docs, which still described two gated capabilities before long-path staging became the third. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The long-path tests padded from `std::env::temp_dir()`, which can hand back an 8.3 short name: a GitHub runner's `%TEMP%` lives under `RUNNER~1`, while git expands that to `runneradmin` before it measures. Three characters no length computed here could see. That was enough to move the deepest test from one side of a ceiling to the other. It passed locally with `.git/config` at 257 and failed on the runner with the same target at 260, which reads as a Windows-only product failure and is nothing of the sort. Canonicalise the base first, so the padding measures the spelling git will report. Then step the root back to 244 and assert the window it has to sit in, because git's long-path awareness turns out to be per-operation: creating `.git/objects/pack` survives well past the budget, `git init` writing `.git/config` stops exactly at it, and the `git config --local` that follows gives up two characters earlier still. The old literal sat one character inside the tightest of those, with nothing recording that it did. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
POSIX gives this away for free: `getcwd` resolves symlinks, so a Unix process and everything it spawns already agree on how the working directory is spelled. Windows makes no such promise — it hands back whatever spelling the cwd was set with — and one directory there has several valid names: an 8.3 short name, a junction, a `subst` drive, a redirected profile. Nothing forced the two sides of the write guard's comparison onto one of them. Measured, spawning each tool the way a harness is spawned: node's `process.cwd()`, node's `realpathSync`, and `cmd`'s `cd` all echo the alias back, while git prints the resolved name for every path it emits. Both spellings are therefore reachable from inside a single task env, and `is_under` compares strings. Driving the hook with a short-form root, a write relative to a long-form cwd is denied, as is an absolute long-form target under the env — legitimate writes, refused. A genuine escape is still denied, so the guard never got weaker, only falsely strict. Every task env is a git repo, which makes `rev-parse --show-toplevel` a one-step route to the refused spelling. Resolve once, where the run's roots are derived, rather than at either end of the comparison: canonicalising only the marker inverts the failure instead of removing it, denying the same write when the cwd is the alias. Every path in a `RunContext` now shares one spelling, which makes it a property of the struct rather than of the one field someone remembered. Resolution walks up to the deepest ancestor that exists and re-attaches the rest, because a run names directories before it creates them and the alias always lives in an ancestor, never in the leaf. The cli fixtures that built roots with a bare `fs::canonicalize` now use the helper that strips the verbatim prefix. They had been comparing against `\?\` paths, a spelling no agent ever produces and one the CLI no longer emits. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fix(run): resolve a run's cwd to one path spelling
ci: run the suite on windows-latest
The generated recipes carry the absolute paths of the host that prepared the workspace, so the shell that dispatches them has to resolve those same paths. Git Bash shares the Windows filesystem and does; WSL resolves its own namespace, where a `C:\...` path names nothing, and nothing in the tree translates between the two. Guidance that listed Git Bash and WSL side by side — and a preflight warning inviting an operator to "prepare here and dispatch from a POSIX shell" — pointed at a split that fails quietly. POSIX_TOOLING_REQUIREMENT now states the constraint once and names WSL as where eval-magic runs rather than somewhere to dispatch into; its four Markdown consumers pick that up, and AFTER_HELP mirrors it by hand for clap. Also records the support tiers under "Platform support" in the developer overview: Linux and macOS supported, Windows through Git Bash deprecated with removal gated on #256, and the Windows-prepare/WSL-dispatch split unsupported. Before: ⚠ no POSIX shell found. ... The workspace and recipes below are still correct — prepare here and dispatch them from a POSIX shell. After: ⚠ no POSIX shell found. ... The workspace and recipes below are still correct — dispatch them from a POSIX shell on this host. Verification: cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, cargo test --all-targets (1146 passed, 0 failed). Golden fixtures re-blessed with GOLDEN_BLESS=1; the diff is one line per fixture. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fix(run): confine dispatch guidance to the preparing host
slowdini
added a commit
that referenced
this pull request
Aug 16, 2026
Merge pull request #276 from slowdini/dev
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Release notes
Highlights
Windows is a working platform. Prior releases shipped a Windows binary that
could not spawn a shell for
harness lint --probeor multi-turn dispatch, wrotemalformed paths into every generated artifact, and refused legitimate writes
through the sandbox guard. This release fixes the platform end to end — path
rendering, shell discovery, command quoting, long paths, working-directory
spelling, and the
jqpipelines the shipped recipes depend on — and the testsuite now runs on
windows-latestin CI so it stays fixed.Generated artifacts are a wire format, spelled the same way everywhere.
dispatch.json,conditions.json,judge-tasks.json,dispatch-manifest.md,RUNBOOK.md, the guard's deny reasons, and the shadow report now render everypath with forward slashes regardless of which host produced them. On Windows
these fields previously mixed separators within a single value
(
/work/cond\run.json). Linux and macOS output is byte-identical to 0.9.0.The POSIX shell and
jqrequirement is stated instead of assumed.runpreflights for both and warns up front, rather than handing you a correctly
prepared workspace full of commands your host cannot execute. The requirement
names
jqalongside the shell because the parallel-dispatch and judge recipesare
jqpipelines, and Git for Windows bundlessh,xargs,tr, andwcbut not
jq.Behavior changes to know about
through Git Bash works but is deprecated. Preparing a workspace on Windows and
dispatching it from WSL is unsupported and fails quietly: recipes carry the
absolute paths of the preparing host, and WSL resolves its own filesystem
namespace where a
C:\...path names nothing. Earlier guidance suggested thatsplit; it no longer does.
EVAL_MAGIC_SHoverrides shell discovery. Without it,shis resolved fromPATH, then a Git for Windows install, then/bin/sh.bashis neveraccepted, because
System32\bash.exeis the WSL launcher and resolves adifferent filesystem namespace.
runprints a preflight warning when no POSIX shell orjqis present. Itwarns and continues rather than failing —
runnever dispatches, so preparingon a host that cannot dispatch stays valid.
above. Anything parsing those fields on a native separator should read forward
slashes. Unix output is unchanged.
.gitattributesnow checks out every text file as LF, notjust
tests/golden/**. A Windows checkout with the defaultcore.autocrlf=truepreviously produced a CRLF working tree, and since harness descriptors and
profiles are embedded verbatim in generated artifacts, that silently changed
program output.
Fixes
harness lint --probeand multi-turn dispatch could not start on Windows at all—
/bin/shwas hardcoded in both production paths. They now resolve a realshell; the shipped POSIX
exec_templates run unmodified.command_checksilently arrived at the child split atits spaces on Windows.
Command::argescapes embedded quotes as\", whichcmd.exedoes not understand; the command line is now handed over verbatim.agents spell paths POSIX-style on every host, and
std::path::absolutegraftedthe process drive onto them, so
/dev/nullbecameC:\dev\nullandcmd >/dev/nullwas refused as a file write. Separately, a single directory canhave several valid Windows spellings (8.3 short name, junction,
substdrive),and nothing forced the two sides of the guard's comparison onto one — so a write
relative to a long-form cwd under a short-form root was denied. A genuine escape
was always still denied; the guard was falsely strict, never weaker.
0/N verdicts presentand parallel dispatch handedevery task a corrupted
eval_root,dispatch_prompt_path, andoutputs_dironWindows.
jq's native Windows build opens stdout in text mode, so every\narrived as
\r\nand the carriage return rode on the end of each path.skill under test. Staged
.claude/skills/<slug>/SKILL.mdcrosses Windows'260-character limit, and the task repository's deliberate config isolation
discarded any global
core.longpaths. The loud form aborted withFilename too long; a few characters deeper,git addwarned, exited zero, andcommitted silently — so every later diff would have been measured against a
baseline missing its subject.
skipped_prompt_unreadstayed0on Windows, recording a dispatch that neverreceived its instructions as valid data. The check searched serialized JSON for
a raw path, and serialization escapes every
\separator. It now walks theargs' string leaves and compares with separators normalized — which also matters
off Windows, since the dispatch path is wire format while the transcript echoes
the agent's own spelling.
source, presenting a contaminated arm as comparable data.
collision it exists to block, having found no basename to compare.
harness lint --probereported false failures for{guard_args}in codex'sparallel and judge command templates. The placeholder had no render-only
stand-in, though the real dispatch path resolves it fine.
Internal changes
ubuntu-latestandwindows-latestwithfail-fast: false; clippy runs on both, since the#[cfg(windows)]arms arelint-invisible on Ubuntu.
#[cfg(unix)]hid testsfrom compilation and clippy on the other host and hid the coverage gap with
them;
report_skipprints why a test skipped, andEVAL_MAGIC_REQUIRE_POSIX_TOOLS=1— set on both runners — turns any skip into afailure.
__fixturesubcommand supplies the predictable child process the suiteneeds, in one invocation that
sh -candcmd /Cparse identically, replacingtwo divergent dialects of shell stub.
the guard: they built marker and manifest JSON with
format!, and aninterpolated Windows path embeds invalid JSON escapes. They now serialize with
serde_json, as the production installer does.core::fsgainsartifact_pathandnormalize_separators, the rendering andcomparison halves of the wire-format work.
record_runs.rsintorecord_runs/prompt_read.rs, bringing the module back under the size threshold.%TEMP%can be an 8.3 short name that git expands before measuring, which moved the
deepest test across a ceiling and read as a Windows-only product failure.
docs/developer_overview.mdrecords the platform support tiers; contributordocs updated for the third gated capability.