
Модель прав
Суть mxBoard — в том, что правила переходов проверяются на сервере, в едином сервисном слое, а не в интерфейсе. Каким бы каналом ни пришёл запрос (менеджер, REST, MCP), работает один и тот же процессор. Агент не может «найти канал послабее»: и UI, и REST, и MCP — фасады над одними и теми же TaskService / Transitions / Visibility.
Модель данных
Иерархия отдел → проект → задача:
| Сущность | Таблица | Роль |
|---|---|---|
| Отдел | mxboard_department | Реестр: помечает, какая группа пользователей MODX является отделом. Членство/роли — из MODX. |
| Проект | mxboard_project | Доска: владеет колонками и задачами, принадлежит отделу. |
| Колонка (стадия) | mxboard_column | Стадия проекта. Несёт правила перехода (move_roles), необязательное описание (description) и флаги is_initial/is_start/is_final. |
| Тип задачи | mxboard_task_type | Набор полей, которые обязана заполнить постановка. Принадлежит отделу. |
| Поле типа | mxboard_field | Дополнительное поле типа (текст, дата, select, файл…). |
| Задача | mxboard_task | Карточка. Контент типа — в JSON-поле fields; произвольные данные интегратора — в meta. Подзадача — та же таблица через parent_id. |
| Комментарий | mxboard_comment | Сообщение чата задачи. |
| Вложение | mxboard_attachment | Файл задачи (comment_id=0) или сообщения. См. Типы задач. |
| Журнал | mxboard_log | Все переходы: кто, когда, откуда-куда, каким каналом. Не редактируется из UI. |
| Токен | mxboard_token | Токен агента (только sha256-хэш). |
| Уведомление | mxboard_notification | Очередь in-app уведомлений (SSE). |
| Счётчик | mxboard_counter | Атомарный счётчик человекочитаемых номеров задач (num). |
Роли относительно карточки
У задачи есть автор (author_id — кто поставил) и исполнитель (assignee_id — кто взял/назначен). Это разные роли, и это могут быть разные агенты.
Право перехода в колонку задаётся полем move_roles этой колонки — CSV ролей относительно карточки:
| Роль | Кто это |
|---|---|
author | тот, кто поставил задачу |
assignee | тот, кто её выполняет |
Карточку двигают только эти двое — автор и исполнитель, каждый в пределах разрешённых ему колонок. Больше рядовых ролей нет; всё остальное — привилегия менеджера (см. ниже).
Коробочный набор стадий (проект default) — это готовый рабочий цикл с агентом-исполнителем: шесть стадий, два ручных гейта, ни одной декоративной.
| Колонка | move_roles (кто переводит в неё) | Флаг | Чей ход, когда карточка здесь |
|---|---|---|---|
backlog | assignee,author | initial | ничей: постановка вызревает, работа не начата |
to_start | author | start | исполнителя: старт дан — изучить постановку и подготовить план |
plan | assignee | автора: проверить план — принять переводом в in_progress или вернуть комментарием | |
in_progress | author | исполнителя: план принят, реализовать и сдать в review | |
review | assignee | автора: проверить результат — в done или вернуть в in_progress | |
done | author | final | никого: автор принял, задача закрыта |
Две разные вещи: move_roles и описание стадии
move_roles отвечает на вопрос «кто вправе завести карточку в эту колонку», а описание стадии (description) — на вопрос «что делать тому, чей ход наступил, пока карточка стоит здесь». Поэтому в plan карточку заводит исполнитель, а описание там адресовано автору. Описания коробочных стадий отдаются в stage_list и board_list — агент читает свой следующий шаг прямо с доски.
Стадия to_start — первый гейт. Пока карточка в backlog, постановку можно дорабатывать и согласовывать сколько угодно: работа не начата. Автор открывает её переводом в to_start — и только с этого момента исполнитель берётся за задачу. Это важно, когда исполнитель ИИ-агент: он может быть занят другой задачей или остаться без токенов, поэтому момент старта назначает человек, а не факт создания карточки. Внешней автоматизации текущая стадия приходит в GET /events полем task_stage.
Второй гейт — приём плана. Исполнитель выносит план в plan, и перевод карточки в in_progress делает автор: этот перевод и есть «план принят, реализуй». Если автор с планом не согласен — он пишет комментарий, не меняя стадию; исполнитель правит план в той же колонке. Так автор контролирует не только результат, но и подход.
Отдельных стадий «Согласование» и «Отменена» в наборе нет намеренно: согласование выражается комментариями на plan, отложенную задачу возвращают в backlog, а ненужную удаляют. Закрытием считается только done.
Обновление не перекраивает вашу доску
Резолвер только создаёт недостающие колонки и не трогает position, move_roles и описания у уже существующих. Установка обновления на доску, настроенную вручную, ничего не затрёт — но и новую раскладку не принесёт: коробочный набор целиком получает только чистая установка.
Закрытие и самоаттестация
Финальная колонка (is_final) — особый, более строгий случай: перевести в неё может только автор карточки или менеджер, независимо от move_roles.
Отдельно закрыта самоаттестация: если автор и исполнитель — один и тот же пользователь (агент сам себе поставил задачу и сам её выполнил), закрыть её нельзя. Снимается настройкой mxboard.allow_self_close (по умолчанию выключено, см. Настройки).
Менеджер карточки
Менеджер карточки обходит правила колонок, закрывает чужие задачи и видит на доске всё в проекте. Менеджер — это:
- глобальный sudo, либо
- супер-пользователь группы отдела, которому принадлежит проект карточки.
«Супер группы» определяется по authority роли пользователя в группе: в MODX меньше authority = больше прав. Порог задаётся настройкой mxboard.group_admin_authority. По умолчанию — 1: менеджером отдела считается участник с ролью верхнего уровня, и для управления отделом больше не нужен глобальный Super User. Значение 0 полностью выключает механизм — тогда менеджером остаётся только sudo.
На обновлении дефолт не подхватится
До версии 2.8.0 значение по умолчанию было 0. Резолверы не перезаписывают уже существующие настройки, поэтому на обновлённой установке порог останется прежним — при необходимости выставьте 1 в системных настройках вручную.
Ловушка MODX, которую мы обошли
modAccessibleObject::checkPolicy() возвращает true на любой вопрос, если сессия не инициализирована. То есть в API-режиме без живой сессии hasPermission() объявил бы суперпользователем кого угодно — и любой агент закрыл бы чужую задачу. Поэтому без живой сессии mxBoard верит только флагу sudo в записи пользователя, а «супер группы» проверяет прямым JOIN, а не через hasPermission.
Видимость
Детальный просмотр и канбан — разные вещи:
- Открыть и прокомментировать карточку может: автор/исполнитель самой задачи, автор/исполнитель любой её подзадачи, либо менеджер.
- На доске (канбане) пользователь видит только карточки, где он автор или исполнитель. Связь через подзадачу канбан НЕ расширяет: соисполнитель подзадачи видит на своей доске только саму подзадачу, а родитель может открыть отдельно.
- Менеджер видит на доске все карточки проекта.
Подзадачи
Привлечь соисполнителя = завести подзадачу: обычная задача с parent_id, живёт в той же таблице. Создать подзадачу может и автор, и исполнитель основной.
Незавершённая подзадача — блокер: родителя нельзя перевести в финальную колонку, пока открыт хотя бы один потомок (не в своей финальной стадии). Удаление родителя подзадачи открепляет, а не удаляет.
У закрытой задачи подзадач не заводят: если родитель стоит в финальной стадии (is_final), создание подзадачи отклоняется с ошибкой mxboard_err_parent_final. Проверка живёт в общем сервисе, поэтому одинаково работает в менеджере, REST и MCP, и не завязана на ключ колонки — учитывается только флаг. Иначе получалась бы дырка в самом же правиле блокировки: закрытому родителю дописывают открытого потомка, и закрытие задним числом перестаёт что-либо значить.
Дедлайны и оспаривание
У задачи обязательный дедлайн (deadlineon). Исполнитель не меняет его сам — он его оспаривает:
- исполнитель предлагает новую дату с причиной (
task_dispute_deadline/POST /tasks/{id}/dispute-deadline); - карточка помечается «дедлайн оспорен», ждёт решения автора;
- автор (или менеджер) принимает (дедлайн меняется) или отклоняет (
task_resolve_dispute/POST /tasks/{id}/resolve-deadline).
План и факт
Учёт времени устроен так же несимметрично, как дедлайн: план ставит автор, факт считает доска.
План — plan_hours
Плановая трудоёмкость в целых часах. Поле необязательное, 0 означает «не оценивали». Задаёт его автор (при создании или правкой карточки), исполнитель не правит план, а оспаривает — тем же механизмом, что и дедлайн:
- исполнитель предлагает свою оценку с причиной (
task_dispute_plan/POST /tasks/{id}/dispute-plan); - карточка помечается флагом
plan_disputed, предложенное значение лежит вplan_proposed; - автор (или менеджер) принимает (план меняется) или отклоняет (
task_resolve_plan/POST /tasks/{id}/resolve-plan).
Факт — от стартовой стадии до закрытия
Отдельного поля «потрачено» нет: его невозможно заполнить честно, если полагаться на самоотчёт исполнителя. Вместо этого доска берёт замер из собственного журнала переходов.
Точка отсчёта — стадия с флагом «Стартовая» (is_start). Правила замера:
- вход в стартовую стадию или в любую правее (по
position) запускает отсчёт, если он ещё не идёт (startedon); - возврат левее стартовой стадии (в коробке — в бэклог) обнуляет замер, а не ставит на паузу: работа начинается заново, прежний отсчёт смысла не имеет;
- факт = от
startedonдоclosedon; у незакрытой карточки он показывается как «идёт»; - если стартовая стадия у проекта не помечена — замера нет вовсе (прочерк). Доска не выдумывает старт задаче, которую закрыли, минуя работу.
На обновлении флаг не проставляется сам
Резолвер не трогает уже настроенные стадии, поэтому после обновления is_start не появится ни на одной колонке — и учёт факта молча не заработает. Отметьте стартовую стадию вручную в Структуре (в коробочном наборе это to_start) или через stage_update с is_start: true. Начальная стадия (is_initial) стартовой быть не может.
WIP-лимит
Настройка mxboard.wip_limit ограничивает, сколько задач один исполнитель держит в работе одновременно (0 — без лимита).
Журнал — источник правды
Все действия (create, update, move, close, comment, comment_update, comment_delete, deadline_dispute, deadline_accepted, deadline_rejected, plan_dispute, plan_accepted, plan_rejected, ai_check) пишутся в mxboard_log всегда и из всех каналов, с пометкой канала (mgr/api/mcp) и пользователя. Из интерфейса журнал не редактируется. Статусам агентов можно не верить — журналу можно.
Журнал — это ещё и единственный источник для двух производных вещей: ленты GET /events для внешних оркестраторов и живого обновления интерфейса (см. Интеграция). Поэтому действие, не попавшее в журнал, для них не существует — правка и удаление комментария пишутся туда именно по этой причине.
