agent-profile — Sync Baseline Amendment¶
Date: 2026-06-10
Status: Implemented
Amends: 2026-05-24-claude-profile-design.md (§5, §8, and parts of §9/§11)
Why¶
Runtime verification of the v0.1.5 implementation found that the original
design's merge baseline — a committed, shared meta.json recording
filesystem mtimes — is unsound across machines:
- mtimes recorded by machine A are meaningless on machine B (clone/pull resets mtimes; clocks and copy times differ), so B misclassified every file on first sync (false-conflict storm);
- a home edit older than another machine's sync timestamp was classified "unchanged" and silently overwritten (data loss, the §8.1 conflict row never fired);
- deletions classified as "delete-vs-change" and resurrected.
The §8.3 rationale ("mtime vs baseline survives mtime resets") only holds when the baseline was recorded by the same machine that evaluates it.
What changed¶
-
Baseline is per-machine and content-addressed.
~/.agent-profile/state/<profile>/meta.json(version 2) maps each tracked path to the sha256 of its content at this machine's last successful sync, and each tracked~/.claude.jsonkey to the sha256 of its JSON value. It is never committed. The old committed<checkout>/.agent-profile/state dir is no longer read, written, or staged; v1 metas parse as "no baseline". -
§8.1 decision table reclassified by hash. "Changed since last sync" ⇔ hash ≠ baseline. Mtimes only break conflict ties (newer wins, tie → home). Two new behaviors:
adopt-baseline: both sides hold identical content → record, no copy, no conflict. This makes first contact on a fresh machine a clean no-op and lets independently-converged edits settle silently.-
First contact with differing content is a conflict with the loser preserved — never a silent overwrite (the original table's "copy newer → other" row allowed silent loss).
-
Repo-side artifacts never materialize into
~/.claude/:.conflicts/**,claude.json.partial, andagent-profile.yamlare excluded from the home side of sync/use/init (they remain committed for visibility). -
§11 error handling implemented as specified (it previously wasn't): unreachable remote → local merge still runs, warn, exit 0; malformed
~/.claude.json→ skip key merge, file merge proceeds, exit 1; unrelated dirty checkout → abort, exit 2;--dry-runprints the plan and writes nothing; the sync lock covers the--pull/--pushhook paths; commander usage errors exit 2 (1 stays reserved for "ran with conflicts"). -
Divergence recovery. Sync/adopt/merge pull (ff-only) before committing; a rejected push triggers fetch + rebase of our own unpushed sync commits + one retry. Remote history is never rewritten; an unresolvable rebase aborts cleanly with exit 2.
-
In-process composition.
use/merge/initinvoke the sync engine directly (the previousnpx tsx src/...self-exec only worked in a dev checkout).useaborts if the outgoing flush fails. Backups now capture the tracked~/.claude.jsonsubset asclaude.json.partial, andrestorere-applies it as a key overlay (never as a literal home file). -
merge --strategy newercompares last-commit times (git log -1 --format=%ct -- <path>), which are machine-independent, instead of filesystem mtimes. -
config getresolves through all layers (defaults < repo YAML < profile YAML < per-machine overrides) with--explainnaming the source layer; dottedconfig setkeys persist underoverridesin the machine config (§3 layer 4 as designed). -
New command:
tui— interactive full-screen viewer over thedescribereport for every profile in the repo (non-TTY invocations print the reports sequentially).
Regression coverage¶
tests/integration/regressions.test.ts (R1–R17) pins each of the above:
cross-machine conflict preservation, deletion propagation, clean first sync
on a second machine, artifact exclusion, status settling, dry-run, offline,
dirty-checkout abort, malformed-JSON exit 1, adopt-pull-first, use abort,
in-process merge apply, restore key overlay, config resolution, usage exit
codes, __preexisting__ backup, and init's initial sync.