feat(explain): phase progress for fetch + data-loading pipeline#1222
feat(explain): phase progress for fetch + data-loading pipeline#1222peyton-alt wants to merge 4 commits into
Conversation
Wraps formatFilteredFetchError's flat string error in a typed struct that carries the redacted target, trimmed/redacted stderr, and underlying exec error. Callers can extract these via errors.As to build per-attempt diagnostics rather than parsing the message; the Error() output is intentionally identical to the previous flat format so existing log/test expectations keep working. Pure addition — no behaviour change yet. Future commits will use this to attribute fetch-chain failures in `entire explain`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds explainProgressWriter for status-flip phase events (→ label / ✓ label / ✗ label) plus an in-place sublabel ticker for sub-step transitions during the data-loading pipeline. Shares visual conventions with the streaming summary UX: cyan arrow, green check, red cross, ACCESSIBLE mode strips ANSI and substitutes [ok]/[fail], non-TTY writers get one line per event. No callers yet — wiring follows in the next commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the single "Loading checkpoints" / "Fetching checkpoint metadata from remote" spinners with per-attempt phase lines, so users see which strategy is running (checkpoint_remote, treeless origin, local, full origin, remote-tracking — plus the v2 variants) and which one errored on failure. Mechanics: - checkpoint.AttemptHooks (new): OnStart/OnFinish notifications around each fallback strategy. Zero-value is a no-op so existing callers (resume, attach) are unaffected. - checkpoint.GetV2MetadataTreeWithHooks: variant that emits hook events around the 3 internal strategies; backward-compatible GetV2MetadataTree delegates with empty hooks. - getMetadataTreeWithHooks / getV2MetadataTreeWithHooks (resume.go): same pattern at the cli layer; wrap the 5-strategy v1 chain and the v2 chain (including checkpoint_remote tier). - newPhaseProgressHooks (explain_progress.go): adapter that pipes hook events into explainProgressWriter, rendering → label / ✓ label (duration) / ✗ label: <first stderr line> with FetchAttemptError unwrapping for accurate diagnostics. - runExplainAuto + matchCheckpointPrefixWithRemoteFallback: use the hooks pipeline instead of startSpinner. No behaviour change for the resume/attach paths (they continue to pass zero-value hooks, getting the same no-op rendering as before). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the single opaque "Loading checkpoint <id>" spinner with a phase whose sublabel ticks through the 4 internal sub-steps: prefetching blobs → metadata → content → associated commits. On TTY this is one line that updates in place; on non-TTY each sub-step emits its own line so logs stay readable. The phase finishes with the total elapsed time. Threading is a simple onSubStep callback into loadCheckpointForExplain; nil callers (none currently) get the old silent behaviour. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR improves the entire explain user experience by replacing coarse spinners with phase-aware progress output across the metadata fetch + checkpoint data-loading pipeline, while also surfacing captured git fetch stderr (redacted) inline via a typed error wrapper.
Changes:
- Introduces a typed
FetchAttemptErrorto preserve redacted fetch target + trimmed stderr while keeping the sameError()string anderrors.Isbehavior. - Adds a small progress renderer (
explainProgressWriter) and a hook mechanism (checkpoint.AttemptHooks) to emit per-attempt start/finish events during fetch/read fallback chains. - Updates
explainand metadata-tree resolution paths to use hook-aware variants and to provide sub-step ticking during checkpoint load; adds unit tests for both the typed error and renderer.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| cmd/entire/cli/git_operations.go | Adds FetchAttemptError and returns it from formatFilteredFetchError for structured diagnostics. |
| cmd/entire/cli/git_operations_test.go | Adds tests for typed fetch error behavior, redaction, and errors.Is/As. |
| cmd/entire/cli/explain_progress.go | New progress writer + hook adapter to render phase lines and sub-step updates. |
| cmd/entire/cli/explain_progress_test.go | New tests covering non-TTY output, accessible glyphs, and duration formatting. |
| cmd/entire/cli/checkpoint/attempt.go | New AttemptHooks primitive (OnStart/OnFinish) with WithLabel helper. |
| cmd/entire/cli/checkpoint/v2_resolve.go | Adds hook-aware v2 metadata tree resolution with labeled attempts. |
| cmd/entire/cli/resume.go | Adds hook-aware v1/v2 metadata tree helpers and wraps fallback strategies with attempt hooks. |
| cmd/entire/cli/explain.go | Replaces spinners with phase progress; adds sub-step ticking during checkpoint load. |
| cmd/entire/cli/explain_export.go | Updates remote-fallback matching to use hook-aware metadata fetch and render phase attempts. |
| @@ -381,6 +381,14 @@ func resolveLatestCheckpoint(ctx context.Context, checkpointIDs []id.CheckpointI | |||
| // | |||
| // Fallback order: treeless fetch → local → checkpoint_remote → full origin fetch → remote tree. | |||
| return nil, nil, repoErr | ||
| } | ||
| logRefHash(remoteRepo, "remote-tracking") | ||
| remoteTree, err := strategy.GetRemoteMetadataBranchTree(remoteRepo) | ||
| if err != nil { | ||
| remoteErr = err | ||
| logging.Debug(logCtx, "remote metadata tree also not available", | ||
| slog.String("error", err.Error()), | ||
| ) | ||
| return nil, nil, fmt.Errorf("read remote v1 metadata tree: %w", err) |
| var ( | ||
| tree *object.Tree | ||
| repo *git.Repository | ||
| runErr error | ||
| started bool |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 9390f45. Configure here.
| // Handle raw transcript output | ||
| if rawTranscript { | ||
| stopLoad(false) | ||
| loadPW.FinishPhase(loadPrefix, true, formatPhaseDuration(time.Since(loadStart))) |
There was a problem hiding this comment.
Duplicate progress line when generate and rawTranscript both set
Low Severity
When both --generate and --raw-transcript flags are set, loadPW.FinishPhase(loadPrefix, true, ...) is called twice for the same loadPrefix — once at the start of the if generate block and again in the if rawTranscript block. The old code avoided this by reassigning stopLoad to a new spinner inside the generate block, so the rawTranscript block stopped the reload spinner instead. The new code uses the same loadPW/loadPrefix throughout, producing a duplicate "✓ Loading checkpoint X" line with a misleading duration.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 9390f45. Configure here.
| if remoteErr != nil { | ||
| return nil, nil, fmt.Errorf("metadata branch not available: %w", remoteErr) | ||
| } | ||
| return nil, nil, errors.New("metadata branch not available") |
There was a problem hiding this comment.
Final error loses cause when openRepository fails last
Low Severity
In getMetadataTreeWithHooks, the remoteErr variable is only set when GetRemoteMetadataBranchTree fails, but not when openRepository fails in the remote-tracking attempt. If openRepository fails, remoteErr stays nil, and the function falls through to errors.New("metadata branch not available") — losing the underlying cause. The old code returned fmt.Errorf("failed to open repository: %w", repoErr) with the specific error.
Reviewed by Cursor Bugbot for commit 9390f45. Configure here.


https://entire.io/gh/entireio/cli/trails/388
Summary
Replaces the two coarse spinners in
entire explainwith phase-aware progress events so users can see which fetch/load sub-step is running, and surfaces stderr from failed fetch attempts inline rather than silently swallowing them.Before:
Loading checkpointsspinner → silent fetch chain →Loading checkpoint Xspinner (covering 4-5 internal sub-steps). On a teammate's checkpoint lookup, you saw an opaque "loading" stretch with no information about what was happening or why a fetch failed.After (TTY):
After (non-TTY /
| cat): same line content, no\rrewriting, each sub-step is its own line.ACCESSIBLE=1:→becomes->,✓becomes[ok],✗becomes[fail].NO_COLOR=1: no ANSI escapes.Architecture
FetchAttemptError(git_operations.go) typed wrapper aroundformatFilteredFetchError's flat string. CarriesPrefix,RedactedTarget,Stderr,Err. Existing callers see the same.Error()string; new diagnostic callers canerrors.Asfor structured fields. Per repo memory on error classification: no speculative phrase-matching — verbatim git stderr passthrough.explainProgressWriter(explain_progress.go) the renderer. Three methods (StartPhase,FinishPhase,UpdateSublabel) backed by sharedprintLine/updateLineprimitives. TTY detection viainteractive.IsTerminalWriter; visual style matches existingstatusStylesglyphs/colours.checkpoint.AttemptHooks(checkpoint/attempt.go) primitive type withOnStart/OnFinishcallbacks and aWithLabelhelper. Defined in the checkpoint package so it doesn't import cli; zero value is a no-op soentire resume/entire attach(which also usegetMetadataTree/getV2MetadataTree) keep their existing silent behaviour.getMetadataTreeWithHooks/getV2MetadataTreeWithHooks(resume.go) andcheckpoint.GetV2MetadataTreeWithHooksthe hook-aware variants of the fetch helpers. Backward-compatible wrappersgetMetadataTreeandGetV2MetadataTreedelegate with empty hooks.newPhaseProgressHooks(explain_progress.go) adapter that pipesAttemptHooksevents into the writer, unwrappingFetchAttemptError.Stderrfor the inline✗excerpt.loadCheckpointForExplaingained anonSubStep func(string)callback parameter; the caller inrunExplainCheckpointWithLookupticks throughprefetching blobs → metadata → content → associated commitson a single in-place line.Surface area
cmd/entire/cli/git_operations.gocmd/entire/cli/git_operations_test.gocmd/entire/cli/explain_progress.gocmd/entire/cli/explain_progress_test.gocmd/entire/cli/checkpoint/attempt.gocmd/entire/cli/checkpoint/v2_resolve.gocmd/entire/cli/resume.gocmd/entire/cli/explain.gocmd/entire/cli/explain_export.goNo behaviour change for the resume/attach paths — they continue to call the legacy (no-hooks) wrappers and pass zero-value
AttemptHookswhere the variant signatures require them.Test plan
go test ./cmd/entire/cli/ ./cmd/entire/cli/checkpoint/passes (incl. 3 new typed-error tests and 8 new renderer tests)mise run lintclean (no new issues)✓ Loading checkpoint X (XXms)ace97f8not-found: full fetch-chain narration with✗ Treeless fetch of v2 /main from origin: fatal: couldn't find remote ref...inline before the final "no checkpoint or commit found" error| cat: each sub-step on its own line, no\rrewritingACCESSIBLE=1: ASCII glyph substitutionentire resume --help: unchangedDeferred follow-up
The final
no checkpoint or commit founderror itself is still terse — the diagnostic appears in the inline✗lines above it. A follow-up PR could wrap it with the per-attempt block (matching PR #964'scause/tryrow style) for users whose terminals truncate scrollback or for CI logs that grep only the final line.🤖 Generated with Claude Code
Note
Cursor Bugbot is generating a summary for commit 9390f45. Configure here.