Туториал по Kanban

Разбираем четыре сценария, ради которых задумывалась система Kanban в Hermes, держа дашборд открытым в браузере. Если вы ещё не читали обзор Kanban — начните с него: здесь предполагается, что вы уже знаете, что такое задача, прогон (run), исполнитель и диспетчер.

Подготовка

hermes kanban init           # optional; first `hermes kanban <anything>` auto-inits
hermes dashboard             # opens http://127.0.0.1:9119 in your browser
# click Kanban in the left nav

Дашборд — самое удобное место, чтобы вам наблюдать за системой. Агенты-воркеры, которых порождает диспетчер, не видят ни дашборда, ни CLI: они управляют доской через специальный набор инструментов kanban_* (kanban_show, kanban_list, kanban_complete, kanban_block, kanban_heartbeat, kanban_comment, kanban_create, kanban_link, kanban_unblock). Все три точки входа — дашборд, CLI, инструменты воркеров — ходят в одну и ту же SQLite-базу для каждой доски (~/.hermes/kanban.db для доски по умолчанию, ~/.hermes/kanban/boards/<slug>/kanban.db для любой доски, которую вы создадите позже). Поэтому состояние доски остаётся согласованным, с какой бы стороны изменение ни пришло.

Весь туториал работает с доской default. Если вам нужно несколько изолированных очередей (по одной на проект / репозиторий / домен), загляните в раздел Доски (мультипроект) обзора — там те же потоки CLI / дашборда / воркеров применяются к каждой доске отдельно, а воркеры физически не способны видеть задачи на чужих досках.

По всему туториалу действует простое правило: блоки кода с меткой bash — это команды, которые запускаете вы. Блоки с меткой # worker tool calls — это вызовы инструментов, которые выдаёт модель порождённого воркера. Они приведены, чтобы вы видели цикл агента целиком, а не для того, чтобы вы их сами набирали.

Доска с высоты птичьего полёта

Обзор доски Kanban

Шесть колонок слева направо:

  • Triage — сырые идеи. По умолчанию диспетчер автоматически запускает на здешних задачах декомпозер: встроенный декомпозер использует auxiliary.kanban_decomposer, читает ваш набор профилей и описания и строит граф дочерних задач, разведённых по наиболее подходящим специалистам. Исходная задача остаётся живой как родитель, чтобы её исполнитель (kanban.orchestrator_profile, либо активный профиль по умолчанию, если параметр не задан) снова проснулся и оценил завершение, когда всё доделается. Чтобы переключить режим, щёлкните по пилюле Orchestration: Auto/Manual в верхней части страницы kanban. В ручном режиме нажмите ⚗ Decompose на карточке либо выполните hermes kanban decompose <id> / /kanban decompose <id>. Для одиночных задач, которым fan-out не нужен, кнопка ✨ Specify делает однократную переписку спецификации (цель, подход, критерии приёмки) и продвигает задачу в todo. Модели для этого настраиваются через auxiliary.kanban_decomposer и auxiliary.triage_specifier в config.yaml. См. Авто- против ручной оркестрации в основном руководстве по Kanban.
  • Todo — задача создана, но ждёт зависимостей или ещё не назначена.
  • Ready — назначена и ждёт, пока диспетчер её заберёт.
  • In progress — воркер активно выполняет задачу. С включёнными «Lanes by profile» (по умолчанию) колонка группируется по исполнителю, и вы с одного взгляда видите, кто чем занят.
  • Blocked — воркер запросил вмешательство человека либо сработал предохранитель (circuit breaker).
  • Done — выполнено.

В верхней панели — фильтры по поиску, тенанту и исполнителю, плюс переключатель Lanes by profile и кнопка Nudge dispatcher, которая прокручивает один такт диспетчера прямо сейчас, не дожидаясь следующего интервала демона. Клик по любой карточке открывает её ящик (drawer) справа.

Плоский вид

Если дорожки профилей рябят в глазах, отключите «Lanes by profile» — и колонка In Progress схлопнется в единый плоский список, упорядоченный по времени захвата:

