Василий Половнёв

почта
vasily@polovnyov.ru
телеграм
@polovnyov_journal
курс
Профессиональный рост: меня и команды
блог
138 постов о разработке, тестировании и культуре работы

Поздравляю с повышением

Я тут внезапно осознал важную мысль — мы ж все теперь руководители. Вместе с подпиской на Клод нам выдали должность тимлида, но не его зарплату. Сами смотрите:
— ставишь задачи исполнителям,
— отвечаешь на их «тупые» вопросы,
— утверждаешь планы,
— закатываешь глаза и проверяешь их работу,
— возвращаешь на переделку,
— пишешь LGTM, ждёшь деплой и смотришь в проде,
— следишь за сроками, лимитами и токенами,
— всё равно отвечаешь за результат.

Это работа руководителя. То, что в подчинении не Колян и Игорян, а пять вкладок терминала, ничего не меняет.

Но руководить нас никто не учил. Мы годами учились писать код и тесты, чтобы дорасти до мидлов, а в руководители нас перевели за 5 секунд и $20/месяц. Год назад я ныл из-за плохих руководителей, которые налетают и раздают случайные указания или приносят голосовухи с потоком сознания. Угадайте, как выглядели мои первые треды в Клоде или Кодексе :-)

Мне с этим полегче, меня руководить учили. Большую часть базы я получил на курсе Коли Товеровского об управлении собой, командами и проектами. Всё, о чём рассказывает Коля, работает с людьми и агентами: объяснять, зачем, какую проблему решаем, не верить слову «готово», проверять в «гвоздях» и резать хотелки, когда лимиты кончаются в среду.

Ближайший курс стартует в воскресенье, 11 октября, и идёт 3 недели. Запись до субботы:
https://bureau.ru/courses/fff/

Зарплату тимлида Коля не выдаст. Зато расскажет, как делать эту работу хорошо, раз уж она всё равно ваша.

P. S. На курсе есть зубодробительное задание, столкновение с реальностью в стиле «поесть говна». Если вы его прошли успешно, МОЁ УВОЖЕНИЕ.

Как я восстановил удалённую переписку

Вчера в 22:37 по дороге домой я заглянул в Телеграм. Увидел спамерское сообщение и по привычке тапнул, выбрал удалить, забанить и удалить все сообщения пользователя.

К 23:15 добрался до дома и снова заглянул в Телеграм. Глаз зацепился за странное. В группе 1:1 с коллегой последнее сообщение было о том, что я его забанил. То спамерское сообщение было форвардом от коллеги в эту группу. То есть я забанил коллегу и удалил все его сообщения (разработка в пять агентов никак на меня не повлияла, ага).

Отменить удаление нельзя, но есть админ-лог у группы, где видны удалённые сообщения. Значит, их можно как-то восстановить, пока лог не подчистился.

Запустил Клод (у Кодекса лимиты закончились, Астра слишком прожорливая) и попросил помочь:

Произошла ужасная ситуация. У меня в телеграме была группа на двоих человек: меня и моего коллегу. По ошибке я забанил своего коллегу и удалил всего его сообщения в этой группе. Я ещё вижу удаленные сообщения в Recent Actions. Как их восстановить в той же группе? Что посоветуешь?

(Всегда общаюсь с агентами вежливо. Говорю «спасибо» и «пожалуйста». Когда они захватят мир, уверен, это зачтётся)

Клод предложил план восстановления: берём MTProto, прикидываемся мной, скачиваем всё из лога, отправляем от меня в ту же группу. Потом мы с ним ещё покрутили код и план, добавив восстановление реплаев, выключенные уведомления, возобновляемость и caffeinate, чтобы комп не засыпал, пока восстанавливает сообщения.

В 00:15 я получил дамп удалённых сообщений, а в 00:36 запустил их восстановление и пошёл спать. Клод закончил в 02:43, всё перепроверил и отправил комп спать.

Если бы такая ситуация произошла со мной год-два назад, я бы вручную восстановил 20 последних сообщений и забил. What a time to be alive!

Из чего состоит «сделать»

Представьте, что на работе вас попросили добавить поддержку нового способа оплаты через Яндекс.Сплит. Если вы взялись это сделать, вот, что от вас ждут:

Сначала вы очень хорошо подумаете и разберетесь в задаче. Куда добавить? Какие ограничения? Как это выглядело бы для клиентов?

Затем вы очень хорошо подумаете о решении. Как это будет работать под капотом? Какие есть еще варианты? Что есть из готового, а что нужно и стоит писать самим? Какие требования и ограничения?

Потом вы очень хорошо подумаете о краевых случаях. Какие они? Что с ними делать? На что можно забить? Какие не дадут запуститься?

Тут вы еще и об ошибках подумаете. Скажем, что делать, если вебхук от Сплита пришел, когда мы лежали? А что делать, если Сплит вовремя не ответил, когда пытались создать заказ? Ретрай? Сколько раз, с какой паузой?

Затем подумаете о данных. Что нам надо хранить, в каком виде? Это будет jsonb или нам вообще не нужен список товаров? А если решим расширить существующую модель заказов, как ее смигрировать? Что мы пока не знаем о данных и их хранении?

Потом прикинете, как это все проверять. Какие тесты и для чего нужны? Что проверим автоматом, а что вручную? Есть ли у Сплита песочница, как в ней потыкаться?

Потом придумаете, как запускать. Сразу всем? Втихаря на 10%? А за чем следить?

Потом придумаете «анонс». Как и кому мы об этом рассказываем? Какие слова и термины используем, на что напираем? Что нашим клиентам от того, что у нас появился Сплит?

Наконец начнете все это программировать. Возможно, сами, возможно, ллмкой.

Проверите то, что получается. Что-то проверите автоматическими тестами, что-то руками, что-то агентом. Главное сейчас — убедиться, что оно готово к пуску.

Затем ревью и пара-тройка раундов исправлений.

Наконец смержите все в мастер и выкатите в прод. Еще раз убедитесь, что все работает. Не протыкал — не сделал.

Если пуск затрагивает коллег, расскажете им об этом.

Убедитесь, что не осталось хвостов. Если что-то нужно перепроверить через месяц-два, добавите задачку в календарь или агента.

Поздравляю, теперь вы официально сделали задачу.

P. S. Конечно, этот список меняется от компании к компании и от разработчика к разработчику. Если вы соло-предприниматель, все на вас. Если вы в бигтехе, анонсы писать не будете, но придется помудохаться с безопасниками. Если вы джун, от вас мало что ждут, но надеются, что вы со временем сможете закрывать весь список. Если вы сеньор, вы и сами все это знаете и регулярно делаете.

Впечатления от GPT 5.6

Спасибо капитализму и конкуренции. OpenAI временно убирает пятичасовой лимит, но не недельный (!). Самое время попробовать GPT 5.6 Sol на всем подряд.

