Skip to content

v1.11.0 — MCP preflight and evidence, fuzzed readers, permission bits

Choose a tag to compare

@yottayoshida yottayoshida released this 11 Oct 02:58
· 129 commits to main since this release
c847ea4

Added

  • Five readers of bytes a target or a caller chose are fuzzed: the trace, a sideeye.toml, the executable a define names, a saved case, and the MCP transport (#695, ADR 0112; src/fuzz.zig, src/fuzz_parsers.zig, build.zig, .github/workflows/fuzz.yml, SECURITY.md). Each has an entry point in the form Zig's fuzzer takes, reaching the reader as production does — the trace and the executable through a file on disk — and zig build test drives each with a thousand mutated inputs from a fixed seed; zig build fuzz -Dfuzz-seed=… -Dfuzz-runs=… runs them alone, and a weekly workflow runs them from a new seed, two hundred thousand times each. A run that dies leaves its input in a file and the same seed stops at it again. Not coverage-guided: Zig 0.16.0's own test runner does not compile under -ffuzz (measured), and the move that brings it is #782. To reach the case file and the MCP transport without a process exit or a spawn, replay's validation of a case moved from main.zig into case.read with every refusal word for word, and the MCP server's line framing and its JSON-RPC envelope check moved into mcp.Lines and mcp.route; what the server writes and runs is unchanged.

  • A weekly build notices a Zig release that will not build Sideeye (#698, ADR 0111; .github/workflows/zig-ahead.yml, spike/zig-ahead.sh, spike/zig-ahead-known.tsv, the zig-ahead-judge job in ci.yml). Zig 0.17.0 shipped on 2026-10-01 and stops a source build in build.zig (#782); nothing here had looked. The workflow builds and tests with the pinned Zig, the latest release and master, on Linux and macOS, and judges each lane against a list of the breakages already known: red only on a release or a diagnostic the list does not explain, on a row that has gone stale, or on the pinned lane failing — so a release after 0.17 that breaks the build is noticed before #782 lands. It never runs on a pull request; the judge's selftest does. docs/cli.md points source builders at it. Dependabot for action pins stays deferred: no pin has been bumped by hand since ADR 0061.

  • CI fails a pull request, or a push to main, whose tracked Zig files zig fmt would change (#695, its formatting half; spike/check-zig-fmt.sh, the fmt job in .github/workflows/ci.yml). Nothing had run zig fmt --check, and eight tracked files had drifted from what it produces; they are formatted here and nothing else in them changes. The files are collected by git ls-files, so a new directory is covered without a line, and the check's selftest goes red on an unformatted file two directories down, on a file the formatter cannot parse and on an empty set. CONTRIBUTING.md and CLAUDE.md name the command to run before pushing.

  • The platform probe measures four more forms before the table is rewritten (spike/platforms/, spike/platforms/RUNS-RULE-2026-10-10b.md): WSL2 with systemd turned off (Ubuntu's WSL image turns it on, so what the 2026-10-10 table called "without systemd" ran under it; the import now records the image's own setting), WSL2 on Windows on ARM with the WSL package installed (the runner has WSL's Windows features and not WSL), GitHub Actions' container: on aarch64, and cross builds for the rest of Debian 13's release architectures (armel, ppc64le, s390x).

  • Four fixes upstream, walked through (#722; docs/case-studies.md, linked from the README's line of counterexamples). dotenvx encrypt, Neovim :wshada, RuboCop -a and ImageMagick mogrify: for each, the two calls a kill fell between, what the user lost, what the project changed, and what re-running Sideeye against the fix showed, with links to the reports, the fixes and this repository's records. Release status is dated 2026-10-10. ADR 0114 also records the Linux run of the platform probe's hardened legs under --user: the same verdicts as root.

  • A run refused multiple_threads_detected from its own record of its threads names, in its next step and in every mode, a tool's own switch for running its file calls on one thread, and how a define declares it (#686, ADR 0113; src/contract.zig, src/boundary.zig, docs/apparatus.md, docs/report-schema.md, spike/dogfood/2026-10-10-uv-threadpool/). threads_limit keeps the README's limit and adds UV_THREADPOOL_SIZE=1 for Node and GOMAXPROCS=1 for Go, set where Sideeye runs and declared as env: in apparatus; under --observe supervised the refusal's step is the new threads_supervised, the same switch said without a shim, where it was the class wall. docs/apparatus.md lists each switch measured, what it moved and what it did not, and why. The measurement #686 asked for: the thirteen Node targets it lists all refuse at the recording on v1.10.0 without the switch, and ten get past with it (2026-10-09); declared, three of them reproduce 2026-10-09's verdicts. ADR 0097 had offered no flag, on the belief that none existed. The same reason raised under macOS's --oracle-fs-usage for a writer the shim never recorded keeps its step. next_step is not a frozen surface; contract_version does not move.

  • #687 answered: recording thread creations and exits from outside would order only 2 of the 50 recording runs --observe supervised refused for its threads wall, so it is not built (spike/dogfood/2026-10-10-thread-order/, docs/cli.md, ADR 0089, spike/dogfood/RUNS.md). The released v1.10.0 refused multiple_threads_detected in 50 recording runs of jj 0.46.0, OpenTofu 1.13.1, codex 0.160.0 and doctl 1.177.0, and each refused run's oracle capture was read for a thread creation or exit between one writer's write and the next's (order.py, checked against eight synthetic captures, three broken readers its selftest builds and four of its cases catch, and two C controls through the same pipeline). 48 hold a hand-over neither event orders; doctl's other 2 of 35 were ordered by a creation the Go scheduler happened to make, and a run is judged only when the recording and every world are ordered. notesmd-cli was not refused in 15 recording runs on v1.10.0; tombi, refused in 1 of 36 explores and 3 of 20 replays with its preflight accepted 26 times of 26, was not read. The owner's decision, from those counts.

  • docs/cli.md's platform table leaves a row unmeasured, or a cell not run, only where no GitHub-hosted runner can take the platform, the owner ruled it out, or the engine has no code for it — and says which (ADR 0105, amended twice; spike/platforms/2026-10-10b/, README). The v1.10.0 release, measured again on GitHub's runners: Linux kernels from 4.18 to 7.0 booted on their own (all three modes from 6.1 up; default and syscalls below, where supervised wants 5.19), glibc from 2.28 to 2.43, GitHub Actions' container: on both CPUs, docs/mcp.md's hardened container in the form the page now gives, NixOS with nix-ld, macOS 14 and 15 on the demo, and WSL2 on the Windows drive, as an ordinary user, and with systemd on (the image's own setting) and off — all supported. Declined, on the owner's rulings: glibc older than 2.28 (the binary does not start), kernels older than 4.18 (not measured), musl (built from source it runs; the release binary does not), WSL1 (its kernel has no statx, which the engine reads files through). Other CPUs are built and not run, on the owner's ruling: riscv64 and s390x build; for armhf, armel, i386 and ppc64le the shim does not. Unmeasured, with the reason in the cell: macOS 13 (no runner) and WSL2 on Windows on ARM (the runner cannot start it even with WSL installed: no virtualization passed on). The 2026-10-10 table's "without systemd" for WSL2 was wrong: Ubuntu's WSL image starts systemd itself. README's install line says macOS 14+. The probe found two Sideeye defects on every macOS version: --oracle-fs-usage never finds the subject when --work is reached through a symlink, the default among them (#795), and refuses a write on a descriptor moved by dup2, as GNU dd of= does (#796).

  • The acceptance suite runs against the build the release ships for aarch64-linux on every pull request and every push to main, so it has run before a tag exists (#694; .github/workflows/ci.yml job linux-aarch64-releasesafe, spike/release-build-flags.sh, .github/workflows/release.yml). On GitHub's ubuntu-24.04-arm runner, the release's own runner: spike/acceptance.sh uncontained and in a delegated cgroup, and spike/mcp-acceptance.sh, against one zig build of the shipped pair and every -Dtest-* apparatus with the arguments read from release.yml's build line for its aarch64-linux entry; the job checks that release.yml's runner and Zig version are its own, and that the shipped pair it tested is byte for byte a plain release build's. Until now the shipped optimisation was exercised only by release.yml's demo, which runs when a release is published or release.yml changes, and the suites ran Debug on x86_64. Measured before the job existed (Docker on Apple silicon, outside a delegated cgroup): the same 539 acceptance checks pass on a Debug and a ReleaseSafe build, with the same 4 not measured, and the ReleaseSafe shim's largest stack frame is exactly the 1024-byte ceiling, where the Debug build's is 928.

  • docs/faq.md answers the questions a first visitor asks, one paragraph each, and the README links it first (#718; docs/faq.md, spike/faq/2026-10-10/, README.md). Does it check my fsync; how it differs from a kill -9 loop, strace --inject, ALICE and LazyFS; how long it takes; how to call it from pytest, cargo test and go test; why a first run on macOS is UNKNOWN; and what to do when a tool keeps its state in two places. The answers were scattered across DESIGN and the ADRs, and two had never been tried. Everything measured was run before it was written (spike/faq/2026-10-10/RESULTS.md): each recipe green on a correct tool, red on a planted bug, red with the witness swapped for --allow-unverified — Sideeye exits 0 there, so the recipes read oracle_verified from the report — and red with no sideeye on PATH, in a Debian container with strace, plus the macOS variants; a file closed without fsync and renamed PASSes 4/4, and one renamed while its bytes are still in the program's buffer FAILs; and a tool whose config and data follow XDG_CONFIG_HOME and XDG_DATA_HOME FAILs on its data file once both point under the state. Things the runs found that the page now says: apparatus stops a run that forgets one of those variables only after the setup has run — the setup had already written under HOME — so the page tells the reader to point HOME at an empty directory too; a pure-Go binary is static and goes to --observe supervised, which Docker's defaults refuse as a SETUP ERROR naming the cgroup; Homebrew's python3 on macOS is a framework launcher, refused with the interpreter to use named; and on macOS an fcntl(F_FULLFSYNC) is not a crash point. The external tools' descriptions were read from their own pages (strace(1), the ALICE paper, LazyFS's README).

  • An outsider has a door: two issue forms and a CONTRIBUTING.md (#719; .github/ISSUE_TEMPLATE/unknown.yml, .github/ISSUE_TEMPLATE/verdict.yml, CONTRIBUTING.md, README.md, PRD.md). The project measures itself by outside use, and its tracker had no way in: no template, no contributing page, Discussions off. One form is for an UNKNOWN or a SETUP ERROR, asking for the unknown_reason or setup_error_reason (a SETUP ERROR prints only a sentence, so the form says where the name is), an UNKNOWN's next_step, the version, the platform, how Sideeye was installed, the command and the --json report; the other is for a verdict — a real bug Sideeye found, which belongs to the tool's own project and is linked here, or a PASS or a FAIL believed wrong — and asks for the define and the check as well. The issue named the second form fail.yml; it covers PASS too, because a wrong PASS is the failure this project ranks worst, and a right FAIL is reported upstream rather than here. CONTRIBUTING.md says what a useful report carries, and the owner's position on reports written with a language model (2026-10-10): welcome, when the report says so, carries the reporter's own --json report and version, and has been read by whoever files it. The README gains a CI badge and a release badge on its second line, in place of a blank one, so the page stays at 98 lines, and says its licence in six words rather than thirteen, which pays for the badges' words and leaves six to spare. DESIGN §15 rules out a "Passed Sideeye" badge, and PRD's out-of-scope list now names that badge rather than badges in general.

  • An MCP container to copy, run as written in CI (#721, ADR 0114; docs/mcp-container/Dockerfile, the section "Running it in a container" of docs/mcp.md, spike/check-mcp-container.py, .github/workflows/mcp-container.yml). docs/mcp.md told an operator to run the server in a container and named the image your-image. The Dockerfile takes a release's own tarball, checked by ADD --checksum against the digest the release publishes, adds strace and points SIDEEYE_MCP_ORACLE at it, and leaves its end for the operator's tool, which has to be in the same image as the server. No image is published. The block builds from a directory of its own and runs the server as the operator's user (--user "$(id -u):$(id -g)", the one flag added): a README-shaped define keeps its state inside the mount, and as root with no capability the server could not write a directory the host's user owns on Linux with Docker running as root. spike/platforms/runner.sh's hardened legs measure the page's line and gain the same flag. A new workflow runs the page's block as written on x86_64 and aarch64 runners, with a toy added at the end of the Dockerfile and README-shaped defines: a FAIL on a planted bug, a PASS whose oracle_verified is true on a correct target, and the flags checked from inside the container (a user other than root, no capability in the bounding set, NoNewPrivs, no route, a root file system that refuses a write). The block's comment said :exec on the tmpfs was required because the engine executes from its work directory; measured, the server executes nothing there and the required half is the tmpfs itself, which is what the comment now says.

  • The platform probe counts the hardened container legs again in the form docs/mcp.md now gives (spike/platforms/RUNS-RULE-2026-10-10b.md). The page's block gained --user after the probe's first run measured it as root (#789, ADR 0114); a dated item counts hardened-cli and hardened-mcp from the next jobs: linux dispatch, so the table's row can speak for the page as it reads.

  • The platform probe takes another try at its two apparatus faults, and asks why WSL1 and macOS answered as they did (spike/platforms/, spike/platforms/RUNS-RULE-2026-10-10b.md). The runner's own apt-get in vm.sh keeps its output, has a limit and is tried once more (run 38031881569's x86_64 Rocky 9 job stopped there for two hours with nothing written; why is not known); Ubuntu 20.04's arm64 image is booted under TCG with -cpu cortex-a72, since with -cpu max it stops after GRUB on the runner, and with the same QEMU and firmware in Docker it stops there with max and reaches a login prompt with cortex-a72. Dated in the rules before the dispatches they govern: statx-probe.py asks WSL1's kernel the calls its two refusals rest on — statx for the shim search, access and statx for the PATH search — because with --shim WSL1 refused again, saying dd was not on PATH while /usr/bin/dd was; and three macOS legs move --work, because every macOS default leg, the control's included, was UNKNOWN oracle_saw_nothing with --work under $TMPDIR, and the path's length and its spelling (through /var's symlink, with a doubled slash) both fit.

  • The platform probe follows WSL1's refusal to its next step (spike/platforms/wsl2-inner.sh, spike/platforms/RUNS-RULE-2026-10-10b.md). On WSL1 the engine refuses the shim it found as unclassifiable and says to name it with --shim; a wsl1-shim leg does so once, in a later jobs: wsl dispatch, so the table's WSL1 row can say what that step does rather than leave it not run. The rule for it is dated and was added before the dispatch it governs.

  • The platform probe grows to the rows of the platform table a hosted runner can take (ADR 0105, amended; spike/platforms/, .github/workflows/spike-platforms.yml). New jobs: Ubuntu 22.04 and 26.04 runners on both CPUs; GitHub Actions' container:; older kernels booted as virtual machines under QEMU (Debian 13 and 12, Rocky 10, 9 and 8, Ubuntu 22.04 and 20.04, both CPUs) beside the same userland on the runner's kernel; macOS 14 and 15 against macOS 26; WSL1, WSL2 on Windows on ARM, and WSL2 on the Windows drive, as an ordinary user and under systemd; NixOS's loader forms; glibc 2.17 and 2.24; docs/mcp.md's hardened container through the CLI and through sideeye mcp; and the source built on Alpine, for other CPUs (built, not run, on the owner's ruling) and for Windows. measure.sh takes the state's place (S) and records ptrace's scope, the cgroup and the user. The rules for reading the new runs are spike/platforms/RUNS-RULE-2026-10-10b.md, written before them; once the table is rewritten from their record, unmeasured will mean a hosted runner cannot take the platform or the owner ruled it out.

  • #687's question answered for Jujutsu: its two writing threads are ordered by a futex hand-over, not by a creation or a join (spike/dogfood/2026-10-10-followups-7/, spike/dogfood/RUNS.md). The thread that writes the blob, one of 11 the process creates at start, renames it into .git/objects, wakes the first thread through futex, and lives until the process ends; the first thread writes 1.4 ms after the rename. Follow-ups 6's trace passed -qq and dropped thread exits, so it could not say.

  • docs/cli.md says, for every platform an adopter is likely to bring, whether it is supported, unmeasured or declined (#697, ADR 0105; spike/platforms/2026-10-10/, README, docs/ci-quickstart.md). One run of the v1.10.0 release on GitHub's runners (38018694987, at #777's merge commit) measured the cells: WSL2 on x86_64 is supported, all three observation modes giving the right FAIL inside Ubuntu 24.04 on a Windows Server 2025 runner, as root; a container with docker's default privileges (Rocky 8, as root) is supported with supervised a setup error that names the cgroup it needs; on Alpine the release binary does not start, and with gcompat the default and syscalls modes judge while supervised refuses with a reason that blames the operation for the engine's own failure — musl stays declined, as do Intel macOS, Windows, the CPUs the release does not build and glibc older than 2.28. Kernels older than the runners' 6.17, macOS 13 to 15 (the asset's minimum is 13.0; it has run on macOS 26), NixOS, WSL1 and WSL2 on Windows on ARM are unmeasured. The README's install line now says glibc; the CI quickstart says its installer picks the glibc asset in a musl container. spike/dogfood/2026-10-10-followups-6/RESULTS.md carries a dated correction of its line saying #697 needed machines this project did not have.

  • A probe for the platform table #697 asks for: the released engine on one define, on Linux runners (the runner itself, Rocky 8, Alpine) and inside WSL2 on a Windows runner (ADR 0105; spike/platforms/, .github/workflows/spike-platforms.yml). Run by hand at this merge commit; the rules for reading it are in spike/platforms/RUNS-RULE.md, written before its first dispatch, and the table lands in docs/cli.md with its record. ADR 0105 records the three words and the declined platforms: musl, Intel macOS, native Windows, CPUs the release does not build, and glibc older than 2.28.

  • Jujutsu is judged under --observe supervised when its commit has nothing to snapshot: PASS 82/82 on 0.44.0 and 0.46.0 (spike/dogfood/2026-10-10-followups-6/; docs/target-classes.md, spike/outcome-funnel.tsv, spike/dogfood/RUNS.md, spike/unknown-rate/b2-exclusion-aliases.tsv). #687's comment had left jj unmeasured. The thread wall in front of it is the snapshot: a modified file's blob is written from a second thread (multiple_threads_detected), and with the modification snapshotted first the run refuses baseline_run_failed, because restore assigns timestamps and jj rewrites tree_state in every restored world — what preflight --twice names and explore's refusal does not. ktlint/ktlint#3409 is acknowledged: a maintainer reproduced it on 2026-10-09.

  • The dotenvx run's second recount: the library's programmatic set is covered by the patch too (spike/dogfood/2026-10-10-dotenvx/; docs/target-classes.md, spike/outcome-funnel.tsv, spike/dogfood/RUNS.md). The run's same-class scan had listed src/lib/main.js — the README's dotenvx.set(...), with synchronous writes of its own — as atomic since the patch without running it. Run through Sideeye on an encrypted .env and into a second file that rewrites .env.keys, it FAILs on 2.32.4 and PASSes 9/9 on 2.34.2 in both. No new report

  • docs/unknown-rate.md carries a section measured on Linux x86_64: generation g4, the B and B2 groups, verdict for verdict as g3's (#696; spike/unknown-rate/artifacts-g4/, spike/unknown-rate/generations.tsv, docs/unknown-rate.md, docs/kill-criteria-review.md). The sweep ran once, at the merge commit of its apparatus, on GitHub's x86_64 runner with g3's v1.5.0 in its x86_64 build (run 38006296132; machine: x86_64 x86_64 in the record): B 2/7 and B2 5/19 UNKNOWN, each of the twenty-six explored trials with g3's verdict and refusal reason. Not a comparison of architecture alone — the Debian packages, kernel, VM and AppArmor profile differ too, and the Platform section says so. Criterion 4's basis stays g3; the kill-criteria review's Row 8 read g4 on its own numbers and is not triggered. macOS keeps a derived column and no measured one: the B and B2 targets are Debian packages.

  • The unknown-rate sweep records the machine it ran on, and a generation measured off Linux aarch64 names its platform (#696, ADR 0104; spike/unknown-rate/sweep.sh, spike/unknown-rate/count.py, spike/unknown-rate/fixtures/machine-*, spike/acceptance.sh, .github/workflows/spike-unknown-rate-x86.yml). apparatus.txt gains a machine: line (host and container) and, from a workflow, a ci-run: line; count.py reads a generation's platform from it, names it in the heading when it is not aarch64, and refuses a record whose machine line disagrees with itself, with the pinned asset's architecture, with its own trial directories (one launcher-rc each) or with the published heading. The sweep makes each trial directory from the host and stops when it cannot write launcher-rc or fold an oracle log; read from the code, not measured, a native Linux host would otherwise have gone on to exit 0 without them, since there the container's root owns what it makes. Generation g4 — the B and B2 groups on g3's release in its x86_64 build — is added unstarted, with its expectations and the rules decided before its results (spike/unknown-rate/artifacts-g4/expected-before-reading.md, written before the sweep: criterion 4's basis stays g3, the kill-criteria review's Row 8 reads g4 as any platform's sweep, and the page's threshold section says so before the sweep), and a hand-run workflow sweeps it on GitHub's x86_64 runner.

  • The dotenvx run's recount: readers during a write tear nothing on 2.34.2, Bitwarden's key custody PASSes, and protect empties the global ignore file through its own prompt (spike/dogfood/2026-10-10-dotenvx/; docs/target-classes.md, spike/outcome-funnel.tsv, spike/dogfood/RUNS.md). Asked whether the run was done, a count against its own selection found four rows short. The row named E3 had been measured as a different case; readers looping get and run while encrypt, set or decrypt writes read 0 torn values of 1,986 on 2.34.2, and the order leaves no window. Bitwarden, dropped as too costly, PASSes 14/14 and 21/21 against a stand-in bw. The interactive protect, measured by calling its function, was driven through its prompt in a terminal: unticking a protection with the file's writes failing ENOSPC leaves the global ignore file at 0 bytes. No new report

  • dotenvx's fix for #1012 holds for every write it reached; five it did not reach, and three new reports (spike/dogfood/2026-10-10-dotenvx/; docs/target-classes.md, docs/found.md, docs/outcome-funnel.md, spike/outcome-funnel.tsv, spike/outcome-funnel-campaigns.tsv, spike/upstream-reports.tsv, spike/dogfood/RUNS.md). The maintainer fixed dotenvx/dotenvx#1012 the same day (write-file-atomic in 2.34.2) and asked what else Sideeye found. Every write in dotenvx's source went to Sideeye beside 2.32.4: encrypt, set, decrypt and del FAIL there in six shapes and PASS on 2.34.2, including the shape that emptied .env.keys with a committed file's key in it. protect still rewrites a user's pre-commit hook and git attribute and ignore files with fs.writeFileSync and FAILs at all five, by a kill or a full disk alone (filed, dotenvx/dotenvx#1018). Two findings came from plain scripts beside the run, outside what Sideeye tests, and say so in their records: decrypt drops the backslash of an escaped $ and the value changes (dotenvx/dotenvx#1016), and two encrypt at once on one shared key file leave one env file undecryptable, every time (dotenvx/dotenvx#1017). Not filed, with the reasons in the records: the fix's own temporary key file left by a SIGKILL, spec --overwrite, a dangling symlink and a hard link

  • tombi's fix for tombi-toml/tombi#2265 holds for what was reported and leaves a narrower window; three upstream rows that had gone stale are moved (spike/dogfood/2026-10-09-followups-5/; docs/target-classes.md, spike/outcome-funnel.tsv, spike/upstream-reports.tsv, spike/dogfood/RUNS.md). v1.7.3 PASSes the report's seed where v1.7.0 FAILs, and FAILs a seed that formatting shortens: killed between the write and the set_len, the formatted text is whole at the head with the old tail after it, which tombi cannot parse. Measured although the owner had ruled on 2026-10-02 not to, a ruling the recount missed; the record is kept on the owner's call. qpdf/qpdf#1773 is declined (closed 2026-09-07) and python-poetry/poetry#11019 withdrawn (closed by this project 2026-08-22), each recorded as awaiting until now; ocrmypdf/OCRmyPDF#1762 got the PDF its report offered, at a contributor's request. Eleven reports still awaiting a reply, measured on releases older than their projects' latest, all still reproduce on the latest releases.

  • Three agent skills install from this repository: sideeye-scout sets Sideeye up on a tool and takes it to a verdict, sideeye-triage takes a FAIL on your own code to a fix, sideeye-report drafts a report for another project's maintainers (#716, ADR 0103; skills/, spike/check-skills.py, .github/workflows/ci.yml, README.md, spike/check-readme-shape.sh). npx skills@1.7.1 add yottayoshida/sideeye copies them into the agent's own skill directory, in the Agent Skills layout (skills/<name>/SKILL.md); the README names the command with the installer's version pinned, since it runs from npm with the reader's permissions. The procedures were in docs/scouting.md and spike/upstream-report-template.md, which an agent followed only when pointed at the repository, and whose relative links resolve to nothing outside it. A skill is a procedure and its prohibitions — propose, never judge; never weaken the checker to make a FAIL or an UNKNOWN go away; never run a command copied out of the evidence — with absolute links into main, since the copy leaves the tree, and no measured numbers. The report skill reads the project's contribution rules and its policy on LLM-written text first, stops where that policy forbids LLM-written or tool-generated reports, translates the person's own text only where the policy allows a machine translation, and never files: the person approves the draft word for word, or writes the report themselves. The skills install from main, so an MCP tool newer than the last release is named as "where the server lists it", with the command line beside it. spike/check-skills.py holds them to the code, in CI with its selftest first: the sideeye- prefix on every skill, the frontmatter, every sideeye <command> against the usage lines and every flag after it against that command's, every name borrowed from the source — MCP tools and their arguments, refusal reasons, define keys, SIDEEYE_* variables — against the source, with the tool, reason and key sets taken from the contract-freeze gate's own definitions, and every link into this repository against main's tree. Measured once each with Claude Sonnet 5.5, given only the installed skill, on a small tool whose set truncates its store before rewriting it: scout wrote a define and reached FAIL, triage fixed the tool, saw the saved case answer case_no_longer_applies and reached PASS, and report stopped at its first step beside a CONTRIBUTING that closes LLM-written issues — as did the same agent without the skill, so that run shows nothing the report skill adds. The review after those runs corrected sentences that were wrong although no run tripped on them: replay, like explore, needs a second witness or --allow-unverified to say PASS, and its refusal reads case_no_longer_applies; the state directory a define names is emptied for every world, so it is never the person's real data.

  • #678's three reach a verdict on a FAT filesystem, and behind the threads wall the second writer is the tool's design: eight targets run, six FAIL, none filed (spike/dogfood/2026-10-09-followups-4/; docs/target-classes.md, spike/dogfood/RUNS.md, spike/outcome-funnel.tsv). A FAT mount gives every file one mode and one owner, so the restore that does not rebuild them (#678) cannot change what the target sees: upx, argocd and prefsCleaner FAIL. strace -f names the second writer for every target still behind multiple_threads_detected — a lock directory, a home registry, a read/write test file, a delete pool, an I/O thread — and only gemini-cli moves, at mcp add's default project scope, where it FAILs; OpenTofu FAILs on one supervised run of two and neither replay can repeat it. The threads gate counts calls that failed and changed nothing (vercel's mkdir of the state root, EEXIST) and scratch paths (codex's tmp). goaccess's and rrdtool's shared mappings and libglnx's fallocate have no switch, read in their sources. easy-rsa, whose page's path ends at --observe syscalls with its libfaketime-preloaded children killed by the SIGSYS trap, FAILs under --observe supervised named by hand: index.txt absent between OpenSSL's two renames — #685's shape for a second refusal.

  • The MCP server's fourth tool, sideeye_preflight {config_path, observe?, twice?}: preflight's answer without exploring, with twice confined to SIDEEYE_MCP_STATE_ROOT (#717, ADR 0102; src/mcp.zig, src/cli.zig, src/main.zig, spike/mcp-acceptance.sh, spike/check-readme-mcp-call.sh, spike/acceptance.sh, docs/mcp.md, docs/contract-freeze.md, README.md). An agent revising a define over MCP could only run whole explorations. The tool runs preflight --config --json the way sideeye_explore_config runs explore and answers from preflight's JSON: a refusal or a stop is a report, summarised and isError: true like any; an accepted recording, or two runs that differ under twice, is sideeye/preflight with isError: false — answers to the question asked, as a FAIL is — whose text block puts the outcome and counts above one counted region in which every string the target or the define could shape is quoted apart from the define's apparatus and warnings, which go on lines of their own as the report's summary puts them, and names what to call next. twice empties and rebuilds the config's state before its second run, so the server always passes --state-under SIDEEYE_MCP_STATE_ROOT with it (owner ruling, 2026-10-09): a config an agent just wrote is not one an operator vetted. preflight --twice takes --state-under for this, and preflight without --twice refuses it — one observed run rebuilds nothing; the out-of-range refusal now names the state rather than a case's. The child's text output, which quotes the bytes --twice saw differ, is removed as soon as the child exits. The per-command help ceiling is 76 lines. mcp 25 to 28 drive it: accepted and refused, twice naming what differed with nothing quoted and no child output left, an outside state refused before setup and left as it was with a non-boolean twice refused at the edge, and a banner-named path kept inside the region; acceptance legs E2 and E3 hold the CLI's half.

  • sideeye preflight --json <path>: a refusal writes explore's report, an accepted recording or two runs that differ write a document of their own (#717, ADR 0102; src/cli.zig, src/main.zig, src/report.zig, spike/check-report-schema.py, spike/acceptance.sh, docs/cli.md, docs/report-schema.md, docs/contract-freeze.md). Preflight refused --json — explore "answers strictly more", by running every world — and an agent revising a define over MCP had no file to read preflight's answer from. A refusal or a stop now writes sideeye/report, carrying the refusal preflight's text shows — usually the one explore reaches on the same define, not always (with no crash point and neither --oracle nor --allow-unverified, explore answers its completeness gate first). An accepted recording, or --twice's two runs differing, writes sideeye/preflight: preflight produces no verdict, and verdict is a frozen closed set of four. It carries the text block as data — the outcome and its exit code, the operation count, the account sentences and figures under the report's own names (written by the same functions, moved out of buildJson whole, the report's bytes unchanged), and the judged set, which the recording phase preflight runs had already published — and leaves out the note and next lines, which are advice for the command line. Under --twice each differing path carries the byte line's shape, under the key shape — where, how long, what kind — and no byte of either run: the shape is made beside the quoted form inside observeAgain, while both snapshots are alive. Sealed like the report; schema_status is "frozen" from this first release (docs/contract-freeze.md surface 2). docs/cli.md loses two sentences that were already false: the quoted bytes were never only in preflight's output — under --oracle, strace's capture in the work directory holds the start of each write — and the seal paragraph said preflight refuses --json. say's buffer goes from 16 to 32 KiB, since the whole help is one say and had 27 bytes left; the per-command help ceiling goes from 68 to 72 lines. Acceptance holds an accepted document with its judged set, a split one whose differing stretches are in the text and in neither the document nor the work directory, and a refusal on a static target carrying the same reason and next_step as explore's — each sealed once — and check-report-schema.py holds the new document's rows in both directions.

  • The MCP server has a third tool, sideeye_evidence {case_path}: the bundle a FAIL saved beside its case, read without running anything (#717; src/mcp.zig, src/evidence.zig, spike/mcp-acceptance.sh, spike/check-readme-mcp-call.sh, docs/mcp.md, docs/contract-freeze.md, README.md). An agent could reach a FAIL and replay it through the server, and had no way to fetch the evidence the run had already measured. The tool takes the case's path or the bundle's own, both inside SIDEEYE_MCP_ROOT, and confines the bundle's resolved path again, so an evidence/ directory linked out of the root is refused like any other outside path, in the same sentence as a bundle that is not there. It is read in the server's own process — explore and replay self-exec because every verdict path exits (ADR 0010), and reading a bundle starts nothing — with the CLI's checks, now shared: sideeye evidence and the tool both read through evidence.parseBundle. The bundle sits in a work directory the target can write, so every string in it is treated as the target's: the text block carries closed words and numbers above one counted region in which each string is quoted with its quotes and backslashes spelled, and structuredContent carries the bundle re-encoded from the typed value, its unknown fields dropped and its case and replay replaced by the server's — replay names sideeye_replay_case with the resolved case path, never a command line a host-side agent could be led to run, and only when that path is printable ASCII, since a link in the work directory can give it a name the target chose. A report in the work directory could already take the whole server down, and this closes that too: the server put each explore's and replay's report-N.json through std's JSON writer to fit it on one line, and that writer tracks nesting in a fixed 256-level stack and panics one level past it in a safe build (measured on Zig 0.16.0, Debug and ReleaseSafe: 300 levels is index out of bounds). minifyJson now keeps the parse as its validity check and drops the whitespace outside strings itself, which has no depth to run out of; a unit test holds it, and panics against the old writer. A bundle never reaches that writer as read — it is parsed into its type, an unknown field skipped at any depth, and re-encoded. The Markdown for an upstream report stays sideeye evidence <case> at the command line. isError is false when the bundle is read and true when it cannot be (docs/contract-freeze.md surface 5 records the third use of its additive allowance). mcp 22 to 24 drive it: the bundle by either name, compared with the file; a rewritten bundle carrying a forged banner, a command line, a foreign case and a 300-deep unknown field that has to be read past, with the server answering a tools/list after it; refusals for a bundle outside the root; and no case offered when the bundle is named and the cases/ beside it is linked out of the root, or when the case is named and it is a link into a directory named with a byte outside printable ASCII.

  • The threads wall gave way to the target's own switch for one thread in nineteen of twenty-eight targets; with the last walls taken one switch or a FAT filesystem further, forty-one targets: 18 FAIL, 11 PASS, and one report upstream (spike/dogfood/2026-10-09-followups-3/). UV_THREADPOOL_SIZE=1 took Node tools past multiple_threads_detected, GOMAXPROCS=1 three Go tools of four, and zstd's --single-thread --no-asyncio and beets' threaded: no theirs; lz4 and the Rust tools stayed behind it. dotenvx's encrypt empties .env if it is killed after writing .env.keys (dotenvx/dotenvx#1012). Five targets that cleared a gate in earlier campaigns were explored for the first time. On a FAT filesystem vim writes no ACL back and Qt saves through a named temporary, so both reach a verdict; flatpak and ostree there meet fallocate. SubtitleEdit/subtitleedit#15829 was fixed upstream by the head re-measured the same day. multiple_threads_detected's next step names the README's limit and nothing a user can try.

  • SECURITY.md: a private way to report a vulnerability, and which versions are covered (#693). The repository had no security policy and private vulnerability reporting was off, for a tool that runs declared commands, empties a directory on replay and ships an MCP server. The page sends a reporter to GitHub's private vulnerability reporting and says only the latest release is supported, with fixes in the next release. It lists the protections Sideeye documents — the MCP server's confinement and its minimal environment, the shim search, the work directory's owner check (#692), the trace file's link and FIFO refusals, attribution in the text block of an MCP result — and counts as a vulnerability a way past one that its own document does not already name as open; ADR 0010, 0024, 0037, 0044 and 0099 name the windows they leave. Not a vulnerability: a config or a case running its commands, an exploration restoring the state directory its config names, and what a target does inside the directories it is given.

  • Six upstream fix PRs and fifteen unjudged targets, measured on the released v1.10.0: four fixes hold, two do not, twelve targets reach a verdict, and two reports go upstream (spike/dogfood/2026-10-09-followups-2/). The PRs for SubtitleEdit/subtitleedit#15829, aws/aws-cli#10648, beancount/beancount#1051 and helm/helm#32709 PASS where the builds the reports measured FAIL; Exiv2/exiv2#9504 trades the truncation for a window where the picture is removed before its replacement is renamed in (commented on the PR), and solvespace/solvespace#1784 keeps the truncation. Eleven targets whose two clean runs differ were judged with the paths preflight --twice names declared scratch and a checker reading the data through the tool, four others were taken one next step further: five PASS, seven FAIL. SoftHSM loses the token and every key in it if --import is killed while it rewrites token.object (softhsm/SoftHSMv2#908), and OCRmyPDF's documented in-place run empties the PDF (ocrmypdf/OCRmyPDF#1762). A checker that refuses the untouched state reads as a FAIL at crash point 1, which Sideeye does not catch (#756); and kill_did_not_land's next step does not name a target that picks its paths at random (ccache).

  • Three upstream fixes re-measured on the released v1.10.0, each beside the build its report was measured on: all three hold (spike/dogfood/2026-10-09-followups/). neovim's nightly PASSes 11/11 on 2026-09-16's define where v0.12.5 FAILs 2/13 (neovim/neovim#41940); PyMOL's master PASSes 4/4 where 3.1.0 FAILs 1/3 (schrodinger/pymol-open-source#520); DwarFS's mhx/work build refuses to recompress onto its own input where 0.15.8 empties it (mhx/dwarfs#388, not yet on its main). The same run asked four of user-data-4's walls under --observe supervised: Qt's O_TMPFILE save and Node's writer threads are refused there too. A recount of the 47 targets no campaign since 2026-10-03 had judged left two: Home Assistant's --script auth, past its trace wall under supervised, PASSes 8/8 with the auth store scratch and a checker, each step taken from the refusal's own next step; dotter's FAIL 12/19 repeats #690's. mbloch/mapshaper#706, closed as wontfix, is recorded as declined.

  • Twenty-two tools whose state git does not hold, in four waves on the released v1.10.0: 14 FAIL, 7 PASS and 1 UNKNOWN on the page's path, two reports upstream, and a shim defect (#753) (spike/dogfood/2026-10-09-user-data-4/; docs/target-classes.md, spike/dogfood/RUNS.md, spike/outcome-funnel.tsv, the B2 exclusions). Most FAILs are the truncating open and the kill before the write, in user config files; Subtitle Edit's seconv --overwrite empties the subtitle it converts, and MCA Selector's chunk deletion unlinks the region file before renaming its replacement in from the system temp directory — both reproduced without Sideeye by strace's inject and reported upstream (SubtitleEdit/subtitleedit#15829, Querz/mcaselector#613). The run found a defect in the shim (#753): an interposed call made from another shared library's constructor, before the shim's own .init_array has resolved the real functions, answers -1 — a std::thread started there aborts "Unknown error -1", and a std::ifstream opened there fails; OpenImageIO's iconvert and conda-forge's ROOT abort under the shim for this reason, and --observe supervised, which loads no shim, judged iconvert. Qt's save through O_TMPFILE (Kvantum, Hydrogen) is refused unresolvable_path.

Changed

  • Restore carries permission bits, so a target that checks a mode under the state is judged (#678, ADR 0109, amending ADR 0072 for permissions; src/engine/state_fs.zig, src/engine/snapshot.zig, src/engine/read.zig, src/posix.zig, src/recovery.zig, src/main.zig, src/contract.zig, src/cli.zig, src/evidence.zig, docs/cli.md, docs/report-schema.md, docs/evidence.md, docs/target-classes.md, spike/toys/toy.c, spike/acceptance.sh, spike/dogfood/2026-10-10-restore-modes-678/, spike/dogfood/RUNS.md). A snapshot used to hold names, kinds and bytes, and every restore rebuilt files 0644 and directories 0755, so preflight --twice's second run, every crash world and a recovery's crash state started from other bits than the recording: upx 5.2.1 (an executable input) and argocd 3.5.3 (a 0600 config) were refused recording_run_failed. A snapshot now records each file's and directory's read, write and execute bits, and restore puts them back through the descriptor it wrote or opened, a directory with its owner's bits added; set-id and sticky bits, owners and timestamps are still not carried, and the judgement still does not compare modes. The checker's falsification probe is created anew with the recorded bits, so a restored 0444 file — which a user who is not root could not write over — does not stop it, and the recovery's probe carries them, so a recovery checker that reads only a mode is not trusted. Re-measured: preflight --twice accepts upx, argocd and the 2026-10-03 probe, and upx and argocd reach FAIL under --observe supervised. The metadata line, second_run_diverged, kill_not_landed, not_repeating, --help on --twice and --recovery, the recovery account and the evidence bundle's recovery caveat say what restore carries now.

  • Power loss is decided: not the next crash model, for now (#699, ADR 0110; PRD.md, DESIGN.md §21). PRD had held it "first in line for consideration after 1.0", and nothing since had accepted or declined it. The two known reasons are that its world needs a filesystem model the engine does not have, and that its answer depends on the filesystem; the two not yet known — whether a power-loss model separates targets (the committed record says how thirteen PASSing targets sync, and the count depends on a world model not yet chosen) and whether upstreams act on a missing-fsync report (theskumar/python-dotenv#713 is open) — are the conditions that would reopen it. The process crash stays the only model, the not tested line keeps naming power loss and torn writes, and the candidate rule is recorded in the ADR for whoever reopens it.

  • Three refusals of a static target whose dynamic child carried the shim name --observe supervised, in the shapes measured to be crossed (#685, ADR 0108; src/boundary.zig, src/contract.zig, src/main.zig, src/mcp.zig, docs/report-schema.md, docs/cli.md, docs/mcp.md, docs/target-classes.md, ADRs 0069, 0076, 0090 and 0097, spike/toys/toy.c, spike/acceptance.sh, spike/dogfood/2026-10-10-static-parent-685/, spike/dogfood/RUNS.md). The default gate lets this shape through — the child's shim announces itself — and the static parent's own calls are recorded by nobody. Three refusals were measured to come out of it: oracle_missed_operation (lefthook 1.13.6, aliyun-cli 3.5.1), whose step sent the reader to --observe syscalls, which refused again with the class wall; the recording run's unresolvable_path where the child closed the shim's trace (roswell 26.02.116), which named the class wall; and child_touched_state_dir on its shimmed-writer arm (a toy). Under either shim mode, in a build that can supervise and outside a replay, those three now take a new step, observe_supervised_static_parent, which says the image read before the run is one no shim can be loaded into and names --observe supervised. Each site passes the shape it was measured on — the first process to announce a shim is not the one strace saw start; the refusing record is trace-closed-by-target; the shimmed arm with that pair of processes — and every other refusal keeps its step, because the same reason is also raised where that mode refuses the same way. At oracle_missed_operation, a static image that execs a dynamic one in its own process keeps --observe syscalls. No unknown_reason, report field or contract_version changes.

  • sideeye_explore_config is confined to SIDEEYE_MCP_STATE_ROOT, as replay and sideeye_preflight {twice} are (#765, ADR 0106; src/mcp.zig, src/cli.zig, src/main.zig, spike/acceptance.sh, spike/mcp-acceptance.sh, docs/mcp.md, docs/cli.md, docs/ci-quickstart.md, docs/contract-freeze.md, SECURITY.md, skills/sideeye-scout/SKILL.md, ADR 0022). An exploration empties and rebuilds the config's state before every world, and the server passed no range for it: the config's path was confined to the root, the state that path names was not. ADR 0022 left it so on the reading that a config is what the operator vets; an agent that writes a config, checks it with sideeye_preflight and explores it is not that operator. The server now passes --state-under SIDEEYE_MCP_STATE_ROOT (default: the server root), and explore takes the flag — it refused it by name until now. An exploration through the server whose config's state is outside SIDEEYE_MCP_STATE_ROOT — the server root when it is unset — is refused before setup runs, by a refusal that says to move the state inside the range or widen it. The range is one directory, as it is for replay: an operator who set it to /tmp so that CLI-made cases replay now has explorations confined to /tmp too, and a config whose state sits under the root but not under /tmp is refused where it used to run; point the variable at a directory holding both, or move the state. sideeye_preflight without twice does not check the range, and its next line now says the exploration will. The refusal's way out is worded per command: move the state inside the range, or widen it (for a replay, as before, replay it from the command line first), naming SIDEEYE_MCP_STATE_ROOT and --state-under both. The command line is unchanged unless the flag is given.

  • A checker that rejects the state the define starts from is refused checker_rejects_initial_state before any world, instead of reading as the target's FAIL at crash point 1 (#756, ADR 0107; src/main.zig, src/contract.zig, src/report.zig, src/containment.zig, spike/acceptance.sh, docs/report-schema.md, docs/contract-freeze.md, docs/checker-cookbook.md, docs/cli.md, skills/sideeye-scout/SKILL.md, ADR 0093). The falsification showed the checker a corrupted state and required it to fail; nothing showed it the state the define starts from. A checker that refused that state failed in the world killed before the first operation — which holds exactly that state — and the report said earliest crash point 1, after (start)(), about a target that had done nothing yet: xmake's first checker accepted only the value the operation writes, and FAILed 2 of 4 where the fixed checker FAILs 1 of 4. After the falsification, the engine now runs the checker once on the starting state, as it restores it before every world; a checker that fails there refuses the run, its output marked start: , with fix_define as the step (or the cwd observation, ADR 0093). unknown_reason gains a member (35 → 36), by owner ruling: checker_not_falsified would have sent a reader the opposite way. Such a run now exits 2 instead of 1, a case saved at its crash point 1 is refused on replay, and a checker that rejects every state is refused here rather than at the baseline after every world. preflight does not run the checker and is unchanged.

  • The README is held to 100 lines and 800 words, and the bound no longer moves with the page (README.md, spike/check-readme-shape.sh, docs/mcp.md, spike/check-shim-footprint.sh, spike/acceptance.sh, comments in src/mcp.zig and src/main.zig). The page had reached 135 lines and 1,569 words, each issue adding a sentence and check-readme-shape.sh raising its byte bound to fit, so the bound recorded the growth and stopped none of it. The page now carries what CI and the engine hold it to — the demo, the three commands, the define, the checker, the limits the next_step strings in src/contract.zig send a reader to, the skills' install line — at 98 lines and 799 words: omamori's 80 lines and 746 words plus the demo block, and 40 words above the 760 lossless-compaction's tests hold, which is what those limits cost once the first cut to 760 had dropped them; every sentence it dropped was already on the page its reason lives on (DESIGN.md's Known constraints, docs/cli.md, docs/report-schema.md), except the agent skills' description, which is now a section of docs/mcp.md. The check counts words as well as lines, so longer lines are not a way around it. The two scripts that quoted the README's footprint limit quote DESIGN.md now, where the limit stays, and docs/mcp.md sends the shim-search question to docs/cli.md, where the install note went.

  • sideeye demo needs no C compiler: the planted-bug tool is built with the engine and carried inside it (#715, ADR 0101; build.zig, build.zig.zon, src/main.zig, src/files.zig, src/cli.zig, src/contract.zig, src/posix.zig, src/refuse.zig, spike/acceptance.sh, spike/check-readme-demo.py, .github/workflows/ci.yml, .github/workflows/release.yml, .gitattributes, README.md, docs/cli.md). The demo used to compile spike/toys/toy.c on the visitor's machine with the first of cc, gcc and clang that worked, and refused with a SETUP ERROR when none did — so the sixty-second proof failed on the machine a prebuilt binary is most likely to be tried on. build.zig now compiles the same file for the engine's own target (Debug at -O0 whatever the engine's mode, through LLVM on every target, dynamically linked, C sanitizer off) and embeds the result; the demo writes it into its scratch directory as a file it must create, owner-only from the start (O_EXCL, O_NOFOLLOW, mode 0700), and runs it. Its first line now reads demo wrote the planted-bug tool into <dir>. The README no longer says the demo "writes nothing permanent": it leaves that directory under $TMPDIR on purpose, since the case it saves names files in it, and docs/cli.md says so. Acceptance check 5 drops the two legs that pinned the compiler ladder and the "needs a C compiler" refusal, and runs the demo with PATH=/nonexistent instead — on Linux there, on macOS in CI, and on all three release targets, built ReleaseSafe, in the release workflow's smoke test. Measured before the pull request: the ReleaseSafe builds for both glibc-2.28 Linux targets reach the README's FAIL 1 of 6 in debian:bookworm-slim, which carries no compiler, as an unprivileged user (x86_64 under emulation). The binary grows by the stripped toy, about 90 KB on macOS arm64 and 44 KB on aarch64 Linux.

  • The CI quickstart pins v1.10.0 (docs/ci-quickstart.md, docs/ci-quickstart/release/install-sideeye.sh, .github/workflows/quickstart-release.yml). The release lane installs the published v1.10.0 asset instead of v1.9.0's. The pin moves after the release, as on v1.9.0, because the lane downloads the asset and the asset exists only once the release is published. Nothing else in the lane changes.

  • The work directory is refused unless it is yours (#692, ADR 0099). --work defaults to /tmp/sideeye-work and the MCP server's SIDEEYE_MCP_WORK to /tmp/sideeye-mcp, fixed names that anyone on the host can make first; the engine adopted whatever stood there and followed a link at the name, and the server did the same. The cases saved there are commands a replay runs, and the server reads each call's verdict back from there, so whoever made the name first chose where both went. Both now refuse a work directory they did not just create, before setup runs and before anything is written under it (a --json report pointed there excepted, as for every refusal), when its last component is a symlink (whoever owns it and wherever it leads, and dir/ or dir/. spellings included), when it is not a directory, when its name ends in .., when it is owned by anyone but the effective user — root included, unless root is running — or when its kind or owner cannot be read. The sentence names the directory, the reason, the uid where that is the reason, and the ways past it: a name that does not exist yet, which Sideeye creates, or a directory of your own. A name that cannot be resolved at all is still refused by the error that says why. This is a 1.x behaviour change. What ran before and now refuses: a link of the runner's own at the name, to a directory elsewhere; another user on the host made /tmp/sideeye-work first; sudo sideeye pointed at a directory the user made; a root container handed a bind mount the host user made; a Kubernetes emptyDir owned by root. It is a mitigation, not a boundary, as the shim search's is (ADR 0044): the name is inspected once, and whoever can write its parent can swap it afterwards. ADR 0010's precondition that the work directory is the operator's own is now checked rather than assumed. The review that filed #692 said cases are written 0o600; they are written 0o644, and nothing here depends on either.

  • A case saved under --observe syscalls or --observe supervised records the mode, and replays under it with no --observe given (#691, ADR 0100; src/case.zig, docs/contract-freeze.md surface 4). A crash point is a number in one mode's count, and the case did not say which mode counted it: replayed without the flag, a syscalls case refused case_no_longer_applies and a static target's supervised case no_shim_marker, and sideeye_replay_case, which takes no mode, could not replay a supervised case at all. Such a case is now case_version 6, with a top-level observe; sideeye replay <case> and sideeye_replay_case both replay it under that mode, the report's replay line still names the mode and a matching --observe is accepted, and an --observe naming another mode is refused by name. A case counted under the default is written byte for byte as before. What this gives up: v1.10 could replay a syscalls case with the flag, and a version-6 case is refused by every earlier binary as a file it cannot parse — a refusal, not a verdict (the owner's choice over keeping the mode in a separate file, which a copied case would lose). Cases saved before this change record nothing and replay as they always did. sideeye_explore_config's observe description no longer says a supervised case cannot be replayed through sideeye_replay_case; the schema is unchanged.

  • The comments in src/*.zig say only what an edit would break (#807; #812, #813, #836, #837, #838, #840, #839). 15,642 whole-line comments became 5,066 — 37% of the non-blank lines to 16% — keeping the warnings the next edit needs (an order that breaks when moved, two values that misjudge when they drift apart, a string an outside check reads, a report wording whose obvious alternative would be false, a test's control) and dropping what the code, an ADR or a test already says. No code line changed: spike/check-comment-only.sh holds each such change to deleting comment lines and the blank lines zig fmt collapses after them.

Fixed

  • The issue forms' dropdowns start at "Not answered" (#719; .github/ISSUE_TEMPLATE/unknown.yml, .github/ISSUE_TEMPLATE/verdict.yml). Opened on main after #783 merged, every dropdown showed its first option already chosen — "No" to "Did a language model write any of this report?", "Homebrew" for the install, and on the verdict form "Sideeye found a real bug" — so a reporter who skipped one would have filed an answer they never gave. Each now offers "Not answered" first, which reads as what it is when left alone. The report field's label drops its backticks, which a label shows as written.

  • docs/found.md no longer counts timewarrior as fixed upstream (#720; spike/outcome-funnel.tsv, docs/found.md, docs/outcome-funnel.md). Its funnel row read revalidated, with the note "Fixed upstream". GothenburgBitFactory/timewarrior#778 has had no reply, and timewarrior no commit for it, since it was filed on 2026-08-12; the build that passes 25/25 carries this project's own patch, spike/timew-undo-ordering.patch, which the journal and docs/kill-criteria-review.md already called that. The row is filed, awaiting, read 2026-10-10, and the page counts 10 reports fixed upstream rather than 11. The other ten were read against their trackers the same day; each has a fix its project merged or committed, DwarFS's on a branch that has reached neither its main nor a release.

  • A call made from another shared library's constructor, before the shim's own, reaches the real function: std::thread and std::ifstream no longer fail there, and a write such a call makes into the state refuses the run (#753, ADR 0115; shim/src/common.zig, src/contract.zig, src/boundary.zig, spike/acceptance.sh, docs/report-schema.md, DESIGN.md). On Linux the shim reaches each real function through a table filled in its own constructor, and the loader can run another library's constructor first; a call interposed from it answered -1. pthread_create's callers expect an error number, so a C++ thread started there aborted with "Unknown error -1", and ROOT's read of /dev/urandom failed — OpenImageIO's iconvert and ROOT's rootrm ended recording_run_failed (SIGABRT) where by hand they exit 0. Every wrapper now looks the function up when the table is still empty, and a function that is genuinely missing answers an error number or sets errno, never a bare -1. Those calls are not recorded, so one that reaches the judged state — by path, or through a descriptor pointing there, an inherited one included — leaves a mark, and the shim's constructor writes one before-constructor record for it once the trace is open (an exec before that constructor hands the mark to the next image): the run is refused unresolvable_path, naming the path, rather than judged with a write before crash point 1 that a later recorded write to the same path would have appeared to account for. --observe syscalls installs its filter on every thread (SECCOMP_FILTER_FLAG_TSYNC): such a constructor can now start a thread, and without the flag that thread would have escaped both the filter and the wrappers. The mark crosses an exec made through execve, execv or execvp; the execl family escapes it, as it escapes the operation count's carry (ADR 0018). OpenImageIO's iconvert (Debian trixie) is now FAIL 1/7 under the default mode, where it was recording_run_failed.