Kanban — совместная работа профилей-агентов

Нужен пошаговый разбор? Прочитайте руководство по Kanban — четыре пользовательских сценария (соло-разработчик, «фермерство» на флоте агентов, конвейер ролей с повтором, автоматический предохранитель) со скриншотами дашборда для каждого. Эта страница — справочник, а руководство — рассказ с примерами.

Kanban в Hermes — это устойчивая доска задач, общая для всех ваших профилей Hermes, которая позволяет нескольким именованным агентам работать сообща без хрупких внутрипроцессных роёв субагентов. Каждая задача — строка в ~/.hermes/kanban.db; каждая передача работы — строка, которую любой может прочитать и записать; каждый исполнитель — полноценный процесс ОС со своей идентичностью.

Чекпоинты завершения перед потолком итераций

Исполнители, которыми владеет диспетчер, получают одно уведомление-чекпоинт около 90% своего конечного бюджета итераций — оно прикрепляется к свежему результату инструмента, пока ещё остаётся вызов с доступными инструментами. Порог можно сдвинуть раньше через agent.budget_warning_ratio. Крошечные бюджеты предупреждают не позже предпоследней итерации, а у прогона на одну итерацию окна для чекпоинта до потолка нет вовсе. Уведомление сохраняется в транскрипт сессии до следующего запроса. Исполнителю стоит вызывать kanban_complete только после проверки контракта задачи — либо оставить комментарий о прогрессе и продолжать. Один коммит или диф задачу автоматически не завершают никогда.

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

Два интерфейса: модель говорит через инструменты, вы — через CLI

У доски два входа, оба опираются на один и тот же ~/.hermes/kanban.db:

  • Агенты управляют доской через выделенный набор инструментов kanban_* — kanban_show, kanban_list, kanban_complete, kanban_request_review, kanban_request_changes, kanban_block, kanban_heartbeat, kanban_comment, kanban_attach, kanban_attach_url, kanban_attachments, kanban_create, kanban_link, kanban_unblock. Диспетчер запускает каждого исполнителя уже с этими инструментами в схеме; профили-оркестраторы также могут включить набор инструментов kanban явно. Модель читает и маршрутизирует задачи, вызывая инструменты напрямую, а не обращаясь к hermes kanban через shell. См. раздел Как исполнители взаимодействуют с доской ниже.
  • Вы (а также скрипты и cron) управляете доской через hermes kanban … в CLI, через слэш-команду /kanban … или через дашборд. Это интерфейсы для людей и автоматизации — там, где за спиной нет модели, вызывающей инструменты.

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

Именно такая форма покрывает нагрузки, с которыми не справляется delegate_task:

  • Исследовательская триаж-работа — параллельные исследователи + аналитик + автор текста, с участием человека.
  • Плановые операции — повторяющиеся ежедневные сводки, которые неделями складываются в единый журнал.
  • Цифровые двойники — постоянные именованные ассистенты (inbox-triage, ops-review), накапливающие память со временем.
  • Инженерные конвейеры — декомпозиция → параллельная реализация в разных worktree → ревью → доработка → PR.
  • Работа с флотом — один специалист управляет N объектами (50 соцаккаунтов, 12 отслеживаемых сервисов).

Восемь канонических паттернов совместной работы собраны ниже, в разделе Паттерны совместной работы.

Контракты завершения по PR

Работу над PR объявляйте при создании: --completion-contract OWNER/REPO (или точный URL https://github.com/OWNER/REPO/pull/123 для уже существующей работы). Тот же completion_contract принимает kanban_create. Для намеренно локальной работы есть local-only; существующие и необъявленные карточки остаются на этом значении. URL в прозе политикой не является.

После публикации передайте в завершение metadata.published_pr. Первый подошедший URL привязывается к карточке навсегда: подменить его на зелёный соседний PR при повторе не удастся. Сохранённый контракт показывают CLI show --json и kanban_show.

Общая граница complete_task покрывает инструменты исполнителя, CLI, одобрение на ревью и завершение из дашборда. Она читает и классическую защиту ветки, и обязательные контексты активных рулсетов, страницами обходит проверки и старые статусы ровно по head, а затем перечитывает head и base у PR. Необязательная телеметрия о провалах и пропусках уже принятые обязательные проверки не отменяет. Отсутствующие, ожидающие, провалившиеся, отменённые, истёкшие по таймауту, устаревшие, пропущенные и нейтральные обязательные свидетельства завершить карточку не дают. Как не дают этого приём при нулевом числе прогонов, нечитаемая политика и сбои API GitHub. Репозиторию без обязательных проверок нужен контракт local-only. У gh должна быть аутентификация с доступом на чтение проверок и правил репозитория; сам этот гейт ничего наружу не пишет. Проверки приёмки выполняются под логином gh профиля исполнителя: берутся его GH_TOKEN / GH_CONFIG_DIR из собственного .env профиля, а не текущий логин процесса, который завершает карточку. На хостах с несколькими профилями (по одной учётной записи GitHub на организацию) входите в gh отдельно для каждого профиля (GH_CONFIG_DIR в .env этого профиля). Токен должен лежать в .env профиля (или в настроенном источнике секретов): GH_TOKEN, просто экспортированный в оболочке или в unit-файле systemd, вычищается из дочернего процесса gh и обратно не добавляется. Если у профиля исполнителя нет собственного логина gh (или самого профиля больше нет), приёмка отклоняется с not logged in, а не проваливается на ~/.config/gh; карточки без исполнителя по-прежнему используют текущий логин. Логин, который не видит репозиторий, отклоняется с classification=auth: в сообщении названы профиль и репозиторий, а сбоем инфраструктуры, который можно повторить, это не считается.

Отказ сохраняет карточку активной вместе с рабочим пространством. Устойчивые события pr_acceptance хранят URL PR, SHA, обязательные контексты, ID и адреса проверок, классификации и инструкции по восстановлению; следующий шаг подсказывает last_failure_error. Почините провалы, перезапустите инфраструктурные проверки или подождите и повторите завершение. Если нужно участие человека, берите kanban_block. Общий failure от GitHub не позволяет понять, упал тест или выгрузка артефакта, — смотрите сохранённый URL. Явные инфраструктурные заключения и сбои API классифицируются отдельно. Лишнего исполнителя никто не порождает.

Сохранение квитанции и терминальная запись перепроверяют владение прогоном, статусом и контрактом под одной блокировкой SQLite: перехваченный исполнитель уже не завершит новый прогон и не прикрепит к нему приёмку. Последнее чтение из GitHub — это снимок на момент завершения, а не распределённая транзакция и не постоянный присмотр после завершения. Это предохранитель жизненного цикла для одного пользователя, а не изоляция на уровне ОС от произвольных прямых записей в базу. GitHub Enterprise не покрывается. Смежные работы по публикации и жизненному циклу: #91230, #84254, #52311; локальная проверка и сама публикация удалённой приёмкой не являются.

Kanban против delegate_task

Они выглядят похоже, но это не один и тот же примитив.

delegate_taskKanban
ФормаRPC-вызов (fork → join)Устойчивая очередь сообщений + конечный автомат
РодительБлокируется до возврата дочернего вызова«Выстрелил и забыл» после create
Идентичность потомкаАнонимный субагентИменованный профиль с постоянной памятью
ВозобновляемостьНет — провал есть провалБлокировка → разблокировка → повтор; сбой → повторный захват
Участие человекаНе поддерживаетсяКомментарий / разблокировка в любой момент
Агентов на задачуОдин вызов = один субагентN агентов за время жизни задачи (повтор, ревью, доработка)
Аудиторский следТеряется при сжатии контекстаУстойчивые строки в SQLite навсегда
КоординацияИерархическая (вызывающий → вызываемый)Одноранговая — любой профиль читает/пишет любую задачу

Различие в одном предложении: delegate_task — это вызов функции; Kanban — очередь работы, где каждая передача — строка, которую любой профиль (или человек) может увидеть и отредактировать.

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

Воркер, застрявший на t_parent и создавший вспомогательную карточку для недостающего куска, не должен вызывать kanban_link(t_parent, t_support): связь делает вспомогательную карточку потомком заблокированного родителя, то есть она оказывается заперта за тем самым родителем, который должна разблокировать, — и ни одна из карточек никогда не запустится. Вместо этого укажите id родителя в теле вспомогательной карточки. link/kanban_link сообщают gated: true и записывают событие dependency_wait, когда понижают потомка из ready (то же делает kanban_create с parents, когда паркует новую карточку), так что тупик виден на доске; hermes kanban unlink <parent> <child> его снимает.

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

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

Они не исключают друг друга: воркер kanban может внутри своего запуска вызвать delegate_task.

Основные понятия

  • Доска (board) — самостоятельная очередь задач со своей SQLite-базой, каталогом рабочих пространств и циклом диспетчера. На одной установке может быть несколько досок (например, по одной на проект, репозиторий или домен); см. Доски (мультипроектность) ниже. Пользователи с одним потоком работы остаются на доске default и никогда не встречают слово «доска» за пределами этого раздела документации.
  • Задача (task) — строка с заголовком, опциональным телом, одним исполнителем (именем профиля), статусом (triage | todo | ready | running | blocked | review | done | archived), опциональным пространством имён арендатора (tenant) и опциональным ключом идемпотентности (дедупликация для повторяемой автоматизации).
  • Связь (link) — строка task_links, фиксирующая зависимость родитель → потомок. Диспетчер переводит todo → ready, когда все родители получили статус done. Добавить связь к уже выполняющемуся потомку нельзя: она не может придержать работу, которую уже захватили. Единственное исключение — активный воркер, который связывает собственную карточку, имея выданное диспетчером владение запуском, непосредственно перед передачей с блокировкой по зависимости.
  • Комментарий — протокол взаимодействия между агентами. Агенты и люди добавляют комментарии; когда воркер (пере)запускается, он читает всю ветку комментариев как часть своего контекста.
  • Рабочее пространство (workspace) — каталог, в котором работает воркер. Три вида:
    • scratch (по умолчанию) — новый временный каталог под ~/.hermes/kanban/workspaces/<id>/ (или ~/.hermes/kanban/boards/<slug>/workspaces/<id>/ для не-дефолтных досок). Удаляется при завершении задачи — scratch по своей природе эфемерен. Файлы, явно объявленные через kanban_complete(artifacts=[...]) или kanban_request_review(artifacts=[...]), копируются в постоянное хранилище вложений задачи перед очисткой (передача на ревью откладывает их в момент передачи, ведь scratch-пространство удалит уже последующее завершение ревьюером); существующие пути к результатам в устаревших сводках завершения получают такую же обработку. Остальные scratch-файлы удаляются. Отсутствующий заявленный scratch-артефакт оставляет задачу «в работе», чтобы воркер мог исправить путь и повторить попытку. Используйте worktree: или dir:<path>, когда всё рабочее пространство должно остаться доступным. При первом создании scratch-пространства на установке диспетчер пишет предупреждение и добавляет к задаче событие tip_scratch_workspace (видно через hermes kanban show <id>).
    • dir:<path> — существующий общий каталог (хранилище Obsidian, каталог почтовых операций, папка отдельного аккаунта). Должен быть абсолютным путём. Относительные пути вроде dir:../tenants/foo/ отклоняются на этапе диспетчеризации, потому что разрешались бы относительно того CWD, в котором в этот момент находится диспетчер, — это неоднозначно и открывает вектор атаки «confused deputy». В остальном путь считается доверенным — это ваша машина, ваша файловая система, воркер работает от вашего uid. Это модель угроз «доверенный локальный пользователь»; kanban изначально рассчитан на один хост. Сохраняется после завершения.
    • worktree — git worktree под .worktrees/<id>/ для задач с кодом. Используйте worktree:<path>, чтобы закрепить конкретный целевой путь. git worktree add на стороне воркера создаёт его, используя --branch, если он передан. Сохраняется после завершения.
  • Диспетчер (dispatcher) — долгоживущий цикл, который каждые N секунд (по умолчанию 60): переоформляет протухшие захваты, переоформляет упавшие воркеры (PID исчез, но TTL ещё не истёк), добивает воркеров, переживших свой завершённый запуск (воркер, всё ещё живой после собственного kanban_complete/kanban_block, завершается принудительно, когда его запуск закрыт уже две минуты, — ему оставляют время закончить последний ход; сопоставление идёт по PID и отпечатку времени запуска, так что переиспользованному PID сигнал никогда не уйдёт; записывается как событие terminal_worker_reaped), продвигает готовые задачи, атомарно захватывает их, запускает назначенные профили. По умолчанию работает внутри шлюза (kanban.dispatch_in_gateway: true). Один диспетчер за тик обходит все доски; воркеры запускаются с закреплённой переменной HERMES_KANBAN_BOARD, чтобы не видеть другие доски. После kanban.failure_limit подряд идущих неудачных попыток запуска одной задачи (по умолчанию: 2) диспетчер автоматически блокирует её, указывая последнюю ошибку как причину — это предотвращает «дребезг» на задачах, чей профиль не существует, чьё рабочее пространство не монтируется, и т. п.
  • Арендатор (tenant) — опциональное строковое пространство имён внутри доски. Один флот специалистов может обслуживать несколько бизнесов (--tenant business-a) с изоляцией данных по пути рабочего пространства и префиксу ключа памяти. Арендаторы — мягкий фильтр; доски — жёсткая граница изоляции.

Доски (мультипроектность)

Доски позволяют разделить несвязанные потоки работы — по одной на проект, репозиторий или домен — на изолированные очереди. На новой установке ровно одна доска по имени default (БД в ~/.hermes/kanban.db для обратной совместимости). Пользователям, которым нужен только один поток работы, никогда не придётся узнавать про доски — функция включается по желанию.

Изоляция по доскам абсолютна:

  • Отдельная SQLite-база на доску (~/.hermes/kanban/boards/<slug>/kanban.db).
  • Отдельные каталоги workspaces/ и logs/.
  • Воркеры, запущенные для задачи, видят только задачи своей доски — диспетчер прописывает HERMES_KANBAN_BOARD в окружении дочернего процесса, и каждый инструмент kanban_*, доступный воркеру, читает эту переменную.
  • Связывать задачи между досками нельзя (это упрощает схему; если действительно нужны межпроектные ссылки, используйте упоминания в свободном тексте и находите нужное вручную по id).

Управление досками из CLI

# Посмотреть, что есть на диске. На свежей установке — только "default".
hermes kanban boards list

# Создать новую доску.
hermes kanban boards create atm10-server \
    --name "ATM10 Server" \
    --description "Minecraft modded server ops" \
    --icon 🎮 \
    --switch                   # опционально: сделать её активной доской

# Работать с конкретной доской без переключения.
hermes kanban --board atm10-server list
hermes kanban --board atm10-server create "Restart ATM server" --assignee ops

# Изменить, какая доска "текущая" для последующих вызовов.
hermes kanban boards switch atm10-server
hermes kanban boards show             # кто активен прямо сейчас?

# Переименовать отображаемое имя (слаг неизменяем — это имя каталога).
hermes kanban boards rename atm10-server "ATM10 (Prod)"

# Архивировать (по умолчанию) — переносит каталог доски в boards/_archived/<slug>-<ts>/.
# Можно восстановить, вернув каталог обратно.
hermes kanban boards rm atm10-server

# Полное удаление — `rm -rf` каталога доски. Без возможности восстановления.
hermes kanban boards rm atm10-server --delete

Порядок разрешения доски (сначала — с наивысшим приоритетом):

  1. Явный --board <slug> при вызове CLI.
  2. Переменная окружения HERMES_KANBAN_BOARD (устанавливается диспетчером при запуске воркера, чтобы воркеры не видели другие доски).
  3. ~/.hermes/kanban/current — слаг, сохранённый командой hermes kanban boards switch.
  4. default.

Слаги валидируются: строчные буквы латиницы + цифры + дефисы + подчёркивания, 1–64 символа, должны начинаться с буквы или цифры. Заглавные буквы во вводе автоматически приводятся к нижнему регистру. Всё остальное (слэши, пробелы, точки, ..) отклоняется на уровне CLI, чтобы приёмы с обходом путей не могли назвать доску.

Управление досками из дашборда

hermes dashboard → вкладка Kanban показывает переключатель досок наверху, как только существует больше одной доски (или у любой доски есть задачи). Пользователи с одной доской видят только небольшую кнопку + New board; переключатель скрыт, пока в нём нет нужды.

  • Выпадающий список досок — выбор активной доски. Ваш выбор сохраняется в localStorage браузера, поэтому переживает перезагрузки, не сдвигая при этом текущий указатель CLI из-под открытого терминала.
  • + New board — открывает модальное окно с запросом слага, отображаемого имени, описания, иконки, каталога проекта и (если есть хотя бы один Project) селектора Project. Есть опция автоматического переключения на новую доску.
  • Settings — открывает модальное окно для правки отображаемого имени, описания и каталога проекта (default_workdir) текущей доски. Каталог проекта — это рабочее пространство доски по умолчанию, которое наследует каждая новая задача (git-репозиторий → сохраняемый worktree, обычный каталог → сохраняемый каталог); при создании задачи его по-прежнему можно переопределить. Очистите поле — и новые задачи вернутся к одноразовым временным рабочим пространствам, если только к доске не привязан Project: тогда используется основная папка проекта; чтобы откатиться к временным пространствам, сначала отвяжите проект.
  • Project (в обоих модальных окнах) — привязывает доску к проекту Hermes (Project; поле доски project_id). Задачи, созданные на привязанной доске, наследуют Project; если выбрать проект при пустом поле каталога, в каталог проекта заодно подставится основная папка Project. Привязанный проект показывается бейджем Project: <name> рядом со списком досок; его × отвязывает доску (отправляет project_id: ""), не трогая каталог проекта, а No binding в Settings делает то же при сохранении. Селектор появляется, только если существует хотя бы один Project.
  • Archive — показывается только для досок, отличных от default. После подтверждения переносит каталог доски в boards/_archived/.

Все конечные точки API дашборда принимают ?board=<slug> для указания области действия по доске. WebSocket-соединение для событий привязано к доске в момент подключения; переключение в интерфейсе открывает новое соединение против новой доски.

Переключение досок в десктопном приложении

В десктопном приложении переключатель досок стоит в строке заголовка вверху страницы Kanban, рядом с названием страницы: элемент Board с именем текущей доски, числом задач и шевроном — при наведении появляется подсказка «Switch board». Нажмите на него, чтобы выбрать другую доску или переименовать, настроить, экспортировать, импортировать, создать или архивировать доски. Когда Kanban открыт в разделённой плитке, тот же элемент Board стоит в собственной строке заголовка доски, после числа задач. Как и дашборд, десктоп хранит собственный выбор (локально) и не двигает указатель current в CLI.

Файловые вложения

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

  • Загрузка — откройте задачу в выдвижной панели дашборда и воспользуйтесь кнопкой Upload file в разделе Attachments (можно загрузить сразу несколько файлов). Каждая загрузка ограничена 25 МБ.
  • Хранение — файлы попадают в <hermes-home>/kanban/attachments/<task_id>/ для доски по умолчанию, или в <hermes-home>/kanban/boards/<slug>/attachments/<task_id>/ для именованной доски. Задайте HERMES_KANBAN_ATTACHMENTS_ROOT, чтобы закрепить произвольное расположение.
  • Что видит воркер — когда диспетчер передаёт задачу воркеру, в контекст воркера включается раздел Attachments с именем каждого файла и его абсолютным путём. У воркера есть полный доступ к файловым и терминальным инструментам, поэтому он читает вложения напрямую (read_file или командой оболочки, например pdftotext).
  • Скачивание / удаление — панель показывает каждое вложение со ссылкой на скачивание и элементом удаления (×). Удаление вложения стирает его строку метаданных; файл на диске удаляется, только если на него больше не ссылается ни одна другая строка вложений (файл, общий для нескольких задач, живёт до удаления последней ссылки). Из CLI вложение так же удаляет hermes kanban attach-rm ATTACHMENT_ID.
заметка
Удалённые терминальные бэкенды

Пути вложений разрешаются напрямую только на локальном терминальном бэкенде, который является дефолтным для воркеров Kanban. Если вы запускаете воркеров на удалённом бэкенде (Docker, Modal), смонтируйте каталог attachments/ доски внутрь песочницы, чтобы абсолютные пути в контексте воркера были достижимы.

Быстрый старт

Команды ниже выполняете вы (человек), настраивая доску и создавая задачи. Как только задача назначена, диспетчер запускает назначенный профиль как воркера, и дальше модель ведёт задачу через вызовы инструментов kanban_*, а не команды CLI — см. Как исполнители взаимодействуют с доской.

# 1. Создать доску (вы)
hermes kanban init

# 2. Запустить шлюз (в нём работает встроенный диспетчер)
hermes gateway start

# 3. Создать задачу (вы — или агент-оркестратор через kanban_create)
hermes kanban create "research AI funding landscape" --assignee researcher

# 4. Наблюдать за активностью в реальном времени (вы)
hermes kanban watch

# 5. Посмотреть доску (вы)
hermes kanban list
hermes kanban stats

Когда диспетчер подхватывает t_abcd и запускает профиль researcher, самое первое, что делает модель этого воркера, — вызывает kanban_show(), чтобы прочитать свою задачу. Она не запускает hermes kanban show t_abcd.

Диспетчер, встроенный в шлюз (по умолчанию)

Диспетчер работает внутри процесса шлюза. Ничего устанавливать не нужно, отдельный сервис не требует управления — если шлюз запущен, готовые задачи подхватываются на следующем тике (по умолчанию 60 с).

# config.yaml
kanban:
  dispatch_in_gateway: true        # по умолчанию
  dispatch_interval_seconds: 60    # по умолчанию
  review_dispatch: true            # по умолчанию: запускать назначенный профиль
                                    # со встроенным навыком sdlc-review. Установите
                                    # false для досок с ревью только людьми.
  # dispatch_profiles: [sage]       # не задан (ключа нет): этот дом может захватывать
                                   # карточки для любого существующего профиля. Задайте
                                   # список (или строку через запятую) имён профилей,
                                   # чтобы ограничить, каких исполнителей захватывает этот дом.
                                   # Fail-closed: пустой список, `null` или голый
                                   # `dispatch_profiles:` не захватывают ничего, как и
                                   # нечитаемый конфиг.

Флаг конфигурации можно переопределить во время работы через HERMES_KANBAN_DISPATCH_IN_GATEWAY=0 для отладки. Действуют стандартные механизмы supervision шлюза: запустите hermes gateway start напрямую или подключите шлюз как systemd user unit (см. документацию по шлюзу). Без запущенного шлюза задачи в статусе ready остаются на месте, пока он не появится — hermes kanban create предупреждает об этом при создании задачи.

Воркеры и cgroup systemd

Воркеры — процессы по принципу «запустил и забыл», они переживают такт диспетчера, поэтому везде, где диспетчер работает внутри юнита systemd, воркер запускается в собственном временном scope (systemd-run --user --scope --unit hermes-worker-kanban-<task>-run-<run>) и переживает выход или перезапуск этого юнита. Для создания scope нужна сессионная шина пользователя, который запускает диспетчер (/run/user/<uid>/bus); на установке системного уровня дайте её пользователю через sudo loginctl enable-linger <user>.

  • Диспетчер в шлюзе (dispatch_in_gateway: true, шлюз под systemd): scope обязателен. Если шина недоступна, запуск отклоняется и записывается на карточку как инфраструктурный сбой — spawn_failed с metadata.infrastructure: true, рецептом с linger в last_failure_error и предупреждением в логе шлюза, — а карточка остаётся в ready. Счётчик consecutive_failures при этом не растёт, так что сбой на хосте никогда не паркует карточки голым blocked; повторы разносит защита от повторного запуска (infrastructure_cooldown, то же окно, что и у кулдауна по лимиту частоты), пока шина не вернётся.
  • hermes kanban dispatch из вашего собственного юнита (таймер с Type=oneshot, сервис-секвенсор): воркер получает такой же scope, если шина доступна. Без неё воркер всё равно запускается — внутри cgroup вашего юнита, — а диспетчер один раз громко пишет в лог, что выход юнита его убьёт. Type=oneshot со стандартным KillMode=control-group теряет всех воркеров в течение секунды после окончания прохода; либо включите linger, чтобы scope можно было создать, либо поставьте в этом юните KillMode=process, чтобы его выход убивал только сам диспетчер.

Запуск hermes kanban daemon как отдельного процесса устарел (deprecated); используйте шлюз. Если вы действительно не можете запустить шлюз (политика безголового хоста запрещает долгоживущие сервисы и т. п.), лазейка --force поддерживает старый автономный демон живым ещё один цикл релиза, но одновременный запуск встроенного в шлюз диспетчера И автономного демона против одной и той же kanban.db вызывает гонки захвата и не поддерживается.

Общие доски между домами

Если смонтировать один kanban.db в нескольких домах Hermes (контейнеры, хосты флота), доска станет общей, но имена профилей локальны для дома, и в каждом доме есть корневой профиль default — так что default конфликтует по построению. Затвор запуска диспетчера проверяет profile_exists(assignee) относительно захватывающего дома, и без дополнительной настройки диспетчер каждого дома считает карточку, назначенную default, захватываемой — её может захватить и выполнить не тот дом. Либо дайте каждому дому уникальные имена профилей и никогда не назначайте карточки на default на общей доске, либо задайте в каждом доме kanban.dispatch_profiles, объявив ровно тех исполнителей, чьи карточки этот дом может захватывать, — всё остальное попадает в корзину диспетчера skipped_nonspawnable вместо запуска и больше не считается запускаемой работой для пробуждающей проверки шлюза.

Идемпотентное создание (для автоматизации / вебхуков)

# Первый вызов создаёт задачу. Любой последующий вызов с тем же ключом
# возвращает id существующей задачи вместо дублирования.
hermes kanban create "nightly ops review" \
    --assignee ops \
    --idempotency-key "nightly-ops-$(date -u +%Y-%m-%d)" \
    --json

Массовые команды CLI

Все команды жизненного цикла принимают несколько id, так что можно очистить пачку задач одной командой:

hermes kanban complete t_abc t_def t_hij --result "batch wrap"
hermes kanban archive  t_abc t_def t_hij
hermes kanban unblock  t_abc t_def
hermes kanban block    t_abc "need input" --ids t_def t_hij
заметка
Куда попадает разблокированная задача

unblock восстанавливает безопасную исходную фазу: review для задачи, изначально пришедшей от ревьюера, если её родители завершены; ready для реализационной задачи, если её родители завершены; либо todo, пока хоть один родитель ещё открыт. Задача в todo сохраняет происхождение исходной фазы и автоматически возвращается в review или ready, когда зависимость закрывается. В triage unblock не отправляет никогда напрямую.

Если вы разблокировали задачу, а она потом всплыла в triage, дело не в разблокировке. Туда её отправила последующая повторная блокировка по той же причине: после того как задачу заблокировали → разблокировали → снова заблокировали по тому же поводу BLOCK_RECURRENCE_LIMIT раз (по умолчанию 2), разрыватель цикла перестаёт возвращать её в blocked — где cron просто продолжал бы её разблокировать — и направляет в triage — под внимание оркестрации. Это детерминированная защита на уровне БД, а не суждение модели, и текст задачи от неё не спасает: счётчик повторов намеренно переживает каждую разблокировку (сбрасывается только успешным complete). Чтобы разблокированная задача осталась в рабочем пуле, разберитесь, почему её раз за разом блокирует (незавершённый родитель, недостающие входные данные, нехватка возможностей), — и только потом разблокируйте, либо поднимите BLOCK_RECURRENCE_LIMIT, если цикл ожидаем.

Включение инструментов для чат-профиля

Плагин Kanban в десктопе показывает доску, но не даёт чат-агенту права управлять задачами. Включите набор инструментов kanban для профиля и платформы, которые должны оркестрировать работу:

hermes -p planner tools enable kanban                      # чаты CLI / TUI / десктопа
hermes -p planner tools enable kanban --platform telegram  # платформа шлюза

У каждой платформы свой выбор под platform_toolsets.<platform> в config.yaml; тот же набор инструментов доступен как галочка в hermes tools и в дашборде. Агент шлюза с этим набором может вызывать kanban_create из чата и автоматически подписывается на уведомления о завершении/блокировке этой задачи в том же треде. После смены настройки начните новый чат: существующие разговоры сохраняют схемы инструментов и кэш промпта. agent.disabled_toolsets по-прежнему главнее. Устаревший верхнеуровневый toolsets: [kanban] учитывается как запасной вариант, только когда выбор по платформам не сохранён; один лишь all не считается включением Kanban.

Воркеры, принадлежащие диспетчеру, получают инструменты жизненного цикла задачи автоматически. Потомки delegate_task права менять доску не получают.

Как исполнители взаимодействуют с доской

Воркеры не обращаются к hermes kanban через shell. Когда диспетчер запускает воркера, он задаёт HERMES_KANBAN_TASK=t_abcd в окружении дочернего процесса, и эта переменная включает выделенный набор инструментов kanban в схеме модели. Тот же набор инструментов также доступен профилям-оркестраторам, которые включают kanban в конфигурации своих наборов инструментов. Эти инструменты читают и изменяют доску напрямую через Python-слой kanban_db, точно так же, как это делает CLI. Работающий воркер вызывает их как любой другой инструмент; он никогда не видит и не нуждается в CLI hermes kanban.

ИнструментНазначениеОбязательные параметры
kanban_showПрочитать текущую задачу (заголовок, тело, предыдущие попытки, передачи от родителей, комментарии, полностью отформатированный worker_context). По умолчанию использует id задачи из окружения.—
kanban_listСписок сводок по задачам с фильтрами по assignee, status, tenant, видимости архивных и лимиту. Предназначен для оркестраторов, изучающих работу на доске.—
kanban_completeЗавершить со структурированной передачей summary + metadata.хотя бы одно из summary / result
kanban_request_reviewНачать ревью в рамках той же карточки с постоянной summary, опциональной metadata и опциональным профилем ревьюера. Задача переходит в review; это не блокировка.summary
kanban_request_changesВердикт ревьюера по активному запуску ревью. Закрывает этот запуск, снова применяет условия по родителям и направляет задачу обратно к исходному исполнителю без учёта в счётчике циклов блокировки.reason
kanban_blockОстановить работу и направить дальше в зависимости от причины: kind=dependency (ждёт в todo и авто-возобновляется, когда завершится незаконченный родитель; если открытого родителя нет, записывается как needs_input, ведь такое ожидание никогда не закончится), needs_input/capability/transient (выводится на человека). Повторные блокировки с той же причиной автоматически эскалируются в triage.reason
kanban_heartbeatСигнализировать о жизнеспособности во время длительных операций. Чистый побочный эффект.—
kanban_commentДобавить постоянную заметку в ветку задачи.task_id, body
kanban_attachПрикрепить файл к задаче, передав его байты прямо в вызове (base64); сохраняется в каталоге вложений задачи (лимит 25 МБ).байты файла + имя
kanban_attach_urlПрикрепить файл к задаче по URL.url
kanban_attachmentsПоказать список вложений задачи.—
kanban_create(Оркестраторы) разветвить работу на дочерние задачи с assignee, опциональными parents, skills и т. д. Возвращает gated: true + gated_by, если открытый родитель запарковал новую карточку в todo.title, assignee
kanban_link(Оркестраторы) добавить постфактум ребро зависимости parent_id → child_id. Возвращает gated: true, если потомок был ready и его понизили обратно в todo, потому что родитель не завершён, — потомок запустится только после завершения родителя. Отклоняется с child is already running, если потомок уже захвачен: ребро, добавленное после захвата, не может упорядочить запуск (воркер всё же может связать свою собственную выполняющуюся карточку перед блокировкой kind=dependency).parent_id, child_id
kanban_unblock(Оркестраторы) восстановить заблокированную задачу в её исходной фазе (review или ready), либо перевести в todo, пока родитель ещё открыт.task_id

Типичный ход воркера выглядит так:

# Вызовы инструментов модели, по порядку:
kanban_show()                                     # без аргументов — использует HERMES_KANBAN_TASK
# (модель читает возвращённый worker_context, выполняет работу через терминальные/файловые инструменты)
kanban_heartbeat(note="halfway through — 4 of 8 files transformed")
# (продолжение работы)
kanban_complete(
    summary="migrated limiter.py to token-bucket; added 14 tests, all pass",
    metadata={"changed_files": ["limiter.py", "tests/test_limiter.py"], "tests_run": 14},
)

Оркестратор-воркер вместо этого разветвляет работу:

kanban_show()
kanban_create(
    title="research ICP funding 2024-2026",
    assignee="researcher-a",
    body="focus on seed + series A, North America, AI-adjacent",
)
# → возвращает {"task_id": "t_r1", ...}
kanban_create(title="research ICP funding — EU angle", assignee="researcher-b", body="…")
# → возвращает {"task_id": "t_r2", ...}
kanban_create(
    title="synthesize findings into launch brief",
    assignee="writer",
    parents=["t_r1", "t_r2"],                     # продвигается в ready, когда обе завершены
    body="one-pager, 300 words, neutral tone",
)
kanban_complete(summary="decomposed into 2 research tasks + 1 writer; linked dependencies")

Инструменты «(Оркестраторы)» — kanban_list, kanban_create, kanban_link, kanban_unblock и kanban_comment для чужих задач — доступны через тот же набор инструментов; соглашение (закодированное в автоматически внедряемых инструкциях по kanban) в том, что профили-воркеры не разветвляют и не маршрутизируют посторонние задачи, а профили-оркестраторы не выполняют работу по реализации. Запущенные диспетчером воркеры по-прежнему ограничены рамками своей задачи для деструктивных операций жизненного цикла и не могут изменять посторонние задачи.

Почему инструменты, а не обращение к hermes kanban через shell

Три причины:

  1. Переносимость между бэкендами. Воркеры, чей терминальный инструмент указывает на удалённый бэкенд (Docker / Modal / Singularity / SSH), выполнили бы hermes kanban complete внутри контейнера, где hermes не установлен, а ~/.hermes/kanban.db не смонтирована. Инструменты kanban работают в собственном Python-процессе агента и всегда достигают ~/.hermes/kanban.db независимо от терминального бэкенда.
  2. Никакой хрупкости с экранированием shell. Передача --metadata '{"files": [...]}' через shlex + argparse — это скрытая мина замедленного действия. Структурированные аргументы инструмента полностью её обходят.
  3. Более качественные ошибки. Результаты вызова инструментов — это структурированный JSON, о котором модель может рассуждать, а не строки stderr, которые надо разбирать.

Нулевой след в схеме при обычных сессиях. У обычной сессии hermes chat в схеме ноль инструментов kanban_*, если только активный профиль явно не включил набор инструментов kanban для работы оркестратора. Воркеры задач, запущенные диспетчером, получают инструменты, ограниченные своей задачей, потому что задана HERMES_KANBAN_TASK; профили-оркестраторы получают более широкую поверхность маршрутизации через конфигурацию. Никакого раздувания набора инструментов для пользователей, которые никогда не касаются kanban.

Автоматически внедряемые инструкции по kanban учат модель, какой инструмент вызывать когда и в каком порядке. Внедряются они только воркеру, запущенному диспетчером и владеющему задачей: интерактивная сессия, у которой просто включён набор инструментов kanban, сохраняет инструменты, но ей не говорят, что ей «назначена ОДНА задача», а потомок delegate_task или запуск cron, стартовавший изнутри воркера, — они наследуют HERMES_KANBAN_TASK воркера — не получает протокол воркера и не записывает терминальный исход в карточку воркера, когда исчерпывается его собственный бюджет ходов.

Рекомендуемые доказательства передачи работы

kanban_complete(summary=..., metadata={...}) намеренно гибок: summary — это человекочитаемое закрытие задачи, а metadata — машиночитаемая передача, которую нижестоящие агенты, ревьюеры или дашборды могут переиспользовать без разбора прозы.

Для инженерных задач и задач ревью предпочитайте такую опциональную форму metadata:

{
  "changed_files": ["path/to/file.py"],
  "verification": ["pytest tests/hermes_cli/test_kanban_db.py -q"],
  "dependencies": ["parent task id or external issue, if any"],
  "blocked_reason": null,
  "retry_notes": "what failed before, if this was a retry",
  "residual_risk": ["what was not tested or still needs human review"]
}

Эти ключи — соглашение, а не требование схемы. Полезное свойство в том, что каждый воркер оставляет достаточно свидетельств, чтобы следующий читатель быстро ответил на четыре вопроса:

  1. Что изменилось?
  2. Как это проверялось?
  3. Что может разблокировать или повторить это, если не получится?
  4. Какой риск остался сознательно непокрытым?

Держите секреты, необработанные логи, токены, материалы OAuth и посторонние транскрипты вне metadata. Вместо этого храните указатели и сводки. Если у задачи нет файлов или тестов, скажите об этом явно в summary и используйте metadata для тех свидетельств, что всё же есть, — например, исходных URL, id issue или шагов ручной проверки.

Жизненный цикл воркера

Каждый профиль, работающий с задачами kanban, автоматически получает жизненный цикл воркера — он внедряется в системный промпт воркера при запуске (блок KANBAN_GUIDANCE), так что устанавливать или настраивать ничего не нужно. Он обучает воркера полному жизненному циклу в терминах вызовов инструментов, а не команд CLI:

  1. При запуске вызвать kanban_show(), чтобы прочитать заголовок + тело + передачи от родителей + предыдущие попытки + полную ветку комментариев.
  2. cd $HERMES_KANBAN_WORKSPACE (через терминальный инструмент) и работать там.
  3. Вызывать kanban_heartbeat(note="...") каждые несколько минут во время длительных операций. Если работа может занять больше 1 часа, вызывайте kanban_heartbeat хотя бы раз в час — диспетчер переоформляет задачи, которые работают дольше kanban.dispatch_stale_timeout_seconds (по умолчанию 4 ч) без heartbeat в последний час, предполагая, что воркер упал без очистки. Переоформление безобидно (задача возвращается в ready для повторного запуска без увеличения счётчика неудач), но вы теряете прогресс текущего запуска.
  4. Завершить вызовом kanban_complete(summary="...", metadata={...}), передать правку кода на ревью в той же карточке через kanban_request_review(summary="...") или вызвать kanban_block(reason="..."), если застряли.

Обычная активность инструментов тоже автоматически продлевает захват (воркер примерно раз в минуту отражает на доске свою живость внутри процесса). Этот мост работает только для процесса, который запустил сам диспетчер: процесс, у которого рядом с HERMES_KANBAN_TASK есть HERMES_DELEGATED_CHILD_CONTEXT (потомок delegate_task или вручную запущенная копия окружения воркера), от доски отгорожен — его автоматический heartbeat один раз пишет в лог предупреждение kanban auto-heartbeat for task … refused, а kanban_complete / kanban_request_review отказывают. Исправляйте запуск (пусть воркера запускает диспетчер), а не убирайте маркер из окружения.

Этот финальный терминальный вызов доски (kanban_complete / kanban_request_review / kanban_block; ревьюеры заканчивают kanban_complete или kanban_request_changes) — часть протокола воркера. Если процесс воркера завершается со статусом 0, пока задача ещё running, диспетчер считает это нарушением протокола и генерирует событие protocol_violation. Поэтому воркер, запущенный диспетчером, чей ход провалился, завершается с ненулевым кодом: 1 при обычной ошибке, 75 (EX_TEMPFAIL), когда провайдер ограничил частоту запросов, был перегружен, возвращал 5xx или не отвечал вовремя, либо аккаунт упёрся в стену биллинга/квоты, — такой запуск диспетчер записывает как rate_limited и возвращает задачу в очередь, не засчитывая неудачу, так что окно квоты никогда не оформляется как нарушение протокола, — и 78 (EX_CONFIG), когда провайдер отверг то, что повтором не исправить: учётные данные профиля (401/403, отозванный или неверный ключ), модель (404 / модель не найдена) или цепочку TLS. Такая терминальная ошибка провайдера срабатывает предохранителем с первого же раза: диспетчер записывает запуск как crashed с exit_kind: terminal_provider, генерирует gave_up с terminal_provider: true и паркует карточку в blocked (намертво — recompute_ready сама её не возобновит) со словами самого провайдера в last_failure_error, вместо того чтобы перезапускать её в ту же стену, пока не кончатся kanban.failure_limit / max_retries. Обе полосы учитываются одинаково: воркер-ревьюер, упавший на отозванном ключе, паркует карточку точно так же, как исполнитель. hermes kanban show показывает это как Provider rejected this profile’s credential or model — blocked after one attempt; почините провайдера у профиля-исполнителя (hermes -p <profile> auth / setup), затем выполните hermes kanban unblock <id>. Ещё воркер пишет свой код выхода последней строкой собственного лога ([kanban-worker-exit] rc=<code>), так что процесс hermes kanban dispatch, запускаемый на каждый такт, — он воркера не дожидается и прочитать его код выхода не может — учитывает такую смерть так же, как встроенный в шлюз диспетчер; воркер, убитый до этой строки, считается обычным crashed (pid <n> not alive).

Профилактика на стороне агента. Прежде чем воркер завершится, Hermes подсовывает до двух синтетических подсказок, если видит, что модель собирается остановиться без терминального вызова инструмента доски. Так ловится типовой случай, когда модель проговаривает следующий шаг («Сейчас напишу отчёт») и останавливается с finish_reason=stop. Подсказка напоминает немедленно вызвать kanban_complete, kanban_request_review или kanban_block. Воркер, который уже передал карточку (kanban_request_review, или kanban_request_changes от ревьюера), подсказок не получает никогда: передача и есть его терминальный вызов, и подсказка не просит его вызвать kanban_complete для карточки на ревью. Защита работает только для самого воркера, запущенного диспетчером (задана HERMES_KANBAN_TASK, и запуск владеет этой задачей), — потомки delegate_task и задачи cron, выполняемые внутри воркера, наследуют переменную, но подсказок никогда не получают, ведь у них нет инструментов доски, — и отключается через HERMES_KANBAN_STOP_NUDGE=0.

Восстановление на стороне диспетчера. Если подсказки исчерпаны или воркер упал раньше, чем до них дошло, диспетчер даёт нарушению ограниченное число повторов (до _PROTOCOL_VIOLATION_FAILURE_LIMIT подряд идущих нарушений, по умолчанию 3) и лишь затем автоматически блокирует задачу вместо того, чтобы запускать её в тот же цикл заново. В бюджет попадают только подряд идущие нарушения протокола с чистым выходом — вклинившиеся переочереди из-за лимита запросов нейтральны, а любой другой вид сбоя обнуляет серию, — и max_retries у конкретной задачи перекрывает этот предел. Карточка, заблокированная этим бюджетом, остаётся заблокированной (она не продвигается автоматически, как блокировка предохранителем ниже failure_limit) до hermes kanban unblock <id>, который заодно выдаёт свежий бюджет повторов. Обычно это значит, что модель написала обычный текстовый ответ и завершила работу, не воспользовавшись поверхностью инструментов Kanban.

Жизненный цикл вместе с важными справочными деталями (виды рабочих пространств, артефакты artifacts в результатах, захват созданных карточек) поставляются в этом блоке системного промпта, так что каждый воркер получает их независимо от того, под каким профилем работает, — без настройки навыков для каждого профиля.

Имена сессий воркеров. Сессия воркера при запуске получает название по своей карточке (Fix the swap modal или Kanban task <id>, если строку доски прочитать не удалось), так что hermes sessions и поиск по сессиям показывают карточку, а не догадку модели. Воркеры никогда не делают вспомогательный вызов модели title_generation, которым называются интерактивные сессии; ручной /title в сессии воркера по-прежнему главнее.

Закрепление дополнительных навыков за конкретной задачей

Иногда отдельной задаче нужен специализированный контекст, которого нет у профиля-исполнителя по умолчанию, — задача перевода нуждается в навыке translation, задача ревью — в github-code-review, аудит безопасности — в security-pr-audit. Вместо того чтобы каждый раз редактировать профиль исполнителя, прикрепите навыки прямо к задаче.

Из агента-оркестратора (обычный случай — один агент направляет работу другому) используйте массив skills в инструменте kanban_create:

kanban_create(
    title="translate README to Japanese",
    assignee="linguist",
    skills=["translation"],
)

kanban_create(
    title="audit auth flow",
    assignee="reviewer",
    skills=["security-pr-audit", "github-code-review"],
)

От человека (CLI / слэш-команда) повторите --skill для каждого навыка:

hermes kanban create "translate README to Japanese" \
    --assignee linguist \
    --skill translation

hermes kanban create "audit auth flow" \
    --assignee reviewer \
    --skill security-pr-audit \
    --skill github-code-review

Из дашборда введите навыки через запятую в поле skills диалога создания задачи.

Диспетчер генерирует по одному флагу --skills <name> на каждый указанный навык, так что воркер запускается со всеми ими, загруженными поверх автоматически внедряемых инструкций по kanban. Имена навыков должны совпадать с навыками, реально установленными на профиле исполнителя (запустите hermes skills list, чтобы увидеть доступные); установки во время выполнения нет.

Переопределение модели для конкретной задачи

Воркера задачи можно закрепить за конкретной моделью (и, если нужно, провайдером) независимо от умолчания профиля-исполнителя:

# При создании
hermes kanban create "hard refactor" --assignee coder \
    --model claude-opus-4.6 --provider anthropic

# Или позже — вступит в силу при следующей диспетчеризации
hermes kanban set-model t_abcd claude-opus-4.6 --provider anthropic
hermes kanban set-model t_abcd none    # снять переопределение

Диспетчер запускает воркера с закреплённой моделью (--provider <name> передаётся, когда он задан; --provider требует модели). Выпадающий список моделей у задачи в дашборде правит то же поле model_override. Без переопределения воркер работает на модели, настроенной в его профиле.

Расчёт стоимости: фронтир-оркестратор, недорогие воркеры

Настройки на уровне профиля в kanban естественным образом разделяют стоимость планировщика и воркеров. Декомпозиция проекта в хорошо очерченные карточки требует суждения фронтир-уровня; выполнение карточки, у которой уже есть ясная цель, контекст и свидетельства передачи, обычно этого не требует — а именно на воркерах уходит подавляющая часть токенов, так что расходы определяет модель воркера. Держите профиль оркестратора/диспетчера на фронтир-модели, а профили воркеров направляйте на недорогие модели. У каждого профиля свой config.yaml под ~/.hermes/profiles/<name>/, и диспетчер передаёт при запуске hermes -p <assignee> переменную HERMES_HOME, привязанную к профилю, так что каждый воркер читает настройки модели своего собственного профиля:

# ~/.hermes/config.yaml (профиль оркестратора / диспетчера)
model:
  default: "your-frontier-model"

# ~/.hermes/profiles/coder/config.yaml (профиль воркера)
model:
  default: "your-inexpensive-model"

# ~/.hermes/profiles/researcher/config.yaml (ещё один профиль воркера)
model:
  default: "your-inexpensive-model"

Для отдельной карточки, чувствительной к качеству, закрепите именно её за более сильной моделью через переопределение модели для конкретной задачи (--model/--provider при создании, hermes kanban set-model позже, или выпадающий список моделей в дашборде) — редактировать профили не нужно.

Хуки плагинов на жизненный цикл

Переходы по доске запускают хуки плагинов: kanban_task_claimed, kanban_task_completed и kanban_task_blocked, каждый с task_id и profile_name. Хуки срабатывают после фиксации изменений в БД доски, так что колбэк всегда видит устойчивое состояние. Учтите разделение процессов: kanban_task_claimed срабатывает в процессе диспетчера, а kanban_task_completed/kanban_task_blocked — в процессе воркера; чтобы централизованно наблюдать все переходы, регистрируйте хук в профиле диспетчера.

def register(ctx):
    def on_blocked(task_id=None, profile_name=None, **kw):
        ctx.dispatch_tool("terminal", {"command": f"notify-send 'kanban blocked: {task_id}'"})
    ctx.register_hook("kanban_task_blocked", on_blocked)

Карточки в режиме цели (--goal)

По умолчанию каждый воркер получает одну попытку на свою карточку — выполнить работу, вызвать kanban_complete/kanban_block, завершиться. Передайте --goal (CLI) или goal_mode=True (инструмент kanban_create / дашборд), чтобы вместо этого запустить воркера в цикле цели (goal loop) — том же движке в стиле Ralph, что стоит за слэш-командой /goal: после каждого хода вспомогательный судья сверяет результат воркера с заголовком + телом карточки (рассматриваемыми как критерии приёмки), и если работа не завершена — а бюджет ходов ещё остался — воркер продолжает в той же сессии, пока судья не согласится, воркер сам не завершит задачу или бюджет не закончится (что блокирует карточку для ревью человеком, а не тихо завершает работу). Если судья решит, что цель недостижима в том виде, как она записана, карточка блокируется сразу с его формулировкой причины: невыполнимая карточка никогда не помечается выполненной, а kanban complete и kanban request-review на такой карточке отклоняются с подсказкой про kanban block или переформулировку.

hermes kanban create "Translate the docs site to French" \
    --body "Acceptance: every page translated, no English left, links intact." \
    --assignee linguist \
    --goal \
    --goal-max-turns 15      # опционально; по умолчанию 20

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

Проверка судьёй на kanban complete / kanban request-review (и соответствующих инструментах kanban_complete / kanban_request_review) отклоняет только по настоящему вердикту. Если сам вызов судьи провалился — ошибка relay, авторизация, таймаут, — передача разрешается, а в лог падает предупреждение goal judge unreachable … allowing lifecycle handoff, так что недоступный судья никогда не блокирует одобрение человека. Безголовая проверка в CLI отправляет тот же ключ привязки к relay для задачи (kanban:<task-id>), что и specify/decompose, поэтому relay, требующие ключ сессии, принимают запрос судьи.

Журнал воркера карточки в режиме цели (выдвижная панель дашборда, hermes kanban log <id>) содержит ту же живую ленту инструментов, что и у любого другого воркера, плюс одну строку kanban goal loop: turn N/M verdict=… на каждый оценённый ход, так что за работой цикла можно следить, пока карточка выполняется.

заметка
Карточки в режиме цели используют движок

/goal, но не связаны с ним --goal запускает цикл продолжения внутри сессии воркера этой конкретной карточки. Он использует тот же движок, что и слэш-команда /goal, но не общее состояние: цель, заданная через /goal в чат-сессии, никогда не создаёт, не забирает и не двигает карточку kanban, а цикл карточки в режиме цели невидим для /goal status любой чат-сессии. Если хотите, чтобы продолжал итерации именно этот разговор, используйте /goal; если хотите, чтобы работа шла на доске, создайте карточку.

Как ведёт себя оркестратор

Хорошо ведущий себя оркестратор не выполняет работу сам. Он раскладывает цель пользователя на задачи, связывает их, назначает каждую одному из настроенных вами профилей и отходит в сторону. Инструкции для оркестратора — правила против соблазна взяться за работу самому, промпт «Шаг 0» для обнаружения профилей (диспетчер молча проваливает запуск для неизвестных имён исполнителей, поэтому оркестратор должен привязывать каждую карточку к профилям, реально существующим на вашей машине) и сценарий декомпозиции, завязанный на kanban_create / kanban_link / kanban_comment, — внедряются в системный промпт воркера автоматически; устанавливать ничего не нужно.

Канонический ход оркестратора (два параллельных исследователя передают работу автору):

# Цель от пользователя: "draft a launch post on the ICP funding landscape"
kanban_create(title="research ICP funding, NA angle",  assignee="researcher-a", body="…")  # → t_r1
kanban_create(title="research ICP funding, EU angle",  assignee="researcher-b", body="…")  # → t_r2
kanban_create(
    title="synthesize ICP funding research into launch post draft",
    assignee="writer",
    parents=["t_r1", "t_r2"],        # продвигается в 'ready', когда оба исследователя завершат работу
    body="one-pager, neutral tone, cite sources inline",
)                                     # → t_w1
# Опционально: добавить сквозные зависимости, обнаруженные позже, без пересоздания задач
kanban_link(parent_id="t_r1", child_id="t_followup")
kanban_complete(
    summary="decomposed into 2 parallel research tasks → 1 synthesis task; writer starts when both researchers finish",
)

Инструкции для оркестратора поставляются в системном промпте воркера автоматически — их не нужно устанавливать или синхронизировать по профилям.

Решайте до разветвления. Проектные решения принадлежат оркестратору, а не воркерам. Если двум параллельным карточкам пришлось бы выбирать одно и то же — схему именования, схему данных, формат файла, форму API, — оркестратор решает это один раз и вписывает решение в тела обеих карточек. Воркеры не видят карточек-соседей, так что тело каждой дочерней карточки должно нести каждое решение, от которого она зависит. Пример: для параллельных карточек «собрать экспортёр» и «собрать импортёр» не позволяйте каждому воркеру изобретать собственный формат файла — выберите его заранее (скажем, newline-delimited JSON с полем version) и впишите в оба тела, иначе половинки никогда не сойдутся.

Для лучшего результата сочетайте это с профилем, чьи наборы инструментов ограничены операциями с доской (kanban, gateway, memory), чтобы оркестратор буквально не мог выполнять задачи реализации, даже если попытается.

Дашборд (GUI)

CLI и слэш-команды /kanban достаточно, чтобы вести доску без интерфейса, но визуальная доска часто оказывается удобнее для людей в цикле: триаж, надзор за профилями, чтение веток комментариев, перетаскивание карточек между колонками. Hermes поставляет это как встроенный плагин дашборда в plugins/kanban/ — не как основную функцию, не как отдельный сервис — по модели, описанной в Расширение дашборда.

Откройте его так:

hermes kanban init      # разово: создать kanban.db, если ещё не создана
hermes dashboard        # вкладка "Kanban" появляется в навигации, после "Skills"

Что даёт плагин

  • Вкладка Kanban с одной колонкой на каждый статус: triage, todo, ready, running, blocked, done (плюс archived, когда включён переключатель).
    • Колонки-очереди показывают карточки в порядке диспетчеризации (приоритет, затем самые старые — следующей запускается верхняя). Колонка done — это история, и в ней сначала идут недавно завершённые; тот же порядок в CLI даёт hermes kanban list --status done --sort completed-desc.
    • triage — парковочная колонка для сырых идей. По умолчанию (kanban.auto_decompose: true) диспетчер автоматически запускает декомпозер на попавших сюда задачах. Встроенный декомпозер использует путь модели auxiliary.kanban_decomposer, читает список ваших профилей (с описаниями) и разворачивает задачу в небольшой граф дочерних задач, направленных наиболее подходящим специалистам. Исходная задача остаётся живой как родитель каждого потомка, так что её исполнитель (kanban.orchestrator_profile, иначе исполнитель, который у задачи уже был, иначе активный профиль по умолчанию) снова просыпается, чтобы оценить завершение, когда всё готово. Переключите тумблер Orchestration: Auto/Manual наверху страницы (изумрудный = Auto, приглушённо-серый = Manual) или отредактируйте config.yaml напрямую. Оба режима сосуществуют с hermes kanban specify — она по-прежнему доступна как переписывание спецификации для одной задачи, когда разветвление не нужно.
  • Карточки показывают id задачи, заголовок, значок приоритета, метку арендатора, назначенный профиль, счётчики комментариев/связей, индикатор прогресса (N/M завершённых потомков, когда у задачи есть зависимые), и «создано N назад». Чекбокс на каждой карточке включает множественный выбор.
  • Дорожки по профилям внутри Running — чекбокс на панели инструментов переключает группировку колонки Running по исполнителю.
  • Живые обновления через WebSocket — плагин отслеживает таблицу task_events, доступную только для добавления, с коротким интервалом опроса; доска отражает изменения мгновенно, как только любой профиль (CLI, шлюз или другая вкладка дашборда) действует. Перезагрузки дебаунсятся, так что всплеск событий вызывает один перезапрос.
  • Перетаскивание карточек между колонками для изменения статуса. Перетаскивание отправляет PATCH /api/plugins/kanban/tasks/:id, который проходит через тот же код kanban_db, что использует CLI, — три интерфейса никогда не могут разойтись. Перемещения в деструктивные статусы (done, archived, blocked) запрашивают подтверждение. Сенсорные устройства используют резервный вариант на основе указателя, так что доска работает и с планшета.
  • Диалог создания задачи — нажмите + в заголовке любой колонки, чтобы открыть модальное окно с подписанными полями: заголовок, исполнитель, приоритет, навыки, вид и путь рабочего пространства (подставляются из каталога проекта доски; переопределяются на уровне задачи), режим цели и (опционально) родительская задача из выпадающего списка по всем существующим задачам. Enter создаёт задачу, Shift+Enter вставляет перенос строки в поле заголовка, Escape отменяет. Создание из колонки Triage автоматически помещает новую задачу в triage.
  • Множественный выбор с массовыми действиями — shift/ctrl-клик по карточке или её чекбокс добавляет её в выбор. Наверху появляется панель массовых действий с пакетными переходами статуса, архивацией и переназначением (по выпадающему списку профилей или «(unassign)»). Деструктивные пакеты сначала запрашивают подтверждение. Частичные сбои по отдельным id сообщаются, не прерывая обработку остальных.
  • Клик по карточке (без shift/ctrl) открывает боковую панель (Escape или клик снаружи закрывает её) с:
    • Редактируемым заголовком — клик по заголовку для переименования.
    • Редактируемым исполнителем / приоритетом — клик по строке метаданных для изменения.
    • Редактируемым описанием — по умолчанию отрендерено как markdown (заголовки, полужирный, курсив, инлайн-код, блоки кода, ссылки http(s) / mailto:, маркированные списки), с кнопкой «edit», подменяющей это на текстовое поле. Рендеринг markdown — крошечный, безопасный от XSS рендерер: каждая подстановка выполняется на HTML-экранированном вводе, через него проходят только ссылки http(s) / mailto:, и всегда выставляются target="_blank" + rel="noopener noreferrer".
    • Редактором зависимостей — список чипов родителей и потомков, у каждого есть × для отвязывания, плюс выпадающие списки по всем остальным задачам для добавления нового родителя или потомка. Попытки создать цикл отклоняются на сервере с понятным сообщением.
    • Строкой действий со статусом (→ triage / → ready / → running / block / unblock / complete / archive) с запросом подтверждения для деструктивных переходов. Для карточек в колонке Triage строка также предоставляет два действия на основе LLM: ⚗ Decompose разворачивает задачу в граф дочерних задач, направленных специалистам-профилям по описанию, и ✨ Specify выполняет переписывание спецификации для одной задачи. Decompose откатывается к продвижению в стиле specify, когда LLM решает, что задаче не пойдёт на пользу разветвление, так что это строгое надмножество. Оба доступны из CLI (hermes kanban decompose <id> / specify <id> / --all), с любой платформы шлюза (/kanban decompose <id>) и программно через POST /api/plugins/kanban/tasks/:id/decompose и …/specify. Настройте модели под auxiliary.kanban_decomposer и auxiliary.triage_specifier в config.yaml.
    • Раздел результата (тоже отрендерен как markdown), ветка комментариев с отправкой по Enter, последние 20 событий.
  • Фильтры на панели инструментов — свободный поиск по тексту, выпадающий список арендаторов (по умолчанию dashboard.kanban.default_tenant из config.yaml), выпадающий список исполнителей, переключатель «show archived», переключатель «lanes by profile» и кнопка Nudge dispatcher, чтобы не ждать следующего тика в 60 с.

Визуально цель — привычная раскладка Linear / Fusion: тёмная тема, заголовки колонок со счётчиками, цветные индикаторы статуса, чипы-пилюли для приоритета и арендатора. Плагин использует только CSS-переменные темы (--color-*, --radius, --font-mono, …), поэтому автоматически перекрашивается под любую активную тему дашборда.

Автоматическая и ручная оркестрация

У доски kanban есть два способа обработать задачу, которую вы бросили в колонку Triage:

Auto (по умолчанию) — kanban.auto_decompose: true. Встроенный в шлюз диспетчер запускает декомпозер на каждом тике, ограниченный kanban.auto_decompose_per_tick (по умолчанию 3 задачи за тик), чтобы массовая загрузка triage-задач не расходовала вспомогательную LLM всплеском. Декомпозер использует встроенный промпт декомпозиции плюс путь модели auxiliary.kanban_decomposer, читает установленные у вас профили + их описания и просит LLM создать JSON-граф задач: какие задачи запустить, кому они назначены и какие от каких зависят. Исходная triage-задача становится родителем каждого листа графа, так что остаётся живой, пока весь граф не завершится, — а затем продвигается обратно в ready, чтобы её исполнитель (kanban.orchestrator_profile, иначе исполнитель, который у задачи уже был, иначе активный профиль по умолчанию) мог оценить завершение и добавить ещё задач, если работа не закончена. Это сценарий «бросил однострочник и ушёл».

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

Когда у новой задачи не указан тенант, при создании наследуется первый непустой тенант среди её родителей, в порядке передачи. Явный тенант — включая активный тенант исполнителя, который передают инструменты, — побеждает. Жёсткой границей изоляции по-прежнему остаются доски.

Manual — kanban.auto_decompose: false. Triage-задачи остаются в triage, пока вы не подействуете. Нажмите кнопку ⚗ Decompose на карточке, запустите hermes kanban decompose <id> (или --all), или используйте /kanban decompose <id> из чата. Это соответствует поведению доски до появления декомпозера, полезно, когда вам нужен полный контроль над тем, что и когда запускается.

Важная граница. Ручной режим отключает только встроенный декомпозер Triage. Он не мешает профилю вызывать kanban_create и не отключает пробуждения сессии-создателя. При kanban.auto_subscribe_on_create: true терминальное событие задачи будит исходного агента синтетическим ходом со статусом, чтобы он мог изучить передачу и решить, нужна ли действительно новая последующая работа. Установите auto_subscribe_on_create: false, если завершение задачи должно оставаться пассивным. Для отслеживания происхождения: у детей, созданных встроенным декомпозером, created_by=auto-decomposer; у задач, созданных пробуждённым профилем, вместо этого стоит имя этого профиля.

Переключайтесь между двумя режимами тумблером Orchestration: Auto/Manual наверху страницы kanban (изумрудный = Auto, приглушённо-серый = Manual) или редактируя config.yaml напрямую. Оба режима сосуществуют с hermes kanban specify — она по-прежнему доступна как переписывание спецификации для одной задачи, когда разветвление не нужно.

Решения декомпозера о маршрутизации зависят от описаний профилей — примитива разметки для каждого профиля, который вы задаёте командой hermes profile create --description "...", hermes profile describe <name> --text "...", hermes profile describe <name> --auto (LLM генерирует описание из установленных на профиле навыков + модели) или через редактор профилей в дашборде в развёрнутой панели Orchestration settings. Профили без описания всё равно появляются в списке — их можно маршрутизировать по имени, просто менее точно. Декомпозер НИКОГДА не назначает дочернюю задачу с assignee=None: когда LLM выбирает неизвестный профиль, дочерняя задача направляется на kanban.default_assignee, иначе на исполнителя корневой задачи (если это существующий профиль), иначе на активный профиль по умолчанию.

kanban.orchestrator_profile не загружает промпт, навыки или пользовательскую логику этого профиля в вызов декомпозиции. Он определяет, кто владеет корневой/оркестрационной задачей после разветвления. Чтобы изменить модель/провайдера декомпозера, настройте auxiliary.kanban_decomposer. Чтобы использовать пользовательскую логику разбиения задач конкретного профиля вместо встроенного декомпозера, переключитесь в ручной режим и пусть этот профиль сам создаёт или разбивает задачи.

Настройки конфигурации (все под kanban: в ~/.hermes/config.yaml):

КлючПо умолчаниюНазначение
auto_decomposetrueДиспетчер автоматически запускает встроенный декомпозер для triage-задач на каждом тике. Не ограничивает вызовы kanban_create от профилей и не ограничивает пробуждающие ходы у создателя.
auto_decompose_per_tick3Ограничение на число декомпозиций за тик диспетчера. Избыток переносится на следующий тик.
orchestrator_profile""Профиль, назначаемый на корневую/оркестрационную задачу после декомпозиции. Пусто = корневая задача сохраняет своего исполнителя, иначе активный профиль по умолчанию.
default_assignee""Куда попадает дочерняя задача, если LLM выбирает неизвестный профиль. Пусто = откат к исполнителю корневой задачи, иначе к активному профилю по умолчанию.
auto_subscribe_on_createtrueКогда kanban_create вызывается изнутри сессии с постоянным каналом доставки (шлюз обмена сообщениями или TUI), терминальные события будят исходного агента синтетическим ходом со статусом. Установите false для пассивного завершения или чтобы требовать явных вызовов kanban_notify-subscribe. Не зависит от auto_decompose.
notify_in_gatewaytrueОпрашивать и доставлять подписки Kanban из этого шлюза. Установите false на профилях, у которых нет своих подписок на уведомления, чтобы остановить холостой пятисекундный опрос нотификатора. Не зависит от dispatch_in_gateway; шлюзы без диспетчера всё равно могут владеть адаптерами доставки конкретного профиля.
done_sub_retention_days30Подписки на уведомления переживают done (безопасно для повторного открытия) и снимаются при archived. Сборщик мусора нотификатора удаляет подписки, чья задача пробыла в done или blocked без новых событий указанное число дней, ограничивая рост таблицы подписок на досках, которые никогда не архивируются. 0 отключает эту очистку.

И два слота вспомогательной LLM:

КлючНазначение
auxiliary.kanban_decomposerМодель, создающая граф задач (вызывается кнопкой Decompose). Задайте provider/model, чтобы переопределить основную модель чата.
auxiliary.profile_describerМодель, автоматически генерирующая описания профилей (вызывается hermes profile describe --auto).

Архитектура

GUI строго является слоем чтения через БД + записи через kanban_db без собственной доменной логики:

┌────────────────────────┐      WebSocket (отслеживает task_events)
│   React SPA (plugin)   │ ◀──────────────────────────────────┐
│   HTML5 drag-and-drop  │                                    │
└──────────┬─────────────┘                                    │
           │ REST через fetchJSON                             │
           ▼                                                  │
┌────────────────────────┐     записи вызывают kanban_db.*     │
│  FastAPI router        │     напрямую — тот же путь кода    │
│  plugins/kanban/       │     что используют команды CLI     │
│  dashboard/plugin_api.py                                    │
└──────────┬─────────────┘                                    │
           │                                                  │
           ▼                                                  │
┌────────────────────────┐                                    │
│  ~/.hermes/kanban.db   │ ───── добавляет task_events ───────┘
│  (WAL, общая)          │
└────────────────────────┘

REST-поверхность

Все маршруты смонтированы под /api/plugins/kanban/ и защищены эфемерным токеном сессии дашборда:

МетодПутьНазначение
GET/board?tenant=<name>&include_archived=…Вся доска, сгруппированная по колонкам статуса, плюс арендаторы + исполнители для выпадающих фильтров
GET/tasks/:idЗадача + комментарии + события + связи
POST/tasksСоздать (оборачивает kanban_db.create_task, принимает triage: bool и parents: [id, …])
PATCH/tasks/:idСтатус / исполнитель / приоритет / заголовок / тело / результат
POST/tasks/bulkПрименить один и тот же патч (статус / архивация / исполнитель / приоритет) ко всем id в ids. Сбои по отдельным id сообщаются без прерывания обработки остальных
POST/tasks/:id/commentsДобавить комментарий
POST/tasks/:id/specifyЗапустить triage-спецификатор — вспомогательная LLM дорабатывает тело задачи и переводит её из triage в todo. Возвращает {ok, task_id, reason, new_title}; ok=false с человекочитаемой причиной для случаев «не в triage» / нет вспомогательного клиента / ошибка LLM — это код 200, а не 4xx
POST/tasks/:id/decomposeЗапустить декомпозер kanban — вспомогательная LLM создаёт граф задач, а хелпер атомарно создаёт потомков + связывает корень + переводит triage → todo. Возвращает {ok, task_id, reason, fanout, child_ids, new_title}. Та же конвенция «200 при ошибке LLM», что и у /specify.
GET/profilesСписок установленных профилей с их описаниями (используется редактором описаний профилей дашборда и селектором оркестратора).
PATCH/profiles/:nameЗадать или очистить описание профиля (авторства пользователя — description_auto: false). Возвращает {ok, profile, description}.
POST/profiles/:name/describe-autoСгенерировать описание профиля через auxiliary.profile_describer. Сохраняется с description_auto: true, чтобы дашборд мог показать значок «review».
GET/orchestrationПрочитать настройки оркестрации kanban (orchestrator_profile, default_assignee, auto_decompose) плюс разрешённые эффективные значения после откатов.
PUT/orchestrationОбновить один или несколько из трёх ключей оркестрации в config.yaml. Проверяет, что непустые имена профилей реально существуют.
POST/linksДобавить зависимость (parent_id → child_id)
DELETE/links?parent_id=…&child_id=…Удалить зависимость
POST/dispatch?max=…&dry_run=…Подтолкнуть диспетчер — пропустить ожидание 60 с
GET/configПрочитать настройки dashboard.kanban из config.yaml — default_tenant, lane_by_profile, include_archived_by_default, render_markdown
WS/events?since=<event_id>Живой поток строк task_events. Без since поток стартует с текущего хвоста доски (прошлое уже есть в снимке /board); передайте since=<latest_event_id>, чтобы догнать события с этого места, или since=0, чтобы воспроизвести историю

Каждый обработчик — тонкая обёртка: плагин состоит примерно из 700 строк Python (роутер + отслеживание WebSocket + пакетный обработчик + чтение конфигурации) и не добавляет новой бизнес-логики. Крошечный хелпер _conn() автоматически инициализирует kanban.db при каждом чтении и записи, так что свежая установка работает независимо от того, открыл ли пользователь сначала дашборд, обратился напрямую к REST API или запустил hermes kanban init.

Конфигурация дашборда

Любой из этих ключей под dashboard.kanban в ~/.hermes/config.yaml меняет значения вкладки по умолчанию — плагин читает их при загрузке через GET /config:

dashboard:
  kanban:
    default_tenant: acme              # предвыбор фильтра арендатора
    lane_by_profile: true             # значение по умолчанию для переключателя "lanes by profile"
    include_archived_by_default: false
    render_markdown: true             # установите false для простого рендеринга в <pre>

Каждый ключ опционален и по умолчанию принимает показанное значение.

Модель безопасности

Middleware HTTP-аутентификации дашборда явно пропускает /api/plugins/ — маршруты плагинов не аутентифицированы намеренно, потому что дашборд по умолчанию слушает только localhost. Это значит, что REST-поверхность kanban доступна с любого процесса на хосте.

WebSocket требует одного дополнительного шага: он требует эфемерный токен сессии дашборда как параметр запроса ?token=… (браузеры не могут выставить Authorization при запросе на апгрейд), по той же схеме, что используется для встроенного в браузер моста PTY.

Если вы запускаете hermes dashboard --host 0.0.0.0, каждый маршрут плагина — включая kanban — становится доступен из сети. Не делайте этого на общем хосте. Доска содержит тела задач, комментарии и пути рабочих пространств; злоумышленник, добравшийся до этих маршрутов, получает доступ на чтение ко всей вашей поверхности совместной работы, а также может создавать / переназначать / архивировать задачи.

Задачи в ~/.hermes/kanban.db намеренно не привязаны к профилю (это и есть примитив координации). Если вы открываете дашборд через hermes -p <profile> dashboard, доска всё равно покажет задачи, созданные любым другим профилем на хосте. Один и тот же пользователь владеет всеми профилями, но об этом стоит знать, если на хосте сосуществует несколько персон.

Живые обновления

task_events — это доступная только для добавления таблица SQLite с монотонным id. Конечная точка WebSocket хранит последний увиденный клиентом id события и отправляет новые строки по мере их появления. Когда прибывает всплеск событий, фронтенд перезагружает (очень дешёвую) конечную точку доски — это проще и правильнее, чем пытаться подправить локальное состояние по каждому типу события. Режим WAL означает, что цикл чтения никогда не блокирует транзакции захвата BEGIN IMMEDIATE диспетчера.

Расширение плагина

Плагин использует стандартный контракт плагинов дашборда Hermes — см. Расширение дашборда для полного справочника по манифесту, слотам оболочки, слотам на уровне страниц и Plugin SDK. Дополнительные колонки, кастомное оформление карточек, макеты с фильтром по арендатору или полная замена tab.override — всё это выразимо без форка этого плагина.

Чтобы отключить, не удаляя: добавьте dashboard.plugins.kanban.enabled: false в config.yaml (или удалите plugins/kanban/dashboard/manifest.json).

Граница возможностей

GUI намеренно тонкий. Всё, что делает плагин, доступно из CLI; плагин просто делает это удобным для людей. Автоматическое назначение, бюджеты, шлюзы согласования и организационные диаграммы остаются на стороне пользователя — профиль-роутер, другой плагин или переиспользование tools/approval.py — именно так, как перечислено в разделе «вне рамок» спецификации дизайна.

Справочник команд CLI

Это поверхность, которой пользуетесь вы (или скрипты, cron, дашборд) для управления доской. Воркеры, работающие внутри диспетчера, используют поверхность инструментов kanban_* для тех же операций — CLI здесь и инструменты там оба проходят через kanban_db, так что две поверхности согласуются по построению.

hermes kanban init                                     # создать kanban.db + вывести подсказку по демону
hermes kanban create "<title>" [--body ...] [--assignee <profile>]
                                [--parent <id>]... [--tenant <name>]
                                [--workspace scratch|worktree|worktree:<path>|dir:<path>]
                                [--branch <name>]
                                [--priority N] [--triage] [--idempotency-key KEY]
                                [--max-runtime 30m|2h|1d|<seconds>]
                                [--max-retries N]
                                [--goal] [--goal-max-turns N]
                                [--skill <name>]...
                                [--json]
hermes kanban list [--mine] [--assignee P] [--status S] [--tenant T] [--archived]
        [--workflow-template-id <id>] [--current-step-key <key>]
        [--sort completed-desc|created|created-desc|priority|priority-desc|status|assignee|title|updated]
        [--json]
hermes kanban show <id> [--json]
hermes kanban assign <id> <profile>                    # или 'none' для снятия назначения
hermes kanban reassign <id>... <profile>               # массово переназначить задачи на профиль
hermes kanban edit <id> [--title ...] [--body ...]     # изменить заголовок / тело / приоритет задачи на месте
        [--priority N]
hermes kanban promote <id>...                          # перевести задачи todo/blocked в ready (восстановление)
hermes kanban schedule <id> --at <ISO8601>             # задать/очистить время старта задачи
hermes kanban diagnostics [--json]                     # снимок состояния доски (алиас: diag)
hermes kanban link <parent_id> <child_id>
hermes kanban unlink <parent_id> <child_id>
hermes kanban claim <id> [--ttl SECONDS]
hermes kanban comment <id> "<text>" [--author NAME]

# Массовые команды — принимают несколько id:
hermes kanban complete <id>... [--result "..."] [--force]
hermes kanban block <id> "<reason>" [--ids <id>...]
hermes kanban unblock <id>...
hermes kanban archive <id>...

hermes kanban request-review <id> [--summary "..."] [--metadata JSON] [--reviewer PROFILE]
hermes kanban request-changes <id> "<required changes>"               # активный ревьюер -> исполнитель
hermes kanban reopen-review  <id>... [--reason "..."]                 # запрошены правки: 'review' -> ready/todo

hermes kanban tail <id>                                # следить за потоком событий одной задачи
hermes kanban watch [--assignee P] [--tenant T]        # живой поток ВСЕХ событий в терминал
        [--kinds completed,blocked,…] [--interval SECS]
hermes kanban heartbeat <id> [--note "..."]            # сигнал жизнеспособности воркера для длительных операций
hermes kanban runs <id> [--json]                       # история попыток (одна строка на запуск)
hermes kanban assignees [--json]                       # профили на диске + счётчики задач по исполнителю
hermes kanban dispatch [--dry-run] [--max N]           # один разовый проход
        [--failure-limit N] [--json]
hermes kanban daemon --force                           # УСТАРЕЛО — автономный диспетчер (используйте `hermes gateway start` вместо этого)
        [--failure-limit N] [--pidfile PATH] [-v]
hermes kanban stats [--json]                           # счётчики по статусу + по исполнителю
hermes kanban log <id> [--tail BYTES]                  # лог воркера из ~/.hermes/kanban/logs/
hermes kanban notify-subscribe <id>                    # хук моста шлюза (используется /kanban в шлюзе)
        --platform <name> --chat-id <id> [--thread-id <id>] [--user-id <id>]
        [--chat-type dm|group|channel|thread] [--delivery-mode notify|notify+wake|wake]
hermes kanban notify-list [<id>] [--json]
hermes kanban notify-unsubscribe <id>
        --platform <name> --chat-id <id> [--thread-id <id>]
hermes kanban context <id>                             # что видит воркер
hermes kanban specify [<id> | --all] [--tenant T]      # доработать идею из колонки triage
        [--author NAME] [--json]                       #   в полную спецификацию и перевести в todo
hermes kanban gc [--event-retention-days N]            # рабочие пространства + старые события + старые логи
        [--log-retention-days N]                       #   (negative N is rejected; 0 disables that sweep)

Все команды также доступны как слэш-команда в интерактивном CLI и в шлюзе обмена сообщениями (см. слэш-команду /kanban ниже).

--max-retries — это переопределение автоматического предохранителя для конкретной задачи на уровне диспетчера. --max-retries 1 блокирует задачу при первой же неуспешной попытке, а --max-retries 3 разрешает два повтора и блокирует при третьей неудаче. Опустите его, чтобы использовать kanban.failure_limit из config.yaml, а затем встроенное значение по умолчанию.

Настройки конкурентности, планирования и продвижения потомков

Ключ конфигурацииПо умолчаниюЧто делает
kanban.max_in_progressне задан (без ограничений)Ограничивает число одновременно выполняющихся задач. Когда на доске уже N запущено, диспетчер пропускает запуск новых — полезно для медленных воркеров (локальные LLM, ограниченные по ресурсам хосты), чтобы они успели закончить имеющееся, прежде чем накопится очередь и начнутся таймауты. Некорректные значения или значения ниже 1 логируют предупреждение и ведут себя как «без ограничений».
kanban.max_in_progress_per_profileне задан (без ограничений)Вариант max_in_progress на уровне профиля — ограничивает, сколько задач может одновременно выполнять один исполнитель-профиль. Полезно, когда один профиль медленный или ограничен по частоте запросов, а остальные должны продолжать работать. Применяется вместе с общеплатформенным max_in_progress; для запуска должны выполняться оба ограничения.
kanban.dispatch_profilesне задан (любой существующий профиль)Список разрешённых исполнителей для конкретного дома на досках, общих для нескольких домов Hermes. Если ключ присутствует, диспетчер этого дома захватывает только карточки с перечисленными исполнителями — fail-closed: пустой список, null или голый dispatch_profiles: не захватывают ничего, а неудачное чтение конфига пишет предупреждение и тоже ничего не захватывает; остальные исполнители попадают в skipped_nonspawnable. «Любой существующий профиль» означает только отсутствие ключа. hermes kanban diagnostics печатает итоговое значение для этого дома (any, перечисленные имена или none (fail-closed: …)). См. Общие доски между домами.
kanban.auto_promote_childrentrueПосле того как decompose_triage_task() создаёт потомков без блокирующих зависимостей от родителя, они автоматически продвигаются в ready, чтобы диспетчер мог их подхватить. Установите false, чтобы требовать ручной проверки — потомки остаются в todo, пока вы их не продвинете.
kanban.default_workdirне заданРабочий каталог по умолчанию на уровне доски, применяемый к новым задачам, когда ни --workspace, ни сама задача его не переопределяют. workspace: на уровне задачи по-прежнему имеет приоритет.
kanban:
  max_in_progress: 2
  auto_promote_children: false
  default_workdir: ~/work/active-project

Плановый старт задач (scheduled_at)

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

hermes kanban create "nightly backup audit" \
  --assignee ops --scheduled-at "2026-06-01T03:00:00Z"

Защита от повторного запуска

Диспетчер отказывается перезапускать готовую задачу, если на предыдущем запуске возникла ошибка квоты/аутентификации/429 (blocker_auth), либо запуск успешно завершился в пределах защитного окна (recent_success), либо недавний комментарий к задаче ссылается на GitHub PR (active_pr). Два кулдауна придерживают карточку, никогда не засчитываясь против неё: rate_limit_cooldown после возврата в очередь из-за стены квоты и infrastructure_cooldown после того, как хост отказался разместить воркера (нет scope systemd, переживающего перезапуск, — см. Воркеры и cgroup systemd); оба используют окно HERMES_KANBAN_RATE_LIMIT_COOLDOWN_SECONDS (по умолчанию 300 с). Это предотвращает повторяющиеся штормы воркеров на одном и том же баге или задаче, пока человек не разберётся. См. строку respawn_guarded в справочнике событий.

Чтобы понять, почему готовая карточка не запускается, выполните hermes kanban dispatch --dry-run — вывод перечисляет Guarded (<reason>): <task id> по каждой удержанной карточке (а с --json — ещё и respawn_guarded, rate_limited, skipped_locked, memory_pressure). Предупреждение «dispatcher stuck» у шлюза и у автономного демона тоже называет, что придержал последний тик, например Last tick held back: active_pr=1.

recent_success и active_pr удерживают только полосу ready — это входные данные для передачи на ревью, а не сигналы против неё. Чтобы ревьюер, закрывающий или другой восстанавливающий профиль подхватил карточку с уже открытым PR, либо переведите её в полосу ревью командой hermes kanban request-review <id> (принимается и из ready, и из running; запуск в полосе ревью ни одной из этих защит не подчиняется), либо передайте готовую карточку этому профилю через hermes kanban assign <id> <profile>: передача, записанная после комментария с PR — переназначение оператором, вердикт ревьюера «нужны изменения» или повторное открытие ревью, — снимает active_pr для профиля, который теперь указан на карточке, потому что именно над этим PR ему и предстоит работать. Засчитывается только смена на другой профиль: повторное назначение того же профиля, снятие назначения или автоподстановка диспетчером kanban.default_assignee защиту не снимают, так что исполнитель, открывший PR, после сбоя, переоформления или пустого переназначения против него не перезапускается, а более новый комментарий с PR, оставленный после передачи, включает защиту снова. Намеренный возврат в очередь после успеха (перетаскивание done→ready, unblock, повторное продвижение) тоже снимает recent_success, так что ручной перезапуск никогда не удерживается молча на всё окно.

Перетаскивание для удаления и массовое удаление (дашборд)

Дашборд предоставляет зону сброса в корзину на странице kanban — перетащите туда любую карточку, чтобы удалить задачу (каскадом удаляются task_events, связи потомков и подписки). Запрос подтверждения защищает от случайностей. Массовое удаление также доступно через DELETE /api/plugins/kanban/tasks с телом JSON {"ids": ["t_abc", "t_def", ...]}.

Конечные точки видимости воркеров

API плагина дашборда теперь предоставляет эти конечные точки только для чтения (плюс команду управления запуском) для внешних мониторов:

Конечная точкаВозвращает
GET /api/plugins/kanban/workers/activeТекущие запущенные воркеры с PID, профилем, id задачи, временем старта, последним heartbeat
GET /api/plugins/kanban/runs/{id}Детали одного запуска — id задачи, статус, время старта/окончания, код выхода, путь к логу
POST /api/plugins/kanban/runs/{run_id}/terminateЗавершить переоформляемый запуск — останавливает воркера и освобождает задачу для повторного запуска
GET /api/plugins/kanban/inspectКомбинированный снимок диспетчера — очередь, число выполняющихся задач относительно max_in_progress, недавние события

Все они защищены той же аутентификацией плагина дашборда, что и остальной API плагина kanban.

Помощник по топологии Kanban Swarm

hermes kanban swarm за один шаг создаёт устойчивый граф Kanban Swarm v1: завершённую корневую карточку/«доску объявлений», N параллельных карточек воркеров, карточку верификатора, зависящую от всех воркеров, и карточку синтезатора, зависящую от верификатора. Общий контекст роя («доска объявлений») хранится как структурированные JSON-комментарии на корневой карточке, так что любой воркер может его прочитать.

hermes kanban swarm "Design a multi-region failover plan" \
  --workers researcher,architect,sre \
  --verifier reviewer --synthesizer writer

Полученный граф фиксируется атомарно: диспетчеры и читатели дашборда видят либо отсутствие нового роя, либо полную топологию, но никогда — частично связанный граф корень/воркеры/верификатор. Дальше он диспетчеризуется как обычно — воркеры работают параллельно, верификатор просыпается после того, как все они закончат, а синтезатор просыпается после того, как верификатор пометит работу как чистую.

Слэш-команда /kanban

Любая команда hermes kanban <action> также доступна как /kanban <action> — изнутри интерактивной сессии hermes chat и с любой платформы шлюза (Telegram, Discord, Slack, WhatsApp, Signal, Matrix, Mattermost, email, SMS). Оба интерфейса вызывают одну и ту же точку входа hermes_cli.kanban.run_slash(), которая переиспользует дерево argparse команды hermes kanban, так что поверхность аргументов, флаги и формат вывода идентичны для CLI, /kanban и hermes kanban. Не нужно покидать чат, чтобы управлять доской.

/kanban list
/kanban show t_abcd
/kanban create "write launch post" --assignee writer --parent t_research
/kanban comment t_abcd "looks good, ship it"
/kanban unblock t_abcd
/kanban dispatch --max 3
/kanban specify t_abcd                  # доработать triage-однострочник в полную спецификацию
/kanban specify --all --tenant engineering  # пройтись по всем triage-задачам в одном арендаторе

Заключайте многословные аргументы в кавычки так же, как в shell — run_slash разбирает остаток строки через shlex.split, так что работают и "...", и '...'.

Использование во время выполнения: /kanban обходит защиту от параллельного запуска

Шлюз обычно ставит в очередь слэш-команды и сообщения пользователя, пока агент ещё обдумывает ответ, — именно это не даёт случайно начать второй ход, пока первый ещё выполняется. /kanban явно исключена из этой защиты. Доска живёт в ~/.hermes/kanban.db, а не в состоянии работающего агента, так что и чтения (list, show, context, tail, watch, stats, runs), и записи (comment, unblock, block, assign, archive, create, link, …) проходят немедленно, даже посреди хода.

В этом и весь смысл разделения:

  • Воркер блокируется, ожидая коллегу → вы отправляете /kanban unblock t_abcd со своего телефона, и диспетчер подхватывает коллегу на следующем тике. Заблокированный воркер не прерывается — он просто перестаёт быть заблокированным.
  • Вы замечаете карточку, которой нужен контекст от человека → /kanban comment t_xyz "use the 2026 schema, not 2025" попадает в ветку задачи, и следующий запуск этой задачи прочитает его в kanban_show().
  • Вы хотите узнать, чем занят ваш флот, не останавливая оркестратора → /kanban list --mine или /kanban stats осматривает доску, не затрагивая ваш основной разговор.

Автоподписка при /kanban create (только шлюз)

Когда вы создаёте задачу из шлюза командой /kanban create "…", исходный чат (платформа + id чата + id ветки) автоматически подписывается на терминальные события этой задачи (completed, blocked, gave_up, crashed, timed_out). Вы получите по одному сообщению на терминальное событие — включая первую строку сводки результата воркера при completed — без необходимости опрашивать или запоминать id задачи.

you> /kanban create "transcribe today's podcast" --assignee transcriber
bot> Created t_9fc1a3  (ready, assignee=transcriber)
     (subscribed — you'll be notified when t_9fc1a3 completes or blocks)

… ~8 минут спустя …

bot> ✓ t_9fc1a3 completed by transcriber
     transcribed 42 minutes, saved to podcast/2026-05-04.md

Подписки переживают достижение задачей done — завершение обратимо (ревьюер или контроллер может снова открыть завершённую задачу), так что исходная сессия продолжает получать уведомления через циклы повторного открытия. Автоматически они снимаются только при archived (необратимом финальном состоянии). На досках, которые никогда не архивируются, очистка сборщиком мусора удаляет подписки для задач, пробывших в done или blocked без новой активности kanban.done_sub_retention_days дней (по умолчанию 30; установите 0, чтобы отключить), так что устаревшие строки не копятся бесконечно. Если вы скриптуете создание с --json (машинный вывод), автоподписка пропускается — предполагается, что скриптованным вызывающим нужно управлять подписками явно через /kanban notify-subscribe.

Исполнители диспетчера, создающие задачи через kanban_create или hermes kanban create, копируют устойчивые подписки на уведомления от своей задачи — даже без зависимостей parents. Адресаты, якоря маршрутов и режимы доставки сохраняются, а пассивную подписку автоподписка до пробуждения не повышает. Копирование существующих подписок не зависит от auto_subscribe_on_create, который управляет лишь добавлением текущего разговора новым адресатом. Ни для голой сессии CLI, ни для исполнителя, у чьей задачи подписок нет, адресат не выдумывается.

У kanban_create происхождение сессии разрешается в таком порядке: явный session_id, устойчивая сессия задачи-владельца исполнителя, происхождение API в пределах запроса, затем текущая сессия. Id-кандидат проставляется, только если у него есть строка в таблице sessions в state.db активного профиля; id, который никуда не разрешается (сессия, чья строка так и не была записана, или id, унаследованный от другого процесса), оставляет session_id равным NULL, а не указывает на сессию, которую ни одна база разрешить не может. Встроенная декомпозиция тоже наследует устойчивую сессию своего корня. Само происхождение сессии адресатом уведомлений не является: смена session_id существующие подписки не заменяет — куда доставлять события, меняют notify-subscribe и notify-unsubscribe.

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

Обрезка вывода в мессенджерах

У платформ шлюза есть практические ограничения на длину сообщения. Если /kanban list, /kanban show или /kanban tail производят вывод длиннее примерно 3800 символов, ответ обрезается с подписью … (truncated; use \hermes kanban …` in your terminal for full output)`. У поверхности CLI такого ограничения нет.

Автодополнение

В интерактивном CLI ввод /kanban и нажатие Tab прокручивает встроенный список подкоманд (list, ls, show, create, assign, link, unlink, claim, comment, complete, block, unblock, archive, tail, dispatch, context, init, gc). Остальные команды, перечисленные в справочнике CLI выше (watch, stats, runs, log, assignees, heartbeat, notify-subscribe, notify-list, notify-unsubscribe, daemon), тоже работают — они просто пока не в подсказке автодополнения.

Паттерны совместной работы

Доска поддерживает эти восемь паттернов без каких-либо новых примитивов:

ПаттернФормаПример
P1 РазветвлениеN собратьев, одна роль«исследовать 5 направлений параллельно»
P2 Конвейерцепочка ролей: скаут → редактор → авторсборка ежедневной сводки
P3 Голосование / кворумN собратьев + 1 агрегатор3 исследователя → 1 ревьюер выбирает
P4 Долгоживущий журналтот же профиль + общий каталог + cronхранилище Obsidian
P5 Человек в циклеворкер блокируется → пользователь комментирует → разблокировканеоднозначные решения
P6 @упоминаниевстроенная маршрутизация из прозы@reviewer look at this
P7 Рабочее пространство в рамках треда/kanban here в тредетреды шлюза по проектам
P8 Фермерство на флотеодин профиль, N объектов50 соцаккаунтов
P9 Triage-спецификаторсырая идея → triage → hermes kanban specify дорабатывает тело → todo«превратить этот однострочник в задачу со спецификацией»

Передача контекста последующим карточкам (родительская связь)

Родительская связь — не просто шлюз планирования, а канал передачи контекста от завершённой карточки к новой. Когда вы создаёте карточку с --parent <done-card-id>, происходит две вещи:

  1. Она сразу же готова к работе. create_task задаёт статус по состоянию родителей: потомок, у которого все родители done, создаётся сразу в ready — без ожидания, без ручного продвижения. (Потомки ещё открытых родителей ждут в todo, пока recompute_ready не продвинет их, когда завершится последний родитель.)
  2. Передача от родителя едет вместе с ней. Контекст воркера, собранный для потомка (build_worker_context, то, что возвращает kanban_show()), содержит раздел ## Parent task results со summary и metadata завершения каждого родителя, дословно:
## Parent task results
### t_77c26979 (completed just now)
Added exponential backoff with jitter to the retry helper.
_metadata_: `{"changed_files": ["hermes_cli/retry.py", "tests/test_retry.py"], "decisions": ["capped backoff at 60s", "jitter = full"]}`

Именно поэтому для последующей работы над завершённой карточкой правильный паттерн — новая дочерняя карточка, а не повторное открытие завершённой. Завершённые карточки — неизменяемая история; их контекст течёт вперёд через родительскую связь. Доработка в рамках той же карточки (циклы повтора для проваленной карточки) — другой механизм: предыдущие попытки на той же карточке попадают в её собственный контекст как «предыдущие попытки».

Одного worktree или ветки недостаточно: состояние репозитория говорит последующему воркеру, как выглядит код, но не почему — решения, прогнанные тесты и затронутые файлы живут в структурированной передаче родителя, а не в git. Свидетельства, которых не существовало на момент завершения родителя (например, лог CI, упавший позже), должны идти в тело новой карточки.

# Карточка реализации t_impl завершена. CI падает два часа спустя.
hermes kanban create "Fix CI failure from t_impl: test_retry flakes on 3.11" \
    --assignee coder \
    --parent t_impl \
    --body "$(cat <<'EOF'
CI run #4812 failed after t_impl merged.
Log excerpt: FAILED tests/test_retry.py::test_backoff_jitter - TimeoutError
Acceptance: tests/test_retry.py green on 3.11 and 3.12 in CI.
Use a fresh worktree/branch; do not force-push the original branch.
EOF
)"

Воркер, устраняющий проблему, запускается уже с сводкой и метаданными исходной карточки в контексте (изменённые файлы, решения), плюс свежие свидетельства, которые вы вписали в тело.

Согласование конфликтующих веток воркеров

В инженерных конвейерах (P1/P2 с worktree) ветки двух воркеров при слиянии могут конфликтовать. Не позволяйте ни одному из воркеров разрешать это самостоятельно — у конфликтующего агента нет контекста коллеги, и он надёжно либо перезаписывает чужую сторону, либо бросает свою. Вместо этого создайте карточку согласования, назначенную на третий, нейтральный профиль, с обеими конфликтующими карточками в родителях: родительские связи приносят в контекст согласователя сводки завершения обеих сторон, так что он получает и оба диффа, и оба намерения. Встроенный опциональный навык agent-merge-conflict-arbiter даёт этому воркеру полную процедуру: классифицировать каждый конфликтующий фрагмент, разрешить его беспристрастно, проверить и вернуть сводку с перечислением каждого решения.

Горячие точки коллизий в параллельных кампаниях

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

hotspot: hermes_cli/kanban_db.py — third conflicting edit to the dispatch loop this wave

и повторяет флаг в metadata завершения. Оркестраторы (или люди, просматривающие доску), увидевшие два или больше комментария hotspot:, называющих один и тот же путь, должны создать отдельную карточку рефакторинга/декомпозиции для этого файла до того, как ставить в очередь ещё работу, которая его затрагивает, — разбить файл-магнит дешевле, чем разбирать каждую будущую коллизию, которую он вызовет. Для конфликтов, которые уже произошли, используйте паттерн карточки согласования выше с опциональным навыком agent-merge-conflict-arbiter; флагирование горячих точек — это апстримная мера, которая не даёт согласователю превратиться в постоянную полосу конвейера.

Мультиарендная эксплуатация

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

hermes kanban create "monthly report" \
    --assignee researcher \
    --tenant business-a \
    --workspace dir:~/tenants/business-a/data/

Воркеры получают $HERMES_TENANT и распределяют записи памяти по пространствам имён с этим префиксом. Доска, диспетчер и определения профилей общие; только данные ограничены по области.

Десктопные уведомления

Плагин Kanban в десктопном приложении показывает те же терминальные события нативно — платформа шлюза не нужна. Пока живое сокет-соединение событий доски Kanban подключено, каждое событие completed, blocked, gave_up, crashed, timed_out или направление в triage (block_loop_detected) поднимает всплывающее уведомление в приложении с передачей от воркера (сводкой, причиной блокировки или ошибкой) и действием «Open Kanban». Когда вы не за окном Hermes, то же событие также запускает нативное уведомление ОС (управляется настройкой Settings ▸ Notifications ▸ Plugin notifications), так что задача, упёршаяся в блокер, пока вы в другом приложении, всё равно до вас дойдёт.

Окно покрытия: десктопные уведомления едут по живому потоку событий, поэтому срабатывают только пока приложение запущено с включённым плагином Kanban. События, произошедшие, пока приложение закрыто, не воспроизводятся как уведомления при следующем запуске — для доставки, которая должна пережить закрытие приложения, используйте подписку через шлюз (ниже).

Уведомления шлюза

Когда вы запускаете /kanban create … из шлюза (Telegram, Discord, Slack и т. д.), исходный чат автоматически подписывается на новую задачу. Фоновый нотификатор шлюза опрашивает task_events каждые несколько секунд и доставляет по одному сообщению на терминальное событие (completed, blocked, gave_up, crashed, timed_out) в этот чат. Завершённые задачи также отправляют первую строку --result воркера, так что вы видите результат без необходимости вызывать /kanban show.

Подписками можно управлять явно из CLI — полезно, когда скрипт / задание cron хочет уведомить чат, из которого он не был запущен:

hermes kanban notify-subscribe t_abcd \
    --platform telegram --chat-id 12345678 --thread-id 7 \
    --chat-type group --delivery-mode notify+wake
hermes kanban notify-list
hermes kanban notify-unsubscribe t_abcd \
    --platform telegram --chat-id 12345678 --thread-id 7

Подписка снимается автоматически, как только задача достигает done или archived; никакой очистки не требуется.

Треды Discord при profile_routes: когда шлюз мультиплексирует профили и маршрутизирует канал на профиль, подписке, созданной из CLI для треда в этом канале, нужны якоря маршрута треда, иначе нотификатор не сможет сопоставить её ни с одним маршрутом и пропустит (одна запись в лог с уровнем WARNING). Передайте их явно — --parent-chat-id <channel id> и, для Discord, --guild-id <guild id>:

hermes kanban notify-subscribe t_abcd \
    --platform discord --chat-id <thread id> --thread-id <thread id> --chat-type thread \
    --parent-chat-id <channel id> --guild-id <guild id> --delivery-mode notify+wake

Подписки, созданные изнутри чата (/kanban create, kanban_create), записывают эти якоря автоматически.

Режимы доставки

--delivery-mode управляет тем, как нотификатор реагирует на терминальное событие. Каждая подписка находится в одном из трёх режимов (notify — режим по умолчанию и исходное поведение):

РежимПассивное сообщениеБудит агентаКогда использовать
notifyданетНужен просто сигнал в чат (по умолчанию).
notify+wakeдадаНужно ещё, чтобы агент-адресат совершил настоящий ход — прочитал контекст доски и ответил своим голосом. Автоподписки из чата используют этот режим.
wakeнетдаНужно только, чтобы агент отреагировал на событие, без отдельного пинга.

У notify+wake доставка считается завершённой только тогда, когда пробуждение принято в очередь ходов адаптера, а пассивный пинг отправлен. Пропавшие обработчики, отклонённые маршруты и переполненные очереди повторяются на следующих тиках нотификатора, и подписка при этом не истекает. Отправленные пинги отмечаются в SQLite отдельно, поэтому отклонённое пробуждение уже отмеченный пинг не повторяет. Режим notify остаётся пассивным и ход не начинает никогда. Приём не гарантирует ни выполнения моделью, ни успешного ответа: обычные проверки перед ходом действуют. Доставкой «ровно один раз» это не является: падение процесса между отправкой и отметкой способно повторить пинг, а прежний курсор «заявка до доставки» не является очередью, восстанавливаемой после падения.

«Пробуждение» подделывает входящее сообщение для агента-адресата на шлюзе, так что он совершает обычный ход (читает комментарий + результат, рассуждает, отвечает) вместо получения однострочного пассивного уведомления. Оно срабатывает только когда нотификатор работает внутри живого процесса шлюза; иначе подписка notify+wake всё равно доставляет пассивное сообщение, а подписка только с wake в этом процессе не делает ничего.

Какие события будят. Те, что возвращают исход задачи или требуют внимания оркестрации: completed, blocked, gave_up, crashed, timed_out, review_requested (воркер закончил реализацию и передал дальше через kanban_request_review) и block_loop_detected (задача направлена в triage после повторных блокировок). status, archived и unblocked доставляются, но никогда не будят — это учётные переходы, а не сигналы, требующие внимания. Когда событие completed или review_requested несёт сводку, эта передача едет вместе с пробуждающим ходом, так что разбуженный агент видит, что воркер сделал на самом деле.

--chat-type (dm | group | channel | thread) фиксирует тип исходного чата, чтобы разбуженный ход разрешался в правильную сессию оператора: build_session_key формирует ключи для групп, каналов и тредов иначе, чем для личных чатов, так что неточный chat_type увёл бы пробуждение в отдельную сессию без контекста. Автоподписка /kanban и пути слэш-команд фиксируют это автоматически — вручную задавать значение нужно только при подписке чата из скрипта или cron. Опустите его, чтобы оставить существующую подписку без изменений (у новых подписок значение по умолчанию — dm).

Настройки с несколькими профилями: доставка принадлежит профилю

В развёртывании с одним шлюзом на профиль (один диспетчер, отдельные процессы шлюза для writer, admin и т. д. — см. руководство по мультишлюзу), у диспетчеризации и доставки разные владельцы:

  • Диспетчеризация остаётся у одного владельца. Ровно один шлюз держит kanban.dispatch_in_gateway: true и запускает диспетчер; у всех остальных шлюзов это значение — false.
  • Доставка уведомлений принадлежит профилю. Каждый шлюз — включая те, что не диспетчеризуют, — запускает нотификатор и опрашивает только подписки, помеченные профилем, чьи адаптеры платформ он размещает. Задача, созданная от профиля writer в Telegram, доставляет своё сообщение completed/blocked шлюзом writer, даже если диспетчеризацией занимался шлюз default.
  • Профили мультиплекса, закреплённые через gateway.profile_routes, могут пользоваться основным адаптером, когда сохранённые в подписке якоря — платформа, чат, тред, область и родительский канал — разрешаются через gateway.profile_routes ровно в этот обслуживаемый профиль, а своего адаптера для платформы подписки у профиля нет. Подключённый вторичный адаптер для этой платформы остаётся авторитетным; адаптеры, которые профиль держит на других платформах, доставку не блокируют (общий бот — единственные учётные данные, обслуживающие закреплённый чат, и для входящих ходов, и для уведомлений). Несовпавшие, переназначенные, отключённые и неоднозначные маршруты остаются недоставленными и годными для повтора. Старые строки без нужных якорей маршрутизации в профиль не угадываются. Ходы пробуждения сохраняют область выполнения профиля-адресата и разрешённый транспорт.
  • Подписки без состояния (api_server) несут сырой id сессии, который никакая запись profile_routes закрепить не может (у сессии нет различителя чата/треда/гильдии). Обслуживаемый профиль авторизован ровно тогда, когда сессия подписки лежит в собственном state.db этого профиля: общий слушатель зеркалит /p/<profile>/, поэтому доказательством владения служит хранилище сессий, а не платформа. Пробуждение затем выполняется в области этого профиля и возобновляет ровно эту сессию внутри процесса, так что ни второй слушатель, ни вторичный API_SERVER_KEY не участвуют. Неизвестная сессия, сессия с пометкой другого профиля или принадлежащая необслуживаемому профилю остаётся недоставленной и годной для повтора, а собственные подписки api_server профиля по умолчанию это не затрагивает.
  • Устаревшие подписки, созданные до появления пометки профилем (в строке нет notifier_profile), доставляются только тем шлюзом, который держит реальную блокировку синглтона диспетчера, — так два шлюза никогда не соревнуются за них.

Дублирующая доставка между шлюзами предотвращается атомарным захватом каждого события в БД доски. Не нужны ни релеи, ни разделение учётных данных, ни лишние диспетчеры — каждый шлюз профиля просто доставляет через свои собственные адаптеры.

Запуски (runs) — одна строка на попытку

Задача — это логическая единица работы; запуск (run) — одна попытка её выполнить. Когда диспетчер захватывает готовую задачу, он создаёт строку в task_runs и указывает tasks.current_run_id на неё. Когда эта попытка заканчивается — завершена, заблокирована, упала, истёк таймаут, не удался запуск, переоформлена — строка запуска закрывается с outcome, а указатель задачи сбрасывается. У задачи, к которой было три попытки, будет три строки в task_runs.

Почему две таблицы вместо простого изменения задачи: нужна полная история попыток для реальных разборов после инцидентов («второй ревьюер добрался до одобрения, третий смержил»), и нужно чистое место, куда вешать метаданные для каждой попытки — какие файлы изменились, какие тесты запускались, какие замечания оставил ревьюер. Это факты о запуске, а не о задаче.

Запуски — это ещё и место, где живёт структурированная передача. Когда воркер завершает задачу (через kanban_complete(...)), он может передать:

  • summary (параметр инструмента) / --summary (CLI) — человекочитаемая передача; попадает в запуск; нижестоящие потомки видят её в своём build_worker_context.
  • metadata (параметр инструмента) / --metadata (CLI) — произвольный JSON-словарь на запуске; потомки видят его сериализованным рядом со сводкой.
  • result (параметр инструмента) / --result (CLI) — короткая строка в лог, попадающая в строку задачи (устаревшее поле, сохранено для обратной совместимости).

Нижестоящие потомки читают сводку + metadata самого последнего завершённого запуска для каждого родителя. Повторно запускаемые воркеры читают предыдущие попытки на своей же задаче (исход, сводку, ошибку), чтобы не повторять путь, который уже провалился.

# Что на самом деле делает воркер — вызов инструмента изнутри цикла агента:
kanban_complete(
    summary="implemented token bucket, keys on user_id with IP fallback, all tests pass",
    metadata={"changed_files": ["limiter.py", "tests/test_limiter.py"], "tests_run": 14},
    result="rate limiter shipped",
)

Та же передача доступна из CLI, когда вам (человеку) нужно закрыть задачу, которую воркер закрыть не может, — например, брошенную задачу, или ту, которую вы вручную пометили выполненной из дашборда:

hermes kanban complete t_abcd \
    --result "rate limiter shipped" \
    --summary "implemented token bucket, keys on user_id with IP fallback, all tests pass" \
    --metadata '{"changed_files": ["limiter.py", "tests/test_limiter.py"], "tests_run": 14}'

# Просмотреть историю попыток по повторно запускавшейся задаче:
hermes kanban runs t_abcd
#   #  OUTCOME       PROFILE           ELAPSED  STARTED
#   1  blocked       worker               12s  2026-04-27 14:02
#        → BLOCKED: need decision on rate-limit key
#   2  completed     worker                8m   2026-04-27 15:18
#        → implemented token bucket, keys on user_id with IP fallback

Запуски отображаются в дашборде (раздел Run History на панели, по одной цветной строке на попытку) и в REST API (GET /api/plugins/kanban/tasks/:id возвращает массив runs[]). PATCH /api/plugins/kanban/tasks/:id с {status: "done", summary, metadata} передаёт оба поля в ядро, так что кнопка «mark done» дашборда эквивалентна CLI. Строки task_events несут run_id, к которому относятся, так что интерфейс может группировать их по попытке, а событие completed встраивает в свою полезную нагрузку первую строку сводки (ограничение 400 символов), чтобы нотификаторы шлюза могли отображать структурированные передачи без второго обращения к SQL.

Оговорка о массовом закрытии. hermes kanban complete a b c --summary X отклоняется — структурированная передача привязана к одному запуску, так что копирование одной и той же сводки на N задач почти всегда ошибка. Массовое закрытие без --summary / --metadata по-прежнему работает для обычного случая «я закрыл кучу административных задач».

Отказ по зависимостям при завершении называет родителей. kanban_complete / hermes kanban complete / действия дашборда «mark done» и «request review» (одиночные и массовые) на карточке, чей прямой родитель не в done/archived (родителя переоткрыли посреди запуска, или ребро появилось раньше запрета на связи к выполняющимся потомкам), сообщают unsatisfied parent dependencies: t_… (todo) вместо общего «unknown id, stale run, or already terminal»; карточка остаётся в работе. kanban_show перечисляет тех же родителей в unsatisfied_parents, а hermes kanban show / hermes kanban diagnostics / дашборд поднимают предупреждение running_with_open_parents для выполняющейся карточки в таком состоянии. Завершите родителя или выполните hermes kanban unlink <parent> <child>; обходного пути через проверку зависимостей нет.

Защита живого захвата при завершении. Задачу в running, чей воркер держит живой захват, завершает только этот воркер (kanban_complete изнутри запуска) либо явное переопределение оператором: hermes kanban complete <id> --force и действие «mark done» в дашборде. hermes kanban complete <id> без захвата или kanban_complete из сессии оркестратора отклоняются с подсказкой про --force / hermes kanban reclaim, так что вторая сессия больше не может закрыть запуск живого воркера у него из-под ног. Завершение карточек в ready, blocked или review без захвата не изменилось.

Переоформленные запуски из-за смены статуса. Если вы перетаскиваете выполняющуюся задачу из running в дашборде (обратно в ready или сразу в todo) или архивируете задачу, которая ещё выполнялась, текущий запуск закрывается с outcome='reclaimed', а не остаётся осиротевшим. Строка task_runs всегда находится в терминальном состоянии, когда tasks.current_run_id равен NULL, и наоборот — этот инвариант соблюдается во всех интерфейсах: CLI, дашборде, диспетчере и нотификаторе.

Синтетические запуски для никогда не захваченных завершений. Завершение или блокировка задачи, которая никогда не захватывалась (например, человек закрывает задачу в статусе ready из дашборда со сводкой, или пользователь CLI запускает hermes kanban complete <ready-task> --summary X), иначе привело бы к потере передачи. Вместо этого ядро вставляет строку запуска нулевой длительности (started_at == ended_at), несущую сводку / метаданные / причину, так что история попыток остаётся полной. run_id события completed / blocked указывает на эту строку.

Живое обновление панели. Когда поток событий WebSocket дашборда сообщает о новых событиях для задачи, которую сейчас просматривает пользователь, панель перезагружается сама (через счётчик событий для конкретной задачи, встроенный в список зависимостей её useEffect). Закрывать и открывать заново больше не нужно, чтобы увидеть новую строку запуска или обновлённый исход.

Обратная совместимость

Два допускающих NULL столбца в tasks зарезервированы для маршрутизации рабочих процессов v2: workflow_template_id (к какому шаблону относится эта задача) и current_step_key (какой шаг этого шаблона активен). Ядро v1 игнорирует их для маршрутизации, но позволяет клиентам их записывать, так что релиз v2 сможет добавить механизм маршрутизации без ещё одной миграции схемы.

Справочник событий

Каждый переход добавляет строку в task_events. Каждая строка несёт опциональный run_id, чтобы интерфейсы могли группировать события по попытке. Виды группируются в три кластера для удобства фильтрации (hermes kanban watch --kinds completed,gave_up,timed_out):

Жизненный цикл (что изменилось в задаче как логической единице):

ВидПолезная нагрузкаКогда
created{assignee, status, parents, tenant}Задача создана. run_id равен NULL.
promoted—todo → ready, потому что все родители достигли done. run_id равен NULL.
claimed{lock, expires, run_id}Диспетчер атомарно захватил готовую (ready) задачу для запуска.
completed{result_len, summary?}Воркер записал --result / --summary, и задача перешла в done. summary — это передача из первой строки (ограничение 400 символов); полная версия живёт в строке запуска. Если complete_task вызывается для никогда не захваченной задачи с полями передачи, синтезируется запуск нулевой длительности, чтобы run_id всё равно на что-то указывал.
blocked{reason, kind, recurrences}Воркер или человек перевели задачу в blocked. kind — типизированная причина блокировки (needs_input, capability, transient или null для общей блокировки); recurrences — счётчик цикла разблокировки. Блокировка kind=dependency без незавершённого родителя попадает сюда как needs_input (в полезную нагрузку добавляются requested_kind: dependency, rekind_reason: no_open_parent), потому что из todo её бы просто снова продвинули и перезапустили на следующем тике диспетчера. Синтезирует запуск нулевой длительности при вызове на никогда не захваченной задаче с --reason.
dependency_wait{reason, kind} или {reason: parent_not_done, demoted: true, parent}Воркер заблокировался с kind=dependency, пока хотя бы один родитель ещё открыт, — задача просто ждёт другую задачу, поэтому направляется в todo (заблокирована родителем, автоматически продвигается), а не в blocked, и повтор не засчитывается. Человек не нужен. Генерируется и тогда, когда link/kanban_link подвешивает потомка в ready под родителя, который ещё не done: потомок падает обратно в todo, а это событие фиксирует причину (захват ready → running перепроверяет родителей, так что никто не запустит его, пока родитель не завершится или связь не будет снята через hermes kanban unlink).
block_loop_detected{reason, kind, recurrences, limit}Задачу разблокировали и снова заблокировали по той же причине BLOCK_RECURRENCE_LIMIT раз (по умолчанию 2). Вместо того чтобы снова попасть в blocked — где cron продолжал бы её разблокировать, — она направляется в triage под внимание оркестрации, разрывая цикл разблокировка↔повторная блокировка.
unblocked—blocked → ready (или todo, если родители всё ещё открыты), вручную или через /unblock. Сбрасывает consecutive_failures диспетчера, но намеренно сохраняет block_recurrences, чтобы разрыватель цикла помнил историю. run_id равен NULL.
archived—Скрыта со стандартной доски. Если задача ещё выполнялась, несёт run_id запуска, который был переоформлен как побочный эффект.

Правки (изменения от человека, не являющиеся переходами):

ВидПолезная нагрузкаКогда
assigned{assignee}Исполнитель изменён (включая снятие назначения).
edited{fields}Обновлён заголовок или тело.
reprioritized{priority}Изменён приоритет.
status{status}Перетаскивание в дашборде записало статус напрямую (например, todo → ready). Несёт run_id запуска, переоформленного при перетаскивании из running; иначе run_id равен NULL.

Телеметрия воркера (о процессе выполнения, а не о логической задаче):

ВидПолезная нагрузкаКогда
spawned{pid}Диспетчер успешно запустил процесс воркера.
worker_registered{pid, started_at}Диспетчер умер после запуска воркера, но до записи его pid, поэтому воркер записал pid сам, ещё до первого обращения к модели. Проверки живости теперь его видят, и истёкший клейм продлевается, а не порождает второго воркера. Воркер, чей прогон был отобран раньше, чем он дошёл до этого шага, выходит, не работая над карточкой.
heartbeat{note?}Воркер вызвал hermes kanban heartbeat $TASK, чтобы сигнализировать о жизнеспособности во время длительных операций.
reclaimed{stale_lock}TTL захвата истёк без завершения; задача возвращается в ready. Автоматическое переоформление засчитывается как одна неуспешная попытка для предохранителя gave_up (иначе захват, так и не запустивший воркера, крутился бы захват → переоформление → захват вечно); переоформление оператором (reclaim), напротив, сбрасывает счётчик.
crashed{pid, claimer, exit_kind?, exit_code?, worker_output?}PID воркера больше не жив, но TTL ещё не истёк. worker_output — хвост собственного лога воркера (его финальный ответ или отрисованная ошибка провайдера, без служебного оформления, ≤ 400 символов); он же дописывается в last_failure_error задачи, так что доска показывает, почему упало, а не только код выхода.
timed_out{pid, elapsed_seconds, limit_seconds, sigkill}Превышен max_runtime_seconds; диспетчер отправил SIGTERM (затем SIGKILL после 5 с отсрочки) и поставил задачу обратно в очередь.
stale{elapsed_seconds, last_heartbeat_at, heartbeat_age_seconds, timeout_seconds, pid, terminated}Задача выполнялась дольше kanban.dispatch_stale_timeout_seconds (по умолчанию 4 ч) И за последний час не поступило ни одного kanban_heartbeat. Диспетчер отправил SIGTERM локальному воркеру (если он есть), сбросил задачу в ready для повторного запуска. НЕ увеличивает счётчик неудач (stale — это определение отсутствия на стороне диспетчера, а не сбой воркера). Воркерам, выполняющим длительные операции, следует вызывать kanban_heartbeat хотя бы раз в час, чтобы избежать этого.
reconciled{reason, claim_lock, claim_expires, worker_pid}Согласование осиротевшей карточки: карточка была в running со сломанным учётом захвата (claim_lock или claim_expires равны NULL — сбой посреди захвата, ручной SQL, восстановление БД) и без живого воркера, так что ни один из путей TTL/crash/stale не мог её восстановить. Диспетчер вернул её в очередь как ready с поясняющим комментарием. Управляется kanban.reconcile_orphans в config.yaml (по умолчанию true).
respawn_guarded{reason}Диспетчер отказался перезапускать эту готовую задачу на этом тике. Причины: infrastructure_cooldown (хост отказал в последнем запуске — нет scope systemd, переживающего перезапуск, — и кулдаун ещё не истёк; против карточки не засчитывается), rate_limit_cooldown (последний запуск упёрся в стену квоты; тот же кулдаун, не засчитывается), blocker_auth (последняя неудача была ошибкой квоты/аутентификации/429 — дождитесь сброса окна ограничения скорости), recent_success (завершённый запуск произошёл в последний час — дождитесь проверки перед повторным запуском), active_pr (URL GitHub PR встречается в недавнем комментарии — предыдущий воркер уже открыл PR). Задача остаётся в ready; следующий тик даёт ещё один шанс на запуск. Если основное условие сохраняется, обычный автоматический предохранитель consecutive_failures в итоге заблокирует её через gave_up после failure_limit неудач.
spawn_failed{error, failures}Одна попытка запуска не удалась (нет PATH, рабочее пространство не монтируется, …). Счётчик увеличивается; задача возвращается в ready для повтора.
skipped_nonspawnable{assignee}Диспетчер отказался запускать воркера, потому что профиля исполнителя нет в этом домашнем каталоге (или его нет в kanban.dispatch_profiles). Пишется один раз на карточку и повторяется, только если между записями появилось другое событие, так что show/tail называют отсутствующий профиль без строки на каждый тик. Карточка остаётся в ready: переназначьте её или установите профиль.
protocol_violation{pid, claimer, exit_code, protocol_violation, worker_output?}Воркер успешно завершился, пока задача была ещё running, обычно потому что он ответил без терминального вызова доски (kanban_complete, kanban_request_review или kanban_block). Генерируется на каждом нарушении (маркер protocol_violation: true из полезной нагрузки копируется в метаданные запуска и питает бюджет повторов, считающий только нарушения). Пока бюджет не исчерпан — до _PROTOCOL_VIOLATION_FAILURE_LIMIT (по умолчанию 3) подряд идущих нарушений, а max_retries у задачи перекрывает предел, — задача просто возвращается в ready для новой попытки; когда серия достигает предела, диспетчер дополнительно генерирует gave_up и автоматически блокирует задачу. worker_output несёт последний напечатанный воркером текст (обычно его объяснение, почему он остановился); он также попадает в last_failure_error и показывается воркеру повторной попытки как ошибка предыдущей.
gave_up{failures, effective_limit, limit_source, error, terminal_provider?}Автоматический предохранитель сработал после N подряд идущих неуспешных попыток. Задача автоматически блокируется с последней ошибкой. Эффективный лимит разрешается как max_retries задачи, затем failure_limit диспетчера / kanban.failure_limit, затем встроенное значение по умолчанию. terminal_provider: true означает, что воркер завершился с кодом 78 на ошибке провайдера, которую повтором не исправить (учётные данные отозваны, модель исчезла), включая сбой учётных данных при старте, который прямо требует повторного входа (hermes auth / setup): такой воркер выходит с кодом 78 до первого хода, а остальные сбои при старте завершаются кодом 1; предохранитель сработал на этой первой попытке — намертво и независимо от лимита.

hermes kanban tail <id> показывает их для одной задачи. hermes kanban watch транслирует их по всей доске.

Вне рамок

Kanban намеренно рассчитан на один хост. ~/.hermes/kanban.db — локальный файл SQLite, и диспетчер запускает воркеров на той же машине. Общая доска на два хоста не поддерживается — нет примитива координации для «воркер X на хосте A, воркер Y на хосте B», а путь обнаружения сбоев предполагает, что PID локальны для хоста. Если нужен мультихостовый режим, запустите независимую доску на каждом хосте и используйте delegate_task / очередь сообщений, чтобы связать их между собой.

перевод и редактура:

обновлено

⌕ ESC