cli/enable: gate import offer on checkpoint policy; don't auto-import non-interactively · Entire
cli/enable: gate import offer on checkpoint policy; don't auto-import non-interactively
Two fixes from PR review:
- Validate the checkpoint policy before importing agent history, matching the standalone
entire importcommand's ensureCheckpointPolicyAllowsCheckpointData check. Best-effort: an unsupported/unreadable policy skips the import (logged + noted) rather than failing enable. - Only auto-import all eligible agents under an explicit --yes ("accept all defaults"). A non-interactive enable without --yes (script, pipe, or an agent with no TTY) no longer silently imports history; it points at
entire importinstead.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com
Sessions
01KWWDN6KCZAM8GGPP06D8A1HDView transcript
Changes
2
cmd/entire/cli
Msetup_import.go+32/-4
Msetup_import_test.go+70
38 unmodified lines
// lookback, matching `entire import`). It is best-effort — discovery or import
// failures are logged and reported to the user but never fail enable.
//
// Interactive runs present a multi-select with nothing pre-checked, so import
// only happens when the user actively selects agents. Non-interactive runs
// (`--yes` or no TTY) auto-import all eligible agents.
// Import only happens on an explicit choice: an interactive run presents a
// multi-select (nothing pre-checked) and imports what the user selects; `--yes`
// ("accept all defaults") auto-imports all eligible agents. A non-interactive
// run without `--yes` (a script, a piped shell, or an agent with no TTY) makes
// no choice, so it imports nothing and just points at `entire import` — silently
// importing history there would be surprising.
func maybeOfferSessionImport(ctx context.Context, w io.Writer, agents []agent.Agent, opts EnableOptions, firstRun bool) {
if !firstRun {
return
}
selected := eligible
if !opts.Yes && interactive.CanPromptInteractively() {
if !opts.Yes {
if !interactive.CanPromptInteractively() {
// Non-interactive without --yes: don't silently import. Leave a
// pointer so scripted/agent enables can still import on demand.
logging.Info(ctx, "session import offer skipped: non-interactive without --yes", "eligible", len(eligible))
fmt.Fprintf(w, "Found importable history for %s. Run 'entire import <agent>' to import it.\n", pluralAgents(len(eligible)))
return
}
selected, err = sessionImportPrompt(ctx, w, eligible)
if err != nil {
// Best-effort: a prompt/UI failure must never fail enable. Log,
}
}
}
// Gate on the checkpoint policy before writing any checkpoint data, matching
// the standalone `entire import` command. Best-effort: an unsupported or
// unreadable policy skips the import (logged and noted) instead of failing
// enable, since the offer must never break enable.
if err := ensureCheckpointPolicyAllowsCheckpointData(ctx, repo); err != nil {
logging.Warn(ctx, "session import skipped: checkpoint policy not satisfied", "error", err)
fmt.Fprintf(w, "Note: skipping agent history import: %v\n", err)
return
}
// Load repo/user-configured redaction before any checkpoint write, matching
// import_cmd.go; without it only always-on secret scanning would run.
strategy.EnsureRedactionConfigured()
}
// pluralAgents renders an agent count with correct pluralization.
func pluralAgents(n int) string {
if n == 1 {
return "1 agent"
}
return fmt.Sprintf("%d agents", n)
}
func TestMaybeOfferSessionImport_NonInteractiveWithoutYesSkips(t *testing.T) {
// Not parallel: overrides seams and chdirs into a temp repo.
dir := t.TempDir()
testutil.InitRepo(t, dir)
t.Chdir(dir)
// No ENTIRE_TEST_TTY => CanPromptInteractively() is false (non-interactive),
// e.g. a scripted or agent-driven enable.
promptCalled := false
var ran []eligibleImport
withImportSeams(t,
func(context.Context, []agent.Agent, string) []eligibleImport {
return []eligibleImport{{displayName: testAgentClaude, sessionCount: 3}}
},
func(context.Context, io.Writer, []eligibleImport) ([]eligibleImport, error) {
promptCalled = true
return nil, nil
},
func(_ context.Context, _ io.Writer, _ string, sel []eligibleImport) { ran = sel },
)
// No --yes and no TTY: neither prompt nor auto-import; just hint at the
// manual command.
var buf bytes.Buffer
maybeOfferSessionImport(context.Background(), &buf, nil, EnableOptions{}, true)
if promptCalled {
t.Error("prompt shown in a non-interactive context")
}
if len(ran) != 0 {
t.Errorf("auto-imported %d agent(s) without --yes in a non-interactive context; expected skip", len(ran))
}
if got := buf.String(); !strings.Contains(got, "entire import") {
t.Errorf("expected a pointer to 'entire import', got %q", got)
}
}
func TestMaybeOfferSessionImport_NoEligibleIsNoOp(t *testing.T) {
dir := t.TempDir()
testutil.InitRepo(t, dir)
}
func TestRunSelectedImports_UnsatisfiablePolicySkips(t *testing.T) {
dir := t.TempDir()
testutil.InitRepo(t, dir)
t.Chdir(dir)
ctx := context.Background();
// Install a checkpoint policy this CLI cannot satisfy (a future format).
// The gate must skip the import, matching the standalone `entire import`
// command's ensureCheckpointPolicyAllowsCheckpointData check.
repo, err := openRepository(ctx)
if err != nil {
t.Fatalf("open repository: %v", err)
}
future := checkpointpolicy.Policy{CheckpointVersion: "branch-v99", CheckpointMinVersion: "branch-v99"};
if _, err := checkpointpolicy.WriteLocal(ctx, repo, plumbing.ZeroHash, future); err != nil {
t.Fatalf("write local policy: %v", err)
}
repo.Close();
var buf bytes.Buffer
runSelectedImports(ctx, &buf, dir, []eligibleImport{{displayName: testAgentClaude}})
if got := buf.String(); !strings.Contains(got, "skipping agent history import") {
t.Errorf("expected a skip note for an unsatisfiable checkpoint policy, got %q", got)
}
}
func TestMaybeOfferSessionImport_PromptErrorIsBestEffort(t *testing.T) {
dir := t.TempDir()
testutil.InitRepo(t, dir)
}