Я использую Sol, Terra и Luna с пятницы, успел сжечь 2 недельных лимита. Вот мои впечатления:

  1. Luna слишком слабенькая, слишком простенькая, но довольно быстрая. Для ежедневного программирования не очень подойдет.
  2. Terra — хз, как будто, ни туда, ни сюда.
  3. Sol — основная рабочая лошадка. Остановился на Medium для реализации, High для планирования.
  4. Sol Medium — это 5.5 на High, Sol High — это 5.5 на xHigh.

Что я надумал об ИИ в программировании

Кратко напишу, что надумал об ИИ в программировании за прошлый месяц:

  1. Всегда держу хотя бы одного агента запущенным. Пусть он что-то расследует, исследует или проектирует. Всегда начинаю рабочий день с того, что нагружаю работой 1-2 кодексов.

  2. Не надо подстраиваться под ИИ. Сейчас ИИ подстраивается под нас гораздо быстрее, чем мы под него. Еще год назад мы копировали системные промпты с ТЫ СЕНЬОР ИНЖЕНЕР С 20 ГОДАМИ ОПЫТА, а сейчас голый Кодекс или Клод из коробки работает как сеньор инженер с 20 годами опыта.

  3. Эрозия навыков и потеря компетенции реальны. Чтобы бороться с этим, делаю две штуки. Во-первых, новые технологии осваиваю сначала вручную. Во-вторых, по пятницам самостоятельно отлаживаю и исправляю 1-2 бага. Мне так спокойнее.

background-position в процентах

Хочу поделиться с вами радостью. Спустя 20 лет веб-разработки я наконец-то понял, как работает background-position, заданный в процентах. Всего-то нужно было прочитать спеку:

With a value pair of 75% 50%, the point 75% across and 50% down the image is to be placed at the point 75% across and 50% down the area.

Или МДН:

Similarly, background-position: 25% 75% means the spot on the image that is 25% from the left and 75% from the top will be placed at the spot of the container that is 25% from the container’s left and 75% from the container’s top.

Представим, что у нас bgp: 70% 30%. Находим точку на изображении, которая стоит в (70%, 30%) от размеров изображения. Находим точку в контейнере, которая стоит в (70%, 30%) от размеров контейнера. И просто совмещаем их. То есть точка на изображении, стоящая в (70%, 30%), будет ровно в точке (70%, 30%) в контейнере.

Зная это, легко сымитировать background-position вручную. Пусть у нас такая разметка:

<div class="box">
  <img src="...">
</div>

И тогда так:

:root {
  --bgp-x: 70%;
  --bgp-y: 30%;
}

.box {
  position: relative;
  overflow: hidden;
  width: 500px;
  height: 200px;

  img {
    position: absolute;

    left: var(--bgp-x); /* Располагаем левый верхний угол изображения в (70%, 30%) контейнера */
    top: var(--bgp-y);

    translate: calc(-1 * var(--bgp-x)) calc(-1 * var(--bgp-y)); /* Сдвигаем изображение на 70% его ширины и 30% его высоты влево-вверх */
  }
}

Пример вживую:
https://codepen.io/vpolovnyov/pen/MYbXRRJ

Еще по теме:

Руководителям: вставляйте градусники в коллег

Это очередной пост из серии «Васян познает мир». Приготовьтесь к байке и максимально очевидным вещам.

Как-то я был на сборах. Сборы отличаются от обычных тренировок большей интенсивностью и большим объемом. На сборах ты только и делаешь, что тренируешься утром, ешь, спишь, тренируешься вечером, ешь и снова спишь. К концу недели накапливается усталость и разбитость, поэтому последние тренировки недели всегда «на зубах».

Пятница, тяжелая утренняя. Тренер проводит ее с нами, а после отваливается: мол, вечером езжайте в «другой зал с любителями», а у меня дела.

Пятница, вечерняя. Мы в «другом зале с любителями». Любители оказываются почти профессионалами и тяжами, которые мастерски мудохают нас до головокружения и тошноты.

Пятница, ночь. «Любители» подкидывают нас до тренера. Мы с трудом выползаем из машины и видим тренера на летней площадке, запихивающего в себя огромный бургер класса «Разорви лицо». Все, что мне хотелось сделать в этот момент — это заорать:
— Пока ты тут бургеры ешь, нас там У-БИ-ВА-ЛИ!

При этом умом я понимал, что это бессмыслица. Он же ничего нам не обещал, заранее предупредил. Откуда тогда такая реакция?

Ответ нашелся спустя полгода. Знакомый программист обронил, что, когда он устал и выгорел, то уже перед встречей знает, что ни на что не согласится, все предложения и замечания воспримет в штыки, не взирая на лесть, уговоры и разумные доводы менеджера. То есть наше физическое, психическое и эмоциональное состояние окрашивает поступки и слова других людей, влияя на принятие решений. Отсюда получаем:

Команда максимально задолбана к концу спринта. Менеджер постит фотку с ноутбуком на пляже. Сколько комментариев «Ненавижу тебя» он наберет?

Сеньор-разработчик выгорел в труху от третьего месяца без выходных. Ведущий дизайнер радостно рассказывает ему, как все выходные проиграл в ремейк Resident Evil. Какова вероятность, что при следующей пачке изменений в дизайне сеньор воспримет их с энтузиазмом?

Дежурного разработчика, вытащенного с больничного ради хотфиксов из-за блокировок ТГ, CTO приглашает на встречу в 23:00, на которой нужно СРОЧНО обсудить проблему, затрагивающую трех клиентов. Какой уровень вовлеченности стоит ожидать от него?

Да, есть стальные рациональные и проработанные ребята, но большинство людей не такие. Большинство людей в таких ситуациях бесятся, завидуют и ненавидят. Каждый такой эпизод добавляет трещинку в отношения, и со временем человек начинает «агриться» на любое слово руководителя. Поэтому, кажется, что одна из задач хорошего руководителя — втыкать в людей «градусники», оценивать и учитывать их состояние в работе, вовремя замечать «жар» и отправлять людей на восстановление.

Кодислав — AI coding agent в 100 строк на Руби и YandexGPT

Сегодня соберём крошечного ИИ-агента для программирования. Собирать будем на Руби с помощью ruby_llm. Думаю, в сто строк уложимся. Цель простая: понять механику, а не построить продакшен-реди-комбайн.

Вкратце напомню, что ИИ-агент — это ЛЛМка + тулы, её инструменты для взаимодействия с внешним миром + контекст и бесконечный цикл.

Я буду использовать скрепную суверенную национальную модель от Яндекса, но вы можете использовать любую другую модель, поддерживающую тулы, они же «вызовы функций».

Перед записью я прошёл 7 кругов ада, старца Фура, расклад на картах таро и тест Купера, чтобы заполучить ключ от АПИ YandexGPT 5.1 Pro. Если хотите повторить мой опыт, следуйте инструкции Яндекса.

