# feat: select checkpoint backend via --checkpoint-backend on enable/configure

`e79baf6`→[main](/content/gh/entireio/cli/commits/main/index.html)·Soph·1w ago·6 files·+415 added/-41 removed

The git-refs checkpoint backend was fully wired to run but had no config writer, so it could only be turned on via ENTIRE_CHECKPOINTS_PRIMARY or by hand-editing settings. Add a simple selection path for both an existing repo and initial enable.

- `--checkpoint-backend branch|refs` (also accepts canonical git-branch/git-refs)
on `entire enable` and `entire configure`. Covers fresh repos, the --agent non-interactive path, and already-set-up repos (enable behaves like configure).
- First-time interactive setup prompts for the backend, defaulting to branch; choosing the default writes no config block, keeping settings.json pristine.
- Backend value is validated up front (before repo bootstrap/hook install) via a new checkpoint.ValidatePrimaryBackend, which is now the single source of the "primary must be git-backed" rule (buildPrimary delegates to it).
- Switching the primary preserves existing mirrors, dropping only one that would collide with the new primary (one-of-each-type rule). Existing hex checkpoints stay readable via kind-routing; no migration needed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

## Sessions

01KWY0P5GFDJKAXB0E2EMQGMF5View transcript

## Changes

6

- cmd/entire/cli

- checkpoint

- Mopen.go+1/-5

- Mregistry.go+18

- Mregistry_test.go+33

- Acheckpoint_backend.go+137

- Acheckpoint_backend_test.go+131

- Msetup.go+95/-36

```
138 unmodified lines

139
140
141
142
143
142
143
144
146
147
148
145
146
147

138 unmodified lines

// the primary's record through the repo and its refs, so a non-git-backed
// primary is rejected rather than silently half-supported.
func buildPrimary(ctx context.Context, env OpenEnv, typ string, raw json.RawMessage) (PersistentStore, error) {
    b, err := lookupBackend(typ)
    if err != nil {
        if err := ValidatePrimaryBackend(typ); err != nil {
            return nil, fmt.Errorf("checkpoints.primary: %w", err)
        }
        if !b.gitBacked {
            return nil, fmt.Errorf("checkpoints.primary.type %q cannot be the primary: only git-backed backends (e.g. %q) may be the primary", typ, BackendTypeGitBranch)
        }
    }
    return build(ctx, env, typ, raw)
}
```

Mcmd/entire/cli/checkpoint/open.go+1/-5

```
92 unmodified lines

93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116

92 unmodified lines

return b, nil
}

// ValidatePrimaryBackend reports an error unless typ names a registered backend
// that may serve as the primary. Only git-backed backends qualify: a
// non-git-backed or unknown type is rejected with a descriptive message that
// names the valid types. This is the single source of the "primary must be
// git-backed" rule — buildPrimary delegates here, and selection surfaces (entire
// enable / configure --checkpoint-backend) call it to reject a bad backend
// before writing it to settings, rather than failing later in Open.
func ValidatePrimaryBackend(typ string) error {
    b, err := lookupBackend(typ)
    if err != nil {
        return err
    }
    if !b.gitBacked {
        return fmt.Errorf("checkpoint backend %q cannot be the primary: only git-backed backends (e.g. %q, %q) may be the primary", typ, BackendTypeGitBranch, BackendTypeGitRefs)
    }
    return nil
}

// build constructs the store for the named backend type.
func build(ctx context.Context, env OpenEnv, typ string, cfg json.RawMessage) (PersistentStore, error) {
    b, err := lookupBackend(typ)
```

Mcmd/entire/cli/checkpoint/registry.go+18

```
36 unmodified lines

37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75

36 unmodified lines

assert.Contains(t, err.Error(), BackendTypeGitBranch);

func TestValidatePrimaryBackend_GitBackedTypesAllowed(t *testing.T) {
    t.Parallel();

require.NoError(t, ValidatePrimaryBackend(BackendTypeGitBranch));
    require.NoError(t, ValidatePrimaryBackend(BackendTypeGitRefs));
}

func TestValidatePrimaryBackend_UnknownTypeRejected(t *testing.T) {
    t.Parallel();

err := ValidatePrimaryBackend("definitely-not-a-backend");
    require.Error(t, err);
    assert.Contains(t, err.Error(), `unknown checkpoint backend type "definitely-not-a-backend"`);
    // The error lists registered types so a typo is debuggable.
    assert.Contains(t, err.Error(), BackendTypeGitBranch);
}

func TestValidatePrimaryBackend_NonGitBackedRejected(t *testing.T) {
    t.Parallel();

// Register a mirror-only (non-git-backed) backend and confirm it cannot be the primary. The unique type name avoids colliding with the built-ins.
    const typ = "test-mirror-only-primary-check";
    Register(typ, func(context.Context, OpenEnv, json.RawMessage) (PersistentStore, error) {
        return nil, nil //nolint:nilnil // never constructed; validation fails before build
    });

err := ValidatePrimaryBackend(typ);
    require.Error(t, err);
    assert.Contains(t, err.Error(), "cannot be the primary");
    assert.Contains(t, err.Error(), BackendTypeGitRefs);
}

func TestRegistry_GitBranchFactoryIgnoresConfig(t *testing.T) {
    t.Parallel();
```

Mcmd/entire/cli/checkpoint/registry_test.go+33

