Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 127 additions & 17 deletions cmd/entire/cli/doctor.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,18 @@ Checks performed:
entire/checkpoints/v1 branches share no common ancestor (caused by a
previous bug). Fixes by cherry-picking local checkpoints onto remote tip.

2. Checkpoint read mirror (checkpoints v1.1 only): detects when the
local-only refs/entire/checkpoints/v1.1 read mirror is missing, stale,
or diverged relative to entire/checkpoints/v1, which makes reads miss
checkpoints. Fixes by pointing the mirror at the v1 tip.

When Codex hooks are installed:
2. Codex hook trust: warn when hooks declared in .codex/hooks.json
3. Codex hook trust: warn when hooks declared in .codex/hooks.json
lack a trusted_hash entry in the user's Codex config (i.e. /hooks
review hasn't run yet on this machine, or a newer entire release
added a hook the user hasn't approved yet).

3. Stuck sessions: sessions stuck in ACTIVE or ENDED phase that need cleanup.
4. Stuck sessions: sessions stuck in ACTIVE or ENDED phase that need cleanup.

A session is considered stuck if:
- It is in ACTIVE phase with no interaction for over 1 hour
Expand Down Expand Up @@ -90,6 +95,15 @@ func runSessionsFix(cmd *cobra.Command, force bool) error {
fmt.Fprintf(cmd.ErrOrStderr(), "Error: metadata check failed: %v\n", metadataErr)
finalErr = NewSilentError(fmt.Errorf("metadata check failed: %w", metadataErr))
}

// Check 2: v1.1 read-mirror drift (after check 1: reconciliation rewrites
// v1 and re-mirrors).
if mirrorErr := checkCommittedMetadataMirror(cmd, force); mirrorErr != nil {
fmt.Fprintf(cmd.ErrOrStderr(), "Error: checkpoint read mirror check failed: %v\n", mirrorErr)
if finalErr == nil {
finalErr = NewSilentError(fmt.Errorf("checkpoint read mirror check failed: %w", mirrorErr))
}
}
fmt.Fprintln(cmd.OutOrStdout())

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

