<< All versions

Skill v1.0.0

currentAutomated scan100/100
dewil/claude-toolkit/asana-blockers
──Details
PublishedSeptember 26, 2026 at 06:52 PM
Content Hashsha256:d7626b4ca045b01b...
Git SHA
──Files
Files (1 file, 24.1 KB)
SKILL.md24.1 KBactive
SKILL.md · 149 lines · 24.1 KB

version: "1.0.0" name: asana-blockers description: Проставить связи между задачами Asana текстом в описании - чего ждет и что держит: "проставь блокировки", "видно, что мы ждем клиента". Нативные зависимости недоступны - тариф без них (402).


asana-blockers

Цепочки блокировок пишутся в описание задачи первым блоком - упоминаниями связанных задач и их исполнителей. Движок - scripts/asana-blockers.py (top-level папка scripts/ в корне проекта), цепочки - в .asana-blockers.json там же. Только stdlib, из внешнего - опционально certifi.

Зачем

Общее обоснование - rules/tasks-tracking.md, раздел "Зависимость выражается связью, а не сдвигом срока": там про то, почему ожидание фиксируется связью и почему обе стороны важнее одной.

Здесь - почему связь ставится текстом: тариф бесплатный, POST /tasks/{gid}/addDependencies отдает 402 Payment Required, и единственный доступный носитель связи - описание задачи. Побочная выгода: связь видна прямо в карточке и в предпросмотре, без открывания отдельной вкладки.

Как выглядит блок

СВЯЗИ ЗАДАЧИ
ЭТА ЗАДАЧА ЖДЕТ - ее нельзя сделать, пока не закрыты:
- <упоминание задачи> - <упоминание исполнителя>, открыта
ЭТА ЗАДАЧА ДЕРЖИТ - они не сдвинутся, пока не закрыта эта:
- <упоминание задачи> - <упоминание исполнителя>
- - -

Пример показывает секции этого скрипта. В том же блоке живет третья секция - СВЯЗАННЫЕ ЗАДАЧИ - откуда выросла эта работа:, ее пишет asana-project (поле related), и пункты в ней короче: только упоминание задачи, без исполнителя и статуса. Полный разбор - ниже, в "Блок один на все скрипты".

Три решения в этом формате не косметические, каждое куплено разбором на живых людях:

  • направление связи - целым предложением, а не термином. Первая редакция писала ЗАБЛОКИРОВАНА: и БЛОКИРУЕТ:, и заказчик сказал прямо: непонятно, кто кого блокирует - та задача эту или эта ту. Термин читается в обе стороны одинаково правдоподобно, а ошибка не видна: строка выглядит осмысленной при любом прочтении;
  • исполнитель - рядом с каждой связанной задачей. Увидев блокировку, человек первым делом хочет знать, ждет он своего шага или чужого. У задачи без исполнителя пишется "исполнитель не назначен": разница между "ничей" и "мой" важнее аккуратности строки;
  • статус (открыта/закрыта) - только у тех, кого ждем. У задач, которые держим мы, он избыточен и мешает читать.

Ни название задачи, ни имя исполнителя не пишутся текстом - в описании лежат упоминания (<a data-asana-gid="..."/>), а подставляет их сама Asana. Отсюда две выгоды: задачу переименовали - в блоке сразу новое имя, без нашего прогона; и отдельная строка с голым URL не нужна, упоминание уже ссылка.

Блок один на все скрипты, секции у каждого свои

Технический блок - общая конвенция, а не деталь этого скрипта. Каркас один: маркеры СВЯЗИ ЗАДАЧИ / - - -, блок первым абзацем описания, внутри - секции, у каждой заголовок целым предложением капсом и под ним пункты с упоминаниями. Секции сейчас три:

ЗаголовокКто пишет
ЭТА ЗАДАЧА ЖДЕТ - ...asana-blockers
ЭТА ЗАДАЧА ДЕРЖИТ - ...asana-blockers
СВЯЗАННЫЕ ЗАДАЧИ - откуда выросла эта работа:asana-project (поле related)

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

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

Отсюда устройство: общий реестр заголовков (KNOWN_HEADS) знают все, свои (OWN_HEADS) - каждый свои. Порядок секций в блоке канонический (по реестру), а не по тому, кто писал последним: иначе два скрипта переставляли бы секции друг друга, каждый прогон видел бы изменение, и идемпотентности не было бы ни у одного.

Каркас живет в asana-blockers.py в одном экземпляре; asana-project.py подгружает его боковым импортом соседа по scripts/ (тот же прием, что md-docx -> md-pdf). Копию не заводить: две копии разойдутся на первом же фиксе распознавания блока, а цена расхождения - стертое описание задачи.

Блок первой редакции секций не имеет (термин плюс голая ссылка). Читать его умеет только этот скрипт - и он же переписывает такой блок в новый формат на первом прогоне. asana-project с ним работать отказывается: слить старый блок он не умеет, а перезапись стерла бы записанные в нем блокировки. Наткнулся на отказ - прогони сначала asana-blockers.

Заводя новую секцию, добавь заголовок в реестр обоих скриптов сразу - иначе тот, который его не знает, начнет плодить вторые блоки.

Смена исполнителя так не подхватывается, и это важно не перепутать. Упоминание задачи динамическое, а упоминание человека - нет: в описании лежит gid того, кто был исполнителем в момент записи. Сменили исполнителя - в блоке останется прежний, пока не пройдет новый прогон. Переименование задачи прогона не требует, смена исполнителя требует.

Когда применять

  • На доске есть задачи, ждущие внешней стороны, и это надо показать не сдвигом срока.
  • Связей несколько и они переживают неделю: цепочка "наполнить сайт -> запустить -> поставить метрику" держится месяцами и проставляется повторными прогонами.
  • Тариф бесплатный: POST /tasks/{gid}/addDependencies отдает 402 Payment Required.

Когда НЕ применять

  • Одна связь на один раз - дописать строку в описание руками (или прямым REST) дешевле, чем заводить конфиг.
  • Платный тариф с нативными зависимостями - там связь ставится штатно, машинно читается и не трогает текст описания. Этот скилл - обход ограничения, а не улучшение.
  • Задача просрочена сама по себе, без внешнего ожидания - связи тут нечего показывать, это вопрос срока и владельца.
  • Нужно подвинуть срок или закрыть задачу - этот скрипт такого не делает намеренно: он описывает связи. Срок и пакетное закрытие - в skills/asana-project (tasks и complete, оба под своим гейтом); решение о переносе и закрытии в любом случае за человеком.
  • Трекер без упоминаний в описании. Прием опирается на то, что трекер подставляет имя задачи и исполнителя по идентификатору. Где этого нет, придется писать текстом - и тогда возвращается вся морока с переименованиями.

Жесткий гейт перед --send

Скрипт переписывает описания живых задач на общей доске: их сразу видят заказчик и команда, а откат - руками по каждой задаче.

  1. Всегда сначала dry-run (дефолт, без --send). Он печатает по задачам: какие связи встанут и где блок будет снят (СНЯТЬ БЛОК СВЯЗЕЙ). Имена в выводе человеческие, хотя записываются упоминания, - иначе проверить строку глазами невозможно.
  2. Чей токен и чья доска - реши до `--send`. Аккаунт может состоять сразу в нескольких организациях, и клиентская доска отличается от личной одним числом в конфиге. Разведены аккаунты - --auth <файл> обязателен в каждой команде, включая dry-run (rules/mcp-multi-account.md).
  3. Показать вывод пользователю и получить явное "ок" на этот конкретный вывод - только потом --send. Прошло время или доску трогали - повторить dry-run: send перечитывает доску заново.
  4. Проверить формулировки глазами. Названия задач скрипт не пишет, но Asana подставит их в упоминание при просмотре карточки - результат тот же: внутренняя рабочая формулировка в названии задачи ("ждем, пока клиент проснется") станет видна заказчику.

CLI

bash
python3 scripts/asana-blockers.py --board <ключ> # предпросмотр одной доски
python3 scripts/asana-blockers.py --board <ключ> --send # записать
python3 scripts/asana-blockers.py # все доски конфига (предпросмотр)
python3 scripts/asana-blockers.py --auth ~/.config/asana/auth-work.json --board <ключ>

Формат .asana-blockers.json в корне проекта (--config - другой путь):

json
{
"<ключ доски>": {
"project": "<gid доски: последнее число в ее URL>",
"chains": {
"<задача, которая ждет>": ["<без чего ее не сделать>", "<и еще одна>"]
}
}
}

chains читается как "чтобы сделать A, нужны B и C". Обратную сторону скрипт выводит сам с меткой ЭТА ЗАДАЧА ДЕРЖИТ - руками ее не дублировать. В старых блоках была метка БЛОКИРУЕТ:; скрипт распознает ее при обновлении.

Идемпотентность и повторные прогоны

  • Блок распознается по заголовку `СВЯЗИ ЗАДАЧИ` и линейке `- - -` и заменяется целиком: повторный прогон ничего не дублирует и печатает задач с изменениями: 0. Исключение одно и оно в "Граблях": блок, сдвинутый вниз по описанию, скрипт уже не видит.
  • Трогаются только свои блоки. Блок считается нашим, если состоит целиком из известных заголовков секций (см. реестр выше) и пунктов-упоминаний под ними - либо из строк первой редакции (ЗАБЛОКИРОВАНА:/БЛОКИРУЕТ: плюс ссылка). Описание, которое человек начал теми же словами, но продолжил обычным текстом, остается как есть - как и задача, у которой нет ни связей в конфиге, ни нашего блока.