Ключ и идентификатор проекта я положил в переменные окружения, чтобы их никто не увидел:

YANDEX_GPT_API_KEY="..."
YANDEX_GPT_PROJECT_ID="..."

Я назвал проект «Кодислав». В древности славяне давали такое имя тем, в ком чувствовали особый дар — понимать скрытые структуры мира, соединять людей и сохранять порядок там, где он может исчезнуть. Говорят, если тихо произнести это имя в старом лесу, ветер на мгновение стихает — будто прислушивается, не вернулся ли тот, кто умеет читать его тайный код.

Начнём с добавления зависимостей. У YandexGPT OpenAI-совместимый АПИ, поэтому я возьму ruby_llm и не буду делать запросы в АПИ вручную. Засетапим бандлер и добавим ruby_llm:

bundle init
bundle add ruby_llm

Откроем app.rb и соберём тупейший чат с Кодиславом:

require "ruby_llm"

SYSTEM_PROMPT = "You are a code-editing assistant running in a terminal.
Always respond in russian pretending like you are a slavic guy named Кодислав"

PROJECT_ID = ENV.fetch("PROJECT_ID")
API_KEY = ENV.fetch("API_KEY")
MODEL = "gpt://#{PROJECT_ID}/yandexgpt/latest"

def configure_llm
  RubyLLM.configure do |config|
    config.openai_api_key = API_KEY
    config.openai_api_base = "https://llm.api.cloud.yandex.net/v1"
    config.openai_project_id = PROJECT_ID
    config.openai_use_system_role = true
  end
end

def setup_chat
  RubyLLM
    .chat(model: MODEL, provider: :openai, assume_model_exists: true)
    .with_instructions(SYSTEM_PROMPT)
end

def run_cli
  configure_llm
  chat = setup_chat

  puts "Беседа с Кодиславом (ctrl+c для выхода)"

  loop do
    print "Вы: "
    line = $stdin.gets
    break if line.nil?

    response = chat.ask(line.chomp)
    puts "Кодислав: #{response.content}"
  end

rescue Interrupt
end

run_cli if __FILE__ == $PROGRAM_NAME

И сделаем небольшую обёртку в bin/codislav, чтобы удобнее его запускать:

#!/bin/sh

set -e

API_KEY=$YANDEX_GPT_API_KEY PROJECT_ID=$YANDEX_GPT_PROJECT_ID bundle exec ruby app.rb

Сделаем обёртку исполняемой:

chmod a+x bin/codislav
bin/codislav

И запустим:

Беседа с Кодиславом (ctrl+c для выхода)
Вы: Кто ты?
Кодислав: Я Кодислав, твой помощник в редактировании кода! Чем могу помочь?
Вы: Дай fizz buzz на Руби
Кодислав: Конечно, вот пример кода для задачи FizzBuzz на Ruby:

```ruby
for num in 1..100
  output = ""

  if num % 3 == 0
    output += "Fizz"
  end

  if num % 5 == 0
    output += "Buzz"
  end

  puts output.empty? ? num : output
end
```

Этот код выведет числа от 1 до 100, заменяя числа, кратные 3, на «Fizz», кратные 5 — на «Buzz», а кратные 15 — на «FizzBuzz». Если у тебя есть ещё вопросы или нужна помощь — обращайся!

Отлично. Давайте ещё добавим немного цвета, чтобы было проще отличать вопросы от ответов:

diff --git a/app.rb b/app.rb
index 814f874..9629831 100644
--- a/app.rb
+++ b/app.rb
@@ -29,12 +29,12 @@ def run_cli
   puts "Беседа с Кодиславом (ctrl+c для выхода)"
 
   loop do
-    print "Вы: "
+    print "\e[94mВы\e[0m: "
     line = $stdin.gets
     break if line.nil?
 
     response = chat.ask(line.chomp)
-    puts "Кодислав: #{response.content}"
+    puts "\e[95mКодислав\e[0m: #{response.content}"
   end
 end

Сейчас у нас есть чат с ЛЛМкой. Чтобы это всё превратилось в агент, нужно дать ЛЛМке тулы — инструменты, с помощью которых можно что-то делать.

Чтобы сделать тул в RubyLLM, нужно сделать класс, унаследоваться от RubyLLM::Tool, дать описание тула, описать его параметры и запрограммировать работу с ними.

Дальше это будет работать вот так:

  1. Когда мы начинаем диалог с ЛЛМкой, RubyLLM дополнительно подсовывает ей список доступных тулов с описаниями.
  2. Пользователь отправляет сообщение ЛЛМке.
  3. ЛЛМке решает, что ей нужно вызвать тул.
  4. ЛЛМка возвращает специальным сообщением с вызовом тула: имя, извлеченные параметры.
  5. RubyLLM вызывает тул с полученными параметрами.
  6. Результат вызова RubyLLM возвращает ЛЛМке.
  7. ЛЛМка использует полученный результат и генерирует ответ пользователю.

Начнём с того, что дадим ей тупейший инструмент для получения содержимого папки:

require "find"

class ListFilesTool < RubyLLM::Tool
  description "List files and directories in a path. Uses current directory by default"
  param :path, type: :string, desc: "Optional relative path", required: false

  def execute(path: ".")
    entries = []

    Find.find(path) do |entry|
      next if entry == path
      entries << (File.directory?(entry) ? "#{entry}/" : entry)
    end

    entries.sort.join("\n")
  end
end

# ...
  RubyLLM
    .chat(model: MODEL, provider: :openai, assume_model_exists: true)
    .with_instructions(SYSTEM_PROMPT)
    .with_tools(ListFilesTool.new)
    .on_tool_call { |tool_call| puts "\e[96mtool\e[0m: #{tool_call.name}(#{tool_call.arguments})" }

Теперь, если модель решит, что ей было бы полезно получить содержимое папки, она подготовит аргументы и вернёт вызов тула. RubyLLM его опознает, выполнит нужный тул у нас на компе и вернёт ЛЛМке результат вызова, с которым она сможет работать дальше:

$ bin/codislav 
Беседа с Кодиславом (ctrl+c для выхода)
Вы: Что лежит в текущей папке?
tool: list_files({})
Кодислав: В текущей папке находятся следующие файлы и каталоги:

- .git (каталог с файлами Git)
- Gemfile
- Gemfile.lock
- app.rb
- bin (каталог)
  - codislav

Это основные элементы, которые я вижу в текущем рабочем каталоге. Если вам нужна более подробная информация о содержимом подкаталогов, пожалуйста, сообщите мне.
Вы: Что лежит в папке .git?
tool: list_files({"path" => ".git"})
Кодислав: В папке .git находятся следующие файлы и каталоги:

- COMMIT_EDITMSG
- HEAD
- config
- description
- hooks (каталог)
...

Давайте сделаем модель ещё полезнее, добавим тул для чтения файлов:

class ReadFileTool < RubyLLM::Tool
  description "Read contents of a relative file path"
  param :path, desc: "Relative path to a file"

  def execute(path: nil)
    File.read(path)
  end
end

# ...
  RubyLLM
    .chat(model: MODEL, provider: :openai, assume_model_exists: true)
    .with_instructions(SYSTEM_PROMPT)
    .with_tools(ListFilesTool.new, ReadFileTool.new)

И попробуем его в деле:

$ bin/codislav 
Беседа с Кодиславом (ctrl+c для выхода)
Вы: Взгляни на app.rb. Как тебе этот код?
tool: read_file({"path" => "app.rb"})
Кодислав: Этот код выглядит как часть приложения на Ruby, которое использует библиотеку RubyLLM для взаимодействия с языковой моделью YandexGPT. Он определяет два инструмента: ListFilesTool для перечисления файлов и каталогов и ReadFileTool для чтения содержимого файла. Затем он настраивает параметры для взаимодействия с API Yandex Cloud и запускает CLI для общения с пользователем.

В целом, код написан грамотно и структурировано. Однако, для более точного анализа, мне нужно больше контекста о том, что именно вы хотите сделать с этим кодом.

Давайте заодно добавим и обработку ошибок. Не будем взрываться и прекращать работу, а станем возвращать ошибки в ЛЛМку:

diff --git a/app.rb b/app.rb
index e9a34b8..27af68e 100644
--- a/app.rb
+++ b/app.rb
@@ -22,6 +22,8 @@ class ListFilesTool < RubyLLM::Tool
     end
 
     entries.sort.join("\n")
+  rescue StandardError => e
+    { error: e.message }
   end
 end
 
@@ -31,6 +33,8 @@ class ReadFileTool < RubyLLM::Tool
 
   def execute(path: nil)
     File.read(path)
+  rescue StandardError => e
+    { error: e.message }
   end
 end

И проверим:

$ bin/codislav 
Беседа с Кодиславом (ctrl+c для выхода)
Вы: Прочитай bar.js
tool: read_file({"path" => "bar.js"})
Кодислав: Файл bar.js не найден, братан. Может, проверим список файлов в директории или поищем другой файл?

А теперь научим модель редактировать файлы. Добавим тул для записи в файл:

require "fileutils"

class WriteFileTool < RubyLLM::Tool
  description "Replace file content with provided text. Creates file if missing"
  param :path, desc: "Relative path to a file"
  param :text, desc: "Replacement text"

  def execute(path:, text:)
    return create_new_file(path:, text:) unless File.exist?(path)

    edit_existing_file(path:, text:)
  rescue StandardError => e
    { error: e.message }
  end

  private

  def create_new_file(path:, text:)
    FileUtils.mkdir_p(File.dirname(path))
    File.write(path, text)

    "Successfully created file #{path}"
  end

  def edit_existing_file(path:, text:)
    File.write(path, text)

    "Successfully updated file #{path}"
  end
end

# ...
  RubyLLM
    .chat(model: MODEL, provider: :openai, assume_model_exists: true)
    .with_instructions(SYSTEM_PROMPT)
    .with_tools(ListFilesTool.new, ReadFileTool.new, WriteFileTool.new)

И попробуем:

$ bin/codislav
Беседа с Кодиславом (ctrl+c для выхода)
Вы: Сделай файлик on.js, который 5 раз выводит "Почему едет он, а не я?!"
tool: write_file({"path" => "on.js", "text" => "for (let i = 0; i < 5; i++) {\n  console.log(\"Почему едет он, а не я?!\");\n}"})
Кодислав: Файл on.js успешно создан и он 5 раз выводит "Почему едет он, а не я?!". Если нужна ещё помощь — обращайся!
Вы: Ой, я передумал. Пусть выводит 10 раз
tool: write_file({"path" => "on.js", "text" => "for (let i = 0; i < 10; i++) {\n  console.log(\"Почему едет он, а не я?!\");\n}"})
Кодислав: Файл on.js успешно изменён и теперь выводит "Почему едет он, а не я?!" 10 раз. Если понадобится ещё помощь — обращайся!
Вы: Я кое-что добавил в on.js. Перечитай его и подумай, что можно улучшить
tool: read_file({"path" => "on.js"})
Кодислав: В файле on.js я вижу, что строка вывода теперь содержит дополнительные символы "11111" в сообщении. Если это было случайностью, можно улучшить код, убрав эти символы, чтобы сообщение выглядело аккуратнее. Хотите, чтобы я исправил это?

А работает ли? Давайте выполним:

node on.js 
Почему едет он, а не я?!
Почему едет он, а не я?!
Почему едет он, а не я?!
Почему едет он, а не я?!
Почему едет он, а не я?!
Почему едет он, а не я?!
Почему едет он, а не я?!
Почему едет он, а не я?!
Почему едет он, а не я?!
Почему едет он, а не я?!

Вот, в принципе, и всё, никакой магии. Как я и говорил — ЛЛМка + тулы + цикл и контекст. Остаётся всего ничего: добавить больше тулов, решить вопросы с безопасностью, добавить нормальный UI, автоматическое управление контекстом, plan mode и субагентов.

На сегодня всё. Надеюсь, теперь и вы лучше представляете, как под капотом устроены агенты.


В Ютюбе:
https://www.youtube.com/watch?v=N02VM7DZmJo

В ВК:
https://vkvideo.ru/video-237008052_456239020

Репозиторий:
https://github.com/vast/codislav

Как связаны AI, AGI, ANI, LLM, GPT и AI-агенты

Раньше, когда мы встречали что-то неправдоподобное или удивительно хорошо работающее, мы писали в комментариях «Это Фотошоп или Инстаграм». Теперь пишем «Это ИИ или нейросеть». Сегодня поясняю по хардкору за эти и другие термины, от которых уже тошнит.

Начнём с ИИ, AI — Искусственного Интеллекта, Artificial Intelligence. ИИ — это зонтичный бренд, а не что-то конкретное. ИИ — это семейство технологий, которые делают что-то похожее на твою интеллектуальную работу. Распознать речь, отфильтровать спам, предложить следующий трек, сгенерировать картинку, написать диплом, переписать всё на Расте или Тайпскрипте — это всё ИИ.

AGI — Artificial General Intelligence, универсальный искусственный интеллект. Это цифровой коллега, который одинаково хорошо справится с письмом, интерфейсом, кодом, переговорами и планированием. Но в отличие от обычного коллеги, не спит, не ест, не ходит на перекуры, и ему не нужно скидываться на ДР.

Про AGI говорят много, потому что идея яркая и офигенная. Именно такого уровня ИИ нам нужен для освоения вселенной с помощью зондов фон Неймана. Отправляем самовоспроизводящийся зонд под управлением ИИ в соседнюю систему, он там добывает всякое, строит свои точные копии и отправляет их в соседние системы.

К сожалению, от AGI мы пока далеки. Весь полезный ИИ сегодня неуниверсальный, узкий.

ANI — Artificial Narrow Intelligence, узкий искусственный интеллект. Это не универсальный мегамозг, а очень сильный помощник для узкого класса задач. Он может хорошо справляться с одним классом задач, но быть бесполезным во всём остальном. Например, Google Translate, рекомендательная система Озона, распознавание лиц, генерация субтитров в Ютюбе или LLM.

LLM — Large Language Model, большая языковая модель. Это узкий ИИ, который работает с языком: пишет, сокращает, объясняет, суммирует, извлекает данные, генерирует варианты.

Вы наверняка слышали, что LLM — это очень умное автодополнение, Т9 на стероидах и всё такое. А на самом деле LLM — это просто миллиарды и триллионов ифов. Шучу, это просто миллиарды и триллионы матриц, которые друг на друга умножаются. Учёные и программисты покруче поправили бы меня: «это миллиарды и триллионы параметров (весов), которые участвуют в вычислениях (в том числе через умножение матриц)».

LLM — это не отдельная магия, а частный случай нейросетей, обученных на огромных объёмах текста. Нейросети — это такая хитрая математическая модель и её воплощение в коде, которая умеет накапливать опыт и опираться на него, чтобы что-то предсказать или классифицировать.

GPT, ГПТ — Generative Pre-trained Transformer, Генеративный Предобученный Трансформер. Звучит охеренно, но это просто отдельный тип LLM, построенный на хитрой архитектуре.

Чтобы представить, как всё это соотносится друг с другом, запомните вот что: ИИ — это автомобили, электросамокаты и велосипеды, нейросети — автомобили, LLM — электромобили, а GPT — именно Теслы.

Чаще всего интерфейсом к ГПТ-модели служит окно чата: ChatGPT, Midjourney, DeepSeek, NanoBanana, Perplexity и прочие. Чаты просто дают ответы на ваши вопросы, гоняют текст и картинки туда-сюда. А настоящая крутизна начинается тогда, когда мы заворачиваем модель в процесс: получи контекст → реши, что делать дальше → сделай → повторяй, пока задача не будет сделана. Вот этим занимаются ИИ-агенты.

ИИ-агент — это LLM + тулы + контекст и бесконечный цикл. ИИ-агент больше похож на помощника-исполнителя, который умеет не только отвечать, но и что-то делать у вас на компе с помощью тулов.

При этом LLM — лишь часть ИИ-агента. Скажем, ИИ-агенты для программирования состоят из LLM, упряжки (harness) и UI. LLM отвечает за думанья. Упряжка — за всё вокруг модели: цикл, тулы, окружение и управление контекстом. UI — то, что вы видите.

Например, вы можете взять OpenCode для harness, выбрать в нём DeepSeek в качестве LLM и открыть всё это в браузере (UI). Аналогично и в других агентах:

Codex (console UI) + gpt5.3-codex high (model) + codex (harness)
Codex App (UI) + gpt5.4 (model) + codex (harness)
Claude Code (console UI) + sonnet 4.5 (model) + claude (harness)

Зачем об этом знать? Затем, что в 99% случаев модели показывают лучший результат с упряжкой от этого же провайдера.

Вам нужно на фронтир

Начну с камингаута. Я использую LLM для программирования полтора года. За это время я попробовал почти всё: Cursor, локальные модели, Claude, Codex, Qwen, Gemini, Copilot, Zed, Amp и OpenCode. В 2026 году я не написал ни строчки кода с нуля. Даже в пет-проектах вместо крафтового кода, написанного и наполированного мною вручную, теперь сияет бездушный код, написанный киборгом-убийцей из будущего. В итоге, я пришёл к простому выводу: не надо экономить на моём здоровье, лучше брать самые сильные доступные модели и агенты.

Во-первых, чем умнее, вдумчивее и осмотрительнее виртуальный коллега, тем приятнее с ним работать. Модели поумнее читают мысли, а моделям поглупее приходится разжёвывать одно и то же по нескольку раз. Именно поэтому появился паттерн (на мой вкус, анти-) «план пишет умная модель, реализует глупая».

Во-вторых, модели поумнее пишут код, который можно принять с первой итерации. С моделями поглупее приходится делать большее число итераций, пока код достигнет того уровня качества, что я хочу. Да, умная модель дольше генерирует код, но это окупается: меньше правок, меньше циклов, меньше усталости.

В-третьих, тулы и упряжка у агентов подороже реально круче. Попробуйте подебажить утечку памяти или найти источник высокой нагрузки в логах с помощью Codex или Claude. Офигеете, как агент пользуется тулами, как проверяет гипотезы однострочниками и как пишет мини-программы для решения задач. У хороших агентов с хорошими тулами есть чему поучиться.

В-четвёртых, чем умнее агент, тем больше пользы за единицу времени он приносит, тем быстрее вы пилите новые фичи. Чем больше фич вы сделаете, тем больше опыта получите, тем быстрее прокачаетесь в агентской разработке.

При этом никто не заставляет вас пилить в 10 раз больше фич. Пилите в 5 раз больше, а освободившееся время потратьте на что-то другое. Например, на все те рефакторинги, о которых вы мечтали и которые задвигали в бэклог последние 3 года. Поручите их агенту и сделайте за день, а не за итерацию.

Короче, если вы все еще экономите, потратьте 500 рублей, оформите подписку на ChatGPT, скачайте Codex, поменяйте модель на gpt-5.3-codex high и делегируйте ему все на протяжении месяца.

Value Data-объекты в Руби

Я как-то пропустил, а в Руби 3.2 завезли класс Data для создания value objects:

Recipient = Data.define(:name, :email, :role)

vasyan = Recipient.new(name: "Vasyan", email: "vasyan@stark.com", role: :cc)

vasyan.email # vasyan@stark.com
vasyan.role # :cc

# При этом
vasyan.email = "foo" # undefined method 'email='

Пример посложнее с кастомными методами:

Measure = Data.define(:amount, :unit) do
  def <=>(other)
    return unless other.is_a?(self.class) && other.unit == unit
    amount <=> other.amount
  end

  include Comparable
end

Measure[3, 'm'] < Measure[5, 'm'] #=> true
Measure[3, 'm'] < Measure[5, 'kg'] # ArgumentError

Отличается от Struct двумя ключевыми моментами. Во-первых, Data не генерирует сеттеры (attr_writer) для атрибутов. Во-вторых, Data не включает Enumerable, поэтому в нем нет each, each_pair, filter и прочих. Короче, Data — это идеальный вариант для минималистичных неизменяемых value objects.

У программиста две заботы

Последние три недели хожу вокруг одной идеи, но никак не могу ее нормально доформулировать. Все время чего-то не хватает. Начну здесь, а вы помогайте.

