Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
cc87ddc
Merge pull request #243 from slowdini/main
slowdini Aug 13, 2026
0ecf650
fix(tests): let the suite build and compare correctly on Windows
slowdini Aug 14, 2026
2c62ad3
Merge pull request #250 from slowdini/fix/windows-test-portability
slowdini Aug 14, 2026
7ad2c64
feat(core): add artifact path rendering helpers
slowdini Aug 14, 2026
cdda80f
fix(sandbox): keep POSIX-rooted paths rooted when classifying
slowdini Aug 14, 2026
caeba6d
fix(artifacts): render generated path fields as forward-slash wire fo…
slowdini Aug 14, 2026
1302843
fix(pipeline): match paths across host spellings when comparing
slowdini Aug 14, 2026
379c996
fix(tests): stop encoding one platform's path rules in fixtures
slowdini Aug 14, 2026
7a3e326
refactor(record-runs): extract the prompt-read guard; finish path cov…
slowdini Aug 14, 2026
2feea35
Merge pull request #251 from slowdini/fix/windows-path-handling
slowdini Aug 14, 2026
f50ca56
fix(windows): resolve a POSIX shell; gate tests on capabilities, not …
slowdini Aug 14, 2026
baba434
fix(probe): supply a {guard_args} stand-in to the render-only checks
slowdini Aug 14, 2026
15f0749
Merge pull request #267 from slowdini/fix/windows-posix-shell
slowdini Aug 14, 2026
a0dc795
Merge branch 'dev' into fix/probe-guard-args-stand-in
slowdini Aug 14, 2026
ef67a7f
Merge pull request #268 from slowdini/fix/probe-guard-args-stand-in
slowdini Aug 14, 2026
7fd7096
fix(run): declare a POSIX shell + jq requirement and preflight for it
slowdini Aug 15, 2026
cfbcaeb
Merge pull request #269 from slowdini/fix/posix-tooling-requirement
slowdini Aug 15, 2026
7a07772
fix(run): set core.longpaths on runner-owned task repositories
slowdini Aug 15, 2026
e4d52ea
docs(run): tighten the long-path comments
slowdini Aug 15, 2026
26fae16
Merge pull request #271 from slowdini/fix/windows-longpaths
slowdini Aug 15, 2026
e446cec
fix(recipe): strip the carriage return a Windows jq emits
slowdini Aug 15, 2026
191e231
ci: run the suite on windows-latest
slowdini Aug 15, 2026
5bb6d27
test(run): measure deep task roots the way git measures them
slowdini Aug 15, 2026
4147ca8
fix(run): resolve a run's cwd to one path spelling
slowdini Aug 15, 2026
3fe5eb1
Merge pull request #273 from slowdini/fix/canonical-workspace-root
slowdini Aug 15, 2026
3a06a9c
Merge branch 'dev' into ci/windows-job
slowdini Aug 15, 2026
ed3afbd
Merge pull request #272 from slowdini/ci/windows-job
slowdini Aug 15, 2026
1a5363a
fix(run): confine dispatch guidance to the preparing host
slowdini Aug 15, 2026
157878f
Merge pull request #274 from slowdini/fix/windows-wsl-hybrid-guidance
slowdini Aug 15, 2026
234dcc3
chore: bump version to 0.9.1
github-actions[bot] Aug 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@
# Check every text file out with LF on all platforms. Generated artifacts embed the bytes of
# descriptors, profiles, and docs verbatim, and the byte-exact tests compare against LF fixtures, so
# a CRLF working tree on Windows silently changes program output.
* text=auto eol=lf

