@llodev/pm-tasks-asana 1.0.0 → 1.0.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 CHANGED
@@ -1,5 +1,24 @@
1
1
  # @llodev/pm-tasks-asana
2
2
 
3
+ ## 1.0.2
4
+
5
+ ### Patch Changes
6
+
7
+ - Allow `defaults.escalateToAlias` (and `defaults.assigneeAlias` on Trello) in the config schemas. The v1.0.1 init scripts emit these keys, but the JSON Schemas still had `additionalProperties: false` rejecting them — making `npx @llodev/pm-tasks-{asana,trello} init` fail at the validate step with "must NOT have additional properties". Trello init also now emits `assigneeAlias: "me"` for parity with Asana.
8
+
9
+ ## 1.0.1
10
+
11
+ ### Patch Changes
12
+
13
+ - Fix `npx @llodev/pm-tasks-asana init` silently doing nothing. Two bugs collapsed the init flow: (a) the `bin` entry was named `pm-tasks-asana-init` (not matching the package name), so npx never resolved the binary; (b) the script's `import.meta.url === file://${process.argv[1]}` guard failed under npx's symlinked bin shim, so even when invoked the `run()` entry-point was skipped. The bin is now `pm-tasks-asana` and the entry-point runs unconditionally.
14
+ - Cross-platform global config path. The init prompt now honors `LLODEV_PM_TASKS_CONFIG_HOME` first, then `XDG_CONFIG_HOME` (macOS/Linux), then `%APPDATA%` (Windows), then `~/.config` as the last fallback. The prompt prints the absolute path before asking, so the destination is always visible.
15
+ - Slug aliases are now Unicode-aware (via `@llodev/pm-tasks-core@1.0.1`): `"Em execução"` → `"em-execucao"`, `"Média"` → `"media"` (was `"em-execu-o"` / `"m-dia"`).
16
+ - Drop the language-specific regex for inferring the "closed" section. The init now explicitly asks: "Which section is the default for newly-created tasks?" and "Which section means 'closed / done'?" with sensible defaults (first / last picked) and a skip option.
17
+ - Add an explicit escalation prompt. After the member list is collected, the init asks: "Pick the escalation contact (will receive escalation comments + add_member on critical cards)" and stores it as `defaults.escalateToAlias`. The chosen member is re-aliased to `"owner"` if no other member already holds that alias.
18
+ - Fallback for empty membership lists: if the PAT's scope didn't return any project members beyond `me`, the init now offers a manual entry (gid + name + alias) so single-collaborator projects can still wire an escalation contact.
19
+ - `multiSelect`: empty input now means "select all" instead of "select none".
20
+ - README clarifies the per-OS defaults and the env override.
21
+
3
22
  ## 1.0.0
4
23
 
5
24
  ### Major Changes
package/README.md CHANGED
@@ -34,7 +34,15 @@ export LLODEV_PM_TASKS_ASANA_PAT=...
34
34
  npx @llodev/pm-tasks-asana init
