@llodev/pm-tasks-asana 1.3.1 → 1.5.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 +54 -0
- package/SKILL.md +28 -18
- package/dist/bin/init.d.ts +3 -0
- package/dist/bin/init.d.ts.map +1 -0
- package/dist/bin/init.js +280 -0
- package/dist/bin/init.js.map +1 -0
- package/dist/doctor-cli.d.ts +6 -0
- package/dist/doctor-cli.d.ts.map +1 -0
- package/dist/doctor-cli.js +166 -0
- package/dist/doctor-cli.js.map +1 -0
- package/manifest.json +1 -0
- package/package.json +27 -9
- package/references/operations.md +62 -0
- package/i18n/parity.test.mjs +0 -33
- package/scripts/init.mjs +0 -319
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,59 @@
|
|
|
1
1
|
# @llodev/pm-tasks-asana
|
|
2
2
|
|
|
3
|
+
## 1.5.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 1200b4e: v1.8.0 — Observability v1. pm-tasks-core-doctor CLI validates workspace config / autonomous allowlist / audit writability / (when probes are injected) MCP & network reach BEFORE the first publish attempt fails noisily. Adapter init bins expose `--doctor` for per-tool checks (C-TRL-1..3 + C-ASN-1..3, gated by auth env). Smart audit-log rotation (size + age + multi-tool, atomic, idempotent, gzipped archives, keep-N) replaces the rudimentary shell script; new `rotate-audit.mjs` CLI emits structured JSON status. Pre-release gate now blocks on doctor errors. Closes roadmap §2.4 O1 + O3.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [1200b4e]
|
|
12
|
+
- @llodev/pm-tasks-core@1.8.0
|
|
13
|
+
|
|
14
|
+
## 1.4.0
|
|
15
|
+
|
|
16
|
+
### Minor Changes
|
|
17
|
+
|
|
18
|
+
- v1.5.0 — Adapters TypeScript migration, @llodev/pm-tasks-testkit, 7th canonical verb `task.move`, E2E canary.
|
|
19
|
+
|
|
20
|
+
**Core (`@llodev/pm-tasks-core` 1.4.0 → 1.5.0)** — adds the 7th canonical verb `task.move` to the public contract.
|
|
21
|
+
|
|
22
|
+
- New verb `task.move({ cardId, targetList })` formalizes the WIP transition that the autonomous-mode lifecycle has always required but no canonical verb expressed. `targetList` accepts the enum `"open" | "wip" | "done"` plus raw list IDs.
|
|
23
|
+
- `task.move` is INDEPENDENT of `task.close`: move only repositions, close moves AND sets the completion flag. Useful in adapters where the visual transition and the closed-flag are separate operations (Asana section change vs. `completed: true`).
|
|
24
|
+
- Schema regex updated to recognize `task.move` as canonical (no namespace prefix required).
|
|
25
|
+
- `pm-tasks-core/schemas/adapter-manifest.schema.json` + `references/contract.md` + `references/crud-vocabulary.md` + `references/autonomous-mode.md` updated.
|
|
26
|
+
- Contract-check (`scripts/checks/contract-check.mjs`) `CANONICAL_VERBS` Set updated; regression test added.
|
|
27
|
+
- Additive only — existing consumers calling the original 6 verbs continue to work unchanged.
|
|
28
|
+
|
|
29
|
+
**Adapters (`@llodev/pm-tasks-asana` 1.3.1 → 1.4.0, `@llodev/pm-tasks-trello` 1.3.1 → 1.4.0)** — full TypeScript migration mirroring core's v1.4.0 pattern.
|
|
30
|
+
|
|
31
|
+
- Source moved from `scripts/init.mjs` to `src/bin/init.ts` (strict TS).
|
|
32
|
+
- Build pipeline: `tsc` produces `dist/bin/init.{js,d.ts,js.map,d.ts.map}`. `dist/bin/init.js` is `chmod +x` so the bin works on install.
|
|
33
|
+
- `package.json` exports map points at compiled `dist/`; `bin` field exposes `npx @llodev/pm-tasks-<tool> init`.
|
|
34
|
+
- i18n parity tests migrated from `node:test` to Vitest 2.x (2/2 tests per adapter).
|
|
35
|
+
- Both adapters now declare `task.move` in their `manifest.json` `verbs` array (7 canonical verbs) and document the MCP mapping in `references/operations.md`:
|
|
36
|
+
- Trello: `mcp__trello__move_card({ cardId, idList })` resolving `"wip"`/`"done"`/`"open"` from `lists.<alias>` in `.trello.json`.
|
|
37
|
+
- Asana: `mcp__claude_ai_Asana__update_tasks` with `memberships: [{ project, section }]` resolving via `defaults.<state>SectionAlias` in `.asana.json`.
|
|
38
|
+
- Trello autonomous overlay (`pm-tasks-trello/references/autonomous.md`) updated to invoke canonical `task.move` at task start and before task close.
|
|
39
|
+
- Backwards compatibility: consumers keep importing `@llodev/pm-tasks-core/init-lib` via the subpath; resolution now hits the v1.5.0 dist.
|
|
40
|
+
|
|
41
|
+
**Testkit (`@llodev/pm-tasks-testkit` 0.0.0 → 0.1.0)** — new package, first release.
|
|
42
|
+
|
|
43
|
+
- Pure TypeScript library. 7 in-memory fakes covering every canonical verb: `task.create`, `task.move`, `task.close`, `task.comment.add`, `task.due-date.set`, `task.assignee.add`, `checklist.check`.
|
|
44
|
+
- `createFakeAdapter({ idGenerator?, clock? })` returns an adapter-shaped object with all 7 verbs plus introspection (`getTask`, `getAllTasks`, `reset`).
|
|
45
|
+
- `peerDependencies`: `@llodev/pm-tasks-core ^1.4.0`.
|
|
46
|
+
- Useful for testing custom skills/adapters without hitting real MCP servers; 14/14 vitest tests cover each fake's behavior and idempotency.
|
|
47
|
+
|
|
48
|
+
**Pipeline hardening — E2E canary** — `scripts/checks/canary-e2e.mjs` packs all 4 packages, npm-installs them in a clean sandbox, and exercises core/asana/trello/testkit smoke checks. Wired into `make e2e`, a new `.github/workflows/e2e.yml` (PR + manual dispatch), and as a gate in `release.yml`. Runs in ~3.4 s; catches tarball drift the snapshot test alone misses.
|
|
49
|
+
|
|
50
|
+
**Skill-judge:** non-functional change (TS migration of adapter source + additive task.move documentation in SKILL.md verb lists). Expect Δ ≈ 0; ratchet baseline with updated `capturedAt` per v1.5.0 release per NOISE_BAND policy.
|
|
51
|
+
|
|
52
|
+
### Patch Changes
|
|
53
|
+
|
|
54
|
+
- Updated dependencies []:
|
|
55
|
+
- @llodev/pm-tasks-core@1.5.0
|
|
56
|
+
|
|
3
57
|
## 1.3.1
|
|
4
58
|
|
|
5
59
|
### Patch Changes
|
package/SKILL.md
CHANGED
|
@@ -9,13 +9,13 @@ description: >-
|
|
|
9
9
|
--auto sentinel. Asana hierarchy: workspace > project > section > parent task
|
|
10
10
|
> subtasks (one level), with custom fields and multi-assignee support. Modes:
|
|
11
11
|
paste-ready (no MCP needed), MCP publish (via claude.ai Asana MCP), autonomous
|
|
12
|
-
(write-through with allowlist). Implements
|
|
13
|
-
checklist.check, task.close, task.due-date.set, task.assignee.add,
|
|
12
|
+
(write-through with allowlist). Implements 7 CRUD verbs (task.create,
|
|
13
|
+
task.move, checklist.check, task.close, task.due-date.set, task.assignee.add,
|
|
14
14
|
task.comment.add) from pm-tasks/pm-tasks-core/references/contract.md. Requires
|
|
15
15
|
@llodev/pm-tasks-core installed.
|
|
16
16
|
license: MIT
|
|
17
17
|
metadata:
|
|
18
|
-
version: 1.
|
|
18
|
+
version: 1.5.0
|
|
19
19
|
tags:
|
|
20
20
|
- agent-skill
|
|
21
21
|
- asana
|
|
@@ -103,14 +103,14 @@ Mandatory when the controller is executing a plan with multiple tasks autonomous
|
|
|
103
103
|
|
|
104
104
|
Asana-specific verb mapping:
|
|
105
105
|
|
|
106
|
-
| Transition | MCP call(s)
|
|
107
|
-
| ----------------------- |
|
|
108
|
-
| Task start | `
|
|
109
|
-
| Step complete (subtask) | `
|
|
110
|
-
| Task complete (full) | `
|
|
111
|
-
| Task failed | `
|
|
106
|
+
| Transition | Canonical verb + MCP call(s) |
|
|
107
|
+
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
108
|
+
| Task start | `task.move(cardId, "wip")` → `mcp__claude_ai_Asana__update_tasks { task: <gid>, memberships: [{ project: <projectGid>, section: <wipSectionGid> }] }` |
|
|
109
|
+
| Step complete (subtask) | `checklist.check` → `mcp__claude_ai_Asana__update_tasks { task: <subtaskGid>, completed: true }` |
|
|
110
|
+
| Task complete (full) | `task.move(cardId, "done")` → `mcp__claude_ai_Asana__update_tasks { task: <parentGid>, memberships: [{ project: <projectGid>, section: <doneSectionGid> }] }` then `task.comment.add` → `mcp__claude_ai_Asana__add_comment { task_id: <parentGid>, text: "🤖 [agent] Task complete. Commit: <SHA>. <branch>." }` then `task.close` → `mcp__claude_ai_Asana__update_tasks { task: <parentGid>, completed: true }` |
|
|
111
|
+
| Task failed | `task.comment.add` → `mcp__claude_ai_Asana__add_comment` with failure mode + `mcp__claude_ai_Asana__update_tasks { task: <gid>, add_followers: ["<escalateToAliasGid>"] }` for human escalation. Do NOT call `task.move(_, "done")` or `task.close`. |
|
|
112
112
|
|
|
113
|
-
Resolve `<wipSectionGid>` / `<doneSectionGid>` from `.asana.json` `
|
|
113
|
+
Resolve `<wipSectionGid>` / `<doneSectionGid>` from `.asana.json` using `defaults.wipSectionAlias` / `defaults.doneSectionAlias` to look up the matching entry in `sections[]` by alias. Both must already be in `autonomous.scope.sections` — otherwise the verb returns `OUT_OF_SCOPE`.
|
|
114
114
|
|
|
115
115
|
**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.
|
|
116
116
|
|
|
@@ -118,14 +118,15 @@ Resolve `<wipSectionGid>` / `<doneSectionGid>` from `.asana.json` `sections[]` b
|
|
|
118
118
|
|
|
119
119
|
For verbs other than `task.create`, jump directly to the operation. Verb → MCP tool mapping:
|
|
120
120
|
|
|
121
|
-
| Core verb | Asana MCP tool | Notes
|
|
122
|
-
| ------------------- | ------------------------------ |
|
|
123
|
-
| `task.create` | `create_tasks` | parent + subtasks per Phase 5
|
|
124
|
-
| `
|
|
125
|
-
| `
|
|
126
|
-
| `task.
|
|
127
|
-
| `task.
|
|
128
|
-
| `task.
|
|
121
|
+
| Core verb | Asana MCP tool | Notes |
|
|
122
|
+
| ------------------- | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
123
|
+
| `task.create` | `create_tasks` | parent + subtasks per Phase 5 |
|
|
124
|
+
| `task.move` | `update_tasks` | `memberships: [{ project, section: <resolvedGid> }]`. Resolve `"wip"` / `"done"` / `"open"` via `defaults.wipSectionAlias` / `defaults.doneSectionAlias` / `defaults.openSectionAlias` in `.asana.json`. Raw section GIDs pass through. |
|
|
125
|
+
| `checklist.check` | `update_tasks` | for subtasks: `completed: true`; emulates checklist via subtask model |
|
|
126
|
+
| `task.close` | `update_tasks` | `completed: true` on parent |
|
|
127
|
+
| `task.due-date.set` | `update_tasks` | `due_on: "YYYY-MM-DD"` |
|
|
128
|
+
| `task.assignee.add` | `update_tasks` + `addFollower` | primary assignee replaces; additional are followers |
|
|
129
|
+
| `task.comment.add` | `add_comment` (story) | adds a comment story to the task; apply attribution prefix if enabled |
|
|
129
130
|
|
|
130
131
|
`<task-ref>` resolution: accept Asana permalinks (`https://app.asana.com/0/<project>/<task>`), bare GIDs, or aliases from `.asana.json` `taskAliases[]`.
|
|
131
132
|
|
|
@@ -150,6 +151,7 @@ Asana-specific `details` per verb:
|
|
|
150
151
|
| Verb | `details` fields |
|
|
151
152
|
| ------------------- | ------------------------------------------------------------------------ |
|
|
152
153
|
| `task.create` | `{ parentGid, subtaskGids[], projectGid, sectionGid?, customFields[]? }` |
|
|
154
|
+
| `task.move` | `{ taskGid, sectionGid, targetList }` |
|
|
153
155
|
| `checklist.check` | `{ subtaskGid, completed: true }` |
|
|
154
156
|
| `task.close` | `{ parentGid, completed: true }` |
|
|
155
157
|
| `task.due-date.set` | `{ taskGid, due_on }` |
|
|
@@ -177,3 +179,11 @@ npx @llodev/pm-tasks-asana init
|
|
|
177
179
|
```
|
|
178
180
|
|
|
179
181
|
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`).
|
|
182
|
+
|
|
183
|
+
Pass `--doctor` to run workspace health checks before prompting:
|
|
184
|
+
|
|
185
|
+
```sh
|
|
186
|
+
npx @llodev/pm-tasks-asana init --doctor
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Runs core checks (C-FS-1..3, C-CFG-1..4) plus Asana-specific probes (C-ASN-1..3, gated on `LLODEV_PM_TASKS_ASANA_PAT`). Full check matrix in [`pm-tasks/pm-tasks-core/references/doctor.md`](../pm-tasks-core/references/doctor.md).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/bin/init.ts"],"names":[],"mappings":""}
|
package/dist/bin/init.js
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
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 { promptScope, promptYesNo, multiSelect, promptPick, aliasOf, writeConfig, validateConfig, probeMCP, printInstructions, promptLocale, loadStrings, registerI18nRoot, interpolate, } from "@llodev/pm-tasks-core/init-lib";
|
|
7
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
8
|
+
async function loadSchema() {
|
|
9
|
+
const raw = await readFile(path.join(ROOT, "schemas", "config.json"), "utf8");
|
|
10
|
+
return JSON.parse(raw);
|
|
11
|
+
}
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Helpers
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
async function asanaProbe() {
|
|
16
|
+
// The MCP runs in a different process. To probe within this script, call the
|
|
17
|
+
// Asana REST API directly with a Personal Access Token.
|
|
18
|
+
// Used only to enumerate workspaces / projects / sections / custom fields / members.
|
|
19
|
+
const TOKEN = process.env.LLODEV_PM_TASKS_ASANA_PAT;
|
|
20
|
+
if (!TOKEN)
|
|
21
|
+
throw new Error("auth: LLODEV_PM_TASKS_ASANA_PAT missing");
|
|
22
|
+
const j = async (p) => {
|
|
23
|
+
const r = await fetch(`https://app.asana.com/api/1.0${p}`, {
|
|
24
|
+
headers: { Authorization: `Bearer ${TOKEN}` },
|
|
25
|
+
});
|
|
26
|
+
if (!r.ok)
|
|
27
|
+
throw new Error(`HTTP ${r.status}`);
|
|
28
|
+
const body = (await r.json());
|
|
29
|
+
return body.data;
|
|
30
|
+
};
|
|
31
|
+
return {
|
|
32
|
+
getMe: () => j("/users/me?opt_fields=gid,name,email"),
|
|
33
|
+
getWorkspaces: () => j("/workspaces?opt_fields=name"),
|
|
34
|
+
getProjects: (workspaceGid) => j(`/projects?workspace=${workspaceGid}&opt_fields=name&limit=100`),
|
|
35
|
+
getSections: (projectGid) => j(`/projects/${projectGid}/sections?opt_fields=name&limit=100`),
|
|
36
|
+
getCustomFields: (projectGid) => j(`/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`),
|
|
37
|
+
getMembers: (projectGid) => j(`/projects/${projectGid}/members?opt_fields=user.name,user.gid&limit=100`),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
async function promptManualMember(asanaStrings) {
|
|
41
|
+
const { createInterface } = await import("node:readline/promises");
|
|
42
|
+
const { stdin: input, stdout: output } = await import("node:process");
|
|
43
|
+
const r = createInterface({ input, output });
|
|
44
|
+
try {
|
|
45
|
+
const gid = (await r.question(asanaStrings.manualMemberGid)).trim();
|
|
46
|
+
if (!gid)
|
|
47
|
+
return null;
|
|
48
|
+
const name = (await r.question(asanaStrings.manualMemberName)).trim() || "owner";
|
|
49
|
+
const alias = (await r.question(asanaStrings.manualMemberAlias)).trim() || "owner";
|
|
50
|
+
return { id: gid, name, alias };
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
r.close();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function collectEscalationMember(out, { coreStrings, asanaStrings }) {
|
|
57
|
+
const candidates = out.members.filter((m) => m.alias !== "me");
|
|
58
|
+
if (candidates.length) {
|
|
59
|
+
const choices = candidates.map((m) => ({ label: `${m.name} (${m.alias})`, value: m }));
|
|
60
|
+
const picked = await promptPick(asanaStrings.escalationPrompt, choices, {
|
|
61
|
+
defaultIndex: 0,
|
|
62
|
+
allowSkip: true,
|
|
63
|
+
strings: coreStrings,
|
|
64
|
+
});
|
|
65
|
+
if (picked) {
|
|
66
|
+
if (picked.alias !== "owner") {
|
|
67
|
+
if (!out.members.find((m) => m.alias === "owner"))
|
|
68
|
+
picked.alias = "owner";
|
|
69
|
+
}
|
|
70
|
+
out.defaults.escalateToAlias = picked.alias;
|
|
71
|
+
}
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const add = await promptYesNo(asanaStrings.noMembersPrompt, { strings: coreStrings });
|
|
75
|
+
if (!add)
|
|
76
|
+
return;
|
|
77
|
+
const manual = await promptManualMember(asanaStrings);
|
|
78
|
+
if (!manual)
|
|
79
|
+
return;
|
|
80
|
+
out.members.push(manual);
|
|
81
|
+
out.defaults.escalateToAlias = manual.alias;
|
|
82
|
+
}
|
|
83
|
+
function mapResourceType(subtype) {
|
|
84
|
+
switch (subtype) {
|
|
85
|
+
case "enum":
|
|
86
|
+
return "enum";
|
|
87
|
+
case "multi_enum":
|
|
88
|
+
return "multi_enum";
|
|
89
|
+
case "number":
|
|
90
|
+
return "number";
|
|
91
|
+
case "date":
|
|
92
|
+
return "date";
|
|
93
|
+
case "people":
|
|
94
|
+
return "people";
|
|
95
|
+
default:
|
|
96
|
+
return "text";
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// Main
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
async function run() {
|
|
103
|
+
const argv = process.argv.slice(2);
|
|
104
|
+
if (argv.includes("--doctor")) {
|
|
105
|
+
const { runDoctor } = await import("../doctor-cli.js");
|
|
106
|
+
await runDoctor({ tool: "asana", argv });
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
registerI18nRoot("asana", path.join(ROOT, "i18n"));
|
|
110
|
+
const locale = await promptLocale("core", { defaultLocale: "en-US" });
|
|
111
|
+
const coreStrings = await loadStrings("core", locale);
|
|
112
|
+
const asanaStrings = await loadStrings("asana", locale);
|
|
113
|
+
console.log(`\n${asanaStrings.header}\n`);
|
|
114
|
+
const { path: outPath } = await promptScope("asana", { strings: coreStrings });
|
|
115
|
+
const probe = await probeMCP({
|
|
116
|
+
tool: "asana",
|
|
117
|
+
probeCommand: async () => {
|
|
118
|
+
const api = await asanaProbe();
|
|
119
|
+
await api.getMe();
|
|
120
|
+
return api;
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
if (probe.unauthenticated) {
|
|
124
|
+
printInstructions([asanaStrings.patMissingTitle, asanaStrings.patMissingBody]);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (!probe.mcpAvailable) {
|
|
128
|
+
printInstructions([asanaStrings.mcpMissingTitle, asanaStrings.mcpMissingBody]);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const api = probe.result;
|
|
132
|
+
const me = await api.getMe();
|
|
133
|
+
const workspaces = await api.getWorkspaces();
|
|
134
|
+
const pickedWorkspaces = await multiSelect(asanaStrings.workspacePrompt, workspaces.map((w) => ({ label: `${w.name} (${w.gid})`, value: w })), { strings: coreStrings });
|
|
135
|
+
if (!pickedWorkspaces.length) {
|
|
136
|
+
console.error(asanaStrings.noWorkspace);
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
const workspace = pickedWorkspaces[0];
|
|
140
|
+
const projects = await api.getProjects(workspace.gid);
|
|
141
|
+
const pickedProjects = await multiSelect(interpolate(asanaStrings.projectsPrompt, { workspace: workspace.name }), projects.map((p) => ({ label: `${p.name} (${p.gid})`, value: p })), { strings: coreStrings });
|
|
142
|
+
if (!pickedProjects.length) {
|
|
143
|
+
console.error(asanaStrings.noProject);
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
const out = {
|
|
147
|
+
$schema: "https://llodev.github.io/skills/schemas/pm-tasks-asana.json",
|
|
148
|
+
version: "1",
|
|
149
|
+
locale,
|
|
150
|
+
workspace: { id: workspace.gid, name: workspace.name },
|
|
151
|
+
projects: [],
|
|
152
|
+
sections: [],
|
|
153
|
+
customFields: [],
|
|
154
|
+
members: [{ id: me.gid, name: me.name, email: me.email, alias: "me" }],
|
|
155
|
+
defaults: {},
|
|
156
|
+
};
|
|
157
|
+
const inheritFieldIds = new Set();
|
|
158
|
+
for (const p of pickedProjects) {
|
|
159
|
+
const alias = aliasOf(p.name);
|
|
160
|
+
out.projects.push({ id: p.gid, name: p.name, alias });
|
|
161
|
+
const sections = await api.getSections(p.gid);
|
|
162
|
+
const pickedSections = await multiSelect(interpolate(asanaStrings.sectionsPrompt, { project: p.name }), sections.map((s) => ({ label: s.name, value: s })), { strings: coreStrings });
|
|
163
|
+
for (const s of pickedSections) {
|
|
164
|
+
out.sections.push({
|
|
165
|
+
projectAlias: alias,
|
|
166
|
+
id: s.gid,
|
|
167
|
+
name: s.name,
|
|
168
|
+
alias: aliasOf(s.name),
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
const cfSettings = await api.getCustomFields(p.gid);
|
|
172
|
+
const fields = cfSettings
|
|
173
|
+
.map((cs) => cs.custom_field)
|
|
174
|
+
.filter((f) => f !== null);
|
|
175
|
+
if (fields.length) {
|
|
176
|
+
const pickedFields = await multiSelect(interpolate(asanaStrings.customFieldsPrompt, { project: p.name }), fields.map((f) => ({ label: `${f.name} [${f.resource_subtype}]`, value: f })), { strings: coreStrings });
|
|
177
|
+
for (const f of pickedFields) {
|
|
178
|
+
const entry = {
|
|
179
|
+
projectAlias: alias,
|
|
180
|
+
id: f.gid,
|
|
181
|
+
name: f.name,
|
|
182
|
+
type: mapResourceType(f.resource_subtype),
|
|
183
|
+
alias: aliasOf(f.name),
|
|
184
|
+
};
|
|
185
|
+
if (Array.isArray(f.enum_options) && f.enum_options.length) {
|
|
186
|
+
entry.options = f.enum_options.map((opt) => ({
|
|
187
|
+
id: opt.gid,
|
|
188
|
+
name: opt.name,
|
|
189
|
+
alias: aliasOf(opt.name),
|
|
190
|
+
}));
|
|
191
|
+
}
|
|
192
|
+
out.customFields.push(entry);
|
|
193
|
+
}
|
|
194
|
+
if (pickedFields.length) {
|
|
195
|
+
const inheritPicked = await multiSelect(asanaStrings.inheritPrompt, pickedFields.map((f) => ({ label: f.name, value: f })), { strings: coreStrings });
|
|
196
|
+
for (const f of inheritPicked)
|
|
197
|
+
inheritFieldIds.add(f.gid);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
const memberships = await api.getMembers(p.gid);
|
|
202
|
+
const users = memberships.map((m) => m.user).filter((u) => u !== null);
|
|
203
|
+
for (const u of users) {
|
|
204
|
+
if (u.gid === me.gid)
|
|
205
|
+
continue;
|
|
206
|
+
if (out.members.find((m) => m.id === u.gid))
|
|
207
|
+
continue;
|
|
208
|
+
out.members.push({ id: u.gid, name: u.name, alias: aliasOf(u.name) });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
catch (_e) {
|
|
212
|
+
// Project membership listing may require additional scopes; skip silently.
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (out.projects.length === 1) {
|
|
216
|
+
out.defaults.projectAlias = out.projects[0].alias;
|
|
217
|
+
out.defaults.assigneeAlias = "me";
|
|
218
|
+
const sectionChoices = out.sections.map((s) => ({
|
|
219
|
+
label: `${s.name} (${s.alias})`,
|
|
220
|
+
value: s,
|
|
221
|
+
}));
|
|
222
|
+
if (sectionChoices.length) {
|
|
223
|
+
const open = await promptPick(asanaStrings.openSectionPrompt, sectionChoices, {
|
|
224
|
+
defaultIndex: 0,
|
|
225
|
+
allowSkip: true,
|
|
226
|
+
strings: coreStrings,
|
|
227
|
+
});
|
|
228
|
+
if (open)
|
|
229
|
+
out.defaults.sectionAlias = open.alias;
|
|
230
|
+
const close = await promptPick(asanaStrings.closeSectionPrompt, sectionChoices, {
|
|
231
|
+
defaultIndex: sectionChoices.length - 1,
|
|
232
|
+
allowSkip: true,
|
|
233
|
+
strings: coreStrings,
|
|
234
|
+
});
|
|
235
|
+
if (close)
|
|
236
|
+
out.defaults.closeSectionAlias = close.alias;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
await collectEscalationMember(out, { coreStrings, asanaStrings });
|
|
240
|
+
if (inheritFieldIds.size) {
|
|
241
|
+
out.subtaskDefaults = {
|
|
242
|
+
inheritParentFields: [...inheritFieldIds],
|
|
243
|
+
inheritAssignee: true,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
const wantAuto = await promptYesNo(coreStrings.autonomousPromptQuestion, {
|
|
247
|
+
strings: coreStrings,
|
|
248
|
+
});
|
|
249
|
+
if (wantAuto) {
|
|
250
|
+
out.autonomous = {
|
|
251
|
+
enabled: false,
|
|
252
|
+
allow: ["task.create", "checklist.check", "task.close", "task.comment.add"],
|
|
253
|
+
scope: {
|
|
254
|
+
projects: out.projects.map((p) => p.id),
|
|
255
|
+
sections: out.sections.map((s) => s.id),
|
|
256
|
+
},
|
|
257
|
+
rateLimit: { writesPerMinute: 30, commentsPerMinute: 10 },
|
|
258
|
+
auditLog: "~/.local/share/llodev/pm-tasks/asana/audit.log",
|
|
259
|
+
};
|
|
260
|
+
printInstructions([coreStrings.autonomousAddedTitle, coreStrings.autonomousReviewBody]);
|
|
261
|
+
}
|
|
262
|
+
const schema = await loadSchema();
|
|
263
|
+
const valid = await validateConfig(out, schema);
|
|
264
|
+
if (!valid.ok) {
|
|
265
|
+
console.error(`${coreStrings.errInvalidConfig}:`, JSON.stringify(valid.errors, null, 2));
|
|
266
|
+
process.exit(1);
|
|
267
|
+
}
|
|
268
|
+
await writeConfig(outPath, out);
|
|
269
|
+
printInstructions([
|
|
270
|
+
interpolate(coreStrings.configWritten, { path: outPath }),
|
|
271
|
+
asanaStrings.tryItTitle,
|
|
272
|
+
asanaStrings.tokenReminderTitle,
|
|
273
|
+
asanaStrings.tokenReminderBody,
|
|
274
|
+
]);
|
|
275
|
+
}
|
|
276
|
+
run().catch((e) => {
|
|
277
|
+
console.error(e);
|
|
278
|
+
process.exit(1);
|
|
279
|
+
});
|
|
280
|
+
//# sourceMappingURL=init.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"init.js","sourceRoot":"","sources":["../../src/bin/init.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doctor-cli.d.ts","sourceRoot":"","sources":["../src/doctor-cli.ts"],"names":[],"mappings":"AAiIA,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,wBAAsB,SAAS,CAAC,EAAE,IAAI,EAAE,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CA+CtE"}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// pm-tasks-asana doctor-cli — adapter-specific health checks + runner
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { resolveDataDir } from "@llodev/pm-tasks-core/audit";
|
|
6
|
+
import { runChecks } from "@llodev/pm-tasks-core/doctor";
|
|
7
|
+
import { renderReport } from "@llodev/pm-tasks-core/bin/doctor";
|
|
8
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Asana-specific checks (C-ASN-1..3)
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
async function asanaFetch(urlPath, pat) {
|
|
13
|
+
try {
|
|
14
|
+
const res = await globalThis.fetch(`https://app.asana.com/api/1.0${urlPath}`, {
|
|
15
|
+
headers: { Authorization: `Bearer ${pat}` },
|
|
16
|
+
});
|
|
17
|
+
return { status: res.status, ok: res.ok };
|
|
18
|
+
}
|
|
19
|
+
catch (e) {
|
|
20
|
+
throw new Error(`network error: ${e.message}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const C_ASN_1 = {
|
|
24
|
+
id: "C-ASN-1",
|
|
25
|
+
label: "Asana REST /users/me returns 200",
|
|
26
|
+
severity: "error",
|
|
27
|
+
async run(_ctx) {
|
|
28
|
+
const pat = process.env.LLODEV_PM_TASKS_ASANA_PAT;
|
|
29
|
+
if (!pat) {
|
|
30
|
+
return {
|
|
31
|
+
ok: true,
|
|
32
|
+
message: "LLODEV_PM_TASKS_ASANA_PAT env var missing — skipping auth probe",
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const res = await asanaFetch("/users/me", pat);
|
|
37
|
+
if (res.ok) {
|
|
38
|
+
return { ok: true, message: "/users/me returned 200" };
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
ok: false,
|
|
42
|
+
message: `/users/me returned HTTP ${res.status}`,
|
|
43
|
+
fixHint: "Re-generate your Asana personal access token.",
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
catch (e) {
|
|
47
|
+
return { ok: false, message: e.message };
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
const C_ASN_2 = {
|
|
52
|
+
id: "C-ASN-2",
|
|
53
|
+
label: "Configured projects resolve",
|
|
54
|
+
severity: "error",
|
|
55
|
+
async run(ctx) {
|
|
56
|
+
const pat = process.env.LLODEV_PM_TASKS_ASANA_PAT;
|
|
57
|
+
if (!pat) {
|
|
58
|
+
return {
|
|
59
|
+
ok: true,
|
|
60
|
+
message: "LLODEV_PM_TASKS_ASANA_PAT env var missing — skipping project probe",
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const cfg = ctx.config;
|
|
64
|
+
const projects = cfg["projects"] ?? [];
|
|
65
|
+
const failures = [];
|
|
66
|
+
for (const proj of projects) {
|
|
67
|
+
const id = String(proj["id"]);
|
|
68
|
+
try {
|
|
69
|
+
const res = await asanaFetch(`/projects/${id}?opt_fields=gid`, pat);
|
|
70
|
+
if (!res.ok)
|
|
71
|
+
failures.push(`${id} (HTTP ${res.status})`);
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
failures.push(`${id} (${e.message})`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (failures.length === 0) {
|
|
78
|
+
return { ok: true, message: `all ${projects.length} project(s) resolve` };
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
ok: false,
|
|
82
|
+
message: `unresolvable projects: ${failures.join(", ")}`,
|
|
83
|
+
fixHint: "Verify the project GID still exists or remove from config.",
|
|
84
|
+
};
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
const C_ASN_3 = {
|
|
88
|
+
id: "C-ASN-3",
|
|
89
|
+
label: "Configured sections reachable",
|
|
90
|
+
severity: "error",
|
|
91
|
+
async run(ctx) {
|
|
92
|
+
const pat = process.env.LLODEV_PM_TASKS_ASANA_PAT;
|
|
93
|
+
if (!pat) {
|
|
94
|
+
return {
|
|
95
|
+
ok: true,
|
|
96
|
+
message: "LLODEV_PM_TASKS_ASANA_PAT env var missing — skipping section probe",
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
const cfg = ctx.config;
|
|
100
|
+
const sections = cfg["sections"] ?? [];
|
|
101
|
+
const failures = [];
|
|
102
|
+
for (const sec of sections) {
|
|
103
|
+
const id = String(sec["id"]);
|
|
104
|
+
try {
|
|
105
|
+
const res = await asanaFetch(`/sections/${id}?opt_fields=gid`, pat);
|
|
106
|
+
if (!res.ok)
|
|
107
|
+
failures.push(`${id} (HTTP ${res.status})`);
|
|
108
|
+
}
|
|
109
|
+
catch (e) {
|
|
110
|
+
failures.push(`${id} (${e.message})`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (failures.length === 0) {
|
|
114
|
+
return { ok: true, message: `all ${sections.length} section(s) reachable` };
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
ok: false,
|
|
118
|
+
message: `unreachable sections: ${failures.join(", ")}`,
|
|
119
|
+
fixHint: "Verify the section GID still exists or remove from config.",
|
|
120
|
+
};
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
const ADAPTER_CHECKS = [C_ASN_1, C_ASN_2, C_ASN_3];
|
|
124
|
+
export async function runDoctor({ argv }) {
|
|
125
|
+
const jsonMode = argv.includes("--json");
|
|
126
|
+
const fixHintsOnly = argv.includes("--fix-hints-only");
|
|
127
|
+
// --config override
|
|
128
|
+
const configIdx = argv.indexOf("--config");
|
|
129
|
+
let configPath;
|
|
130
|
+
if (configIdx !== -1 && argv[configIdx + 1]) {
|
|
131
|
+
configPath = argv[configIdx + 1];
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
configPath = path.resolve(process.cwd(), ".asana.json");
|
|
135
|
+
}
|
|
136
|
+
let config;
|
|
137
|
+
try {
|
|
138
|
+
const raw = await readFile(configPath, "utf8");
|
|
139
|
+
config = JSON.parse(raw);
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
config = {};
|
|
143
|
+
}
|
|
144
|
+
const schemaRaw = await readFile(path.join(ROOT, "schemas", "config.json"), "utf8");
|
|
145
|
+
const manifestRaw = await readFile(path.join(ROOT, "manifest.json"), "utf8");
|
|
146
|
+
const schema = JSON.parse(schemaRaw);
|
|
147
|
+
const manifest = JSON.parse(manifestRaw);
|
|
148
|
+
const ctx = {
|
|
149
|
+
tool: "asana",
|
|
150
|
+
configPath,
|
|
151
|
+
config,
|
|
152
|
+
manifest,
|
|
153
|
+
schema,
|
|
154
|
+
auditLogPath: path.join(resolveDataDir("asana"), "audit.log"),
|
|
155
|
+
auditRotationMaxBytes: 10 * 1024 * 1024,
|
|
156
|
+
};
|
|
157
|
+
const report = await runChecks(ctx, ADAPTER_CHECKS);
|
|
158
|
+
const opts = {
|
|
159
|
+
format: jsonMode ? "json" : "md",
|
|
160
|
+
fixHintsOnly,
|
|
161
|
+
};
|
|
162
|
+
console.log(renderReport(report, opts));
|
|
163
|
+
const hasError = report.results.some((r) => !r.result.ok && r.check.severity === "error");
|
|
164
|
+
process.exit(hasError ? 1 : 0);
|
|
165
|
+
}
|
|
166
|
+
//# sourceMappingURL=doctor-cli.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doctor-cli.js","sourceRoot":"","sources":["../src/doctor-cli.ts"],"names":[],"mappings":"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"}
|
package/manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llodev/pm-tasks-asana",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
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",
|
|
@@ -9,18 +9,20 @@
|
|
|
9
9
|
"url": "https://github.com/llodev/skills.git",
|
|
10
10
|
"directory": "pm-tasks/pm-tasks-asana"
|
|
11
11
|
},
|
|
12
|
+
"main": "./dist/bin/init.js",
|
|
13
|
+
"types": "./dist/bin/init.d.ts",
|
|
12
14
|
"files": [
|
|
13
15
|
"SKILL.md",
|
|
14
16
|
"README.md",
|
|
17
|
+
"LICENSE",
|
|
18
|
+
"CHANGELOG.md",
|
|
15
19
|
"docs",
|
|
16
|
-
"schemas",
|
|
17
|
-
"references",
|
|
18
20
|
"anti-patterns",
|
|
19
|
-
"
|
|
21
|
+
"references",
|
|
20
22
|
"i18n",
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
23
|
+
"schemas",
|
|
24
|
+
"manifest.json",
|
|
25
|
+
"dist"
|
|
24
26
|
],
|
|
25
27
|
"keywords": [
|
|
26
28
|
"agent-skill",
|
|
@@ -30,16 +32,32 @@
|
|
|
30
32
|
"asana-mcp"
|
|
31
33
|
],
|
|
32
34
|
"type": "module",
|
|
35
|
+
"exports": {
|
|
36
|
+
".": {
|
|
37
|
+
"types": "./dist/bin/init.d.ts",
|
|
38
|
+
"import": "./dist/bin/init.js"
|
|
39
|
+
},
|
|
40
|
+
"./i18n/*.json": "./i18n/*.json",
|
|
41
|
+
"./schemas/*.json": "./schemas/*.json",
|
|
42
|
+
"./manifest.json": "./manifest.json"
|
|
43
|
+
},
|
|
33
44
|
"bin": {
|
|
34
|
-
"pm-tasks-asana": "./
|
|
45
|
+
"pm-tasks-asana": "./dist/bin/init.js"
|
|
35
46
|
},
|
|
36
47
|
"dependencies": {
|
|
37
|
-
"
|
|
48
|
+
"ajv": "^8.17.1",
|
|
49
|
+
"ajv-formats": "^3.0.1",
|
|
50
|
+
"@llodev/pm-tasks-core": "^1.8.0"
|
|
38
51
|
},
|
|
39
52
|
"publishConfig": {
|
|
40
53
|
"access": "public"
|
|
41
54
|
},
|
|
42
55
|
"engines": {
|
|
43
56
|
"node": ">=20"
|
|
57
|
+
},
|
|
58
|
+
"scripts": {
|
|
59
|
+
"build": "tsc -p tsconfig.json && chmod +x dist/bin/init.js",
|
|
60
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
61
|
+
"test": "pnpm build && vitest run --passWithNoTests"
|
|
44
62
|
}
|
|
45
63
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Asana CRUD operations (v1)
|
|
2
|
+
|
|
3
|
+
Verb → MCP tool mapping for `@llodev/pm-tasks-asana`. All verbs return the core result envelope; see [`../../pm-tasks-core/references/contract.md`](../../pm-tasks-core/references/contract.md) § Result envelope.
|
|
4
|
+
|
|
5
|
+
## Verb → MCP tool
|
|
6
|
+
|
|
7
|
+
| Core verb | Asana MCP tool (`mcp__claude_ai_Asana__*`) | Notes |
|
|
8
|
+
| ------------------- | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
9
|
+
| `task.create` | `create_tasks` | parent + subtasks; `memberships: [{ project, section }]` places card in the correct section at creation time. |
|
|
10
|
+
| `task.move` | `update_tasks` | `memberships: [{ project: <projectGid>, section: <resolvedGid> }]`. See § `task.move` below for resolution rules. Idempotency: check current section before calling. |
|
|
11
|
+
| `checklist.check` | `update_tasks` | `{ task: <subtaskGid>, completed: true }`. Subtask model emulates checklist items. |
|
|
12
|
+
| `task.close` | `update_tasks` | `{ task: <parentGid>, completed: true }`. Visual section move is handled separately by `task.move(cardId, "done")` before calling `task.close`. |
|
|
13
|
+
| `task.due-date.set` | `update_tasks` | `{ task: <gid>, due_on: "YYYY-MM-DD" }`. Pass `null` to clear. |
|
|
14
|
+
| `task.assignee.add` | `update_tasks` + `update_tasks` (add_followers) | Primary assignee via `assignee` field; additional users become followers via `add_followers: [<gid>]`. |
|
|
15
|
+
| `task.comment.add` | `add_comment` | Creates a comment story on the task. Apply attribution prefix per `config.locale` if `config.attribution` is enabled. |
|
|
16
|
+
|
|
17
|
+
## `task.move` — resolution rules
|
|
18
|
+
|
|
19
|
+
Schema: `{ cardId: string, targetList: "open" | "wip" | "done" | string }`
|
|
20
|
+
|
|
21
|
+
Resolution order for `targetList`:
|
|
22
|
+
|
|
23
|
+
1. If `targetList` is `"wip"` → look up `defaults.wipSectionAlias` in `.asana.json`, find the matching entry in `sections[]` by alias, use its `id` as the section GID.
|
|
24
|
+
2. If `targetList` is `"done"` → look up `defaults.doneSectionAlias` in `.asana.json`.
|
|
25
|
+
3. If `targetList` is `"open"` → look up `defaults.openSectionAlias` in `.asana.json`.
|
|
26
|
+
4. Otherwise → treat `targetList` as a raw Asana section GID and pass through directly.
|
|
27
|
+
|
|
28
|
+
The resolved section GID MUST be in `autonomous.scope.sections` — otherwise the verb returns `{ ok: false, code: "OUT_OF_SCOPE" }`.
|
|
29
|
+
|
|
30
|
+
Idempotency: fetch current task memberships before calling `update_tasks`. If the task is already in the target section, return `{ ok: true }` without an MCP write.
|
|
31
|
+
|
|
32
|
+
## `<task-ref>` resolution for Asana
|
|
33
|
+
|
|
34
|
+
Accept, in order:
|
|
35
|
+
|
|
36
|
+
1. Full Asana permalink (`https://app.asana.com/0/<project>/<task>`).
|
|
37
|
+
2. Bare GID (numeric string).
|
|
38
|
+
3. Alias from `.asana.json` `taskAliases[]`.
|
|
39
|
+
4. `clientToken` match in audit log (most recent).
|
|
40
|
+
5. Name partial match in audit log scoped to `autonomous.scope`.
|
|
41
|
+
6. Otherwise → `{ ok: false, code: "REF_NOT_RESOLVED", candidates: [...] }`.
|
|
42
|
+
|
|
43
|
+
## Idempotency
|
|
44
|
+
|
|
45
|
+
| Verb | Idempotency rule |
|
|
46
|
+
| ------------- | -------------------------------------------------------------------------------------------------------------- |
|
|
47
|
+
| `task.create` | Before creating, search scope for task with matching `clientToken`. If found, return existing ref. |
|
|
48
|
+
| `task.move` | Check current section before `update_tasks`. No-op if already in target. |
|
|
49
|
+
| `task.close` | Natural — `completed: true` is a no-op if already completed. |
|
|
50
|
+
| others | Natural or `clientToken`-based; see [`crud-vocabulary.md`](../../pm-tasks-core/references/crud-vocabulary.md). |
|
|
51
|
+
|
|
52
|
+
## Result envelope — Asana-specific `details`
|
|
53
|
+
|
|
54
|
+
| Verb | `details` fields |
|
|
55
|
+
| ------------------- | ------------------------------------------------------------------------ |
|
|
56
|
+
| `task.create` | `{ parentGid, subtaskGids[], projectGid, sectionGid?, customFields[]? }` |
|
|
57
|
+
| `task.move` | `{ taskGid, sectionGid, targetList }` |
|
|
58
|
+
| `checklist.check` | `{ subtaskGid, completed: true }` |
|
|
59
|
+
| `task.close` | `{ parentGid, completed: true }` |
|
|
60
|
+
| `task.due-date.set` | `{ taskGid, due_on }` |
|
|
61
|
+
| `task.assignee.add` | `{ taskGid, assignee, followers[]? }` |
|
|
62
|
+
| `task.comment.add` | `{ taskGid, storyGid }` |
|
package/i18n/parity.test.mjs
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
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/scripts/init.mjs
DELETED
|
@@ -1,319 +0,0 @@
|
|
|
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
|
-
promptPick,
|
|
11
|
-
aliasOf,
|
|
12
|
-
writeConfig,
|
|
13
|
-
validateConfig,
|
|
14
|
-
probeMCP,
|
|
15
|
-
printInstructions,
|
|
16
|
-
promptLocale,
|
|
17
|
-
loadStrings,
|
|
18
|
-
registerI18nRoot,
|
|
19
|
-
interpolate,
|
|
20
|
-
} from "@llodev/pm-tasks-core/init-lib";
|
|
21
|
-
|
|
22
|
-
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
23
|
-
|
|
24
|
-
async function loadSchema() {
|
|
25
|
-
const raw = await readFile(path.join(ROOT, "schemas", "config.json"), "utf8");
|
|
26
|
-
return JSON.parse(raw);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async function asanaProbe() {
|
|
30
|
-
// The MCP runs in a different process. To probe within this script, call the
|
|
31
|
-
// Asana REST API directly with a Personal Access Token.
|
|
32
|
-
// Used only to enumerate workspaces / projects / sections / custom fields / members.
|
|
33
|
-
const TOKEN = process.env.LLODEV_PM_TASKS_ASANA_PAT;
|
|
34
|
-
if (!TOKEN) throw new Error("auth: LLODEV_PM_TASKS_ASANA_PAT missing");
|
|
35
|
-
const j = async (p) => {
|
|
36
|
-
const r = await fetch(`https://app.asana.com/api/1.0${p}`, {
|
|
37
|
-
headers: { Authorization: `Bearer ${TOKEN}` },
|
|
38
|
-
});
|
|
39
|
-
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
|
40
|
-
const body = await r.json();
|
|
41
|
-
return body.data;
|
|
42
|
-
};
|
|
43
|
-
return {
|
|
44
|
-
getMe: () => j("/users/me?opt_fields=gid,name,email"),
|
|
45
|
-
getWorkspaces: () => j("/workspaces?opt_fields=name"),
|
|
46
|
-
getProjects: (workspaceGid) =>
|
|
47
|
-
j(`/projects?workspace=${workspaceGid}&opt_fields=name&limit=100`),
|
|
48
|
-
getSections: (projectGid) => j(`/projects/${projectGid}/sections?opt_fields=name&limit=100`),
|
|
49
|
-
getCustomFields: (projectGid) =>
|
|
50
|
-
j(
|
|
51
|
-
`/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`,
|
|
52
|
-
),
|
|
53
|
-
getMembers: (projectGid) =>
|
|
54
|
-
j(`/projects/${projectGid}/members?opt_fields=user.name,user.gid&limit=100`),
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async function promptManualMember(asanaStrings) {
|
|
59
|
-
const { createInterface } = await import("node:readline/promises");
|
|
60
|
-
const { stdin: input, stdout: output } = await import("node:process");
|
|
61
|
-
const r = createInterface({ input, output });
|
|
62
|
-
try {
|
|
63
|
-
const gid = (await r.question(asanaStrings.manualMemberGid)).trim();
|
|
64
|
-
if (!gid) return null;
|
|
65
|
-
const name = (await r.question(asanaStrings.manualMemberName)).trim() || "owner";
|
|
66
|
-
const alias = (await r.question(asanaStrings.manualMemberAlias)).trim() || "owner";
|
|
67
|
-
return { id: gid, name, alias };
|
|
68
|
-
} finally {
|
|
69
|
-
r.close();
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async function collectEscalationMember(out, { coreStrings, asanaStrings }) {
|
|
74
|
-
const candidates = out.members.filter((m) => m.alias !== "me");
|
|
75
|
-
if (candidates.length) {
|
|
76
|
-
const choices = candidates.map((m) => ({ label: `${m.name} (${m.alias})`, value: m }));
|
|
77
|
-
const picked = await promptPick(asanaStrings.escalationPrompt, choices, {
|
|
78
|
-
defaultIndex: 0,
|
|
79
|
-
allowSkip: true,
|
|
80
|
-
strings: coreStrings,
|
|
81
|
-
});
|
|
82
|
-
if (picked) {
|
|
83
|
-
if (picked.alias !== "owner") {
|
|
84
|
-
if (!out.members.find((m) => m.alias === "owner")) picked.alias = "owner";
|
|
85
|
-
}
|
|
86
|
-
out.defaults.escalateToAlias = picked.alias;
|
|
87
|
-
}
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
const add = await promptYesNo(asanaStrings.noMembersPrompt, { strings: coreStrings });
|
|
91
|
-
if (!add) return;
|
|
92
|
-
const manual = await promptManualMember(asanaStrings);
|
|
93
|
-
if (!manual) return;
|
|
94
|
-
out.members.push(manual);
|
|
95
|
-
out.defaults.escalateToAlias = manual.alias;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function mapResourceType(subtype) {
|
|
99
|
-
switch (subtype) {
|
|
100
|
-
case "enum":
|
|
101
|
-
return "enum";
|
|
102
|
-
case "multi_enum":
|
|
103
|
-
return "multi_enum";
|
|
104
|
-
case "number":
|
|
105
|
-
return "number";
|
|
106
|
-
case "date":
|
|
107
|
-
return "date";
|
|
108
|
-
case "people":
|
|
109
|
-
return "people";
|
|
110
|
-
default:
|
|
111
|
-
return "text";
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
async function run() {
|
|
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`);
|
|
121
|
-
|
|
122
|
-
const { path: outPath } = await promptScope("asana", { strings: coreStrings });
|
|
123
|
-
|
|
124
|
-
const probe = await probeMCP({
|
|
125
|
-
tool: "asana",
|
|
126
|
-
probeCommand: async () => {
|
|
127
|
-
const api = await asanaProbe();
|
|
128
|
-
await api.getMe();
|
|
129
|
-
return api;
|
|
130
|
-
},
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
if (probe.unauthenticated) {
|
|
134
|
-
printInstructions([asanaStrings.patMissingTitle, asanaStrings.patMissingBody]);
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
if (!probe.mcpAvailable) {
|
|
139
|
-
printInstructions([asanaStrings.mcpMissingTitle, asanaStrings.mcpMissingBody]);
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const api = probe.result;
|
|
144
|
-
const me = await api.getMe();
|
|
145
|
-
const workspaces = await api.getWorkspaces();
|
|
146
|
-
|
|
147
|
-
const pickedWorkspaces = await multiSelect(
|
|
148
|
-
asanaStrings.workspacePrompt,
|
|
149
|
-
workspaces.map((w) => ({ label: `${w.name} (${w.gid})`, value: w })),
|
|
150
|
-
{ strings: coreStrings },
|
|
151
|
-
);
|
|
152
|
-
if (!pickedWorkspaces.length) {
|
|
153
|
-
console.error(asanaStrings.noWorkspace);
|
|
154
|
-
process.exit(1);
|
|
155
|
-
}
|
|
156
|
-
const workspace = pickedWorkspaces[0];
|
|
157
|
-
|
|
158
|
-
const projects = await api.getProjects(workspace.gid);
|
|
159
|
-
const pickedProjects = await multiSelect(
|
|
160
|
-
interpolate(asanaStrings.projectsPrompt, { workspace: workspace.name }),
|
|
161
|
-
projects.map((p) => ({ label: `${p.name} (${p.gid})`, value: p })),
|
|
162
|
-
{ strings: coreStrings },
|
|
163
|
-
);
|
|
164
|
-
if (!pickedProjects.length) {
|
|
165
|
-
console.error(asanaStrings.noProject);
|
|
166
|
-
process.exit(1);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const out = {
|
|
170
|
-
$schema: "https://llodev.github.io/skills/schemas/pm-tasks-asana.json",
|
|
171
|
-
version: "1",
|
|
172
|
-
locale,
|
|
173
|
-
workspace: { id: workspace.gid, name: workspace.name },
|
|
174
|
-
projects: [],
|
|
175
|
-
sections: [],
|
|
176
|
-
customFields: [],
|
|
177
|
-
members: [{ id: me.gid, name: me.name, email: me.email, alias: "me" }],
|
|
178
|
-
defaults: {},
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
const inheritFieldIds = new Set();
|
|
182
|
-
|
|
183
|
-
for (const p of pickedProjects) {
|
|
184
|
-
const alias = aliasOf(p.name);
|
|
185
|
-
out.projects.push({ id: p.gid, name: p.name, alias });
|
|
186
|
-
|
|
187
|
-
const sections = await api.getSections(p.gid);
|
|
188
|
-
const pickedSections = await multiSelect(
|
|
189
|
-
interpolate(asanaStrings.sectionsPrompt, { project: p.name }),
|
|
190
|
-
sections.map((s) => ({ label: s.name, value: s })),
|
|
191
|
-
{ strings: coreStrings },
|
|
192
|
-
);
|
|
193
|
-
for (const s of pickedSections) {
|
|
194
|
-
out.sections.push({
|
|
195
|
-
projectAlias: alias,
|
|
196
|
-
id: s.gid,
|
|
197
|
-
name: s.name,
|
|
198
|
-
alias: aliasOf(s.name),
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const cfSettings = await api.getCustomFields(p.gid);
|
|
203
|
-
const fields = cfSettings.map((cs) => cs.custom_field).filter(Boolean);
|
|
204
|
-
if (fields.length) {
|
|
205
|
-
const pickedFields = await multiSelect(
|
|
206
|
-
interpolate(asanaStrings.customFieldsPrompt, { project: p.name }),
|
|
207
|
-
fields.map((f) => ({ label: `${f.name} [${f.resource_subtype}]`, value: f })),
|
|
208
|
-
{ strings: coreStrings },
|
|
209
|
-
);
|
|
210
|
-
for (const f of pickedFields) {
|
|
211
|
-
const entry = {
|
|
212
|
-
projectAlias: alias,
|
|
213
|
-
id: f.gid,
|
|
214
|
-
name: f.name,
|
|
215
|
-
type: mapResourceType(f.resource_subtype),
|
|
216
|
-
alias: aliasOf(f.name),
|
|
217
|
-
};
|
|
218
|
-
if (Array.isArray(f.enum_options) && f.enum_options.length) {
|
|
219
|
-
entry.options = f.enum_options.map((opt) => ({
|
|
220
|
-
id: opt.gid,
|
|
221
|
-
name: opt.name,
|
|
222
|
-
alias: aliasOf(opt.name),
|
|
223
|
-
}));
|
|
224
|
-
}
|
|
225
|
-
out.customFields.push(entry);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
if (pickedFields.length) {
|
|
229
|
-
const inheritPicked = await multiSelect(
|
|
230
|
-
asanaStrings.inheritPrompt,
|
|
231
|
-
pickedFields.map((f) => ({ label: f.name, value: f })),
|
|
232
|
-
{ strings: coreStrings },
|
|
233
|
-
);
|
|
234
|
-
for (const f of inheritPicked) inheritFieldIds.add(f.gid);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
try {
|
|
239
|
-
const memberships = await api.getMembers(p.gid);
|
|
240
|
-
const users = memberships.map((m) => m.user).filter(Boolean);
|
|
241
|
-
for (const u of users) {
|
|
242
|
-
if (u.gid === me.gid) continue;
|
|
243
|
-
if (out.members.find((m) => m.id === u.gid)) continue;
|
|
244
|
-
out.members.push({ id: u.gid, name: u.name, alias: aliasOf(u.name) });
|
|
245
|
-
}
|
|
246
|
-
} catch (e) {
|
|
247
|
-
// Project membership listing may require additional scopes; skip silently.
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
if (out.projects.length === 1) {
|
|
252
|
-
out.defaults.projectAlias = out.projects[0].alias;
|
|
253
|
-
out.defaults.assigneeAlias = "me";
|
|
254
|
-
const sectionChoices = out.sections.map((s) => ({
|
|
255
|
-
label: `${s.name} (${s.alias})`,
|
|
256
|
-
value: s,
|
|
257
|
-
}));
|
|
258
|
-
if (sectionChoices.length) {
|
|
259
|
-
const open = await promptPick(asanaStrings.openSectionPrompt, sectionChoices, {
|
|
260
|
-
defaultIndex: 0,
|
|
261
|
-
allowSkip: true,
|
|
262
|
-
strings: coreStrings,
|
|
263
|
-
});
|
|
264
|
-
if (open) out.defaults.sectionAlias = open.alias;
|
|
265
|
-
const close = await promptPick(asanaStrings.closeSectionPrompt, sectionChoices, {
|
|
266
|
-
defaultIndex: sectionChoices.length - 1,
|
|
267
|
-
allowSkip: true,
|
|
268
|
-
strings: coreStrings,
|
|
269
|
-
});
|
|
270
|
-
if (close) out.defaults.closeSectionAlias = close.alias;
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
await collectEscalationMember(out, { coreStrings, asanaStrings });
|
|
275
|
-
|
|
276
|
-
if (inheritFieldIds.size) {
|
|
277
|
-
out.subtaskDefaults = {
|
|
278
|
-
inheritParentFields: [...inheritFieldIds],
|
|
279
|
-
inheritAssignee: true,
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
const wantAuto = await promptYesNo(coreStrings.autonomousPromptQuestion, {
|
|
284
|
-
strings: coreStrings,
|
|
285
|
-
});
|
|
286
|
-
if (wantAuto) {
|
|
287
|
-
out.autonomous = {
|
|
288
|
-
enabled: false,
|
|
289
|
-
allow: ["task.create", "checklist.check", "task.close", "task.comment.add"],
|
|
290
|
-
scope: {
|
|
291
|
-
projects: out.projects.map((p) => p.id),
|
|
292
|
-
sections: out.sections.map((s) => s.id),
|
|
293
|
-
},
|
|
294
|
-
rateLimit: { writesPerMinute: 30, commentsPerMinute: 10 },
|
|
295
|
-
auditLog: "~/.local/share/llodev/pm-tasks/asana/audit.log",
|
|
296
|
-
};
|
|
297
|
-
printInstructions([coreStrings.autonomousAddedTitle, coreStrings.autonomousReviewBody]);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
const schema = await loadSchema();
|
|
301
|
-
const valid = await validateConfig(out, schema);
|
|
302
|
-
if (!valid.ok) {
|
|
303
|
-
console.error(`${coreStrings.errInvalidConfig}:`, JSON.stringify(valid.errors, null, 2));
|
|
304
|
-
process.exit(1);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
await writeConfig(outPath, out);
|
|
308
|
-
printInstructions([
|
|
309
|
-
interpolate(coreStrings.configWritten, { path: outPath }),
|
|
310
|
-
asanaStrings.tryItTitle,
|
|
311
|
-
asanaStrings.tokenReminderTitle,
|
|
312
|
-
asanaStrings.tokenReminderBody,
|
|
313
|
-
]);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
run().catch((e) => {
|
|
317
|
-
console.error(e);
|
|
318
|
-
process.exit(1);
|
|
319
|
-
});
|