@llodev/pm-tasks-asana 1.0.0

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 ADDED
@@ -0,0 +1,25 @@
1
+ # @llodev/pm-tasks-asana
2
+
3
+ ## 1.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - [`a571ab1`](https://github.com/llodev/skills/commit/a571ab1537ea7d3fe61c7b89c5be0f08d01f3838) - First stable release of the pm-tasks-\* family.
8
+
9
+ - `@llodev/pm-tasks-core` — Phases 1–3 extraction pipeline (input → sections → generic card), 6 CRUD verbs (`task.create`, `checklist.check`, `task.close`, `task.due-date.set`, `task.assignee.add`, `task.comment.add`), autonomous-mode contract (allowlist + scope + rate-limit + audit log), shared init UX library.
10
+ - `@llodev/pm-tasks-trello` — Trello adapter on the canonical generic card. Paste-friendly output, MCP-driven publish, autonomous mode against a board allowlist.
11
+ - `@llodev/pm-tasks-asana` — Asana adapter with workspace/project/section + custom-field + subtask-inheritance support. Paste, MCP-driven publish, autonomous mode.
12
+
13
+ Architecture, contract, and CRUD vocabulary documented in `docs/specs/2026-06-11-pm-tasks-design.md` and `docs/plans/2026-06-11-pm-tasks-v1.md`.
14
+
15
+ ### Patch Changes
16
+
17
+ - Updated dependencies [[`a571ab1`](https://github.com/llodev/skills/commit/a571ab1537ea7d3fe61c7b89c5be0f08d01f3838)]:
18
+ - @llodev/pm-tasks-core@1.0.0
19
+
20
+ ## 0.1.0 (unreleased)
21
+
22
+ - Initial extraction from `plan-to-task-cards` Phase 5b (Asana).
23
+ - 6 CRUD verbs (create, checklist.check, close, due-date.set, assignee.add, comment.add).
24
+ - Parent task + subtasks model with custom-field inheritance.
25
+ - Autonomous mode behind `[autonomous]` sentinel + allowlist.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 LLDev Information Solutions
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # @llodev/pm-tasks-asana
2
+
3
+ Asana adapter for the `@llodev/pm-tasks-*` family. Convert implementation plans into Asana parent tasks + subtasks (paste-ready or published via MCP) and operate them (`checklist.check`, `task.close`, `task.comment.add`, etc.).
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ # npm (with skillpm or Claude Code marketplace)
9
+ npm i @llodev/pm-tasks-core @llodev/pm-tasks-asana
10
+
11
+ # Vercel CLI (install core manually too)
12
+ npx skills add llodev/skills/pm-tasks-core
13
+ npx skills add llodev/skills/pm-tasks-asana
14
+ ```
15
+
16
+ ## Setup the MCP
17
+
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 — no extra step.
19
+
20
+ For any MCP-capable agent (Claude Code, Cursor, Codex, Windsurf, Cline, Roo Code):
21
+
22
+ 1. Open the agent's MCP settings.
23
+ 2. Enable / register `claude.ai Asana` (or your agent's equivalent Asana MCP).
24
+ 3. Approve the OAuth flow in your browser.
25
+
26
+ In Claude Code, verify with `claude mcp list` — `claude.ai Asana` should appear as authenticated. Other agents have their own listing commands; see your agent's MCP docs.
27
+
28
+ ## Setup the config
29
+
30
+ The `init` script runs **outside** the MCP, so it needs a Personal Access Token to enumerate your workspaces / projects / sections / custom fields. Generate one at https://app.asana.com/0/my-apps, then:
31
+
32
+ ```bash
33
+ export LLODEV_PM_TASKS_ASANA_PAT=...
34
+ npx @llodev/pm-tasks-asana init
35
+ ```
36
+
37
+ Walk through the prompts. Output: `.asana.json` (repo) or `~/.config/llodev/pm-tasks/asana.json` (global).
38
+
39
+ The PAT is **only** used by `init`. The MCP itself uses OAuth — never put tokens in the JSON.
40
+
41
+ ## Use
42
+
43
+ - `"publish this plan as Asana tasks"` → publish flow (parent + subtasks)
44
+ - `"check subtask 3 on task X in Asana"` → CRUD op
45
+ - `"close task Y"` → close
46
+ - `"comment on task X: ..."` → comment
47
+ - `"[autonomous] create task in asana from plan @docs/plans/X.md"` → autonomous (requires `autonomous.enabled: true` in config)
48
+
49
+ ## Asana-specific notes
50
+
51
+ - **Subtasks are one level deep** — the adapter flattens nested checklists into a single subtask layer.
52
+ - **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.
53
+ - **Assignee is a single field** — use `task.assignee.add` to add followers; the primary assignee replaces on conflict.
54
+
55
+ ## License
56
+
57
+ MIT — see [LICENSE](./LICENSE).
package/SKILL.md ADDED
@@ -0,0 +1,157 @@
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.
11
+ Modes: paste-ready (no MCP needed), MCP publish (via claude.ai Asana MCP),
12
+ autonomous (write-through with allowlist). Implements 6 CRUD verbs
13
+ (task.create, checklist.check, task.close, task.due-date.set,
14
+ task.assignee.add, task.comment.add) from
15
+ pm-tasks/pm-tasks-core/references/contract.md. Requires @llodev/pm-tasks-core
16
+ installed.
17
+ license: MIT
18
+ metadata:
19
+ version: 1.0.0
20
+ tags:
21
+ - agent-skill
22
+ - asana
23
+ - plan-to-tasks
24
+ - pm-tools
25
+ family: pm-tasks
26
+ role: adapter
27
+ tool: asana
28
+ compatibility:
29
+ agents:
30
+ - claude-code
31
+ - cursor
32
+ - codex
33
+ - windsurf
34
+ - cline
35
+ - roo-code
36
+ ---
37
+
38
+ # pm-tasks-asana
39
+
40
+ 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.
41
+
42
+ ## Routing
43
+
44
+ | Mode | Trigger | Path |
45
+ | ----------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
46
+ | Paste-only | "format as Asana task" without MCP intent | Phase 3 (core) → Phase 4 (this skill, format only) → output paste blocks |
47
+ | MCP publish | "publish to Asana", "create on Asana", "--publish-asana" | Phase 3 → Phase 4 → Phase 5 (publish via MCP) |
48
+ | Autonomous | `[autonomous]` or `--auto` in prompt OR `LLODEV_PM_TASKS_AUTONOMOUS=1` | Phase 3 → Phase 4 → Phase 5b (write-through, no preview) |
49
+ | CRUD ops | "check subtask N on task X", "close task Y", "assign Alice to task Z", "comment on task X" | Phase 6 (operations, direct verb dispatch) |
50
+
51
+ ## Asana model
52
+
53
+ Asana tasks have:
54
+
55
+ - **Name** (title, ≤80 chars for board view).
56
+ - **Description** (rich text; prefer `**Section**` bold labels — `##` headings render inconsistently).
57
+ - **Subtasks** — one level deep. Custom fields and assignee do NOT auto-propagate from parent; the adapter sets them explicitly per `subtaskDefaults.inheritParentFields` in `.asana.json`.
58
+ - **Sections** — group tasks within a project.
59
+ - **Custom fields** — per-project; API always uses option GIDs, never display names.
60
+ - **Multi-assignee** — Asana allows multiple followers; primary assignee is a single field. Use `task.assignee.add` to add followers.
61
+
62
+ ## Phase 4 — Asana formatting
63
+
64
+ Apply the generic card from core's [`../pm-tasks-core/references/generic-card.md`](../pm-tasks-core/references/generic-card.md). Then map to Asana:
65
+
66
+ - Title → task `name`.
67
+ - Sections of the generic card → bold `**Section**` labels inside `description` (not `##`).
68
+ - "Implementation Checklist" + "Verification Checklist" → subtasks (flatten any nested bullets; Asana supports one level only).
69
+ - Labels → custom field options (resolved via `.asana.json` `customFields[]`).
70
+ - Due date → `due_on` (YYYY-MM-DD).
71
+ - Assignee → `assignee` GID resolved from `.asana.json` `members[]` or `me` at publish time.
72
+
73
+ ## Phase 5 — MCP publish
74
+
75
+ **Prerequisites:** Asana MCP server (`claude.ai Asana`) connected in your agent. The MCP handles OAuth; the adapter never sees tokens. Configuration steps differ per agent — register the same Asana MCP endpoint your agent supports:
76
+
77
+ - **Claude Code**: `claude mcp add asana -s project -- npx -y claude-ai-asana-mcp` (or follow Anthropic's setup for the hosted `claude.ai Asana` connector).
78
+ - **Cursor / Windsurf / Cline / Roo Code**: add an entry to that agent's MCP settings JSON pointing at the same `claude-ai-asana-mcp` command (envelope identical to the Trello example in `pm-tasks-trello/references/mcp-config.md`).
79
+ - **Codex**: TOML entry under `[mcp_servers.asana]` in `~/.codex/config.toml`.
80
+ - **Other MCP-capable agents**: consult that agent's MCP docs; the server command and OAuth flow are constant.
81
+
82
+ Strict order: 5.1 read `.asana.json` (full file) → 5.2.5 resolve assignee + custom fields + per-subtask field map → 5.2 preview & approval → 5.3 publish via MCP → 5.4 error handling.
83
+
84
+ MCP publish sequence:
85
+
86
+ 1. **Parent task** — `create_tasks` with `name`, `notes` (description), `projects: [projectGid]`, `memberships: [{ project, section }]`, `assignee` (resolved GID), `due_on`, `custom_fields` (JSON string of `{fieldGid: optionGid}`).
87
+ 2. **Subtasks** — `create_tasks` per subtask with `parent: parentGid`, `name`, `assignee` (inherited or per-subtask), `custom_fields` matching `subtaskDefaults.inheritParentFields`.
88
+ 3. **Tags** (optional) — `addTag` per tag GID.
89
+ 4. **Confirm** — list parent + subtasks with permalinks.
90
+
91
+ ## Phase 5b — Autonomous
92
+
93
+ Skip 5.2 preview & approval. Apply autonomous-mode contract from [`../pm-tasks-core/references/autonomous-mode.md`](../pm-tasks-core/references/autonomous-mode.md). Audit log entries per [`../pm-tasks-core/references/audit-log-format.md`](../pm-tasks-core/references/audit-log-format.md).
94
+
95
+ 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).
96
+
97
+ ## Phase 6 — CRUD operations (existing tasks)
98
+
99
+ For verbs other than `task.create`, jump directly to the operation. Verb → MCP tool mapping:
100
+
101
+ | Core verb | Asana MCP tool | Notes |
102
+ | ------------------- | ------------------------------ | --------------------------------------------------------------------- |
103
+ | `task.create` | `create_tasks` | parent + subtasks per Phase 5 |
104
+ | `checklist.check` | `update_tasks` | for subtasks: `completed: true`; emulates checklist via subtask model |
105
+ | `task.close` | `update_tasks` | `completed: true` on parent |
106
+ | `task.due-date.set` | `update_tasks` | `due_on: "YYYY-MM-DD"` |
107
+ | `task.assignee.add` | `update_tasks` + `addFollower` | primary assignee replaces; additional are followers |
108
+ | `task.comment.add` | `add_comment` (story) | adds a comment story to the task |
109
+
110
+ `<task-ref>` resolution: accept Asana permalinks (`https://app.asana.com/0/<project>/<task>`), bare GIDs, or aliases from `.asana.json` `taskAliases[]`.
111
+
112
+ ## Result envelope
113
+
114
+ Every verb returns the core contract shape (see [`../pm-tasks-core/references/contract.md`](../pm-tasks-core/references/contract.md) §Result envelope):
115
+
116
+ ```json
117
+ {
118
+ "ok": true,
119
+ "verb": "task.create",
120
+ "tool": "asana",
121
+ "ref": { "id": "<gid>", "url": "https://app.asana.com/0/<project>/<gid>", "alias": "<optional>" },
122
+ "details": { /* Asana-specific (see table below) */ }
123
+ }
124
+ ```
125
+
126
+ Asana-specific `details` per verb:
127
+
128
+ | Verb | `details` fields |
129
+ | ------------------- | ------------------------------------------------------------------------ |
130
+ | `task.create` | `{ parentGid, subtaskGids[], projectGid, sectionGid?, customFields[]? }` |
131
+ | `checklist.check` | `{ subtaskGid, completed: true }` |
132
+ | `task.close` | `{ parentGid, completed: true }` |
133
+ | `task.due-date.set` | `{ taskGid, due_on }` |
134
+ | `task.assignee.add` | `{ taskGid, assignee, followers[]? }` (primary vs follower split) |
135
+ | `task.comment.add` | `{ taskGid, storyGid }` |
136
+
137
+ On failure: `{ ok: false, verb, tool, error: { code, message, retriable } }`. Common codes: `FORBIDDEN_VERB`, `OUT_OF_SCOPE`, `NOT_FOUND`, `RATE_LIMITED`, `PARTIAL_CREATE` (subtask failed mid-create — see [`../pm-tasks-core/references/contract.md`](../pm-tasks-core/references/contract.md) §Partial-create recovery).
138
+
139
+ ## Anti-patterns
140
+
141
+ See [`anti-patterns/asana.md`](anti-patterns/asana.md) — paste health, custom-field rules, GID requirements, partial-create handling.
142
+
143
+ ## Standalone fallback
144
+
145
+ If `@llodev/pm-tasks-core` is not installed: ask the user for minimum input (title + subtask names) and produce a paste-ready Asana task body from this content alone. Quality is degraded — no scope/audience/fidelity inference. Print: *"Install `@llodev/pm-tasks-core` for the full flow."*
146
+
147
+ ## Config
148
+
149
+ Lookup order: `<git-root>/.asana.json` → `~/.config/llodev/pm-tasks/asana.json` → abort with init instructions. Schema: [`schemas/config.json`](schemas/config.json). Secrets NEVER in JSON — Asana MCP holds OAuth; `init` uses `LLODEV_PM_TASKS_ASANA_PAT` env var only.
150
+
151
+ ## Init
152
+
153
+ ```
154
+ npx @llodev/pm-tasks-asana init
155
+ ```
156
+
157
+ See [`../pm-tasks-core/references/init-ux.md`](../pm-tasks-core/references/init-ux.md) for the shared flow. Asana init reads workspaces / projects / sections / custom fields via the Asana REST API using a Personal Access Token (env `LLODEV_PM_TASKS_ASANA_PAT`).
@@ -0,0 +1,41 @@
1
+ # Asana ingestion anti-patterns
2
+
3
+ Apply when Phase 4 / Phase 5 / Phase 5b target Asana. Authoritative formatting + publish detail lives in [`../SKILL.md`](../SKILL.md).
4
+
5
+ ---
6
+
7
+ ## Paste health and fallbacks
8
+
9
+ | Healthy paste | If it degrades → fallback |
10
+ | ------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
11
+ | Prefer `**Section**` in task descriptions when `##` headings are unreliable; **only one** nesting level for subtasks. | Swap heading hierarchy for bold labels; split deep trees into sibling tasks. See [`../SKILL.md`](../SKILL.md) §Asana model. |
12
+
13
+ **Switching tools mid-thread:** re-load the new tool's adapter; do not carry Asana rules into Trello / Jira / Linear (see **Cross-tool** below).
14
+
15
+ ---
16
+
17
+ ## Asana
18
+
19
+ **NEVER** rely on `## Heading` hierarchy alone in descriptions for teams that proved **headings don't render**. **Why:** Asana rich-text inconsistency across clients; bold section labels (`**Deliverables**`) are safer fallback per [`../SKILL.md`](../SKILL.md) §Asana model.
20
+
21
+ **NEVER** model **multiple nesting levels** of subtasks beyond what Asana supports (one level of subtasks natively). **Why:** falsely implies hierarchy; flatten or split tasks.
22
+
23
+ **NEVER** put **Out of scope** or **Next step** into published Asana descriptions. **Why:** deferred work stays in the plan file, not in the task body.
24
+
25
+ **NEVER** call `create_tasks` / `create_task_preview_v4` before the user confirms the Phase 5 preview. **Why:** creates real workspace objects without approval.
26
+
27
+ **NEVER** use `create_task_preview_v4` as the default path for a full phase card with many checklist lines. **Why:** inline subtasks cap at 5; use parent + batched `create_tasks` with `parent` instead.
28
+
29
+ **NEVER** pass custom field **display names** (e.g. `"Discovery"`, `"Medium"`) in `custom_fields` to MCP — only **field GID → option GID** pairs resolved from `.asana.json`. **Why:** the API returns 400; display names drift between Asana UI edits and the config file.
30
+
31
+ **NEVER** guess `project.id`, `section.id`, or custom field GIDs when `.asana.json` exists. **Why:** stale or invented GIDs silently create tasks in the wrong project or column; always read the file or refresh via `get_project`.
32
+
33
+ **NEVER** send `custom_fields` as an **object** on `create_tasks` — it must be a **JSON string**; object form is only valid for `update_tasks`. **Why:** the tool schemas differ; wrong format causes silent failure or a 400 error.
34
+
35
+ **NEVER** set `section_id` without also setting `project_id` / `default_project`. **Why:** Asana API requires project context for section placement.
36
+
37
+ ---
38
+
39
+ ## Cross-tool
40
+
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.
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@llodev/pm-tasks-asana",
3
+ "version": "1.0.0",
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
+ "license": "MIT",
6
+ "homepage": "https://github.com/llodev/skills/tree/main/pm-tasks/pm-tasks-asana",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/llodev/skills.git",
10
+ "directory": "pm-tasks/pm-tasks-asana"
11
+ },
12
+ "files": [
13
+ "SKILL.md",
14
+ "schemas",
15
+ "references",
16
+ "anti-patterns",
17
+ "scripts",
18
+ "LICENSE",
19
+ "README.md",
20
+ "CHANGELOG.md"
21
+ ],
22
+ "keywords": [
23
+ "agent-skill",
24
+ "asana",
25
+ "plan-to-tasks",
26
+ "pm-tools",
27
+ "asana-mcp"
28
+ ],
29
+ "type": "module",
30
+ "bin": {
31
+ "pm-tasks-asana-init": "./scripts/init.mjs"
32
+ },
33
+ "dependencies": {
34
+ "@llodev/pm-tasks-core": "^1.0.0"
35
+ },
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
+ "engines": {
40
+ "node": ">=20"
41
+ }
42
+ }
@@ -0,0 +1,166 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://llodev.github.io/skills/schemas/pm-tasks-asana.json",
4
+ "title": "pm-tasks-asana config",
5
+ "type": "object",
6
+ "required": ["version", "projects", "sections"],
7
+ "additionalProperties": false,
8
+ "properties": {
9
+ "$schema": { "type": "string" },
10
+ "version": { "const": "1" },
11
+ "workspace": {
12
+ "type": "object",
13
+ "additionalProperties": false,
14
+ "properties": {
15
+ "id": { "type": "string", "minLength": 4 },
16
+ "name": { "type": "string" }
17
+ }
18
+ },
19
+ "projects": {
20
+ "type": "array",
21
+ "minItems": 1,
22
+ "items": {
23
+ "type": "object",
24
+ "required": ["id", "alias"],
25
+ "additionalProperties": false,
26
+ "properties": {
27
+ "id": { "type": "string", "minLength": 4 },
28
+ "name": { "type": "string" },
29
+ "alias": { "type": "string", "pattern": "^[a-z0-9-]+$" },
30
+ "url": { "type": "string", "format": "uri" }
31
+ }
32
+ }
33
+ },
34
+ "sections": {
35
+ "type": "array",
36
+ "items": {
37
+ "type": "object",
38
+ "required": ["projectAlias", "id", "alias"],
39
+ "additionalProperties": false,
40
+ "properties": {
41
+ "projectAlias": { "type": "string" },
42
+ "id": { "type": "string", "minLength": 4 },
43
+ "name": { "type": "string" },
44
+ "alias": { "type": "string", "pattern": "^[a-z0-9-]+$" }
45
+ }
46
+ }
47
+ },
48
+ "customFields": {
49
+ "type": "array",
50
+ "items": {
51
+ "type": "object",
52
+ "required": ["projectAlias", "id", "name", "type", "alias"],
53
+ "additionalProperties": false,
54
+ "properties": {
55
+ "projectAlias": { "type": "string" },
56
+ "id": { "type": "string", "minLength": 4 },
57
+ "name": { "type": "string" },
58
+ "type": {
59
+ "type": "string",
60
+ "enum": ["text", "number", "enum", "multi_enum", "date", "people"]
61
+ },
62
+ "alias": { "type": "string", "pattern": "^[a-z0-9-]+$" },
63
+ "options": {
64
+ "type": "array",
65
+ "items": {
66
+ "type": "object",
67
+ "required": ["id", "name"],
68
+ "additionalProperties": false,
69
+ "properties": {
70
+ "id": { "type": "string", "minLength": 4 },
71
+ "name": { "type": "string" },
72
+ "alias": { "type": "string", "pattern": "^[a-z0-9-]+$" }
73
+ }
74
+ }
75
+ }
76
+ }
77
+ }
78
+ },
79
+ "subtaskDefaults": {
80
+ "type": "object",
81
+ "additionalProperties": false,
82
+ "properties": {
83
+ "inheritParentFields": {
84
+ "type": "array",
85
+ "items": { "type": "string", "minLength": 4 },
86
+ "description": "Custom field IDs whose values flow from parent to subtask at create time."
87
+ },
88
+ "inheritAssignee": { "type": "boolean" }
89
+ }
90
+ },
91
+ "members": {
92
+ "type": "array",
93
+ "items": {
94
+ "type": "object",
95
+ "required": ["id", "alias"],
96
+ "additionalProperties": false,
97
+ "properties": {
98
+ "id": { "type": "string", "minLength": 4 },
99
+ "name": { "type": "string" },
100
+ "email": { "type": "string", "format": "email" },
101
+ "alias": { "type": "string", "pattern": "^[a-z0-9-]+$" }
102
+ }
103
+ }
104
+ },
105
+ "defaults": {
106
+ "type": "object",
107
+ "additionalProperties": false,
108
+ "properties": {
109
+ "projectAlias": { "type": "string" },
110
+ "sectionAlias": { "type": "string" },
111
+ "closeSectionAlias": { "type": "string" },
112
+ "assigneeAlias": { "type": "string" }
113
+ }
114
+ },
115
+ "taskAliases": {
116
+ "type": "array",
117
+ "items": {
118
+ "type": "object",
119
+ "required": ["alias", "id"],
120
+ "additionalProperties": false,
121
+ "properties": {
122
+ "alias": { "type": "string", "pattern": "^[a-z0-9-]+$" },
123
+ "id": { "type": "string" },
124
+ "url": { "type": "string", "format": "uri" }
125
+ }
126
+ }
127
+ },
128
+ "autonomous": {
129
+ "type": "object",
130
+ "additionalProperties": false,
131
+ "properties": {
132
+ "enabled": { "type": "boolean" },
133
+ "allow": {
134
+ "type": "array",
135
+ "items": {
136
+ "enum": [
137
+ "task.create",
138
+ "checklist.check",
139
+ "task.close",
140
+ "task.due-date.set",
141
+ "task.assignee.add",
142
+ "task.comment.add"
143
+ ]
144
+ }
145
+ },
146
+ "scope": {
147
+ "type": "object",
148
+ "additionalProperties": false,
149
+ "properties": {
150
+ "projects": { "type": "array", "items": { "type": "string" } },
151
+ "sections": { "type": "array", "items": { "type": "string" } }
152
+ }
153
+ },
154
+ "rateLimit": {
155
+ "type": "object",
156
+ "additionalProperties": false,
157
+ "properties": {
158
+ "writesPerMinute": { "type": "integer", "minimum": 1, "maximum": 600 },
159
+ "commentsPerMinute": { "type": "integer", "minimum": 1, "maximum": 600 }
160
+ }
161
+ },
162
+ "auditLog": { "type": "string" }
163
+ }
164
+ }
165
+ }
166
+ }
@@ -0,0 +1,270 @@
1
+ #!/usr/bin/env node
2
+ // pm-tasks-asana init — interactive config bootstrapper
3
+ import { readFile } from "node:fs/promises";
4
+ import { fileURLToPath } from "node:url";
5
+ import path from "node:path";
6
+ import {
7
+ promptScope,
8
+ promptYesNo,
9
+ multiSelect,
10
+ writeConfig,
11
+ validateConfig,
12
+ probeMCP,
13
+ printInstructions,
14
+ } from "@llodev/pm-tasks-core/init-lib";
15
+
16
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
17
+
18
+ async function loadSchema() {
19
+ const raw = await readFile(path.join(ROOT, "schemas", "config.json"), "utf8");
20
+ return JSON.parse(raw);
21
+ }
22
+
23
+ async function asanaProbe() {
24
+ // The MCP runs in a different process. To probe within this script, call the
25
+ // Asana REST API directly with a Personal Access Token.
26
+ // Used only to enumerate workspaces / projects / sections / custom fields / members.
27
+ const TOKEN = process.env.LLODEV_PM_TASKS_ASANA_PAT;
28
+ if (!TOKEN) throw new Error("auth: LLODEV_PM_TASKS_ASANA_PAT missing");
29
+ const j = async (p) => {
30
+ const r = await fetch(`https://app.asana.com/api/1.0${p}`, {
31
+ headers: { Authorization: `Bearer ${TOKEN}` },
32
+ });
33
+ if (!r.ok) throw new Error(`HTTP ${r.status}`);
34
+ const body = await r.json();
35
+ return body.data;
36
+ };
37
+ return {
38
+ getMe: () => j("/users/me?opt_fields=gid,name,email"),
39
+ getWorkspaces: () => j("/workspaces?opt_fields=name"),
40
+ getProjects: (workspaceGid) =>
41
+ j(`/projects?workspace=${workspaceGid}&opt_fields=name&limit=100`),
42
+ getSections: (projectGid) =>
43
+ j(`/projects/${projectGid}/sections?opt_fields=name&limit=100`),
44
+ getCustomFields: (projectGid) =>
45
+ j(
46
+ `/projects/${projectGid}/custom_field_settings?opt_fields=custom_field.name,custom_field.gid,custom_field.resource_subtype,custom_field.enum_options.name,custom_field.enum_options.gid&limit=100`,
47
+ ),
48
+ getMembers: (projectGid) =>
49
+ j(`/projects/${projectGid}/members?opt_fields=user.name,user.gid&limit=100`),
50
+ };
51
+ }
52
+
53
+ function aliasOf(name) {
54
+ return name
55
+ .toLowerCase()
56
+ .replace(/[^a-z0-9]+/g, "-")
57
+ .replace(/^-|-$/g, "");
58
+ }
59
+
60
+ function mapResourceType(subtype) {
61
+ switch (subtype) {
62
+ case "enum":
63
+ return "enum";
64
+ case "multi_enum":
65
+ return "multi_enum";
66
+ case "number":
67
+ return "number";
68
+ case "date":
69
+ return "date";
70
+ case "people":
71
+ return "people";
72
+ default:
73
+ return "text";
74
+ }
75
+ }
76
+
77
+ async function run() {
78
+ console.log("\n@llodev/pm-tasks-asana init\n");
79
+
80
+ const { path: outPath } = await promptScope("asana");
81
+
82
+ const probe = await probeMCP({
83
+ tool: "asana",
84
+ probeCommand: async () => {
85
+ const api = await asanaProbe();
86
+ await api.getMe();
87
+ return api;
88
+ },
89
+ });
90
+
91
+ if (probe.unauthenticated) {
92
+ printInstructions([
93
+ "Asana MCP detected, but credentials missing for init probe.",
94
+ "Generate a Personal Access Token at https://app.asana.com/0/my-apps,",
95
+ "then set it in your shell and re-run:",
96
+ " export LLODEV_PM_TASKS_ASANA_PAT=...",
97
+ "(The token is used only by this init script; the MCP itself uses OAuth.)",
98
+ ]);
99
+ return;
100
+ }
101
+
102
+ if (!probe.mcpAvailable) {
103
+ printInstructions([
104
+ "Asana MCP not available. Connect your Asana account first:",
105
+ " In Cursor / Claude Code settings → MCP → enable 'claude.ai Asana'.",
106
+ "Then re-run this init.",
107
+ ]);
108
+ return;
109
+ }
110
+
111
+ const api = probe.result;
112
+ const me = await api.getMe();
113
+ const workspaces = await api.getWorkspaces();
114
+
115
+ const pickedWorkspaces = await multiSelect(
116
+ "Available workspaces (select 1):",
117
+ workspaces.map((w) => ({ label: `${w.name} (${w.gid})`, value: w })),
118
+ );
119
+ if (!pickedWorkspaces.length) {
120
+ console.error("no workspace selected, aborting");
121
+ process.exit(1);
122
+ }
123
+ const workspace = pickedWorkspaces[0];
124
+
125
+ const projects = await api.getProjects(workspace.gid);
126
+ const pickedProjects = await multiSelect(
127
+ `Projects in "${workspace.name}" (select 1+):`,
128
+ projects.map((p) => ({ label: `${p.name} (${p.gid})`, value: p })),
129
+ );
130
+ if (!pickedProjects.length) {
131
+ console.error("no project selected, aborting");
132
+ process.exit(1);
133
+ }
134
+
135
+ const out = {
136
+ $schema: "https://llodev.github.io/skills/schemas/pm-tasks-asana.json",
137
+ version: "1",
138
+ workspace: { id: workspace.gid, name: workspace.name },
139
+ projects: [],
140
+ sections: [],
141
+ customFields: [],
142
+ members: [{ id: me.gid, name: me.name, email: me.email, alias: "me" }],
143
+ defaults: {},
144
+ };
145
+
146
+ const inheritFieldIds = new Set();
147
+
148
+ for (const p of pickedProjects) {
149
+ const alias = aliasOf(p.name);
150
+ out.projects.push({ id: p.gid, name: p.name, alias });
151
+
152
+ const sections = await api.getSections(p.gid);
153
+ const pickedSections = await multiSelect(
154
+ `Sections in "${p.name}":`,
155
+ sections.map((s) => ({ label: s.name, value: s })),
156
+ );
157
+ for (const s of pickedSections) {
158
+ out.sections.push({
159
+ projectAlias: alias,
160
+ id: s.gid,
161
+ name: s.name,
162
+ alias: aliasOf(s.name),
163
+ });
164
+ }
165
+
166
+ const cfSettings = await api.getCustomFields(p.gid);
167
+ const fields = cfSettings.map((cs) => cs.custom_field).filter(Boolean);
168
+ if (fields.length) {
169
+ const pickedFields = await multiSelect(
170
+ `Custom fields in "${p.name}" (select fields to expose to the adapter):`,
171
+ fields.map((f) => ({ label: `${f.name} [${f.resource_subtype}]`, value: f })),
172
+ );
173
+ for (const f of pickedFields) {
174
+ const entry = {
175
+ projectAlias: alias,
176
+ id: f.gid,
177
+ name: f.name,
178
+ type: mapResourceType(f.resource_subtype),
179
+ alias: aliasOf(f.name),
180
+ };
181
+ if (Array.isArray(f.enum_options) && f.enum_options.length) {
182
+ entry.options = f.enum_options.map((opt) => ({
183
+ id: opt.gid,
184
+ name: opt.name,
185
+ alias: aliasOf(opt.name),
186
+ }));
187
+ }
188
+ out.customFields.push(entry);
189
+ }
190
+
191
+ if (pickedFields.length) {
192
+ const inheritPicked = await multiSelect(
193
+ `Of those, which should subtasks inherit from the parent?`,
194
+ pickedFields.map((f) => ({ label: f.name, value: f })),
195
+ );
196
+ for (const f of inheritPicked) inheritFieldIds.add(f.gid);
197
+ }
198
+ }
199
+
200
+ try {
201
+ const memberships = await api.getMembers(p.gid);
202
+ const users = memberships.map((m) => m.user).filter(Boolean);
203
+ for (const u of users) {
204
+ if (u.gid === me.gid) continue;
205
+ if (out.members.find((m) => m.id === u.gid)) continue;
206
+ out.members.push({ id: u.gid, name: u.name, alias: aliasOf(u.name) });
207
+ }
208
+ } catch (e) {
209
+ // Project membership listing may require additional scopes; skip silently.
210
+ }
211
+ }
212
+
213
+ if (out.projects.length === 1) {
214
+ out.defaults.projectAlias = out.projects[0].alias;
215
+ const backlog = out.sections.find((s) => /backlog|todo|to.do|inbox/i.test(s.name));
216
+ const done = out.sections.find((s) => /done|completed|published|conclu/i.test(s.name));
217
+ if (backlog) out.defaults.sectionAlias = backlog.alias;
218
+ if (done) out.defaults.closeSectionAlias = done.alias;
219
+ out.defaults.assigneeAlias = "me";
220
+ }
221
+
222
+ if (inheritFieldIds.size) {
223
+ out.subtaskDefaults = {
224
+ inheritParentFields: [...inheritFieldIds],
225
+ inheritAssignee: true,
226
+ };
227
+ }
228
+
229
+ const wantAuto = await promptYesNo(
230
+ "Enable autonomous mode? (adds an autonomous block with conservative defaults)",
231
+ );
232
+ if (wantAuto) {
233
+ out.autonomous = {
234
+ enabled: false,
235
+ allow: ["task.create", "checklist.check", "task.close", "task.comment.add"],
236
+ scope: {
237
+ projects: out.projects.map((p) => p.id),
238
+ sections: out.sections.map((s) => s.id),
239
+ },
240
+ rateLimit: { writesPerMinute: 30, commentsPerMinute: 10 },
241
+ auditLog: "~/.local/share/llodev/pm-tasks/asana/audit.log",
242
+ };
243
+ printInstructions([
244
+ "autonomous block added with enabled:false.",
245
+ "Review scope.projects and scope.sections in the JSON before enabling.",
246
+ ]);
247
+ }
248
+
249
+ const schema = await loadSchema();
250
+ const valid = await validateConfig(out, schema);
251
+ if (!valid.ok) {
252
+ console.error("invalid config:", JSON.stringify(valid.errors, null, 2));
253
+ process.exit(1);
254
+ }
255
+
256
+ await writeConfig(outPath, out);
257
+ printInstructions([
258
+ `Config written to ${outPath}.`,
259
+ "Try it in Claude Code: 'create an Asana task from this plan'.",
260
+ "Reminder: the Asana MCP holds OAuth — never put tokens in this JSON.",
261
+ "The LLODEV_PM_TASKS_ASANA_PAT env var is only used by this init script.",
262
+ ]);
263
+ }
264
+
265
+ if (import.meta.url === `file://${process.argv[1]}`) {
266
+ run().catch((e) => {
267
+ console.error(e);
268
+ process.exit(1);
269
+ });
270
+ }