Доска с отключёнными дорожками профилей

История 1 — Один разработчик выкатывает фичу

Вы пилите фичу. Классический поток: спроектировать схему, реализовать API, написать тесты. Три задачи с зависимостями родитель→потомок.

SCHEMA=$(hermes kanban create "Design auth schema" \
    --assignee backend-dev --tenant auth-project --priority 2 \
    --body "Design the user/session/token schema for the auth module." \
    --json | jq -r .id)

API=$(hermes kanban create "Implement auth API endpoints" \
    --assignee backend-dev --tenant auth-project --priority 2 \
    --parent $SCHEMA \
    --body "POST /register, POST /login, POST /refresh, POST /logout." \
    --json | jq -r .id)

hermes kanban create "Write auth integration tests" \
    --assignee qa-dev --tenant auth-project --priority 2 \
    --parent $API \
    --body "Cover happy path, wrong password, expired token, concurrent refresh."

Поскольку родитель API — это SCHEMA, а родитель tests — это API, в ready стартует только SCHEMA. Остальные две лежат в todo, пока их родители не завершатся. Так работает движок продвижения зависимостей — ни один воркер не возьмётся за написание тестов, пока нет API, который можно тестировать.

На следующем такте диспетчера (по умолчанию 60 секунд, либо сразу, если нажать Nudge dispatcher) профиль backend-dev порождается как воркер с HERMES_KANBAN_TASK=$SCHEMA в окружении. Вот как выглядит цикл вызовов инструментов изнутри агента:

# worker tool calls — NOT commands you run
kanban_show()
# → возвращает title, body, worker_context, parents, прошлые попытки, комментарии

# (воркер читает worker_context, проектирует схему через терминал/файловые
#  инструменты, пишет миграции, гоняет собственные проверки, коммитит —
#  реальная работа происходит здесь)

kanban_heartbeat(note="schema drafted, writing migrations now")

kanban_complete(
    summary="users(id, email, pw_hash), sessions(id, user_id, jti, expires_at); "
            "refresh tokens stored as sessions with type='refresh'",
    metadata={
        "changed_files": ["migrations/001_users.sql", "migrations/002_sessions.sql"],
        "decisions": ["bcrypt for hashing", "JWT for session tokens",
                      "7-day refresh, 15-min access"],
    },
)

У kanban_show параметр task_id по умолчанию равен $HERMES_KANBAN_TASK, так что воркеру не нужно знать собственный id. kanban_complete записывает summary и metadata в текущую строку task_runs, закрывает этот прогон и переводит задачу в done — всё одним атомарным шагом через kanban_db.

Как только SCHEMA доходит до done, движок зависимостей автоматически продвигает API в ready. Воркер API, когда возьмётся за дело, вызовет kanban_show() и увидит summary и metadata от SCHEMA, приложенные к родительской передаче. Так он узнаёт решения по схеме, не перечитывая длинный проектный документ.

Кликните по завершённой задаче со схемой — и ящик покажет всё:

Один разработчик — ящик завершённой задачи со схемой

Главное добавление — секция Run History внизу. Одна попытка: исход completed, воркер @backend-dev, длительность, метка времени и полный текст summary передачи. Блок metadata (changed_files, decisions) тоже хранится на прогоне и попадает к любому нижестоящему воркеру, который читает этого родителя.

Те же данные можно посмотреть из терминала в любой момент — эти команды вы подглядываете за доской, а не воркер:

hermes kanban show $SCHEMA
hermes kanban runs $SCHEMA
# #  OUTCOME       PROFILE       ELAPSED  STARTED
# 1  completed     backend-dev        0s  2026-04-27 19:34
#     → users(id, email, pw_hash), sessions(id, user_id, jti, expires_at); refresh tokens ...

История 2 — Конвейер из множества воркеров

У вас три воркера (переводчик, транскрибатор, копирайтер) и куча независимых задач. Хочется, чтобы все трое тянули работу параллельно и давали видимый прогресс. Это простейший сценарий kanban — и тот, под который исходно затачивался дизайн.

Создаём работу:

for lang in Spanish French German; do
    hermes kanban create "Translate homepage to $lang" \
        --assignee translator --tenant content-ops
done
for i in 1 2 3 4 5; do
    hermes kanban create "Transcribe Q3 customer call #$i" \
        --assignee transcriber --tenant content-ops
done
for sku in 1001 1002 1003 1004; do
    hermes kanban create "Generate product description: SKU-$sku" \
        --assignee copywriter --tenant content-ops
done

Запустите шлюз и идите заниматься своими делами — он держит встроенный диспетчер, который разбирает задачи всех трёх профилей-специалистов на одной и той же kanban.db:

hermes gateway start

Теперь отфильтруйте доску по content-ops (или просто поищите «Transcribe») — и получите вот что:

Вид конвейера с фильтром по задачам транскрибации

Две транскрибации готовы, одна выполняется, две в ready ждут следующего такта диспетчера. Колонка In Progress сгруппирована по профилю (поведение «Lanes by profile» по умолчанию), так что активная задача каждого воркера видна без вычитывания смешанного списка. Диспетчер продвинет следующую готовую задачу в работу, как только завершится текущая. Три демона трудятся над тремя пулами исполнителей параллельно — и вся очередь контента вычерпывается без дальнейшего участия человека.

Всё, что в Истории 1 говорилось про структурированную передачу, действует и здесь. Воркер-переводчик, завершая звонок, выдаёт kanban_complete(summary="translated 4 pages, style matched existing marketing voice", metadata={"duration_seconds": 720, "tokens_used": 2100}) — это полезно и для аналитики, и для любой нижестоящей задачи, которая от этой зависит.

История 3 — Ролевой конвейер с повторной попыткой

Вот где Kanban оправдывает себя по сравнению с плоским списком TODO. PM пишет спецификацию. Инженер её реализует. Ревьюер отклоняет первую попытку. Инженер пробует снова, с правками. Ревьюер одобряет.

Вид дашборда с фильтром по auth-project:

Вид конвейера для многоролевой фичи

Трёхэтапная цепочка видна целиком: Spec: password reset flow (DONE, pm), Implement password reset flow (DONE, backend-dev), Review password reset PR (READY, reviewer). У каждой внизу зелёным виден родитель, а потомки — как зависимости.

Самая интересная здесь — задача реализации, потому что она была заблокирована и повторена. Вот вся хореография трёх агентов, показанная как вызовы инструментов, которые делает модель каждого воркера:

# --- PM worker spawns on $SPEC and writes the acceptance criteria ---
# worker tool calls
kanban_show()
kanban_complete(
    summary="spec approved; POST /forgot-password sends email, "
            "GET /reset/:token renders form, POST /reset applies new password",
    metadata={"acceptance": [
        "expired token returns 410",
        "reused last-3 password returns 400 with message",
        "successful reset invalidates all active sessions",
    ]},
)
# → $SPEC выполнена; $IMPL автоматически продвигается из todo в ready

# --- Engineer worker spawns on $IMPL (first attempt) ---
# worker tool calls
kanban_show()   # читает summary $SPEC + метаданные acceptance в worker_context
# (инженер пишет код, гоняет тесты, открывает PR)
# Приходит фидбэк ревью — инженер решает, что замечания обоснованы, и блокируется
kanban_block(
    reason="Review: password strength check missing, reset link isn't "
           "single-use (can be replayed within 30min)",
)
# → $IMPL переходит в blocked; прогон 1 закрывается с исходом 'blocked'

Теперь вы (человек или отдельный профиль-ревьюер) читаете причину блокировки, понимаете, что направление фикса ясно, и снимаете блокировку кнопкой «Unblock» в дашборде — либо из CLI / слеш-командой:

hermes kanban unblock $IMPL
# or from a chat: /kanban unblock $IMPL

Диспетчер продвигает $IMPL обратно в ready и на следующем такте заново порождает воркер backend-dev. Этот второй запуск — новый прогон по той же задаче:

# --- Engineer worker spawns on $IMPL (second attempt) ---
# worker tool calls
kanban_show()
# → worker_context теперь содержит причину блокировки из прогона 1, поэтому воркер
#   знает, какие две вещи поправить, вместо перечитывания всей спецификации
# (инженер добавляет проверку zxcvbn, делает токены сброса одноразовыми, перезапускает тесты)
kanban_complete(
    summary="added zxcvbn strength check, reset tokens are now single-use "
            "(stored + deleted on success)",
    metadata={
        "changed_files": [
            "auth/reset.py",
            "auth/tests/test_reset.py",
            "migrations/003_single_use_reset_tokens.sql",
        ],
        "tests_run": 11,
        "review_iteration": 2,
    },
)

Кликните по задаче реализации. Ящик показывает две попытки:

Задача реализации с двумя прогонами — сначала заблокирована, потом завершена

  • Прогон 1blocked от @backend-dev. Фидбэк ревью лежит прямо под исходом: «password strength check missing, reset link isn’t single-use (can be replayed within 30min)».
  • Прогон 2completed от @backend-dev. Свежее summary, свежие metadata.

Каждый прогон — это строка в task_runs со своим исходом, summary и metadata. История повторов — не концептуальная надстройка поверх задачи с «последним состоянием», а основное представление данных. Когда воркер берётся за повтор и открывает задачу, build_worker_context показывает ему прошлые попытки, и воркер второго прохода видит, почему первый проход заблокировали, и закрывает именно эти находки, а не запускается с нуля.

Дальше за дело берётся ревьюер. Открыв Review password reset PR, он видит:

Ящик ревьюера в конвейере

Родительская ссылка — это завершённая реализация. Когда воркер ревьюера порождается на Review password reset PR и вызывает kanban_show(), возвращённый worker_context включает summary и metadata самого свежего завершённого прогона родителя. Так ревьюер читает «added zxcvbn strength check, reset tokens are now single-use» и держит список изменённых файлов под рукой ещё до взгляда на диф.

История 4 — Предохранитель и восстановление после краха

Реальные воркеры падают. Отсутствующие учётные данные, OOM-киллы, разовые сетевые сбои. У диспетчера две линии обороны: предохранитель (circuit breaker), который автоблокирует задачу после N подряд идущих сбоев, чтобы доска не молотила вхолостую вечно, и детектор краха, который возвращает задачу, чей воркер пропал по PID раньше истечения её TTL.

Предохранитель — сбой, похожий на постоянный

Задача деплоя, которая не может породить воркер, потому что в окружении профиля не задан AWS_ACCESS_KEY_ID:

hermes kanban create "Deploy to staging (missing creds)" \
    --assignee deploy-bot --tenant ops \
    --max-retries 3

Диспетчер пытается породить воркер. Порождение падает (RuntimeError: AWS_ACCESS_KEY_ID not set). Диспетчер снимает захват, увеличивает счётчик сбоев и пробует снова на следующем такте. Поскольку в этом примере задан --max-retries 3, предохранитель срабатывает после трёх подряд идущих сбоев: задача уходит в blocked с исходом gave_up. Если опустить флаг, Hermes использует kanban.failure_limit (по умолчанию: 2). Дальнейших повторов не будет, пока человек не снимет блокировку.

Кликните по заблокированной задаче:

Предохранитель — 2 spawn_failed + 1 gave_up

Три прогона, у всех одинаковая ошибка в поле error. Первые два — spawn_failed (повторяемые), третий — gave_up (терминальный). Журнал событий выше показывает всю последовательность: created → claimed → spawn_failed → claimed → spawn_failed → claimed → gave_up.

В терминале:

