Skill v1.0.0
currentAutomated scan100/100version: "1.0.0" name: asana-project description: Пакетная работа с доской Asana из JSON-плана: развернуть, досыпать задачи, закрыть, свести часы: "разверни доску", "посчитай часы за период". Лента комментариев - asana-comments.
asana-project
Пакетная запись в Asana из JSON-плана: create - проект с секциями и задачами с нуля, tasks - досыпка задач в существующий проект, move - перенос существующих задач в секцию, complete - пакетное закрытие, summary - сводка часов по секции (только чтение). Скрипт-эталон - scripts/asana-project.py (top-level папка scripts/ в корне проекта). Только stdlib, из внешнего - опционально certifi.
Зачем это поверх поштучных вызовов
Поштучный канал (что MCP, пока он использовался, что ручные REST-запросы) заводит задачи по одной, и у него нет ни идемпотентного повтора, ни синка сроков из плана-файла. Развернуть доску по итогам встречи (13 задач по трем секциям, у каждой исполнитель, описание, срок) - десятки шагов; через JSON-план - одна команда и одно подтверждение. Пробел не в "поштучно нельзя", а в пакетности и повторяемости: план приходит целиком, живет файлом под рукой, и повторный прогон того же файла безопасен.
Границы
- Единичные операции (создать одну задачу, сменить статус, написать комментарий) - REST напрямую тем же PAT: база
https://app.asana.com/api/1.0, заголовокAuthorization: Bearer <token>; комментарий -POST /tasks/{gid}/storiesс полемhtml_text. Понадобилось часто - завести маленький скрипт рядом, а не поднимать MCP (см. "Граница с Asana MCP" ниже). - Чтение длинной ленты комментариев - скилл
asana-comments: поштучный запрос задачи отдает ленту урезанной и молча. - Пакетное разворачивание, досыпка, пересмотр сроков, перенос в секцию, закрытие, сводка часов - этот скилл.
- Скрипт намеренно НЕ делает: стирание срока (срок только добавляется и двигается), смену исполнителя существующей задачи, удаление задач, снятие галки "выполнено". Массовое переоткрытие на общей доске опаснее, чем разовое снятие галки руками.
Перенос и закрытие раньше были в этом списке - и это было ошибкой. Формально они единичные операции, фактически - два шага одной регулярной процедуры (закрытие отчетного периода), и делались прямым REST в обход dry-run. То есть ровно те две операции, которые правят чужие существующие задачи, шли без предпросмотра, тогда как безобидное создание новых было им закрыто. Пробел был не в удобстве, а в гейте.
Граница с Asana MCP
MCP не используется: снят 17.08.2026, возвращать не надо. За одну сессию три сбоя - дисконнекты, зависание вызова на 300 с, Not Authorized сразу после переавторизации, - при нуле сбоев у скриптов на PAT. Отдельно от надежности: OAuth молча берет аккаунт из того браузера, где залогинен человек, поэтому привязка не к тому аккаунту проходит незаметно (rules/mcp-multi-account.md), а в headless и cron протухший токен некому переавторизовать. Все, что раньше закрывал MCP, теперь закрывают PAT-скрипты и прямой REST.
Жесткий гейт перед --send
Доска общая: созданные задачи сразу видит команда, а откатывать массовую заливку - руками удалять задачи. Поэтому:
- Всегда сначала dry-run (дефолт, без
--send). Он показывает построчно: секция (есть / будет создана), задача, исполнитель, срок, и что произойдет -ПРОПУСК (уже есть, без изменений)либоСРОК: 2026-08-03 -> 2026-08-10. Уtasksdry-run и send решают по одной и той же функции (plan_effect) - предпросмотр не разойдется с действием; уcreateдействие всегда одно (создать все перечисленное), там расходиться нечему. - Чей токен и какой воркспейс - реши до `--send`. Dry-run показывает содержимое, но не от чьего имени уйдет запись. Аккаунтов несколько (личный и заказчика отдельными файлами) -
--auth <файл>обязателен в каждой команде: без него скрипт молча возьметASANA_TOKENили дефолтный~/.config/asana/auth.json(rules/mcp-multi-account.md). Аккаунт один, но состоит сразу в нескольких организациях - файл не спасает, потому что различает не он: смотриworkspaceв плане уcreate(workspacesпокажет gid каждой) и проект уtasks. Личная доска и клиентская отличаются одним числом в плане, и ошибка здесь видна только на доске заказчика. - Показать вывод пользователю и получить явное "ок" на этот конкретный вывод - только потом
--send, сразу после "ок". Прошло заметное время или доску кто-то трогал - повтори dry-run: доска общая, состояние могло уехать, а send перечитывает ее заново. - `complete` - необратимая операция, и dry-run у нее единственная защита: он печатает поименно, что будет закрыто, и сколько задач уже закрыто. Писать он умеет только формой
{project, tasks: [...]}- перечнем gid, который сам же и печатает; форма{project, section}с--sendотклоняется, потому что состав секции между показом и записью может измениться (подробнее - в "Закрытии отчетного периода"). - План со сроками - это еще и даты, которые увидят исполнители: проверь их на здравость (не в прошлом, не на выходных без причины).
CLI
python3 scripts/asana-project.py workspaces # аккаунт и gid воркспейсовpython3 scripts/asana-project.py create --plan план.json # предпросмотрpython3 scripts/asana-project.py create --plan план.json --send # создать проектpython3 scripts/asana-project.py tasks --plan досыпка.json --send # досыпать в существующийpython3 scripts/asana-project.py tasks --plan план.json --auth ~/.config/asana/auth-work.jsonpython3 scripts/asana-project.py move --plan перенос.json --send # перенести в секциюpython3 scripts/asana-project.py summary --project 12345 --section "Отчет 01.03 - 31.08"python3 scripts/asana-project.py complete --plan закрытие.json --send # НЕОБРАТИМО
В проекте с разведенными аккаунтами --auth <файл> добавляется к каждой из этих команд, включая dry-run (пункт 2 гейта).
Формат плана для create - workspace (gid из workspaces), name, notes, sections; для tasks вместо них один ключ project (gid доски - последнее число в ее URL):
{"project": "1216875635976802","sections": [{"name": "Этап 0", "tasks": [{"name": "Задача", "notes": "контекст", "assignee": "user@example.com", "due_on": "2026-08-03"}]}]}
Исполнитель задается почтой - Asana принимает email в assignee наравне с gid, резолвить пользователей отдельным запросом не нужно; в плане человекочитаемые адреса. Опечатка в почте уронит прогон на середине. Для tasks это безопасно: повторный прогон доздаст недостающее без дублей. Для create - НЕТ: проект к этому моменту уже создан, повтор create создаст второй; скрипт при обрыве сам печатает в stderr gid проекта и напоминание доздать через tasks (то же напечатано в начале вывода: "проект создан ... gid").
Повтор имени задачи внутри одного плана - ошибка, скрипт откажется до первого сетевого вызова: какую из двух строк исполнять - неоднозначно. Имена в плане должны быть уникальны (сравниваются без хвостовых пробелов).
Закрытие отчетного периода
Регулярная процедура у любого подрядчика, работающего по актам: собрать выполненные работы в секцию периода, показать часы, отдать на подпись, закрыть. Порядок - четыре команды, каждая со своим dry-run:
- `tasks` - завести секцию периода и новые задачи в ней (секции создает только эта команда).
- `move` - перенести в ту же секцию уже существующие задачи из рабочих секций. План:
{project, section, tasks: ["<имя или gid>", ...]}. Задача уже в целевой секции - "без изменений", а не ошибка. - `summary` - сводка часов: построчно
часы | задачаи итог. Делать до закрытия, пока расхождение еще можно исправить: сумма уходит в документ, по которому выставляется счет. - `complete` - пакетно проставить "выполнено". План:
{project, section}(вся секция) либо{project, tasks: [...]}; ровно одно из двух, иначе отказ. Необратимо: снятие галки скрипт не делает.
Форма `{project, section}` - только предпросмотр, `--send` с ней отклоняется. Она набор не фиксирует: между показом и записью в секцию могли добавить задачу, и она закрылась бы тоже - на общей доске это чужая задача, закрытая без спроса. Инвариант "dry-run показывает ровно то, что сделает send" тут невыполним в принципе, поэтому вместо предупреждения стоит запрет: dry-run печатает готовый план по gid, им и закрывают.
закрыть этот набор - планом по gid (форма по секции пишет только через него):{"project": "12345", "tasks": ["1001", "1002"]}
Секция, в которой все уже закрыто, - штатный no-op, а не отказ.
Закрытие глобально для задачи, а не для доски. Флаг "выполнено" принадлежит самой задаче: multi-homed задача, видимая в нескольких проектах, закроется во всех. Предпросмотр этого не показывает - это свойство Asana, а не скрипта.
Подзадачи в перечень не попадают. move, complete и summary работают со списком задач проекта или секции, а подзадача в нем не значится. Закрывать и считать их надо отдельно.
Часы берутся из имени задачи регуляркой. Дефолт:
(?<![\w.,\-\u2010-\u2015\u2212])(\d+(?:[.,]\d+)?)\s*(?:ч|h)\b
Первое число перед "ч"/"h", у которого слева нет буквы, цифры, точки, запятой или тире любого начертания. Ловит (dwl 20ч) и (dwl 23,5ч); не ловит "за 3 часа", "2 hotfix", (20-25ч), 1,5,5ч, 1e3ч. Своя конвенция - флаг --hours-re с группой захвата (без группы или с битой регуляркой - отказ). Формат имен задач - проектная договоренность, в каноне ее нет: держи ее в правиле своего проекта.
Нераспознанные часы печатаются строкой с `?` и в сумму не входят, а в конце идет явное предупреждение. Молчаливый пропуск занизил бы счет неотличимо от честной суммы (rules/silent-failure.md).
Дефолт намеренно отказывается от неоднозначного, а не угадывает: (20-25ч), 1,5,5ч, 1e3ч - это ?, а не 25, 5,5 и 3. Раньше такие строки молча уходили в сумму счета. Абсурдно большие числа тоже отбрасываются: строка из четырехсот цифр давала бесконечность и отравляла итог целиком.
Сумма считается в десятичной арифметике (Decimal), а не в двоичной: 0,005 трижды в float дают 0,01 вместо 0,015, и расхождение с ручной проверкой возникало бы на ровном месте.
Своя регулярка - твоя ответственность. Проверяется только то, что она компилируется и имеет группу захвата; смысл и катастрофический бэктрекинг (((a+)+$) на длинном имени вешает процесс) на совести автора. Если сомневаешься - оставь дефолт и переименуй задачи.
Связи "откуда выросла эта работа"
У отчетной задачи за выполненные работы блокировок нет по определению - она никого не ждет, - но показать, из чего работа выросла, надо. Для этого у задачи в плане tasks есть поле related:
{"name": "Отчет: доработка бота (dwl 20ч)", "related": ["Правки формы заявки", "1216875635976802"]}
Скрипт пишет в описание задачи секцию СВЯЗАННЫЕ ЗАДАЧИ - откуда выросла эта работа: с упоминаниями. Что важно:
- блок в описании один на все скрипты, и каждый перезаписывает только свои секции: секция блокировок от
asana-blockersпереживает запись, и наоборот. Конвенция и ее устройство - в скиллеasana-blockers, раздел "Блок один на все скрипты"; - каркас берется из `scripts/asana-blockers.py` боковым импортом - оба скрипта должны лежать в
scripts/проекта. Нет соседа - команда скажет об этом и назовет, что взять из канона; - идемпотентно: повторный прогон с тем же
relatedничего не пишет; - ссылаться можно на имя или gid, в том числе на задачу, создаваемую этим же прогоном (блок пишется после создания всех задач). Задачи плана участвуют в резолве наравне с доской, поэтому имя из плана, совпавшее с gid чужой задачи, - отказ, а не молчаливый выбор одной из трактовок;
- остаточный риск, который не закрывается: описание задачи читается дважды - на проверке до записей и на самом слиянии. Если между этими моментами кто-то заменит описание блоком старого формата, прогон остановится уже после создания задач. Блок при этом не пострадает, но доска останется наполовину обработанной; лечится прогоном
asana-blockersи повтором; - ключа `related` нет - задача не трогается вовсе;
"related": []наоборот означает "связей больше нет" и снимает нашу секцию. Разделение намеренное: без него последнюю связь нельзя было бы убрать иначе как руками; - блок первой редакции `asana-blockers` (термин плюс голая ссылка) команда сливать не умеет и отказывается работать с такой задачей - перезапись стерла бы записанные в нем блокировки. Лечится прогоном
asana-blockers: он перепишет блок в новый формат; - ссылка на задачу с неуникальным именем - отказ: связь ушла бы на произвольную из одноименных. Указывай gid.
Идемпотентность и повторные прогоны
- Ключ - имя задачи. Повторный прогон
tasksс тем же планом не плодит дубли; изменение имени в плане означает новую задачу. Если одноименные задачи открыта и закрыта, сопоставляется открытая. Секции тоже переиспользуются по имени. - Матчинг не различает, кто задачу создал. Одноименная задача, заведенная человеком руками, считается плановой: ей подтянется срок (а если она multi-homed - срок изменится во всех проектах, где она видна). В dry-run это видно как неожиданный
ПРОПУСК/СРОКу задачи, которую ты считал новой - разберись до--send, обычно достаточно переименовать задачу в плане. - `move` и `complete` идемпотентны по той же модели: перенос в ту же секцию и закрытие закрытого - это "без изменений", а не ошибка.
- Повтор после обрыва безопасен: сделанное станет "без изменений". Скрипт при обрыве печатает в stderr, сколько задач подтвержденно обработано - исход последнего запроса неизвестен всегда, ответ мог потеряться уже после применения.
- Числовая ссылка, совпавшая с именем другой задачи, - отказ. Строка из цифр читается как gid, но задача может быть и НАЗВАНА числом ("2026"). Совпали обе трактовки - скрипт не выбирает, а называет обе.
- Одноименные секции - отказ во всех командах, включая
tasks. Asana их разрешает; молчаливый выбор одной из двух отправлял бы запись в непредсказуемую. - Две или более открытых одноименных задачи - отказ до первой записи в
tasks,move,completeпо списку иrelated. Область сопоставления - весь проект, не отдельная секция. Отказ с кодом 1 перечисляет имя, gid, секцию и срок каждого открытого кандидата; dry-run и--sendпоказывают один текст. Указанный явно gid работает и для закрытой задачи. - Часы округляются при разборе, а не при печати: поэтому сумма показанных строк всегда равна показанному итогу. Шаг - две цифры после запятой.
- Неоднозначное имя задачи - отказ до записи. Выбор наугад тут означает изменение не той задачи на общей доске. Ошибки собираются по всем ссылкам плана сразу, а не по одной за прогон.
- `move` не создает секцию: промах в имени вероятнее намерения завести секцию именно этой командой (
"Отчет"против"Отчёт"- разные строки). Отказ печатает список существующих секций; заводит ихtasks. - `create` НЕ идемпотентен: повторный прогон создаст второй проект с тем же именем. Развернул доску - дальше только
tasksс gid проекта. - Срок синхронизируется. Если
due_onсуществующей задачи отличается от плана - подтягивается (в dry-run виднобыло -> стало). Практический смысл: план дат живет в JSON под рукой, пересмотр сроков после этапа - правка файла и один прогон, а не ручной обход доски. Убрать дату из плана - НЕ стирает ее на доске.
Авторизация
Порядок выбора PAT - как в asana-comments, раздел "Авторизация".
Как получить PAT по шагам и три способа словить 401 (OAuth-токен MCP-сервера, client_id, client_secret - все не подходят) - в скилле asana-comments, раздел "Авторизация".
Связанные файлы
scripts/asana-project.pyв корне проекта - эталонный скрипт (workspaces / create / tasks, dry-run по умолчанию).scripts/asana-comments.py- чтение ленты комментариев (read-only сосед по токену).scripts/asana-blockers.py- связи задач в описании; отсюда берется каркас технического блока дляrelated(нужен вscripts/проекта, если пользуешься этим полем).~/.config/asana/auth.json- PAT, вне репозитория, права 600.