Остаточный риск назовем прямо: текст, воспроизводящий грамматику блока дословно - маркер, заголовок из реестра, пункты с @-упоминаниями, линейка, - будет опознан нашим и перезаписан, даже если его набрал человек. Распознавать мягче нельзя (тогда стирается ручной текст, начатый теми же словами), поэтому граница проведена здесь: под маркером СВЯЗИ ЗАДАЧИ пишем только мы.

  • Блоки первой редакции узнаются и переписываются. На досках уже стоят блоки старого формата (термин плюс голая ссылка); прогон опознает их как свои и заменяет новыми. Не узнавать значило бы дописать второй блок сверху и оставить на карточке две противоречивые версии связей. Пишем только в новом формате, старый остается известен чтению.
  • Убранная из конфига цепочка снимает блок с описания. Иначе доска врала бы про связь, которой больше нет. Предпросмотр показывает снимаемый блок построчно - видно, что именно исчезнет.
  • Статус блокирующей задачи (`открыта`/`закрыта`) обновляется прогоном. Закрыли блокер - прогон перепишет строку; сам скрипт за доской не следит, ставить его в расписание или гонять руками решает проект.
  • Перед записью задача перечитывается. У Asana нет ни ETag, ни условной записи: PUT перезаписывает поле целиком, и правка, сделанная человеком после предпросмотра, потерялась бы. Скрипт читает задачу заново прямо перед записью и накладывает блок на свежий текст, сообщая об этом в stderr. Окно этим сужено до миллисекунд, но не закрыто - правка ровно в момент записи все равно пропадет.
  • Четыре расхождения со снимком предпросмотра отменяют запись по этой задаче (в stderr - причина, в конце - пропущено: N, код возврата ненулевой): задачу переименовали; ее убрали с этой доски; описание успели оформить (значит предупреждения про сплющивание человек не видел и согласия не давал); Asana вернула неполный ответ. Пропуск лечится повторным предпросмотром - молчаливая запись поверх такого расхождения не лечится ничем.
  • Имена сверяются дословно (с точностью до краевых пробелов и формы записи Unicode). Переименовали задачу на доске - прогон печатает перечень ненайденных, ничего не пишет по этой доске и возвращает ненулевой код. Это защита: молча пропущенная связь неотличима от ее отсутствия (rules/silent-failure.md).
  • Две задачи с одинаковым именем среди упомянутых в конфиге - тоже отказ: какую из них связывать, неоднозначно.
  • Ошибка в самом конфиге (project не число, зависимость от самой себя, повтор ключа, перевод строки внутри имени) останавливает весь прогон до первого сетевого вызова - ни одна доска не тронута. Ошибка на конкретной доске (ненайденные или неоднозначные задачи) останавливает только ее; остальные обрабатываются, код возврата все равно ненулевой.

Грабли

  • Маркеры блока видит заказчик. Asana показывает описание как есть, HTML-комментарии в нем не прячутся - первая версия разметки читалась служебным мусором. Поэтому границы блока человеческие.
  • Блок опознается только в начале описания. Если его сдвинули вниз (дописали текст сверху), прогон его не увидит и добавит новый блок над ним - старый останется и будет врать. Убирается руками; поиск блока по всему описанию не делается намеренно, чтобы регулярка не съела кусок чужого текста.
  • Работа идет с `html_notes`, а не с `notes`. Упоминания живут только там, а заодно это снимает прежний дефект: запись плоского notes сбрасывала списки и жирный в чужом описании. Оформление теперь переживает прогон, и отдельный гейт "описание оформлено" ушел вместе с причиной.
  • Предпросмотр печатает имена, а не gid. Он тут единственный канал контроля перед записью, а строку из идентификаторов глазами не проверить. Записываем упоминания, показываем имена.
  • Запись не транзакционна. Задачи пишутся по одной; на 429 и временных ошибках сети скрипт ждет и повторяет (Retry-After), но постоянная ошибка на середине оставляет часть доски со связями, а часть без - односторонняя картина врет сильнее, чем ее отсутствие. Лечится повторным прогоном: он идемпотентен. Не лечится двумя случаями - постоянная ошибка (403 на чужую задачу, 400 на кривые данные) будет упираться в ту же задачу каждый раз, а удаленная блокирующая задача остановит доску целиком на ненайденном имени, пока не поправят конфиг.
  • Скрипт не двигает сроки и не закрывает задачи. Только описывает связи.
  • Цикл в конфиге не ловится. A ждет B, B ждет A запишется буквально в обе стороны - смысл цепочек проверяет человек.

Прием шире Asana

Механика не про Asana: связь в тексте задачи со ссылкой на связанную ложится на любой трекер, где у задачи есть описание, - Trello, Redmine, Планфикс, issue в GitHub. Меняется только транспорт (API и поле), неизменны три вещи: обе стороны связи проставляются вместе, блок стоит первым и отбит человекочитаемым маркером, а прогон идемпотентен и снимает блок при снятии связи. Реализация под Asana дана как первая - под другой трекер переписывается движок, а не прием.

Авторизация

Порядок выбора PAT - как в asana-comments, раздел "Авторизация".

Связанные файлы

  • scripts/asana-blockers.py в корне проекта - движок (dry-run по умолчанию).
  • .asana-blockers.json в корне проекта - цепочки по доскам, ведется руками.
  • skills/asana-project - пакетное создание задач, синк сроков и закрытие отчетного периода; берет отсюда каркас технического блока (секция "СВЯЗАННЫЕ ЗАДАЧИ"); skills/asana-comments - чтение ленты комментариев.
  • rules/tasks-tracking.md - формат задач и запрет молча переносить просрочку.
All versions