Skip to content

Stability between upgrades: a per-layer policy, pinned or live -- deployed links point at the kept snapshot or the working tree #847

Open
0 of 2 issues completed

Description

@David-Noble-at-work

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 by readlink. Thread:WritOrigin beat 3 (Record the deployed commit for each layer source #473) becomes structural for pinned
    layers rather than recorded.
  • --allow-dirty becomes 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

  1. 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_HOME for 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.
  2. writ upgrade neither pins layers nor records origin: a regenerated template or secret has no provenance #844 — upgrade pins and records origin the way deploy does. Without it there is nothing to
    re-pin to.
  3. writ upgrade does its whole job: links added, orphans removed, stale links relinked, copies regenerated -- what the name promises #845 — upgrade does 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

  1. added
    featureDeliverable unit within an epic
    Epic:LayerSplitSplit the dotfiles into base, team, and personal layers
    Thread:WritOriginCross-cutting: writ records and verifies where a deployment came from
    on Sep 6, 2026
  2. David-Noble-at-work commented on Sep 27, 2026

    @David-Noble-at-work
    ContributorAuthor

    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:

    reconcile should run against the git ref that produced the result. we deploy, we upgrade, we reconcile. when we deploy, we have one ref. when we upgrade we have another ref. when we reconcile we run against the ref that produced the result.

    we must keep worktrees to support linkage. i realize this is a design change. we cannot properly support a deployment lifetime without keeping our snapshot and the mechanism for retaining this is a worktree.

    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:

    1. A private ref namespace pins each snapshot's commit (refs/devlore/...), not tags: tags are pushed by git push --tags and fetched by every clone, deleted by git fetch --prune-tags, and read by git tag and git describe.
    2. Snapshots are writable. "Reconcile detects drift with the data at hand and can easily restore content."
    3. Retention is a branch cut from the pinned commit, and the developer opens the pull request. "I'm talking about the PR process in this case." Retain commits the edit on 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.
    4. An edit is always retained. A plain writ reconcile always 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.scopes adds (#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 personal survives; 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).

  3. David-Noble-at-work commented on Sep 27, 2026

    @David-Noble-at-work
    ContributorAuthor

    Noted by the owner, 2026-09-27: the narration carries the choice

    make note that the reconcile narrations will be critical to understanding the result of reconciliation and the path forward for the deployment. It's a simple choice that needs to be well understood: retain the changes, toss the changes, and optionally follow-up with a PR to the branch of origin or elsewhere.

    What writ reconcile narrates is a requirement of this design: for every edit, what it did (retained on retain/<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.

  4. David-Noble-at-work commented on Sep 28, 2026

    @David-Noble-at-work
    ContributorAuthor

    Ruled 2026-09-28, and the invariant

    1. Upgrade moves the links. When the checkout moves from C to D, upgrade pins D and relinks the layer's links to the snapshot of D as well as re-rendering copies: one commit per layer per lifetime. Revises lane 11 (upgrade updates the record: it refreshes the stale copies only, and its receipt stacks on the current lifetime #928, "the stale copies only"). A file new in D waits for deploy; a file gone in D keeps its link on C's snapshot and is reported for deploy.
    2. Writ writes into the owner's clones. Refs, retain branches and locked worktree registrations live in the registered layer clone, whoever owns it: an exception to writ repo remove leaves the clone it made in the writ-owned home #792, scoped to 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 into docs/architecture/5.1-reconciliation.md (the kept-snapshots note) with a pointer from 10-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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Epic:LayerSplitSplit the dotfiles into base, team, and personal layersThread:WritOriginCross-cutting: writ records and verifies where a deployment came fromfeatureDeliverable unit within an epic

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions