Scheduled sweep of incomplete Asana tasks assigned to you — triage into auto/ask/skip, dedupe against existing work, then call asana-to-harness to open GitLab issues that trigger an AI coding harness. Built for headless scheduled runs (claude -p); also runs interactively, with --dry-run support.
Scanned 9/5/2026
Install to Claude Code
npx -y skills add YCyrus/asana-sweep --skill asana-sweep --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Asana Sweep?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/ycyrus-asana-sweep)More formats (shields.io, HTML) on the badges page.
---
name: asana-sweep
description: Scheduled sweep of incomplete Asana tasks assigned to you — triage into auto/ask/skip, dedupe against existing work, then call asana-to-harness to open GitLab issues that trigger an AI coding harness. Built for headless scheduled runs (claude -p); also runs interactively, with --dry-run support.
---
# Asana sweep → triage → dedupe → hand off to asana-to-harness
Periodically sweep the incomplete Asana tasks assigned to you, decide which ones can safely become issues for an AI coding harness, and only report the rest — never act on ambiguity. The per-task work (write plan → open issue → trigger the harness) is delegated to the `asana-to-harness` skill; this skill only does **sweep + triage + dedupe + trigger**.
## Configuration (read this first, every run)
All behavior comes from `asana-sweep.config.json` in the repository root of the current working directory. **If the file is missing, stop immediately** and tell the user to copy `asana-sweep.config.example.json` from this plugin's repo and fill it in. Do not guess values.
Key fields (see the example file for the full shape):
- `workspace_gid` — the Asana workspace to sweep.
- `paid_workspace` — if `false`, the workspace is on Asana's free tier: **only `asana_get_my_tasks` (User Task List endpoint) works; `asana_search_tasks` returns `payment_required`.** Do not retry search endpoints on free workspaces.
- `window_days` — processing window (see Flow step 2-5).
- `gitlab.project` / `gitlab.base_branch` — where issues are opened.
- `asana_projects` — **optional** list of Asana projects (boards) to sweep, by gid or by name. Empty/absent = sweep everything assigned to you (see Flow step 1).
- `repo_scope` — what counts as "work this harness can actually do" (see triage gate 0). Always applied, whether or not `asana_projects` is set.
- `skip_rules` — task types that must never become issues.
- `state_file` / `summary_file` / `plans_dir` — where run state and outputs live. **These paths must NOT be under `.claude/`** — Claude Code write-protects that directory in headless runs; keep state inside a normal repo directory and gitignore it.
## Flow
### 0. Read state
- Read the file at `state_file` (treat a missing file as `{}`). Task gids present in it were already handled — skip them this run.
### 1. Fetch my incomplete tasks
- Call `asana_get_my_tasks` with:
- `workspace: <workspace_gid>`
- `completed_since: <today 00:00 ISO>` (Asana convention: returns incomplete tasks plus ones completed today — effectively "incomplete")
- `opt_fields: name,completed,due_on,notes,permalink_url,projects.gid,projects.name,parent.name,parent.notes,parent.permalink_url,parent.projects.gid,parent.projects.name`
- Filter out any stragglers with `completed: true`.
- **Board filter — only when `asana_projects` is non-empty.** Keep a task only if **the task itself OR its parent** belongs to one of the listed projects. Match an all-digits entry against `projects[].gid`; match anything else case-insensitively as a substring of `projects[].name`.
⚠️ **Always check `parent.projects` too.** Asana subtasks normally belong to *no* project — the board membership lives on the parent — so a filter that only looks at the task's own `projects` array discards everything (verified: an assigned "開發"-style subtask returns `projects: []` while its parent carries the board).
When `asana_projects` is empty or absent, keep every task and let triage gate 0 decide on content alone.
### 2. Three-way triage (apply per task, in this exact order)
0. **🚧 Hard gate: only work deliverable as code in the configured repo.** The harness can only open MRs in `gitlab.project` — it cannot change any other system. Skip (⚪, reason "outside repo scope") anything matching `repo_scope.other_systems` (other products, other clients' sites, external hosting, infrastructure). ⚠️ Judge by **"is the deliverable code in this repo?"**, not by surface keywords. If unsure → demote to 🟡, never 🟢.
1. **No `due_on` (null) → ⚪ skip**, reason "no due date". (Filters out placeholder buckets and vague long-term items.)
2. **Matches a `skip_rules` entry → ⚪ skip** (never becomes an issue). Typical rules: ops/admin work (renewals, credentials, permissions, environment setup) and pure research/investigation with no code deliverable.
3. **Placeholder title (e.g. "develop", "implement", "code fix") → resolve via the parent task**: fetch the parent (`asana_get_task` with `opt_fields=name,notes,parent.name,parent.notes,parent.permalink_url`) and use the parent's title + notes as the real spec (the subtask is just the "this one's yours" signal). ⚠️ Resolve the parent **first**, then re-apply gate 0 to the parent's content.
4. Classify:
- **🟢 AUTO**: passed gate 0, is implementation/bugfix work in the configured repo, and has an actionable spec (notes on the task or its parent, or a self-explanatory title).
- **🟡 ASK**: looks like implementation but the spec is thin, the scope is huge (e.g. a whole "refactor X" with no detail), or repo ownership is uncertain → **report only, never open an issue**.
- **⚪ SKIP**: gates 0–2 above.
- When in doubt, always demote to 🟡. Opening an issue is an external, hard-to-undo action.
- **Image attachments — read them before demoting**: PM specs often live in screenshots or annotated printouts attached to the task. This plugin ships a helper on PATH — run it for the task AND its resolved parent, then Read every path it prints (Claude reads images, including handwritten annotations):
```
asana-fetch-attachments <task_gid>
```
The helper reads the `ASANA_PAT` env var internally (set in the scheduler wrapper / shell profile — the plugin's secret store is not exposed to shell commands), so the command line contains no secrets and no variable expansion; allowlist it as `Bash(asana-fetch-attachments:*)`. ⚠️ Do NOT probe the environment first and do NOT hand-roll curl with `$ASANA_PAT` — env probes and `$VAR` expansions are denied by headless permission matching. If the helper exits non-zero saying the PAT is missing (or the command isn't found), fall back to the demotion rule below.
Treat image content as part of the spec; if it supplies the missing details, the task may stay 🟢 — but quote the image-derived values explicitly in what you hand to asana-to-harness so a human can verify them. Videos and other non-image files cannot be read — they still trigger the demotion below.
- **External-spec detection → demote to 🟡**: if a 🟢 candidate's real spec lives mainly in external links (Google Sheets/Docs, Figma) or in attachments you could not read (no `ASANA_PAT`, video files) **and the inline notes alone aren't enough to start work**, demote to 🟡 "spec lives in an external document — confirm or paste the key points into the task notes", and include the links in the report. If the inline text alone is sufficient and the links are merely supplementary, keep 🟢.
5. **Processing window (avoid Monday floods)**: tasks are often scheduled a week ahead; opening them all at once overloads a single session. A 🟢 only proceeds when `due_on <= today + window_days` (overdue tasks always proceed). Later 🟢 tasks are marked **⏳ "not in window yet (due X)" — report only**; a future sweep picks them up when they enter the window (the state file guarantees each opens at most once).
### 3. Dedupe (🟢 only; cheap read-only queries)
- Extract the task's main tracking number (from patterns like `#N`, `N -`, `[N]` in the title) plus **one set of distinctive keywords** (the most unique words in the resolved title — never broad domain words that would match unrelated old items).
- ⚠️ **Iron rule of number matching: GitLab issue iids and branch numbers auto-increment and are COMPLETELY UNRELATED to your PM/task-tracker numbers.**
- **Never** skip a task because "a branch name contains that number" or "issue iid == that number" — bare-number collisions are false positives.
- The only trustworthy places a PM number appears are **plan filenames** (under `plans_dir`) and **issue title text** (issues opened by this pipeline carry the full task title).
- Decision order:
1. **Task gid already in the state file** → skip (already created).
2. **An "opened" GitLab issue whose title contains both the number and a distinctive keyword** (`list_issues` on `gitlab.project`) → skip (already in progress).
3. **A plan file under `plans_dir` whose filename contains the number and whose topic matches** → skip (already planned).
4. **A git branch whose slug clearly matches the topic** (`git branch -a`) → skip. **A bare number match alone never counts.**
- **Only-closed-issue hits or loose/broad keyword hits → do NOT auto-skip; demote to 🟡 "possible duplicate, please confirm"** and include the matched issue/plan links in the report.
### 4. Trigger (each surviving 🟢)
- Invoke the `asana-to-harness` skill with: the resolved title + spec (parent notes), the task permalink, and any extracted number/base-branch reminders.
- On success, merge `{task_gid: {issue_url, ts}}` into the state file (read existing content, merge, rewrite the whole file).
### 5. Report (STRICT template — no freestyle)
Write the summary in the language of the tasks/config, following exactly this template (sections in this order; omit a section entirely when it has no items):
```
# Asana 自動巡檢摘要
巡檢時間:<YYYY-MM-DD>(排程自動巡檢,非 dry-run)
處理窗口:due_on ≤ <date>(今天+<window_days> 天,含逾期)
## 總計
- 掃描任務:X 張
- 本輪開 issue:Y 張 🟢
- 待你確認(🟡):Z 張
- 略過(⚪):W 張
- 未到處理窗口(⏳):V 張
- 已處理過(跳過):U 張
> <one-sentence headline of this run>
## 🟢 本次開出的 issue ← only when Y > 0
- <task title>(due <date>)
→ issue #N(link)|計畫已貼入描述、已觸發 harness。
做法:<one sentence>。去重:<one sentence>。
## 🟡 待你確認 ← only when Z > 0
- <task title>:<why it wasn't opened — what's missing>;在 notes 補「<exact sentence>」即可自動化。
## 明細
| 狀態 | 任務 | due | 說明 |
|---|---|---|---|
(one row per task; 狀態 = 🟢開issue / 🟢跳過 / 🟡 / ⚪略過 / ⏳未到窗口)
```
Nothing outside the template — any other observations go to stdout only, NOT the summary file. Keep the whole summary under ~1800 characters (webhooks truncate near 2000); if the 明細 table would push it over, merge同類 ⚪ rows into one row (e.g. `7 筆重構子項` style) rather than dropping 🟢/🟡 detail.
- **Always write the same summary to `summary_file`** (plain markdown, totals first, issue links included) — even when there is nothing new (it doubles as a heartbeat: no summary = something broke). External notification (Discord/Slack/email) is the job of whatever wrapper invokes this skill; the skill itself never calls webhooks.
## Dry-run (use this before going fully automatic)
- With `--dry-run`: **stop after step 3.** Do not invoke asana-to-harness, do not open issues, do not write state. Output only "what would have happened to which tasks". Verify triage + dedupe look right before unattended scheduling.
## Headless notes
- Permissions must be an **explicit allowlist** passed via `--allowedTools` (headless has no human to ask). Never use `--dangerously-skip-permissions`. See the plugin README for the recommended allowlist.
- Anything outside the allowlist: skip and report — never improvise.
## Guardrails
- Only 🟢 tasks that also pass dedupe ever become issues — issue creation is external and hard to undo.
- Never embellish requirements: if the spec is thin, demote to 🟡 and quote the Asana text verbatim.
- The Asana PAT is equivalent to a password; revoke it from the Asana developer console to kill access.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!