Краткий обзор ponytail
Уже больше недели использую ponytail на постоянной основе в opencode, и мне нравятся результаты.
Как это работает
Предлагаю прочитать ридми - там есть достаточное количество примеров и описание этого инструмента, а сюда просто скопирую оттуда кусочек:
Как я это использую
Сценарий 1
В пет-проектах, над которыми давно не работал, использую основную команду /ponytail-audit - эта команда проверяет весь репозиторий, например, проверим SwiftUI-Days:
Now I have a complete picture of the entire codebase. Here's the audit:
yagni ItemSchema.swift + ItemMigrationPlan.swift (157 lines). Three versioned schemas duplicating the model + two custom migration stages that only set optional fields to nil. SwiftData lightweight migration handles this automatically when you just add optional properties. SwiftUI-Days/Models/ItemSchema.swift, SwiftUI-Days/Models/ItemMigrationPlan.swift
shrink BackupFileDocument.swift (217 lines). RawBackupItem duplicates BackupItem with different encode/decode strategy — same 5 fields, two Codable impls. BackupWrapper envelope format + legacy flat array = two parsing paths for one export format. Merge into a single type, drop the legacy path. SwiftUI-Days/Models/BackupFileDocument.swift
yagni Analytics — 4 files (AnalyticsProvider protocol + FirebaseAnalyticsProvider + NoopAnalyticsProvider + AnalyticsService) for one real provider. The protocol has one method and one implementation in production. A single AnalyticsService with a Firebase-only init (or a log closure parameter) replaces 4 files. SwiftUI-Days/Services/Analytics/
yagni SectionTitleView.swift (35 lines). A .bold().foregroundStyle(isEnabled ? .primary : .secondary).frame(maxWidth: .infinity, alignment: .leading) modifier on Text. That's a one-line View extension, not a file. SwiftUI-Days/Screens/CommonViews/SectionTitleView.swift
yagni EditSectionView.swift (30 lines). Used in one screen (EditItemScreen). Inline the SectionTitleView + TextField pair. SwiftUI-Days/Screens/Detail/EditSectionView.swift
yagni ReadSectionView.swift (33 lines). Used in one screen (ItemScreen). Inline the SectionTitleView + Text pair. SwiftUI-Days/Screens/Detail/ReadSectionView.swift
yagni DaysEditButton.swift (13 lines). Button { action } Label(.edit, systemImage: "pencil"). Used in two places; inline Button directly is less code than the file overhead. SwiftUI-Days/Screens/Main/DaysEditButton.swift
yagni DaysDeleteButton.swift (19 lines). Same as DaysEditButton. SwiftUI-Days/Screens/Main/DaysDeleteButton.swift
yagni ItemDatePicker.swift (14 lines). A DatePicker with .bold(). Inline. SwiftUI-Days/Screens/Detail/ItemDatePicker.swift
yagni ItemDisplayOptionPicker.swift (24 lines). A Picker with SectionTitleView. Used in two screens; inline both calls. SwiftUI-Days/Screens/Detail/ItemDisplayOptionPicker.swift
yagni ColorTagFilterSheetState.swift (36 lines). Compute-only struct used by one sheet. The three computed properties + two methods can live in the sheet body. SwiftUI-Days/Screens/Main/ColorTagFilterSheetState.swift
yagni Binding+mappedToBool.swift (21 lines). Single-use extension. Inline init(bindingOptional:) at the call site in AppDataScreen. SwiftUI-Days/Extensions/Binding+mappedToBool.swift
yagni ThemeIconScreen+IconViewModel.swift IconError (10 lines). Single-case error enum. Use a descriptive string instead. SwiftUI-Days/Screens/More/ThemeIcon/ThemeIconScreen+IconViewModel.swift
delete BackupFormat.android case. iOS-only app. SwiftUI-Days/Models/BackupFormat.swift
delete AppDataError.swift (5 lines). Single-case enum used once. Use a string literal. SwiftUI-Days/Models/AppDataError.swift
delete ColorTagFilterSheet+Preview.swift (31 lines). 10 static let constants configuring hue/saturation/brightness/count for previews. The previews call static constants that equal literal numbers — use the numbers directly. SwiftUI-Days/Preview Content/ColorTagFilterSheet+Preview.swift
stdlib Color+Hex.swift body (51 lines discounting preview). Hand-rolled hex-to-UInt64 scanner. Scanner already does scanHexInt64. The bit-shifting color extraction is just CGFloat math that UIColor component accessor already provides. Could halve the line count. SwiftUI-Days/Extensions/Color+Hex.swift
shrink DisplayOption.unitsStyle (20 lines). Switch-picking .full/.short/.abbreviated based on which components are nonzero. This is polish: DateComponentsFormatter auto-selects a reasonable style. SwiftUI-Days/Models/DisplayOption.swift
shrink Item.makeDaysCount creates a new DateComponentsFormatter per call. A static let formatter saves the allocation. SwiftUI-Days/Models/Item.swift
shrink FeedbackSender.platformName — nested function inside a computed var body. Extract or inline. SwiftUI-Days/Services/FeedbackSender.swift
yagni ThemeIconScreen+IconVariant.variantNumber — manual switch self returning 1...7. A @frozen enum with Int raw value gives this for free. SwiftUI-Days/Screens/More/ThemeIcon/ThemeIconScreen+IconVariant.swift
net: -~660 lines, -1 dependency (Firebase could go closure-based but kept), -1 file (BackupFormat.android dead path)Дальше закидываю этот анализ в другую модель и прошу составить план безопасного рефактора, чтобы ничего не сломать.
Сценарий 2
В процессе работы проверяю конкретный коммит с помощью команды /ponytail-review, например:
/ponytail-review коммит 8e82b6cad16c12c1dcca1574b6908304ef15ccc8
В результате получаю похожий отчет, с которым потом можно работать дальше:
Ponytail review — коммит 8e82b6ca Ниже только over-engineering, не correctness. - ColorTagFilterSheetState.swift:1 yagni Целая структура + enum для двух if-else (applyEnabled/resetEnabled/toggledColorHex/resetResult). Всё в 4 строчки в теле view. Удалить файл. - MainScreenFilterState.swift:11 shrink itemsCountForFilterButton — синоним visibleItems.count. Убрать, заменить на visibleItems.count в isFilterButtonVisible. - ColorTagFilterSheet+Preview.swift:9 yagni colors1 = 1, colors3 = 3, colors8 = 8, colors30 = 30 — константы, равные числу. Лишние имена. Вставить числа в вызовы. - docs/doc-main-screen-color-tag-filter.md:1 yagni 78 строк внутренней документации, которая устареет при следующем рефакторинге. Удалить, пока не появился внешний контрибьютор, которому она нужна. Net removable: ~40 строк
Что с рабочим проектом
Если запустить аудит всего рабочего проекта, то агент предлагает поудалять много нужного кода и зависимостей, поэтому не рекомендую запускать аудит большого проекта целиком. Во всяком случае недорогие модели с такой задачей справляются плохо.
При этом польза ощутима при выполнении конкретных задач, когда срабатывает проверка на YAGNI из раздела "Как это работает", и получается меньше кода, чем могло бы быть без этих проверок.
Заключение
Это уже пятый инструмент для агентов, который я использую на постоянной основе внутри opencode, а полный список вот: serena, codegraph, rtk, openspec, ponytail, и все складывается очень даже удобно для работы с агентом 👍