<< All versions

Skill v1.0.0

currentAutomated scan100/100
dewil/claude-toolkit/asana-project
──Details
PublishedSeptember 26, 2026 at 06:52 PM
Content Hashsha256:952d47f15974f3d8...
Git SHA
──Files
Files (1 file, 25.7 KB)
SKILL.md25.7 KBactive
SKILL.md · 154 lines · 25.7 KB

version: "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

Доска общая: созданные задачи сразу видит команда, а откатывать массовую заливку - руками удалять задачи. Поэтому:

  1. Всегда сначала dry-run (дефолт, без --send). Он показывает построчно: секция (есть / будет создана), задача, исполнитель, срок, и что произойдет - ПРОПУСК (уже есть, без изменений) либо СРОК: 2026-08-03 -> 2026-08-10. У tasks dry-run и send решают по одной и той же функции (plan_effect) - предпросмотр не разойдется с действием; у create действие всегда одно (создать все перечисленное), там расходиться нечему.
  2. Чей токен и какой воркспейс - реши до `--send`. Dry-run показывает содержимое, но не от чьего имени уйдет запись. Аккаунтов несколько (личный и заказчика отдельными файлами) - --auth <файл> обязателен в каждой команде: без него скрипт молча возьмет ASANA_TOKEN или дефолтный ~/.config/asana/auth.json (rules/mcp-multi-account.md). Аккаунт один, но состоит сразу в нескольких организациях - файл не спасает, потому что различает не он: смотри workspace в плане у create (workspaces покажет gid каждой) и проект у tasks. Личная доска и клиентская отличаются одним числом в плане, и ошибка здесь видна только на доске заказчика.
  3. Показать вывод пользователю и получить явное "ок" на этот конкретный вывод - только потом --send, сразу после "ок". Прошло заметное время или доску кто-то трогал - повтори dry-run: доска общая, состояние могло уехать, а send перечитывает ее заново.
  4. `complete` - необратимая операция, и dry-run у нее единственная защита: он печатает поименно, что будет закрыто, и сколько задач уже закрыто. Писать он умеет только формой {project, tasks: [...]} - перечнем gid, который сам же и печатает; форма {project, section} с --send отклоняется, потому что состав секции между показом и записью может измениться (подробнее - в "Закрытии отчетного периода").
  5. План со сроками - это еще и даты, которые увидят исполнители: проверь их на здравость (не в прошлом, не на выходных без причины).

CLI

bash
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.json
python3 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):

json
{
"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:

  1. `tasks` - завести секцию периода и новые задачи в ней (секции создает только эта команда).
  2. `move` - перенести в ту же секцию уже существующие задачи из рабочих секций. План: {project, section, tasks: ["<имя или gid>", ...]}. Задача уже в целевой секции - "без изменений", а не ошибка.
  3. `summary` - сводка часов: построчно часы | задача и итог. Делать до закрытия, пока расхождение еще можно исправить: сумма уходит в документ, по которому выставляется счет.
  4. `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:

json
{"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.
All versions