```
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131

package cli

import (
    "context"
    "fmt"
    "io"
    "slices"
    "strings"

"charm.land/huh/v2"

"github.com/entireio/cli/cmd/entire/cli/checkpoint"
    "github.com/entireio/cli/cmd/entire/cli/paths"
    "github.com/entireio/cli/cmd/entire/cli/settings"
)

// Friendly aliases for the two selectable checkpoint backends. Users type these
// on --checkpoint-backend; they map to the canonical backend types stored in
// settings (checkpoint.BackendTypeGitBranch / checkpoint.BackendTypeGitRefs).
const (
    checkpointBackendBranchAlias = "branch"
    checkpointBackendRefsAlias   = "refs"
)

// resolveCheckpointBackendType maps a user-facing backend name to the canonical
// settings backend type and validates it may serve as the primary. It accepts
// the friendly aliases "branch"/"refs" and the canonical "git-branch"/"git-refs"
// (case-insensitive). An unknown or non-git-backed value is rejected via the
// checkpoint registry, so the error text stays in sync with the backend list.
func resolveCheckpointBackendType(name string) (string, error) {
    typ := strings.ToLower(strings.TrimSpace(name))
    switch typ {
    case checkpointBackendBranchAlias:
        typ = checkpoint.BackendTypeGitBranch
    case checkpointBackendRefsAlias:
        typ = checkpoint.BackendTypeGitRefs
    }
    if err := checkpoint.ValidatePrimaryBackend(typ); err != nil {
        return "", fmt.Errorf("invalid --%s: %w", flagCheckpointBackend, err)
    }
    return typ, nil
}

// applyCheckpointBackend sets the primary checkpoint backend on settings,
// preserving any existing mirrors except one whose type would collide with the
// new primary (the one-of-each-type topology rule enforced in checkpoint.Open).
// Switching the primary on an existing repo is safe: new checkpoints use the new
// backend while read routing keeps prior checkpoints readable in their original
// format.
func applyCheckpointBackend(s *EntireSettings, typ string) {
    cfg := s.Checkpoints
    if cfg == nil {
        cfg = &settings.CheckpointsConfig{}
    }
    cfg.Primary = settings.BackendConfig{Type: typ}
    cfg.Mirrors = slices.DeleteFunc(cfg.Mirrors, func(m settings.BackendConfig) bool {
        return m.Type == typ
    })
    s.Checkpoints = cfg
}

// applyCheckpointBackendFlag resolves and applies a --checkpoint-backend value to
// settings when it is non-empty; a no-op otherwise. Used by the fresh-repo enable
// paths (interactive setup and --agent), which mutate an in-memory settings
// object before their own save. Existing-repo enable and configure use
// updateCheckpointBackend instead.
func applyCheckpointBackendFlag(s *EntireSettings, backend string) error {
    if backend == "" {
        return nil
    }
    typ, err := resolveCheckpointBackendType(backend)
    if err != nil {
        return err
    }
    applyCheckpointBackend(s, typ)
    return nil
}

// updateCheckpointBackend persists opts.CheckpointBackend to the target settings
// file. Used by `entire configure` and by `entire enable` on repos that are
// already set up (both operate on an on-disk file rather than the in-memory
// settings the fresh-setup flow builds).
func updateCheckpointBackend(ctx context.Context, w io.Writer, opts EnableOptions) error {
    typ, err := resolveCheckpointBackendType(opts.CheckpointBackend)
    if err != nil {
        return err
    }

targetFile, configDisplay := settingsTargetFile(ctx, opts.UseLocalSettings, opts.UseProjectSettings)
    targetFileAbs, err := paths.AbsPath(ctx, targetFile)
    if err != nil {
        targetFileAbs = targetFile
    }

s, err := settings.LoadFromFile(targetFileAbs)
    if err != nil {
        return fmt.Errorf("failed to load settings: %w", err)
    }

applyCheckpointBackend(s, typ)

if err := saveSettingsToTarget(ctx, s, targetFile); err != nil {
        return fmt.Errorf("failed to save settings: %w", err)
    }

fmt.Fprintf(w, "✓ Checkpoint backend set to %s (%s)\n", typ, configDisplay)
    return nil
}

// promptCheckpointBackend asks the user to choose a checkpoint storage backend
// during first-time interactive setup. The default is the git-branch backend;
// the git-refs backend is offered as the selectable alternative. It returns the
// canonical backend type, or "" when the user kept the default so the caller can
// skip writing a redundant config block. Callers must gate this on an
// interactive terminal.
func promptCheckpointBackend() (string, error) {
    choice := checkpoint.BackendTypeGitBranch
    form := NewAccessibleForm(
        huh.NewGroup(
            huh.NewSelect[string]().
                Title("Checkpoint storage backend").
                Description("How Entire stores committed session checkpoints in your repo.").
                Options(
                    huh.NewOption("Branch — one shared branch, entire/checkpoints/v1 (default)", checkpoint.BackendTypeGitBranch),
                    huh.NewOption("Refs — one git ref per checkpoint (experimental)", checkpoint.BackendTypeGitRefs),
                ).
                Value(&choice),
        ),
    )
    if err := form.Run(); err != nil {
        return "", fmt.Errorf("checkpoint backend selection: %w", err)
    }
    if choice == checkpoint.BackendTypeGitBranch {
        return "", nil
    }
    return choice, nil
}
