Promises an Agent Can Check

I wrote down the promises my CLI makes. A random test broke one in 16 tries.

The promise was simple: paper update brings your components up to date and respects your changes. But if you deleted a file you got from paper, the CLI counted it as new, and the next update quietly put it back. A deletion is a change. The CLI didn’t think so.

Agents skim the promises a design system makes in its docs, or never load them, so every one of those promises should be something a program can check. That is the idea behind agentproof-ui: typed component docs, a CLI called paper that agents call to list, read, add, and check components, and rules written as checks instead of prose.

That moves the question one level down. If the CLI is what keeps the promises, who checks the CLI?

To know what “respects your changes” means, the CLI gives every file a status. I wrote that part as a small model in Haskell. rec is what paper last gave you, dsk is what’s on disk now:

status rec dsk p c = case M.lookup p dsk of
  Nothing | M.member p rec        -> Deleted
          | otherwise             -> New
  Just cur | cur == c             -> Unchanged
           | M.lookup p rec == Just cur -> Clean
           | otherwise            -> Modified

A file that isn’t on disk is either one you deleted or one you never had. A file that is on disk either matches the registry already, matches what paper last gave you, or contains your edits. This is the fixed version: the Deleted status didn’t exist before.

Then I wrote the promises as laws, properties that should hold for any project and any registry. Running update twice should leave the project exactly as running it once. Update should never touch a file you modified. QuickCheck, a Haskell testing library, generates random projects, each with its own history and local edits, and tries to break each law. I ran each one 10,000 times.

Seven laws held. Three things the CLI took for granted did not, and I reproduced all three in the real CLI.

The second: files the registry stopped shipping stayed in your project forever, as orphans nobody tracked or removed.

The third was that two components could ship the same path. The last component written won, and from then on every update failed on that path with exit code 1, so the project never settled.

A fourth bug came from reading the code, not from QuickCheck: broken markers in AGENTS.md could make paper delete the text the user had written below its block.

All four are fixed in the CLI now. The model has 13 laws, and all of them pass:

update . update == update: +++ OK, passed 10000 tests.
update never overwrites Modified: +++ OK, passed 10000 tests.
deleted files stay deleted: +++ OK, passed 10000 tests.
orphans: clean deleted, edited kept: +++ OK, passed 10000 tests.
shared paths are rejected: +++ OK, passed 10000 tests; 1129 discarded.

None of these bugs would show up in a demo. They show up in a project that has a few months of history, where someone deleted a file on purpose, the registry dropped a file, and an agent runs paper update because the docs told it to. The agent will trust the output. It won’t notice that a file it deleted came back.

A person who sees something strange stops and asks. An agent keeps going. So a tool an agent relies on needs stronger guarantees than one a person uses.

If your design system has a CLI or a script that agents call, write down what it promises, in one sentence per promise. Then pick the one that would hurt most if it were false and try to break it with random inputs. You don’t need Haskell for that. You need the sentence first.

← Writing