Skip to main content
Version: 0.0.5

Change detection

graft uses content hashes stored in a .meta.json sidecar to detect what changed — and on which side — before touching any provider file. This is what makes sync fast: most agents are a no-op in under a millisecond.

The sidecar: .graft/agents/<name>/.meta.json

Each canonical agent directory contains a .meta.json file alongside agent.yaml and instructions.md. It holds two families of hashes:

FieldScopeValue
canonicalHashtop-levelsha256 of the canonical agent content (field-sorted, normalized — cosmetic whitespace changes never shift it)
providers.<id>.sourceHashper providersha256 of that provider's file on disk at the last sync
providers.<id>.canonicalHashper providerthe canonicalHash value at the time this provider file was last written
providers.<id>.lastCommitHashper providergit commit SHA when sourceHash was recorded (provenance only — not used for sync decisions)

All hash values are plain hex SHA-256 of file content. lastCommitHash is the only git-derived value.

How hashes classify each agent

On every sync graft computes two comparisons per agent:

  1. Has the provider file changed? — compare sha256(provider file on disk) against the recorded sourceHash.
  2. Has the canonical changed? — compare sha256(canonical content) against the recorded canonicalHash.

Those two bits determine the action:

Provider changedCanonical changedAction
YesNoIngest — pull provider edit into canonical, fan out to all providers
NoYesFan-out — write canonical to all providers
YesYes3-way merge (git beta worktree)
NoNoNo-op — already in sync

Subset-sync staleness healing

When you sync only a subset of providers (e.g. --providers=claude-code), opencode's file is not rewritten. Its sourceHash still matches its on-disk file, so it would look "in sync" on the next run — but its providers.opencode.canonicalHash differs from the current canonicalHash, revealing it was last rendered from an older canonical.

On the next full sync, graft detects this staleness and force-rewrites opencode's file to match the current canonical. This is how subset syncs stay self-healing.

.meta.json is a derived cache

.meta.json is a cache that can be reconstructed from the committed files:

  • sourceHash and canonicalHash are recomputable from files already in the repo.
  • lastCommitHash is not recomputable from files alone; it re-stamps to the current HEAD on first sync.
  • The merge ancestor for 3-way merges is the git-committed canonical state, not .meta.json. A fresh clone without .meta.json stays merge-safe.

Fresh-clone behavior:

ScenarioResult
.meta.json presentTrue no-op when nothing changed
.meta.json absentgraft rewrites provider files to byte-identical content and regenerates .meta.json. No data loss.
Absent + a provider edit disagrees with canonicalThe edit is preserved and promoted (not silently dropped)
Absent + canonical and provider both edited to different valuesA surfaced, resumable git conflict (no silent data loss)

graft commits .meta.json by default (it is not gitignored). This keeps the first sync after a git pull a true no-op rather than a full identical-content rewrite, and avoids spurious conflicts.