Merge pull request #1799 from entireio/feat/storage-question-recommended · Entire
Merge pull request #1799 from entireio/feat/storage-question-recommended
c6fc04f→main·
peyton-alt·9h ago·6 files·+125 added/-12 removed
enable: restore checkpoint-storage question with git-refs recommended
Changes
6
cmd/entire/cli
Mcheckpoint_backend.go+42
Mcheckpoint_backend_test.go+21
Msetup.go+46/-10
Msetup_test.go+14
docs/architecture
Mref-checkpoint-backend.md+1/-1
Msessions-and-checkpoints.md+1/-1
6 unmodified lines
7
8
9
10
11
12
13
14
61 unmodified lines
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
6 unmodified lines
"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"
61 unmodified lines
return nil
}
// checkpointBackendChoices returns the storage picker's options — git-refs
// first, labeled recommended — and the recommended value the caller
// pre-selects.
// Split from promptCheckpointBackend so the ordering/labeling contract is
// unit-testable without a TTY.
func checkpointBackendChoices() (opts []huh.Option[string], recommended string) {
return []huh.Option[string]{
huh.NewOption("Refs — one git ref per checkpoint (recommended)", checkpoint.BackendTypeGitRefs),
huh.NewOption("Branch — one shared branch, entire/checkpoints/v1", checkpoint.BackendTypeGitBranch),
}, checkpoint.BackendTypeGitRefs
}
// promptCheckpointBackend asks the user to choose a checkpoint storage backend
// during first-time interactive setup, with the git-refs backend pre-selected
// as the recommendation — most users should just press Enter. It returns the
// chosen canonical backend type; cancellation (Ctrl+C or a cancelled ctx)
// prints a cancellation note and returns "" (a soft skip, nil error, like
// other setup prompts) so the caller falls through to the recommended
// default. Callers must gate this on
// an interactive terminal (and skip it when ENTIRE_CHECKPOINTS_PRIMARY is
// active — the env fully replaces settings, so an answer could not take
// effect and would only write diverging config).
func promptCheckpointBackend(ctx context.Context, w io.Writer) (string, error) {
opts, recommended := checkpointBackendChoices()
choice := recommended
form := NewAccessibleForm(
huh.NewGroup(
huh.NewSelect[string]().
Title("Checkpoint storage").
Description("How Entire stores committed session checkpoints in your repo.").
Options(opts...).
Value(&choice),
),
)
if err := form.RunWithContext(ctx); err != nil {
return "", handleFormCancellation(w, "Checkpoint storage selection", err)
}
return choice, 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
Mcmd/entire/cli/checkpoint_backend.go+42
2 unmodified lines
3 4 5 6 7 8 9 120 unmodified lines
130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152
2 unmodified lines
import ( "context" "io" "strings" "testing"
"github.com/stretchr/testify/assert" 120 unmodified lines
require.Error(t, err) assert.Contains(t, err.Error(), flagCheckpointBackend)
// The storage picker's contract from the team decision: the question stays, // but git-refs is listed first and pre-selected as the recommendation — // "default" wording is gone, one Enter accepts refs. func TestCheckpointBackendChoices_RefsRecommendedFirst(t *testing.T) { t.Parallel() opts, recommended := checkpointBackendChoices() if recommended != checkpoint.BackendTypeGitRefs { t.Errorf("recommended = %q, want git-refs pre-selected", recommended) } if len(opts) != 2 || opts[0].Value != checkpoint.BackendTypeGitRefs { t.Fatalf("options = %+v, want refs listed first", opts) } if !strings.Contains(opts[0].Key, "(recommended)") { t.Errorf("refs label = %q, want '(recommended)' suffix", opts[0].Key) } if strings.Contains(opts[0].Key+opts[1].Key, "default") { t.Errorf("labels must not say 'default' (team decision: recommended, not default): %q / %q", opts[0].Key, opts[1].Key) } }
Mcmd/entire/cli/checkpoint_backend_test.go+21
784 unmodified lines
785 786 787 788 788 789 790 791 139 unmodified lines
931 932 933 934 934 935 936 937 301 unmodified lines
1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1242 1243 1244 1245 1250 1246 1247 1248 1249 105 unmodified lines
1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400
784 unmodified lines
cmd.Flags().BoolVarP(&opts.ForceHooks, flagForce, "f", false, "Reinstall the Entire git hook") cmd.Flags().BoolVar(&opts.SkipPushSessions, flagSkipPushSessions, false, "Disable automatic pushing of session logs on git push") cmd.Flags().StringVar(&opts.CheckpointRemote, flagCheckpointRemote, "", "Checkpoint remote in provider:owner/repo format (e.g., github:org/checkpoints-repo)") cmd.Flags().StringVar(&opts.CheckpointBackend, flagCheckpointBackend, "", "Checkpoint storage backend: refs (one git ref per checkpoint; default for new setups) or branch (shared entire/checkpoints/v1 branch)") cmd.Flags().StringVar(&opts.CheckpointBackend, flagCheckpointBackend, "", "Checkpoint storage backend: refs (one git ref per checkpoint; recommended) or branch (shared entire/checkpoints/v1 branch)") cmd.Flags().StringVar(&summarizeProvider, flagSummarizeAgent, "", "Set the provider used by explain --generate (e.g., claude-code, codex, gemini, pi, cursor, copilot-cli)") cmd.Flags().StringVar(&summarizeModel, flagSummarizeModel, "", "Set the model hint used by explain --generate") cmd.Flags().IntVar(&summarizeTimeoutSeconds, flagSummarizeTimeout, 0, "Set the hard deadline (seconds) for explain --generate summary generation. 0 clears (falls back to 5m default).") 139 unmodified lines
opts.applyStrategyOptions(settings)
// Checkpoint storage backend. An explicit --checkpoint-backend always wins. // Otherwise every first run gets the git-refs backend, written explicitly // into the new settings file: a storage-topology question is unanswerable // during first-time setup (the old wizard prompt), and the config-less // runtime default stays git-branch so existing repos are untouched. if opts.CheckpointBackend == "" && firstRun { opts.CheckpointBackend = firstRunCheckpointBackendDefault() backend, err := resolveFirstRunCheckpointBackend(ctx, w, opts, firstRun) if err != nil { return err } if err := applyCheckpointBackendFlag(settings, opts.CheckpointBackend); err != nil { if err := applyCheckpointBackendFlag(settings, backend); err != nil { return err }
105 unmodified lines
fmt.Fprintln(w, "\nTo add more agents, run entire agent add <name>.")
}
// resolveFirstRunCheckpointBackend decides the checkpoint storage backend // the setup flow writes. An explicit --checkpoint-backend always wins. // Otherwise a first interactive setup asks, with git-refs pre-selected as // the recommendation (one Enter for most users); non-interactive/--yes // first runs take the recommendation silently, and a cancelled prompt takes // it with an explicit note (unless the command context itself was cancelled // — then enable stops). Either way the choice is written explicitly into // the new settings file, and the config-less runtime fallback stays // git-branch so existing repos are untouched. The prompt is skipped while // ENTIRE_CHECKPOINTS_PRIMARY is active (firstRunCheckpointBackendDefault // returns ""): the env fully replaces settings, so an answer could not take // effect and would only write diverging config. func resolveFirstRunCheckpointBackend(ctx context.Context, w io.Writer, opts EnableOptions, firstRun bool) (string, error) { backend := opts.CheckpointBackend if backend == "" && firstRun && !opts.Yes && firstRunCheckpointBackendDefault() != "" && interactive.CanPromptInteractively() { chosen, err := promptCheckpointBackend(ctx, w) if err != nil { return "", err } if ctx.Err() != nil { // A cancelled command context (SIGINT/SIGTERM) surfaces as a // form cancellation, but the user asked to stop: setup must not // adopt a default and keep mutating the repo. return "", fmt.Errorf("checkpoint storage selection: %w", ctx.Err()) } if chosen == "" { // Cancelled prompt: the recommendation is adopted, but never // silently — every other cancelled setup prompt skips its // action, so persisting a choice here must be disclosed. fmt.Fprintln(w, "Using the recommended git-refs checkpoint storage.") } backend = chosen // "" (cancelled) falls through to the recommendation } if backend == "" && firstRun { backend = firstRunCheckpointBackendDefault() } return backend, nil }
// firstRunCheckpointBackendDefault is the backend written on first-time // setups when --checkpoint-backend wasn't passed: the git-refs store (a // storage-topology question is unanswerable during first-time setup). Empty —
Mcmd/entire/cli/setup.go+46/-10
3925 unmodified lines
3926
3927
3928
3929
3930
3931
3932
3933
3934
3935
3936
3937
3938
3939
3940
3941
3942
3943
3944
3945
3925 unmodified lines
}
})
t.Run("non-interactive first run without --yes takes the recommendation", func(t *testing.T) {
// The most common real path: a headless first run without --yes
// (go test => CanPromptInteractively false). The storage prompt must
// be skipped and the recommendation written — every other first-run
// subtest passes Yes: true, so this is the only coverage of the
// !opts.Yes non-TTY branch. Telemetry: false dodges the telemetry
// prompt, which (pre-existing) has no headless guard.
setupTestRepo(t)
cfg := enable(t, EnableOptions{Telemetry: false})
if cfg == nil || cfg.Primary.Type != checkpoint.BackendTypeGitRefs {
t.Errorf("Checkpoints = %+v, want the git-refs recommendation", cfg)
}
})
t.Run("re-run of an existing config-less repo stays config-less", func(t *testing.T) {
setupTestRepo(t)
// A repo set up before this change: settings.json exists, no
Mcmd/entire/cli/setup_test.go+14
163 unmodified lines
164 165 166 167 167 168 169 170
163 unmodified lines
| State | primary |
Behavior |
|---|---|---|
| Config-less fallback | git-branch |
Hex checkpoints on the v1 branch; unchanged legacy behavior for repos set up before the git-refs default (new setups get an explicit git-refs primary written by entire enable) |
| Config-less fallback | git-branch |
Hex checkpoints on the v1 branch; unchanged legacy behavior for repos set up before the git-refs default (new setups write an explicit primary — git-refs as the recommended pick unless the setup question chose branch) |
| Refs-only | git-refs |
New checkpoints are ULIDs written as per-checkpoint refs; pre-existing hex/v1 checkpoints stay readable via the read-routing fallback |