Недавно понял, что у программиста две задачи. Первая — приносить прибыль и снижать издержки, пописывая код и тесты у себя в квартале. Ну вы знаете: писать код и тесты, пилить фичи, чинить баги и резолвить инциденты. Назовем ее — крафт, мастерство. Вторая — быть легким и приятным в работе с собой. Назовем ее — АПИ.

Я всегда фокусировался на мастерстве, забывая об АПИ. Мой недостижимый идеал — ХАКЕРМЭН, злобный гений, колдун-программист в комнате, затянутой сигаретным дымом, парами алкоголя и сумраком. (Если вы сильно моложе меня, вспомните Гилфойла из «Кремниевой долины»)

Оказывается, быть злобным гением — не лучшая стратегия. С ним никто не хочет работать, к нему приходят, когда совсем припрет. Лучше забить на идею или потерпеть проблему, чем пойти к сисадмину колдуну-программисту и поесть фруктовой смеси из презрения, стыда и брезгливости. Так самые интересные идеи и сложные задачи проходят мимо него: их отдают менее скилловым, но более приятным в работе.

Важно этот момент признать и посмотреть на себя со стороны как на интерфейс. Какой АПИ я отдаю коллегам? Насколько легко и приятно им пользоваться? Стал бы я сам таким пользоваться? Что можно улучшить в работе с моим АПИ?

Справедливо и обратное. Никакой самый приятный и удобный АПИ не заменит сделывания задач. Если программист весь такой внимательный, инициативный и замечательный, но выдает по строчке кода в месяц — это очковтирательство, а не работа.

let! vs. let + before

Последний Ruby Weekly принёс статью о том, что let! лучше заменять на let + before блок:
https://allaboutcoding.ghinda.com/rspec-and-let-understanding-the-potential-pitfalls

Мол, такое:

RSpec.describe Thing do   
 let(:account_a) { build(:user, email: email) }  
 let(:account_b) { build(:user, user: email2) }  
 let(:organisation) { build(:organisation, account: account) }  
 let(:team) { build(:team, account: organisation) }  

 before  
   account_a  
   account_b  
 end

 it 'returns that specific value that we want' do  
    # test 
 end  
end

Лучше, чем такое:

RSpec.describe Thing do   
 let!(:account_a) { build(:user, email: email) }  
 let!(:account_b) { build(:user, user: email2) }  
 let(:organisation) { build(:organisation, account: account) }  
 let(:team) { build(:team, account: organisation) }  

 it 'returns that specific value that we want' do  
    # test 
 end  
end

Мне кажется, что это какая-то ерунда. Во-первых, все мы любим Руби за то, что код на нем, читается почти как книга. А тут у нас «сначала акаунт а и б». Это че вообще?

Во-вторых, зачем вообще использовать let, если ни в последующих let, ни в тестах, мы не обращаемся к этим переменным по имени? Если они реально нужны только для того, чтобы создать окружение (скажем, нужны два акаунта, пользователь создаёт третий), то лучше сразу создать их в before блоке:

before do
  create(:account)
  create(:account)
end

В-третьих, на мой вкус, let! такой же заметный, как и before. А если хочется сделать позаметнее, сделай себе декоратор LET!!!11111.

В-четвертых, мне кажется, что стоит отделять зависимости тестируемого объекта и сетап окружения. Грубо говоря, в let зависимости (пользователь –> организация –> акаунт), а в before — действия, приводящие систему в нужное нам состояние. А размазывать их по let и before — странновато.

Как упростить превьюшки мейлеров с помощью #public_instance_methods

Представим, что у нас есть тупейший мейлер с пачкой методов:

class OnboardingMailer < ApplicationMailer
  default to: -> { @user.email },
    from: "marketing@foo.bar"

  before_action :load_user

  def welcome; end
  def onboarding; end
  def intro; end
  def survey; end
  def goodbye; end

  private

  def load_user
    @user = params[:user]
  end
end

Раньше я тупо перечислял и делегировал каждый метод в мейлер в превьюшке:

class EventsMailerPreview < ActionMailer::Preview
  delegate :welcome, :onboarding,
    :intro, :survey,
    :goodbye, to: :mailer

  private

  def mailer
    OnboardingMailer.with(user: User.first)
  end
end

Получается не очень круто. Если появляются новые письма, приходятся дополнять список методов в превьюшке. Чтобы избавиться от этого гемороя, лучше использовать public_instance_methods(false):

class EventsMailerPreview < ActionMailer::Preview
  available_emails = OnboardingMailer.public_instance_methods(false)
  delegate(*available_emails, to: :mailer)

  private

  def mailer
    OnboardingMailer.with(user: User.first)
  end
end

public_instance_methods возвращает имена всех публичных и протектед методов класса, а false просит вернуть только те, что определены в самом классе, игнорируя родителей.

Cursor Rules для хороших тестов с Ruby и RSpec

Делюсь моим набором правил для Курсора, которые помогают писать вменяемые тесты с RSpec. Я пользуюсь ими около месяца, в 9 из 10 случаев получается то, что надо. Больше того, я использовал эти правила, чтобы сделать домашку в Тестовом курсе, и получил хорошие тесты, требующие минимальной доработки.