# Golden fixtures are byte-exact — never EOL-normalize them.
tests/golden/** -text
tests/fixtures/** -text
32 changes: 31 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,13 @@ env:
jobs:
test:
name: Test suite
runs-on: ubuntu-latest
strategy:
# Windows is the platform this matrix exists to cover, so a Linux failure
# must not cancel it — that is exactly when its result is worth having.
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Install Rust toolchain
Expand All @@ -25,11 +31,35 @@ jobs:
components: rustfmt, clippy
- name: Cache cargo build
uses: Swatinem/rust-cache@v2
- name: Install jq
# Git for Windows supplies sh, xargs, tr, and wc, but not jq, and the
# judge-recipe tests execute the shipped pipeline text rather than a
# stand-in for it.
if: runner.os == 'Windows'
run: choco install jq --yes --no-progress
- name: Permit symlink creation
# Windows creates symlinks only under Developer Mode or elevation, and
# the core::fs round-trips need one. Asking for it explicitly beats
# depending on how the runner's token happens to be built.
if: runner.os == 'Windows'
run: >
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock"
/t REG_DWORD /f /v AllowDevelopmentWithoutDevLicense /d 1
- name: Format check
run: cargo fmt --all -- --check
- name: Clippy
run: cargo clippy --all-targets --all-features -- -D warnings
- name: Test
# Capability-gated tests (the POSIX recipe pipelines, symlink
# round-trips, long-path staging) skip with a printed reason on a host
# that lacks the capability. This turns every such skip into a failure,
# so neither runner can quietly stop covering them. Ubuntu ships the
# recipe tools; the steps above provide them on Windows. Long paths need
# no provisioning — the runner passes core.longpaths to git itself.
# EVAL_MAGIC_SH stays unset deliberately: discovering the shell from the
# Git install root is what a Windows user hits, so CI should run it too.
env:
EVAL_MAGIC_REQUIRE_POSIX_TOOLS: 1
run: cargo test --all-targets
- name: Build release
run: cargo build --release
Expand Down
39 changes: 39 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,45 @@ is the default — most modules use it. Extract only when that module outgrows i
do) or to a single `<topic>_tests.rs` sibling (as `adapters/guard/guard_denial_tests.rs` does).
Extraction is a size decision, not a style preference; don't split a small inline module.

**Spawning a child process from a test.** Use the hidden `__fixture` subcommand, never `sh`, `true`,
`printf`, or a `#!/bin/sh` stub. It exits with a chosen code, emits chosen bytes, writes a chosen
file, or checks a file or variable — see `FixtureArgs` in `src/cli/args.rs`. One invocation parses
the same under `sh -c` and `cmd /C`, which is what keeps `command_check` tests off per-OS command
strings. Build the command with the `fixture` helper (`tests/run/helpers.rs` for integration tests,
the one in `src/pipeline/grade/command_check/tests.rs` for unit tests). Because the fixture is the
binary, `cargo test --lib` alone does not build it — run `cargo test`, or `cargo build` first.

**Tests are gated on capabilities, not on the OS.** `#[cfg(unix)]` on a test hides it from
compilation and clippy on the other host and hides the coverage gap. Instead, probe for what the
test actually needs and call `report_skip` (`src/core/runtime.rs`), which prints the reason and
returns `true`. Setting `EVAL_MAGIC_REQUIRE_POSIX_TOOLS=1` turns every skip into a failure; CI sets
it on both runners, so neither can quietly stop covering something. Three capabilities are gated
today: the recipe tools beyond the shell itself (`require_posix_toolchain` — in practice `jq`),
symlink creation, which Windows allows only under Developer Mode, and creating a path past
Windows' 259-character limit (`deep_task_root`, `src/cli/run/orchestrate/git.rs`). The Windows
runner is provisioned for those rather than exempted from them, so a skip there is a red build. The
shell is not one of them; it is a hard requirement, per the section below.
`require_posix_toolchain` is not test-only either — the `run` preflight uses it to warn about the
same gap. Where a genuine per-OS difference is the behavior under test — signals, path separators —
branch on `cfg!(windows)` at runtime so both arms still compile everywhere.

**A POSIX shell is required, for use and for development.** Harness `exec_template`s are POSIX
command lines, so the dispatch and probe paths spawn `sh` via `posix_shell()`
(`src/core/runtime.rs`) rather than a hardcoded `/bin/sh`: it searches `PATH`, then a Git for
Windows install. Set `EVAL_MAGIC_SH` to override it. `cargo test` inherits the requirement — the
scripted-turn tests spawn a `#!/bin/sh` harness stub through the resolved shell and do not skip —
so a host without `sh` fails the suite instead of quietly covering less. `jq` is required alongside
it for the parallel-dispatch and judge recipes; Git for Windows supplies the shell, `xargs`, `tr`,
and `wc`, but not `jq`. `POSIX_TOOLING_REQUIREMENT` (`src/core/runtime.rs`) is the one wording the
Markdown-carrying surfaces reuse: the shell-discovery errors, the `run` preflight warnings,
`RUNBOOK.md`, and `dispatch-manifest.md`. State the requirement from there rather than rephrasing
it. `--help` is the one deliberate restatement (`AFTER_HELP` in `src/cli/help.rs`), hard-wrapped and
backtick-free because clap renders into a terminal; keep the two in step by hand.

Which platforms that requirement is honored on — and why preparing on Windows but dispatching from
WSL is a correctness boundary rather than a preference — is stated once under "Platform support" in
`docs/developer_overview.md`.

**Where user-facing warnings come from.** Library modules (`pipeline`, `workspace`, `sandbox`,
`adapters`) never print. They return warning strings on their result struct — `#[serde(skip)]` when
that struct is also a serialized artifact — and the `cli` handler prints them with the `⚠ ` prefix.
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "eval-magic"
version = "0.9.0"
version = "0.9.1"
edition = "2024"
description = "One-stop CLI for running skill evals — measure whether an agent skill actually shifts behavior."
license = "MIT"
Expand Down
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,16 @@ The installed CLI is the primary manual. Start with `eval-magic --help`, and use

## Install

Git is required at runtime. Prebuilt binaries for macOS, Linux, and Windows are attached to each
Git is required at runtime, plus a POSIX shell with `jq`: the dispatch and judge recipes eval-magic
generates are POSIX command lines built on `jq`, `xargs`, `tr`, and `wc`. The shell that runs them
has to resolve the same paths the workspace was prepared with. On Windows that is Git Bash (Git for
Windows), with `jq` installed separately — Git for Windows does not bundle it. WSL resolves a
different filesystem namespace, so run eval-magic inside WSL rather than dispatching into it.
Set `EVAL_MAGIC_SH` to select a specific `sh`.

Windows support runs through Git Bash and is deprecated: a future release will require WSL.

Prebuilt binaries for macOS, Linux, and Windows are attached to each
[GitHub release](https://github.com/slowdini/eval-magic/releases).

macOS or Linux:
Expand Down Expand Up @@ -143,6 +152,10 @@ Issues and planned work are tracked in the

## Development

Development carries the same host requirement as use: a POSIX shell with `jq`. The scripted-turn
tests spawn `#!/bin/sh` harness stubs through the resolved shell and do not skip, so the suite
cannot pass without one. Tests that need `jq` or symlink creation report a skip instead.

```bash
cargo fmt --check
cargo build
Expand Down
27 changes: 27 additions & 0 deletions docs/developer_overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,40 @@ following authorities:
infer one harness's flags or event shapes from another harness.
- Tests and golden artifacts for behavior that crosses a module or CLI boundary.

## Platform support

| Tier | Platform | Verified by |
| --- | --- | --- |
| Supported | Linux, macOS | the `ubuntu-latest` CI job |
| Deprecated | Windows, through Git Bash (Git for Windows) | the `windows-latest` CI job |
| Unsupported | preparing a workspace on Windows and dispatching it from WSL | — |

Windows support is deprecated in favor of WSL, and its removal is gated on #256, which replaces
the generated POSIX recipes with a runner-driven `eval-magic dispatch`. Until that lands, the
Windows runner stays green and Windows-native behavior is held to the same bar as any other
platform: a Windows failure is a real failure, not an accepted gap. Do not add new Windows-native
accommodation in the meantime.

The unsupported row is a correctness boundary rather than a preference. A generated recipe carries
the absolute paths of the host that prepared the workspace. Git Bash shares the Windows filesystem,
so those paths resolve; WSL resolves its own namespace, where a `C:\…` path names nothing. Nothing
in the tree translates between the two, so the split fails quietly instead of loudly.
`POSIX_TOOLING_REQUIREMENT` (`src/core/runtime.rs`) is the single wording every user-facing surface
reuses to state this; `src/cli/help.rs` restates it for clap by hand.

## Make and verify a change

Trace the user-visible behavior from the CLI handler into library-owned logic and artifacts before
editing. Add a focused failing test at the narrowest useful boundary, implement the change, then
run the focused test again. Cross-harness changes belong at shared descriptor, runner, or adapter
boundaries unless the evidence requires a named harness capability.

Development carries the host requirement the tool itself declares: a POSIX shell with `jq`. The
scripted-turn tests spawn `#!/bin/sh` harness stubs through the resolved shell and do not skip, so
the suite cannot pass without one. Tests needing `jq`, symlink creation, or a path past Windows'
259-character limit report a skip instead; `EVAL_MAGIC_REQUIRE_POSIX_TOOLS=1` turns those skips into
failures, as CI sets it to do on both its Ubuntu and its Windows runner.

Before handing work off, run:

```text
Expand Down
2 changes: 2 additions & 0 deletions profiles/shared/runbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ This runbook is for a human driving the run from a terminal. Work from this iter
and copy-paste each step. The workspace is self-contained — you should not need the surrounding
repo.

> **Requires:** {{POSIX_REQUIREMENT}}

- **Skill under test:** {{SKILL_NAME}}
- **Mode:** {{MODE}} — comparing `{{COND_A}}` vs `{{COND_B}}`
- **Dispatches:** {{NUM_TASKS}} (the `tasks[]` array in `{{DISPATCH_JSON}}`)
Expand Down
Loading
Loading