Проблема
Агенты у меня в ежедневной работе: скелет фичи, рефакторинг, тесты, скучная половина задачи. Я не против AI и не собираюсь писать ещё один пост «AI-код нельзя сопровождать».
Сбой уже и раздражающе. Вывод выглядит готовым. Типы сходятся, тесты зелёные, summary уверенный. Через неделю открываешь diff - и не хватает как раз сложного: почему четыре разных отказа схлопнулись в один false, кто владеет этим состоянием при конкуренции, зачем комментарий, если имя и так всё говорит, поймал бы тест тот баг, от которого он якобы защищает.
На одном файле нормально. На неделе agent-assisted работы в живом проекте это уже налог. Надоело его платить, поэтому собрал skills for intentional code.
Что я называю review cost
Не «насколько код правильный в абстрактном смысле», а сколько времени человеку реально стоит ему доверять: прочитать имена, восстановить failure modes, проверить concurrency-историю, понять, доказывают ли тесты хоть что-то. Эта цена растёт, когда автор оптимизирует за «выглядит готовым».
Тот же инстинкт, что и у security-maintenance cost для фреймворков. Конкретный вопрос простой: если отдать этот diff другому разработчику - или себе через три месяца - можно ли его проревьюить, не восстанавливая замысел с нуля?
Примеры, которые всплывают снова и снова
Ниже - укороченные куски из before/after пака. Полные версии в репозитории; здесь важна форма проблемы.
Один false на все ошибки
Путь списания с кошелька, который «работает», но прячет все исходы за одним возвратом:
export function process(d: any, flag = true, mode = 0): any {
try {
if (mode !== 1) return false;
if (
typeof d.uid !== "string" ||
typeof d.amt !== "number" ||
d.amt <= 0 ||
!(d.uid in wallets)
) {
return false;
}
const bal = wallets[d.uid] - d.amt;
wallets[d.uid] = bal;
if (flag) notifyProvider(d.uid, -d.amt);
return bal;
} catch {
return false;
}
}Невалидная сумма, нет кошелька, неверный mode, notify упал после успешного списания - снаружи всё одно и то же. Переименовать в updateWallet почти бесполезно, если модель ошибок осталась схлопнутой.
От агента я хочу другое: явная domain-операция для новых callers, разные failures, а старый process - тонкий adapter, если нужна совместимость:
export function debitWallet(
userId: string,
amount: number,
opts: { notify?: boolean } = {},
): number {
if (amount <= 0) throw new InvalidDebit(amount);
if (!(userId in wallets)) throw new WalletNotFound(userId);
const balance = wallets[userId] - amount;
wallets[userId] = balance;
if (opts.notify ?? true) {
try {
notifyProvider(userId, -amount);
} catch (cause) {
// Списание уже закоммичено - не прячем это за общим false.
throw new NotifyFailedAfterDebit(userId, balance, cause);
}
}
return balance;
}Вот для чего jane-street-style: не «красивее имена», а API, по которому ревьюер может рассуждать.
Комментарии, которые пересказывают код
Агенты обожают такой паттерн:
// Validate the request.
if (!req.productId || req.qty <= 0) return false;
// Get the stock.
const stock = inventory[req.productId] ?? 0;
// Charge the user.
charge(req.userId, req.unitPrice * req.qty);От этого толку ноль. Помогает то, чего имена не несут - policy, порядок, внешние ограничения:
// force is the operations override for VIP oversell.
if (stock < req.qty && !opts.force) return false;
// Reserve before charge so concurrent checkouts cannot both pass the
// stock check. Charge failure restores the accepted brief over-hold.
inventory[req.productId] = stock - req.qty;
// Stripe webhook delivery can race the read model; see #1842.
scheduleConfirm(req.orderId, { afterMs: 2000 });Это essential-comments: оставлять why, выкидывать narration.
Зелёные тесты почти без доказательств
it("processTransfer calls its dependencies", async () => {
wallets.get.mockResolvedValue({ id: "w1", balance: 100 });
await processTransfer({ from: "w1", to: "w2", amount: 40 }, { wallets, notify });
expect(wallets.get).toHaveBeenCalledWith("w1");
expect(wallets.save).toHaveBeenCalled();
expect(notify).toHaveBeenCalled();
});Такой тест может остаться зелёным, пока уезжает неверная сумма, сохраняется только одна сторона, или failure notify внезапно откатывает уже закоммиченные балансы. Мне нужно скорее такое:
it("keeps committed balances when notification fails", async () => {
const wallets = memoryWallets({ w1: 100, w2: 10 });
const notify = vi.fn().mockRejectedValue(new Error("provider down"));
const result = await processTransfer(
{ from: "w1", to: "w2", amount: 40 },
{ wallets, notify },
);
expect(result).toEqual({
ok: false,
error: "notify_failed",
committed: true,
});
expect(wallets.snapshot()).toEqual({ w1: 60, w2: 50 });
});Это tests-that-matter. Зелёный цвет - ещё не confidence.
Что в паке
Репозиторий публичный: github.com/yakoshiq/skills. Установка - одна команда:
npx skills add yakoshiq/skills
Работает с Pi, Claude Code, Cursor, Codex и другими агентами со skills. В picker полный набор - Intentional Code. Skills ровно пять, файлы короткие специально:
- jane-street-style - semantic clarity, когда это явная цель: domain-имена, честные failures, полезные типы. Следуй языку и репозиторию, не тащи functional religion.
- essential-comments - оставлять why / invariants / tradeoffs / external constraints. Удалять narration и AI-residue.
- concurrency-invariants - назвать гарантию до выбора мьютекса, retry, транзакции или idempotency key. Ownership, commit points, replay, interruption. Timeout обычно unknown outcome, пока API не доказывает обратное.
- surgical-changes - наименьшее coherent-изменение, которое полностью закрывает запрос. Только required и coupled правки; adjacent cleanup - не в diff.
- tests-that-matter - доказывать observable behavior, failure semantics, invariants, boundaries.
Before/after лежат отдельно от skill-папок, чтобы установленные skills оставались model-facing. Ещё прогонял пак на нескольких актуальных моделях - DeepSeek, MiMo, GLM, Qwen, MiniMax, Kimi, GPT среди них - и выкидывал то, что «работало» только на одной любимой. Универсальности это не даёт. Просто меньше привязки к defaults одной лаборатории.
Как я ими реально пользуюсь
Auto-trigger по description зависит от модели. Кто-то подхватывает skill сам, кто-то нет. Когда задача ровно про одно, предпочитаю явный invoke: /skill:essential-comments или «use surgical-changes» в запросе.
Грубо:
- размытый domain API, схлопнутые ошибки, primitive soup → jane-street-style
- шумные комментарии, AI-residue, нет why → essential-comments
- очереди, воркеры, retries, websockets, гонки → concurrency-invariants
- маленький фикс, соседний код не трогать → surgical-changes
- coverage без confidence, mock-heavy suite → tests-that-matter
Diff я всё равно читаю. Skills в основном меняют, на чём спотыкаюсь: меньше narration-комментариев, меньше API вида success: boolean, меньше тестов, которые проверяют только что мок вызвали. Иногда сложный вопрос только назван и всё ещё неверен - это уже лучше тихой каши и дешевле чинить.
Skill не выдаст вкус и не создаст культуру ревью. Кривая задача останется кривой. Если код никто не будет читать - пак не нужен, просто шипь. Если модель skill полуигнорирует, считай это bias, не контракт.
Зачем выложил
Не потому что боюсь, будто агенты «убьют craft». Генерация дешёвая. Дорогими остаются ревью, доверие и возможность потом менять код без археологии.
Если полезно:
skills.yakoshi.dev · github.com/yakoshiq/skills
npx skills add yakoshiq/skills