35
35
  ```
36
36
 
37
- Walk through the prompts. Output: `.asana.json` (repo) or `~/.config/llodev/pm-tasks/asana.json` (global).
37
+ Walk through the prompts. Pick where the config should live:
38
+
39
+ - **local** → `./.asana.json` (recommended for project-scoped configs, can be committed).
40
+ - **global** → a platform default, customizable. Defaults:
41
+ - macOS / Linux (XDG): `$XDG_CONFIG_HOME/llodev/pm-tasks/asana.json`, falling back to `~/.config/llodev/pm-tasks/asana.json`.
42
+ - Windows: `%APPDATA%\llodev\pm-tasks\asana.json`.
43
+ - Override on any OS with `LLODEV_PM_TASKS_CONFIG_HOME=/your/path` — the file lands at `$LLODEV_PM_TASKS_CONFIG_HOME/asana.json`.
44
+
45
+ The `init` prompt prints the absolute path it will write to, so you always see exactly where the file goes.
38
46
 
39
47
  The PAT is **only** used by `init`. The MCP itself uses OAuth — never put tokens in the JSON.
40
48
 
package/SKILL.md CHANGED
@@ -7,16 +7,15 @@ description: >-
7
7
  existing tasks (check subtask, close task, change due-date, assign person,
8
8
  comment); OR when invoked autonomously by another agent with [autonomous] /
9
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.
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.
17
16
  license: MIT
18
17
  metadata:
19
- version: 1.0.0
18
+ version: 1.0.2
20
19
  tags:
21
20
  - agent-skill
22
21
  - asana
@@ -41,11 +40,11 @@ Adapter for Asana within the `@llodev/pm-tasks-*` family. Use the core skill's e
41
40
 
42
41
  ## Routing
43
42
 
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) |
43
+ | Mode | Trigger | Path |
44
+ | ----------- | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------ |
45
+ | Paste-only | "format as Asana task" without MCP intent | Phase 3 (core) → Phase 4 (this skill, format only) → output paste blocks |
46
+ | MCP publish | "publish to Asana", "create on Asana", "--publish-asana" | Phase 3 → Phase 4 → Phase 5 (publish via MCP) |
47
+ | Autonomous | `[autonomous]` or `--auto` in prompt OR `LLODEV_PM_TASKS_AUTONOMOUS=1` | Phase 3 → Phase 4 → Phase 5b (write-through, no preview) |
49
48
  | 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
49
 
51
50
  ## Asana model
@@ -119,7 +118,9 @@ Every verb returns the core contract shape (see [`../pm-tasks-core/references/co
119
118
  "verb": "task.create",
120
119
  "tool": "asana",
121
120
  "ref": { "id": "<gid>", "url": "https://app.asana.com/0/<project>/<gid>", "alias": "<optional>" },
122
- "details": { /* Asana-specific (see table below) */ }
121
+ "details": {
122
+ /* Asana-specific (see table below) */
123
+ }
123
124
  }
