Agent-First Files v0.1.1

by Agent-First Kit Contributors

On Windows, a watch on a subdirectory reported nothing at all — the code found the directory a change belonged to by splitting the path on a separator that platform does not use.

A watch on a subdirectory reported nothing on Windows. Not intermittently, not late: nothing, ever. A watch on the root reported changes that were not in it.

The cause is one line, and it is the kind that only a second platform can find.

Two separators that look like one

A listing in affiles is keyed by a /-joined path, on every platform. That is deliberate — the key is part of the contract a caller reads, not a reflection of the host’s filesystem.

A path handed back by a platform watcher is spelled the platform’s way.

Translator::directory_of has to turn the second into the first: given the file that changed, name the directory whose listing changed with it. It did that by reading the relative path as text and splitting on the last /:

let relative = relative.to_str()?;
let parent = match relative.rsplit_once('/') {
    Some((parent, _)) => parent,
    None => "",
};

On Unix those two separators are the same character, so this is correct and stays correct. On Windows the path arrives as src\main.rs, nothing splits on /, and the fallback attributes the change to "" — the root.

Every nested change was therefore filed under the root. A caller watching src never heard about src/main.rs, because that change had been recorded as happening in the root instead. A caller watching the root heard about changes that, by its own contract, were not in it.

The key is now rebuilt from the path’s components, which is the one spelling both platforms agree on, and a component that is not a plain name refuses rather than guesses.

The test that cannot fail where the bug is not

The regression is more interesting than the fix, because the obvious way to write it does not work.

You cannot simulate this on Unix. A backslash is an ordinary character in a Unix filename — Path::new("src\\main.rs") there is a single file whose name contains a backslash, not a file inside src. Any test that writes the Windows spelling by hand is testing something else entirely.

So the test builds its paths with join and lets each platform spell them its own way:

assert_eq!(
    translator.directory_of(&root.join("src").join("main.rs")),
    Some("src".to_owned()),
);

On a platform whose separator is already / this cannot fail. That is the point, and it is worth being explicit about rather than treating as a weakness: the test asks a question only one platform can answer wrongly, and it is now run there.

Why it took until now

affiles has shipped a Windows binary in every release and tested it in none of them. The continuous gate ran on Linux; the only Windows leg lived in the release workflow, which does not start until a tag exists. Windows was first exercised when the crate was one step from publication — and only far enough to confirm it built.

That is fixed alongside this: the suites now run on Windows on every push. This bug was in the first thing they said.

Two limits are worth naming rather than leaving implicit. The symlink-escape tests stay Unix-only, because creating a symlink on Windows needs a privilege the host may not grant, and a test that turns on the host’s privileges reports on the host rather than on this crate. And Windows leaves a directory by means those tests would not cover anyway — junctions, \\?\ paths, drive-relative paths. What the root promises on that platform is written out in the README, and this release does not change it.

Getting it

$ brew install agentfirstkit/tap/affiles      # macOS and Linux
$ scoop bucket add agentfirstkit https://github.com/agentfirstkit/scoop-bucket && scoop install affiles   # Windows
$ cargo install agent-first-files --locked --features api