Skip to content

Move review preferences from project settings to local preferences#1181

Merged
peyton-alt merged 14 commits into
mainfrom
review-fix-local
May 13, 2026
Merged

Move review preferences from project settings to local preferences#1181
peyton-alt merged 14 commits into
mainfrom
review-fix-local

Conversation

@peyton-alt
Copy link
Copy Markdown
Contributor

@peyton-alt peyton-alt commented May 12, 2026

https://entire.io/gh/entireio/cli/trails/356

Summary

  • Move entire review picker persistence from committed .entire/settings.json into
    clone-local <git-common-dir>/entire/preferences.json.
  • Add settings load precedence for project settings, clone-local preferences, then
    .entire/settings.local.json, including a review merge fix.
  • Prompt users to migrate project-level review settings to local preferences and
    strip the project keys after migration.

Why

entire review --edit was writing personal review preferences into project settings,
making them easy to commit. The new clone-local preference layer keeps review choices
uncommitted while sharing them across linked worktrees in the same clone.

Validation

  • mise run check
  • go test ./cmd/entire/cli/... -count=1
  • Manual smoke test: migrated review fields from .entire/settings.json to .git/ entire/preferences.json in a temp repo.

Notes

  • Real-agent E2E tests were not run, per repo guidance to only run them when
    explicitly requested

Note

Medium Risk
Medium risk because it changes settings load precedence and introduces a new clone-local preferences file plus an on-run migration that rewrites .entire/settings.json, which could affect user configuration if bugs slip through. Also tweaks entire recap auth/network error handling, impacting CLI UX for auth and connectivity failures.

Overview
entire review now stores review configuration in clone-local preferences (git common dir entire/preferences.json) instead of committed .entire/settings.json, and updates picker/help text accordingly.

Adds a one-time interactive migration prompt that copies review/review_fix_agent from project settings into clone-local preferences (without overwriting existing local prefs) and then removes those keys from .entire/settings.json.

Extends settings.Load to apply project settings → clone-local preferences → .entire/settings.local.json, and fixes merge semantics so a present review key wholesale replaces prior review config.

Improves entire recap failure UX by creating a token lookup path that distinguishes keyring read failures vs missing tokens, enforces HTTPS only when a token exists, and adds a clearer NXDOMAIN/DNS-not-found error message.

Reviewed by Cursor Bugbot for commit df11c92. Configure here.

peyton-alt and others added 4 commits May 8, 2026 17:35
The auth pre-check in runRecap collapsed three distinct failure modes
(missing token, keyring read error, insecure base URL) into a single
"Sign in with `entire login`" message, with the real error swallowed by
NewSilentError. This made flags appear broken on first run because the
function exited before any line that consumed --week, --agent, etc.

Replace the gating call with a newRecapClient helper that builds the
client even with an empty token, so FetchMeRecap always runs and surfaces
the actual cause via the existing recapLoadErrorMessage mapping (401 →
"Run `entire login` to re-authenticate.", 4xx/5xx → specific guidance).
The insecure-URL check stays but only when a token exists, with a
friendlier message that names the offending env var and the fix.

Also split DNS NXDOMAIN out of the generic network-error path so a
misconfigured ENTIRE_API_BASE_URL says "Could not resolve API host..."
instead of blaming the user's internet connection.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: d534f23017c5
Address Copilot review on #1168: newRecapClient was discarding the error
from auth.LookupCurrentToken, which silently turned a keyring read
failure (locked, permission denied, etc.) into a "no token" state.
That re-introduced the same collapsed-error UX the PR set out to fix —
the user would be told to run `entire login`, which can't help when the
keyring itself is the problem.

Wrap the underlying error in a *keyringReadError (preserving the cause
via errors.As) and route it in runRecap to a targeted message that
states the keyring is the problem and that re-login is unlikely to
help. Truly-missing tokens (keyring.ErrNotFound resolves to "", nil
upstream) still flow through to FetchMeRecap and the existing 401 →
"Run `entire login` to re-authenticate." mapping.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 60d4f6868ab0
Copilot AI review requested due to automatic review settings May 12, 2026 00:00
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR moves review configuration out of the committed project settings (.entire/settings.json) into clone-local preferences stored under the git common dir (shared across worktrees), and adds a one-time interactive migration path. It also improves entire recap error handling by distinguishing keyring-read failures and DNS NXDOMAIN from other connectivity/auth errors.

Changes:

  • Add clone-local preferences (.git/entire/preferences.json) and merge them into loaded settings with precedence between project, clone, and local overrides.
  • Update review config persistence to write/read clone-local preferences and add an interactive migration that removes legacy review keys from project settings.
  • Improve recap client creation and error messaging (keyring-read errors, insecure base URL, DNS-not-found host).

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
cmd/entire/cli/settings/settings.go Introduces clone-local preferences (path resolution, load/save, merge into settings) and adds wholesale review merge behavior.
cmd/entire/cli/settings/settings_test.go Adds tests for clone-preferences precedence and wholesale review replacement behavior.
cmd/entire/cli/review/picker.go Switches review config persistence from project settings to clone-local preferences; updates user messaging.
cmd/entire/cli/review/picker_test.go Updates tests to validate writing/reading clone-local preferences and preserving fix-agent preference.
cmd/entire/cli/review/migration.go New migration helper to move legacy review/review_fix_agent keys from project settings into clone-local preferences.
cmd/entire/cli/review/migration_test.go Adds coverage for migration prompt flow, non-overwrite behavior, and no-op cases.
cmd/entire/cli/review/cmd.go Triggers the migration prompt at the start of entire review execution.
cmd/entire/cli/recap.go Adds newRecapClient with targeted error classification (keyring vs insecure URL vs other).
cmd/entire/cli/recap_test.go Tests keyring error wrapping and DNS NXDOMAIN messaging behavior.
cmd/entire/cli/recap_errors.go Adds DNS NXDOMAIN detection to provide a more accurate user-facing error message.

Comment thread cmd/entire/cli/settings/settings.go
Comment thread cmd/entire/cli/review/picker.go Outdated
Comment thread cmd/entire/cli/review/cmd.go Outdated
Entire-Checkpoint: a1631c3ee912
@peyton-alt
Copy link
Copy Markdown
Contributor Author

@BugBot review

@peyton-alt peyton-alt changed the title Store review config in clone-local preferences Move review preferences from project settings to local preferences May 12, 2026
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit df11c92. Configure here.

@peyton-alt peyton-alt marked this pull request as ready for review May 12, 2026 02:56
@peyton-alt peyton-alt requested a review from a team as a code owner May 12, 2026 02:56
peyton-alt and others added 9 commits May 12, 2026 00:41
The review-settings migration rewrites two files (clone-local prefs +
the committed project settings). Three safety issues addressed:

1. Atomic writes (jsonutil.WriteFileAtomic). Both writers used plain
   os.WriteFile, so SIGINT or disk-full mid-write truncated the project
   .entire/settings.json to partial JSON — every subsequent CLI invocation
   then failed to parse settings. Replaces os.WriteFile in
   saveClonePreferencesToFile, saveToFile, and the migration's project
   rewrite with a temp-then-rename helper.

2. Conflict detection. The old migration silently dropped project review
   config whenever clone prefs were already populated, then unconditionally
   stripped the project keys — destroying user data without notice. Now
   detects same-key-different-value conflicts and aborts with a clear
   "reconcile manually" message; non-overlapping entries are merged.

3. Honest success message. The "Moved ..." line printed even when nothing
   actually moved (e.g. project had `review: null`). Now branches on
   whether prefs changed, distinguishing migration from cleanup.

Test changes:
- TestReviewSettingsMigration_DoesNotOverwriteExistingClonePreferences
  (which enshrined the old silent-drop) is replaced by
  _MergesNonOverlappingPrefs covering the safe merge case.
- New _RefusesConflictingPrefs asserts the abort message names the
  conflicting agent and the project file is untouched on the abort path.
- New _NoMoveCleansUpKeys covers the cleanup-only path
  (`review: null` → prefs unchanged, project keys stripped).
- Existing happy-path test's prompt assertion is updated for the new
  copy ("typically committed", "clone-local preferences"), which
  surfaces the why behind the migration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two UX fixes for the migration prompt:

1. Persist user dismissal. Declining the prompt previously persisted no
   state, so `entire review` re-prompted on every run — a real annoyance
   for teams who intentionally commit review prefs. Adds a
   ReviewMigrationDismissed bool to ClonePreferences; once set, the
   prompt is skipped silently. Users who change their mind can clear
   the key from preferences.json.

2. Only prompt for paths that interact with the picker. The prompt
   previously fired in `RunE` before the mode branches, so
   `entire review --findings` (read-only browsing) and `entire review
   --fix` (uses ReviewFixAgent only) interrupted users with a prompt
   irrelevant to what they were doing. Now gated on `!findings && !fix`.

Also documents ClonePreferences's purpose at the type level.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Four correctness/forward-compat fixes around the settings package
boundary that the new migration introduced:

1. Route project-file IO through settings. migration.go previously
   used os.ReadFile and bare jsonutil writes against
   .entire/settings.json, duplicating settings-parsing logic and
   violating CLAUDE.md's "Settings access must go through the settings
   package" rule. Adds LoadProjectRaw / SaveProjectRaw helpers; the
   migration now calls those and never touches paths/os directly.

2. Lenient decoding for clone preferences. The file lives in .git/
   and persists across CLI versions running against the same clone,
   so DisallowUnknownFields meant a newer binary's added field bricked
   an older binary's settings.Load. Dropped strict decoding here;
   accepted trade-off is that hand-edit typos in this file are silently
   ignored (documented in-place).

3. Surface ClonePreferencesPath failures. settings.Load() previously
   dropped any error from ClonePreferencesPath silently, so a git PATH
   issue or .git/ permission failure would silently disable the prefs
   layer and the user's picker choices would seem to vanish. Now logs
   at Debug — findable via ENTIRE_LOG_LEVEL=debug when triaging
   "my preferences aren't loading" without spamming the non-repo
   commands that legitimately fail this resolution.

4. Non-interactive migration logs at Warn. Scripted/CI invocations
   previously emitted a one-shot stderr hint that scrolled past
   unnoticed. Adds a structured logging.Warn alongside the stderr
   message so operators tailing .entire/logs/ catch the pending
   migration even when the user never sees the prompt.

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

Three small follow-ups from review of this PR:

1. WriteFileAtomic now fsyncs the temp file between Write and Close. The
   prior implementation relied on Close to flush, which doesn't sync
   data to disk on its own — some filesystems (notably ext4 with
   non-default mount options) can surface the rename as completed
   while the file is still empty after a hard crash. tmp.Sync before
   Close closes that gap and matches the comment's "leaves the
   original file intact rather than a truncated partial" promise.

2. Bail the migration up front when .entire/settings.local.json
   already has review keys. settings.local.json overrides clone-local
   preferences via mergeJSON's wholesale-replace path, so migrating
   without cleaning the local file first silently nullifies the
   migration on the next settings.Load — the user clicks "yes",
   their config moves to clone prefs, then the local override hides
   it. Now detects the precondition, prints a clear "remove these
   keys from settings.local.json" message, and returns without
   prompting or setting ReviewMigrationDismissed (this is a fixable
   precondition, not a user-rejected migration). New settings helper
   LoadLocalRaw mirrors LoadProjectRaw so the IO stays inside the
   settings package per CLAUDE.md's boundary rule.

3. Expand the strict/lenient decoding comment to explain why
   EntireSettings stays strict while ClonePreferences is lenient.
   The prior comment justified the lenient half but a reader of
   loadFromFile saw DisallowUnknownFields without a paired
   explanation. The new comment makes the asymmetry deliberate and
   self-documenting.

New test TestReviewSettingsMigration_BailsOnLocalSettingsReviewKeys
pins the bail contract: prompt not called, project file untouched,
ReviewMigrationDismissed not set, and the warning message names
both settings.local.json and the conflicting keys.

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

The redaction custom-rules test merged in from main calls
loadMergedSettings(base, local), but PR 1181's merge adds a
preferencesFileAbs argument between them. Passing "" for that arg
skips the clone-preferences layer, which matches the test's intent
(it only exercises project + local merge).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two follow-ups from review of this PR:

1. WriteFileAtomic now best-effort fsyncs the parent directory after
   the rename. The prior code fsynced the temp file (so contents are
   durable) and renamed it into place, but never synced the directory
   entry. On a hard crash POSIX can replay the file-contents fsync
   without the rename, leaving the original file in place even though
   os.Rename returned nil — which silently breaks the docstring's
   "leaves the original intact rather than a truncated partial"
   promise. The fsync is best-effort because Windows doesn't support
   directory sync and the rename has already succeeded by the time we
   reach it; failing here would mislead callers whose write is done.

2. Add direct test coverage for the package. WriteFileAtomic is now
   the entry point for every settings writer in the codebase
   (settings.go:373, 487, 1134) but had no tests of its own. Covers
   happy path, replace-existing, perm-bit application, no-leftover
   on success, temp cleanup on rename failure, and missing-parent
   error propagation. The rename-failure test reaches the failing
   step by renaming onto a non-empty directory, which fails on every
   POSIX filesystem and on Windows without needing fault injection.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
golangci-lint config has errcheck.check-blank=true so `_ = d.Sync()`
gets flagged even with blank assignment, and gosec G304 flags
os.Open(dir) on a variable path. Both are intentional here — dir is
filepath.Dir of the caller-supplied destination, and the directory
fsync is best-effort because the rename has already succeeded. Add
nolint comments matching the codebase pattern in transcript.go and
trace.go.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@peyton-alt peyton-alt merged commit 77fe0c5 into main May 13, 2026
9 checks passed
@peyton-alt peyton-alt deleted the review-fix-local branch May 13, 2026 20:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants