Repository navigation
Stability between upgrades: a per-layer policy, pinned or live -- deployed links point at the kept snapshot or the working tree #847
Description
Today every deployed link points at a layer's working tree. A git pull on the layer, or an
uncommitted edit in it, changes $HOME at once with no upgrade having happened. The layer you
consume behaves like the layer you author. This feature separates the two.
The feature: a per-layer stability policy
| Policy | Links point at | $HOME changes when |
Suits |
|---|---|---|---|
| pinned | the kept snapshot — the detached worktree at the commit writ planned from | writ upgrade re-pins and relinks |
layers you consume: base, team |
| live | the working tree, as today | the tree changes | the layer you author: personal |
Defaults: base and team pinned, personal live. Overridable per layer in writ.yaml:
layers:
base: { stability: pinned }
team: { stability: pinned }
personal: { stability: live }Stability where you consume, liveness where you author.
What pinned buys
- A deployment is a commit. The link path carries the hash; "what is deployed here, and from
where" is answered byreadlink.Thread:WritOriginbeat 3 (Record the deployed commit for each layer source #473) becomes structural for pinned
layers rather than recorded. --allow-dirtybecomes honest. A dirty working tree cannot leak into$HOME, because$HOME
never points at it.- Rollback is re-pointing. Keep the previous generation and one command goes back.
- "What changed since my last upgrade" is
git diff <old>..<new>in the layer.
What it costs, measured 2026-09-06
One working-tree copy per pinned layer per kept generation; the .git object store is shared and paid
once.
| Layer | Deployable tree | Files | Per kept generation |
|---|---|---|---|
| noblefactor-ops | 68 KB | 6 | negligible |
| personal | 84 MB | 435 | 84 MB — and personal defaults to live |
Two generations for rollback doubles it. The other cost is the one that matters: a pinned layer's
author edits, commits, and runs writ upgrade to see the change. That is exactly right for a
consumed layer and exactly wrong for the authored one, which is why this is per-layer.
Prerequisites, in order
- A pinned snapshot worktree survived its run: cleanup is not reached on every path, and the cache is the wrong home if snapshots are ever kept #846 — snapshots leave
XDG_CACHE_HOMEfor the state or data home, and the lifecycle never
leaks. A cache is something the user may delete; a retained snapshot with links into it cannot
live there. - writ upgrade neither pins layers nor records origin: a regenerated template or secret has no provenance #844 —
upgradepins and records origin the waydeploydoes. Without it there is nothing to
re-pin to. - writ upgrade does its whole job: links added, orphans removed, stale links relinked, copies regenerated -- what the name promises #845 —
upgradedoes its whole job, since relinking a pinned layer to its new generation is
the stale-link case.
Lifecycle
upgrade on a pinned layer: pin the new HEAD into a new generation; plan; relink every target from
the old generation to the new; retain the previous generation (one, configurable); garbage-collect
older generations nothing points at, including git worktree prune. deploy on a pinned layer
behaves the same on first placement. reconcile reports the generation each link points at, which
is how a half-relinked layer would show.
Not this feature
Cross-layer shadowing (#470) and per-source drift (#471) are unchanged by which tree a link points
at; they get a cleaner answer once a link names a commit, but they are their own work.
Epic: #466
Activity
David-Noble-at-work commentedon Sep 27, 2026
Ruled 2026-09-27: kept snapshots, and reconcile against the ref that produced each entry
Found while lane 5 of #916 (#924, reconcile repairs) was stopped before any code. The owner:
This supersedes the 2026-08-08 ruling that links target the origin because snapshots are removed, and widens this feature from a per-layer pinned-or-live policy into the model for every lifetime. Designed, not built, not yet placed.
Rulings:
refs/devlore/...), not tags: tags are pushed bygit push --tagsand fetched by every clone, deleted bygit fetch --prune-tags, and read bygit tagandgit describe.retain/<host>/<date>in the layer clone; the PR's base does the three-way merge. Rejected: writ pushing and opening the PR; the edit left uncommitted in the working tree.writ reconcilealways cuts the retain branch; the developer may then restore the record, by a command flag, breaking the no-command-flags ruling (The writ lifecycle surface: reconcile, scopes, and the vocabulary settlement #762 Requirement 2) knowingly. The branch stays; the developer tosses it, merges it, and upgrades as they like. Restoring by default was rejected as silent data loss.Amended: the sparse checkout covers every scope directory: Home, System and, especially on Windows, ProgramFiles, ProgramData and whatever
writ.scopesadds (#926).Still to rule, five: upgrade and links (revises #928, overlaps #845); writing refs, worktrees and retain branches into clones registered by path (an exception to #792); the words and exit code against the ref (revises #756 and #923); whether this feature's per-layer live policy for
personalsurvives; the replan of #916, on which lane 5 now waits. Also to place: an exception in noblefactor-ops' development-process.md for machine-originated retain branches.Defaults, the lifecycle and every consequence: docs/plans/feature/847-kept-snapshots.md (draft, on
feature/924-reconcile-repair).David-Noble-at-work commentedon Sep 27, 2026
Noted by the owner, 2026-09-27: the narration carries the choice
What
writ reconcilenarrates is a requirement of this design: for every edit, what it did (retained onretain/<host>/<date>; the record restored or not), what the deployment now shows, and the path forward -- keep the change and take it back by a pull request, to the branch the pinned commit came from or elsewhere, or toss the branch. Recorded in the plan.David-Noble-at-work commentedon Sep 28, 2026
Ruled 2026-09-28, and the invariant
refs/devlore/,retain/and locked worktree registrations. Never the working tree, the index, HEAD or any other branch. Rejected: a writ-owned mirror per layer.Invariant, stated by the owner: the maintainer owns every merge. Writ never merges, commits to, moves or pushes any branch except its own
retain/*branches; every change that reaches HEAD, the default branch or main is a merge the maintainer makes, by a pull request. Written intodocs/architecture/5.1-reconciliation.md(the kept-snapshots note) with a pointer from10-command-line-interface.md§3.1; the plan refers to it.Three rulings remain: the words and the exit code against the ref (put as one table, awaiting an answer), whether the per-layer live policy survives, and the replan of #916.
doc(writ): the maintainer owns every merge; upgrade moves links; writ…
doc(writ): kept snapshots complete -- the words against the ref, live…
doc(writ): kept snapshots -- reconcile against the ref that produced …