From project-management-plugin
Syncs tasks bidirectionally between Claude Code and PM platforms including GitHub Projects, Linear, Notion, Asana, Trello, ClickUp, Monday.com, Todoist, and local storage. Handles credentials via env vars and resolves sync conflicts with internal state as source of truth.
npx claudepluginhub markus41/claude --plugin project-planner-pluginThis skill uses the workspace's default tool permissions.
All PM platform credentials are read exclusively from environment variables injected by Claude Code's plugin userConfig system. The environment variable names follow the pattern `CLAUDE_PLUGIN_OPTION_{KEY}` where `{KEY}` is the uppercase version of the userConfig key defined in `plugin.json`. For example, the `github_token` userConfig entry is available at runtime as `CLAUDE_PLUGIN_OPTION_GITHU...
Manages ClickUp tasks via CLI: create/edit/view/search tasks, check sprints, add comments, link PRs, update statuses, manage Docs. Handles auth, git integration, fuzzy matching.
Manage GitHub Issues and Linear tasks: list, create, update, close, comment via ralph_task tool. Auto-detects platform from ID format.
Share bugs, ideas, or general feedback.
All PM platform credentials are read exclusively from environment variables injected by Claude Code's plugin userConfig system. The environment variable names follow the pattern CLAUDE_PLUGIN_OPTION_{KEY} where {KEY} is the uppercase version of the userConfig key defined in plugin.json. For example, the github_token userConfig entry is available at runtime as CLAUDE_PLUGIN_OPTION_GITHUB_TOKEN. Credentials are never hardcoded, never written to state files, never echoed in logs, and never included in research briefs or progress entries. If a credential is missing at sync time, the operation fails with a clear error naming the missing environment variable — it does not fall back to anonymous access or cached credentials.
All integrations follow the same conflict resolution strategy: the plugin's internal task state is treated as the source of truth for task content (title, description, completion criteria, status). The external platform is treated as the source of truth for metadata that originates there (assignees, labels added externally, comments, external IDs). When a task is updated both internally and externally between syncs, the internal content wins for the fields the plugin owns (title, description, status, priority) and the external platform's values are preserved for fields the plugin does not manage (assignees, external_labels, comments). This means bidirectional sync is limited — the plugin pushes status changes outward and pulls external IDs inward, but does not attempt full two-way content sync.
Auth method: Personal Access Token read from CLAUDE_PLUGIN_OPTION_GITHUB_TOKEN. The token requires project scope for GitHub Projects v2 API access and repo scope if the project is linked to a repository. Authentication is verified at connection time by calling the GraphQL viewer query; if it fails, the integration is aborted with a clear auth error.
Key API operations: GitHub Projects v2 uses a GraphQL API exclusively. Creating an issue: use the createIssue mutation on the linked repository, then add it to the project with addProjectV2ItemById. Updating status: use updateProjectV2ItemFieldValue with the Status field's ID. Listing tasks: use the projectV2 query with item iteration, filtering by the project node ID. The project node ID is stored in pm_integration.external_project_id after initial connection.
Field mapping: Internal title → GitHub issue title. Internal description → issue body. Internal status maps to a GitHub project Status field: PENDING → "Todo", IN_PROGRESS → "In Progress", COMPLETE → "Done", BLOCKED → "Blocked" (custom field value; created if not present). Internal priority maps to a GitHub project Priority field if one exists; otherwise it is embedded as a label (priority:critical, priority:high, etc.). The internal id is stored as a custom text field named "pm-plugin-id" on the project item.
Sync approach: Push-first. On each sync cycle, all tasks with status changes since last_sync_at are pushed to GitHub first. Then tasks with a null external_id are created as new issues. Finally, any items in the GitHub project that do not have a "pm-plugin-id" field value matching a known internal task ID are logged as "untracked external items" in the progress log (they are not imported or deleted).
Auth method: API Key read from CLAUDE_PLUGIN_OPTION_LINEAR_API_KEY. Linear uses this key in the Authorization header as a bearer token. Verified at connection by calling the viewer query on Linear's GraphQL API at https://api.linear.app/graphql.
Key API operations: Creating an issue: issueCreate mutation specifying teamId, title, description, and stateId. Updating status: issueUpdate mutation with the stateId corresponding to the target status. Listing tasks: issues query filtered by teamId and a label matching the project ID. The team ID and workflow state IDs are fetched once at connection time and cached in pm_integration.external_project_id as a JSON object (serialized).
Field mapping: Internal title → Linear issue title. Internal description → Linear issue description (markdown supported). Internal priority maps directly to Linear's priority integers: CRITICAL → 1 (Urgent), HIGH → 2 (High), MEDIUM → 3 (Medium), LOW → 4 (Low). Internal status maps to Linear workflow states: the mapping uses the state names "Todo," "In Progress," "Done," and "Cancelled" — if a team uses custom state names, the connection wizard prompts the user to map each internal status to the correct state. Internal estimate_minutes maps to Linear's cycle time estimate field (divided by 60, rounded to nearest 0.5).
Sync approach: Linear's webhook system is not available in the CLI context, so sync is polling-based. The sync cycle reads all issues in the team with the project label, compares their updatedAt timestamps against last_sync_at, and pulls status changes for issues whose external status differs from the internal status. Internal status is authoritative when both changed since last sync.
Auth method: Integration Token read from CLAUDE_PLUGIN_OPTION_NOTION_TOKEN. Notion uses this token in the Authorization: Bearer header. The integration must be connected to the target Notion workspace page before it will have access. Verified at connection by calling GET /v1/users/me.
Key API operations: Creating a page (task) in a database: POST /v1/pages with parent.database_id set to the Notion database ID and properties matching the database schema. Updating properties: PATCH /v1/pages/{page_id}. Querying the database: POST /v1/databases/{database_id}/query with filter and sort options. Notion's API is REST-based and paginated; the sync layer handles cursor-based pagination automatically.
Field mapping: Internal title → Notion "Name" (title) property. Internal status → Notion "Status" select property with options matching internal status names. Internal priority → Notion "Priority" select property. Internal description → Notion page body as a paragraph block (sent as a Blocks write after page creation). Internal estimate_minutes → Notion "Estimate" number property (in minutes). Internal tags → Notion "Tags" multi-select property. The Notion database must have these property names; the connection wizard validates their presence and creates missing ones using the PATCH /v1/databases/{database_id} endpoint.
Sync approach: Notion does not support webhooks in the API tier available to integrations, so sync is polling-based using the last_edited_time property. Pages modified since last_sync_at are fetched and their status changes are reconciled using the conflict resolution strategy above.
Auth method: Personal Access Token read from CLAUDE_PLUGIN_OPTION_ASANA_TOKEN. Sent in the Authorization: Bearer header. Verified at connection by calling GET /api/1.0/users/me. Asana also supports OAuth; the PAT is used here for simplicity in the CLI context.
Key API operations: Creating a task: POST /api/1.0/tasks with workspace, name, notes, projects (array), and custom_fields. Updating a task: PUT /api/1.0/tasks/{task_gid}. Listing tasks in a project: GET /api/1.0/projects/{project_gid}/tasks. Updating custom fields requires the custom field GID, fetched once at connection from GET /api/1.0/projects/{project_gid}/custom_field_settings.
Field mapping: Internal title → Asana task name. Internal description → Asana task notes (plain text; markdown is not rendered by Asana). Internal status maps to Asana task completion: COMPLETE → completed: true; all other statuses → completed: false. Finer-grained status (IN_PROGRESS, BLOCKED) is stored in a custom field named "Plugin Status." Internal priority maps to Asana's native priority field if available (Asana Premium), otherwise to a "Priority" custom field. Internal estimate_minutes maps to Asana's "Time estimate" field (if enabled) or a custom field.
Sync approach: Asana supports webhooks but requires a public HTTPS endpoint, which is not available in the CLI context. Sync is polling-based. The sync cycle uses the modified_since query parameter on the tasks endpoint to fetch only recently changed tasks.
Auth method: Trello API Key from CLAUDE_PLUGIN_OPTION_TRELLO_API_KEY and Token from CLAUDE_PLUGIN_OPTION_TRELLO_TOKEN. Both are sent as query parameters: key={api_key}&token={token}. Verified at connection by calling GET /1/members/me. The API key is obtained from https://trello.com/app-key; the token is generated by the user via the Trello authorization URL.
Key API operations: Creating a card: POST /1/cards with idList (the target list ID), name, and desc. Updating a card: PUT /1/cards/{card_id}. Moving a card to a different list (status change): PUT /1/cards/{card_id}/idList. Listing cards in a board: GET /1/boards/{board_id}/cards. Lists on the board represent workflow stages and are fetched at connection time.
Field mapping: Internal status maps to Trello lists by name: PENDING → list named "To Do," IN_PROGRESS → "In Progress," VALIDATING → "In Review," COMPLETE → "Done," BLOCKED → "Blocked." If these list names do not exist, the connection wizard creates them. Internal title → card name. Internal description → card description. Internal priority → a colored label: CRITICAL → red, HIGH → orange, MEDIUM → yellow, LOW → green. Internal tags → Trello labels with matching names (created on the board if not present).
Sync approach: Trello webhooks require a callback URL; polling is used instead. The sync cycle fetches all cards in the board and compares dateLastActivity against last_sync_at to identify changed cards.
Auth method: API Token read from CLAUDE_PLUGIN_OPTION_CLICKUP_TOKEN. Sent in the Authorization header (no "Bearer" prefix — ClickUp uses the token directly). Verified at connection by calling GET /api/v2/user.
Key API operations: Creating a task: POST /api/v2/list/{list_id}/task with name, description, status, and priority. Updating a task: PUT /api/v2/task/{task_id}. Listing tasks: GET /api/v2/list/{list_id}/task with filtering. ClickUp's task API supports rich metadata including custom fields, time estimates (in milliseconds), and dependencies.
Field mapping: Internal title → ClickUp task name. Internal description → ClickUp task description. Internal status → ClickUp status matching by name (PENDING → "Open," IN_PROGRESS → "In Progress," COMPLETE → "Complete," BLOCKED → "Blocked"). Internal priority → ClickUp priority integers: CRITICAL → 1 (Urgent), HIGH → 2 (High), MEDIUM → 3 (Normal), LOW → 4 (Low). Internal estimate_minutes → ClickUp time_estimate in milliseconds (multiply by 60,000).
Sync approach: ClickUp webhooks are available but require a public endpoint. Polling is used, with date_updated_gt filter set to last_sync_at millisecond timestamp.
Auth method: API Key read from CLAUDE_PLUGIN_OPTION_MONDAY_API_KEY. Monday uses GraphQL exclusively; the key is sent in the Authorization header. Verified at connection by calling the me query.
Key API operations: Creating an item: create_item mutation with board_id, item_name, and column_values (JSON object mapping column IDs to values). Updating column values: change_multiple_column_values mutation. Listing items: items_by_board query with pagination. Column IDs are board-specific and fetched at connection time via the boards query with columns sub-selection.
Field mapping: Internal title → Monday item name. Internal status → Monday "Status" column (type: status) with label mappings configured during connection. Internal priority → Monday "Priority" column (type: color). Internal description → Monday "Notes" column (type: long_text). Internal estimate_minutes → Monday "Numbers" column named "Estimate (min)."
Sync approach: Monday supports webhooks requiring a public endpoint; polling is used. Items updated since last_sync_at are detected using the updated_at column.
Auth method: API Token read from CLAUDE_PLUGIN_OPTION_TODOIST_TOKEN. Todoist uses both a REST API (v2) and a Sync API. The token is sent in the Authorization: Bearer header. Verified at connection by calling GET /rest/v2/projects.
Key API operations: Creating a task: POST /rest/v2/tasks with content, description, project_id, priority, and labels. Updating a task: POST /rest/v2/tasks/{task_id} (Todoist uses POST for updates). Completing a task: POST /rest/v2/tasks/{task_id}/close. Listing tasks: GET /rest/v2/tasks?project_id={id}. Todoist does not support custom statuses beyond open/closed — subtler status distinctions are encoded in labels.
Field mapping: Internal title → Todoist task content. Internal description → Todoist task description. Internal status maps to open/closed state: COMPLETE → closed task (via close endpoint); BLOCKED → open task with label blocked; IN_PROGRESS → open task with label in-progress; PENDING → open task with no status label. Internal priority maps to Todoist priority integers (inverted): CRITICAL → p1, HIGH → p2, MEDIUM → p3, LOW → p4. Internal tags → Todoist labels. Internal estimate_minutes is stored in the task description as [Estimate: Xm] since Todoist has no native estimate field on the free tier.
Sync approach: Todoist webhooks require a public endpoint; polling is used via the REST API. Tasks are re-fetched in full on each sync cycle (Todoist's API does not support filtering by modification time on the REST endpoint).
The "local" platform is selected when no external PM tool integration is desired. In local mode, all task state is maintained exclusively in the plugin's state files. The PM sync section of the dashboard shows "Local mode — no external sync." No credentials are required or accessed. This is the default mode for new projects that have not run /pm-connect.