Skip to content
Back to skills

Asana Project

ASecurity

Пакетная работа с доской Asana из JSON-плана: развернуть, досыпать задачи, закрыть, свести часы: "разверни доску", "посчитай часы за период". Лента комментариев - `asana-comments`.

  • 8 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 29, 2026
developmentpythonbashapi

Works with

  • cli
  • api
  • mcp

Security analysis

A100/100

Scanned October 3, 2026

npx -y skills add dewil/claude-toolkit --skill asana-project --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Asana Project?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Asana Project
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/dewil-asana-project/badge)](https://www.skillsdirectory.com/skills/dewil-asana-project)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
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.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.