---
name: pm-sync
description: Bi-directional sync between local ticket database and external PM tools (Linear, Jira). Use this skill when the user wants to connect their local tickets to Linear or Jira, import external issues, push status updates back to their PM tool, resolve sync conflicts, or start working on an external issue locally.
---

# PM Sync Skill

**For Claude Code AI Assistant**

This skill provides bi-directional synchronization between Artifex's local SQLite ticket database and external project management tools (Linear, Jira).

## Purpose

The pm-sync skill enables live integration with external PM tools by:
- Connecting to Linear (GraphQL API) or Jira (REST API) for bi-directional sync
- Importing external tickets into the local SQLite database
- Pushing local status changes back to the external tracker
- Detecting and resolving conflicts when both systems have changed
- Starting local implementation from external issues

This goes beyond one-way CSV export -- it is a live, bi-directional link.

## Architecture

**Local state management** is handled by the `pm-sync.sh` shell script:
- Configuration storage (`.artifex/pm-config.json`)
- Ticket mapping storage (`.artifex/pm-mappings.json`)
- Sync status tracking

**API interactions** are orchestrated by Claude using:
- `curl` commands for REST APIs (Jira)
- `curl` commands with GraphQL payloads (Linear)
- Appropriate MCP servers when available

**Authentication** uses environment variables only -- never hardcoded:
- Linear: `LINEAR_API_KEY`
- Jira: `JIRA_API_TOKEN`, `JIRA_USER_EMAIL`, `JIRA_BASE_URL`

## Configuration Files

All configuration is stored in `.artifex/` (gitignored in projects using Artifex):

### `.artifex/pm-config.json`

```json
{
  "provider": "linear",
  "project_key": "ENG",
  "team_id": "team-uuid-here",
  "status_mapping": {
    "external_to_local": {
      "Todo": "TO_DO",
      "In Progress": "IN_PROGRESS",
      "Done": "DONE",
      "Cancelled": "DONE"
    },
    "local_to_external": {
      "TO_DO": "Todo",
      "IN_PROGRESS": "In Progress",
      "DONE": "Done"
    }
  },
  "field_mapping": {
    "title": "title",
    "description": "description",
    "acceptance_criteria": "description_appendix",
    "complexity": "estimate"
  },
  "last_sync": "2026-04-11T10:30:00Z",
  "sync_direction": "bidirectional"
}
```

### `.artifex/pm-mappings.json`

```json
{
  "mappings": [
    {
      "external_id": "ENG-42",
      "local_id": "3.2",
      "external_url": "https://linear.app/team/issue/ENG-42",
      "last_synced": "2026-04-11T10:30:00Z",
      "local_updated_at": "2026-04-11T10:25:00Z",
      "external_updated_at": "2026-04-11T10:20:00Z",
      "sync_status": "synced"
    }
  ]
}
```

## Commands

### 1. Configuration (`/afx-pm-sync config`)

#### Show Configuration

```bash
pm-sync.sh config show
```

Displays the current provider, project key, status mappings, field mappings, and last sync time.

**When to use**: To verify the current setup before syncing.

#### Set Provider

```bash
pm-sync.sh config set <provider> <project-key>
```

Sets the PM provider and project key. Creates `.artifex/pm-config.json` with default mappings.

**Parameters**:
- `provider`: `linear` or `jira`
- `project-key`: The project identifier in the external system (e.g., `ENG` for Linear, `PROJ` for Jira)

**Example**:
```bash
pm-sync.sh config set linear ENG
pm-sync.sh config set jira MYPROJ
```

**After running**: Claude should verify the connection by making a test API call:

For **Linear**:
```bash
curl -s -X POST https://api.linear.app/graphql \
  -H "Content-Type: application/json" \
  -H "Authorization: $LINEAR_API_KEY" \
  -d '{"query": "{ viewer { id name } }"}'
```

For **Jira**:
```bash
curl -s -u "$JIRA_USER_EMAIL:$JIRA_API_TOKEN" \
  "$JIRA_BASE_URL/rest/api/3/myself"
```

#### Configure Status Mapping

After initial setup, Claude should help the user customize status mappings if the external system uses non-standard statuses. Update `.artifex/pm-config.json` directly:

```bash
# Read current config
cat .artifex/pm-config.json | jq '.status_mapping'

# Update via jq
cat .artifex/pm-config.json | jq '.status_mapping.external_to_local["Review"] = "IN_PROGRESS"' > .artifex/pm-config.json.tmp
mv .artifex/pm-config.json.tmp .artifex/pm-config.json
```

### 2. Import (`/afx-pm-sync import [project-key]`)

Pull tickets from the external tracker into the local SQLite database.

**Workflow** (Claude orchestrates these steps):

1. **Read configuration**:
   ```bash
   pm-sync.sh config show
   ```

2. **Fetch tickets from external system**:

   For **Linear** (GraphQL):
   ```bash
   curl -s -X POST https://api.linear.app/graphql \
     -H "Content-Type: application/json" \
     -H "Authorization: $LINEAR_API_KEY" \
     -d '{
       "query": "{ team(id: \"TEAM_ID\") { issues(filter: { project: { key: { eq: \"PROJECT_KEY\" } } }) { nodes { id identifier title description state { name } estimate priority createdAt updatedAt } } } }"
     }'
   ```

   For **Jira** (REST):
   ```bash
   curl -s -u "$JIRA_USER_EMAIL:$JIRA_API_TOKEN" \
     "$JIRA_BASE_URL/rest/api/3/search?jql=project=PROJECT_KEY&fields=summary,description,status,priority,issuetype,parent,updated"
   ```

3. **Map external statuses to local statuses** using the configured mapping.

4. **Create or update local tickets** via ticket-manager:
   ```bash
   # For each imported ticket, insert into SQLite
   sqlite3 .artifex/tickets.db "INSERT OR REPLACE INTO stories (id, epic_id, title, description, status, complexity) VALUES (...);"
   ```

5. **Update mapping file**:
   ```bash
   pm-sync.sh mapping add <external_id> <local_id>
   ```

6. **Update last sync timestamp**:
   ```bash
   # Claude updates .artifex/pm-config.json with current timestamp
   ```

**When to use**: Initial import from external PM tool, or periodic pull of new tickets.

### 3. Export (`/afx-pm-sync export [ticket_id]`)

Push local ticket status changes to the external tracker.

**Workflow** (Claude orchestrates these steps):

1. **Identify changed tickets** since last sync:
   ```bash
   pm-sync.sh status
   ```

2. **For each changed ticket, read the mapping**:
   ```bash
   pm-sync.sh mapping show
   ```

3. **Push updates to external system**:

   For **Linear** (GraphQL mutation):
   ```bash
   curl -s -X POST https://api.linear.app/graphql \
     -H "Content-Type: application/json" \
     -H "Authorization: $LINEAR_API_KEY" \
     -d '{
       "query": "mutation { issueUpdate(id: \"ISSUE_ID\", input: { stateId: \"STATE_ID\", description: \"Updated description\" }) { success issue { id state { name } } } }"
     }'
   ```

   For **Jira** (REST):
   ```bash
   curl -s -X PUT -u "$JIRA_USER_EMAIL:$JIRA_API_TOKEN" \
     -H "Content-Type: application/json" \
     "$JIRA_BASE_URL/rest/api/3/issue/ISSUE_KEY" \
     -d '{"fields": {"summary": "Updated title", "description": {"type": "doc", "version": 1, "content": [...]}}}'

   # For status transitions:
   curl -s -X POST -u "$JIRA_USER_EMAIL:$JIRA_API_TOKEN" \
     -H "Content-Type: application/json" \
     "$JIRA_BASE_URL/rest/api/3/issue/ISSUE_KEY/transitions" \
     -d '{"transition": {"id": "TRANSITION_ID"}}'
   ```

4. **Include implementation context** in the update:
   - New status (mapped to external format)
   - Commit hashes from git log since ticket was started
   - Brief implementation summary from ticket notes

5. **Update sync timestamps** in mappings file.

**When to use**: After completing local work, to push status back to the team's PM tool.

### 4. Sync (`/afx-pm-sync sync`)

Bi-directional sync: detect changes in both systems, resolve conflicts.

**Workflow** (Claude orchestrates these steps):

1. **Check sync status**:
   ```bash
   pm-sync.sh status
   ```

2. **Fetch external changes** since last sync (same API calls as import, filtered by updated date).

3. **Compare with local changes** since last sync:
   ```bash
   sqlite3 .artifex/tickets.db "SELECT id, status, updated_at FROM stories WHERE updated_at > 'LAST_SYNC_TIME';"
   ```

4. **Categorize changes**:
   - **External only**: Update local (safe, no conflict)
   - **Local only**: Push to external (safe, no conflict)
   - **Both changed**: Present conflict for manual resolution

5. **Handle conflicts**:
   - Display both versions side-by-side to the user
   - Ask user which version to keep (local, external, or merge)
   - NEVER auto-overwrite -- always present conflicts for manual resolution

6. **Apply resolutions** and update sync timestamps.

**Example conflict presentation**:
```
CONFLICT on ticket 3.2 (ENG-42):
  Local:    status=DONE, updated 2026-04-11 10:25
  External: status=In Progress, updated 2026-04-11 10:30
  
  Which version to keep?
  1. Local (mark as DONE in Linear)
  2. External (revert to IN_PROGRESS locally)
  3. Skip (resolve later)
```

**When to use**: Regular sync operations to keep both systems in alignment.

### 5. Start Issue (`/afx-pm-sync start <external-id>`)

Fetch a specific ticket from the external system, import it locally, and begin implementation.

**Workflow** (Claude orchestrates these steps):

