Skip to content

Host cache: read by content, written by main's cold nightly alone, sealed and bounded, with its workflow rules in the review prompt - #669

Merged
Japabu merged 28 commits into
mainfrom
wt/toyos-cicache
Oct 2, 2026
Merged

Japabu merged 28 commits into
mainfrom
wt/toyos-cicache

Conversation

@Japabu

@Japabu Japabu commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

The host job's cache, as it lands:

  • an entry is read by content, never by mtime (src/cicache.rs);
  • nightly's host is its one writer: cargo run -- --ci seal builds from a cold tree and seals it, and the save, on main alone, follows only a green run;
  • the seal refuses a tree whose cached paths hold more than 8,000,000,000 B, so no save stores one; a pull request's run, warm or cold, never seals;
  • the steps of a job that carries the cache build with no incremental state and line tables alone;
  • what the workflows must keep for it is the Caches bullet of .claude/agents/reviewer.md, read off the diff; no gate reads the workflows for it;
  • toyos-abi's clippy shape lints in a target dir of its own.

Before and after (host on macos-latest)

before: PR median, 10 runs (CI profile) run 1: cold, writes run 2: warm, one leaf changed run 3: cold, no incremental, writes run 4: warm, toyos-build + main's #668 changed
entry restored 4.25 GB in 134 s none 3.25 GB in 109 s none 1.17 GB in 36 s
Compiling / Checking lines 152 / 155 595 / 289 20 / 1 595 / 289 21 / 2
compile (sum of cargo's Finished) 472 s 553 s 23.0 s 734 s 163 s
cargo run … --ci host 817 s 921 s 377 s 1150 s 433 s
job 944 s 1004 s 504 s 1201 s 481 s
entry saved — 3,250,736,567 B — 1,171,199,636 B —

Runs: 1, 2, 3, 4.

  • Run 2 is the gate. It read run 1's entry: "2211 of 2212 sources unchanged, 1 changed". It compiled toyos-dhcp and nothing else, beyond the driver's 19 path crates, which every run rebuilds incrementally (13.2 s). 455780e is that one-line change; e57e0be reverts it.
  • Run 4 is the common case: a pull request whose toyos-build differs from the entry. It compiled the driver, the lib test and the checks, plus 2 clippy checks of toyos-build.
  • Runs 1–4 read entries a pull request's own save wrote, which this PR does not keep, so they measured another reader. The one that lands differs in four ways: it hashes with SHA-256 in-process, it dates a changed package whole, it dates every package with a build script or a proc macro whole on every read, and it checks the image.

Decisions

Freshness by content, not by mtime. Every path crate recompiled in every run. Checkout dates every source at checkout time, and cargo's path freshness is source mtime <= build reference.

  • -Z checksum-freshness is refused. It is still unstable: nightly-2026-09-21 lists it under -Z, and stable 1.98.1 rejects -Z. Turning it on takes a nightly toolchain or RUSTC_BOOTSTRAP=1 on stable. Rustc needs it too, because cargo passes it -Zchecksum-hash-algorithm. Either way the gate would stop compiling what a user's stable compiles: RUSTC_BOOTSTRAP lifts the feature gate for every crate and flips dependencies' nightly probes.
  • The fork builds no cargo of its own; toolchain.rs lends it the machine's. Its rustc is not stable either.
  • An mtime taken from history (a commit's time) would call a PR file fresh whenever its commit predates the entry's build, whatever its bytes.

What src/cicache.rs does instead:

  • An entry carries target/ci-sources: the commit, the runner, and the SHA-256 of every tracked file.
    • The hash is sha2, already a dependency, run over sysroot::tracked_files. The gitlink rust/ is skipped as a directory.
    • Bytes, not identity::of: the toyos-abi shape's undocumented_unsafe_blocks reads // SAFETY: comments, and line numbers reach panic locations.
  • The writer dates every file under every target 2001-09-09.
  • A reader dates each file whose hash matches the same, and every other file now. Cargo calls a source stale only when it is strictly newer than the build, so a match is fresh, and a changed or added file is newer than everything in the entry.
  • A package with any file changed, added or removed has every file dated now. Cargo knows only the files a build read. A new src/x/mod.rs beside src/x.rs is E0761 to a cold build, yet it is in no dep-info, so dating that file alone left the crate fresh. Dating its package rebuilds the crate.
    • The cost is a rebuild of a package when one of its files that no build reads changes. For the root package, toyos-build, that is the 59.2 s lib test plus 54.8 s of checks measured in run 4. 79 of main's last 80 landings touched a root-package file, and 60 of them touched src/ or tests/. An entry is the nightly's and a pull request is based on a later main, so toyos-build is almost always rebuilt anyway.
  • Every package with a build script or a proc macro has every file dated now, on every read. What code run at build time reads, cargo knows only if that code says so: a build script's rerun-if-* lines, and nothing at all for a proc macro. Dated whole, the package's build script reruns and its proc macro is rebuilt, and so is every crate that expands it, whatever either reads.
    • A package is one when its manifest's [package], or [project], which cargo reads as the same table, has build.rs beside it and no build = false, or a build key naming a script; or when its [lib] says proc-macro or proc_macro, or has a proc-macro crate type. All 99 tracked manifests parse; the three such packages are userland/calc, userland/doom and userland/snake.
    • The host job builds calc alone of them. Its font sits in the root package, so it already reran on nearly every warm read.
  • A reader whose clock does not read after 2001-09-09 is refused: a file dated now would not be newer than the entry.

The runner image is in the manifest, not the key. No workflow expression sees ImageOS or ImageVersion, because the env context holds only what a workflow sets. rclone's build_publish_docker_image.yml says so ("ImageOS is only available to processes"), as does apache/httpd's linux.yml ("the env context, which does not see the runner's own $ImageOS"); ${{ env.ImageOS }} would key on the empty string.

  • The key carries runner.os and runner.arch, so a runner of another OS or arch never restores an entry. On ubuntu-24.04 the key's prefix is host-sealed-Linux-X64-.
  • runner() names the runner by RUNNER_OS RUNNER_ARCH ImageOS ImageVersion and refuses an unset one. The manifest records that name, and the seal prints it. A reader on another runner deletes every restored target and runs cold, because the image's linker and C compiler made the entry's units and cargo's fingerprint names neither. An image move therefore costs a cold run on every pull request until the next nightly writes on the new image.

Only the writer seals. nightly.yml's host runs cargo run -- --ci seal; ci.yml's host runs cargo run -- --ci host.

  • ci::seal calls cicache::cold, then host, then cicache::seal, which takes the Cold only cicache::cold returns. No flag decides whether a run seals: host holds no seal.
  • cicache::cold refuses a driver built outside target/ci-driver, then any target or manifest but the driver's own, and dates every source as built. So an entry is one cold build.
  • host reads the cache in every job that carries it. In the seal job that read follows the cold start, finds nothing, and says the run is cold.
  • A pull request's run needs no part of the seal. The seal's source check, its dating and its manifest exist so that the manifest describes the tree the save stores, and a pull request's tree is never saved.
  • A reader deletes the manifest it reads: a warm tree holds units none of its steps rebuilt, which no manifest describes. Targets restored without one are refused, the root's target/ included.
  • A step that writes a tracked source is refused at the seal, even if it writes the same bytes back.

A job carries the cache when its workflow builds the driver in target/ci-driver. Cargo builds the driver before any of this code runs, so its path crates are compiled again every run. In the steps' target, that rebuild would make every dependent stale.

  • The directory is the job's CARGO_TARGET_DIR in ci.yml's and nightly.yml's host, so each of their steps is one cargo run -- --ci <job> and nothing else. Cargo hands that variable to the program it runs. ci::carry, which host calls for a job that carries the cache, takes it out of the environment before the first step, so every step builds in its own target.
  • cicache::carried asks where the driver runs from. A job whose driver runs from anywhere else runs the host suite as a developer's tree does. That is nightly's portability-macos, which runs cargo run -- --ci host after --build-only: its driver is in target/debug, nothing restores its tree, and read would refuse the targets --build-only left, which no manifest describes.
  • A reader without that CARGO_TARGET_DIR would run its restored tree judged by cargo's mtimes alone, with no image check, and nothing at run time would say so. The Caches bullet holds both jobs to it.

What an entry holds. ci::carry sets what the steps of a job that carries the cache build with; the driver keeps its own incremental state and debuginfo.

  • No incremental state (CARGO_INCREMENTAL=0).
    • The macOS entry fell from 56,592 files / 9,553 MiB to 14,262 files / 3,589 MiB; compressed, from 3.25 GB to 1.17 GB. Restore fell from 109 s to 36 s.
    • Run 4 recompiled the changed toyos-build lib test in 59.2 s and its checks in 54.8 s. The before median was 50.6 s and 47.6 s with incremental state present. On a runner, then, that state did not buy a changed crate's compile back.
  • Line tables alone (CARGO_PROFILE_DEV_DEBUG=line-tables-only). db4654f's run sealed 7684 MiB in 11823 files, against 3575 MiB in 15182 files on macOS (59cc178's run 36855422648).
    • A Linux link copies the DWARF of every object it takes into the binary, and macOS leaves it in the objects. So each of the steps' 231 test binaries carried its dependencies' DWARF again.
    • Measured on the dev host: toyos-build's lib test, cross-linked for x86_64-unknown-linux-gnu by rust-lld with no C runtime, so that what it keeps is almost all DWARF, is 124,452,112 B with full debuginfo, 35,983,096 B with line tables alone, and 848,744 B with none.
    • Line tables, because a step's log reads them: a panic while panicking prints a backtrace, as the doorbell-kick-relaxed control does in run 36867673999. Nothing reads the rest: no step sets RUST_BACKTRACE, and no host test reads a binary's DWARF.
    • e0ced58's Linux run sealed 5369 MiB in 11827 files, against db4654f's 7684 MiB in 11823 files.

The seal bounds what the save would store. cicache::seal sums the lengths of the files under PATHS, the list the save archives, with ~ taken from HOME, and refuses above LIMIT, 8,000,000,000 B, before it dates a target or writes the manifest.

  • Only the writer seals, so only the writer is bounded. A refused tree carries no manifest, and the save, which follows only a green step, stores nothing.
  • So a landing that takes the cold tree past LIMIT is first refused by the next nightly's seal. Until an entry is sealed again, a pull request restores the last sealed one, or runs cold once the runner image has moved: issues/build/the-host-caches-limit-reaches-the-10-gb-only-through-one-measured-ratio.md, whose exit is a gate.
  • The Caches bullet holds both steps' path: lists to PATHS: actions/cache computes an entry's version from the list, so a restore whose list differs finds nothing, and the bound counts PATHS alone.
  • The limit. Eviction is by last access past the repository's 10 GB. On a night whose guest jobs restore after host saves, the repository holds two host entries, yesterday's and tonight's, beside yesterday's guest entry; then tonight's host entry beside two guest entries. Both must fit: 2H + G ≤ 10 GB and H + 2G ≤ 10 GB. G is 3,281,375,938 B, the last guest entry.
  • The ratio and the arithmetic: e0ced58's run 36878222090 sealed 5369 MiB of targets (at least 5,629,804,544 B) and archived the cache's paths in 1,403,326,566 B, a ratio of 4.01, at which the limit stores at most 1,994,138,951 B, so 2H + G = 7,269,653,840 B and H + 2G = 8,556,890,827 B; the ratio may fall to 2.38 before 2H + G reaches 10 GB, and the lowest this PR measured is 3.08 (macOS run 1, 9553 MiB saved as 3,250,736,567 B). The bound counts the ~/.cargo paths as well as the targets, so under it that run's ratio is at least 4.01. No gate reads a stored size: the limit's issue above.

One writer. nightly.yml's host restores nothing and runs seal, so its entry is one cold build, never an accumulation. seal's last step is the seal, so the job is green only with a sealed tree within the bound.

  • The save's guard, github.ref == 'refs/heads/main', is its only if: and names no status function, so GitHub prefixes success() && and the save follows only a green run. On a branch, a workflow_dispatch builds cold and seals, bounded, and saves nothing.
  • On main's own nightlies a cold night took less runner time than the warm night after it. Both ran host on macos-latest at the same 54 steps: cold on 09-29 in 593 s, and warm on 09-30 in 695 s (run 36550208853, run 36696295750). Restoring took 1 s against 76 s, cargo run 541 s against 502 s, and saving 38 s against 104 s.
  • Main's nightly restores its last entry, builds warm and saves, and cargo deletes no superseded unit, so its entry grows night over night. Its host logs, on macos-26-arm64: on 09-29 it found no entry and saved 2,779,017,614 B; on 09-30 it restored that and saved 4,456,914,229 B; on 10-01 it restored that and saved 5,272,701,372 B. On 09-28 it had restored 6,911,542,445 B and saved 7,910,605,002 B, and on 09-29 that entry was gone.
  • The seal deletes nothing: it dates every file under every target and writes the manifest. The writer starts from no target but the driver's own, since cold refuses any other, so what the seal keeps is what that night's build produced.
  • No pruning: eviction is by last access, and the bound above is what keeps both entries.

The workflow rules are review sentences, not a gate. A gate that re-reads GitHub's YAML lags GitHub's reader: actions/runner splits a uses: path on / and \ and drops empty segments, and YamlDotNet, which it reads with, breaks a line at U+0085, U+2028 and U+2029, where yaml-rust2 does not.

  • Caches, in .claude/agents/reviewer.md: each cache has one writer, a nightly.yml job, and no workflow uses the combined actions/cache, which saves too; the host cache's is nightly's host and its one reader ci.yml's host, on one runs-on, both caching PATHS with DRIVER as their CARGO_TARGET_DIR, the one variable an env: gives either; the reader's restore-keys is the writer's key up to its run id; those jobs run checkout, their cache step and cargo run -- --ci <job> alone, seal in the writer and host in the reader, the reader restoring before that step and the writer saving after it; the save's guard is their only step-level if:, ci.yml's host skips only a draft, and nightly's host has no if: of its own, since a skipped job is a green check; no continue-on-error, shell:, defaults: or cargo runner reaches them; nightly.yml's on: is one daily schedule and workflow_dispatch.
    • The env: clause: runner() reads ImageOS and ImageVersion from the environment, so one set in both jobs would outlive an image move and pass the image check.
    • The <job> clause: a writer on --ci host saves no manifest, and every reader then refuses the targets it restored.
    • The keys clause: a key's head renamed on one side leaves every pull request on the old name's last entry, or on none, behind green checks.
    • The order clause: a save above --ci seal stores no target, the nightly stays green, and every pull request reads "none restored".
  • No tracked .cargo/config.toml sets a runner: the three are bootloader/'s, kernel/'s and userland/'s, and git grep runner finds nothing in them.
  • Gone: ci::tests::each_cache_has_one_writer. Its rule is the prompt's: Growth's sentence on when a test is cut gains the case, "when this prompt takes its rule", and Caches' first sentence holds what the test asserted, one actions/cache/save per cache, each in nightly.yml, and no combined actions/cache.
  • Three tests still read the workflows as text, each passed by a patch: issues/build/the-workflow-gates-that-read-workflows-as-text-become-reviewer-instructions-or-go.md, owned by issues/build/the-tooling-is-a-review-prompt-and-three-workflows.md.

toyos-abi's clippy shape gets its own target dir. It lints the unit the workspace shape lints, under other lints. So each shape checked it again every run, and the workspace shape then checked its 12 dependents again. Measured here: 13 crates re-checked with the shared target, 0 with its own. In CI, run 2 did 1 Checking, against 155 before.

Stale-build cases (covered means a warm run rebuilds or refuses)

  • A changed tracked file, whatever its mtime (1970 included): covered, dated now, with its whole package.
  • An added or removed tracked file: covered, because its package is dated whole.
  • A file rustc probed for but no dep-info names (src/x/mod.rs beside src/x.rs): covered when it is in the package that read it, or in the package of a #[path] file that read it.
  • A path build script or proc macro, whatever it reads and wherever, declared or not: covered, because its package is dated whole on every read.
  • A registry crate's build script or proc macro: judged by cargo alone, because cargo never dates a registry source.
  • A registry proc macro that reads a file it does not name, outside the package of the path crate that expands it: not covered. It runs inside every compile of that crate, and a warm run compiles the crate only when its own package changed. Of the proc macros in the lockfiles of the five workspaces the host job builds, wayland-scanner alone reads a file it does not name, and no tracked source names it. Filed as issues/build/a-warm-host-run-keeps-what-a-registry-proc-macro-expanded-from-a-file-it-never-named.md, with a test as its exit.
  • A file only a flag names (a linker script in -Clink-arg=-T… from a .cargo/config.toml or RUSTFLAGS), outside the package that links with it: not covered. Cargo compares the flag, never the file, in its own incremental build as well. No flag the host job passes names a file. Filed as issues/build/a-warm-host-run-never-relinks-for-a-file-only-a-flag-names.md, with a test as its exit.
  • The runner image or arch: covered (arch by the key, image by the manifest).
  • A reader's clock earlier than the entry: refused.
  • Cargo's other inputs (rustc -vV, profile, features, RUSTFLAGS, env! values, rerun-if-env-changed values, the unit graph, the package path): covered by cargo itself.

Gates

  • cargo run -- --ci host on 79bd4ec, the head with main's Worktrees and the primary's sync are git commands in the role prompts, a sysroot placement sweeps its store, and the prompts take the owner's decisions #678 (de5f63c) merged, on a developer's tree (macOS arm64), which carries no cache: "Host: 59 step(s), all green", EXIT=0. Its cargo test --lib: 373 passed, 0 failed, 8 ignored. Its clippy step, warnings denied: "clean". Its app steps: 11 apps each, built for aarch64-apple-darwin and checked for x86_64-unknown-linux-gnu and x86_64-pc-windows-msvc.

  • cargo run -- --build-only on 79bd4ec: "Build finished.", EXIT=0.

  • 79bd4ec is 883adf1 with .claude/agents/reviewer.md changed and nothing else (git diff --stat 883adf16a 79bd4ec49: 1 file, +15 −13), so what is measured below on 883adf1's src/ is measured on the head's.

  • No guest test is reached: the diff against main changes --ci host and --ci seal in src/ci.rs, src/cicache.rs, which only they call, one clippy shape's arguments, a doc comment in src/identity.rs, two workflows, a prompt and issues.

  • The text gates' issue, measured on 038da72: each patch applied with git apply --check then git apply, built (cargo test --lib --no-run, EXIT=0 each), the three tests run with --exact, each selecting one test, and reverted; the tree matched its prior state after each. Patches: the PR comment.

    patch workflows_run_against_main_on_hosted_runners the_required_check_is_a_job_on_every_pull_request nothing_that_runs_names_a_target_directory_a_member_does_not_have
    publish.yml's runs-on: label on the next line, - self-hosted 0 0 0
    publish.yml gains pull_request: {branches: [dev]} 0 0 0
    ci.yml's host gets if: false 0 0 0
    publish.yml gains run: "ls userland/sshd/targe\x74" 0 0 0
    publish.yml gains run: ls userland/sshd/target (positive control) 0 0 101: "publish.yml: names userland/sshd/target"
  • CI on f7e0bd4, run 36906786717, host on ubuntu-24.04 green and cold: "Cache not found for input keys: host-sealed-Linux-X64-36906786717, host-sealed-Linux-X64-" (log line 146), "[ci] the cache entry, read by content: none restored: the run is cold" (309), "[ci] Host: 60 step(s), all green" (8496), and no seal step: a pull request's run does not seal. At 883adf1 host holds no seal at all.

  • CI on 7923051, run 36897889906, host on ubuntu-24.04 green and cold, "Host: 61 step(s), all green": this PR's measure of a Linux cold tree against the bound, with seal_at the same code as at 883adf1 but for the call that reads HEAD: "15976 files, 6720362549 B of the 8000000000 B an entry may hold; sealed: 2230 sources, built on Linux X64 ubuntu24 20260927.320.1, every target dated as built".

  • CI on f9d535d, run 36890754662, host on ubuntu-24.04 green and cold. The job took 833 s.

  • CI on b4dfda5, run 36881892289, host on ubuntu-24.04 green and cold, "Host: 58 step(s), all green". The doorbell-kick-relaxed control's backtrace names file and line in the workspace's own frames, such as toyos-sched/loom/src/../../src/mailbox.rs:178:9.

  • CI on e0ced58, run 36878222090, host on ubuntu-24.04 green and cold, is the run the ratio rests on. Its seal: "2229 sources, built on Linux X64 ubuntu24 20260927.320.1; 11827 files, 5369 MiB, dated as built". Its own archive of the cache's paths: "1403326566 B".

Growth, git diff --shortstat origin/main...HEAD: 13 files, +1040 −57. Production Rust +448 −7 (src/cicache.rs 390, src/ci.rs +50 −2); test Rust +416 −29; workflows +21 −18; reviewer.md +16 −3; issues +139.

High-risk checks

  • Oracle: cargo and the program it builds.

    • an_entry_serves_exactly_the_sources_it_was_built_from builds a 5-crate workspace cold and seals it. It then reads the entry from a checkout where one leaf changed and its file was dated 1970, one package lost a file that only its build script counts, and one lost a file nothing reads. The binary must print what a cold build prints, and cargo must report the untouched crate fresh and the other four rebuilt.
    • a_file_cargo_was_never_told_about_rebuilds_its_package holds a warm build to rustc's cold verdict: E0761.
    • code_run_at_build_time_runs_again_on_every_read: a build script and a proc macro, in packages that do not change, read another package's word.txt and never say so. After word.txt changes, the warm build must print two two, as a cold build does.
    • a_step_of_a_job_that_carries_the_cache_builds_in_its_own_target: ci::carry runs in a driver of its own, spawned as the job spawns one, with CARGO_TARGET_DIR=target/ci-driver and without CARGO_INCREMENTAL or CARGO_PROFILE_DEV_DEBUG. Then cargo metadata must name the one-crate fixture's own target, and cargo build -v's rustc line must carry no -C incremental and -C debuginfo=line-tables-only.
  • Negative controls, measured on f7e0bd4. One script applied each patch with git apply --check then git apply, built it (cargo test --lib --no-run, EXIT=0 every time), ran the named test with --exact, and reverted it with git apply -R; git status --porcelain was empty after each. Since f7e0bd4, src/cicache.rs made PATHS private, moved the seal's driver check into cold, and reads HEAD through main's sysroot::git_out, and ci::carry did not change: each of the 69 lines these patches remove is present verbatim at 883adf1 (every one found by grep -xF in the file its patch names: 69 lines, 0 missing, EXIT=0). Patches: the PR comment.

  • The merge's one changed line, measured on 883adf1. The seal reads HEAD through sysroot::git_out and trims it. mH leaves it untrimmed: git apply --check 0, cargo test --lib --no-run EXIT=0, cargo test --lib -- cicache:: EXIT=101 with 5 of 10 red, each on target/ci-sources holds "macOS ARM64 macos15 20260928.1", the runner read where a source line belongs; git apply -R 0 and git status --porcelain empty. Patch: the PR comment. A git that cannot answer rev-parse HEAD panics the seal, as it does main's ci::publish.

    mutation test EXIT what it printed
    mSt: cicache::cold takes a tree with restored targets targets_restored_without_a_manifest_are_refused 101 [Some("…/kernel/target restored without target/ci-sources, …"), None]: the reader refused, the writer did not
    mN: the bound removed a_seal_refuses_what_its_save_would_store_above_the_bound 101 one byte above the bound: "2 files, 8000000001 B of the 8000000000 B an entry may hold; sealed: …"
    mX: one byte above the bound passes same 101 the same
    mO: a tree at the bound is refused same 101 at the bound: "8000000000 B in 1 files under the cache's paths, above the 8000000000 B an entry may hold: …"
    mM: the largest file is counted, not the sum same 101 one byte above the bound: "2 files, 8000000000 B of the 8000000000 B an entry may hold; sealed: …"
    mW: the bound checked after the manifest is written same 101 a refused tree carries a manifest
    mT: every target under the root summed, not PATHS same 101 at the bound: "8000000001 B in 2 files under the cache's paths, …": tests/target, which no save stores, was counted
    mR: ~ read from the root, not HOME same 101 one byte above the bound: "1 files, 8000000000 B of the 8000000000 B …": the home's byte was not counted
    mU: a path the save finds nothing at is read, not skipped same 101 read <tmp>/home/.cargo/registry/index: No such file or directory (os error 2)
    mWk: each_under enters no directory same 101 one byte above the bound: "1 files, 1 B of the 8000000000 B …"
    mWk, same patch a_seal_dates_every_target_and_refuses_a_written_source 101 target/debug/deps/x at tv_sec: 1790878652, not 1000000000
    m1: the seal checks only that a source exists, not its date same 101 unwrap_err() on Ok("2 files, 2 B of the 8000000000 B an entry may hold; sealed: 1 sources, …")
    m11: the seal dates no file same 101 target/debug/deps/x at tv_sec: 1790878663, not 1000000000
    m2: restored() finds nothing in the root's target/ targets_restored_without_a_manifest_are_refused 101 [None, None]: neither the reader nor the writer refused target/debug
    m4: no clock check a_reader_whose_clock_is_not_after_the_entry_is_refused 101 a clock at the entry's date: "built from ff968c0…: 1 of 1 sources dated as built; …"
    m5: no runner check an_entry_built_on_another_runner_is_deleted 101 ImageOS: built from 7bcbf90…: 1 of 1 sources dated as built; …
    m6: a changed or added file is dated now alone, never its package a_file_cargo_was_never_told_about_rebuilds_its_package 101 probed reported "fresh":true, build-finished success:true
    m6, same patch an_entry_serves_exactly_the_sources_it_was_built_from 101 "gone": true
    m7: every file hashes as empty same 101 left: "one 1", right: "two 1"
    m9: only matched files dated same 101 left: "one 1", right: "two 1"
    m10: every file dated old same 101 left: "one 2", right: "two 1"
    m12: the manifest kept on read same 101 a warm tree keeps no manifest
    m19: a removed file dirties no package same 101 "gone": true
    m14a–d: RUNNER_OS, RUNNER_ARCH, ImageOS, ImageVersion dropped from the runner, one each an_entry_built_on_another_runner_is_deleted 101 each m14d: ImageVersion: built from be909b5…: 1 of 1 sources dated as built; …
    m15: an unset runner variable read as empty same 101 a runner without a variable: "macOS ARM64 20260928.1"
    m16: no package dated for code run at build time code_run_at_build_time_runs_again_on_every_read 101 left: "one one", right: "two two"
    m16, same patch every_spelling_of_code_run_at_build_time_is_found 0 (it tests the reading of a manifest, which m16 keeps)
    m17: build scripts only code_run_at_build_time_runs_again_on_every_read 101 left: "two one"
    m17, same patch every_spelling_of_code_run_at_build_time_is_found 101 [package] "[lib]\nproc-macro = true\n", build.rs: false
    m18: proc macros only code_run_at_build_time_runs_again_on_every_read 101 left: "one two"
    m18, same patch every_spelling_of_code_run_at_build_time_is_found 101 [package] "", build.rs: true
    mJ: runs_at_build reads [package] alone same 101 [project] "build = \"gen.rs\"\n", build.rs: false, left: Ok(false), right: Ok(true)
    mA1: once target/ci-driver exists, any driver carries the cache only_a_driver_built_in_its_own_target_carries_the_cache 101 target/debug/toyos-build
    mA2: a tree with no target/ci-driver carries it same 101 a tree with no target/ci-driver
    mB: no driver carries it same 101 target/ci-driver/debug/toyos-build
    mF: carry leaves CARGO_TARGET_DIR a_step_of_a_job_that_carries_the_cache_builds_in_its_own_target 101 left: Some(".../ci-carried-0/target/ci-driver"), right: Some(".../ci-carried-0/target")
    mI: carry leaves incremental state on same 101 the rustc line, with -C incremental=<fixture>/target/debug/incremental
    mG: carry leaves debuginfo whole same 101 the rustc line, with -C debuginfo=2
    • m14b, m14c and m14d drop RUNNER_ARCH, ImageOS and ImageVersion the way m14a drops RUNNER_OS.
    • m16 reverts the whole of what dates a package for code run at build time. m19 is red only through gone: count's build script reruns on every read, so count alone cannot show a removed file's package dating.
  • The seal job's route stands on reading, with no flag in it: Job::Seal => seal(root), and ci::seal calls cicache::cold, host and cicache::seal.

Unsure

  • No warm run of this head's reader has run on a runner, and runs_at_build runs only on a warm read.
  • --ci seal has not run on a runner. A workflow_dispatch of nightly.yml on this branch would run it and save nothing.
  • No nightly has run this design on Linux, so what a cold nightly costs there against a warm one is unmeasured; this PR's cold Linux run took 833 s, with no save.
  • The workflow rules hold as far as a review reads them; no test reds when a workflow breaks one.
  • guest and tcg, which name the guest cache, are held to the one-writer sentence alone: their deps step is the shell issues/build/the-build-runs-host-tools-outside-rust-and-qemu.md declares, and their serial-log upload runs under if: failure() with continue-on-error. The guest cache keeps today's discipline and no bound: issues/build/the-guest-cache-is-read-by-mtime-and-its-writer-restores-before-it-saves.md. At run 36878222090's ratio the host limit leaves it room up to 4,002,930,524 B.

🤖 Generated with Claude Code

https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm

Japabu and others added 3 commits October 1, 2026 11:17
… pruned to one entry; the licence step fetches only the fork's library/

Freshness. Cargo calls a path crate fresh when no source is newer than the
build that read it, and actions/checkout dates every source at the checkout,
so every path crate recompiled in every host run (152 compile events, 472 s
median of a PR run's 817 s). `-Z checksum-freshness` would fix it inside
cargo, and is refused here: it is still unstable (nightly-2026-09-21 lists it
under -Z, stable 1.98.1 rejects -Z), and the only ways to it are a nightly
toolchain or RUSTC_BOOTSTRAP=1 on stable. Either one moves the gate's verdict
off what a user's stable compiles: RUSTC_BOOTSTRAP lifts the feature gate for
every crate and flips the nightly probes in dependencies' build scripts, and
cargo passes -Zchecksum-hash-algorithm to rustc, so rustc needs it too. The
fork builds no cargo of its own (toolchain.rs lends the machine's).

An mtime made up from history (a commit's time) would call a changed file
fresh whenever its commit predates the entry's build. Instead the entry
carries the git blob id of every source its build read (target/ci-sources),
and the writer dates every file under every target 2001-09-09. A reader dates
each source whose blob matches the same, and every other one now. Cargo's
comparison is `source <= reference` is fresh, so a match is fresh and a
change or an addition is newer than everything in the entry, whatever any
runner's clock says. A package that lost a file keeps its Cargo.toml dated
now, because cargo's package fingerprint (a build script that names no input)
is the newest of the remaining files.

Only a cold run seals: a warm run's targets hold units its steps never
rebuilt, compiled from sources no manifest it could write describes. A reader
deletes the manifest it read, so a warm tree is never saved, and targets
restored without a manifest are refused.

The driver builds in target/ci-driver: cargo compiles it before any of this
runs, so its path crates recompile every time, and in the steps' target that
rebuild would make every dependent stale.

Cache discipline. nightly.yml's host is the writer on main and restores
nothing, so the entry is one cold build's tree and never an accumulation
(4.25 GB from 2.65 GB in one write). It then deletes every other host entry,
and reds if its own is not on main. A pull request that found no entry seals
and saves its own, which only its later runs read. The key carries the
runner's OS.

Licence step. The fork's std names toyos and toyos-abi by path from rust/,
so the library stays in rust/; a runner fetches the pinned commit's library/
alone (blobless, sparse) instead of the whole tree, and a developer's
uninitialised rust/ is refused rather than left sparse for a later build.
Cargo keeps one check of a unit, and the toyos-abi shape lints the unit the
workspace shape also lints, under other lints: each re-checks it every run,
and the workspace shape then re-checks every crate depending on it. Measured
on this host: after the shared-target toyos-abi shape the workspace shape
checked 13 crates again; after one in target/clippy-abi, none. A cache read
by content otherwise serves every one of those checks.

The guest cache's discipline is filed: its writer restores before it saves,
and its readers judge by mtime.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
@Japabu
Japabu marked this pull request as ready for review October 1, 2026 09:35
Japabu and others added 6 commits October 1, 2026 11:38
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
…s one leaf crate to recompile

A measurement, reverted by the next commit: the run before this one sealed
its tree into this pull request's cache scope, and this run reads it. Nothing
depends on toyos-dhcp, so it is the only crate whose bytes differ.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
…t run has one leaf crate to recompile"

This reverts commit 455780e: its run
measured what it was for.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
An entry is read by content, so a step recompiles only a crate whose bytes
changed, and incremental state is most of an entry's bytes: on this host one
host run's root target held 4.36 of 6.66 GB under incremental/, and the
workspace's test build took 2.83 GB with it and 1.01 GB without. This
pull request's first warm run restored its 3,250,736,567 B entry in 109 s, more
than every compile in it together (23.0 s).

The driver keeps it: its path crates recompile every run, and incremental
state is what makes that 13.2 s on a runner.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
@Japabu Japabu changed the title The host cache is read by content, sealed only by a cold run, and pruned; the licence step fetches only library/ Host cache: read by content, sealed only by a cold run, no incremental state, pruned to one entry; the licence step fetches only library/ Oct 1, 2026
@Japabu

Japabu commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator Author

Round 1, head 410bcc700. CI: host concluded success at this head (run 36850809890, "57 step(s), all green"). The five cicache tests and the renamed workflow test the branch adds ran green in that run.

Stale-build cases (covered means a warm run rebuilds or refuses)

  • A changed tracked source, whatever its mtime (1970 included): covered, because it is dated now.
  • An added tracked source: covered, because it is dated now and a no-rerun-if build script's package fingerprint changes.
  • A deleted or renamed source: covered three ways. Dep-info names a missing file. lost() dates the package's Cargo.toml now. A rerun-if-changed directory is always stale on a reader, because checkout dates directories and cargo's mtime_recursive counts them.
  • Two paths with one blob id: covered, because the manifest is keyed by path and equal ids mean equal bytes. The tree tracks no symlink. The one gitlink (rust/) is skipped and keeps its fetch date, so whatever reads it rebuilds.
  • Untracked files a build reads: covered for generated or downloaded files outside a target, which are absent or newer on a reader. Out-of-repo dep-info paths are compared against 2001-09-09, so one dated earlier than that which then changes is missed. The tree has none.
  • Inputs cargo is not told about: not covered. Examples: a build script reading an env var, a tool or a file outside its rerun-if-* list; a proc macro reading files; a new src/x/mod.rs beside an existing src/x.rs, which is E0761 cold but fresh warm, in any path crate today. The tree's three build scripts declare their inputs and it has no proc-macro crate. Before this branch every path crate recompiled. Now only the cold nightly catches these, after the merge queue has already moved main.
  • Clock skew: covered between runners, because the entry is pinned to 2001-09-09. A reader whose clock reads earlier than that is not refused.
  • Cargo's other inputs (rustc -vV, profile, features, RUSTFLAGS, env! values, rerun-if-env-changed values, the unit graph, the package path): covered by cargo itself and untouched by the design. The host linker, the SDK and the system libraries are in neither cargo's fingerprint nor the key.

#667: the keys carry runner.os and nothing else, so no image and no arch. The design works on ubuntu-24.04 unchanged: whole-second mtimes survive GNU tar, and set_modified on a directory fd works on Linux. The conflicts are textual: the host-linux- keys, and host()'s vec![ against steps.extend([ plus --no-fail-fast.

First run after landing: a prune failure is contained. Prune runs after the save. It deletes nothing unless the listing is complete and holds this run's key on main, and it never deletes that key. No nightly job needs host. If the listing lags behind the save, though, every nightly goes red, and prune has never run on a runner.

Growth: +675 −39, net +636. Production is +479 −34 (net +445); tests are +196 −5 (net +191). Content-dating earns its growth: jobs went from 944 s to 403–504 s. The deletions are named below: the PR save, prune, and the hash-object child.

BLOCKER

  • .github/workflows/ci.yml:49 — the pull-request save is a second writer that runs exactly when main's entry is missing, which makes it a fallback. It is the reason the one-writer test was loosened (src/ci.rs:1007). Its 3.25 GB and 1.17 GB entries shared the 10 GB in which the guest entry was evicted. Delete the step, ci.yml:27-30 and :47-48, and the loosening, and keep the manifest assertion on nightly's save.
  • src/cicache.rs:206 — prune runs gh. The declaration refuses gh because it is Go, and its row (issues/build/the-build-runs-host-tools-outside-rust-and-qemu.md:32) names only src/release.rs. Delete prune, doomed and gh (cicache.rs:151-216, 514-539), Job::Prune, nightly.yml:27-29 and nightly.yml:51-53. With 1.17 GB entries and one writer, the newest host entry plus the newest guest entry is 4.45 GB, and both are accessed more recently than any older entry, so last-access eviction keeps them.
  • src/cicache.rs:270 — git hash-object --stdin-paths is a git use no row declares, and it needs a writer thread and a count check. sources() also re-lists tracked files beside sysroot::tracked_files. Running sha2 (already a dependency) over sysroot::tracked_files, skipping the gitlink, gives the same byte identity in-process.
  • src/licence.rs:1133-1141 — git init, remote add, an HTTPS fetch --filter=blob:none, sparse-checkout set and checkout --detach are declared nowhere. The admitted git row (issues/build/the-build-runs-host-tools-outside-rust-and-qemu.md:20) still says src/licence.rs updates submodules, and the refused row (:21) says gitoxide does HTTPS fetches. Declare each use with a measured verdict in the same diff.
  • src/cicache.rs:130 — this mutation stays green: change || modified(&root.join(path)).ok() != Some(built()) to || modified(&root.join(path)).is_err(). Every test and clippy still pass. A step that writes a tracked source and then restores its bytes gets sealed over units built from the bytes in between. Make a_seal_dates_every_target_and_refuses_a_written_source red against it: after write(&tmp, "a.rs", "b\n"), add write(&tmp, "a.rs", "a\n") and require a refusal.
  • src/cicache.rs:9-12 — the merge queue's warm run now passes a stale build whenever a path crate's build reads an input cargo is not told about (the "not covered" case above). This is a new compromise and nothing records it. File it in issues/ with an owner, the E0761 case as evidence, and an exit condition that a gate can fail.

NOTE

  • .github/workflows/ci.yml:42 — the key carries neither the runner image nor the arch. Adding ImageOS/ImageVersion makes an image move cost one cold run.
  • src/cicache.rs:103 — refuse when SystemTime::now() <= built(). It is the one clock assumption the design makes.
  • src/cicache.rs:322 — this mutation stays green: reduce restored() to if target != ours { found.push(target); }. The refusal test plants only kernel/target; plant target/debug too.
  • src/cicache.rs:61-72 — read's refusal of a driver built elsewhere has no test.
  • src/identity.rs:28 — identity::of is not the cache's identity and must not become it. The toyos-abi shape's clippy::undocumented_unsafe_blocks reads // SAFETY: comments, and line numbers reach panic locations, so bytes are the right identity.
  • PR body, negative controls — the body gives only exit codes, not the six patches or their logs.
  • 965d7d3, dd2d210 — these commits carry no attribution trailers.

REMOVE

  • .github/workflows/ci.yml:3-4 — "Each step is cargo run -- --ci <job> … which runs the same on a dev host" is no longer true of the host step.
  • src/identity.rs:3-4 — "One definition, read by every question of the form …" is no longer true, because cicache answers that question by bytes.
  • src/cicache.rs:12 — "whatever any runner's clock says" is false for a clock earlier than 2001-09-09.
  • PR body, "Unsure" — the wt/toyos-nomacos bullet and "Until that nightly writes on main …" are landing coordination that will rot in main's record.

SEND BACK

Japabu and others added 2 commits October 1, 2026 13:05
…ole, the image in the manifest

Answers the review at #669 (issuecomment-5930046178).

- The pull request's own cache save goes, with the comments that described
  it and the loosened writer test: nightly.yml's `host` is the host cache's
  one writer again, and `each_cache_has_one_writer` is main's test plus the
  assertion that a host save is guarded by the manifest.
- `--ci prune`, `doomed`, `gh` and nightly's `actions: write` go. Eviction is
  by last access, and with one writer the newest host and guest entries are
  the most recently read.
- A tracked file's identity is the SHA-256 of its bytes, hashed in-process
  over `sysroot::tracked_files`; the `git hash-object` child, its writer
  thread and its count check go. The gitlink is skipped as a directory.
- The licence step's sparse fetch (git init, remote add, an HTTPS
  `fetch --filter=blob:none`, sparse-checkout, checkout --detach) goes back
  to main's `git submodule update --init --depth 1 rust`, the use the
  admitted git row already declares. Measuring gitoxide's partial-clone and
  sparse-checkout support for a verdict was not worth the licence step's
  15-20 s.
- A package with any tracked file changed, added or removed has every file
  dated now. A file rustc probed for and did not find (a new `src/x/mod.rs`
  beside `src/x.rs`, E0761 cold) is in no dep-info, so dating the new file
  alone left the crate fresh; dating its package's files rebuilds it. This
  subsumes the lost-file rule. What a build reads outside its own package
  undeclared stays trusted, as cargo's own incremental build trusts it, and
  is filed with its exit.
- The runner image goes in the manifest, not the key: no workflow
  expression sees `ImageOS` or `ImageVersion` (the `env` context holds only
  what a workflow sets; rclone's and apache/httpd's workflows say so at
  their sites). The key carries `runner.os` and `runner.arch`; a reader whose
  `RUNNER_OS RUNNER_ARCH ImageOS ImageVersion` differs from the entry's
  deletes the restored targets and runs cold.
- A warm reader whose clock does not read after 2001-09-09 is refused.
- New tests: a written-back source is refused by the seal; a restored
  `target/debug` without a manifest is refused; a driver built elsewhere is
  refused; another image's entry is deleted; a clock at the entry's date is
  refused; E0761 rebuilds warm as it fails cold.
- REMOVEs: ci.yml's "runs the same on a dev host", identity.rs's "one
  definition" sentence, and cicache's "whatever any runner's clock says".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
@Japabu Japabu changed the title Host cache: read by content, sealed only by a cold run, no incremental state, pruned to one entry; the licence step fetches only library/ Host cache: read by content, sealed only by a cold run on main, no incremental state Oct 1, 2026
@Japabu

Japabu commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator Author

Round 2, head 59cc178b6. CI: host concluded success at this head (run 36855422648, "58 step(s), all green"). The run was cold: "Cache not found for input keys: host-sealed-macOS-ARM64-…". The seven cicache::tests and ci::tests::each_cache_has_one_writer ran ok in its cargo test --lib (387 passed, 0 failed).

Round 1's BLOCKERs

  • CLOSED — .github/workflows/ci.yml:49, the pull-request save. ci.yml now only restores. each_cache_has_one_writer is main's test plus the host prefix and guard asserts. m8, which puts the save back, is red, EXIT 101.
  • CLOSED — src/cicache.rs:206, prune and gh. prune, doomed, Job::Prune, nightly's prune step and actions: write are gone. git grep finds no prune, "gh", hash-object or actions: write in src/ci.rs, src/cicache.rs or .github/.
  • CLOSED — src/cicache.rs:270, git hash-object. sources() hashes with sha2 over sysroot::tracked_files. m7, which hashes every file as empty, is red, EXIT 101.
  • CLOSED — src/licence.rs:1133-1141. git diff origin/main...HEAD -- src/licence.rs is empty.
  • CLOSED — src/cicache.rs:130, the seal mutation. The test now writes the original bytes back and still requires a refusal. m1 is red, EXIT 101: unwrap_err() on Ok(…).
  • CLOSED — src/cicache.rs:9-12, the unrecorded stale pass. A changed package is now dated whole. m6 is red on a_file_cargo_was_never_told_about_rebuilds_its_package, EXIT 101, with probed fresh. The outside-package rest is filed, and its exit is a test.

Can a stale build still pass? Only through the filed hole.

  • sysroot::tracked_files is the right set on a runner. At read, the checkout holds only tracked files, the empty gitlink and the restored targets. Any untracked file a build reads is then one of three things:

    • a target's own output, which cargo's fingerprint and the seal's single date judge;
    • a file a step makes after read, which is newer than every unit in the entry;
    • a file outside the checkout.

    Cargo 1.98.1's find_stale_file skips $CARGO_HOME/{git,registry} ("Assuming anything in cargo_home/{git, registry} is immutable"). That is why registry sources rebuild nothing, even though they extract at mtime 1153704088 (2006, after the seal's 2001). Everything else outside the checkout comes with the runner image, which the manifest bounds. A tracked generated file is hashed like any other source. The tree's only include! of a target path is OUT_DIR. On a developer's tree this set would be wrong, but read runs only on a runner.

  • Dating a changed package whole is sound for a dependent that did not change. The dependent's own files stay at 2001. Cargo's check_filesystem still makes a unit stale when a dependency is stale ("If our dependency is stale, so are we"), or when a dependency's outputs are newer than its own. The seal leaves every output at one date, so the second rule fires exactly on units rebuilt this run. The oracle measures this: app is unchanged and rebuilds when leaf changes.

  • The filed risk is honestly bounded, and its exit names a test.

    • git ls-files finds build scripts only in userland/calc, doom and snake.
    • The run's userland steps are blockd, calc, fsd, logd, netd, pkg, soundd and sshd, so calc is the only one built, and it declares its font.
    • No tracked manifest says proc-macro = true.
    • Registry build scripts and proc macros are outside the claim. They are no more exposed than before, because cargo never checks their dates.

sha2: sha2 = "0.10" is already in the root Cargo.toml (line 165) and in Cargo.lock (0.10.9), and this branch changes neither. It is RustCrypto's SHA-2: general, widely used, and src/release.rs already hashes with it. It passes the dependency rule.

#667 (cc32f02ee, host still in progress): the key and the manifest work on ubuntu-24.04 unchanged.

One conflict will not show in a textual merge. #667 adds cargo run -- --ci host to nightly's portability-macos job, after --build-only. With GITHUB_ACTIONS set, read refuses that step for two reasons: no target/ci-driver exists, and the targets --build-only left have no manifest. Only the nightly runs that step, so no PR gate would catch a missed fix.

Land #667 first. This branch is going back anyway, so its merge of main carries the reconciliation: the key, and that step made to pass read. Its next CI run is then cicache's first run on Linux, in the PR that owns cicache.

The proposed rule ("a cache key never uses env.<runner variable>"): decline. Its reason is already stated where the check is, at src/cicache.rs:24-25. Breaking the rule is harmless: ${{ env.ImageOS }} keys on '', and the manifest's runner line still refuses another image. A copy at the cache step would be a second home for prose that protects nothing.

Growth: +646 −29, net +617.

  • Production Rust: +334 −7, net +327.
  • Tests: +254 −3, net +251.
  • Workflows: +15 −19.
  • Issues: +43.

Production is down from round 1's net +445, by the named deletions and the licence revert. Content-dating earns the rest: the job went from 944 s to 481–504 s in round 0's warm runs. This round's reader has run only cold.

BLOCKER

  • src/cicache.rs:86 — this mutation stays green:

    -    let runner = ["RUNNER_OS", "RUNNER_ARCH", "ImageOS", "ImageVersion"]
    +    let runner = ["RUNNER_OS", "RUNNER_ARCH", "ImageOS"]

    No test reaches this list. a_driver_built_in_any_other_target_is_refused returns before it, and an_entry_built_on_another_runner_is_deleted passes open two literal strings. So "image by the manifest" is a claim no test can fail. Yet the image check is what bounds the out-of-tree inputs, including generator 0.8.9's cc-built code, which this job compiles for kernel-loom. Make an_entry_built_on_another_runner_is_deleted red against this mutation: build both runner strings with the function read uses, from two environments that differ only in ImageVersion.

NOTE

  • issues/build/a-warm-host-run-trusts-cargo-for-what-a-build-reads-outside-its-package.md:21 — remove the compromise rather than record it. Dating every package with a tracked build script whole, on every warm read, forces the rerun the hole lacks. It costs nothing today: userland/calc is the only such package, and it already reruns on nearly every warm read, because its font sits in the root package that 79 of 80 landings change (per the PR body).
  • src/CLAUDE.md:17 — "A workflow step runs cargo run -- --ci <job> and nothing else" is now false for both host steps (ci.yml:42 and nightly.yml:32 add --target-dir target/ci-driver) and for nightly's guarded save. The PR body states the deviation, but the rule agents read does not. The orchestrator either briefs the exception or sends the design back.

REMOVE

  • src/cicache.rs:47 — this doc line restates the workflows' key suffix, which no test reads, so it goes false on the next key change.
  • issues/build/the-guest-cache-is-read-by-mtime-and-its-writer-restores-before-it-saves.md:16-18 — this round corrected these lines instead of deleting them, and they restate cicache's design, which will drift from the code.
  • PR body, "The licence step is main's" — "Commit 59cc178's message gives that saving as …" corrects a commit message inside main's record.
  • PR body, the bullet after the controls table — "Round 0's six controls ran against round 0's code…" records which review round asked for which control, which main's record does not need.

SEND BACK

Japabu and others added 3 commits October 1, 2026 14:27
…d code run at build time runs again on every read

The runner-variable list in `read` was reached by no test: dropping
`ImageVersion` from it stayed green. `runner` now builds the runner from a
lookup, `read` hands it the environment, and
`an_entry_built_on_another_runner_is_deleted` builds both runners with it,
from environments that differ in one variable, for each of the four. It also
refuses an environment missing one, so an unset variable is never defaulted.

The warm read's trust in cargo for what a build reads outside its package is
removed rather than recorded. Every package with a build script or a proc
macro has every file dated now on every warm read, so cargo reruns the script
and rebuilds the macro and what expands it, whatever either reads. A package
is one when its manifest has `build.rs` beside it and no `build = false`, a
`build` key naming a script, `proc-macro` or `proc_macro`, or a `proc-macro`
crate type. In the host job that is userland/calc alone, whose font already
sits in the root package that nearly every landing changes.

- `code_run_at_build_time_runs_again_on_every_read` is the deleted issue's
  exit: a build script and a proc macro read another package's file and never
  say so, and a warm read after a change to that file prints what a cold
  build prints.
- `every_spelling_of_code_run_at_build_time_is_found` holds each spelling.
- The oracle test gains `gone`, a package that lost a file nothing read.
  `count`'s build script now reruns on every read, which would otherwise hide
  a removed file's package dating from every test.

What a warm run still trusts cargo for is a file only a flag names, a linker
script in another package: filed as
issues/build/a-warm-host-run-never-relinks-for-a-file-only-a-flag-names.md.

Removed: SEALED's doc line, which restated the workflows' key, and the guest
cache issue's restatement of the host cache's design.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
#667 moves `host` to ubuntu-24.04 in both workflows and keys its cache
`host-linux-`. Resolved:
- ci.yml: ubuntu-24.04 and the dropped macOS comment are main's. The key
  stays `host-sealed-${{ runner.os }}-${{ runner.arch }}-`, which is
  `host-sealed-Linux-X64-` on ubuntu-24.04, so it keeps Linux entries apart
  from macOS ones as `host-linux-` did.
- nightly.yml `host`: ubuntu-24.04 is main's. Main's `host-linux-` restore
  and its main-only save step give way to this branch's writer, which
  restores nothing, runs on main alone and saves under the sealed key.
- nightly.yml `portability-macos`: main's `cargo run -- --ci host` after
  `--build-only` is kept as main has it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
… job carries the cache

`src/CLAUDE.md` says a workflow step runs `cargo run -- --ci <job>` and
nothing else. Both host steps passed `--target-dir target/ci-driver`, and
nightly's save was guarded by `hashFiles('target/ci-sources')`.

- The driver's own target is now the `CARGO_TARGET_DIR` of ci.yml's and
  nightly.yml's `host` jobs, so each step is the bare command. Cargo hands
  that variable to the program it runs, so `host` takes it out of its
  environment before any step and no step builds in the driver's target.
- `cicache::carried` asks where the driver runs from: a job that builds it in
  `target/ci-driver` carries the cache, and any other runs the host suite as
  a developer's tree does. That reconciles #667's `cargo run -- --ci host` in
  nightly's `portability-macos`, which follows `--build-only`: its driver is
  in `target/debug`, and `read` would have refused the targets `--build-only`
  left, which no manifest describes.
- No runtime refusal of a driver built elsewhere is left. Instead
  `each_cache_has_one_writer` holds every job that restores or saves the host
  cache to that `CARGO_TARGET_DIR` at job level, so a workflow that drops it
  reds in the pull request that drops it.
- Nightly's save has no guard. Its job restores nothing, so its run is cold,
  and a cold run's last step is the seal: the job is green only with a sealed
  tree, and a save follows only a green job. `each_cache_has_one_writer` reds
  on a host writer that restores anything, in place of the guard it checked.
- The seal names the runner it records, so a cold run's log shows what a warm
  reader will compare against.
- `MANIFEST` is private: the guard was its one reader outside the module.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
@Japabu

Japabu commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator Author

Round 3, head db4654fb6. CI: host concluded success at this head (run 36867673999, ubuntu-24.04, 795 s). The run was cold: "Cache not found for input keys: host-sealed-Linux-X64-36867673999, host-sealed-Linux-X64-", then "[ci] Host: 58 step(s), all green". Its cargo test --lib ran the nine cicache::tests and ci::tests::each_cache_has_one_writer ok (388 passed, 0 failed, 7 ignored).

Round 2's findings, checked against git diff 59cc178b6..db4654fb6 without the merge (02fa5c5, db4654f)

  • CLOSED — BLOCKER src/cicache.rs:86, the runner list. read (:86) passes the environment to runner (:91). an_entry_built_on_another_runner_is_deleted (:622) builds both runners with runner, from environments that differ in one variable, once for each of the four. On db4654f, m14a–d are red (EXIT 101 each), and so is m15 (EXIT 101).
  • Done — NOTE, the build-script residual is removed rather than recorded. m16, m17 and m18 are red on code_run_at_build_time_runs_again_on_every_read (EXIT 101). The issue file is deleted, and git grep of its slug exits 1.
  • Done — NOTE src/CLAUDE.md:17. It is true of every step this branch touches.
    • ci.yml:44 and nightly.yml:36 are cargo run -- --ci host.
    • nightly.yml:38's save has no if:.
    • git diff origin/main...HEAD -- src/CLAUDE.md is empty.
    • The job-level CARGO_TARGET_DIR (ci.yml:23-24, nightly.yml:29-30) is an input, not a step.
  • Done — the four REMOVEs.

Growth: +806 −35, net +771.

  • Production Rust: +371 −7, net +364, against +327 at round 2. The additions are runner, runs_at_build and carried.
  • Tests: +379 −10.
  • Workflows: +21 −18.
  • Issues: +35.

The growth pays only if a Linux entry serves pull requests, and BLOCKER 2 leaves that unshown.

BLOCKER

  • src/ci.rs:485 — mF deletes std::env::remove_var("CARGO_TARGET_DIR"); and passes every host test (the body: 389 passed, EXIT=0). No test reaches std::env::set_var("CARGO_INCREMENTAL", "0"); at :489 either, because no test calls host(). Either deletion takes away the cache's gain, and nothing reds:

    • Without the first, every step builds in the driver's target. The driver's path crates are compiled again there every run (src/cicache.rs:31-35).
    • Without the second, the entry carries incremental state again. In run 3 that was 9,553 MiB, against 3,589 MiB without it.

    Both must turn ci::tests::a_step_of_a_job_that_carries_the_cache_builds_in_its_own_target red. The test:

    • is a child role, as buildlock::tests::child_role is, spawned with CARGO_TARGET_DIR=target/ci-driver and without CARGO_INCREMENTAL;
    • calls the one function host() calls for a job that carries the cache;
    • then runs cargo metadata --format-version 1 --no-deps and cargo build -v on a one-crate fixture;
    • requires target_directory to be the fixture's target, and no rustc line to carry -C incremental.
  • .github/workflows/nightly.yml:38 — The decision not to prune rests on "those two outlive every older entry". Last-access eviction gives that only while the entries fit, and nobody has measured the Linux entry. Take a night whose guest jobs restore after host saves, as on a toolchain bootstrap:

    • At the host save, yesterday's guest entry was last touched when it was saved, while pull requests read yesterday's host entry all day. Once 2H + G > 10 GB, eviction takes the guest entry before tonight's guest jobs read it.
    • If the guest entry survives, the guest jobs read it after tonight's host entry was saved. Once H + 2G > 10 GB, tcg's save then evicts tonight's host entry, unless a pull request reads it after that restore.
    • G was 3,281,375,938 B and grows (issues/build/the-guest-cache-is-read-by-mtime-and-its-writer-restores-before-it-saves.md). So the second bound needs H ≤ 10 GB − 6,562,751,876 B.
    • H on ubuntu-24.04 is unmeasured. The sealed tree there is 7684 MiB, against 3575 MiB on macOS. The body's 4.45 GB sum uses macOS's 1,171,199,636 B.

    Measure H with one run that saves the Linux entry. Then show both bounds for today's G and for The guest suite is every pull request's guest check, on toolchain stores CI caches by the build system's own keys; a landing during a nightly does not red its release #671's guest entry, or bound H. Until then, a night like that can lose one of the two entries, and nothing reds.

NOTE

  • PR body, Unsure (Linux: 7684 MiB in 11823 files; macOS: 3575 MiB in 15182 files) — Nothing explains the doubling. The likely cause is unmeasured:

    • [profile.dev] (Cargo.toml:213) keeps cargo's default full debuginfo.
    • On Linux, split-debuginfo defaults to off, so every test binary the steps link carries all its dependencies' DWARF.
    • On macOS, it defaults to unpacked, which leaves that DWARF once, in the object files. That would also explain macOS's larger file count.

    BLOCKER 2's measurement on a runner settles it.

  • src/cicache.rs:227 — runs_at_build reads [package] only. Cargo's manifest schema reads [project] as the same table (cargo-util-schemas 0.8.2, TomlManifest::package). So [project] with build = "gen.rs" reads as no build script, while cargo runs gen.rs. No tracked manifest says [project] (git grep exits 1). Read either table and add the spelling to every_spelling_of_code_run_at_build_time_is_found, or the doc's "every spelling cargo accepts" (:222) goes.

  • PR body, Stale-build cases, the registry line — A registry proc macro is compiled once, but it runs inside every compile of the path crate that expands it.

    • Suppose it reads another package's file without telling rustc. Before this PR it read that file again every run, because every path crate recompiled. Now it passes stale whenever the expanding crate is unchanged.
    • I find no proc macro in the host build that does this; wayland-scanner reads only its own registry crates' XML.
    • Record it beside issues/build/a-warm-host-run-never-relinks-for-a-file-only-a-flag-names.md, with an exit a test can fail.
  • src/ci.rs:1000 — jobs() reads text, so a step brought in by an alias (- *name) is invisible to it. Nightly's tcg already restores the guest cache that way (nightly.yml:162). A host-cache restore aliased into nightly's host would pass the check that the host writer restores nothing (:1043). Refuse a - * step in a job that touches the host cache.

  • src/ci.rs:1000 — jobs() is the tree's first reader of a workflow's jobs, and The guest suite is every pull request's guest check, on toolchain stores CI caches by the build system's own keys; a landing during a nightly does not red its release #671 adds two tests that read jobs out of the same files. Whichever lands second takes the other's reader rather than adding a second.

REMOVE

  • PR body, Unsure — "After this lands, the orchestrator dispatches main's nightly once, so that its host writes the first host-sealed-Linux-X64- entry; until then every pull request's host runs cold." This is landing coordination, and it rots in main's record.
  • PR body, One writer — "so those two outlive every older entry. With run 3's macOS host entry, together they were 4.45 GB of the 10 GB (1,171,199,636 B + 3,281,375,938 B)." It is false under last-access eviction (BLOCKER 2), and it was measured on the OS the job has left.
  • PR body, Stale-build cases — "as before this PR", in the registry line. It is false for a registry proc macro's expansion (NOTE above).

SEND BACK

…te or full debuginfo, and the writer bounds what actions/cache stores

- `ci::carry` is the one function `host()` calls for a job that carries the
  cache. `a_step_of_a_job_that_carries_the_cache_builds_in_its_own_target`
  runs it in a driver of its own, spawned with the job's `CARGO_TARGET_DIR`
  and without `CARGO_INCREMENTAL` or `CARGO_PROFILE_DEV_DEBUG`, and requires
  `cargo metadata`'s `target_directory` to be its fixture's `target`, and
  `cargo build -v`'s rustc line to carry no `-C incremental` and
  `-C debuginfo=line-tables-only`.
- The steps build with line tables alone
  (`CARGO_PROFILE_DEV_DEBUG=line-tables-only`). A backtrace in a step's log
  reads them: a panic while panicking prints one, as the `doorbell-kick-relaxed`
  control does in run 36867673999. Nothing reads the rest: no step sets
  `RUST_BACKTRACE`, and no host test reads a binary's DWARF. A Linux link
  copies the DWARF of every object it takes into the binary, which macOS
  leaves in the objects: the Linux run sealed 7684 MiB where macOS sealed
  3575 MiB. `toyos-build`'s lib test, cross-linked on the dev host for
  x86_64-unknown-linux-gnu by rust-lld with no C runtime, so that what it keeps
  is almost all DWARF, is 124,452,112 B with full debuginfo, 35,983,096 B with
  line tables alone, and 848,744 B with none.
- `cicache::bound` runs after the seal. It makes the archive actions/cache
  v4.3.0 makes of `cicache::PATHS` (its log on a runner: `gtar --posix -cf
  cache.tzst --exclude cache.tzst -P -C <workspace> --files-from
  manifest.txt --use-compress-program zstdmt`) with the runner's own `tar` and
  `zstdmt`, counts it as it streams, and refuses it above 2,000,000,000 B, so
  the save, which follows only a green run, never runs.
  - The limit: eviction is by last access past the repository's 10 GB, and on
    a night whose guest jobs restore after the host entry is saved the
    repository holds two host entries beside a guest one, then one beside two
    guest ones. With the last guest entry, G = 3,281,375,938 B, and H at the
    limit, 2H + G = 7,281,375,938 B and H + 2G = 8,562,751,876 B. G may grow
    to 4,000,000,000 B before H + 2G reaches 10 GB.
- `each_cache_has_one_writer` holds every host-cache step's `path:` list to
  `cicache::PATHS`, which the bound measures, and refuses a step taken by an
  alias, or anchored for one, in a job that names the host cache: its reader
  sees neither.
- `runs_at_build` reads `[project]` as cargo reads `[package]`.
- `issues/build/a-warm-host-run-keeps-what-a-registry-proc-macro-expanded-from-a-file-it-never-named.md`
  files the residual a warm read leaves for a registry proc macro.
- The host-tools row for `tar` and `zstd` names `src/cicache.rs` beside
  `src/release.rs`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
@Japabu Japabu changed the title Host cache: read by content, sealed only by a cold run on main, no incremental state Host cache: read by content, sealed only by a cold run on main, and bounded as actions/cache stores it Oct 1, 2026
… zstdmt go

The bound ran the runner's `tar` and `zstdmt` to count what actions/cache
would store. The host-tools declaration refuses both, and the Rust tool its
row names is a zstd crate, a new dependency only to measure. The seal already
summed the lengths of the files it dates under every target, for its log
line; it now refuses a tree above `LIMIT` with that sum, before it writes the
manifest. The step is red, so the save, which runs only after a green step,
stores nothing, and the tree carries no manifest a reader would take. The
seal's line prints the sum in bytes beside the limit, where it printed MiB.

The limit is 8,000,000,000 B of targets, uncompressed. Run 36878222090 sealed
5369 MiB of targets, at least 5,629,804,544 B, and the archive the bound made
of the cache's paths, as actions/cache makes one, was 1,403,326,566 B: a
ratio of 4.01. At that ratio the limit stores at most 1,994,138,951 B. With
the last guest entry's 3,281,375,938 B, a night then holds
2H + G = 7,269,653,840 B and H + 2G = 8,556,890,827 B, both under the 10 GB
eviction limit, and the ratio may fall to 2.38 before 2H + G reaches it. The
7,684 MiB tree is db4654f's, sealed with full debuginfo in run 36867673999,
which ran no bound.

`an_entry_above_its_bound_is_refused` seals a fixture whose targets hold the
limit plus one byte, the limit in a sparse file under `target/` and the byte
under `kernel/target/`, and requires the refusal and no manifest. It then
seals the same tree at the limit exactly.

The host-tools row is main's again: no ToyOS code runs `tar` or `zstd` for
the cache.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
@Japabu Japabu changed the title Host cache: read by content, sealed only by a cold run on main, and bounded as actions/cache stores it Host cache: read by content, sealed only by a cold run on main, and bounded by the bytes it seals Oct 1, 2026
@Japabu

Japabu commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator Author

Re-review, head b4dfda550; the last review was at db4654fb6. CI: host concluded success at this head (run 36881892289, ubuntu-24.04, on the merge into a97ff80df). The run was cold ("Cache not found for input keys: host-sealed-Linux-X64-36881892289, host-sealed-Linux-X64-") and ended "[ci] Host: 58 step(s), all green". Its cargo test --lib ran cicache::tests::an_entry_above_its_bound_is_refused ... ok and ci::tests::a_step_of_a_job_that_carries_the_cache_builds_in_its_own_target ... ok. The seal: "11823 files, 5628639857 B of the 8000000000 B an entry may hold".

The last review's BLOCKERs (git diff db4654fb6..b4dfda550)

  • CLOSED — src/ci.rs:485, mF and CARGO_INCREMENTAL untested. host() calls carry() (src/ci.rs:484, :568). a_step_of_a_job_that_carries_the_cache_builds_in_its_own_target (:896) runs it in a child driver spawned with CARGO_TARGET_DIR=target/ci-driver and without CARGO_INCREMENTAL or CARGO_PROFILE_DEV_DEBUG. The body's mF, mI and mG are red, EXIT 101 each, printing …/target/ci-driver against …/target, then -C incremental=…, then -C debuginfo=2.
  • CLOSED — .github/workflows/nightly.yml:38, H never measured against 10 GB.

The bound

  • Arithmetic.
    • bc reproduces 1,994,138,951 B, 7,269,653,840 B, 8,556,890,827 B, 4,002,930,524 B and the 2.38 floor.
    • At the lowest ratio this PR measured (3.08), 2H + G = 8,473,703,328 B and H + 2G = 9,158,915,571 B, both under 10 GB.
    • The limit is GitHub's default: the repository's actions/cache/storage-limit answers 402, so no paid limit is set.
  • What coexists. The body's two sets are the right ones, and both occur every night, not on a bootstrap night only.
    • On nightlies 36843762360, 36696295750 and 36550208853, the guest jobs restored 2 h 09 m to 2 h 51 m after host finished.
    • On 10-01 the first set overflowed. host restored 4,456,914,229 B and saved 5,272,701,372 B. Then tcg found "Cache not found for input keys: guest-36843762360, guest-" at 12:48.
    • After landing, only nightly's host and tcg write.
    • The two entries gh cache list holds today are never read again, so eviction takes them first: host-36843762360 (5,272,701,372 B, main) and host-sealed-macOS-36847583440 (1,171,199,636 B, refs/pull/669/merge).
  • Eviction. A lost entry costs cold runs, never a stale pass, because a reader refuses targets without a manifest. Nothing reds. With LIMIT and today's G, nothing needed is evicted. G has no bound (NOTE).
  • Refusal. It is loud: the step is red, the driver exits 1 (src/ci.rs:85), and the line names the bytes, the files and LIMIT. No manifest is written, and mW tests that. No save follows only because the save has no if: (BLOCKER).
  • The runner's tools are gone. git diff 0edf266ab..HEAD -- issues/build/the-build-runs-host-tools-outside-rust-and-qemu.md is empty. No Command::new of tar, zstd, zstdmt or gh is left in src/ci.rs or src/cicache.rs.
  • Mutations. Reading the code, I find that mN, mX, mO, mM and mW each fail at the assertion whose output the body quotes. So do mF, mI, mG, mP, mY, mH, mK and mJ.
  • Merge with main. Not clean (NOTE).

Growth. git diff --shortstat origin/main...HEAD: 10 files, +944 −35, net +909.

  • Production Rust: +411 −7, net +404.
  • Tests: +454 −10, net +444.
  • Workflows: +21 −18.
  • Issues: +58.

Since db4654fb6: production +53 −13 (net +40), tests +94 −19, issues +23. Accepted: LIMIT, the refusal and carry. Not accepted: PATHS (NOTE).

BLOCKER

  • .github/workflows/nightly.yml:38 — this mutation stays green, and so does continue-on-error: true on the cargo run -- --ci host step at :36:

    -      - uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
    +      - if: always()
    +        uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
    • each_cache_has_one_writer (src/ci.rs:1101-1109) reads neither line, and no other test reads this save.
    • Either patch saves a tree the seal refused. That tree is above LIMIT, which is the size the bound exists to keep out. It also carries no manifest, so every pull request's read refuses it (src/cicache.rs:127-131).
    • This round's claim rests on that missing if: alone: src/cicache.rs:37-38, and the body's "the save, which runs only after a green step, stores nothing".
    • Make ci::tests::each_cache_has_one_writer red against both patches: in a job that saves the host cache, no line may name always(), cancelled(), failure() or continue-on-error.

NOTE

  • src/ci.rs:31, src/ci.rs:566 — the branch does not merge clean onto main.
  • src/cicache.rs:54 — PATHS lost its only production reader when stored() went.
    • Only ci::tests reads it now (src/ci.rs:1104), and pub keeps the dead-code lint quiet.
    • The seal (:209-212) sums every target under the root instead. That leaves out the three ~/.cargo paths the save stores, and would count a target the save does not store.
    • Summing PATHS, with ~ taken from HOME, gives the bound the set the save archives and gives PATHS its reader. Otherwise PATHS moves into the test.
  • src/cicache.rs:65 — LIMIT reaches the 10 GB only through run 36878222090's ratio, taken once with the runner's tar and zstdmt, which the tree no longer runs.
    • No gate reads a stored size. A change in how well the targets compress, or in the uncounted ~/.cargo paths, moves H toward the 2.38 floor with nothing red.
    • The body records this under Unsure, not in issues/. File it with an exit a gate can fail, or remove it.
  • issues/build/the-guest-cache-is-read-by-mtime-and-its-writer-restores-before-it-saves.md:9 — this file records G but not the ceiling the host entry needs G to stay under.
    • H + 2G binds every night. The ceiling is 4,002,930,524 B at LIMIT, or 4,298,336,717 B at run 36878222090's H.
    • Above it, tcg's save evicts that night's host entry unless a pull request has read the entry since the guest restore. Every pull request then runs cold, and nothing reds.
  • src/ci.rs:560 — the bound reaches a pull request only when its run is cold.
    • A pull request that takes the targets past LIMIT lands green on a warm run.
    • It then reds the nightly and every cold pull request after it.
    • After an image move, every pull request is cold.

REMOVE

  • PR body, "The limit." — the clause "as on a toolchain bootstrap": the late restore happens every night (see above).
  • PR body, first list — "an entry holds no incremental state": target/ci-driver, saved under target, holds the driver's incremental state. The driver is built before carry(), with cargo's dev default, and the body's own "What an entry holds" says so.
  • PR body, "Oracle for the limit: actions/cache's own count." — the count is e0ced58's own copy of the archive, not actions/cache's. actions/cache has never saved this entry on Linux, and the command quoted is macOS's gtar.
  • PR body, "The seal bounds what it seals." — "That bound is gone, and so is its addition to the row.": this is branch history that main never had.
  • PR body, the controls' headings and summaries — "(this round)", "(round 3)", "(round 2)", "This round's", "Round 3's", "Round 2's": this is review chronology, and the commit hashes already name each set.

SEND BACK

Japabu and others added 2 commits October 1, 2026 17:56
src/ci.rs conflicted twice, both sides additive, and both are kept
whole: the `use` of `cicache::{self, Start}` beside #674's
`userlandhost::{Host, Os, Program}`, and `carry()` beside #674's
`apps_for`, `verb` and `attempted`. The merged file's changed lines
against main are exactly the branch's, and against the branch exactly
main's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
…save follows only a green run

The bound sums `cicache::PATHS`, the list the save archives, with `~`
taken from HOME: the three `~/.cargo` paths the save stores are counted,
and a target the save does not store is not. That gives `PATHS` the
production reader it lost when `stored()` went.

`cicache::close` runs after the last step of every job that carries the
cache, warm or cold. A cold run is bounded before it seals, so a refused
tree gets no manifest; a warm run is bounded too, so a pull request
whose tree holds more than `LIMIT` reds on its own run, not on the
nightly after it lands. Only the nightly saves, as before. `seal` no
longer counts, and is private to the module.

`each_cache_has_one_writer` reds on any line naming `always()`,
`cancelled()`, `failure()` or `continue-on-error` in a job that saves the
host cache: either would let the save store a tree the seal refused,
above `LIMIT` and with no manifest, which every pull request's read
would then refuse.

`carry()`'s comment loses "in an entry": `target/ci-driver`, saved under
`target`, holds the driver's own incremental state.

LIMIT's tie to the 10 GB, one ratio measured once on run 36878222090, is
filed as issues/build/the-host-caches-limit-reaches-the-10-gb-only-through-one-measured-ratio.md
with its evidence and an exit a gate can fail. The guest-cache issue
records the ceiling H + 2G <= 10 GB leaves the guest entry: 4,002,930,524 B
at LIMIT, 4,298,336,717 B at that run's H.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
@Japabu Japabu changed the title Host cache: read by content, sealed only by a cold run on main, and bounded by the bytes it seals Host cache: read by content, sealed only by a cold run on main, and every carried run bounded by what its save would store Oct 1, 2026
@Japabu Japabu changed the title Host cache: read by content, sealed only by a cold run on main, and every carried run bounded by what its save would store Host cache: read by content, written only by main's cold nightly, and bounded where a tree is sealed Oct 1, 2026
@Japabu

Japabu commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator Author

Re-review, head 792305109; the last review was at f9d535d30. CI: host concluded success at this head (run 36897889906, ubuntu-24.04, 17:14:18Z–17:28:51Z, cold: "Cache not found for input keys: host-sealed-Linux-X64-36897889906, host-sealed-Linux-X64-"). Its cargo test --lib printed "393 passed; 0 failed; 8 ignored", among them ci::tests::each_cache_has_one_writer ... ok and cicache::tests::a_seal_refuses_what_its_save_would_store_above_the_bound ... ok. It ended:

[ci] the tree, bounded as a cache entry: 15976 files, 6720362549 B of the 8000000000 B an entry may hold; sealed: 2230 sources, built on Linux X64 ubuntu24 20260927.320.1, every target dated as built
[ci] Host: 61 step(s), all green

Round 7's BLOCKERs

  • CLOSED as named — nightly.yml:36 and :38, and ci.yml:44: the save past a red run. mS3 (|| true), mS4 (if: success() || true), mS5 (ci.yml's || true) and mS6 (if: Always()) are red, EXIT 101, and each prints the line src/ci.rs:1185-1186 found. The class is still open: the spellings under BLOCKER below keep the test green.
  • CLOSED — src/cicache.rs:211, the warm sum, by ruling 2. seal takes &Cold (src/cicache.rs:198), and host calls it only on Start::Cold (src/ci.rs:567-569), so no warm run reaches the bound. mN, mX, mO, mM, mW, mT, mR, mU and mWk are red on seal_at.

The questions

  • Is the reading sound? No. The job it holds is chosen by text that YAML does not fix, and it refuses three keys by name while passing every other plain key. I traced each patch below through jobs(), the_driver_alone and each_cache_has_one_writer by reading. None was run.

  • The residual. It is recorded in the LIMIT issue, and its exit names two gates that can red. One consequence is missing (NOTE).

  • The body as main's record. Four claims are false until the BLOCKERs close:

    • "a workflow that drops it reds in its own pull request";
    • "holds every host-cache step's path: list";
    • "no folded line, quoted key, alias or anchor hides a step … such a job's verdict is the driver's";
    • Unsure's "each_cache_has_one_writer alone stops such a workflow".

    The chronology is under REMOVE.

  • Since f9d535d30. close and its warm arm are gone. seal_at bounds the tree, then checks sources, dates and writes the manifest. each_under is the one walker, so ruling 4 is closed. the_driver_alone is new. Nothing else is new beyond the findings below.

  • The guest jobs. Ruled to The guest suite is every pull request's guest check, on toolchain stores CI caches by the build system's own keys; a landing during a nightly does not red its release #671. Where that is recorded is a NOTE.

Growth. git diff --shortstat origin/main...HEAD: 11 files, +1046 −35, net +1011.

  • Production Rust: +433 −7, net +426.
  • Tests: +487 −10, net +477.
  • Workflows: +21 −18.
  • Issues: +105.

Since f9d535d30: production +40 −54 (net −14), tests +47 −29 (net +18), issues +16 −7.

BLOCKER

  • src/ci.rs:1157, 1216-1231 — a job is held to the allow-list only if a cache step's key: value starts with a bare host-, and only in a file whose jobs: line is bare. Either patch below takes ci.yml's host out of the test, and mS5 then passes. each_cache_has_one_writer must red on both:
    -          key: host-sealed-${{ runner.os }}-${{ runner.arch }}-${{ github.run_id }}
    +          key: "host-sealed-${{ runner.os }}-${{ runner.arch }}-${{ github.run_id }}"
    -      - run: cargo run -- --ci host
    +      - run: cargo run -- --ci host || true
    -jobs:
    +jobs: # the gate
    -      - run: cargo run -- --ci host
    +      - run: cargo run -- --ci host || true
    • A second nightly writer with a quoted key also passes the second-writer check, because its prefix is "host-sealed-.
    • Require ci.yml's host and nightly.yml's host to be found as the host cache's reader and writer. Refuse a cache key that is not a bare known prefix.
  • src/ci.rs:1170-1191 — the allow-list refuses shell, continue-on-error and a step's own if by name. Every other plain key passes, with any value but run's. So a job keeps its exact run: line while its verdict stops being the driver's, and each_cache_has_one_writer stays green under each patch below.
    • nightly.yml's host, mD respelled. The driver builds in target/debug, and nothing reads, bounds or seals. The save then stores an unbounded tree with no manifest, and every pull request's read refuses it (src/cicache.rs:129-135):

      -    env:
      +    outputs:
             CARGO_TARGET_DIR: target/ci-driver
    • ci.yml's host. Cargo hands the driver to true, and the required check is green having run nothing:

           env:
             CARGO_TARGET_DIR: target/ci-driver
      +      CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUNNER: "true"
    • nightly.yml: the first hunk is on host, and again is a new job after it. That makes a second writer, racing host for the same key, with no CARGO_TARGET_DIR. It refutes "no alias, anchor … hides a step from this reader" (src/ci.rs:1166-1168):

      @@ host
      -    steps:
      +    steps: &host-steps
      @@ after host's last step
      +  again:
      +    runs-on: ubuntu-24.04
      +    steps: *host-steps
    • ci.yml: delete - run: cargo run -- --ci host. Nothing requires a run step.

    • Allow only the keys these jobs use:

      • job: if, runs-on, timeout-minutes, env, steps;
      • env: CARGO_TARGET_DIR alone;
      • step: uses naming checkout, cache/restore or cache/save, with, run;
      • with: fetch-depth, path, key, restore-keys.

      Also require exactly one run:, refuse any value that starts with & or *, and refuse a workflow-level env:.

NOTE

REMOVE

  • PR body, the paragraph "The licence step is main's." — it narrates a sparse fetch main never had, and this merge leaves the step as main has it.
  • PR body, "What an entry holds": "since run 3", and "since e0ced58. On Linux the tree had doubled:" — branch chronology.
  • PR body, Gates, b4dfda5: "Its seal summed every target under the root: …" — main never has that seal. This head's run measures the seal that lands.

SEND BACK

Japabu and others added 2 commits October 1, 2026 20:05
…the host entry seals it

`each_cache_has_one_writer` read the workflows as text, and three rounds
found spellings that escaped it: a quoted key, a comment after `jobs:`, an
unknown job key, a runner variable in `env:`, an anchor and an alias, a
deleted run step.

- `src/workflow.rs` reads every workflow with yaml-rust2's event parser.
  An alias is resolved as GitHub resolves it, and every node an anchor, an
  alias or a tag made is marked. A key given twice, a key that is not a
  plain scalar, and anything but one document are refused. It is compiled
  for tests alone, beside its two tests, and its accessors are what another
  gate over `.github/workflows/` calls.
- yaml-rust2 0.13, as a dev-dependency with its encoding feature off: the
  pure-Rust YAML 1.2 parser with 61,444,840 downloads on crates.io, whose
  events carry the anchors, aliases and tags a loader resolves away.
- The gate works on that structure. Every cache step's key and restore
  keys must begin, up to their first expression, with a known cache's
  prefix, and the combined action is refused. The jobs that name the host
  cache must be exactly ci.yml's `host`, the reader, and nightly.yml's
  `host`, the writer. Each is held to a closed allow-list: its workflow's
  keys, its own keys, its `if:`, `env:` with `CARGO_TARGET_DIR` alone, and
  its steps in order as checkout, restore and the run, or checkout, the run
  and save. No step has a condition of its own, no anchor, alias or tag is
  in the job, and the host cache's paths, key and restore key are exact.
- Only the writer seals. `cargo run -- --ci seal` is `host` from a cold
  tree, which it refuses if anything was restored, then sealed and bounded
  by `LIMIT`. nightly.yml's `host` runs it, and ci.yml's `host` runs
  `--ci host`, whose read never makes a `Cold` and so never seals. A pull
  request's run, warm or cold, is never refused by `LIMIT`.
- The LIMIT issue's residual says what is true now. The guest-cache issue
  records that its jobs are not held to the allow-list, and its exit names
  that allow-list. A new issue records the two workflow gates that still
  read text, with the two patches that pass them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
…s a workflow's extension in any case

- An alias copies a node its anchor marked, so the line that marked the copy
  again was dead: with it deleted, `a_workflow_is_its_structure_and_not_its_spelling`
  was green (EXIT 0), as it is without the deletion.
- `.yml` and `.yaml` are matched in any case, so the files read are a
  superset of those GitHub reads.
- `parse` and `Value` are private: a gate reads through `all` and `Node`'s
  accessors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
@Japabu Japabu changed the title Host cache: read by content, written only by main's cold nightly, and bounded where a tree is sealed Host cache: read by content, sealed and bounded only by main's cold nightly, and gated on the workflows read as YAML Oct 1, 2026
@Japabu

Japabu commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator Author

Re-review, head f7e0bd4b5; the last review was at 792305109. CI: host was still in progress when this review began, and it concluded success during it (run 36906786717, ubuntu-24.04, 18:26:05Z–18:39:38Z).

  • The run was cold: "Cache not found for input keys: host-sealed-Linux-X64-36906786717, host-sealed-Linux-X64-" (log line 146).
  • Its cargo test --lib printed "395 passed; 0 failed; 8 ignored". Among them were ci::tests::each_cache_has_one_writer ... ok and both workflow::tests.
  • The driver printed (log lines 309 and 8496):
[ci] the cache entry, read by content: none restored: the run is cold
[ci] Host: 60 step(s), all green

Round 8's BLOCKERs

  • CLOSED: src/ci.rs:1157 and 1216-1231 at 7923051, a job chosen by text.
    • R1 (quoted key) and R2 (jobs: # the gate) are red, EXIT 101.
    • Both host jobs must be found (src/ci.rs:1321), and a key whose head is not a known prefix is refused (mHead red).
    • A job still escapes the classifier through how its uses: is spelled: BLOCKER 1.
  • CLOSED: src/ci.rs:1170-1191 at 7923051, an allow-list by name.
    • The workflow, the job, env:, each step and each with: are now closed lists, and the steps are pinned in order (src/ci.rs:1199-1293).
    • R3–R6, mEnvW, mEnvS, mRef, mIf, mTag and mDup are red, EXIT 101.
    • One character still hides any key from the reader: BLOCKER 2.

The questions

  • Spellings of my own. Two pass. I traced each by reading the sources; I ran neither.

    • GitHub's converters split a uses: path on / and \ and drop empty segments, and action() does not.
    • GitHub's parser breaks lines where yaml-rust2 does not.
    • These are refused, or read the same way by both parsers:
      • a merge key <<, which GitHub's plain YamlDotNet Parser also reads as a key;
      • a cache key led by an expression;
      • a reusable workflow in this repository;
      • a cache step behind an alias.
  • Does the allow-list hold everything that decides the verdict? Within the workflow, yes, apart from BLOCKER 2.

    • The values it leaves free are runs-on, timeout-minutes, fetch-depth and concurrency. They can red a job or stall it, but cannot turn it green over a red driver.
    • ci.yml's on: is held only by the text gate the filed issue names.
    • Two things outside the allow-list matter: a root .cargo/config.toml, and nightly's on: (NOTEs).
  • The dependency. It is the cleanest choice under the implementer rule, and it is taken only where it is needed.

    • No lockfile in the tree holds a YAML crate.
    • Its event API is what the anchor, alias and tag refusals need.
    • It is a dev-dependency with encoding off, used behind #[cfg(test)], and the workspace shape's --all-targets lints it.
    • Cargo builds a dev-dependency for every test target of the root package, the QEMU harness included. Cargo has no narrower scope.
    • Its line breaks are YAML 1.2's, and GitHub's are not (BLOCKER 2).
  • --ci seal. By reading, it is right on all four counts:

    • One writer, apart from BLOCKER 1.
    • Cold only: start refuses every target but the driver's, and a manifest (src/cicache.rs:180-191; mSt red).
    • Bounded: seal_at and restored are byte-identical to 7923051's. That head's run bounded the Linux cold tree at 6,720,362,549 B.
    • No step a red can skip: the save is the only step after the run, and it has no condition. Inside the driver the seal runs even after a red step; that is harmless, because the job is red and the save is skipped.
    • But it has never run, and nothing tests the route into it (BLOCKER 3).
  • The "no seal" route. This head's cold run proves it.

    • The log shows "none restored: the run is cold" and no seal step.
    • It ran 60 steps, against 7923051's 61.
    • A warm read takes the same branch (src/ci.rs:497-505).
  • The filed issue. It is right about workflows_run_against_main_on_hosted_runners, and its evidence reaches that gate. The rest is a NOTE.

  • The body. It is false where BLOCKERs 1 and 2 reach:

    • "every step that uses actions/cache/restore or actions/cache/save, in any case, names one cache";
    • "no such spelling is known here".

    Four more lines are under REMOVE.

  • Since 792305109. Nothing is new beyond the findings below.

Growth. git diff --shortstat origin/main...HEAD: 15 files, +1432 −47, net +1385.

  • Production Rust: +455 −9, net +446.
  • Test-only Rust, including all of src/workflow.rs: +790 −20, net +770.
  • Workflows: +21 −18.
  • Cargo.toml +3, Cargo.lock +35.
  • Issues: +128.

Since 792305109:

  • production: +70 −52, net +18;
  • test-only: +395 −100, net +295;
  • Cargo.lock +35 and Cargo.toml +3;
  • issues +31 −8.

I accept the growth on rulings 1 and 2.

BLOCKER

  1. src/ci.rs:1207-1224 — action() lowercases the uses: path, but it does not split the path as GitHub does.
    • GitHub runs these as actions/cache with path save: actions/cache/save/@…, actions//cache/save@… and actions\cache\save@…. action() sends each to _ => continue, and the combined action spelled actions/cache/@… goes the same way.
    • The source: actions/runner src/Sdk/DTPipelines/Pipelines/ObjectTemplating/PipelineTemplateConverter.cs:629-645 and src/Sdk/WorkflowParser/Conversion/WorkflowTemplateConverter.cs:1771-1772 split the path on / and \ with StringSplitOptions.RemoveEmptyEntries.
    • So a third writer of the host cache passes. each_cache_has_one_writer must red on this patch, and on the same step spelled actions/cache/@ and actions\cache\save@:
      @@ nightly.yml, portability-linux, after `- *checkout`
      +      - uses: actions/cache/save/@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
      +        with:
      +          path: target
      +          key: host-sealed-${{ runner.os }}-${{ runner.arch }}-${{ github.run_id }}
    • The fix: split the path as GitHub does, and refuse every actions/cache path but restore and save.
  2. src/workflow.rs:93-95 — the parser's line breaks are not GitHub's.
    • yaml-rust2 0.13 breaks a line only at \n and \r (char_traits.rs:11-12), and a comment runs to that break (scanner.rs:857-860).
    • actions/runner's workflow parser reads with YamlDotNet's Parser (src/Sdk/WorkflowParser/Conversion/YamlObjectReader.cs:10-27). It pins YamlDotNet.Signed 5.3.0 (src/Sdk/Sdk.csproj:29), whose IsBreak is "\r\n\x85\x2028\x2029" (CharacterAnalyzer.cs:148-151). A comment ends there too (Scanner.cs:580-599).
    • On the patch below to ci.yml:44, where <U+2028> is the one character, yaml-rust2 reads one key and GitHub reads two. The required check then goes green over a red driver.
    • The same character followed by - run: … in nightly's seal line adds a step between the seal and the save.
    • each_cache_has_one_writer must red on this patch:
      -      - run: cargo run -- --ci host
      +      - run: cargo run -- --ci host #<U+2028>        continue-on-error: true
    • The fix: refuse U+0085, U+2028 and U+2029 in workflow::parse, with a row in a_document_the_reader_cannot_hold_is_refused.
  3. src/ci.rs:77 — Job::Seal => host(root, true) has no test and no run.
    • Its only caller is nightly's host, which runs on main alone. The body measures the seal only through --ci host at 7923051.
    • This patch keeps every test and every pull request's run green. It makes the one writer save an unsealed, unbounded tree, which every read then refuses:
      -        Job::Seal => host(root, true),
      +        Job::Seal => host(root, false),
    • A cheap measurement tells the two routes apart without running the suite. Build the driver in target/ci-driver and set RUNNER_OS, RUNNER_ARCH, ImageOS and ImageVersion, on a tree that holds target/debug:
      • --ci seal must red at its first step with "its writer restores nothing";
      • --ci host must red with "restored without target/ci-sources";
      • under the patch, --ci seal must print the latter.
    • Put the commands, exit codes and lines in the body, or add a test that reds under the patch.

NOTE

  • .cargo/config.toml (absent at the root) — adding [target.x86_64-unknown-linux-gnu] runner = "true" there does to ci.yml's run what R4's env: line did, and no gate reads that file.
  • .github/workflows/nightly.yml:8-16 — on: and concurrency: are free, yet they decide how many host entries a night writes.
    • The LIMIT issue's 2H + G assumes one entry a night.
    • A push: beside schedule: keeps each_cache_has_one_writer green and writes an entry per landing.
  • src/ci.rs:1284 — runs-on is free in both jobs. If the writer runs on another image than the reader, every read deletes the entry and runs cold, and nothing reds. Hold the two equal.
  • issues/build/two-workflow-gates-read-the-workflows-as-text.md:9-17 — this issue is incomplete:
    • A third gate reads the workflows as text: src/hostws.rs:439.
    • Its patches are on publish.yml, which the_required_check_is_a_job_on_every_pull_request never reads.
    • It names no owner.
    • Its exit names a means, not a red. The exit a test can fail is each patch turning its test red.
  • issues/build/ — four of the five issue files this branch adds name no owner. Only the LIMIT issue does.
  • PR body, Gates — this head's run 36906786717 is the measurement for "a pull request's run, warm or cold, never seals" (log lines 309 and 8496), and the body does not cite it.

REMOVE

  • PR body, "The workflows are read as YAML": "and any gate over the workflows reads them through workflow::all and Node's accessors, as The guest suite is every pull request's guest check, on toolchain stores CI caches by the build system's own keys; a landing during a nightly does not red its release #671's are to" — false at this head, since src/ci.rs:1140, src/ci.rs:1151 and src/hostws.rs:439 read text. It also speaks for another pull request.
  • PR body, High-risk checks: "Oracle for the gate: yaml-rust2 …" — yaml-rust2 is not the reader GitHub runs, and BLOCKER 2 is a document that the two read differently.
  • PR body, Unsure: "no such spelling is known here" — BLOCKER 2 is one.
  • src/workflow.rs:6, and the same clause in the body's "The workflows are read as YAML": "a key that is not a plain scalar" — a quoted key is not plain, yet it is taken ("host": in this module's own test). What is refused is a marked key or a collection key.

SEND BACK

Japabu and others added 4 commits October 1, 2026 21:04
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
… the cold start, host and the seal

The workflow gate goes, all of it: src/workflow.rs, the yaml-rust2
dev-dependency and its lock entries, and src/ci.rs's
`each_cache_has_one_writer` (main's text reading of it too), with
`the_driver_alone`, `host_step`, `caches`, `action`, `keys` and their
constants. Round 9's review found two more spellings that GitHub's
reader takes and the gate's did not: a `uses:` path GitHub splits on
`/` and `\` and drops empty segments of, and U+2028, U+0085 and U+2029,
which YamlDotNet breaks a line at and yaml-rust2 does not. A gate that
re-reads GitHub's YAML lags GitHub's reader by construction.

Its rules are now sentences in `.claude/agents/reviewer.md`'s new
**Caches** bullet, read off the diff: one writer per cache, in
nightly.yml; the host cache's writer nightly's `host` and its reader
ci.yml's `host`, on one `runs-on` and both caching `PATHS`; those jobs
run checkout, their cache step and the driver alone; the save's
`github.ref == 'refs/heads/main'` guard is their only step-level `if:`,
and ci.yml's `host` skips only a draft; no `continue-on-error`,
`shell:`, `defaults:` or cargo `runner` reaches them; nightly's `on:`
is one daily schedule and `workflow_dispatch`. The last three come from
the review's NOTEs: a root `.cargo/config.toml` runner, a free
`runs-on`, and a free `on:` were each invisible to the gate. Code stays
where reading cannot see: the seal, its bound, the manifest and its
refusals in src/cicache.rs.

The Tests bullet takes the owner's two principles: a mistake a type
makes unrepresentable needs no test, and a rule a reviewer can check by
reading lives in the prompt, a gate being code only for what reading
cannot see.

`--ci seal` is `ci::seal`: the cold start, then `host`, then the seal,
as calls, so no bool decides whether the one writer seals. `host` reads
the cache in every job that carries it, so in the seal job it reads the
tree the cold start just proved empty and says the run is cold.
`cicache::cold` refuses a driver built outside `target/ci-driver`, the
check `host` made for the seal before.

nightly.yml's save carries the main-only guard again, the only
step-level `if:` the rule allows, and the job carries none: a dispatch
on a branch runs the cold build and the seal, bounded, and saves
nothing.

`cicache::SEALED` was read by the gate alone and goes; `PATHS` is
private to the bound that reads it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
…ctions or go, and each cache issue names its owner

`issues/build/two-workflow-gates-read-the-workflows-as-text.md` is
replaced by
`issues/build/the-workflow-gates-that-read-workflows-as-text-become-reviewer-instructions-or-go.md`.
It names the third gate, src/hostws.rs's
`nothing_that_runs_names_a_target_directory_a_member_does_not_have`,
gives each of the three a patch that reaches it, its owner (the track
`issues/build/the-tooling-is-a-review-prompt-and-three-workflows.md`),
and an exit a test can fail: each is deleted with its rule in the
review prompt, or reds on its patch.

Measured on 038da72. Each patch was applied with `git apply --check`
then `git apply`, built with `cargo test --lib --no-run` (EXIT=0), the
three tests run with `--exact`, and reverted with `git apply -R`; the
tree matched its state before after every one:

  patch                                          hosted  required  member-target
  publish.yml `runs-on:` label on the next line    0       0          0
  publish.yml `pull_request: {branches: [dev]}`    0       0          0
  ci.yml `host`'s `if: false`                      0       0          0
  publish.yml `run: "ls userland/sshd/targe\x74"`  0       0          0
  publish.yml `run: ls userland/sshd/target`       0       0        101

The last is the positive control: "publish.yml: names
userland/sshd/target". Each run selected one test ("1 passed; 400
filtered out", or "1 failed").

The guest cache's issue loses its paragraph on the deleted allow-list
and the exit clause that named it. The two warm-read issues and the
guest cache's name their owners, as the limit's does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
The deleted gate held each job that names the host cache to
`CARGO_TARGET_DIR: target/ci-driver`, and the review sentence did not.
Without it a reader is not carried: it runs its restored tree judged by
cargo's mtimes alone, with no read by content and no image check, and
nothing at run time says so. The writer without it is refused by
`cicache::cold`, so only the reader needed the sentence, but it holds
both, as the gate did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
@Japabu

Japabu commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator Author

Patches the body of this pull request rests on.

The workflow-gate issue's evidence, measured on 038da72: each applied with git apply --check then git apply, built (cargo test --lib --no-run, EXIT=0), the three text gates run with --exact, and reverted with git apply -R; the tree matched its prior state after each.

=== P1a-runs-on-next-line
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -23,7 +23,8 @@ permissions:
 
 jobs:
   publish:
-    runs-on: ubuntu-latest
+    runs-on:
+      - self-hosted
     timeout-minutes: 30
     steps:
       # No submodules: `cargo publish` walks the repository's vcs state, and an
=== P1b-pull-request-flow
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -8,6 +8,7 @@ on:
   push:
     branches: [main]
   workflow_dispatch: {}
+  pull_request: {branches: [dev]}
 
 # Never two publishers, and never cancel one: a cancelled `cargo publish` may
 # have already taken the version.
=== P2-host-if-false
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -16,5 +16,5 @@ concurrency:
 jobs:
   host:
-    if: github.event_name == 'merge_group' || github.event.pull_request.draft == false
+    if: false
     runs-on: ubuntu-24.04
     timeout-minutes: 45
=== P3-dead-target-escaped
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -31,5 +31,7 @@ jobs:
       - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
 
+      - run: "ls userland/sshd/targe\x74"
+
       - id: auth
         uses: rust-lang/crates-io-auth-action@c6f97d42243bad5fab37ca0427f495c86d5b1a18 # v1.0.5
 
=== P3plain-dead-target-plain
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -31,5 +31,7 @@ jobs:
       - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
 
+      - run: ls userland/sshd/target
+
       - id: auth
         uses: rust-lang/crates-io-auth-action@c6f97d42243bad5fab37ca0427f495c86d5b1a18 # v1.0.5
 

The negative controls on src/cicache.rs and ci::carry, measured on f7e0bd4, changed lines. None of these lines differs at c282a76.

=== mSt-writer-takes-a-restored-tree src/cicache.rs
-    let restored = restored(root)?;
-    if !restored.is_empty() {
-        return Err(format!("{}: an entry is one cold build, and its writer restores nothing", listed(&restored)));
-    }
=== mN-no-bound src/cicache.rs
-    if bytes > LIMIT {
-        return Err(format!(
-            "{bytes} B in {files} files under the cache's paths, above the {LIMIT} B an entry may \
-             hold: saved, it could evict the guest entry or the next host one"
-        ));
-    }
=== mX-one-above-passes src/cicache.rs
-    if bytes > LIMIT {
+    if bytes > LIMIT + 1 {
=== mO-at-bound-refused src/cicache.rs
-    if bytes > LIMIT {
+    if bytes >= LIMIT {
=== mM-largest-not-sum src/cicache.rs
-                bytes += meta.len();
+                bytes = bytes.max(meta.len());
=== mW-bound-after-manifest src/cicache.rs
-    if bytes > LIMIT {
-        return Err(format!(
-            "{bytes} B in {files} files under the cache's paths, above the {LIMIT} B an entry may \
-             hold: saved, it could evict the guest entry or the next host one"
-        ));
-    }
+    if bytes > LIMIT {
+        return Err(format!(
+            "{bytes} B in {files} files under the cache's paths, above the {LIMIT} B an entry may \
+             hold: saved, it could evict the guest entry or the next host one"
+        ));
+    }
=== mT-every-target-not-PATHS src/cicache.rs
-    for path in PATHS {
-        let dir = match path.strip_prefix("~/") {
-            Some(under) => home.join(under),
-            None => root.join(path),
-        };
-        // A path the save finds nothing at stores nothing.
-        if !dir.try_exists().map_err(|e| format!("{}: {e}", dir.display()))? {
-            continue;
-        }
+    let _ = home;
+    for dir in targets(root)? {
=== mR-home-from-root src/cicache.rs
-            Some(under) => home.join(under),
+            Some(under) => root.join(under),
=== mU-missing-path-read src/cicache.rs
-        // A path the save finds nothing at stores nothing.
-        if !dir.try_exists().map_err(|e| format!("{}: {e}", dir.display()))? {
-            continue;
-        }
=== mWk-walker-enters-nothing src/cicache.rs
-        if meta.is_dir() {
-            each_under(&entry.path(), visit)?;
-        }
=== m1-source-existence-only src/cicache.rs
-            now.get(*path) != Some(*hash) || modified(&root.join(path)).ok() != Some(built())
+            now.get(*path) != Some(*hash) || modified(&root.join(path)).is_err()
=== m11-seal-dates-no-file src/cicache.rs
-        each_under(&target, &mut |path, _| date(path, built()))?;
+        each_under(&target, &mut |_, _| Ok(()))?;
=== m2-root-target-dropped src/cicache.rs
-            if path != root.join(DRIVER) {
-                found.push(path);
-            }
+            let _ = path;
=== m4-no-clock-check src/cicache.rs
-    if now <= built() {
+    if false && now <= built() {
=== m5-no-runner-check src/cicache.rs
-    if built_on != runner {
+    if false && built_on != runner {
=== m6-file-dated-alone src/cicache.rs
-    let mut dirty: BTreeSet<Option<String>> = current
-        .iter()
-        .filter(|(path, hash)| entry.get(*path) != Some(*hash))
-        .map(|(path, _)| path)
-        .chain(entry.keys().filter(|path| !current.contains_key(*path)))
+    let mut dirty: BTreeSet<Option<String>> = entry
+        .keys()
+        .filter(|path| !current.contains_key(*path))
-        let fresh = entry.get(path) == Some(hash) && !dirty.contains(&package(path, &current));
+        let fresh = entry.get(path) == Some(hash) && !dirty.contains(&Some(path.clone()));
=== m7-every-hash-empty src/cicache.rs
-        sources.insert(path, format!("{:x}", Sha256::digest(bytes)));
+        sources.insert(path, format!("{:x}", Sha256::digest(&bytes[..0])));
=== m9-only-matched-dated src/cicache.rs
-        date(&root.join(path), if fresh { built() } else { now })?;
+        if fresh {
+            date(&root.join(path), built())?;
+        }
=== m10-every-file-dated-old src/cicache.rs
-        date(&root.join(path), if fresh { built() } else { now })?;
+        date(&root.join(path), built())?;
=== m12-manifest-kept-on-read src/cicache.rs
-    fs::remove_file(&manifest).map_err(|e| format!("remove {MANIFEST}: {e}"))?;
=== m19-removed-file-dirties-nothing src/cicache.rs
-        .chain(entry.keys().filter(|path| !current.contains_key(*path)))
=== m14a-no-RUNNER_OS src/cicache.rs
-    let values = ["RUNNER_OS", "RUNNER_ARCH", "ImageOS", "ImageVersion"]
+    let values = ["RUNNER_ARCH", "ImageOS", "ImageVersion"]
=== m14b-no-RUNNER_ARCH src/cicache.rs
-    let values = ["RUNNER_OS", "RUNNER_ARCH", "ImageOS", "ImageVersion"]
+    let values = ["RUNNER_OS", "ImageOS", "ImageVersion"]
=== m14c-no-ImageOS src/cicache.rs
-    let values = ["RUNNER_OS", "RUNNER_ARCH", "ImageOS", "ImageVersion"]
+    let values = ["RUNNER_OS", "RUNNER_ARCH", "ImageVersion"]
=== m14d-no-ImageVersion src/cicache.rs
-    let values = ["RUNNER_OS", "RUNNER_ARCH", "ImageOS", "ImageVersion"]
+    let values = ["RUNNER_OS", "RUNNER_ARCH", "ImageOS"]
=== m15-unset-read-as-empty src/cicache.rs
-        .map(|name| var(name).ok_or_else(|| format!("{name} is unset: a hosted runner sets it")));
+        .map(|name| Ok::<_, String>(var(name).unwrap_or_default()));
=== m16-no-build-time-dating src/cicache.rs
-        if runs_at_build(root, manifest, &current)? {
+        if false && runs_at_build(root, manifest, &current)? {
=== m17-build-scripts-only src/cicache.rs
-    Ok(script || proc_macro)
+    let _ = proc_macro;
+    Ok(script)
=== m18-proc-macros-only src/cicache.rs
-    Ok(script || proc_macro)
+    let _ = script;
+    Ok(proc_macro)
=== mJ-package-table-alone src/cicache.rs
-    let package = doc.get("package").or_else(|| doc.get("project"));
+    let package = doc.get("package");
=== mA1-any-driver-carries src/cicache.rs
-        Ok(driver) => exe.starts_with(driver),
+        Ok(_) => true,
=== mA2-no-driver-dir-carries src/cicache.rs
-        Err(e) if e.kind() == ErrorKind::NotFound => false,
+        Err(e) if e.kind() == ErrorKind::NotFound => true,
=== mB-no-driver-carries src/cicache.rs
-        Ok(driver) => exe.starts_with(driver),
+        Ok(_) => false,
=== mF-carry-keeps-target-dir src/ci.rs
-    std::env::remove_var("CARGO_TARGET_DIR");
=== mI-carry-keeps-incremental src/ci.rs
-    std::env::set_var("CARGO_INCREMENTAL", "0");
=== mG-carry-keeps-debuginfo src/ci.rs
-    std::env::set_var("CARGO_PROFILE_DEV_DEBUG", "line-tables-only");

@Japabu Japabu changed the title Host cache: read by content, sealed and bounded only by main's cold nightly, and gated on the workflows read as YAML Host cache: read by content, written by main's cold nightly alone, sealed and bounded, with its workflow rules in the review prompt Oct 1, 2026
@Japabu

Japabu commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator Author

Re-review, head c282a7669; the last review was at f7e0bd4b5. 51fe187af merges main's #673 clean: the branch's own diff against its base is the same before and after it, index and hunk lines aside.

CI: host was in progress when this review began and concluded success during it (run 36913211608, ubuntu-24.04, 19:17:44Z–19:31:37Z). The run was cold:

Cache not found for input keys: host-sealed-Linux-X64-36913211608, host-sealed-Linux-X64-   (log line 146)
[ci] the cache entry, read by content: none restored: the run is cold                     (309)
[ci] Host: 60 step(s), all green                                                           (8489)

Round 9's BLOCKERs

  • CLOSED: src/ci.rs:1207-1224 at f7e0bd4, action() against GitHub's uses: path split.
    • The function, the gate and src/workflow.rs are deleted.
    • git grep at c282a76 finds no workflow::, yaml-rust2, each_cache_has_one_writer or SEALED.
    • Nothing claims any more to read the workflows as GitHub does.
  • CLOSED: src/workflow.rs:93-95 at f7e0bd4, yaml-rust2's line breaks. They went with the reader. A U+2028 is just as invisible to a reviewer reading the diff, and no instruction names it.
  • CLOSED: src/ci.rs:77 at f7e0bd4, Job::Seal => host(root, true). The bool is gone, so round 9's patch no longer compiles.
    • seal (src/ci.rs:571-582) runs cicache::cold and returns on its refusal. Then it runs host, then cicache::seal.
    • Cold's fields are private (src/cicache.rs:81-84), so nothing seals without cold.
    • The patch Job::Seal => host(root) leaves the private ci::seal with no caller. The workspace shape's -D warnings (src/clippy.rs:68-72) refuses that dead code.
    • This is a reading, not a run: --ci seal has still never run, as the body's Unsure says.

The questions

  • The seal job. Right by reading.
    • The cold step runs before host sets TMPDIR and calls carry, and depends on neither.
    • host's read in that job finds no manifest and no restored target, because cold has just refused both. It says the run is cold.
    • The seal still runs after a red step. The save's implicit success() then stores nothing.
    • nightly.yml's guard moved from the job to the save, so a dispatch on a branch now seals, bounded, and saves nothing.
  • The reviewer.md sentences.
    • The Caches bullet is true of both workflows at this head, and each of its clauses can be checked against a diff. Three things the deleted gate pinned are missing from it (NOTE).
    • The Tests bullet's second new sentence contradicts two lines the prompt keeps (BLOCKER). Its first repeats the Guest tests bullet (NOTE).
  • The body. True at c282a76, apart from the two REMOVEs. Checked:
    • the shortstat and its split;
    • the three tracked .cargo/config.toml files hold no runner;
    • src/CLAUDE.md:17 says what the body cites it for;
    • seal_at is unchanged since 7923051;
    • each of the 69 lines the control patches remove is at c282a76 verbatim.
  • Anything new. Nothing beyond the findings below.

Growth. git diff --shortstat origin/main...HEAD: 13 files, +1036 −54, net +982.

  • Production Rust: +448 −7, net +441.
    • src/cicache.rs 390;
    • src/ci.rs +50 −2;
    • src/clippy.rs +4 −1;
    • src/identity.rs +3 −4;
    • src/lib.rs +1.
  • Test Rust: +416 −29, net +387.
  • Workflows +21 −18, reviewer.md +12, issues +139.

Since f7e0bd4b5 (51fe187af..c282a7669): +75 −478.

  • Production Rust: +23 −26.
  • Test-only Rust: −385 (src/workflow.rs 225, ci::tests 157, src/lib.rs 3).
  • Cargo.toml −3 and Cargo.lock −35.
  • nightly.yml +6 −6, reviewer.md +12, issues +34 −23.

The production growth stands on round 9's acceptance.

BLOCKER

  • .claude/agents/reviewer.md:106-107 — the new sentence says a rule a reviewer can check by reading the diff "lives in this prompt, not in a gate", and two lines the prompt keeps say otherwise — one prompt now gives opposite verdicts on the same cut, so :96 and :34 must each name the case :106 makes.
    • :95-96 lets a test be cut "only when it tests nothing, or as Guest tests says". That sends back this branch's own cut of main's ci::tests::each_cache_has_one_writer, which reds on a plain second actions/cache/save@ writer, and every cut the new issue's exit allows.
    • :33-34 still asks, of a mutation a workflow survives, for "the test it must turn red", as round 9 did.

NOTE

  • .claude/agents/reviewer.md:83-90 — three things the deleted gate pinned are free here, and the first two fail silently:
    • nightly host's own job-level if:: an if: false stops the one writer, and a skipped job is a green check;
    • both jobs' env: beyond CARGO_TARGET_DIR: runner() reads ImageOS and ImageVersion from it, so an ImageVersion set in both outlives an image move and the image check passes;
    • which <job> each runs: a writer on --ci host saves no manifest, and every pull request then reds.
  • .claude/agents/reviewer.md:104-105 — "A mistake a type makes unrepresentable needs no test" repeats the Guest tests bullet's first tier (:112-113) and root CLAUDE.md's "Unrepresentable is best". What is new is the rest: a review asks for no test of a mutation that does not compile.

REMOVE

  • PR body, "Gone:" — src/workflow.rs, "the yaml-rust2 dev-dependency with its Cargo.lock entries (it, arraydeque, hashlink and hashbrown 0.17)", cicache::SEALED and "Cargo.toml and Cargo.lock are main's." — main never had any of them, so as main's record they are this branch's history.
  • PR body, the Caches line: ", so a night writes one entry, as the limit's arithmetic assumes" — the rule allows workflow_dispatch, and a dispatch on main writes a second entry that day.

SEND BACK

@Japabu
Japabu marked this pull request as draft October 2, 2026 07:33
Japabu and others added 2 commits October 2, 2026 09:36
Three files conflicted or needed adapting.

.claude/agents/reviewer.md: main's text is taken whole, and this branch's
Caches bullet is kept, between "What no gate reads" and "Growth". The two
sentences this branch added to the Tests bullet are dropped, because main now
states both in its own words: "Rank every finding" names no mutation a type
refuses or a reader of the diff catches, and "Growth" sends back a new gate,
check, lock or test that guards what a reader can check. The round 10 BLOCKER
was against the second of those sentences, which this branch no longer adds.

src/ci.rs: both imports are kept, this branch's `cicache` and main's
`sysroot::git_out`.

src/cicache.rs: main deleted `src/sync.rs`, and with it `sync::git`. The seal
reads HEAD through `sysroot::git_out`, as main's `ci::publish` now does, and
trims it; a git that cannot answer panics instead of returning the refusal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
…ne's job

Round 10's review names three things the deleted workflow gate pinned and the
Caches bullet did not, each one a line a reviewer reads off a diff:

- nightly's `host` has no job-level `if:`. An `if: false` there stops the one
  writer, and a skipped job is a green check.
- `CARGO_TARGET_DIR` is the one variable an `env:` gives either job.
  `cicache::runner` reads `ImageOS` and `ImageVersion` from the environment, so
  one set in both jobs outlives an image move and the image check passes.
- the writer runs `--ci seal` and the reader `--ci host`. A writer on
  `--ci host` saves no manifest, and every pull request then refuses the
  targets it restored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
@Japabu

Japabu commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

Round 11's mutation patch, measured on 883adf1: applied with git apply --check then git apply, built (cargo test --lib --no-run, EXIT=0), run (cargo test --lib -- cicache::, EXIT=101, 5 of 10 red), and reverted with git apply -R; git status --porcelain was empty after it.

=== mH-head-untrimmed
diff --git a/src/cicache.rs b/src/cicache.rs
index 85192dd10..f5edac0ba 100644
--- a/src/cicache.rs
+++ b/src/cicache.rs
@@ -247,7 +247,7 @@ fn seal_at(root: &Path, home: &Path, cold: &Cold) -> Result<String, String> {
         date(&target, built())?;
     }
     let head = crate::sysroot::git_out(root, &["rev-parse", "HEAD"]);
-    let mut text = format!("{}\n{}\n", head.trim(), cold.runner);
+    let mut text = format!("{}\n{}\n", head, cold.runner);
     for (path, hash) in &cold.sources {
         text.push_str(&format!("{hash} {path}\n"));
     }

Red: a_reader_whose_clock_is_not_after_the_entry_is_refused, an_entry_built_on_another_runner_is_deleted, a_file_cargo_was_never_told_about_rebuilds_its_package, an_entry_serves_exactly_the_sources_it_was_built_from and code_run_at_build_time_runs_again_on_every_read, each on target/ci-sources holds "macOS ARM64 macos15 20260928.1".

@Japabu

Japabu commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

Re-review, head 883adf16a; the last review was at c282a7669. Since then: 2683ca2bb, the merge of main's #678 (de5f63cc2), conflicted in .claude/agents/reviewer.md and src/ci.rs; and 883adf16a, the Caches bullet (+8 −5).

Round 10's BLOCKER

  • CLOSED: .claude/agents/reviewer.md:106-107 at c282a76, the sentence that gave one prompt two verdicts on one cut. The branch no longer adds it: diff of de5f63cc2's reviewer.md against 883adf16a's is 11 added lines, the Caches bullet at :79-89, and no line removed or changed.
    • :33-34's case is now main's own, at :28-29: no mutation is named that "a reader of the diff catches".
    • :95-96's case is named by nothing in this prompt. The sentence is main's, unchanged, at :96-97, so what it says of the cut is the first NOTE, and no longer a contradiction this branch adds.

The merge, read against both parents

  • reviewer.md: as above, main's text whole.
  • src/ci.rs:31-34: both imports. git grep for sync::git, crate::sync and mod sync in src/ at the head finds nothing.
  • src/cicache.rs:249-250: HEAD through sysroot::git_out, trimmed. mH on 883adf1 (669-r11/mH.log): APPLY_CHECK=0, BUILD_EXIT=0, TEST_EXIT=101 with 5 of 10 cicache:: tests red on target/ci-sources holds "macOS ARM64 macos15 20260928.1", RESTORE=0, porcelain empty.
  • git diff c282a7669 883adf16a over the branch's files: those two lines of cicache.rs, the Caches bullet, and main's own hunks in ci.rs and lib.rs. Nothing else of the branch moved.
  • src/main.rs at the head reaches ci::dispatch before anything writes under target/, so restored() sees a runner's tree as run 36913211608 did.

Evidence at 883adf1

  • cargo run -- --ci host: EXIT=0 (669-r11/host.exit) at 883adf16a (host.head); "[ci] Host: 59 step(s), all green" (host.log:6820); lib "373 passed; 0 failed; 8 ignored" (:513); the ten cicache::tests and ci::tests::a_step_of_a_job_that_carries_the_cache_builds_in_its_own_target ok (:135-297). A developer's tree, so the carried path did not run in it.
  • cargo run -- --build-only: EXIT=0 (image.exit), "Build finished." (image.log:508).
  • CI at this head: host skipped, the pull request being a draft (run 36980111127). The carried path last ran on a runner at c282a7669 (run 36913211608), and the reader's code is that head's plus main's.
  • No guest test is reached: the body's reading of the diff holds.

Round 10's NOTEs and REMOVEs are applied: the three clauses are in Caches (:82-87), the repeated Tests sentence went with the merge, "Gone:" is one item, and the "one entry a night" clause is gone.

Growth. git diff --shortstat origin/main...883adf16a: 13 files, +1035 −54, net +981.

  • Production Rust: +448 −7, net +441 (src/cicache.rs 390, src/ci.rs +50 −2, src/clippy.rs +4 −1, src/identity.rs +3 −4, src/lib.rs +1).
  • Test Rust: +416 −29, net +387 (src/cicache.rs 371, src/ci.rs +45 −29).
  • Workflows +21 −18, reviewer.md +11, issues +139.
  • Since c282a7669 the branch's own production is two lines changed and none added; the growth stands on round 9's acceptance.

BLOCKER

None.

NOTE

  • src/ci.rs:1166, the cut of main's each_cache_has_one_writer (de5f63c:src/ci.rs:1074-1101) — nothing in the prompt this branch lands licenses it — reviewer.md:96-97 cuts a test "only when it tests nothing, or as Guest tests says"; this one reds on a plain second actions/cache/save@ writer, reads as passing at this head unchanged, and no stage of a track names it in this diff with an exit a build or test can fail.
    • What speaks for the cut is main's, outside the prompt: root CLAUDE.md's "code enforces only what reading cannot see", and the second stage of issues/build/the-tooling-is-a-review-prompt-and-three-workflows.md.
    • The same sentence reads against the first exit of this branch's own issue, issues/build/the-workflow-gates-that-read-workflows-as-text-become-reviewer-instructions-or-go.md:27-28.
    • Before landing, one of two: the test comes back and Caches' one-writer clause leaves "No gate reads these"; or :96-97 names the case, a clause such as "when this prompt takes its rule", which is a prompt edit the orchestrator briefs.
  • .claude/agents/reviewer.md:79-89 — two more things the deleted gate pinned are free in Caches — each fails as cold pull request runs behind green checks:
    • the keys (f7e0bd4:src/ci.rs:1179, HOST_KEYS): the reader's restore-keys is the writer's key up to its run id. A head renamed on one side leaves every pull request on the old name's last entry, or on none.
    • the order (f7e0bd4:src/ci.rs:1185-1196, HOST_CACHE's steps): the reader restores before its host, and the writer saves after its seal. A save moved above --ci seal stores no target, the nightly stays green, and every pull request reads "none restored". The bullet's own list at :84 is the reader's order and not the writer's.

REMOVE

  • PR body, "The merge's one changed line": "Main deleted sync::git, so" and ", where sync::git returned a refusal" — on main the seal never read HEAD another way; as main's record these are this branch's merge history.

LAND AFTER NAMED CHANGES

…the keys and the order

Round 11's review left two NOTEs, both on `.claude/agents/reviewer.md`.

The cut of `ci::tests::each_cache_has_one_writer` stood on nothing the prompt
said: Growth cut a test "only when it tests nothing, or as **Guest tests**
says". The orchestrator's decision is that the test stays cut and the sentence
names the case, so it gains one clause: a test is also cut when this prompt
takes its rule. Caches then has to hold all the test held. It already said one
writer per cache, a nightly.yml job; the test also refused the combined
`actions/cache`, whose post step saves, and that is now a clause of the same
sentence, since a diff that adds it shows no `save`.

Caches gains the two things the deleted workflow gate pinned and the bullet
did not say, each a cold pull request run behind green checks when broken:
- the reader's `restore-keys` is the writer's `key` up to its run id: a head
  renamed on one side leaves every pull request on the old name's last entry,
  or on none;
- the reader restores before its `--ci host` and the writer saves after its
  `--ci seal`: a save above the seal stores no target, and the nightly stays
  green.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
@Japabu
Japabu marked this pull request as ready for review October 2, 2026 08:18
@Japabu
Japabu enabled auto-merge October 2, 2026 08:18
@Japabu
Japabu added this pull request to the merge queue Oct 2, 2026
Merged via the queue into main with commit 74a2e70 Oct 2, 2026
2 checks passed
@Japabu
Japabu deleted the wt/toyos-cicache branch October 2, 2026 08:39
Japabu added a commit that referenced this pull request Oct 2, 2026
#678 (de5f63c) restructured the review prompt, deleted src/sync.rs and
src/worktree.rs, made a Sysroot owned by what compiles against it, and
swept the sysroot stores at each placement. #669 (74a2e70) landed the
host cache: src/cicache.rs, `--ci seal`, and the review prompt's Caches.

Resolved by hand:
- .claude/agents/reviewer.md: main's text whole, with this branch's six
  Workflows rules after Caches. Caches opened "Each cache has one writer,
  a nightly.yml job"; toolchain.yml's `build` saves the toolchain's layers
  from three workflows, so that sentence is now the host cache's alone.
- .github/workflows/ci.yml: main's header comment said "no guest" and
  goes; the host job is main's, the toolchain and guest jobs this branch's.
- src/ci.rs: main's `seal` job beside this branch's `toolchain`,
  `bootstrap` and `release`; five workflow files.
- src/release.rs: this branch's module; `sync::git` is gone, so its four
  git reads are `sysroot::git_out`, as main's `publish` reads them.
- src/build.rs, tests/common/compile.rs: main's owned Sysroot, with this
  branch's `hosted_rustc` argument to `toolchain::ensure`.
- src/llvm.rs: main's unconditional `keystore::remove`, this branch's
  `keep`.
- issues/build/the-build-runs-host-tools-outside-rust-and-qemu.md: main's
  git rows less src/worktree.rs and src/sync.rs, with this branch's
  submodule callers and "CI's containers".

What the merge makes false, and so changes with it:
- CLAUDE.md said "Guests run nightly."; it says where they run now.
- nightly.yml's guest cache is gone with the jobs that carried it, so
  issues/build/the-guest-cache-is-read-by-mtime-and-its-writer-restores-
  before-it-saves.md is deleted, src/cicache.rs's LIMIT no longer names a
  guest entry, and the host-cache limit issue sums two host entries
  beside the toolchain's layers (918,708,556 B a set, run 36934214557's
  saves) where it summed them beside guest entries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
Japabu added a commit that referenced this pull request Oct 2, 2026
origin/main is 46af79d: #669, #650 and #653 landed since de5f63c. No
conflict; none of them touches toyos-blockring, userland/blockd or the issue
this branch closes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
Japabu added a commit that referenced this pull request Oct 2, 2026
…ain consumes

Main moved under this round three times: #669, #650 and #653. The merge before
this one, 54c375a, took main at 74a2e70 (#669), not at de5f63c as its
title says: the shared `origin/main` ref had been fetched forward between this
round's fetch and its merge. This one names its commit.

The one conflict is the `rust` gitlink. Main pins 3f6050fc829 (#650); this
branch pinned d1b9f2eae3c, its three std commits over the pin before, on a
fork branch of its own. Neither contained the other. The gitlink is now
6c7f996a4fe, the merge of d1b9f2eae3c onto 3f6050fc829: main's pin and this
branch's three commits, and nothing else. The two sides share no file. It is
on the fork's `main` through the merge 012fdce3c79, which also brings that
branch the two of this branch's commits it lacked.

`kernel/src/loader/mod.rs`, `tests/toyos.rs` and `toyos-abi/src/syscall.rs`
merged without a conflict.

Before this commit, on the merged tree: `cargo run -- --build-only` exit 0,
with the sysroot built from the fork at 6c7f996a4fe.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
Japabu added a commit that referenced this pull request Oct 2, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant