Skip to content

Commit 2f5f7c1

Browse files
authored
Merge pull request #1350 from entireio/doctor-v11-mirror-check
doctor: check and repair the v1.1 committed-read mirror
2 parents af71366 + 5a30e1f commit 2f5f7c1

7 files changed

Lines changed: 603 additions & 18 deletions

File tree

cmd/entire/cli/doctor.go

Lines changed: 127 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,18 @@ Checks performed:
3434
entire/checkpoints/v1 branches share no common ancestor (caused by a
3535
previous bug). Fixes by cherry-picking local checkpoints onto remote tip.
3636
37+
2. Checkpoint read mirror (checkpoints v1.1 only): detects when the
38+
local-only refs/entire/checkpoints/v1.1 read mirror is missing, stale,
39+
or diverged relative to entire/checkpoints/v1, which makes reads miss
40+
checkpoints. Fixes by pointing the mirror at the v1 tip.
41+
3742
When Codex hooks are installed:
38-
2. Codex hook trust: warn when hooks declared in .codex/hooks.json
43+
3. Codex hook trust: warn when hooks declared in .codex/hooks.json
3944
lack a trusted_hash entry in the user's Codex config (i.e. /hooks
4045
review hasn't run yet on this machine, or a newer entire release
4146
added a hook the user hasn't approved yet).
4247
43-
3. Stuck sessions: sessions stuck in ACTIVE or ENDED phase that need cleanup.
48+
4. Stuck sessions: sessions stuck in ACTIVE or ENDED phase that need cleanup.
4449
4550
A session is considered stuck if:
4651
- It is in ACTIVE phase with no interaction for over 1 hour
@@ -90,6 +95,15 @@ func runSessionsFix(cmd *cobra.Command, force bool) error {
9095
fmt.Fprintf(cmd.ErrOrStderr(), "Error: metadata check failed: %v\n", metadataErr)
9196
finalErr = NewSilentError(fmt.Errorf("metadata check failed: %w", metadataErr))
9297
}
98+
99+
// Check 2: v1.1 read-mirror drift (after check 1: reconciliation rewrites
100+
// v1 and re-mirrors).
101+
if mirrorErr := checkCommittedMetadataMirror(cmd, force); mirrorErr != nil {
102+
fmt.Fprintf(cmd.ErrOrStderr(), "Error: checkpoint read mirror check failed: %v\n", mirrorErr)
103+
if finalErr == nil {
104+
finalErr = NewSilentError(fmt.Errorf("checkpoint read mirror check failed: %w", mirrorErr))
105+
}
106+
}
93107
fmt.Fprintln(cmd.OutOrStdout())
94108

