Skill v1.0.0
currentAutomated scan100/100version: "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
Скрипт переписывает описания живых задач на общей доске: их сразу видят заказчик и команда, а откат - руками по каждой задаче.
- Всегда сначала dry-run (дефолт, без
--send). Он печатает по задачам: какие связи встанут и где блок будет снят (СНЯТЬ БЛОК СВЯЗЕЙ). Имена в выводе человеческие, хотя записываются упоминания, - иначе проверить строку глазами невозможно. - Чей токен и чья доска - реши до `--send`. Аккаунт может состоять сразу в нескольких организациях, и клиентская доска отличается от личной одним числом в конфиге. Разведены аккаунты -
--auth <файл>обязателен в каждой команде, включая dry-run (rules/mcp-multi-account.md). - Показать вывод пользователю и получить явное "ок" на этот конкретный вывод - только потом
--send. Прошло время или доску трогали - повторить dry-run: send перечитывает доску заново. - Проверить формулировки глазами. Названия задач скрипт не пишет, но Asana подставит их в упоминание при просмотре карточки - результат тот же: внутренняя рабочая формулировка в названии задачи ("ждем, пока клиент проснется") станет видна заказчику.
CLI
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 - другой путь):
{"<ключ доски>": {"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- формат задач и запрет молча переносить просрочку.