Проставить связи между задачами Asana текстом в описании - чего ждет и что держит: "проставь блокировки", "видно, что мы ждем клиента". Нативные зависимости недоступны - тариф без них (402).
Installs into .claude/skills of the current project.
Are you the author of Asana Blockers?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/dewil-asana-blockers)
---
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` - формат задач и запрет молча переносить просрочку.