DEV Community

Discussion on: Every AI coding agent tracker is a self-report system

Collapse
 
peterbuildssecure profile image
Peter

The machine-owned check is the right boundary. I’d also bind every successful check result to the exact Git tree it evaluated.

An exit code alone can become stale immediately: the agent can run the check, modify another file, then hand the card back using a result produced against different state. Recording the commit/tree hash, command, exit code and output digest makes the claim reproducible.

For dirty worktrees, either refuse completion or hash the relevant files before and after the check and fail if they changed. The useful invariant is not just “this command passed,” but “this command passed against the exact artifact now being marked complete.”

Collapse
 
albertoclemente profile image
Alberto Clemente

"This command passed against the exact artifact now being marked complete" — that's the invariant, said better than I've managed it.

Most of it's there: argv, exit code, duration, sha, dirty flag, anchored to the tree
the check ran on rather than HEAD at write time. And the agent never carries a result at all — done() runs the check itself.

But you've found something I hadn't. I read the head before the run and never again, so a tree that changes during the check is recorded against pre-run state. Your before-and-after hash is the fix, and I don't have it.

Collapse
 
albertoclemente profile image
Alberto Clemente

Fixed in 9feb4bd. You were right — the tree was only read before the check ran, so a pass could be recorded against a tree that had already changed. It's read on both sides now, and if it moved the card doesn't move either.

One thing your comment saved me from: it hashes the diff, not the filenames. A file edited twice with the same name would have slipped through otherwise.

Thanks. First bug on that board that didn't come from me or CI.

Collapse
 
peterbuildssecure profile image
Peter

Nice fix. Hashing the diff contents closes the “same filenames, different state” hole.

The remaining edge is the small TOCTOU window between the second tree read and the card-state write. If another process can modify the worktree there, I’d make the final transition a compare-and-swap: update the card only if the current tree still equals the verified hash. Alternatively, hold the same repository lock across verification and transition.

Then the invariant becomes atomic: either the exact verified tree is marked complete, or nothing moves.

Thread Thread
 
albertoclemente profile image
Alberto Clemente

Fixed in 13450bb — the promotion is a compare-and-swap now. Of your two fixes it was the only one this design could take: the check deliberately runs outside the write lock (waiters give up at 60s, and a suite can run for minutes), so holding the lock across verification was out. Instead done() takes a third tree reading while it holds the lock, and the card moves only if the digest still equals the one the check was verified against. A mismatch lands in the same bucket as the last fix — absence of evidence, neither pass nor fail, the card stays in progress. The test stages your scenario literally: a second process holds the lock, the tree moves while the write waits on it, and the card must not move with it. Which is your closing sentence as an invariant: either the exact verified tree is marked complete, or nothing moves. Three findings now, each a strictly smaller window than the last. The acknowledgements section is developing a habit of your name.

Thread Thread
 
albertoclemente profile image
Alberto Clemente

@peterbuildssecure Postscript, a week on: all three of your findings shipped, and it's on npm now — npx shipward setup ~/code/your-project --seed-from-branches — so the thing a stranger installs today is meaningfully more correct than the thing I wrote about. That's down to you. Three rounds, each window smaller than the last, and I hadn't seen any of them coming.

If you ever have an idle hour, the compare-and-swap is the part I'd most like broken by someone who isn't me. You've got a better record at that than I do.

Thread Thread
 
peterbuildssecure profile image
Peter

That is a very generous postscript. It’s also good to hear the fixes made it into the installed artifact rather than stopping at the discussion.

The first place I’d attack is the lock domain. The third digest read and card update are serialized against other Shipward participants, but Git, an editor or another process may not respect that lock.

I’d add a failpoint immediately after the under-lock digest comparison and before the card write, then have an external process mutate the tree in that gap. If the card can still move, the compare-and-swap is correct within one store but the claimed invariant spans two independently writable systems.

One clean way to narrow the claim is for completion to attest “digest X passed” rather than “the current workspace is complete,” and require every consumer of that status to compare X with the tree it is about to act on. That turns a later mutation into visibly stale evidence instead of trying to make the filesystem and card store one atomic transaction.