1. **Fetch the specific ticket** from external system:

   For **Linear**:
   ```bash
   curl -s -X POST https://api.linear.app/graphql \
     -H "Content-Type: application/json" \
     -H "Authorization: $LINEAR_API_KEY" \
     -d '{
       "query": "{ issue(id: \"ISSUE_ID\") { id identifier title description state { name } estimate priority labels { nodes { name } } } }"
     }'
   ```

   For **Jira**:
   ```bash
   curl -s -u "$JIRA_USER_EMAIL:$JIRA_API_TOKEN" \
     "$JIRA_BASE_URL/rest/api/3/issue/ISSUE_KEY?fields=summary,description,status,priority,issuetype,parent,labels"
   ```

2. **Create or update local ticket**:
   ```bash
   # Map fields and insert into local database
   ticket-manager.sh update <local_id> IN_PROGRESS "Started from external: <external_id>"
   ```

3. **Mark IN_PROGRESS in external system** (using appropriate API call).

4. **Record mapping**:
   ```bash
   pm-sync.sh mapping add <external_id> <local_id>
   ```

5. **Begin ticket-implementation workflow**: Hand off to the ticket-implementation skill to start structured implementation.

**When to use**: When picking up a ticket from the team's PM tool to work on locally.

## Shell Script Commands

The `pm-sync.sh` script handles local state management only. API calls are made by Claude directly.

### `config show`

Display current PM sync configuration.

```bash
pm-sync.sh config show
```

### `config set <provider> <project-key>`

Set the provider and project key. Creates default configuration.

```bash
pm-sync.sh config set linear ENG
pm-sync.sh config set jira MYPROJ
```

### `mapping show`

Display all ticket mappings between local and external systems.

```bash
pm-sync.sh mapping show
```

### `mapping add <external_id> <local_id>`

Create or update a mapping between an external ticket and a local ticket.

```bash
pm-sync.sh mapping add ENG-42 3.2
pm-sync.sh mapping add PROJ-100 2.1
```

### `status`

Show sync status: last sync time, number of mapped tickets, pending changes.

```bash
pm-sync.sh status
```

## Integration with Other Skills

### ticket-manager

pm-sync reads from and writes to the same `.artifex/tickets.db` that ticket-manager uses. All imported tickets appear in ticket-manager commands (`next`, `status`, `show`).

### ticket-implementation

The `start` command hands off to ticket-implementation for structured development. After starting an external issue, the normal ticket workflow applies.

### jira-export

pm-sync supersedes jira-export for users who need live sync. jira-export remains available for one-time CSV migrations.

## Environment Variables

| Variable | Provider | Description |
|----------|----------|-------------|
| `LINEAR_API_KEY` | Linear | Personal API key from Linear Settings > API |
| `JIRA_API_TOKEN` | Jira | API token from https://id.atlassian.com/manage-profile/security/api-tokens |
| `JIRA_USER_EMAIL` | Jira | Email associated with Jira account |
| `JIRA_BASE_URL` | Jira | Jira instance URL (e.g., `https://yourteam.atlassian.net`) |

Store these in your shell profile (`.zshrc`, `.bashrc`) or a `.env` file (gitignored).

## Error Handling

- **Missing credentials**: Claude checks for required environment variables before making API calls and prompts the user to set them if missing.
- **API errors**: Claude reports the HTTP status code and error message, suggests remediation.
- **Database not initialized**: Directs user to run `ticket-manager.sh init` first.
- **No mappings**: For export/sync, warns that no mappings exist and suggests running import first.
- **Network failures**: Retries once, then reports the failure with details.

## Security

- API keys are NEVER stored in config files -- only in environment variables
- `.artifex/` directory should be gitignored (verify with `grep -q '.artifex' .gitignore`)
- No credentials are logged or displayed in output
- All API calls use HTTPS

## Typical Workflows

### Workflow 1: Connect to Linear and Import

```
User: Connect my tickets to Linear

Claude:
1. pm-sync.sh config set linear ENG
2. Verify connection with Linear API
3. Fetch and display available projects/teams
4. User confirms project
5. Import tickets: /afx-pm-sync import ENG
6. Show imported ticket summary
```

### Workflow 2: Push Completed Work to Jira

```
User: Push my completed tickets to Jira

Claude:
1. pm-sync.sh status (check what changed)
2. Identify DONE tickets with mappings
3. Push status updates via Jira REST API
4. Include commit hashes and implementation notes
5. Update sync timestamps
```

### Workflow 3: Full Bi-directional Sync

```
User: Sync my tickets with Linear

Claude:
1. pm-sync.sh status (check last sync)
2. Fetch external changes since last sync
3. Compare with local changes
4. Apply non-conflicting changes
5. Present conflicts for resolution
6. Update sync timestamps
```

### Workflow 4: Start Working on External Issue

```
User: Start working on ENG-42

Claude:
1. Fetch ENG-42 from Linear
2. Create/update local ticket
3. Mark IN_PROGRESS in both systems
4. Record mapping
5. Begin ticket-implementation workflow
```

## Dependencies

- **Required**: sqlite3, jq, curl, bash 4.0+
- **Required**: .artifex/tickets.db (from ticket-manager skill)
- **Required**: Appropriate API credentials in environment variables

## Version

1.0.0