95109
ctx := cmd.Context()
@@ -355,22 +369,11 @@ func checkDisconnectedMetadata(cmd *cobra.Command, force bool) error {
355369
fmt.Fprintln(w, " Fix: cherry-pick local checkpoints onto remote tip (preserves all data).")
356370

357371
if !force {
358-
var confirmed bool
359-
form := NewAccessibleForm(
360-
huh.NewGroup(
361-
huh.NewConfirm().
362-
Title("Fix disconnected metadata branches?").
363-
Value(&confirmed),
364-
),
365-
)
366-
if formErr := form.Run(); formErr != nil {
367-
if errors.Is(formErr, huh.ErrUserAborted) {
368-
return nil
369-
}
370-
return fmt.Errorf("prompt failed: %w", formErr)
372+
proceed, promptErr := confirmDoctorFix(ctx, w, "Fix disconnected metadata branches?")
373+
if promptErr != nil {
374+
return promptErr
371375
}
372-
if !confirmed {
373-
fmt.Fprintln(w, " -> Skipped")
376+
if !proceed {
374377
return nil
375378
}
376379
}
@@ -383,6 +386,113 @@ func checkDisconnectedMetadata(cmd *cobra.Command, force bool) error {
383386
return nil
384387
}
385388

389+
// checkCommittedMetadataMirror detects and optionally repairs v1.1 read-mirror
390+
// drift. Read paths use the mirror as-is, so doctor is the repair tool.
391+
// Silent when the topology has no mirror.
392+
func checkCommittedMetadataMirror(cmd *cobra.Command, force bool) error {
393+
ctx := cmd.Context()
394+
repo, err := openRepository(ctx)
395+
if err != nil {
396+
return fmt.Errorf("failed to open repository: %w", err)
397+
}
398+
defer repo.Close()
399+
400+
diag, err := strategy.DiagnoseCommittedMetadataMirror(ctx, repo)
401+
if err != nil {
402+
return fmt.Errorf("could not check checkpoint read mirror state: %w", err)
403+
}
404+
405+
w := cmd.OutOrStdout()
406+
primary := diag.Refs.Primary.Short()
407+
408+
switch diag.Status {
409+
case strategy.MirrorNotConfigured:
410+
return nil
411+
case strategy.MirrorOK:
412+
fmt.Fprintln(w, "✓ Checkpoint read mirror: OK")
413+
return nil
414+
case strategy.MirrorNoMetadata:
415+
fmt.Fprintln(w, "✓ Checkpoint read mirror: OK (no committed metadata yet)")
416+
return nil
417+
case strategy.MirrorPrimaryMissing:
418+
fmt.Fprintf(w, "Checkpoint read mirror: %s\n", diag.Status)
419+
fmt.Fprintf(w, " The read mirror %s exists, but the %s branch it mirrors is gone.\n",
420+
diag.Refs.Mirror, primary)
421+
fmt.Fprintln(w, " Restore the branch and re-run doctor:")
422+
fmt.Fprintf(w, " git fetch origin %s:%s\n", primary, primary)
423+
return nil
424+
case strategy.MirrorMissing:
425+
fmt.Fprintf(w, "Checkpoint read mirror: %s\n", diag.Status)
426+
fmt.Fprintf(w, " %s does not exist; reads will find no checkpoints.\n", diag.Refs.Mirror)
427+
fmt.Fprintf(w, " Fix: seed the mirror at the %s tip.\n", primary)
428+
case strategy.MirrorBehind:
429+
fmt.Fprintf(w, "Checkpoint read mirror: %s\n", diag.Status)
430+
fmt.Fprintf(w, " Mirror is at %s, behind %s at %s; reads miss newer checkpoints.\n",
431+
shortMirrorHash(diag.Mirror), primary, shortMirrorHash(diag.Primary))
432+
fmt.Fprintf(w, " Fix: advance the mirror to the %s tip.\n", primary)
433+
case strategy.MirrorDiverged:
434+
fmt.Fprintf(w, "Checkpoint read mirror: %s\n", diag.Status)
435+
fmt.Fprintf(w, " Mirror at %s has commits not on %s (at %s). Writes never target the\n",
436+
shortMirrorHash(diag.Mirror), primary, shortMirrorHash(diag.Primary))
437+
fmt.Fprintln(w, " mirror, so something outside entire moved it.")
438+
fmt.Fprintf(w, " Fix: reset the mirror to the %s tip — the diverged mirror commits\n", primary)
439+
fmt.Fprintf(w, " are discarded (%s is the source of truth).\n", primary)
440+
}
441+
442+
if !force {
443+
proceed, promptErr := confirmDoctorFix(ctx, w, "Repair checkpoint read mirror?")
444+
if promptErr != nil {
445+
return promptErr
446+
}
447+
if !proceed {
448+
return nil
449+
}
450+
}
451+
452+
if fixErr := strategy.MirrorCommittedMetadataRef(ctx, repo, diag.Refs); fixErr != nil {
453+
return fmt.Errorf("failed to repair checkpoint read mirror: %w", fixErr)
454+
}
455+
fmt.Fprintf(w, " ✓ Fixed: mirror now points at the %s tip\n", primary)
456+
return nil
457+
}
458+
459+
// confirmDoctorFix prompts to apply a doctor fix. Declining (which prints
460+
// "-> Skipped"), aborting (Ctrl+C), and context cancellation all return false
461+
// with no error.
462+
func confirmDoctorFix(ctx context.Context, w io.Writer, title string) (bool, error) {
463+
// huh opens the TTY during form startup regardless of context state, so
464+
// guard explicitly to honor an already-cancelled command context.
465+
if ctx.Err() != nil {
466+
return false, nil //nolint:nilerr // cancelled context is a clean skip, not an error
467+
}
468+
var confirmed bool
469+
form := NewAccessibleForm(
470+
huh.NewGroup(
471+
huh.NewConfirm().
472+
Title(title).
473+
Value(&confirmed),
474+
),
475+
)
476+
if err := form.RunWithContext(ctx); err != nil {
477+
if errors.Is(err, huh.ErrUserAborted) || errors.Is(err, context.Canceled) {
478+
return false, nil
479+
}
480+
return false, fmt.Errorf("prompt failed: %w", err)
481+
}
482+
if !confirmed {
483+
fmt.Fprintln(w, " -> Skipped")
484+
}
485+
return confirmed, nil
486+
}
487+
488+
// shortMirrorHash abbreviates a hash for mirror-check output; "none" when zero.
489+
func shortMirrorHash(h plumbing.Hash) string {
490+
if h.IsZero() {
491+
return "none"
492+
}
493+
return h.String()[:7]
494+
}
495+
386496
// checkCodexHookTrust warns about two kinds of drift in the Codex hook
387497
// setup:
388498
//

cmd/entire/cli/doctor_bundle.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,11 @@ import (
1616

1717
"github.com/entireio/cli/cmd/entire/cli/logging"
1818
"github.com/entireio/cli/cmd/entire/cli/paths"
19+
"github.com/entireio/cli/cmd/entire/cli/settings"
20+
"github.com/entireio/cli/cmd/entire/cli/strategy"
1921
"github.com/entireio/cli/cmd/entire/cli/versioninfo"
2022
"github.com/entireio/cli/redact"
23+
"github.com/go-git/go-git/v6"
2124
"github.com/spf13/cobra"
2225
)
2326

@@ -124,6 +127,10 @@ func writeDoctorBundle(ctx context.Context, repoRoot, outPath string, raw bool)
124127
return err
125128
}
126129

130+
if err := addStringToZip(zw, "entire-refs.txt", entireRefsReport(ctx, repoRoot), raw); err != nil {
131+
return err
132+
}
133+
127134
if err := addStringToZip(zw, "version.txt", versionInfoString(), raw); err != nil {
128135
return err
129136
}
@@ -141,6 +148,46 @@ func writeDoctorBundle(ctx context.Context, repoRoot, outPath string, raw bool)
141148
return nil
142149
}
143150

151+
// entireRefsReport captures entire-related git refs plus the mirror diagnosis.
152+
// Best-effort: failures are recorded in the report, not returned.
153+
func entireRefsReport(ctx context.Context, repoRoot string) string {
154+
var sb strings.Builder
155+
156+
// Broad globs on purpose: refs/heads/entire also catches shadow/trails
157+
// branches, refs/entire catches the v1.1 mirror and future custom refs.
158+
cmd := exec.CommandContext(ctx, "git", "for-each-ref", "--format=%(refname) %(objectname)",
159+
"refs/heads/entire", "refs/entire", "refs/remotes/origin/entire")
160+
cmd.Dir = repoRoot
161+
out, err := cmd.CombinedOutput()
162+
sb.Write(out)
163+
if err != nil {
164+
fmt.Fprintf(&sb, "[error: %v]\n", err)
165+
}
166+
167+
sb.WriteString("\n")
168+
sb.WriteString(mirrorStatusReportLine(ctx, repoRoot))
169+
return sb.String()
170+
}
171+
172+
// mirrorStatusReportLine renders the v1.1 mirror diagnosis for the bundle.
173+
func mirrorStatusReportLine(ctx context.Context, repoRoot string) string {
174+
repo, err := git.PlainOpen(repoRoot)
175+
if err != nil {
176+
return fmt.Sprintf("mirror status: [error: %v]\n", err)
177+
}
178+
defer repo.Close()
179+
// Scope settings to repoRoot; the bundle's CWD may be elsewhere.
180+
diag, err := strategy.DiagnoseCommittedMetadataMirror(settings.WithWorktreeRoot(ctx, repoRoot), repo)
181+
if err != nil {
182+
return fmt.Sprintf("mirror status: [error: %v]\n", err)
183+
}
184+
if diag.Status == strategy.MirrorNotConfigured {
185+
return "mirror status: not configured (checkpoints v1)\n"
186+
}
187+
return fmt.Sprintf("mirror status: %s (mirror %s, v1 %s)\n",
188+
diag.Status, shortMirrorHash(diag.Mirror), shortMirrorHash(diag.Primary))
189+
}
190+
144191
func versionInfoString() string {
145192
var sb strings.Builder
146193
fmt.Fprintf(&sb, "Entire CLI %s (%s)\n", versioninfo.Version, versioninfo.Commit)

cmd/entire/cli/doctor_bundle_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,43 @@ func TestWriteDoctorBundle_ContainsExpectedEntries(t *testing.T) {
7676
}
7777
}
7878

79+
// The bundle must record entire's git refs and the mirror diagnosis so
80+
// support can debug v1.1 read issues from a bundle alone.
81+
func TestWriteDoctorBundle_CapturesEntireRefs(t *testing.T) {
82+
t.Parallel()
83+
84+
dir := t.TempDir()
85+
testutil.InitRepo(t, dir)
86+
testutil.WriteFile(t, dir, "f.txt", "init")
87+
testutil.GitAdd(t, dir, "f.txt")
88+
testutil.GitCommit(t, dir, "init")
89+
90+
entireDir := filepath.Join(dir, ".entire")
91+
if err := os.MkdirAll(entireDir, 0o755); err != nil {
92+
t.Fatalf("mkdir .entire: %v", err)
93+
}
94+
settingsJSON := `{"enabled": true, "strategy_options": {"checkpoints_version": "1.1"}}`
95+
if err := os.WriteFile(filepath.Join(entireDir, "settings.json"), []byte(settingsJSON), 0o600); err != nil {
96+
t.Fatalf("write settings: %v", err)
97+
}
98+
99+
// v1 branch at HEAD with no mirror ref → diagnosis must report MISSING.
100+
runDoctorBundleGit(t, dir, "update-ref", "refs/heads/entire/checkpoints/v1", "HEAD")
101+
102+
out := filepath.Join(dir, "bundle.zip")
103+
if err := writeDoctorBundle(context.Background(), dir, out, false); err != nil {
104+
t.Fatalf("writeDoctorBundle: %v", err)
105+
}
106+
107+
content := readZipEntry(t, out, "entire-refs.txt")
108+
if !strings.Contains(content, "refs/heads/entire/checkpoints/v1") {
109+
t.Errorf("entire-refs.txt missing v1 branch ref, got:\n%s", content)
110+
}
111+
if !strings.Contains(content, "mirror status: MISSING") {
112+
t.Errorf("entire-refs.txt missing mirror diagnosis line, got:\n%s", content)
113+
}
114+
}
115+
79116
func TestWriteDoctorBundle_RedactsCredentialedRemote(t *testing.T) {
80117
t.Parallel()
81118

0 commit comments

Comments
 (0)