fix(onboarding): a partial import-scan failure can no longer read as Done · Entire

fix(onboarding): a partial import-scan failure can no longer read as Done

a5b887d·

peyton-alt·15h ago·2 files·+70 added/-11 removed

When some (not all) agents' dry-run scans failed, their contribution was silently dropped: the History rung could claim 'N sessions imported' or 'no prior history found' while a failed agent's real status was unknown — and since each agent is the sole source for its own history, nothing else could correct it. Failed scans are now recorded (ScanFailed) and the rung degrades honestly: pending imports from healthy agents stay actionable (Missing), but a rung that would otherwise read Done or n/a renders Unknown naming the failed agent. Never memoized, as before.

Sessions

01KXR8Z26Z0YE2P4MH62ECXKKSView transcript

Changes

2

58 unmodified lines

59
60
61
62
63
64
65
66
67
68
244 unmodified lines

313
314
315
312
313
316
317
318
319
320
321
322
323
324
325
152 unmodified lines

478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
482
483
484
485
486
487
514
489
490
491
515
516
517
518
519

58 unmodified lines

Sessions        int `json:"sessions"`
    UnimportedTurns int `json:"unimported_turns"`
    ImportedTurns   int `json:"imported_turns"`
    // ScanFailed marks an agent whose dry-run scan errored: its history
    // exists (files were discovered) but its counts are unknown. Never
    // memoized — failed scans force cacheable=false.
    ScanFailed bool `json:"scan_failed,omitempty"`
}

// newOnboardingRungDeps wires the rung probes to their real backends: agent
244 unmodified lines

RepoRoot: repoRoot, Now: now, DryRun: true,
        })
        if runErr != nil {
            // Best-effort per agent, but a partial result must not be
            // memoized — it would stay wrong until the fingerprint moves.
            // Best-effort per agent, but neither memoized (a partial result
            // would stay wrong until the fingerprint moves) nor silently
            // dropped: each agent is the sole source for its own history, so
            // a failed scan is recorded as such and the rung degrades
            // honestly instead of reporting Done/Missing with an undercount.
            logging.Warn(ctx, "onboarding: import dry-run failed", "agent", d.imp.Name(), "error", runErr)
            statuses = append(statuses, agentImportStatus{Agent: d.imp.Name(), ScanFailed: true})
            runFailures++
            cacheable = false
            continue
        }
        
totalSessions := 0
        var unimported []agentImportStatus
        var scanFailed []string
        for _, s := range statuses {
            if s.ScanFailed {
                scanFailed = append(scanFailed, s.Agent)
                continue
            }
            totalSessions += s.Sessions
            if s.UnimportedTurns > 0 {
                unimported = append(unimported, s)
            }
        }
        if len(unimported) > 0 {
            // Pending imports stay actionable even when another agent's
            // scan failed — the offer is still worth running.
            return onboarding.Check{
                State:  onboarding.StateMissing,
                Detail: unimportedDetail(unimported),
                Hint:   importHint(unimported),
            }
        }
        if len(scanFailed) > 0 {
            // An agent with discovered history whose scan failed is the
            // sole source for its own counts — claiming Done (or "no
            // prior history") would overstate what is known.
            return onboarding.Check{
                State:  onboarding.StateUnknown,
                Detail: strings.Join(scanFailed, ", ") + " history scan failed",
                Hint:   "entire import",
            }
        }
        if totalSessions == 0 {
            return onboarding.Check{State: onboarding.StateNotApplicable, Detail: "no prior history found"}
        }
        if len(unimported) == 0 {
            return onboarding.Check{
                State:  onboarding.StateDone,
                Detail: fmt.Sprintf("%d sessions imported", totalSessions),
            }
        }
        return onboarding.Check{
            State:  onboarding.StateMissing,
            Detail: unimportedDetail(unimported),
            Hint:   importHint(unimported),
            State:  onboarding.StateDone,
            Detail: fmt.Sprintf("%d sessions imported", totalSessions),
        }
    }
}

Mcmd/entire/cli/onboarding_rungs.go+36/-11


700 unmodified lines

701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740

700 unmodified lines

}

// A per-agent scan failure must not vanish: each agent is the sole source
// for its own history, so a rung that would otherwise read Done (or "no
// prior history found") degrades to Unknown naming the failed agent. Pending
// imports from healthy agents stay actionable regardless.
func TestImportRung_PartialScanFailureNeverClaimsDone(t *testing.T) {

t.Parallel()
check := func(statuses []agentImportStatus) onboarding.Check {
    deps := onboardingRungDeps{
        discoverImports: func(context.Context) ([]agentImportStatus, error) { return statuses, nil },
    }
    return importRung(deps).Check(context.Background())
}

imported := agentImportStatus{Agent: "claude-code", Sessions: 2, ImportedTurns: 2}
failed := agentImportStatus{Agent: "codex", ScanFailed: true}
pending := agentImportStatus{Agent: "cursor", Sessions: 1, UnimportedTurns: 3}

got := check([]agentImportStatus{imported, failed})
if got.State != onboarding.StateUnknown {

t.Errorf("imported+failed State = %v, want StateUnknown (not a false Done)", got.State)
}
if !strings.Contains(got.Detail, "codex") {

t.Errorf("Detail = %q, want the failed agent named", got.Detail)
}

if got := check([]agentImportStatus{failed, pending}); got.State != onboarding.StateMissing {

t.Errorf("failed+pending State = %v, want StateMissing (pending imports stay actionable)", got.State)
}

if got := check([]agentImportStatus{imported}); got.State != onboarding.StateDone {

t.Errorf("imported-only State = %v, want StateDone unchanged", got.State)
}
}

// One hint slot, several agents with pending history: naming only the first
// agent's subcommand would read as "import resolved" after running it. The
// hint points at the group command instead, which lists the per-agent