if !force {
var confirmed bool
form := NewAccessibleForm(
huh.NewGroup(
huh.NewConfirm().
Title("Fix disconnected metadata branches?").
Value(&confirmed),
),
)
if formErr := form.Run(); formErr != nil {
if errors.Is(formErr, huh.ErrUserAborted) {
return nil
}
return fmt.Errorf("prompt failed: %w", formErr)
proceed, promptErr := confirmDoctorFix(ctx, w, "Fix disconnected metadata branches?")
if promptErr != nil {
return promptErr
}
if !confirmed {
fmt.Fprintln(w, " -> Skipped")
if !proceed {
return nil
}
}
Expand All @@ -383,6 +386,113 @@ func checkDisconnectedMetadata(cmd *cobra.Command, force bool) error {
return nil
}

// checkCommittedMetadataMirror detects and optionally repairs v1.1 read-mirror
// drift. Read paths use the mirror as-is, so doctor is the repair tool.
// Silent when the topology has no mirror.
func checkCommittedMetadataMirror(cmd *cobra.Command, force bool) error {
ctx := cmd.Context()
repo, err := openRepository(ctx)
if err != nil {
return fmt.Errorf("failed to open repository: %w", err)
}
defer repo.Close()

diag, err := strategy.DiagnoseCommittedMetadataMirror(ctx, repo)
if err != nil {
return fmt.Errorf("could not check checkpoint read mirror state: %w", err)
}

w := cmd.OutOrStdout()
primary := diag.Refs.Primary.Short()

switch diag.Status {
case strategy.MirrorNotConfigured:
return nil
case strategy.MirrorOK:
fmt.Fprintln(w, "✓ Checkpoint read mirror: OK")
return nil
case strategy.MirrorNoMetadata:
fmt.Fprintln(w, "✓ Checkpoint read mirror: OK (no committed metadata yet)")
return nil
case strategy.MirrorPrimaryMissing:
fmt.Fprintf(w, "Checkpoint read mirror: %s\n", diag.Status)
fmt.Fprintf(w, " The read mirror %s exists, but the %s branch it mirrors is gone.\n",
diag.Refs.Mirror, primary)
fmt.Fprintln(w, " Restore the branch and re-run doctor:")
fmt.Fprintf(w, " git fetch origin %s:%s\n", primary, primary)
return nil
case strategy.MirrorMissing:
fmt.Fprintf(w, "Checkpoint read mirror: %s\n", diag.Status)
fmt.Fprintf(w, " %s does not exist; reads will find no checkpoints.\n", diag.Refs.Mirror)
fmt.Fprintf(w, " Fix: seed the mirror at the %s tip.\n", primary)
case strategy.MirrorBehind:
fmt.Fprintf(w, "Checkpoint read mirror: %s\n", diag.Status)
fmt.Fprintf(w, " Mirror is at %s, behind %s at %s; reads miss newer checkpoints.\n",
shortMirrorHash(diag.Mirror), primary, shortMirrorHash(diag.Primary))
fmt.Fprintf(w, " Fix: advance the mirror to the %s tip.\n", primary)
case strategy.MirrorDiverged:
fmt.Fprintf(w, "Checkpoint read mirror: %s\n", diag.Status)
fmt.Fprintf(w, " Mirror at %s has commits not on %s (at %s). Writes never target the\n",
shortMirrorHash(diag.Mirror), primary, shortMirrorHash(diag.Primary))
fmt.Fprintln(w, " mirror, so something outside entire moved it.")
fmt.Fprintf(w, " Fix: reset the mirror to the %s tip — the diverged mirror commits\n", primary)
fmt.Fprintf(w, " are discarded (%s is the source of truth).\n", primary)
}

if !force {
proceed, promptErr := confirmDoctorFix(ctx, w, "Repair checkpoint read mirror?")
if promptErr != nil {
return promptErr
}
if !proceed {
return nil
}
}

if fixErr := strategy.MirrorCommittedMetadataRef(ctx, repo, diag.Refs); fixErr != nil {
return fmt.Errorf("failed to repair checkpoint read mirror: %w", fixErr)
}
fmt.Fprintf(w, " ✓ Fixed: mirror now points at the %s tip\n", primary)
return nil
}

// confirmDoctorFix prompts to apply a doctor fix. Declining (which prints
// "-> Skipped"), aborting (Ctrl+C), and context cancellation all return false
// with no error.
func confirmDoctorFix(ctx context.Context, w io.Writer, title string) (bool, error) {
// huh opens the TTY during form startup regardless of context state, so
// guard explicitly to honor an already-cancelled command context.
if ctx.Err() != nil {
return false, nil //nolint:nilerr // cancelled context is a clean skip, not an error
}
var confirmed bool
form := NewAccessibleForm(
huh.NewGroup(
huh.NewConfirm().
Title(title).
Value(&confirmed),
),
)
if err := form.RunWithContext(ctx); err != nil {
if errors.Is(err, huh.ErrUserAborted) || errors.Is(err, context.Canceled) {
return false, nil
}
return false, fmt.Errorf("prompt failed: %w", err)
}
if !confirmed {
fmt.Fprintln(w, " -> Skipped")
}
return confirmed, nil
}
Comment thread
computermode marked this conversation as resolved.

// shortMirrorHash abbreviates a hash for mirror-check output; "none" when zero.
func shortMirrorHash(h plumbing.Hash) string {
if h.IsZero() {
return "none"
}
return h.String()[:7]
}

// checkCodexHookTrust warns about two kinds of drift in the Codex hook
// setup:
//
Expand Down
47 changes: 47 additions & 0 deletions cmd/entire/cli/doctor_bundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@ import (

"github.com/entireio/cli/cmd/entire/cli/logging"
"github.com/entireio/cli/cmd/entire/cli/paths"
"github.com/entireio/cli/cmd/entire/cli/settings"
"github.com/entireio/cli/cmd/entire/cli/strategy"
"github.com/entireio/cli/cmd/entire/cli/versioninfo"
"github.com/entireio/cli/redact"
"github.com/go-git/go-git/v6"
"github.com/spf13/cobra"
)

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

if err := addStringToZip(zw, "entire-refs.txt", entireRefsReport(ctx, repoRoot), raw); err != nil {
return err
}

if err := addStringToZip(zw, "version.txt", versionInfoString(), raw); err != nil {
return err
}
Expand All @@ -141,6 +148,46 @@ func writeDoctorBundle(ctx context.Context, repoRoot, outPath string, raw bool)
return nil
}

// entireRefsReport captures entire-related git refs plus the mirror diagnosis.
// Best-effort: failures are recorded in the report, not returned.
func entireRefsReport(ctx context.Context, repoRoot string) string {
var sb strings.Builder

// Broad globs on purpose: refs/heads/entire also catches shadow/trails
// branches, refs/entire catches the v1.1 mirror and future custom refs.
cmd := exec.CommandContext(ctx, "git", "for-each-ref", "--format=%(refname) %(objectname)",
"refs/heads/entire", "refs/entire", "refs/remotes/origin/entire")
cmd.Dir = repoRoot
out, err := cmd.CombinedOutput()
sb.Write(out)
if err != nil {
fmt.Fprintf(&sb, "[error: %v]\n", err)
}

sb.WriteString("\n")
sb.WriteString(mirrorStatusReportLine(ctx, repoRoot))
return sb.String()
}

// mirrorStatusReportLine renders the v1.1 mirror diagnosis for the bundle.
func mirrorStatusReportLine(ctx context.Context, repoRoot string) string {
repo, err := git.PlainOpen(repoRoot)
if err != nil {
return fmt.Sprintf("mirror status: [error: %v]\n", err)
}
defer repo.Close()
// Scope settings to repoRoot; the bundle's CWD may be elsewhere.
Comment thread
computermode marked this conversation as resolved.
diag, err := strategy.DiagnoseCommittedMetadataMirror(settings.WithWorktreeRoot(ctx, repoRoot), repo)
if err != nil {
return fmt.Sprintf("mirror status: [error: %v]\n", err)
}
if diag.Status == strategy.MirrorNotConfigured {
return "mirror status: not configured (checkpoints v1)\n"
}
return fmt.Sprintf("mirror status: %s (mirror %s, v1 %s)\n",
diag.Status, shortMirrorHash(diag.Mirror), shortMirrorHash(diag.Primary))
}

func versionInfoString() string {
var sb strings.Builder
fmt.Fprintf(&sb, "Entire CLI %s (%s)\n", versioninfo.Version, versioninfo.Commit)
Expand Down
37 changes: 37 additions & 0 deletions cmd/entire/cli/doctor_bundle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,43 @@ func TestWriteDoctorBundle_ContainsExpectedEntries(t *testing.T) {
}
}

// The bundle must record entire's git refs and the mirror diagnosis so
// support can debug v1.1 read issues from a bundle alone.
func TestWriteDoctorBundle_CapturesEntireRefs(t *testing.T) {
t.Parallel()

dir := t.TempDir()
testutil.InitRepo(t, dir)
testutil.WriteFile(t, dir, "f.txt", "init")
testutil.GitAdd(t, dir, "f.txt")
testutil.GitCommit(t, dir, "init")

entireDir := filepath.Join(dir, ".entire")
if err := os.MkdirAll(entireDir, 0o755); err != nil {
t.Fatalf("mkdir .entire: %v", err)
}
settingsJSON := `{"enabled": true, "strategy_options": {"checkpoints_version": "1.1"}}`
if err := os.WriteFile(filepath.Join(entireDir, "settings.json"), []byte(settingsJSON), 0o600); err != nil {
t.Fatalf("write settings: %v", err)
}

// v1 branch at HEAD with no mirror ref → diagnosis must report MISSING.
runDoctorBundleGit(t, dir, "update-ref", "refs/heads/entire/checkpoints/v1", "HEAD")

out := filepath.Join(dir, "bundle.zip")
if err := writeDoctorBundle(context.Background(), dir, out, false); err != nil {
t.Fatalf("writeDoctorBundle: %v", err)
}

content := readZipEntry(t, out, "entire-refs.txt")
if !strings.Contains(content, "refs/heads/entire/checkpoints/v1") {
t.Errorf("entire-refs.txt missing v1 branch ref, got:\n%s", content)
}
if !strings.Contains(content, "mirror status: MISSING") {
t.Errorf("entire-refs.txt missing mirror diagnosis line, got:\n%s", content)
}
}

func TestWriteDoctorBundle_RedactsCredentialedRemote(t *testing.T) {
t.Parallel()

Expand Down
Loading
Loading