@llodev/pm-tasks-asana 1.0.2 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.es-ES.md +100 -0
- package/README.md +48 -13
- package/README.pt-BR.md +100 -0
- package/SKILL.es-ES.md +166 -0
- package/SKILL.md +26 -1
- package/SKILL.pt-BR.md +166 -0
- package/anti-patterns/asana.md +21 -0
- package/i18n/en-US.json +24 -0
- package/i18n/es-ES.json +24 -0
- package/i18n/parity.test.mjs +33 -0
- package/i18n/pt-BR.json +24 -0
- package/package.json +8 -3
- package/schemas/config.json +4 -0
- package/scripts/init.mjs +54 -54
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# @llodev/pm-tasks-asana
|
|
2
2
|
|
|
3
|
+
## 1.1.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Document recurring gotchas observed during v1.0.x dogfood: Asana subtasks not inheriting custom fields, MCP `get_task` missing activity stories (UI is the source of truth), Trello query-string concat bug class, `create_card` ignoring `idMembers`, and `add_member_to_card` false-error returns.
|
|
8
|
+
|
|
9
|
+
## 1.1.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Ship pt-BR and es-ES translations of SKILL.md and README.md alongside the existing English originals. Adapters keep en-US as the canonical version; localized files follow the `<basename>.<lang-code>.md` convention. The package tarballs now include the localized files via the `files` field.
|
|
14
|
+
|
|
15
|
+
- Updated dependencies []:
|
|
16
|
+
- @llodev/pm-tasks-core@1.1.1
|
|
17
|
+
|
|
18
|
+
## 1.1.0
|
|
19
|
+
|
|
20
|
+
### Minor Changes
|
|
21
|
+
|
|
22
|
+
- Localize init prompts in en-US, pt-BR, es-ES. The first prompt of `npx @llodev/pm-tasks-{asana,trello} init` is now a language picker, and every subsequent prompt is rendered in the chosen locale. The selected locale is recorded as `locale` at the root of the config JSON. New helpers in `@llodev/pm-tasks-core/init-lib`: `promptLocale`, `loadStrings`, `interpolate`, `listLocales`, `registerI18nRoot`. JSON Schemas updated to accept `locale` as an enum. No breaking changes — existing configs without `locale` continue to validate and adapters keep emitting en-US prompts if the strings table is omitted.
|
|
23
|
+
|
|
24
|
+
### Patch Changes
|
|
25
|
+
|
|
26
|
+
- Updated dependencies []:
|
|
27
|
+
- @llodev/pm-tasks-core@1.1.0
|
|
28
|
+
|
|
3
29
|
## 1.0.2
|
|
4
30
|
|
|
5
31
|
### Patch Changes
|
package/README.es-ES.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
<!-- readme-selector:start -->
|
|
2
|
+
<p align="center">
|
|
3
|
+
<a href="https://github.com/llodev/skills/blob/main/pm-tasks/pm-tasks-asana/README.md"><img src="https://raw.githubusercontent.com/lloliveiradev/public-assets/main/images/usa.svg" width="30" alt="English"></a>
|
|
4
|
+
<a href="https://github.com/llodev/skills/blob/main/pm-tasks/pm-tasks-asana/README.pt-BR.md"><img src="https://raw.githubusercontent.com/lloliveiradev/public-assets/main/images/brazil.svg" width="30" alt="Português"></a>
|
|
5
|
+
<a href="https://github.com/llodev/skills/blob/main/pm-tasks/pm-tasks-asana/README.es-ES.md"><img src="https://raw.githubusercontent.com/lloliveiradev/public-assets/main/images/spain.svg" width="30" alt="Español"></a>
|
|
6
|
+
</p>
|
|
7
|
+
<!-- readme-selector:end -->
|
|
8
|
+
|
|
9
|
+
# @llodev/pm-tasks-asana
|
|
10
|
+
|
|
11
|
+
> Adapter para Asana de la familia `@llodev/pm-tasks-*` — convierte planes de implementación en parent tasks + subtasks de Asana y los opera mediante paste, MCP publish o autonomous write-through.
|
|
12
|
+
|
|
13
|
+
[](https://www.npmjs.com/package/@llodev/pm-tasks-asana)
|
|
14
|
+
[](LICENSE)
|
|
15
|
+
[](https://nodejs.org)
|
|
16
|
+
[](https://agentskills.io)
|
|
17
|
+
|
|
18
|
+
Lo que obtienes:
|
|
19
|
+
|
|
20
|
+
- **Output paste-ready** — el agente renderiza un generic card; tú lo pegas en Asana manualmente.
|
|
21
|
+
- **MCP publish** mediante el servidor MCP `claude.ai Asana` (OAuth, sin PATs en JSON). Parent task + subtasks planas + custom fields + colocación en section en un único batch.
|
|
22
|
+
- **Operaciones CRUD** sobre tasks existentes: `checklist.check`, `task.close`, `task.comment.add`, `task.due-date.set`, `task.assignee.add`.
|
|
23
|
+
- **Autonomous mode** — centinela `[autonomous]` / `--auto` para write-through bajo allowlist + scope + rate limits. Los loops multi-task replican el estado a Asana en tiempo real.
|
|
24
|
+
|
|
25
|
+
## Instalación
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# npm (con skillpm o marketplace de Claude Code)
|
|
29
|
+
npm i @llodev/pm-tasks-core @llodev/pm-tasks-asana
|
|
30
|
+
|
|
31
|
+
# Vercel CLI (instala el core manualmente también)
|
|
32
|
+
npx skills add llodev/skills/pm-tasks/pm-tasks-core
|
|
33
|
+
npx skills add llodev/skills/pm-tasks/pm-tasks-asana
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Setup del MCP
|
|
37
|
+
|
|
38
|
+
Asana usa OAuth mediante el MCP `claude.ai Asana`. Si ya conectaste tu cuenta de Asana en los ajustes de Cursor o Claude Code, ya estás listo.
|
|
39
|
+
|
|
40
|
+
Para cualquier agente compatible con MCP (Claude Code, Cursor, Codex, Windsurf, Cline, Roo Code):
|
|
41
|
+
|
|
42
|
+
1. Abre los ajustes de MCP del agente.
|
|
43
|
+
2. Activa / registra `claude.ai Asana` (o el MCP de Asana equivalente de tu agente).
|
|
44
|
+
3. Aprueba el flujo OAuth en tu navegador.
|
|
45
|
+
|
|
46
|
+
En Claude Code, verifica con `claude mcp list` — `claude.ai Asana` debe aparecer como autenticado.
|
|
47
|
+
|
|
48
|
+
## Setup de la config
|
|
49
|
+
|
|
50
|
+
El script `init` se ejecuta **fuera** del MCP, así que necesita un Personal Access Token para enumerar tus workspaces / projects / sections / custom fields. Genera uno en https://app.asana.com/0/my-apps, después:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
export LLODEV_PM_TASKS_ASANA_PAT=...
|
|
54
|
+
npx @llodev/pm-tasks-asana init
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Sigue los prompts. Elige dónde debe vivir la config:
|
|
58
|
+
|
|
59
|
+
- **local** → `./.asana.json` (recomendado para configs con scope de proyecto, se puede commitear).
|
|
60
|
+
- **global** → un default de la plataforma, personalizable. Defaults:
|
|
61
|
+
- macOS / Linux (XDG): `$XDG_CONFIG_HOME/llodev/pm-tasks/asana.json`, con fallback a `~/.config/llodev/pm-tasks/asana.json`.
|
|
62
|
+
- Windows: `%APPDATA%\llodev\pm-tasks\asana.json`.
|
|
63
|
+
- Sobrescribe en cualquier OS con `LLODEV_PM_TASKS_CONFIG_HOME=/your/path` — el archivo acaba en `$LLODEV_PM_TASKS_CONFIG_HOME/asana.json`.
|
|
64
|
+
|
|
65
|
+
El prompt de `init` imprime la ruta absoluta donde va a escribir, así siempre ves exactamente adónde va el archivo.
|
|
66
|
+
|
|
67
|
+
> [!IMPORTANT]
|
|
68
|
+
> El PAT solo lo usa el `init`. El MCP en sí usa OAuth — nunca pongas tokens en el JSON.
|
|
69
|
+
|
|
70
|
+
## Uso
|
|
71
|
+
|
|
72
|
+
| Prompt example | Qué hace el agente |
|
|
73
|
+
| ---------------------------------------------------------------- | --------------------------------------------------------------------------- |
|
|
74
|
+
| `"publish this plan as Asana tasks"` | Phase 5 publish — parent + subtasks + custom fields en un único batch |
|
|
75
|
+
| `"check subtask 3 on task X in Asana"` | Phase 6 CRUD — `checklist.check` sobre la subtask |
|
|
76
|
+
| `"close task Y"` | Phase 6 CRUD — `task.close` (mueve a la section de close + marca completed) |
|
|
77
|
+
| `"comment on task X: shipped"` | Phase 6 CRUD — `task.comment.add` |
|
|
78
|
+
| `"[autonomous] create task in asana from plan @docs/plans/X.md"` | Phase 5b autonomous (requiere `autonomous.enabled: true`) |
|
|
79
|
+
|
|
80
|
+
## Notas específicas de Asana
|
|
81
|
+
|
|
82
|
+
> [!NOTE]
|
|
83
|
+
> **Las subtasks tienen un único nivel** — el adapter aplana checklists anidadas en una sola capa de subtask.
|
|
84
|
+
|
|
85
|
+
> [!WARNING]
|
|
86
|
+
> **Los custom fields NO se heredan por defecto** — lista los IDs de field en `subtaskDefaults.inheritParentFields` para que el adapter los copie del parent a las subtasks en el momento del create.
|
|
87
|
+
|
|
88
|
+
- **Assignee es un campo único** — usa `task.assignee.add` para añadir followers; el assignee principal se reemplaza en caso de conflicto.
|
|
89
|
+
- **El `get_task` del MCP no devuelve activity stories** — el activity feed de la UI de Asana es la fuente de verdad para auditorías de atribución. Mira [`anti-patterns/asana.md`](./anti-patterns/asana.md).
|
|
90
|
+
|
|
91
|
+
## Documentación
|
|
92
|
+
|
|
93
|
+
- [`SKILL.md`](./SKILL.md) — enrutado de phases + vocabulario CRUD.
|
|
94
|
+
- [`schemas/config.json`](./schemas/config.json) — JSON Schema de `.asana.json`.
|
|
95
|
+
- [`anti-patterns/asana.md`](./anti-patterns/asana.md) — gotchas recurrentes.
|
|
96
|
+
- [`../pm-tasks-core/references/autonomous-mode.md`](../pm-tasks-core/references/autonomous-mode.md) — contrato de autonomous mode.
|
|
97
|
+
|
|
98
|
+
## Licencia
|
|
99
|
+
|
|
100
|
+
MIT — mira [LICENSE](./LICENSE).
|
package/README.md
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
|
+
<!-- readme-selector:start -->
|
|
2
|
+
<p align="center">
|
|
3
|
+
<a href="https://github.com/llodev/skills/blob/main/pm-tasks/pm-tasks-asana/README.md"><img src="https://raw.githubusercontent.com/lloliveiradev/public-assets/main/images/usa.svg" width="30" alt="English"></a>
|
|
4
|
+
<a href="https://github.com/llodev/skills/blob/main/pm-tasks/pm-tasks-asana/README.pt-BR.md"><img src="https://raw.githubusercontent.com/lloliveiradev/public-assets/main/images/brazil.svg" width="30" alt="Português"></a>
|
|
5
|
+
<a href="https://github.com/llodev/skills/blob/main/pm-tasks/pm-tasks-asana/README.es-ES.md"><img src="https://raw.githubusercontent.com/lloliveiradev/public-assets/main/images/spain.svg" width="30" alt="Español"></a>
|
|
6
|
+
</p>
|
|
7
|
+
<!-- readme-selector:end -->
|
|
8
|
+
|
|
1
9
|
# @llodev/pm-tasks-asana
|
|
2
10
|
|
|
3
|
-
Asana adapter for the `@llodev/pm-tasks-*` family
|
|
11
|
+
> Asana adapter for the `@llodev/pm-tasks-*` family — turn implementation plans into Asana parent tasks + subtasks and operate them via paste, MCP publish, or autonomous write-through.
|
|
12
|
+
|
|
13
|
+
[](https://www.npmjs.com/package/@llodev/pm-tasks-asana)
|
|
14
|
+
[](LICENSE)
|
|
15
|
+
[](https://nodejs.org)
|
|
16
|
+
[](https://agentskills.io)
|
|
17
|
+
|
|
18
|
+
What you get:
|
|
19
|
+
|
|
20
|
+
- **Paste-ready output** — agent renders a generic card; you paste into Asana manually.
|
|
21
|
+
- **MCP publish** via the `claude.ai Asana` MCP server (OAuth, no PATs in JSON). Parent task + flat subtasks + custom fields + section placement in one batch.
|
|
22
|
+
- **CRUD operations** on existing tasks: `checklist.check`, `task.close`, `task.comment.add`, `task.due-date.set`, `task.assignee.add`.
|
|
23
|
+
- **Autonomous mode** — `[autonomous]` / `--auto` sentinel for write-through under allowlist + scope + rate limits. Multi-task loops mirror state to Asana in real time.
|
|
4
24
|
|
|
5
25
|
## Install
|
|
6
26
|
|
|
@@ -9,13 +29,13 @@ Asana adapter for the `@llodev/pm-tasks-*` family. Convert implementation plans
|
|
|
9
29
|
npm i @llodev/pm-tasks-core @llodev/pm-tasks-asana
|
|
10
30
|
|
|
11
31
|
# Vercel CLI (install core manually too)
|
|
12
|
-
npx skills add llodev/skills/pm-tasks-core
|
|
13
|
-
npx skills add llodev/skills/pm-tasks-asana
|
|
32
|
+
npx skills add llodev/skills/pm-tasks/pm-tasks-core
|
|
33
|
+
npx skills add llodev/skills/pm-tasks/pm-tasks-asana
|
|
14
34
|
```
|
|
15
35
|
|
|
16
36
|
## Setup the MCP
|
|
17
37
|
|
|
18
|
-
Asana uses OAuth via the `claude.ai Asana` MCP. If you've already connected your Asana account in Cursor or Claude Code settings, you're done
|
|
38
|
+
Asana uses OAuth via the `claude.ai Asana` MCP. If you've already connected your Asana account in Cursor or Claude Code settings, you're done.
|
|
19
39
|
|
|
20
40
|
For any MCP-capable agent (Claude Code, Cursor, Codex, Windsurf, Cline, Roo Code):
|
|
21
41
|
|
|
@@ -23,7 +43,7 @@ For any MCP-capable agent (Claude Code, Cursor, Codex, Windsurf, Cline, Roo Code
|
|
|
23
43
|
2. Enable / register `claude.ai Asana` (or your agent's equivalent Asana MCP).
|
|
24
44
|
3. Approve the OAuth flow in your browser.
|
|
25
45
|
|
|
26
|
-
In Claude Code, verify with `claude mcp list` — `claude.ai Asana` should appear as authenticated.
|
|
46
|
+
In Claude Code, verify with `claude mcp list` — `claude.ai Asana` should appear as authenticated.
|
|
27
47
|
|
|
28
48
|
## Setup the config
|
|
29
49
|
|
|
@@ -44,21 +64,36 @@ Walk through the prompts. Pick where the config should live:
|
|
|
44
64
|
|
|
45
65
|
The `init` prompt prints the absolute path it will write to, so you always see exactly where the file goes.
|
|
46
66
|
|
|
47
|
-
|
|
67
|
+
> [!IMPORTANT]
|
|
68
|
+
> The PAT is **only** used by `init`. The MCP itself uses OAuth — never put tokens in the JSON.
|
|
48
69
|
|
|
49
70
|
## Use
|
|
50
71
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
72
|
+
| Prompt example | What the agent does |
|
|
73
|
+
| ---------------------------------------------------------------- | --------------------------------------------------------------------- |
|
|
74
|
+
| `"publish this plan as Asana tasks"` | Phase 5 publish — parent + subtasks + custom fields in one batch |
|
|
75
|
+
| `"check subtask 3 on task X in Asana"` | Phase 6 CRUD — `checklist.check` on the subtask |
|
|
76
|
+
| `"close task Y"` | Phase 6 CRUD — `task.close` (moves to close section + sets completed) |
|
|
77
|
+
| `"comment on task X: shipped"` | Phase 6 CRUD — `task.comment.add` |
|
|
78
|
+
| `"[autonomous] create task in asana from plan @docs/plans/X.md"` | Phase 5b autonomous (requires `autonomous.enabled: true`) |
|
|
56
79
|
|
|
57
80
|
## Asana-specific notes
|
|
58
81
|
|
|
59
|
-
|
|
60
|
-
|
|
82
|
+
> [!NOTE]
|
|
83
|
+
> **Subtasks are one level deep** — the adapter flattens nested checklists into a single subtask layer.
|
|
84
|
+
|
|
85
|
+
> [!WARNING]
|
|
86
|
+
> **Custom fields do NOT inherit by default** — list field IDs in `subtaskDefaults.inheritParentFields` so the adapter copies them from parent to subtasks at create time.
|
|
87
|
+
|
|
61
88
|
- **Assignee is a single field** — use `task.assignee.add` to add followers; the primary assignee replaces on conflict.
|
|
89
|
+
- **MCP `get_task` doesn't return activity stories** — the Asana UI activity feed is the source of truth for attribution audits. See [`anti-patterns/asana.md`](./anti-patterns/asana.md).
|
|
90
|
+
|
|
91
|
+
## Documentation
|
|
92
|
+
|
|
93
|
+
- [`SKILL.md`](./SKILL.md) — phase routing + CRUD vocabulary.
|
|
94
|
+
- [`schemas/config.json`](./schemas/config.json) — `.asana.json` JSON Schema.
|
|
95
|
+
- [`anti-patterns/asana.md`](./anti-patterns/asana.md) — recurring gotchas.
|
|
96
|
+
- [`../pm-tasks-core/references/autonomous-mode.md`](../pm-tasks-core/references/autonomous-mode.md) — autonomous-mode contract.
|
|
62
97
|
|
|
63
98
|
## License
|
|
64
99
|
|
package/README.pt-BR.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
<!-- readme-selector:start -->
|
|
2
|
+
<p align="center">
|
|
3
|
+
<a href="https://github.com/llodev/skills/blob/main/pm-tasks/pm-tasks-asana/README.md"><img src="https://raw.githubusercontent.com/lloliveiradev/public-assets/main/images/usa.svg" width="30" alt="English"></a>
|
|
4
|
+
<a href="https://github.com/llodev/skills/blob/main/pm-tasks/pm-tasks-asana/README.pt-BR.md"><img src="https://raw.githubusercontent.com/lloliveiradev/public-assets/main/images/brazil.svg" width="30" alt="Português"></a>
|
|
5
|
+
<a href="https://github.com/llodev/skills/blob/main/pm-tasks/pm-tasks-asana/README.es-ES.md"><img src="https://raw.githubusercontent.com/lloliveiradev/public-assets/main/images/spain.svg" width="30" alt="Español"></a>
|
|
6
|
+
</p>
|
|
7
|
+
<!-- readme-selector:end -->
|
|
8
|
+
|
|
9
|
+
# @llodev/pm-tasks-asana
|
|
10
|
+
|
|
11
|
+
> Adapter para Asana da família `@llodev/pm-tasks-*` — transforma planos de implementação em parent tasks + subtasks no Asana e opera via paste, MCP publish ou autonomous write-through.
|
|
12
|
+
|
|
13
|
+
[](https://www.npmjs.com/package/@llodev/pm-tasks-asana)
|
|
14
|
+
[](LICENSE)
|
|
15
|
+
[](https://nodejs.org)
|
|
16
|
+
[](https://agentskills.io)
|
|
17
|
+
|
|
18
|
+
O que você ganha:
|
|
19
|
+
|
|
20
|
+
- **Output paste-ready** — o agente renderiza um generic card; você cola no Asana manualmente.
|
|
21
|
+
- **MCP publish** via servidor MCP `claude.ai Asana` (OAuth, sem PATs no JSON). Parent task + subtasks flat + custom fields + placement em section, tudo num batch só.
|
|
22
|
+
- **Operações CRUD** em tasks existentes: `checklist.check`, `task.close`, `task.comment.add`, `task.due-date.set`, `task.assignee.add`.
|
|
23
|
+
- **Autonomous mode** — sentinela `[autonomous]` / `--auto` para write-through sob allowlist + scope + rate limits. Loops multi-task espelham o estado para o Asana em tempo real.
|
|
24
|
+
|
|
25
|
+
## Instalação
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# npm (com skillpm ou marketplace do Claude Code)
|
|
29
|
+
npm i @llodev/pm-tasks-core @llodev/pm-tasks-asana
|
|
30
|
+
|
|
31
|
+
# Vercel CLI (instale o core manualmente também)
|
|
32
|
+
npx skills add llodev/skills/pm-tasks/pm-tasks-core
|
|
33
|
+
npx skills add llodev/skills/pm-tasks/pm-tasks-asana
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Setup do MCP
|
|
37
|
+
|
|
38
|
+
O Asana usa OAuth via o MCP `claude.ai Asana`. Se você já conectou sua conta do Asana nas configurações do Cursor ou Claude Code, está pronto.
|
|
39
|
+
|
|
40
|
+
Para qualquer agente compatível com MCP (Claude Code, Cursor, Codex, Windsurf, Cline, Roo Code):
|
|
41
|
+
|
|
42
|
+
1. Abra as configurações de MCP do agente.
|
|
43
|
+
2. Habilite / registre `claude.ai Asana` (ou o MCP de Asana equivalente do seu agente).
|
|
44
|
+
3. Aprove o fluxo OAuth no navegador.
|
|
45
|
+
|
|
46
|
+
No Claude Code, verifique com `claude mcp list` — `claude.ai Asana` deve aparecer como autenticado.
|
|
47
|
+
|
|
48
|
+
## Setup da config
|
|
49
|
+
|
|
50
|
+
O script `init` roda **fora** do MCP, então precisa de um Personal Access Token para enumerar seus workspaces / projects / sections / custom fields. Gere um em https://app.asana.com/0/my-apps, depois:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
export LLODEV_PM_TASKS_ASANA_PAT=...
|
|
54
|
+
npx @llodev/pm-tasks-asana init
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Siga os prompts. Escolha onde a config deve ficar:
|
|
58
|
+
|
|
59
|
+
- **local** → `./.asana.json` (recomendado para configs com escopo de projeto, pode ser commitado).
|
|
60
|
+
- **global** → um default da plataforma, customizável. Defaults:
|
|
61
|
+
- macOS / Linux (XDG): `$XDG_CONFIG_HOME/llodev/pm-tasks/asana.json`, com fallback para `~/.config/llodev/pm-tasks/asana.json`.
|
|
62
|
+
- Windows: `%APPDATA%\llodev\pm-tasks\asana.json`.
|
|
63
|
+
- Sobrescreva em qualquer OS com `LLODEV_PM_TASKS_CONFIG_HOME=/your/path` — o arquivo vai parar em `$LLODEV_PM_TASKS_CONFIG_HOME/asana.json`.
|
|
64
|
+
|
|
65
|
+
O prompt de `init` imprime o caminho absoluto onde vai escrever, então você sempre vê exatamente para onde o arquivo vai.
|
|
66
|
+
|
|
67
|
+
> [!IMPORTANT]
|
|
68
|
+
> O PAT é usado **somente** pelo `init`. O MCP em si usa OAuth — nunca coloque tokens no JSON.
|
|
69
|
+
|
|
70
|
+
## Uso
|
|
71
|
+
|
|
72
|
+
| Prompt example | O que o agente faz |
|
|
73
|
+
| ---------------------------------------------------------------- | ---------------------------------------------------------------------------- |
|
|
74
|
+
| `"publish this plan as Asana tasks"` | Phase 5 publish — parent + subtasks + custom fields num batch só |
|
|
75
|
+
| `"check subtask 3 on task X in Asana"` | Phase 6 CRUD — `checklist.check` na subtask |
|
|
76
|
+
| `"close task Y"` | Phase 6 CRUD — `task.close` (move para a section de close + marca completed) |
|
|
77
|
+
| `"comment on task X: shipped"` | Phase 6 CRUD — `task.comment.add` |
|
|
78
|
+
| `"[autonomous] create task in asana from plan @docs/plans/X.md"` | Phase 5b autonomous (requer `autonomous.enabled: true`) |
|
|
79
|
+
|
|
80
|
+
## Notas específicas do Asana
|
|
81
|
+
|
|
82
|
+
> [!NOTE]
|
|
83
|
+
> **Subtasks têm um nível só** — o adapter achata checklists aninhadas em uma única camada de subtask.
|
|
84
|
+
|
|
85
|
+
> [!WARNING]
|
|
86
|
+
> **Custom fields NÃO são herdados por default** — liste os IDs de field em `subtaskDefaults.inheritParentFields` para que o adapter copie do parent para as subtasks no momento do create.
|
|
87
|
+
|
|
88
|
+
- **Assignee é um campo único** — use `task.assignee.add` para adicionar followers; o assignee primário é substituído em caso de conflito.
|
|
89
|
+
- **O `get_task` do MCP não retorna activity stories** — o activity feed da UI do Asana é a fonte de verdade para auditorias de atribuição. Veja [`anti-patterns/asana.md`](./anti-patterns/asana.md).
|
|
90
|
+
|
|
91
|
+
## Documentação
|
|
92
|
+
|
|
93
|
+
- [`SKILL.md`](./SKILL.md) — roteamento de phases + vocabulário CRUD.
|
|
94
|
+
- [`schemas/config.json`](./schemas/config.json) — JSON Schema do `.asana.json`.
|
|
95
|
+
- [`anti-patterns/asana.md`](./anti-patterns/asana.md) — pegadinhas recorrentes.
|
|
96
|
+
- [`../pm-tasks-core/references/autonomous-mode.md`](../pm-tasks-core/references/autonomous-mode.md) — contrato do autonomous mode.
|
|
97
|
+
|
|
98
|
+
## Licença
|
|
99
|
+
|
|
100
|
+
MIT — veja [LICENSE](./LICENSE).
|
package/SKILL.es-ES.md
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pm-tasks-asana
|
|
3
|
+
description: >-
|
|
4
|
+
Asana adapter for the @llodev/pm-tasks-* family. Use when the user mentions
|
|
5
|
+
Asana, asks to "create Asana task", "publish to Asana", "post to Asana",
|
|
6
|
+
"publish", "add comment in Asana", or uses --publish-asana; OR for CRUD on
|
|
7
|
+
existing tasks (check subtask, close task, change due-date, assign person,
|
|
8
|
+
comment); OR when invoked autonomously by another agent with [autonomous] /
|
|
9
|
+
--auto sentinel. Asana hierarchy: workspace > project > section > parent task
|
|
10
|
+
> subtasks (one level), with custom fields and multi-assignee support. Modes:
|
|
11
|
+
paste-ready (no MCP needed), MCP publish (via claude.ai Asana MCP), autonomous
|
|
12
|
+
(write-through with allowlist). Implements 6 CRUD verbs (task.create,
|
|
13
|
+
checklist.check, task.close, task.due-date.set, task.assignee.add,
|
|
14
|
+
task.comment.add) from pm-tasks/pm-tasks-core/references/contract.md. Requires
|
|
15
|
+
@llodev/pm-tasks-core installed.
|
|
16
|
+
license: MIT
|
|
17
|
+
metadata:
|
|
18
|
+
version: 1.1.2
|
|
19
|
+
tags:
|
|
20
|
+
- agent-skill
|
|
21
|
+
- asana
|
|
22
|
+
- plan-to-tasks
|
|
23
|
+
- pm-tools
|
|
24
|
+
family: pm-tasks
|
|
25
|
+
role: adapter
|
|
26
|
+
tool: asana
|
|
27
|
+
compatibility:
|
|
28
|
+
agents:
|
|
29
|
+
- claude-code
|
|
30
|
+
- cursor
|
|
31
|
+
- codex
|
|
32
|
+
- windsurf
|
|
33
|
+
- cline
|
|
34
|
+
- roo-code
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
<!-- readme-selector:start -->
|
|
38
|
+
<p align="center">
|
|
39
|
+
<a href="https://github.com/llodev/skills/blob/main/pm-tasks/pm-tasks-asana/SKILL.md"><img src="https://raw.githubusercontent.com/lloliveiradev/public-assets/main/images/usa.svg" width="30" alt="English"></a>
|
|
40
|
+
<a href="https://github.com/llodev/skills/blob/main/pm-tasks/pm-tasks-asana/SKILL.pt-BR.md"><img src="https://raw.githubusercontent.com/lloliveiradev/public-assets/main/images/brazil.svg" width="30" alt="Português"></a>
|
|
41
|
+
<a href="https://github.com/llodev/skills/blob/main/pm-tasks/pm-tasks-asana/SKILL.es-ES.md"><img src="https://raw.githubusercontent.com/lloliveiradev/public-assets/main/images/spain.svg" width="30" alt="Español"></a>
|
|
42
|
+
</p>
|
|
43
|
+
<!-- readme-selector:end -->
|
|
44
|
+
|
|
45
|
+
# pm-tasks-asana
|
|
46
|
+
|
|
47
|
+
Adaptador para Asana dentro de la familia `@llodev/pm-tasks-*`. Usa las fases de extracción de la skill núcleo, luego aplica el formato de Asana y, opcionalmente, publica/opera vía el servidor MCP `claude.ai Asana`.
|
|
48
|
+
|
|
49
|
+
## Enrutamiento
|
|
50
|
+
|
|
51
|
+
| Modo | Disparador | Ruta |
|
|
52
|
+
| ----------- | ----------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
|
|
53
|
+
| Paste-only | "formatear como tarea de Asana" sin intención de MCP | Fase 3 (core) → Fase 4 (esta skill, solo formato) → bloques paste de salida |
|
|
54
|
+
| MCP publish | "publish to Asana", "create on Asana", "--publish-asana" | Fase 3 → Fase 4 → Fase 5 (publish vía MCP) |
|
|
55
|
+
| Autónomo | `[autonomous]` o `--auto` en el prompt O `LLODEV_PM_TASKS_AUTONOMOUS=1` | Fase 3 → Fase 4 → Fase 5b (write-through, sin preview) |
|
|
56
|
+
| Ops CRUD | "marcar subtarea N en tarea X", "cerrar tarea Y", "asignar Alice a la tarea Z", "comentar en tarea X" | Fase 6 (operaciones, dispatch directo del verbo) |
|
|
57
|
+
|
|
58
|
+
## Modelo de Asana
|
|
59
|
+
|
|
60
|
+
Las tareas de Asana tienen:
|
|
61
|
+
|
|
62
|
+
- **Nombre** (título, ≤80 chars para la vista de tablero).
|
|
63
|
+
- **Descripción** (rich text; prefiere etiquetas `**Section**` en negrita — los headings `##` se renderizan de forma inconsistente).
|
|
64
|
+
- **Subtareas** — un nivel de profundidad. Los custom fields y el assignee NO se propagan automáticamente desde el parent; el adaptador los establece explícitamente según `subtaskDefaults.inheritParentFields` en `.asana.json`.
|
|
65
|
+
- **Secciones** — agrupan tareas dentro de un project.
|
|
66
|
+
- **Custom fields** — por project; la API siempre usa GIDs de opción, nunca nombres visibles.
|
|
67
|
+
- **Multi-assignee** — Asana permite múltiples followers; el assignee primario es un campo único. Usa `task.assignee.add` para añadir followers.
|
|
68
|
+
|
|
69
|
+
## Fase 4 — Formato de Asana
|
|
70
|
+
|
|
71
|
+
Aplica el generic card del core [`../pm-tasks-core/references/generic-card.md`](../pm-tasks-core/references/generic-card.md). Luego mapea a Asana:
|
|
72
|
+
|
|
73
|
+
- Título → `name` de la tarea.
|
|
74
|
+
- Secciones del generic card → etiquetas `**Section**` en negrita dentro de `description` (no `##`).
|
|
75
|
+
- "Implementation Checklist" + "Verification Checklist" → subtareas (aplana cualquier bullet anidado; Asana solo admite un nivel).
|
|
76
|
+
- Labels → opciones de custom field (resueltas vía `customFields[]` de `.asana.json`).
|
|
77
|
+
- Due date → `due_on` (YYYY-MM-DD).
|
|
78
|
+
- Assignee → GID de `assignee` resuelto desde `members[]` de `.asana.json` o `me` al momento del publish.
|
|
79
|
+
|
|
80
|
+
## Fase 5 — MCP publish
|
|
81
|
+
|
|
82
|
+
**Prerrequisitos:** servidor MCP de Asana (`claude.ai Asana`) conectado en tu agente. El MCP gestiona OAuth; el adaptador nunca ve tokens. Los pasos de configuración varían por agente — registra el mismo endpoint MCP de Asana que tu agente admita:
|
|
83
|
+
|
|
84
|
+
- **Claude Code**: `claude mcp add asana -s project -- npx -y claude-ai-asana-mcp` (o sigue el setup de Anthropic para el conector hospedado `claude.ai Asana`).
|
|
85
|
+
- **Cursor / Windsurf / Cline / Roo Code**: añade una entrada en el JSON de settings MCP de ese agente apuntando al mismo comando `claude-ai-asana-mcp` (envelope idéntico al ejemplo de Trello en `pm-tasks-trello/references/mcp-config.md`).
|
|
86
|
+
- **Codex**: entrada TOML en `[mcp_servers.asana]` dentro de `~/.codex/config.toml`.
|
|
87
|
+
- **Otros agentes capaces de MCP**: consulta la doc MCP de ese agente; el comando del servidor y el flujo OAuth son constantes.
|
|
88
|
+
|
|
89
|
+
Orden estricto: 5.1 lee `.asana.json` (archivo completo) → 5.2.5 resuelve assignee + custom fields + mapa de fields por subtarea → 5.2 preview & aprobación → 5.3 publish vía MCP → 5.4 manejo de errores.
|
|
90
|
+
|
|
91
|
+
Secuencia del MCP publish:
|
|
92
|
+
|
|
93
|
+
1. **Tarea padre** — `create_tasks` con `name`, `notes` (descripción), `projects: [projectGid]`, `memberships: [{ project, section }]`, `assignee` (GID resuelto), `due_on`, `custom_fields` (string JSON de `{fieldGid: optionGid}`).
|
|
94
|
+
2. **Subtareas** — `create_tasks` por subtarea con `parent: parentGid`, `name`, `assignee` (heredado o por subtarea), `custom_fields` según `subtaskDefaults.inheritParentFields`.
|
|
95
|
+
3. **Tags** (opcional) — `addTag` por GID de tag.
|
|
96
|
+
4. **Confirmación** — lista parent + subtareas con permalinks.
|
|
97
|
+
|
|
98
|
+
## Fase 5b — Autónomo
|
|
99
|
+
|
|
100
|
+
Omite 5.2 preview & aprobación. Aplica el contrato de modo autónomo de [`../pm-tasks-core/references/autonomous-mode.md`](../pm-tasks-core/references/autonomous-mode.md). Entradas del log de auditoría según [`../pm-tasks-core/references/audit-log-format.md`](../pm-tasks-core/references/audit-log-format.md).
|
|
101
|
+
|
|
102
|
+
Scope autónomo específico de Asana: `autonomous.scope.projects[]` + `autonomous.scope.sections[]` deben incluir los GIDs objetivo. Cualquier escritura en custom field debe estar en `autonomous.allow` (`task.create` cubre el set de campos al crear; los cambios continuos de fields están fuera del scope de v1.x).
|
|
103
|
+
|
|
104
|
+
## Fase 6 — Operaciones CRUD (tareas existentes)
|
|
105
|
+
|
|
106
|
+
Para verbos distintos de `task.create`, salta directo a la operación. Mapeo verbo → tool MCP:
|
|
107
|
+
|
|
108
|
+
| Verbo del core | Tool MCP de Asana | Notas |
|
|
109
|
+
| ------------------- | ------------------------------ | ------------------------------------------------------------------------- |
|
|
110
|
+
| `task.create` | `create_tasks` | parent + subtareas según Fase 5 |
|
|
111
|
+
| `checklist.check` | `update_tasks` | para subtareas: `completed: true`; emula checklist vía modelo de subtarea |
|
|
112
|
+
| `task.close` | `update_tasks` | `completed: true` en el parent |
|
|
113
|
+
| `task.due-date.set` | `update_tasks` | `due_on: "YYYY-MM-DD"` |
|
|
114
|
+
| `task.assignee.add` | `update_tasks` + `addFollower` | el assignee primario reemplaza; los adicionales se vuelven followers |
|
|
115
|
+
| `task.comment.add` | `add_comment` (story) | añade una comment story a la tarea |
|
|
116
|
+
|
|
117
|
+
Resolución de `<task-ref>`: acepta permalinks de Asana (`https://app.asana.com/0/<project>/<task>`), GIDs desnudos, o aliases de `taskAliases[]` en `.asana.json`.
|
|
118
|
+
|
|
119
|
+
## Envelope de resultado
|
|
120
|
+
|
|
121
|
+
Cada verbo retorna la shape del contrato del core (consulta [`../pm-tasks-core/references/contract.md`](../pm-tasks-core/references/contract.md) §Result envelope):
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
{
|
|
125
|
+
"ok": true,
|
|
126
|
+
"verb": "task.create",
|
|
127
|
+
"tool": "asana",
|
|
128
|
+
"ref": { "id": "<gid>", "url": "https://app.asana.com/0/<project>/<gid>", "alias": "<optional>" },
|
|
129
|
+
"details": {
|
|
130
|
+
/* específico de Asana (ver tabla abajo) */
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
`details` específicos de Asana por verbo:
|
|
136
|
+
|
|
137
|
+
| Verbo | Campos de `details` |
|
|
138
|
+
| ------------------- | ------------------------------------------------------------------------ |
|
|
139
|
+
| `task.create` | `{ parentGid, subtaskGids[], projectGid, sectionGid?, customFields[]? }` |
|
|
140
|
+
| `checklist.check` | `{ subtaskGid, completed: true }` |
|
|
141
|
+
| `task.close` | `{ parentGid, completed: true }` |
|
|
142
|
+
| `task.due-date.set` | `{ taskGid, due_on }` |
|
|
143
|
+
| `task.assignee.add` | `{ taskGid, assignee, followers[]? }` (split entre primario y follower) |
|
|
144
|
+
| `task.comment.add` | `{ taskGid, storyGid }` |
|
|
145
|
+
|
|
146
|
+
Ante fallo: `{ ok: false, verb, tool, error: { code, message, retriable } }`. Códigos comunes: `FORBIDDEN_VERB`, `OUT_OF_SCOPE`, `NOT_FOUND`, `RATE_LIMITED`, `PARTIAL_CREATE` (la subtarea falló a mitad del create — consulta [`../pm-tasks-core/references/contract.md`](../pm-tasks-core/references/contract.md) §Partial-create recovery).
|
|
147
|
+
|
|
148
|
+
## Anti-patterns
|
|
149
|
+
|
|
150
|
+
Consulta [`anti-patterns/asana.md`](anti-patterns/asana.md) — paste health, reglas de custom field, requisitos de GID, manejo de partial-create.
|
|
151
|
+
|
|
152
|
+
## Fallback standalone
|
|
153
|
+
|
|
154
|
+
Si `@llodev/pm-tasks-core` no está instalado: pide al usuario la entrada mínima (título + nombres de subtareas) y produce un cuerpo de tarea de Asana listo para pegar a partir de ese contenido solo. La calidad se degrada — sin inferencia de scope/audience/fidelity. Imprime: _"Install `@llodev/pm-tasks-core` for the full flow."_
|
|
155
|
+
|
|
156
|
+
## Config
|
|
157
|
+
|
|
158
|
+
Orden de búsqueda: `<git-root>/.asana.json` → `~/.config/llodev/pm-tasks/asana.json` → aborta con instrucciones de init. Schema: [`schemas/config.json`](schemas/config.json). Los secretos NUNCA en JSON — el MCP de Asana sostiene el OAuth; el `init` usa únicamente la env var `LLODEV_PM_TASKS_ASANA_PAT`.
|
|
159
|
+
|
|
160
|
+
## Init
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
npx @llodev/pm-tasks-asana init
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Consulta [`../pm-tasks-core/references/init-ux.md`](../pm-tasks-core/references/init-ux.md) para el flujo compartido. El init de Asana lee workspaces / projects / sections / custom fields vía la REST API de Asana usando un Personal Access Token (env `LLODEV_PM_TASKS_ASANA_PAT`).
|
package/SKILL.md
CHANGED
|
@@ -15,7 +15,7 @@ description: >-
|
|
|
15
15
|
@llodev/pm-tasks-core installed.
|
|
16
16
|
license: MIT
|
|
17
17
|
metadata:
|
|
18
|
-
version: 1.
|
|
18
|
+
version: 1.1.2
|
|
19
19
|
tags:
|
|
20
20
|
- agent-skill
|
|
21
21
|
- asana
|
|
@@ -34,6 +34,14 @@ compatibility:
|
|
|
34
34
|
- roo-code
|
|
35
35
|
---
|
|
36
36
|
|
|
37
|
+
<!-- readme-selector:start -->
|
|
38
|
+
<p align="center">
|
|
39
|
+
<a href="https://github.com/llodev/skills/blob/main/pm-tasks/pm-tasks-asana/SKILL.md"><img src="https://raw.githubusercontent.com/lloliveiradev/public-assets/main/images/usa.svg" width="30" alt="English"></a>
|
|
40
|
+
<a href="https://github.com/llodev/skills/blob/main/pm-tasks/pm-tasks-asana/SKILL.pt-BR.md"><img src="https://raw.githubusercontent.com/lloliveiradev/public-assets/main/images/brazil.svg" width="30" alt="Português"></a>
|
|
41
|
+
<a href="https://github.com/llodev/skills/blob/main/pm-tasks/pm-tasks-asana/SKILL.es-ES.md"><img src="https://raw.githubusercontent.com/lloliveiradev/public-assets/main/images/spain.svg" width="30" alt="Español"></a>
|
|
42
|
+
</p>
|
|
43
|
+
<!-- readme-selector:end -->
|
|
44
|
+
|
|
37
45
|
# pm-tasks-asana
|
|
38
46
|
|
|
39
47
|
Adapter for Asana within the `@llodev/pm-tasks-*` family. Use the core skill's extraction phases, then apply Asana formatting and optionally publish/operate via the `claude.ai Asana` MCP server.
|
|
@@ -93,6 +101,23 @@ Skip 5.2 preview & approval. Apply autonomous-mode contract from [`../pm-tasks-c
|
|
|
93
101
|
|
|
94
102
|
Asana-specific autonomous scope: `autonomous.scope.projects[]` + `autonomous.scope.sections[]` must include the target GIDs. Any custom-field write must be in `autonomous.allow` (`task.create` covers create-time field set; ongoing field changes are out of scope for v1.x).
|
|
95
103
|
|
|
104
|
+
### Continuous operation in multi-task loops
|
|
105
|
+
|
|
106
|
+
Mandatory when the controller is executing a plan with multiple tasks autonomously. Mirror each task transition on the corresponding Asana task in real time — NEVER batch state updates at end-of-loop. See [`../pm-tasks-core/references/autonomous-mode.md`](../pm-tasks-core/references/autonomous-mode.md) § **Continuous operation across multi-task loops** for the full contract and anti-patterns.
|
|
107
|
+
|
|
108
|
+
Asana-specific verb mapping:
|
|
109
|
+
|
|
110
|
+
| Transition | MCP call(s) |
|
|
111
|
+
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
112
|
+
| Task start | `mcp__asana__update_tasks { task: <gid>, add_projects: [{ project_id, section_id: <wipSectionGid> }] }` |
|
|
113
|
+
| Step complete (subtask) | `mcp__asana__update_tasks { task: <subtaskGid>, completed: true }` |
|
|
114
|
+
| Task complete (full) | `mcp__asana__add_comment { task_id: <parentGid>, text: "🤖 [agent] Task complete. Commit: <SHA>. <branch>." }` then `mcp__asana__update_tasks { task: <parentGid>, completed: true, add_projects: [{ project_id, section_id: <doneSectionGid> }] }` |
|
|
115
|
+
| Task failed | `mcp__asana__add_comment` with failure mode + `mcp__asana__update_tasks { task: <gid>, add_followers: ["<escalateToAliasGid>"] }` for human escalation. Do NOT close. |
|
|
116
|
+
|
|
117
|
+
Resolve `<wipSectionGid>` / `<doneSectionGid>` from `.asana.json` `sections[]` by alias (`wip`, `done`, or `closeSectionAlias` from `defaults`). Both must already be in `autonomous.scope.sections` — otherwise the verb returns `OUT_OF_SCOPE`.
|
|
118
|
+
|
|
119
|
+
**Asana caveat (per [`anti-patterns/asana.md`](./anti-patterns/asana.md)):** the MCP `get_task` doesn't return activity stories, so verifying the lifecycle programmatically is incomplete. The UI activity feed IS the human's audit log — keep it dense and accurate.
|
|
120
|
+
|
|
96
121
|
## Phase 6 — CRUD operations (existing tasks)
|
|
97
122
|
|
|
98
123
|
For verbs other than `task.create`, jump directly to the operation. Verb → MCP tool mapping:
|
package/SKILL.pt-BR.md
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pm-tasks-asana
|
|
3
|
+
description: >-
|
|
4
|
+
Asana adapter for the @llodev/pm-tasks-* family. Use when the user mentions
|
|
5
|
+
Asana, asks to "create Asana task", "publish to Asana", "post to Asana",
|
|
6
|
+
"publish", "add comment in Asana", or uses --publish-asana; OR for CRUD on
|
|
7
|
+
existing tasks (check subtask, close task, change due-date, assign person,
|
|
8
|
+
comment); OR when invoked autonomously by another agent with [autonomous] /
|
|
9
|
+
--auto sentinel. Asana hierarchy: workspace > project > section > parent task
|
|
10
|
+
> subtasks (one level), with custom fields and multi-assignee support. Modes:
|
|
11
|
+
paste-ready (no MCP needed), MCP publish (via claude.ai Asana MCP), autonomous
|
|
12
|
+
(write-through with allowlist). Implements 6 CRUD verbs (task.create,
|
|
13
|
+
checklist.check, task.close, task.due-date.set, task.assignee.add,
|
|
14
|
+
task.comment.add) from pm-tasks/pm-tasks-core/references/contract.md. Requires
|
|
15
|
+
@llodev/pm-tasks-core installed.
|
|
16
|
+
license: MIT
|
|
17
|
+
metadata:
|
|
18
|
+
version: 1.1.2
|
|
19
|
+
tags:
|
|
20
|
+
- agent-skill
|
|
21
|
+
- asana
|
|
22
|
+
- plan-to-tasks
|
|
23
|
+
- pm-tools
|
|
24
|
+
family: pm-tasks
|
|
25
|
+
role: adapter
|
|
26
|
+
tool: asana
|
|
27
|
+
compatibility:
|
|
28
|
+
agents:
|
|
29
|
+
- claude-code
|
|
30
|
+
- cursor
|
|
31
|
+
- codex
|
|
32
|
+
- windsurf
|
|
33
|
+
- cline
|
|
34
|
+
- roo-code
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
<!-- readme-selector:start -->
|
|
38
|
+
<p align="center">
|
|
39
|
+
<a href="https://github.com/llodev/skills/blob/main/pm-tasks/pm-tasks-asana/SKILL.md"><img src="https://raw.githubusercontent.com/lloliveiradev/public-assets/main/images/usa.svg" width="30" alt="English"></a>
|
|
40
|
+
<a href="https://github.com/llodev/skills/blob/main/pm-tasks/pm-tasks-asana/SKILL.pt-BR.md"><img src="https://raw.githubusercontent.com/lloliveiradev/public-assets/main/images/brazil.svg" width="30" alt="Português"></a>
|
|
41
|
+
<a href="https://github.com/llodev/skills/blob/main/pm-tasks/pm-tasks-asana/SKILL.es-ES.md"><img src="https://raw.githubusercontent.com/lloliveiradev/public-assets/main/images/spain.svg" width="30" alt="Español"></a>
|
|
42
|
+
</p>
|
|
43
|
+
<!-- readme-selector:end -->
|
|
44
|
+
|
|
45
|
+
# pm-tasks-asana
|
|
46
|
+
|
|
47
|
+
Adapter para Asana dentro da família `@llodev/pm-tasks-*`. Use as fases de extração da skill núcleo, depois aplique a formatação do Asana e opcionalmente publique/opere via o servidor MCP `claude.ai Asana`.
|
|
48
|
+
|
|
49
|
+
## Roteamento
|
|
50
|
+
|
|
51
|
+
| Modo | Gatilho | Caminho |
|
|
52
|
+
| ----------- | ---------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
|
|
53
|
+
| Paste-only | "formatar como task do Asana" sem intenção de MCP | Fase 3 (core) → Fase 4 (essa skill, só formato) → emite blocos de paste |
|
|
54
|
+
| MCP publish | "publish to Asana", "create on Asana", "--publish-asana" | Fase 3 → Fase 4 → Fase 5 (publish via MCP) |
|
|
55
|
+
| Autônomo | `[autonomous]` ou `--auto` no prompt OU `LLODEV_PM_TASKS_AUTONOMOUS=1` | Fase 3 → Fase 4 → Fase 5b (write-through, sem preview) |
|
|
56
|
+
| Ops CRUD | "marcar subtask N da task X", "fechar task Y", "atribuir Alice à task Z", "comentar na task X" | Fase 6 (operações, dispatch direto do verbo) |
|
|
57
|
+
|
|
58
|
+
## Modelo do Asana
|
|
59
|
+
|
|
60
|
+
Tasks do Asana têm:
|
|
61
|
+
|
|
62
|
+
- **Nome** (título, ≤80 chars para a board view).
|
|
63
|
+
- **Descrição** (rich text; prefira rótulos `**Section**` em negrito — headings `##` renderizam de forma inconsistente).
|
|
64
|
+
- **Subtasks** — um nível de profundidade. Custom fields e assignee NÃO propagam automaticamente do parent; o adapter os define explicitamente conforme `subtaskDefaults.inheritParentFields` em `.asana.json`.
|
|
65
|
+
- **Sections** — agrupam tasks dentro de um project.
|
|
66
|
+
- **Custom fields** — por project; a API sempre usa option GIDs, nunca display names.
|
|
67
|
+
- **Multi-assignee** — o Asana permite múltiplos followers; o assignee primário é um campo único. Use `task.assignee.add` para adicionar followers.
|
|
68
|
+
|
|
69
|
+
## Fase 4 — Formatação do Asana
|
|
70
|
+
|
|
71
|
+
Aplique o generic card do core [`../pm-tasks-core/references/generic-card.md`](../pm-tasks-core/references/generic-card.md). Depois mapeie para o Asana:
|
|
72
|
+
|
|
73
|
+
- Título → `name` da task.
|
|
74
|
+
- Seções do generic card → rótulos `**Section**` em negrito dentro de `description` (não `##`).
|
|
75
|
+
- "Implementation Checklist" + "Verification Checklist" → subtasks (achate qualquer bullet aninhado; o Asana suporta apenas um nível).
|
|
76
|
+
- Labels → opções de custom field (resolvidas via `customFields[]` do `.asana.json`).
|
|
77
|
+
- Due date → `due_on` (YYYY-MM-DD).
|
|
78
|
+
- Assignee → GID de `assignee` resolvido a partir de `members[]` do `.asana.json` ou `me` no momento do publish.
|
|
79
|
+
|
|
80
|
+
## Fase 5 — MCP publish
|
|
81
|
+
|
|
82
|
+
**Pré-requisitos:** servidor MCP do Asana (`claude.ai Asana`) conectado no seu agente. O MCP lida com OAuth; o adapter nunca vê tokens. As etapas de configuração diferem por agente — registre o mesmo endpoint MCP do Asana que seu agente suporta:
|
|
83
|
+
|
|
84
|
+
- **Claude Code**: `claude mcp add asana -s project -- npx -y claude-ai-asana-mcp` (ou siga o setup da Anthropic para o conector hosted `claude.ai Asana`).
|
|
85
|
+
- **Cursor / Windsurf / Cline / Roo Code**: adicione uma entrada no JSON de settings MCP daquele agente apontando para o mesmo comando `claude-ai-asana-mcp` (envelope idêntico ao exemplo do Trello em `pm-tasks-trello/references/mcp-config.md`).
|
|
86
|
+
- **Codex**: entrada TOML em `[mcp_servers.asana]` no `~/.codex/config.toml`.
|
|
87
|
+
- **Outros agentes capazes de MCP**: consulte a doc de MCP do agente; o comando do servidor e o fluxo OAuth são constantes.
|
|
88
|
+
|
|
89
|
+
Ordem estrita: 5.1 lê `.asana.json` (arquivo inteiro) → 5.2.5 resolve assignee + custom fields + mapa de fields por subtask → 5.2 preview & aprovação → 5.3 publish via MCP → 5.4 tratamento de erro.
|
|
90
|
+
|
|
91
|
+
Sequência do MCP publish:
|
|
92
|
+
|
|
93
|
+
1. **Task pai** — `create_tasks` com `name`, `notes` (descrição), `projects: [projectGid]`, `memberships: [{ project, section }]`, `assignee` (GID resolvido), `due_on`, `custom_fields` (string JSON de `{fieldGid: optionGid}`).
|
|
94
|
+
2. **Subtasks** — `create_tasks` por subtask com `parent: parentGid`, `name`, `assignee` (herdado ou por subtask), `custom_fields` conforme `subtaskDefaults.inheritParentFields`.
|
|
95
|
+
3. **Tags** (opcional) — `addTag` por GID de tag.
|
|
96
|
+
4. **Confirmação** — lista parent + subtasks com permalinks.
|
|
97
|
+
|
|
98
|
+
## Fase 5b — Autônomo
|
|
99
|
+
|
|
100
|
+
Pule 5.2 preview & aprovação. Aplique o contrato de modo autônomo de [`../pm-tasks-core/references/autonomous-mode.md`](../pm-tasks-core/references/autonomous-mode.md). Entradas no log de auditoria conforme [`../pm-tasks-core/references/audit-log-format.md`](../pm-tasks-core/references/audit-log-format.md).
|
|
101
|
+
|
|
102
|
+
Scope autônomo específico do Asana: `autonomous.scope.projects[]` + `autonomous.scope.sections[]` devem incluir os GIDs alvo. Qualquer escrita em custom field precisa estar em `autonomous.allow` (`task.create` cobre o set de campos no momento da criação; mudanças contínuas em campos estão fora do escopo de v1.x).
|
|
103
|
+
|
|
104
|
+
## Fase 6 — Operações CRUD (tasks existentes)
|
|
105
|
+
|
|
106
|
+
Para verbos diferentes de `task.create`, vá direto para a operação. Mapeamento verbo → tool MCP:
|
|
107
|
+
|
|
108
|
+
| Verbo do core | Tool MCP do Asana | Notas |
|
|
109
|
+
| ------------------- | ------------------------------ | ----------------------------------------------------------------------- |
|
|
110
|
+
| `task.create` | `create_tasks` | parent + subtasks conforme Fase 5 |
|
|
111
|
+
| `checklist.check` | `update_tasks` | para subtasks: `completed: true`; emula checklist via modelo de subtask |
|
|
112
|
+
| `task.close` | `update_tasks` | `completed: true` no parent |
|
|
113
|
+
| `task.due-date.set` | `update_tasks` | `due_on: "YYYY-MM-DD"` |
|
|
114
|
+
| `task.assignee.add` | `update_tasks` + `addFollower` | assignee primário substitui; adicionais viram followers |
|
|
115
|
+
| `task.comment.add` | `add_comment` (story) | adiciona uma comment story na task |
|
|
116
|
+
|
|
117
|
+
Resolução de `<task-ref>`: aceita permalinks do Asana (`https://app.asana.com/0/<project>/<task>`), GIDs nus, ou aliases de `taskAliases[]` em `.asana.json`.
|
|
118
|
+
|
|
119
|
+
## Envelope de resultado
|
|
120
|
+
|
|
121
|
+
Todo verbo retorna a shape do contrato core (veja [`../pm-tasks-core/references/contract.md`](../pm-tasks-core/references/contract.md) §Result envelope):
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
{
|
|
125
|
+
"ok": true,
|
|
126
|
+
"verb": "task.create",
|
|
127
|
+
"tool": "asana",
|
|
128
|
+
"ref": { "id": "<gid>", "url": "https://app.asana.com/0/<project>/<gid>", "alias": "<optional>" },
|
|
129
|
+
"details": {
|
|
130
|
+
/* específico do Asana (veja tabela abaixo) */
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
`details` específicos do Asana por verbo:
|
|
136
|
+
|
|
137
|
+
| Verbo | Campos de `details` |
|
|
138
|
+
| ------------------- | ------------------------------------------------------------------------ |
|
|
139
|
+
| `task.create` | `{ parentGid, subtaskGids[], projectGid, sectionGid?, customFields[]? }` |
|
|
140
|
+
| `checklist.check` | `{ subtaskGid, completed: true }` |
|
|
141
|
+
| `task.close` | `{ parentGid, completed: true }` |
|
|
142
|
+
| `task.due-date.set` | `{ taskGid, due_on }` |
|
|
143
|
+
| `task.assignee.add` | `{ taskGid, assignee, followers[]? }` (split entre primário e follower) |
|
|
144
|
+
| `task.comment.add` | `{ taskGid, storyGid }` |
|
|
145
|
+
|
|
146
|
+
Em caso de falha: `{ ok: false, verb, tool, error: { code, message, retriable } }`. Códigos comuns: `FORBIDDEN_VERB`, `OUT_OF_SCOPE`, `NOT_FOUND`, `RATE_LIMITED`, `PARTIAL_CREATE` (subtask falhou no meio do create — veja [`../pm-tasks-core/references/contract.md`](../pm-tasks-core/references/contract.md) §Partial-create recovery).
|
|
147
|
+
|
|
148
|
+
## Anti-patterns
|
|
149
|
+
|
|
150
|
+
Veja [`anti-patterns/asana.md`](anti-patterns/asana.md) — paste health, regras de custom field, requisitos de GID, tratamento de partial-create.
|
|
151
|
+
|
|
152
|
+
## Fallback standalone
|
|
153
|
+
|
|
154
|
+
Se `@llodev/pm-tasks-core` não estiver instalado: peça ao usuário a entrada mínima (título + nomes das subtasks) e produza um corpo de task do Asana pronto para colar usando apenas esse conteúdo. A qualidade é degradada — sem inferência de scope/audience/fidelity. Imprima: _"Install `@llodev/pm-tasks-core` for the full flow."_
|
|
155
|
+
|
|
156
|
+
## Config
|
|
157
|
+
|
|
158
|
+
Ordem de lookup: `<git-root>/.asana.json` → `~/.config/llodev/pm-tasks/asana.json` → aborta com instruções de init. Schema: [`schemas/config.json`](schemas/config.json). Secrets NUNCA em JSON — o MCP do Asana cuida do OAuth; o `init` usa apenas a env var `LLODEV_PM_TASKS_ASANA_PAT`.
|
|
159
|
+
|
|
160
|
+
## Init
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
npx @llodev/pm-tasks-asana init
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Veja [`../pm-tasks-core/references/init-ux.md`](../pm-tasks-core/references/init-ux.md) para o fluxo compartilhado. O init do Asana lê workspaces / projects / sections / custom fields via a REST API do Asana usando um Personal Access Token (env `LLODEV_PM_TASKS_ASANA_PAT`).
|
package/anti-patterns/asana.md
CHANGED
|
@@ -39,3 +39,24 @@ Apply when Phase 4 / Phase 5 / Phase 5b target Asana. Authoritative formatting +
|
|
|
39
39
|
## Cross-tool
|
|
40
40
|
|
|
41
41
|
**NEVER** apply Asana's quirks to another adapter after switching targets mid-chat. **Why:** the user said "actually use Trello" — re-load that adapter and apply only its rules.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## UI is the source of truth for activity attribution
|
|
46
|
+
|
|
47
|
+
The MCP's `mcp__asana__get_task` returns user-authored comments but does NOT return activity stories (creation, section moves, assignee changes, custom-field edits, follower changes). Programmatic verification of attribution is therefore incomplete by design.
|
|
48
|
+
|
|
49
|
+
When auditing whether an agent action was correctly attributed to the agent account (not the human account), open the task in the Asana UI and inspect the activity feed. Do not infer attribution from the MCP response.
|
|
50
|
+
|
|
51
|
+
## Subtasks do NOT inherit custom fields automatically
|
|
52
|
+
|
|
53
|
+
Asana itself does not propagate `custom_fields` from a parent task to its subtasks at create time, regardless of project-level defaults. The adapter MUST replicate the parent's selected `inheritParentFields` (declared in `.asana.json` under `subtaskDefaults`) when creating each subtask:
|
|
54
|
+
|
|
55
|
+
```javascript
|
|
56
|
+
await createSubtasks({
|
|
57
|
+
parent: parentGid,
|
|
58
|
+
custom_fields: pick(parent.custom_fields, config.subtaskDefaults.inheritParentFields),
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Failing to do this leaves subtasks with empty custom fields even when the parent had them set.
|
package/i18n/en-US.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"header": "@llodev/pm-tasks-asana init",
|
|
3
|
+
"patMissingTitle": "Asana MCP detected, but credentials missing for init probe.",
|
|
4
|
+
"patMissingBody": "Generate a Personal Access Token at https://app.asana.com/0/my-apps,\nthen set it in your shell and re-run:\n export LLODEV_PM_TASKS_ASANA_PAT=...\n(The token is used only by this init script; the MCP itself uses OAuth.)",
|
|
5
|
+
"mcpMissingTitle": "Asana MCP not available. Connect your Asana account first:",
|
|
6
|
+
"mcpMissingBody": " In Cursor / Claude Code settings → MCP → enable 'claude.ai Asana'.\nThen re-run this init.",
|
|
7
|
+
"workspacePrompt": "Available workspaces (select 1):",
|
|
8
|
+
"noWorkspace": "no workspace selected, aborting",
|
|
9
|
+
"projectsPrompt": "Projects in \"{workspace}\" (select 1+):",
|
|
10
|
+
"noProject": "no project selected, aborting",
|
|
11
|
+
"sectionsPrompt": "Sections in \"{project}\":",
|
|
12
|
+
"customFieldsPrompt": "Custom fields in \"{project}\" (select fields to expose to the adapter):",
|
|
13
|
+
"inheritPrompt": "Of those, which should subtasks inherit from the parent?",
|
|
14
|
+
"openSectionPrompt": "Which section is the default for newly-created tasks?",
|
|
15
|
+
"closeSectionPrompt": "Which section means 'closed / done'?",
|
|
16
|
+
"escalationPrompt": "Pick the escalation contact (will receive escalation comments + add_member on critical cards):",
|
|
17
|
+
"noMembersPrompt": "Project membership listing returned nothing (PAT scope or solo project). Add an escalation contact manually?",
|
|
18
|
+
"manualMemberGid": "Escalation member gid (Asana user ID): ",
|
|
19
|
+
"manualMemberName": "Display name: ",
|
|
20
|
+
"manualMemberAlias": "Alias (default \"owner\"): ",
|
|
21
|
+
"tryItTitle": "Try it in Claude Code: 'create an Asana task from this plan'.",
|
|
22
|
+
"tokenReminderTitle": "Reminder: the Asana MCP holds OAuth — never put tokens in this JSON.",
|
|
23
|
+
"tokenReminderBody": "The LLODEV_PM_TASKS_ASANA_PAT env var is only used by this init script."
|
|
24
|
+
}
|
package/i18n/es-ES.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"header": "@llodev/pm-tasks-asana init",
|
|
3
|
+
"patMissingTitle": "MCP de Asana detectado, pero faltan credenciales para el probe del init.",
|
|
4
|
+
"patMissingBody": "Genera un Personal Access Token en https://app.asana.com/0/my-apps,\nluego expórtalo en tu shell y vuelve a ejecutar:\n export LLODEV_PM_TASKS_ASANA_PAT=...\n(El token solo lo usa este script de init; el MCP en sí usa OAuth.)",
|
|
5
|
+
"mcpMissingTitle": "MCP de Asana no disponible. Conecta tu cuenta Asana primero:",
|
|
6
|
+
"mcpMissingBody": " En Cursor / Claude Code settings → MCP → activa 'claude.ai Asana'.\nLuego ejecuta este init nuevamente.",
|
|
7
|
+
"workspacePrompt": "Workspaces disponibles (selecciona 1):",
|
|
8
|
+
"noWorkspace": "ningún workspace seleccionado, abortando",
|
|
9
|
+
"projectsPrompt": "Proyectos en \"{workspace}\" (selecciona 1+):",
|
|
10
|
+
"noProject": "ningún proyecto seleccionado, abortando",
|
|
11
|
+
"sectionsPrompt": "Secciones en \"{project}\":",
|
|
12
|
+
"customFieldsPrompt": "Custom fields en \"{project}\" (selecciona cuáles exponer al adapter):",
|
|
13
|
+
"inheritPrompt": "De esos, ¿cuáles deben heredar las subtareas del padre?",
|
|
14
|
+
"openSectionPrompt": "¿Qué sección es el default para tareas nuevas?",
|
|
15
|
+
"closeSectionPrompt": "¿Qué sección significa 'cerrada / hecha'?",
|
|
16
|
+
"escalationPrompt": "Elige el contacto de escalation (recibe comentarios de escalation + add_member en tarjetas críticas):",
|
|
17
|
+
"noMembersPrompt": "El listado de miembros del proyecto vino vacío (alcance del PAT o proyecto en solitario). ¿Agregar un contacto de escalation manualmente?",
|
|
18
|
+
"manualMemberGid": "Gid del miembro de escalation (Asana user ID): ",
|
|
19
|
+
"manualMemberName": "Nombre a mostrar: ",
|
|
20
|
+
"manualMemberAlias": "Alias (default \"owner\"): ",
|
|
21
|
+
"tryItTitle": "Pruébalo en Claude Code: 'crea una tarea de Asana desde este plan'.",
|
|
22
|
+
"tokenReminderTitle": "Recordatorio: el MCP de Asana usa OAuth — nunca pongas tokens en este JSON.",
|
|
23
|
+
"tokenReminderBody": "La variable LLODEV_PM_TASKS_ASANA_PAT solo la usa este script de init."
|
|
24
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
const HERE = import.meta.dirname;
|
|
7
|
+
|
|
8
|
+
async function loadAll() {
|
|
9
|
+
const files = (await readdir(HERE)).filter((f) => f.endsWith(".json"));
|
|
10
|
+
const out = {};
|
|
11
|
+
for (const f of files) out[f] = JSON.parse(await readFile(path.join(HERE, f), "utf8"));
|
|
12
|
+
return out;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
test("all locale files share the same key set", async () => {
|
|
16
|
+
const all = await loadAll();
|
|
17
|
+
const ref = Object.keys(all["en-US.json"]).sort();
|
|
18
|
+
for (const [name, data] of Object.entries(all)) {
|
|
19
|
+
if (name === "en-US.json") continue;
|
|
20
|
+
const keys = Object.keys(data).sort();
|
|
21
|
+
assert.deepEqual(keys, ref, `locale ${name} differs from en-US`);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("all locale values are non-empty strings", async () => {
|
|
26
|
+
const all = await loadAll();
|
|
27
|
+
for (const [name, data] of Object.entries(all)) {
|
|
28
|
+
for (const [k, v] of Object.entries(data)) {
|
|
29
|
+
assert.equal(typeof v, "string", `${name}/${k} is not a string`);
|
|
30
|
+
assert.ok(v.length > 0, `${name}/${k} is empty`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
});
|
package/i18n/pt-BR.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"header": "@llodev/pm-tasks-asana init",
|
|
3
|
+
"patMissingTitle": "MCP do Asana detectado, mas faltam credenciais para o probe do init.",
|
|
4
|
+
"patMissingBody": "Gere um Personal Access Token em https://app.asana.com/0/my-apps,\ndepois exporte no shell e rode de novo:\n export LLODEV_PM_TASKS_ASANA_PAT=...\n(O token é usado SÓ por este script de init; o MCP em si usa OAuth.)",
|
|
5
|
+
"mcpMissingTitle": "MCP do Asana não disponível. Conecte sua conta Asana primeiro:",
|
|
6
|
+
"mcpMissingBody": " No Cursor / Claude Code settings → MCP → habilite 'claude.ai Asana'.\nDepois rode esse init novamente.",
|
|
7
|
+
"workspacePrompt": "Workspaces disponíveis (selecione 1):",
|
|
8
|
+
"noWorkspace": "nenhum workspace selecionado, abortando",
|
|
9
|
+
"projectsPrompt": "Projetos em \"{workspace}\" (selecione 1+):",
|
|
10
|
+
"noProject": "nenhum projeto selecionado, abortando",
|
|
11
|
+
"sectionsPrompt": "Seções em \"{project}\":",
|
|
12
|
+
"customFieldsPrompt": "Custom fields em \"{project}\" (selecione quais expor ao adapter):",
|
|
13
|
+
"inheritPrompt": "Desses, quais subtasks devem herdar do parent?",
|
|
14
|
+
"openSectionPrompt": "Qual seção é o default para novas tasks?",
|
|
15
|
+
"closeSectionPrompt": "Qual seção significa 'fechada / concluída'?",
|
|
16
|
+
"escalationPrompt": "Escolha o contato de escalation (recebe comentários de escalation + add_member em cards críticos):",
|
|
17
|
+
"noMembersPrompt": "Listagem de membros do projeto retornou vazia (escopo do PAT ou projeto solo). Adicionar um contato de escalation manualmente?",
|
|
18
|
+
"manualMemberGid": "Gid do membro de escalation (Asana user ID): ",
|
|
19
|
+
"manualMemberName": "Nome de exibição: ",
|
|
20
|
+
"manualMemberAlias": "Alias (default \"owner\"): ",
|
|
21
|
+
"tryItTitle": "Teste no Claude Code: 'crie uma task no Asana a partir desse plano'.",
|
|
22
|
+
"tokenReminderTitle": "Lembrete: o MCP do Asana usa OAuth — nunca coloque tokens neste JSON.",
|
|
23
|
+
"tokenReminderBody": "A env LLODEV_PM_TASKS_ASANA_PAT é usada SÓ por este script de init."
|
|
24
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llodev/pm-tasks-asana",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "Asana adapter for the @llodev/pm-tasks-* family. Use when the user mentions Asana (create Asana task, publish to Asana, post to Asana, add comment in Asana, --publish-asana, close task, check subtask) or wants to publish a plan as Asana tasks with subtasks. Modes: paste-ready (no MCP needed), MCP publish (via claude.ai Asana MCP), autonomous (sentinel [autonomous] / --auto). Implements 6 CRUD verbs from @llodev/pm-tasks-core/references/contract.md mapped to Asana (parent task + subtasks, custom fields, sections, multi-assignee). REQUIRES: @llodev/pm-tasks-core installed (skillpm / Claude Code marketplace cascade auto; Vercel CLI users install manually).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/llodev/skills/tree/main/pm-tasks/pm-tasks-asana",
|
|
@@ -11,12 +11,17 @@
|
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
13
|
"SKILL.md",
|
|
14
|
+
"SKILL.pt-BR.md",
|
|
15
|
+
"SKILL.es-ES.md",
|
|
16
|
+
"README.md",
|
|
17
|
+
"README.pt-BR.md",
|
|
18
|
+
"README.es-ES.md",
|
|
14
19
|
"schemas",
|
|
15
20
|
"references",
|
|
16
21
|
"anti-patterns",
|
|
17
22
|
"scripts",
|
|
23
|
+
"i18n",
|
|
18
24
|
"LICENSE",
|
|
19
|
-
"README.md",
|
|
20
25
|
"CHANGELOG.md"
|
|
21
26
|
],
|
|
22
27
|
"keywords": [
|
|
@@ -31,7 +36,7 @@
|
|
|
31
36
|
"pm-tasks-asana": "./scripts/init.mjs"
|
|
32
37
|
},
|
|
33
38
|
"dependencies": {
|
|
34
|
-
"@llodev/pm-tasks-core": "^1.
|
|
39
|
+
"@llodev/pm-tasks-core": "^1.1.1"
|
|
35
40
|
},
|
|
36
41
|
"publishConfig": {
|
|
37
42
|
"access": "public"
|
package/schemas/config.json
CHANGED
package/scripts/init.mjs
CHANGED
|
@@ -13,6 +13,10 @@ import {
|
|
|
13
13
|
validateConfig,
|
|
14
14
|
probeMCP,
|
|
15
15
|
printInstructions,
|
|
16
|
+
promptLocale,
|
|
17
|
+
loadStrings,
|
|
18
|
+
registerI18nRoot,
|
|
19
|
+
interpolate,
|
|
16
20
|
} from "@llodev/pm-tasks-core/init-lib";
|
|
17
21
|
|
|
18
22
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
@@ -51,30 +55,30 @@ async function asanaProbe() {
|
|
|
51
55
|
};
|
|
52
56
|
}
|
|
53
57
|
|
|
54
|
-
async function promptManualMember() {
|
|
58
|
+
async function promptManualMember(asanaStrings) {
|
|
55
59
|
const { createInterface } = await import("node:readline/promises");
|
|
56
60
|
const { stdin: input, stdout: output } = await import("node:process");
|
|
57
61
|
const r = createInterface({ input, output });
|
|
58
62
|
try {
|
|
59
|
-
const gid = (await r.question(
|
|
63
|
+
const gid = (await r.question(asanaStrings.manualMemberGid)).trim();
|
|
60
64
|
if (!gid) return null;
|
|
61
|
-
const name = (await r.question(
|
|
62
|
-
const alias = (await r.question(
|
|
65
|
+
const name = (await r.question(asanaStrings.manualMemberName)).trim() || "owner";
|
|
66
|
+
const alias = (await r.question(asanaStrings.manualMemberAlias)).trim() || "owner";
|
|
63
67
|
return { id: gid, name, alias };
|
|
64
68
|
} finally {
|
|
65
69
|
r.close();
|
|
66
70
|
}
|
|
67
71
|
}
|
|
68
72
|
|
|
69
|
-
async function collectEscalationMember(out) {
|
|
73
|
+
async function collectEscalationMember(out, { coreStrings, asanaStrings }) {
|
|
70
74
|
const candidates = out.members.filter((m) => m.alias !== "me");
|
|
71
75
|
if (candidates.length) {
|
|
72
76
|
const choices = candidates.map((m) => ({ label: `${m.name} (${m.alias})`, value: m }));
|
|
73
|
-
const picked = await promptPick(
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
);
|
|
77
|
+
const picked = await promptPick(asanaStrings.escalationPrompt, choices, {
|
|
78
|
+
defaultIndex: 0,
|
|
79
|
+
allowSkip: true,
|
|
80
|
+
strings: coreStrings,
|
|
81
|
+
});
|
|
78
82
|
if (picked) {
|
|
79
83
|
if (picked.alias !== "owner") {
|
|
80
84
|
if (!out.members.find((m) => m.alias === "owner")) picked.alias = "owner";
|
|
@@ -83,11 +87,9 @@ async function collectEscalationMember(out) {
|
|
|
83
87
|
}
|
|
84
88
|
return;
|
|
85
89
|
}
|
|
86
|
-
const add = await promptYesNo(
|
|
87
|
-
"Project membership listing returned nothing (PAT scope or solo project). Add an escalation contact manually?",
|
|
88
|
-
);
|
|
90
|
+
const add = await promptYesNo(asanaStrings.noMembersPrompt, { strings: coreStrings });
|
|
89
91
|
if (!add) return;
|
|
90
|
-
const manual = await promptManualMember();
|
|
92
|
+
const manual = await promptManualMember(asanaStrings);
|
|
91
93
|
if (!manual) return;
|
|
92
94
|
out.members.push(manual);
|
|
93
95
|
out.defaults.escalateToAlias = manual.alias;
|
|
@@ -111,9 +113,13 @@ function mapResourceType(subtype) {
|
|
|
111
113
|
}
|
|
112
114
|
|
|
113
115
|
async function run() {
|
|
114
|
-
|
|
116
|
+
registerI18nRoot("asana", path.join(ROOT, "i18n"));
|
|
117
|
+
const locale = await promptLocale("core", { defaultLocale: "en-US" });
|
|
118
|
+
const coreStrings = await loadStrings("core", locale);
|
|
119
|
+
const asanaStrings = await loadStrings("asana", locale);
|
|
120
|
+
console.log(`\n${asanaStrings.header}\n`);
|
|
115
121
|
|
|
116
|
-
const { path: outPath } = await promptScope("asana");
|
|
122
|
+
const { path: outPath } = await promptScope("asana", { strings: coreStrings });
|
|
117
123
|
|
|
118
124
|
const probe = await probeMCP({
|
|
119
125
|
tool: "asana",
|
|
@@ -125,22 +131,12 @@ async function run() {
|
|
|
125
131
|
});
|
|
126
132
|
|
|
127
133
|
if (probe.unauthenticated) {
|
|
128
|
-
printInstructions([
|
|
129
|
-
"Asana MCP detected, but credentials missing for init probe.",
|
|
130
|
-
"Generate a Personal Access Token at https://app.asana.com/0/my-apps,",
|
|
131
|
-
"then set it in your shell and re-run:",
|
|
132
|
-
" export LLODEV_PM_TASKS_ASANA_PAT=...",
|
|
133
|
-
"(The token is used only by this init script; the MCP itself uses OAuth.)",
|
|
134
|
-
]);
|
|
134
|
+
printInstructions([asanaStrings.patMissingTitle, asanaStrings.patMissingBody]);
|
|
135
135
|
return;
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
if (!probe.mcpAvailable) {
|
|
139
|
-
printInstructions([
|
|
140
|
-
"Asana MCP not available. Connect your Asana account first:",
|
|
141
|
-
" In Cursor / Claude Code settings → MCP → enable 'claude.ai Asana'.",
|
|
142
|
-
"Then re-run this init.",
|
|
143
|
-
]);
|
|
139
|
+
printInstructions([asanaStrings.mcpMissingTitle, asanaStrings.mcpMissingBody]);
|
|
144
140
|
return;
|
|
145
141
|
}
|
|
146
142
|
|
|
@@ -149,28 +145,31 @@ async function run() {
|
|
|
149
145
|
const workspaces = await api.getWorkspaces();
|
|
150
146
|
|
|
151
147
|
const pickedWorkspaces = await multiSelect(
|
|
152
|
-
|
|
148
|
+
asanaStrings.workspacePrompt,
|
|
153
149
|
workspaces.map((w) => ({ label: `${w.name} (${w.gid})`, value: w })),
|
|
150
|
+
{ strings: coreStrings },
|
|
154
151
|
);
|
|
155
152
|
if (!pickedWorkspaces.length) {
|
|
156
|
-
console.error(
|
|
153
|
+
console.error(asanaStrings.noWorkspace);
|
|
157
154
|
process.exit(1);
|
|
158
155
|
}
|
|
159
156
|
const workspace = pickedWorkspaces[0];
|
|
160
157
|
|
|
161
158
|
const projects = await api.getProjects(workspace.gid);
|
|
162
159
|
const pickedProjects = await multiSelect(
|
|
163
|
-
|
|
160
|
+
interpolate(asanaStrings.projectsPrompt, { workspace: workspace.name }),
|
|
164
161
|
projects.map((p) => ({ label: `${p.name} (${p.gid})`, value: p })),
|
|
162
|
+
{ strings: coreStrings },
|
|
165
163
|
);
|
|
166
164
|
if (!pickedProjects.length) {
|
|
167
|
-
console.error(
|
|
165
|
+
console.error(asanaStrings.noProject);
|
|
168
166
|
process.exit(1);
|
|
169
167
|
}
|
|
170
168
|
|
|
171
169
|
const out = {
|
|
172
170
|
$schema: "https://llodev.github.io/skills/schemas/pm-tasks-asana.json",
|
|
173
171
|
version: "1",
|
|
172
|
+
locale,
|
|
174
173
|
workspace: { id: workspace.gid, name: workspace.name },
|
|
175
174
|
projects: [],
|
|
176
175
|
sections: [],
|
|
@@ -187,8 +186,9 @@ async function run() {
|
|
|
187
186
|
|
|
188
187
|
const sections = await api.getSections(p.gid);
|
|
189
188
|
const pickedSections = await multiSelect(
|
|
190
|
-
|
|
189
|
+
interpolate(asanaStrings.sectionsPrompt, { project: p.name }),
|
|
191
190
|
sections.map((s) => ({ label: s.name, value: s })),
|
|
191
|
+
{ strings: coreStrings },
|
|
192
192
|
);
|
|
193
193
|
for (const s of pickedSections) {
|
|
194
194
|
out.sections.push({
|
|
@@ -203,8 +203,9 @@ async function run() {
|
|
|
203
203
|
const fields = cfSettings.map((cs) => cs.custom_field).filter(Boolean);
|
|
204
204
|
if (fields.length) {
|
|
205
205
|
const pickedFields = await multiSelect(
|
|
206
|
-
|
|
206
|
+
interpolate(asanaStrings.customFieldsPrompt, { project: p.name }),
|
|
207
207
|
fields.map((f) => ({ label: `${f.name} [${f.resource_subtype}]`, value: f })),
|
|
208
|
+
{ strings: coreStrings },
|
|
208
209
|
);
|
|
209
210
|
for (const f of pickedFields) {
|
|
210
211
|
const entry = {
|
|
@@ -226,8 +227,9 @@ async function run() {
|
|
|
226
227
|
|
|
227
228
|
if (pickedFields.length) {
|
|
228
229
|
const inheritPicked = await multiSelect(
|
|
229
|
-
|
|
230
|
+
asanaStrings.inheritPrompt,
|
|
230
231
|
pickedFields.map((f) => ({ label: f.name, value: f })),
|
|
232
|
+
{ strings: coreStrings },
|
|
231
233
|
);
|
|
232
234
|
for (const f of inheritPicked) inheritFieldIds.add(f.gid);
|
|
233
235
|
}
|
|
@@ -254,21 +256,22 @@ async function run() {
|
|
|
254
256
|
value: s,
|
|
255
257
|
}));
|
|
256
258
|
if (sectionChoices.length) {
|
|
257
|
-
const open = await promptPick(
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
);
|
|
259
|
+
const open = await promptPick(asanaStrings.openSectionPrompt, sectionChoices, {
|
|
260
|
+
defaultIndex: 0,
|
|
261
|
+
allowSkip: true,
|
|
262
|
+
strings: coreStrings,
|
|
263
|
+
});
|
|
262
264
|
if (open) out.defaults.sectionAlias = open.alias;
|
|
263
|
-
const close = await promptPick(
|
|
265
|
+
const close = await promptPick(asanaStrings.closeSectionPrompt, sectionChoices, {
|
|
264
266
|
defaultIndex: sectionChoices.length - 1,
|
|
265
267
|
allowSkip: true,
|
|
268
|
+
strings: coreStrings,
|
|
266
269
|
});
|
|
267
270
|
if (close) out.defaults.closeSectionAlias = close.alias;
|
|
268
271
|
}
|
|
269
272
|
}
|
|
270
273
|
|
|
271
|
-
await collectEscalationMember(out);
|
|
274
|
+
await collectEscalationMember(out, { coreStrings, asanaStrings });
|
|
272
275
|
|
|
273
276
|
if (inheritFieldIds.size) {
|
|
274
277
|
out.subtaskDefaults = {
|
|
@@ -277,9 +280,9 @@ async function run() {
|
|
|
277
280
|
};
|
|
278
281
|
}
|
|
279
282
|
|
|
280
|
-
const wantAuto = await promptYesNo(
|
|
281
|
-
|
|
282
|
-
);
|
|
283
|
+
const wantAuto = await promptYesNo(coreStrings.autonomousPromptQuestion, {
|
|
284
|
+
strings: coreStrings,
|
|
285
|
+
});
|
|
283
286
|
if (wantAuto) {
|
|
284
287
|
out.autonomous = {
|
|
285
288
|
enabled: false,
|
|
@@ -291,25 +294,22 @@ async function run() {
|
|
|
291
294
|
rateLimit: { writesPerMinute: 30, commentsPerMinute: 10 },
|
|
292
295
|
auditLog: "~/.local/share/llodev/pm-tasks/asana/audit.log",
|
|
293
296
|
};
|
|
294
|
-
printInstructions([
|
|
295
|
-
"autonomous block added with enabled:false.",
|
|
296
|
-
"Review scope.projects and scope.sections in the JSON before enabling.",
|
|
297
|
-
]);
|
|
297
|
+
printInstructions([coreStrings.autonomousAddedTitle, coreStrings.autonomousReviewBody]);
|
|
298
298
|
}
|
|
299
299
|
|
|
300
300
|
const schema = await loadSchema();
|
|
301
301
|
const valid = await validateConfig(out, schema);
|
|
302
302
|
if (!valid.ok) {
|
|
303
|
-
console.error(
|
|
303
|
+
console.error(`${coreStrings.errInvalidConfig}:`, JSON.stringify(valid.errors, null, 2));
|
|
304
304
|
process.exit(1);
|
|
305
305
|
}
|
|
306
306
|
|
|
307
307
|
await writeConfig(outPath, out);
|
|
308
308
|
printInstructions([
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
309
|
+
interpolate(coreStrings.configWritten, { path: outPath }),
|
|
310
|
+
asanaStrings.tryItTitle,
|
|
311
|
+
asanaStrings.tokenReminderTitle,
|
|
312
|
+
asanaStrings.tokenReminderBody,
|
|
313
313
|
]);
|
|
314
314
|
}
|
|
315
315
|
|