Skip to content

Drift — when something changed that we did not change ​

Load when: a file looks edited, a synced thing disagrees with its source, or a session starts after time away.

A human's edit is truth about intent. Agents read before they write and never clobber. But respecting intent is not the same as accepting anything: an edit that makes the data inconsistent is a finding, not something to adapt to.

Adding blocked_by to a task that is in progress is a finding, not a silent status change. The field is the reason, the status is the state — and tidying it up on the owner's behalf is the same automatic transition that makes boards lie, only started by a person.

Three mechanisms, three questions ​

They are routinely blurred into one, and each answers something the others cannot.

QuestionMechanism
Who authored this?created_by — a field, written once at creation
Who changed it?git — the commit and its trailer. Not a field: a declared "last edited by" rots the first time someone forgets it
Was a generated block edited by hand?a content hash, stored when the generator wrote it

The hash is over content, never over modification time. A timestamp changes when nothing did, and does not change when a file is restored — both failure directions at once.

Generated blocks — views, never sources (./patterns §6) ​

The generator writes between markers and stores the hash of what it wrote. Before the next regeneration it hashes what is there now:

  • matches — rewrite silently, nothing happened
  • differs — someone edited by hand, and it is reported before it is overwritten

That "before" is the whole promise, and it only holds if something checks regularly. Regeneration is one trigger; the audit is the second, and without it the promise depends on the accident of someone regenerating soon.

Never repair a link or fix a typo inside a generated block. It is overwritten at the next regeneration, and worse — the hash check then reports your own fix as an unexplained hand edit, so the system accuses itself. Fix the source the block is generated from.

Synced things: compare three ways ​

Anything with a source outside this repository records its last-known state — version_seen, last_synced. Before acting, compare three ways:

MeaningResponse
theirs changedupstream movedshow what moved, offer to take it
ours changedwe edited locallyshow it, offer to push or keep
bothgenuine divergencesurface with options — never merge silently

Take theirs · keep ours · reconcile. The owner picks. A silent merge is how a deliberate local change disappears without anyone noticing it is gone.

A template is copied, not linked. After importing one, it is ours and it diverges — that is normal. Wanting to track upstream is a different, explicit choice: an external pointer with version_seen, which is a dependency rather than a template. You cannot silently have both.


A date is the cheap version of freshness, and a fingerprint is the honest one. Everything in this system that can rot carries the day it was checked, and a date has one weakness that is structural rather than careless: it does not know that the thing changed. A measurement taken on Tuesday reads as good on Friday whether or not the code under it moved three times.

So where the thing measured is in the tree, the claim can carry the tree's own fingerprint instead — a hash of the content it was measured against — and then it degrades by itself: the same content still committed keeps the claim fresh, and a file that changed, or a new untracked source appearing beside it, invalidates it without anyone deciding to be honest that morning. Read off gstack's evidence ledger, 2026-09-17, which keys freshness to a working-tree content fingerprint rather than to a timestamp.

This is a direction, not a claim that we do it. Today's dates are hand-written and the checks that read them compare days (resources → A claim past its check-date). The cases where a fingerprint would earn its cost are the ones where a claim is about the repository itself — a measured count, a suite's pass, a guard's behaviour — and those are exactly the claims this corpus has caught going stale between writing and shipping. Where a claim is about the outside world, a date is the only thing available, and no fingerprint helps.

The environment fingerprint ​

Three-way compare when both sides can change — patterns §17.

project = f(repo) is false until this exists. Skills, plugins, hooks, settings and servers load from outside the repository into every session, so the same repo on two machines behaves differently — and nothing in the repo says so.

Write a hash per class after any operation that changes state; compare on waking. Store the map plus the commit it was taken at.

A hash detects; it does not rebuild. Cloned onto a second machine the fingerprint says different and stops there, which leaves the premise half-kept. So alongside the hashes the project declares what it requires from outside — each MCP server, skill or plugin by name, with what it is for, where it comes from, and which credential it needs by reference. Values stay out (permissions); the requirement does not. Detection answers "has this changed"; the declaration answers "what do I install to work here", and only the second survives a new laptop.

Classes: skills · plugins · hooks · settings · MCP servers · agent definitions · harness version.

A class nobody hashes is drift nobody sees. When the harness gains a new kind of thing, the fingerprint is blind to it until someone adds it here — so this list is canonical, and health, entering and upgrading all compare against it rather than each carrying their own.

On a difference: attribute first, then ask why, then record the why. Most changes explain themselves from git or from the session history. What stays unexplained becomes a question to whoever made it, and the answer goes into the project's own documentation. Not "revert" and not "accept" — find out the intent and write it down, because the next session will otherwise ask the same question.

Reads outside the repository are a separate axis. Inside the repo an agent reads freely; reaching outside it needs an explicit allowance. That is about the perimeter, not about hierarchy, and conflating the two is easy — enforced_by: prose-only, partly held by the harness's own permission settings.

Coming back after time away ​

Three different questions, and blending them hides the third:

QuestionAnswer from
what needs meopen requests → requests
what happenedthe notification feed → requests
what changed that we did not changethis file

The summary is computed from the task histories, never stored: closed since the last session, requests open and their ages, spend since then, anything that regressed. Atoms recorded, summary derived.

Leaving should be as deliberate as arriving → entering, which owns both ends of it.