fix(trail): flag onboarding-created runners left un-tailored · Entire

fix(trail): flag onboarding-created runners left un-tailored

9ddaef9·

Soph·3w ago·5 files·+88 added/-27 removed

Onboarding writes the default runner set, then tune tailors in place. If a runner's tailoring is skipped, omitted by the model, or returned verbatim, it stays a generic default — and the previous "Updated N… review with git diff" nudged the user to commit those generic placeholders as if they were repo-specific.

ensureRunnersPresent now reports which runners it created; applyTuneWithAgent tracks which were actually tailored and warns about any created-but-untailored ("…still generic defaults, not tailored to this repo: …; re-run or edit/remove before committing"). The --print path likewise notes that freshly-created defaults are untracked and not yet tailored.

Sessions

d87e5916f664View transcript

Changes

5


// untailoredRunners returns the created runner IDs that tuning did NOT tailor
// (still generic defaults), sorted. These were scaffolded by onboarding but
// left unchanged — skipped, omitted by the model, or returned verbatim — so
// they must not be presented as repo-tailored.
func untailoredRunners(createdIDs []string, tailored map[string]bool) []string {
    var out []string
    for _, id := range createdIDs {
        if !tailored[normalizeRunnerID(id)] {
            out = append(out, id)
        }
    }
    sort.Strings(out)
    return out
}

// droppedPlaceholders returns the placeholders present in oldTemplate but not in
// newTemplate, sorted. Used to inform the user when a rewrite stops using one.
func droppedPlaceholders(oldTemplate, newTemplate string) []string {
func TestUntailoredRunners(t *testing.T) {
    t.Parallel()

created := []string{"trail-risk", "trail-drift", "trail-review"}
    tailored := map[string]bool{"risk": true} // normalized IDs (no "trail-" prefix)

got := untailoredRunners(created, tailored)
    want := []string{"trail-drift", "trail-review"}
    if len(got) != len(want) || got[0] != want[0] || got[1] != want[1] {
            t.Errorf("untailoredRunners = %v, want %v", got, want)
    }

// Nothing created → nothing untailored, even with no tailoring recorded.
    if u := untailoredRunners(nil, map[string]bool{}); len(u) != 0 {
        t.Errorf("expected empty, got %v", u)
    }
}

func TestParseTuneOutput(t *testing.T) {
    t.Parallel()
// ensureRunnersPresent scaffolds the default runner set when a repo has none
// yet, so `tune` doubles as onboarding. It is a no-op when runners already
// exist, and returns an error when the user declined or creation failed.
// Writing is gated on confirmation (interactive prompt, or the --yes flag for
// non-interactive runs).
func ensureRunnersPresent(w, errW io.Writer, repoRoot string, assumeYes bool) (created []string, err error) {
    dir := runnersDir(repoRoot)
    existing, _ := filepath.Glob(filepath.Join(dir, "*.json")) //nolint:errcheck // bad pattern only; treated as "none found"
    if len(existing) > 0 {
        return nil, nil
    }

defaults, err := runnerdefaults.Files()
    if err != nil {
        return nil, fmt.Errorf("loading default runners: %w", err)
    }

if !assumeYes {
        if !interactive.CanPromptInteractively() {
            return fmt.Errorf("no runner configs found under %s; re-run with --yes to create the default set (%d runners)", dir, len(defaults))
        }
        confirmed, err := confirmCreateRunners(len(defaults))
        if err != nil {
            return nil, err
        }
        if !confirmed {
            return nil, errors.New("no runner configs created (declined)")
        }
    }

if err := os.MkdirAll(dir, 0o755); err != nil { //nolint:gosec // config dir, conventional perms
        return fmt.Errorf("creating %s: %w", dir, err)
    }
    for _, f := range defaults {
        dest := filepath.Join(dir, f.Name)
        if err := os.WriteFile(dest, f.Data, 0o644); err != nil { //nolint:gosec // runner configs are repo-committed, world-readable config
            return fmt.Errorf("writing %s: %w", dest, err)
        }
        fmt.Fprintf(w, "created %s\n", filepath.Join(paths.EntireDir, "runners", f.Name))
        created = append(created, strings.TrimSuffix(f.Name, ".json"))
    }
    fmt.Fprintf(errW, "Created %d default runner(s); tailoring them to this repo…\n", len(defaults))
    return created, nil
}

func confirmCreateRunners(n int) (bool, error) {}