hermes kanban runs t_ef5d
# #   OUTCOME        PROFILE        ELAPSED  STARTED
# 1   spawn_failed   deploy-bot          0s  2026-04-27 19:34
#       ! AWS_ACCESS_KEY_ID not set in deploy-bot env
# 2   spawn_failed   deploy-bot          0s  2026-04-27 19:34
#       ! AWS_ACCESS_KEY_ID not set in deploy-bot env
# 3   gave_up        deploy-bot          0s  2026-04-27 19:34
#       ! AWS_ACCESS_KEY_ID not set in deploy-bot env

Если подключён Telegram / Discord / Slack, на событие gave_up срабатывает уведомление шлюза — и вы узнаёте об аварии, не заглядывая в доску.

Восстановление после краха — воркер умирает на лету

Иногда порождение проходит успешно, но процесс воркера умирает позже — segfault, OOM, systemctl stop. Диспетчер опрашивает kill(pid, 0), обнаруживает мёртвый pid; захват снимается, задача возвращается в ready, и следующий такт отдаёт её свежему воркеру.

Пример в начальных данных (seed data) — миграция, у которой кончалась память:

# Worker claims, starts scanning 2.4M rows, OOM kills it at ~2.3M
# Dispatcher detects dead pid, releases claim, increments attempt counter
# Retry with a chunked strategy succeeds

Ящик показывает полную историю из двух попыток:

Крах и восстановление — 1 crashed + 1 completed

Прогон 1 — crashed, с ошибкой OOM kill at row 2.3M (process 99999 gone). Прогон 2 — completed, с "strategy": "chunked with LIMIT + WHERE id > last_id" в metadata. Воркер-повторщик увидел в своём контексте крах прогона 1 и выбрал более безопасную стратегию; благодаря metadata будущему наблюдателю (или автору постмортема) очевидно, что именно изменилось.

Структурированная передача — зачем нужны summary и metadata

В каждой истории выше воркеры в конце вызывали kanban_complete(summary=..., metadata=...). Это не украшение, а основной канал передачи между этапами рабочего процесса.

Когда воркер на задаче B порождается и вызывает kanban_show(), в полученном worker_context содержится:

  • Прошлые попытки задачи B (предыдущие прогоны: исход, summary, error, metadata), чтобы воркер-повторщик не пошёл по уже провалившемуся пути.
  • Результаты родительских задач — для каждого родителя summary и metadata самого свежего завершённого прогона — чтобы нижестоящие воркеры видели, зачем и как делалась вышестоящая работа.

Это снимает танец «копайся в комментариях и выводе работы», который мучает плоские kanban-системы. PM пишет критерии приёмки в metadata спецификации — и воркер инженера видит их структурно в родительской передаче. Инженер фиксирует, какие тесты он гонял и сколько прошло, — и у воркера ревьюера этот список под рукой ещё до открытия дифа.

Защита от массового закрытия существует именно потому, что эти данные привязаны к прогону. hermes kanban complete a b c --summary X (вы, из CLI) отклоняется — копипастить одно и то же summary в три задачи почти всегда неправильно. Массовое закрытие без флагов передачи по-прежнему работает для типичного случая «я доделал кучу административных мелочей». В наборе инструментов массового варианта нет вовсе: kanban_complete всегда работает с одной задачей за раз по той же причине.

Осмотр задачи, которая выполняется прямо сейчас

Для полноты картины — вот ящик задачи в полёте (реализация API из Истории 1, захваченная backend-dev, но ещё не завершённая):

Захваченная задача в полёте

Статус — Running. Активный прогон отображается в секции Run History с исходом active и без ended_at. Если этот воркер умрёт или выйдет за таймаут, диспетчер закроет прогон с подходящим исходом и откроет новый при следующем захвате — строка попытки никогда не исчезает.

Дальнейшие шаги

  • Обзор Kanban — полная модель данных, словарь событий и справочник по CLI.
  • hermes kanban --help — каждая подкоманда, каждый флаг.
  • hermes kanban watch --kinds completed,gave_up,timed_out — живой поток терминальных событий по всей доске в терминале.
  • hermes kanban notify-subscribe <task> --platform telegram --chat-id <id> — получить пинг от шлюза, когда конкретная задача завершится.
ESC