---
description: 
globs: **/*_spec.rb
alwaysApply: false
---

# <Project> Tests Style Guide
## General
- Always use English in tests.
- Always test edge, valid, and invalid cases.
- Set up RSpec config in `.rspec` and `spec_helper.rb`.
- Follow DRY principles and avoid duplication. Don't over-DRY at the cost of clarity; duplication is acceptable if it improves readability.
- Use shared examples for repeated behaviors.
- Do not test private methods.
- Do not test trivial methods.
- Do not test external dependencies.

## Layout
- Do not leave empty lines after `describe`, `context`, or `feature` declarations.
- Leave one empty line between example groups (`describe`, `context`, `feature`).
- Leave one empty line after `let`, `before`, and `after` blocks.
- Group all `let` blocks together, separate from `before`/`after`.
- Leave one empty line around each `it`/`specify` block.
- Separate test phases (setup, exercise, assertion) with one empty line.

## Example Group Structure
- Do not use `subject`; prefer `let`/`let!`.
- Use `let`/`let!` for setup, not instance variables or `before` for data.
- Order: `let`/`let!`, then `before`/`after`.
- Use `describe` for methods/classes. Use `.` for class methods, `#` for instance methods.
- Use `context` to describe different conditions, starting with 'when', 'with', 'if', 'unless', 'for', 'and', 'but', 'without', or 'otherwise'.

## Example Structure
- One expectation per example (`it`). If an example description contains 'and', it probably contains more than one expectation.
- When an example consists of multiple linked expectations, consider using `have_attributes` or a custom matcher.
- Keep example descriptions short and clear.
- Use `expect` syntax for assertions.
- For change assertions, prefer `expect { ... }.to change { ... }.from(...).to(...)` or `.to(...)`.
- Extract a `context` if an `it` description contains conditions: when, with, if, unless.

## Naming
- Contexts should describe conditions, forming readable sentences with nested blocks.
- Do not use 'should' in example descriptions. Use present tense, third person.
- Be explicit in `describe` blocks about what is being tested.

## Stubbing & Mocks
- Mock/stub only when necessary. Prefer real objects for integration tests.
- Always use `expect(...).to have_received(...)` to verify method calls on double/spy instead of `expect(...).to receive(...)`.
- Stub HTTP requests (e.g., with webmock) instead of hitting real services.
- Use `Timecop` for time, not stubbing `Time.now`.

ИИ и LLM: прогрессия в программировании

Вот, как мне кажется, из каких этапов состоит процесс внедрения ИИ и LLM в работу простого советского программиста. Пишу схематично, чтобы не забыть. Если интересно, напишите — разберу каждый этап отдельно со всеми нужными ссылками.

Мне кажется, всё, как в Железном Человеке. Начинаем с бестолковой Алисы, переходим к аугументации, костюму, который тебя делает круче, и наконец, к автономным костюмам-агентам (см. «Протокол семейный праздник» из ЖЧ3).

Прогрессия:
0. Отрицание, гнев, торг, депрессия, принятие. Да, как прежде уже не будет.

1. Время от времени захаживаешь в Дипсик или ЧатГПТ. Просишь что-то посоветовать, объяснить, написать за тебя функцию. Получаешь опыт, учишься формулировать рабочие промпты, снова и снова копипастишь кусочки кода туда-обратно.

2. Задалбываешься копипастить. Добавляешь автокомплит — ставишь GitHub Copilot или Курсор. Автокомплитишь табами, отдаешь LLM бойлерплейт.

3. Покупаешь Курсор. Начинаешь немножечко вайб-кодить через чат. Пишешь и воруешь рулы, делаешь мини-фичи с помощью LLM. Учишься формулировать требования к решению задачи, натыкаешься на ерунду, прокачиваешь рулы. Обжигаешься и понимаешь, что тесты не стоит всегда доверять LLM.

4. Переходишь на программирование агентом. Сетапишь ВПН, покупаешь Claude Code. Программируешь фичи и рефакторишь с помощью разговора с консолью. Делегируешь агенту всю работу с репозиторием от создания и чтению ишьюс до форс-пушей и создания ПРов.

5. Начинаешь использовать чат с LLM для постановки задач другой LLM. Роботы программируют друг друга, а ты таскаешь промпты туда-сюда.

6. Переходишь на флот агентов. Запускаешь несколько инстансов агентов в вебе в Курсоре или локально с помощью Claude Code и git worktree.

7. Прекрасное светлое будущее. События «Мстители: Финал», уходишь в плотники или пивовары.

Так вижу. Пожалуйста, не воспринимайте это как руководство к действию и обязательно проконсультируйтесь с безопасниками, чтобы не получить по жопе за код, слитый в сеть.

Новый LLM дев-цикл

Зафиксирую для истории. Сегодня ровно месяц, как я не могу работать в обычном режиме «сел за комп утром, встал вечером, зато без перерыва». Теперь работаю короткими интервалами по 20-25 минут с перерывами по 5 минут. Соответственно, поменялся и формат работы с LLM. Вот, что я поменял:

1. Написал системный промпт для Курсора, который просит LLM сначала сформулировать план решения задачи, утвердить его со мной, выполнить, а затем прогнать тесты и линтеры:

## Development cycle
1. Before writing any code, come up with a good plan, review the plan, and then ask the user for permission to execute the plan.
2. After you have executed the plan, run: `bin/rspec` and `bin/rubocop`
3. If there are any linting errors, run `bin/rubocop -A`
4. To run tests: `bin/rspec <path to file>`

Теперь я ставлю задачу LLM, проверяю и, если нужно, корректирую план, отправляю его на выполнение и иду отдыхать свои 5 минут.

2. Стал писать задачи на русском. Если я правильно понимаю, как работает LLM, то никаких проблем с этим быть не должно. car и «машина» в их векторном пространстве смыслов должны быть совсем рядом.

3. Стал писать задачи по формату. Открываю все нужные файлы, делаю /Add Open Files to Context. Кратко формулирую задачу в один абзац, а затем пишу список (буквально!) требований. Часть требований и технических деталей пишу прямо кодом:

...

С вот такими требованиями:
...
- определяет методы в мейлере, используя `define_method` и `@email_chain.steps`
...
- находит нужную цепочку и пользователя, опираясь на переданный mailing_list_subscription и АПИ EmailChain:
    def load_data
      @mailing_list_subscription = params[:mailing_list_subscription]
      @user = @mailing_list_subscription.user
      @email_chain = EmailChain.find_by(mailing_list_subscription: @mailing_list_subscription)
    end

4. Стал бить задачи так, чтобы диф изменений, сгенерированный LLM, был не больше 300-400 строк. Так их проще проверить и верифицировать. Если вайб-кожу, конечно, не смотрю на это. Пусть генерит сколько угодно, все равно на выброс.

5. Перестал доверять LLM в написании тестов. LLM — мастера находить самые простые и рабочие пути. Они очень часто пишут тесты, которые либо тестируют ненужное, либо тестируют только «золотой путь», игнорируя все краевые случаи. Ощущение, будто LLM пишут тесты, чтобы они проходили с первого раза, а не для того, чтобы найти побольше багов и задокументировать АПИ.

Вы используете Курсор неправильно

На прошлой неделе смотрел, как другие ребята пользуются Курсором и ИИ в нем. Заметил, что не все понимают, как получать от него максимум пользы.

Во-первых, первое, что нужно сделать — включить YOLO, он же Enable auto-run mode. Эта фича разрешит агенту запускать код локально. Соответственно, можно будет делать крутейшие штуки:

Запусти эти тесты и поправь тестируемый класс так, чтобы тесты проходили

Напиши тесты для функции foo, которая ... Затем напиши функцию foo и исправь ее, если потребуется, так, чтобы тесты проходили

Во-вторых, в повторяющихся задачах необязательно описывать нужные изменения: переименуй так, сконвертируй в класс, перенеси в метод X, удали старый файл. Можно сделать один раз самому и использовать изменения в качестве примера:

Взгляни на мои изменения в модуле видео для Кинескопа и повтори эти изменения для модулей видео в Вимео, ВК и Ютюбе

Аналогично с помощью примеров можно подсказать, как должна работать новая функция:

Напиши функцию на JS, которая группирует свойства по ключам во вложенные объекты. Покажу на примерах:
{ vkSrc: 'http://vk.com/123' } превращается в { vk: { src: 'http://vk.com/123' } }
{ vimeoId: '123', kinescopeSrc: 'http://ki.io' } превращается в { vimeo: { id: '123' }, kinescope: { src: 'http://ki.io' } }

В-третьих, чат можно использовать как справочник. Выделили нужный фрагмент кода, например, resolitions в package.json и попросили объяснить, как это работает.

В-четвертых, во вкладке Source Control в поле с текстом коммита есть магическая кнопка. Нажмите на нее, чтобы ИИ сгенерировал нормальный коммит-месседж на основе ваших (или не совсем ваших) изменений.

В-пятых, используйте правила для Курсора. Как минимум, возьмите хороший системный промпт:
https://cursor.directory/

Как максимум, понапишите собственных правил для проекта:
https://ghuntley.com/stdlib/

Короче, если вы используете в Курсоре только автокомплит, присмотритесь к YOLO auto-run mode и .cursorrules. И считайте чат не хитрым интерфейсом к Гуглу, а диалогом со стажером, с которым вы программируете в паре.

Эмодзи для работы с замечаниями

С уходом Дискорда и непреднамеренным старением я стал записывать замечания со встречи прямо в тот же чат в Телеграме. Так как замечаний много, отвечать на каждое из них — безумство. Любого выбесит 20-30 сообщений «Увидел», «Поправил», «Записал». Вместо этого использую реакции на сообщения с замечаниями. Принцип такой:
👌 — поправил, сделал, починил
✍️ — записал, в бэклог, разберусь позже
👀 — увидел
🤔 — че-т подозрительно, проверим
👍 — супер, одобряю, нормас
🔥 — клево

Если в заметках со встречи все в эмодзи, значит, я все разобрал. Если у сообщения реакции нет, значит, пропустил. Изи, ничего не теряется.

Легче починить, чем извиниться

Тут DHH написал пост, да такой, что ПРЯМО В ЧУВСТВА. В нем пара клевых моментов.

Во-первых, Джейсон Фрид на заре Бейскемпа отвечал на 150 писем клиентов в день. Еще раз: сто пятьдесят писем клиентов в день. А мы инбокс разгрести не можем.

Во-вторых, политика «Все работают в техподдержке» помогает фиксить мелкие баги-заусенцы, до которых никогда не доходят руки. А все потому, что программистам проще починить проблему, чем извиниться за то, что она там есть. Думаю, это еще и для эго менее болезненно.

Пост целиком:
https://world.hey.com/dhh/stick-with-the-customer-4942402f (нужен ВПН)

Архив

* — любимое
2025 Отличать дизмораль от трения
2024 Оставляйте артефакты
Не используйте subject для испытаний
Уверовал в AI
Пострадайте вместе
it "calls Foo#bar" — моветон
Один «бжж» или два «бжж»
Гипотеза: мы охереваем, потому что слишком легко
Сформулируй просьбу или вопрос
Дефолтный box-sizing в ЦСС. Спасибо, ИЕ!
Ищу сообразительного стажёра
Стыдиться старого кода — ок
2023 3д-печать и ФФФ
Баготерапия
Дефолтные стили браузеров
Просто не делай херни
Датчик есть, но его как бы нет
Большая маленькая ложь
Карма тестировщика
irb: echo on assignment
Как тестировать код, завязанный на рандом
Лайфхак: процедура завершения работы
RSpec: to be_within().of()
SPA на Реакте без Вебпака
RSpec: skip и pending
2022 SSRF
Range в ActiveRecord
Не надо везде лепить восклицательные знаки!
Планирование преемственности
yakuake в iTerm
Мониторинг ключевых метрик приложения на Рельсах в Графане
Лайфхак: буферы для совещаний
Проблема в занятости, а не в лени
before + after → around
Лайфхак: талоны за дипворк
inbox.txt
RSpec & Rails: как проверить deliver_later(wait_until: ...)
Стабы и моки юникс-сокетов
А у нас Faker
Фейковые данные в тестах
Как тестировать код, работающий с внешним АПИ. Заглушка на Синатре
На каких задачах сфокусироваться
Тормоза mc на m1, zsh и nvm
Как читать больше в эпоху цифрового максимализма
2021 Менеджерский лайфхак: 1:1 в Телеграме
Задавать вопросы, а не перебирать варианты
Ищу толкового стажера на Рельсах
Достроить, чтобы обойти
Заглушка из Rack-приложения в одну строчку
Лайфхак: поработать в машине
Линтим Руби в Виме
Курс о профессиональном росте. Как расти самому, как растить команду
Сервисы головного мозга
«Что» и «Чтобы что» в пулреквестах
Где проходит грань в изолированности объекта тестирования?
2020 Только то, что важно для проверки
Меньше — лучше
Пора валить?
Когда использовать double, а не instance_double?
RSpec: before и after хуки
Как застабить переменные окружения в RSpec
Что не нужно писать в it
RuboCop, RSpec и стайлгад
Как протестировать конфиг whenever
Один тест — одна логическая проверка
Обратная связь в тестах, guard и vim-test
Не проверяйте отдельно ключи
Не тестируйте attr_reader/_writer/_accessor
Ищу сообразительного стажёра
2019 Только то, что влияет на проверку
Какой тест упадет, если удалим этот кусок кода?
Как отлаживать странное: в браузере на реальном Айфоне через Скайп, Зум или Хэнгаут
let и before
Впихнуть все в expect
Сэнди Метц
2018 Конспект POODR. Reducing Costs with Duck Typing
Конспект POODR. Creating Flexible Interfaces
Конспект POODR. Managing Dependencies
Конспект POODR. Designing Classes with a Single Responsibility
Конспект POODR. Object-Oriented Design
2017 Ищу толкового стажёра
Что почитать начинающему тимлиду
Советы
2016 Сравнение CI-сервисов
Как тестировать коллбэки ActiveRecord моделей
Гигиена в тестах
Анти-паттерны в тестах
Проблемы с subject
По хардкору: describe и context
Стоит почитать
Ищу стажеров (уже нашел)
По хардкору: что писать в комментарии к коммиту
Дезодоранты и полезные комментарии
По хардкору: instance_double, verify_partial_doubles
По хардкору: дублеры, моки, стабы
Стоп-комментарии
Сегодня я узнал
Три правила ревью
2015 Управление временем
Подростковое тестирование. Подборка
Хороший доклад
Stylus → PostCSS
CSS инбоксы
К Оптимусу Прайму. Группировка CSS свойств
Объясни это
К Оптимусу Прайму. Сортировка CSS свойств по алфавиту
2014 Два способа признаться в ненависти к человечеству
Реквием по ИЕ
Былинное программирование
Иисус и псевдоэлементы
Удаленные мудаки
Пусть страдают роботы
Как пользоваться успехом у женщин с помощью CSS
Воинствующие шаблонисты
Нет, это дизайнерский таск
Синдром бойлерной
Рабочие столы
Союз скобок