ADR-145: Explicit three-source convergence manifest¶
ARCHIVED — 2026-08-13. No longer part of the active architecture set. Kept for history and so existing references still resolve.
Why: ADR-144 shipped the two-source reconciler the day before this refinement was written, and solved the problem without a pinned Claude-Code baseline. The three-source manifest is absent from manifest.rs and reconcile.rs. Superseded by: ADR-144
Nothing below this line has been edited.
Context¶
This is a child of ADR-144 (install / repair / migrate as one manifest reconciler),
which is itself a child of ADR-142 (agent-ways 1.0). ADR-144 unified install, update,
repair, and migrate into one idempotent reconciler that "converges ~/.claude
toward a git-derived manifest," and framed the four from-states as one convergence
with different entry conditions: "the four from-states are one convergence … they
differ only in starting actual and trust posture." Fresh is "actual is empty →
materialize all entries"; the others re-materialize a delta against a live tree.
ADR-144 names the desired side of that convergence — the manifest, derived from
git ls-files in $XDG_DATA/agent-ways — and the engine that drives toward it. But it
left the actual side, and one whole leg of the desired side, implicit:
- The agent-ways leg is explicit and exists today.
ways manifest(tools/ways-cli/src/cmd/manifest.rs) emits exactly what agent-ways projects, derived from the git-tracked file set:PROJECTED_TREES(skills,agents,commands,hooks/ways),PROJECTED_FILES(the two named hooks), andPROJECTED_BINS(ways,attend,attend-chat,way-embed). It runsgit ls-filesover the tracked trees and allowlists the built binaries by name. This is already one of the three legs the convergence needs. - There is no model of what Claude Code itself owns in
~/.claude. ADR-142's layout table calls~/.claudethe "irreducible Claude-Code-owned floor," but nothing enumerates that floor. The reconciler converges toward (agent-ways manifest) and treats everything else as a single undifferentiated "don't touch" region. - "What is the user's own customization" is therefore decided heuristically — by the
same weak membership test ADR-144 already flagged in today's
build_manifest: "did a prior projection write it," which "can't classify a file the user dropped into a shared dir that happens to match a name we later ship."
Three concrete consequences of that gap motivate this ADR:
-
The fresh-install path was never wired. agent-ways 1.0.0's documented installer (
git clone … ~/.claude && make setup) still produces the pre-1.0 in-place shape, not the projection. A brand-new 1.0 user lands in exactly the topology 1.0 replaced. ADR-144 says fresh install should "fall out" of the engine as materialize the manifest from nothing — but with no explicit target for the engine to materialize into an empty tree, the native projection installer was never built. This is a real, current bug. -
Classification is heuristic and error-prone, and four consumers need it. Deciding whether any
~/.claudefile is Claude-Code-owned, agent-ways-owned, or user-owned is needed by install, repair, migrate, and cleanup — and each currently guesses. ADR-144's own sharpest Negative ("the migrator must detect and rescue hand-edited core files … or it silently destroys the customization it was meant to preserve") is a symptom of having no authoritative classifier. -
The settings.json three-way merge already solves a version of this — for one file.
tools/ways-cli/src/cmd/settings_merge.rsdoes a kubectl-style three-way merge ofsettings.json. It tracks a stored last-applied base — the slice agent-ways itself last wrote, persisted to$XDG_STATE/agent-ways/settings-applied.json— and computes, per owned slice,result = (theirs − base − ours) ++ ours. Field ownership is explicit: agent-ways owns thehooksevents and thepermissions.allowentries matchingWAYS_PERMS; everything else —model,theme,plugins,env, credentials, the user's own hooks, and Claude Code's own default values — is treated as the unmanaged remainder. This is precisely the "tell app from user" computation, made idempotent by the stored base. But note its shape: it is a two-party split (agent-ways-owned vs. everything-else), it does not separately model the Claude-Code floor, and it is scoped to a single JSON document.
The realization: the reconciler's convergence target is implicit and ad-hoc, and the
pattern that would make it explicit already exists — at the granularity of one file, and in
two-party form. This ADR lifts the owned-vs-remainder partition from that one JSON document
to the whole ~/.claude tree, and additionally splits the remainder into its two real
owners — the Claude-Code floor and the user — which the single-file merge currently
conflates.
Decision¶
Make the convergence reconciler's target an explicit, versioned, three-source manifest:
the union of a pinned Claude-Code baseline and the agent-ways manifest, with the user
remainder defined by construction as everything in neither set. The reconciler converges
actual → (CC-baseline ∪ agent-ways-manifest) and, because the user remainder is outside
that target, never touches it.
This is the tree-wide generalization of the settings.json three-way merge.
settings_merge.rs already proves the core move: carve a stable agent-ways-owned layer out
of an unmanaged remainder, idempotently, by tracking what we last applied. This ADR lifts
that move from one file to the whole tree — and, where the single-file merge stops at
two parties (ours vs. everything-else), the tree model splits the remainder into the
Claude-Code floor and the user, the distinction the file merge currently leaves implicit.
1. Three manifest legs¶
| Leg | What it is | Source of truth | Status today |
|---|---|---|---|
| Claude Code baseline | Files/dirs vanilla Claude Code owns or creates in ~/.claude, pinned to a real CC release tag (e.g. 2.1.196). |
An empirical clean-room snapshot (see §2), supplemented by package introspection. | New — does not exist. |
| agent-ways manifest | What agent-ways projects: PROJECTED_TREES, PROJECTED_FILES, PROJECTED_BINS. |
git ls-files in $XDG_DATA/agent-ways, via ways manifest. |
Exists (cmd/manifest.rs). |
| User remainder | Everything in neither set. | Defined by construction — the complement of the union. | Implicit today (heuristic). |
The target the reconciler converges toward is CC-baseline ∪ agent-ways-manifest. The user remainder is not in the target set at all. This is the principled replacement for heuristic "don't clobber the user's stuff" guards: the user's files are not protected by a special case — they are simply not in the manifest, so idempotent convergence has no entry that would write over them.
This maps onto settings_merge.rs, with one deliberate refinement:
- agent-ways-manifest = the owned layer — the file merge's
ours(thehooks+WAYS_PERMSslice). At the tree level, the git-derivedways manifestis the record of what we own, so the tree leg needs no separately stored base the way the file merge keepssettings-applied.json— git tracking plays that role. - user remainder = the unmanaged content — the file merge's
(theirs − base − ours), present and left alone (ours-by-absence). - CC-baseline = the new third leg. The file merge has no separate notion of the Claude-Code floor — it folds CC's default values into the unmanaged remainder. The tree model promotes that floor to its own leg, because at tree scale the difference between "Claude Code created this" and "the user created this" is load-bearing for cleanup and migration in a way it is not for a single settings field.
2. Capturing the CC baseline — empirical clean-room snapshot, pinned to a tag¶
The key technical fact: Claude Code's ~/.claude footprint is mostly runtime-emergent,
not unpacked from the npm package. settings.json defaults, projects/,
history.jsonl, sessions/, and file-history/ are created when Claude Code runs,
not when it installs. A baseline built only from package introspection would miss most of
the floor it is meant to describe.
So the authoritative capture method is an empirical clean-room snapshot: run vanilla
Claude Code with HOME pointed at an empty sandbox directory, exercise it minimally, and
record the resulting ~/.claude tree. Static files that do ship in the package are
captured by package introspection and merged in. The result is pinned to a specific CC
release tag, which makes the baseline versioned and reproducible — a baseline for
2.1.196 is a fact about 2.1.196, regenerable by anyone with that tag and a sandbox.
Decision on storage and shipping: the baseline ships as a committed snapshot file
per CC tag in $XDG_DATA/agent-ways (versioned alongside the code that consumes it),
generated by a ways subcommand that performs the clean-room run. The committed
snapshot is what the reconciler reads at convergence time; the generator is what the
maintainer runs to refresh it when a new CC release moves the floor. This keeps the
runtime path offline and deterministic (no sandbox spin-up during a user's SessionStart)
while keeping the snapshot honestly reproducible. Refresh cadence is maintainer-driven,
not per-release — the baseline is refreshed when CC's footprint actually changes, and
skew between refreshes is absorbed by §3.
3. Tolerating CC version skew — the baseline is an allow-pattern set, not an exact list¶
The user's installed CC version will rarely equal the pinned baseline tag. If the baseline were an exact file list, every patch-level CC release would produce spurious "unclassified" files and risk the reconciler treating a genuine CC runtime file as user remainder (or vice versa).
Decision: the CC baseline is consumed as an allow-pattern set (path globs /
prefixes — projects/, sessions/, history.jsonl, file-history/, settings.json,
statsig/, todos/, …), not an exact inventory. Classification asks "does this path
match a known CC-owned pattern?" rather than "is this path byte-identical to the
snapshot?" Patterns are far more skew-tolerant than file lists: a new session file under
sessions/ is still obviously CC-owned. The per-tag exact snapshot remains the evidence
from which the pattern set is derived and audited, but the pattern set is the runtime
contract. Skew within the pattern set's tolerance is a non-event; skew that introduces a
new top-level CC artifact is the signal that the maintainer should regenerate (§2).
4. The ways surface — a new ways classify¶
Decision: add a new ways classify subcommand that emits the three-way
classification of a real ~/.claude — for each path, which leg it belongs to
(cc-baseline / agent-ways / user-remainder) — rather than overloading ways manifest or
ways status.
Rationale, by single-responsibility:
ways manifestanswers "what does agent-ways project?" — one leg, derived purely from git, with no reference to a live~/.claude. Folding CC-baseline and live-tree classification into it would give it two reasons to change.ways statusis a health/observability summary for the operator, not a per-path classification emitter.ways classifyis the natural home for the set operation over a live tree: it consumes the agent-ways manifest (leg 2) and the CC baseline pattern set (leg 1), reads the actual~/.claude, and emits the three-way partition that install, repair, migrate, and cleanup all consume.
ways classify is the read-only classifier; the reconciler (cmd/reconcile.rs) is the
mutating consumer that acts on the partition.
5. Precedence when a path is claimed by both CC baseline and agent-ways¶
Some paths are claimed by both legs — settings.json is the canonical case: it is part
of CC's baseline floor and a file agent-ways writes into. Decision: the agent-ways
manifest takes precedence for projection (agent-ways is allowed to write the path), but
the already-specialized three-way merge (settings_merge.rs) governs that write — the
whole-tree convergence delegates any path that is both CC-baseline and agent-ways-managed to
the per-file merge that already knows how to combine a CC-default base, the agent-ways layer,
and user fields without clobbering the user. In other words: tree-level classification routes
settings.json to file-level classification; the coarse leg precedence (agent-ways > CC for
projection) selects who may write, and the fine-grained merge decides what to write. No
other current path is expected to be doubly-claimed; if more emerge, the same rule applies —
overlap routes to a path-specific reconciler, defaulting to "agent-ways may write, user
fields preserved."
6. Consumers — all four ADR-144 from-states, plus cleanup¶
The single explicit manifest is consumed by every entry condition ADR-144 defined, plus cleanup:
- fresh — materialize
(CC-baseline ∪ agent-ways-manifest)into an empty tree. This is what makes the native projection installer fall out as "bootstrap shim + reconcile in fresh state," with almost no new code: there is finally an explicit target to materialize into nothing. Wiring this closes the 1.0.0 fresh-install bug (Context #1). - drifted — re-materialize missing/broken entries of the union (repair).
- out-of-date — re-derive the agent-ways leg from the advanced
$XDG_DATAHEAD, materialize the delta, prune orphans (update). - legacy-in-place — the migrator classifies the old clone against all three legs:
agent-ways files relocate to
$XDG_DATA, the user remainder lifts to$XDG_CONFIG/$XDG_STATE, and CC-baseline files stay. The classifier is exactly the rescue mechanism ADR-144's sharpest Negative demanded. - cleanup — the user remainder is precisely the set that is safe to prune or flag; conversely, agent-ways orphans (in the manifest's history but no longer git-tracked) are safe to remove outright. This is the principled form of the by-hand cleanup done in the session that motivated this ADR.
Consequences¶
Positive¶
- The convergence target becomes explicit and testable. ADR-144's "converge toward the manifest" gains a concrete, three-legged, versioned definition of the manifest — install / repair / migrate / cleanup all read one artifact instead of each guessing.
- The 1.0.0 fresh-install bug has a principled fix. Fresh install stops being a missing feature and becomes "reconcile in the fresh from-state against the explicit target," exactly as ADR-144 promised it would fall out.
- "Don't clobber the user" stops being a guard and becomes a set property. The user remainder is untouched not because of a special case, but because it is not in the target — the most robust form of the protection, and the one least likely to regress.
- One pattern, two granularities. The settings.json three-way merge stops being a one-off; it is now the file-level instance of the same model the whole tree uses, which makes both easier to reason about.
- Migration's rescue problem gets a real classifier. ADR-144's "detect and rescue
hand-edited core files" becomes a concrete
ways classifyoutput rather than an aspiration.
Negative¶
- A new external dependency surface: the CC baseline must track Claude Code. agent-ways now maintains a model of a tree it does not own and that changes on Anthropic's clock. The pattern-set design (§3) absorbs most skew, but a CC release that introduces a new top-level artifact requires a maintainer refresh; until then that artifact classifies as user remainder, which is the safe failure direction (we leave it alone) but a misclassification nonetheless.
- The clean-room snapshot is real machinery to build and keep honest. A generator that spins up sandboxed CC, exercises it enough to emit its runtime files, and diffs the result is non-trivial and itself version-sensitive — and "exercise it minimally" is a fuzzy contract (which CC features must run to materialize which files?).
settings.jsonremains the doubly-claimed seam (ADR-142 / ADR-144 already flagged it); this ADR routes it correctly but does not remove the shared-write risk — it just states precisely where the tree model hands off to the file model.- Pattern sets can be wrong in both directions. Too broad, and a genuine user file under a CC-shaped path is misclassified as CC-owned and skipped by cleanup; too narrow, and a CC runtime file is treated as user remainder. The exact per-tag snapshot is the audit evidence, but tuning the pattern breadth is an ongoing judgment.
Neutral¶
ways manifestis unchanged; this ADR addsways classifybeside it rather than altering the existing leg.- The CC baseline snapshot is versioned in
$XDG_DATA/agent-waysand so is replaced wholesale on update like the rest of the application (ADR-142's$XDG_DATAdurability contract) — losing it is a re-derive, not data loss. - This ADR sharpens, but does not resolve, ADR-142's open
$XDG_STATE↔ Claude-Code-owned boundary (see Open Questions); it gives that boundary a mechanism (the CC baseline pattern set) without fixing where the line sits.
Alternatives Considered¶
- Leave the target implicit; keep heuristic classification. The status quo. Rejected for the three consequences in Context — the fresh-install path stays unwired, and migrate / cleanup keep guessing. ADR-144 already rejected the weaker "did a prior sync write it" membership test for the agent-ways leg; this ADR extends the same reasoning to the CC and user legs.
- Model the CC baseline by package introspection alone (no clean-room run). Rejected:
most of CC's
~/.claudefootprint is runtime-emergent, so an install-time inventory would missprojects/,sessions/,history.jsonl,file-history/, and the defaultedsettings.json— i.e. most of the floor. Introspection is kept only as a supplement for the genuinely static files. - Pin the CC baseline as an exact per-tag file list (no pattern set). Rejected: it is brittle under the inevitable version skew between the pinned tag and the user's installed CC — every patch release would manufacture spurious unclassified paths. The exact snapshot is retained as evidence; the runtime contract is the skew-tolerant pattern set (§3).
- Extend
ways manifestto emit all three legs instead of addingways classify. Rejected on single-responsibility grounds:ways manifestanswers "what does agent-ways ship," derived purely from git with no live-tree or CC dependency. Bolting CC-baseline and live~/.claudeclassification onto it gives one command two reasons to change and couples a pure git derivation to an external-dependency model. - Generate the CC baseline live at each SessionStart (no committed snapshot). Rejected: spinning up a sandboxed CC run on the user's machine at session start is slow, non- deterministic, and fragile; the committed-per-tag snapshot keeps the runtime path offline and reproducible, with regeneration a deliberate maintainer act.
Open Questions¶
These are recorded deliberately undecided; they refine, and partly inherit, ADR-142/144's open questions.
- The ADR-142
$XDG_STATE↔ Claude-Code-owned boundary. The CC baseline pattern set is the natural place to draw this line — runtime state CC creates (projects/<slug>/memory/, per ADR-128) that overlaps agent-ways' own$XDG_STATEclaims must land on one side. This ADR provides the mechanism but does not commit the boundary; auto-memory (ADR-128) sitting in CC'sprojects/<slug>/memory/is the specific unresolved overlap. - What "exercise vanilla CC minimally" must include to materialize the full runtime footprint — which CC operations are needed to emit which files, and how the generator guarantees it captured the whole floor rather than a subset.
- Pattern-set breadth and review process — how broad each CC-owned glob should be, and how the maintainer audits a refreshed snapshot against the prior pattern set to catch new top-level artifacts.
- Refresh trigger — whether baseline refresh is purely manual (maintainer notices a CC release moved the floor) or gets a lightweight detector (a CI clean-room run that diffs the current snapshot against the latest CC tag and flags drift).
- Whether this stays a separate ADR or folds into ADR-144. Recommendation: keep separate —
the CC-baseline capture method and the
ways classifysurface are each substantial enough to warrant their own recorded decision, and ADR-144 is already long.