124
125
  ```
125
126
 
@@ -142,7 +143,7 @@ See [`anti-patterns/asana.md`](anti-patterns/asana.md) — paste health, custom-
142
143
 
143
144
  ## Standalone fallback
144
145
 
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
+ 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
 
147
148
  ## Config
148
149
 
@@ -6,8 +6,8 @@ Apply when Phase 4 / Phase 5 / Phase 5b target Asana. Authoritative formatting +
6
6
 
7
7
  ## Paste health and fallbacks
8
8
 
9
- | Healthy paste | If it degrades → fallback |
10
- | ------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
9
+ | Healthy paste | If it degrades → fallback |
10
+ | --------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
11
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
12
 
13
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).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llodev/pm-tasks-asana",
3
- "version": "1.0.0",
3
+ "version": "1.0.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",
@@ -28,10 +28,10 @@
28
28
  ],
29
29
  "type": "module",
30
30
  "bin": {
31
- "pm-tasks-asana-init": "./scripts/init.mjs"
31
+ "pm-tasks-asana": "./scripts/init.mjs"
32
32
  },
33
33
  "dependencies": {
34
- "@llodev/pm-tasks-core": "^1.0.0"
34
+ "@llodev/pm-tasks-core": "^1.0.1"
35
35
  },
36
36
  "publishConfig": {
37
37
  "access": "public"
@@ -109,7 +109,8 @@
109
109
  "projectAlias": { "type": "string" },
110
110
  "sectionAlias": { "type": "string" },
111
111
  "closeSectionAlias": { "type": "string" },
112
- "assigneeAlias": { "type": "string" }
112
+ "assigneeAlias": { "type": "string" },
113
+ "escalateToAlias": { "type": "string" }
113
114
  }
114
115
  },
115
116
  "taskAliases": {
package/scripts/init.mjs CHANGED
@@ -7,6 +7,8 @@ import {
7
7
  promptScope,
8
8
  promptYesNo,
9
9
  multiSelect,
10
+ promptPick,
11
+ aliasOf,
10
12
  writeConfig,
11
13
  validateConfig,
12
14
  probeMCP,
@@ -39,8 +41,7 @@ async function asanaProbe() {
39
41
  getWorkspaces: () => j("/workspaces?opt_fields=name"),
40
42
  getProjects: (workspaceGid) =>
41
43
  j(`/projects?workspace=${workspaceGid}&opt_fields=name&limit=100`),
42
- getSections: (projectGid) =>
43
- j(`/projects/${projectGid}/sections?opt_fields=name&limit=100`),
44
+ getSections: (projectGid) => j(`/projects/${projectGid}/sections?opt_fields=name&limit=100`),
44
45
  getCustomFields: (projectGid) =>
45
46
  j(
46
47
  `/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`,
@@ -50,11 +51,46 @@ async function asanaProbe() {
50
51
  };
51
52
  }
52
53
 
53
- function aliasOf(name) {
54
- return name
55
- .toLowerCase()
56
- .replace(/[^a-z0-9]+/g, "-")
57
- .replace(/^-|-$/g, "");
54
+ async function promptManualMember() {
55
+ const { createInterface } = await import("node:readline/promises");
56
+ const { stdin: input, stdout: output } = await import("node:process");
57
+ const r = createInterface({ input, output });
58
+ try {
59
+ const gid = (await r.question("Escalation member gid (Asana user ID): ")).trim();
60
+ if (!gid) return null;
61
+ const name = (await r.question("Display name: ")).trim() || "owner";
62
+ const alias = (await r.question(`Alias (default "owner"): `)).trim() || "owner";
63
+ return { id: gid, name, alias };
64
+ } finally {
65
+ r.close();
66
+ }
67
+ }
68
+
69
+ async function collectEscalationMember(out) {
70
+ const candidates = out.members.filter((m) => m.alias !== "me");
71
+ if (candidates.length) {
72
+ const choices = candidates.map((m) => ({ label: `${m.name} (${m.alias})`, value: m }));
73
+ const picked = await promptPick(
74
+ "Pick the escalation contact (will receive escalation comments + add_member on critical cards):",
75
+ choices,
76
+ { defaultIndex: 0, allowSkip: true },
77
+ );
78
+ if (picked) {
79
+ if (picked.alias !== "owner") {
80
+ if (!out.members.find((m) => m.alias === "owner")) picked.alias = "owner";
81
+ }
82
+ out.defaults.escalateToAlias = picked.alias;
83
+ }
84
+ return;
85
+ }
86
+ const add = await promptYesNo(
87
+ "Project membership listing returned nothing (PAT scope or solo project). Add an escalation contact manually?",
88
+ );
89
+ if (!add) return;
90
+ const manual = await promptManualMember();
91
+ if (!manual) return;
92
+ out.members.push(manual);
93
+ out.defaults.escalateToAlias = manual.alias;
58
94
  }
59
95
 
60
96
  function mapResourceType(subtype) {
@@ -212,13 +248,28 @@ async function run() {
212
248
 
213
249
  if (out.projects.length === 1) {
214
250
  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
251
  out.defaults.assigneeAlias = "me";
252
+ const sectionChoices = out.sections.map((s) => ({
253
+ label: `${s.name} (${s.alias})`,
254
+ value: s,
255
+ }));
256
+ if (sectionChoices.length) {
257
+ const open = await promptPick(
258
+ "Which section is the default for newly-created tasks?",
259
+ sectionChoices,
260
+ { defaultIndex: 0, allowSkip: true },
261
+ );
262
+ if (open) out.defaults.sectionAlias = open.alias;
263
+ const close = await promptPick("Which section means 'closed / done'?", sectionChoices, {
264
+ defaultIndex: sectionChoices.length - 1,
265
+ allowSkip: true,
266
+ });
267
+ if (close) out.defaults.closeSectionAlias = close.alias;
268
+ }
220
269
  }
221
270
 
271
+ await collectEscalationMember(out);
272
+
222
273
  if (inheritFieldIds.size) {
223
274
  out.subtaskDefaults = {
224
275
  inheritParentFields: [...inheritFieldIds],
@@ -262,9 +313,7 @@ async function run() {
262
313
  ]);
263
314
  }
264
315
 
265
- if (import.meta.url === `file://${process.argv[1]}`) {
266
- run().catch((e) => {
267
- console.error(e);
268
- process.exit(1);
269
- });
270
- }
316
+ run().catch((e) => {
317
+ console.error(e);
318
+ process.exit(1);
319
+ });