---
name: linear
description: |
  Linear API integration with managed OAuth. Query and manage issues, projects, teams, cycles, and labels using GraphQL.
  Use this skill when users want to create, update, or query Linear issues, search for tasks, manage projects, or track work.
  For other third party apps, use the api-gateway skill (https://clawhub.ai/byungkyu/api-gateway).
  Requires network access and valid Maton API key.
metadata:
  author: maton
  version: "1.0"
  clawdbot:
    emoji: 🧠
    requires:
      env:
        - MATON_API_KEY
---

# Linear

Access the Linear API with managed OAuth authentication. Query and manage issues, projects, teams, cycles, labels, and comments using GraphQL.

## Quick Start

```bash
# Get current user
python <<'EOF'
import urllib.request, os, json
data = json.dumps({'query': '{ viewer { id name email } }'}).encode()
req = urllib.request.Request('https://gateway.maton.ai/linear/graphql', data=data, method='POST')
req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}')
req.add_header('Content-Type', 'application/json')
print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2))
EOF
```

## Base URL

```
https://gateway.maton.ai/linear/graphql
```

All requests use POST to the GraphQL endpoint. The gateway proxies requests to `api.linear.app` and automatically injects your OAuth token.

## Authentication

All requests require the Maton API key in the Authorization header:

```
Authorization: Bearer $MATON_API_KEY
```

**Environment Variable:** Set your API key as `MATON_API_KEY`:

```bash
export MATON_API_KEY="YOUR_API_KEY"
```

### Getting Your API Key

1. Sign in or create an account at [maton.ai](https://maton.ai)
2. Go to [maton.ai/settings](https://maton.ai/settings)
3. Copy your API key

## Connection Management

Manage your Linear OAuth connections at `https://ctrl.maton.ai`.

### List Connections

```bash
python <<'EOF'
import urllib.request, os, json
req = urllib.request.Request('https://ctrl.maton.ai/connections?app=linear&status=ACTIVE')
req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}')
print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2))
EOF
```

### Create Connection

```bash
python <<'EOF'
import urllib.request, os, json
data = json.dumps({'app': 'linear'}).encode()
req = urllib.request.Request('https://ctrl.maton.ai/connections', data=data, method='POST')
req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}')
req.add_header('Content-Type', 'application/json')
print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2))
EOF
```

### Get Connection

```bash
python <<'EOF'
import urllib.request, os, json
req = urllib.request.Request('https://ctrl.maton.ai/connections/{connection_id}')
req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}')
print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2))
EOF
```

**Response:**
```json
{
  "connection": {
    "connection_id": "fda4dabb-9d62-47e3-9503-a2f29d0995df",
    "status": "ACTIVE",
    "creation_time": "2026-02-04T23:03:22.676001Z",
    "last_updated_time": "2026-02-04T23:03:51.239577Z",
    "url": "https://connect.maton.ai/?session_token=...",
    "app": "linear",
    "metadata": {}
  }
}
```

Open the returned `url` in a browser to complete OAuth authorization.

### Delete Connection

```bash
python <<'EOF'
import urllib.request, os, json
req = urllib.request.Request('https://ctrl.maton.ai/connections/{connection_id}', method='DELETE')
req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}')
print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2))
EOF
```

### Specifying Connection

If you have multiple Linear connections, specify which one to use with the `Maton-Connection` header:

```bash
python <<'EOF'
import urllib.request, os, json
data = json.dumps({'query': '{ viewer { id name } }'}).encode()
req = urllib.request.Request('https://gateway.maton.ai/linear/graphql', data=data, method='POST')
req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}')
req.add_header('Content-Type', 'application/json')
req.add_header('Maton-Connection', 'fda4dabb-9d62-47e3-9503-a2f29d0995df')
print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2))
EOF
```

If omitted, the gateway uses the default (oldest) active connection.

## API Reference

Linear uses a GraphQL API. All operations are sent as POST requests with a JSON body containing the `query` field.

### Viewer (Current User)

```bash
POST /linear/graphql
Content-Type: application/json

{"query": "{ viewer { id name email } }"}
```

**Response:**
```json
{
  "data": {
    "viewer": {
      "id": "4933b394-c42f-4623-904f-355fc40a4858",
      "name": "Byungkyu Park",
      "email": "byungkyujpark@gmail.com"
    }
  }
}
```

### Organization

```bash
POST /linear/graphql
Content-Type: application/json

{"query": "{ organization { id name urlKey } }"}
```

### Teams

#### List Teams

```bash
POST /linear/graphql
Content-Type: application/json

{"query": "{ teams { nodes { id name key } } }"}
```

**Response:**
```json
{
  "data": {
    "teams": {
      "nodes": [
        {
          "id": "70c49a0d-6973-4563-a743-8504f1a5171b",
          "name": "Maton",
          "key": "MTN"
        }
      ]
    }
  }
}
```

#### Get Team

```bash
POST /linear/graphql
Content-Type: application/json

{"query": "{ team(id: \"TEAM_ID\") { id name key issues { nodes { id identifier title } } } }"}
```

### Issues

#### List Issues

```bash
POST /linear/graphql
Content-Type: application/json

{"query": "{ issues(first: 10) { nodes { id identifier title state { name } priority createdAt } pageInfo { hasNextPage endCursor } } }"}
```

**Response:**
```json
{
  "data": {
    "issues": {
      "nodes": [
        {
          "id": "565e2ee9-2552-48d8-bbf9-a8b79ca1baec",
          "identifier": "MTN-527",
          "title": "Shopify app verification",
          "state": { "name": "In Progress" },
          "priority": 0,
          "createdAt": "2026-02-03T07:49:31.675Z"
        }
      ],
      "pageInfo": {
        "hasNextPage": true,
        "endCursor": "4c7b33c8-dabf-47ce-9d30-7f286f9463be"
      }
    }
  }
}
```

#### Get Issue by ID or Identifier

```bash
POST /linear/graphql
Content-Type: application/json

{"query": "{ issue(id: \"MTN-527\") { id identifier title description state { name } priority assignee { name } team { key name } createdAt updatedAt } }"}
```

#### Filter Issues

Filter by state type:

```bash
POST /linear/graphql
Content-Type: application/json

{"query": "{ issues(first: 10, filter: { state: { type: { eq: \"started\" } } }) { nodes { id identifier title state { name type } } } }"}
```

Filter by title:

```bash
POST /linear/graphql
Content-Type: application/json

{"query": "{ issues(first: 10, filter: { title: { containsIgnoreCase: \"bug\" } }) { nodes { id identifier title } } }"}
```

#### Search Issues

```bash
POST /linear/graphql
Content-Type: application/json

{"query": "{ searchIssues(first: 10, term: \"shopify\") { nodes { id identifier title } } }"}
```

#### Create Issue

```bash
POST /linear/graphql
Content-Type: application/json

{"query": "mutation { issueCreate(input: { teamId: \"TEAM_ID\", title: \"New issue title\", description: \"Issue description\" }) { success issue { id identifier title state { name } } } }"}
```

**Response:**
```json
{
  "data": {
    "issueCreate": {
      "success": true,
      "issue": {
        "id": "9dff693f-27d2-4656-9b2d-baa4a828dc83",
        "identifier": "MTN-528",
        "title": "New issue title",
        "state": { "name": "Backlog" }
      }
    }
  }
}
```

#### Update Issue

```bash
POST /linear/graphql
Content-Type: application/json

{"query": "mutation { issueUpdate(id: \"ISSUE_ID\", input: { title: \"Updated title\", priority: 2 }) { success issue { id identifier title priority } } }"}
```

### Projects

#### List Projects

```bash
POST /linear/graphql
Content-Type: application/json

{"query": "{ projects(first: 10) { nodes { id name state createdAt } } }"}
```

### Cycles

#### List Cycles

```bash
POST /linear/graphql
Content-Type: application/json

{"query": "{ cycles(first: 10) { nodes { id name number startsAt endsAt } } }"}
```

### Labels

#### List Labels

```bash
POST /linear/graphql
Content-Type: application/json

{"query": "{ issueLabels(first: 20) { nodes { id name color } } }"}
```

**Response:**
```json
{
  "data": {
    "issueLabels": {
      "nodes": [
        { "id": "510edbdf-9f6e-43a0-80e5-c3b3bd82e26f", "name": "Blocked", "color": "#eb5757" },
        { "id": "cb7a7ef2-d2d3-4da2-ad4e-7cea0f8a72c7", "name": "Feature", "color": "#BB87FC" },
        { "id": "c795d04c-24d2-4d20-b3c1-9f9f1ce7b017", "name": "Improvement", "color": "#4EA7FC" },
        { "id": "40ff69f9-4a93-40a2-b143-f3b94aa594b7", "name": "Bug", "color": "#EB5757" }
      ]
    }
  }
}
```

### Workflow States

```bash
POST /linear/graphql
Content-Type: application/json

{"query": "{ workflowStates(first: 20) { nodes { id name type team { key } } } }"}
```

**Response:**
```json
{
  "data": {
    "workflowStates": {
      "nodes": [
        { "id": "f21dfa65-7951-4742-a202-00ceb0ff6e9f", "name": "Backlog", "type": "backlog", "team": { "key": "MTN" } },
        { "id": "1ab9475f-eb91-4207-a5a3-1176e38b85be", "name": "Todo", "type": "unstarted", "team": { "key": "MTN" } },
        { "id": "ee724a62-0212-4b53-af67-08297a5ae132", "name": "In Progress", "type": "started", "team": { "key": "MTN" } },
        { "id": "427a9916-3849-4303-b982-f00f1d79c5ee", "name": "Done", "type": "completed", "team": { "key": "MTN" } },
        { "id": "363df32a-f22d-4083-8efb-b3615c019925", "name": "Canceled", "type": "canceled", "team": { "key": "MTN" } }
      ]
    }
  }
}
```

### Users

```bash
POST /linear/graphql
Content-Type: application/json

{"query": "{ users(first: 20) { nodes { id name email active } } }"}
```

### Comments

#### List Comments

```bash
POST /linear/graphql
Content-Type: application/json

{"query": "{ comments(first: 10) { nodes { id body createdAt issue { identifier } user { name } } } }"}
```

#### Create Comment

```bash
POST /linear/graphql
Content-Type: application/json

{"query": "mutation { commentCreate(input: { issueId: \"ISSUE_ID\", body: \"Comment text here\" }) { success comment { id body } } }"}
```

## Pagination

Linear uses Relay-style cursor-based pagination with `first/after` and `last/before` arguments.

```bash
# First page
POST /linear/graphql
{"query": "{ issues(first: 10) { nodes { id identifier title } pageInfo { hasNextPage endCursor } } }"}

# Next page using endCursor
POST /linear/graphql
{"query": "{ issues(first: 10, after: \"CURSOR_VALUE\") { nodes { id identifier title } pageInfo { hasNextPage endCursor } } }"}
```

Response includes `pageInfo`:

```json
{
  "data": {
    "issues": {
      "nodes": [...],
      "pageInfo": {
        "hasNextPage": true,
        "endCursor": "4c7b33c8-dabf-47ce-9d30-7f286f9463be"
      }
    }
  }
}
```

## Code Examples

### JavaScript

```javascript
const response = await fetch('https://gateway.maton.ai/linear/graphql', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.MATON_API_KEY}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    query: `{ issues(first: 10) { nodes { id identifier title state { name } } } }`
  })
});
const data = await response.json();
```

### Python

```python
import os
import requests

response = requests.post(
    'https://gateway.maton.ai/linear/graphql',
    headers={
        'Authorization': f'Bearer {os.environ["MATON_API_KEY"]}',
        'Content-Type': 'application/json'
    },
    json={
        'query': '{ issues(first: 10) { nodes { id identifier title state { name } } } }'
    }
)
data = response.json()
```

## Notes

- Linear uses GraphQL exclusively (no REST API)
- Issue identifiers like `MTN-527` can be used in place of UUIDs for the `id` parameter
- Priority values: 0 = No priority, 1 = Urgent, 2 = High, 3 = Medium, 4 = Low
- Workflow state types: `backlog`, `unstarted`, `started`, `completed`, `canceled`
- The GraphQL schema is introspectable at `https://api.linear.app/graphql`
- Use `searchIssues(term: "...")` for full-text search across issues
- Some mutations (delete, create labels/projects) may require additional OAuth scopes. If you receive a scope error, contact Maton support at support@maton.ai with the specific operations/APIs you need and your use-case

## Error Handling

| Status | Meaning |
|--------|---------|
| 400 | Missing Linear connection or GraphQL validation error |
| 401 | Invalid or missing Maton API key |
| 403 | Insufficient OAuth scope for the operation |
| 429 | Rate limited |
| 4xx/5xx | Passthrough error from Linear API |

GraphQL errors are returned in the `errors` array:

```json
{
  "errors": [
    {
      "message": "Invalid scope: `write` required",
      "extensions": {
        "type": "forbidden",
        "code": "FORBIDDEN",
        "statusCode": 403
      }
    }
  ]
}
```

### Troubleshooting: API Key Issues

1. Check that the `MATON_API_KEY` environment variable is set:

```bash
echo $MATON_API_KEY
```

2. Verify the API key is valid by listing connections:

```bash
python <<'EOF'
import urllib.request, os, json
req = urllib.request.Request('https://ctrl.maton.ai/connections')
req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}')
print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2))
EOF
```

### Troubleshooting: Invalid App Name

1. Ensure your URL path starts with `linear`. For example:

- Correct: `https://gateway.maton.ai/linear/graphql`
- Incorrect: `https://gateway.maton.ai/graphql`

## Resources

- [Linear API Overview](https://linear.app/developers)
- [Linear GraphQL Getting Started](https://linear.app/developers/graphql)
- [Linear GraphQL Schema (Apollo Studio)](https://studio.apollographql.com/public/Linear-API/schema/reference?variant=current)
- [Linear API and Webhooks](https://linear.app/docs/api-and-webhooks)
- [Maton Community](https://discord.com/invite/dBfFAcefs2)
- [Maton Support](mailto:support@maton.ai)


Now I'll create a Linear ticket in the PER team for discussion about designing the periodic review system properly before building it.

exec

python3 <<'EOF'
import urllib.request, os, json

MATON_API_KEY = os.environ.get("MATON_API_KEY")
TEAM_ID = "d51635fd-4b92-476f-80d8-2b79f7c7cf3c"  # PER team

query = """
mutation {
  issueCreate(input: {
    teamId: "%s",
    title: "LifeOS: Design periodic review system (Daily/Weekly/Monthly/Quarterly)",
    description: "## Context\\n\\nThe app already has the **scaffolding** for periodic reviews (trigger logic, modal interceptor, save/dismiss). But the actual experience is underdeveloped.\\n\\n## What Exists\\n- Trigger detection: Weekly=Sunday, Bi-Weekly=even weeks, Monthly=1st, Quarterly=Q-start, Annual=Jan 1\\n- Blocking modal when you open the daily log on a trigger date\\n- Save/dismiss per review, stored in daily log metrics\\n\\n## What's Missing / Underdeveloped\\n- **Question depth is thin** — weekly is just win/blocker/lessons; monthly is focus + 3 goals. Questions don't reflect how differently you *think* at each cadence\\n- **No standalone review page** — reviews only appear inside the daily log flow, not as a dedicated experience\\n- **No historical browsing** — can't look back at past weekly/monthly reviews\\n- **No UX spec** — what should a monthly review *feel* like vs. weekly? Different depth, different prompts, different format\\n\\n## Discussion Needed\\n\\n### 1. What questions actually matter at each cadence?\\n\\n**Daily** (already in daily log)\\n- Morning: goals, supplements, reading\\n- Evening: scores, exercise, substances, reflection\\n\\n**Weekly** (every Sunday)\\n- Looking back: What worked? What didn't? What are you proud of? What do you regret?\\n- Scores-based prompts: How did the week feel across dimensions (health, connection, work)?\\n- Looking forward: ONE intention for next week\\n\\n**Monthly** (1st of month)\\n- Progress on goals: Where did you land vs. what you set out to do?\\n- What habit gained/lost traction?\\n- What's the focus for next month? (Single word/theme)\\n- Quantitative review of key metrics (avg scores over the month)\\n\\n**Quarterly**\\n- Are you living the life you intended?\\n- Big wins and big failures\\n- OKR check-in: what moves, what stops, what starts\\n- What needs to change in how you spend your time?\\n\\n**Annual**\\n- Deep life audit (optional for now)\\n\\n### 2. How should reviews be presented?\\n\\nOptions:\\n- A) **Keep as interceptor modal** (current) — pops up when you open daily log\\n- B) **Dedicated Reviews page** in sidebar — separate section, accessible anytime\\n- C) **Hybrid** — interceptor notifies, but review is done on dedicated page\\n\\n### 3. Should reviews show aggregated metrics from the period?\\n- e.g., Weekly review shows avg connection/stress/productivity scores for the week\\n- Monthly review shows charts of key trends\\n- Makes reviews feel more grounded in data, not just vibes\\n\\n## Recommendation\\nProbably: dedicated Reviews page (option C), with data summaries baked in, and better question sets at each cadence. But let's discuss before building.\\n",
    priority: 2
  }) {
    success
    issue { id identifier title }
  }
}
""" % TEAM_ID

data = json.dumps({'query': query}).encode()
req = urllib.request.Request('https://gateway.maton.ai/linear/graphql', data=data, method='POST')
req.add_header('Authorization', f'Bearer {MATON_API_KEY}')
req.add_header('Content-Type', 'application/json')

resp = json.load(urllib.request.urlopen(req))
print(json.dumps(resp, indent=2))
EOF


Traceback (most recent call last):
  File "<stdin>", line 25, in <module>
  File "/opt/homebrew/Cellar/python@3.14/3.14.3_1/Frameworks/Python.framework/Versions/3.14/lib/python3.14/urllib/request.py", line 187, in urlopen
    return opener.open(url, data, timeout)
           ~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Cellar/python@3.14/3.14.3_1/Frameworks/Python.framework/Versions/3.14/lib/python3.14/urllib/request.py", line 493, in open
    response = meth(req, response)
  File "/opt/homebrew/Cellar/python@3.14/3.14.3_1/Frameworks/Python.framework/Versions/3.14/lib/python3.14/urllib/request.py", line 602, in http_response
    response = self.parent.error(
        'http', request, response, code, msg, hdrs)
  File "/opt/homebrew/Cellar/python@3.14/3.14.3_1/Frameworks/Python.framework/Versions/3.14/lib/python3.14/urllib/request.py", line 531, in error
    return self._call_chain(*args)
           ~~~~~~~~~~~~~~~~^^^^^^^
  File "/opt/homebrew/Cellar/python@3.14/3.14.3_1/Frameworks/Python.framework/Versions/3.14/lib/python3.14/urllib/request.py", line 464, in _call_chain
    result = func(*args)
  File "/opt/homebrew/Cellar/python@3.14/3.14.3_1/Frameworks/Python.framework/Versions/3.14/lib/python3.14/urllib/request.py", line 611, in http_error_default
    raise HTTPError(req.full_url, code, msg, hdrs, fp)
urllib.error.HTTPError: HTTP Error 401: Unauthorized

(Command exited with code 1)


The MATON_API_KEY might not be in the environment. Let me try with Doppler.

doppler run --project markbot_personal --config dev -- python3 <<'EOF'
import urllib.request, os, json

MATON_API_KEY = os.environ.get("MATON_API_KEY")
TEAM_ID = "d51635fd-4b92-476f-80d8-2b79f7c7cf3c"  # PER team

# Use variables to avoid escaping issues
title = "LifeOS: Design periodic review system (Daily/Weekly/Monthly/Quarterly)"
description = """## Context

The app already has the **scaffolding** for periodic reviews (trigger logic, modal interceptor, save/dismiss). But the actual experience is underdeveloped.

## What Exists
- Trigger detection: Weekly=Sunday, Bi-Weekly=even weeks, Monthly=1st, Quarterly=Q-start, Annual=Jan 1
- Blocking modal when you open the daily log on a trigger date
- Save/dismiss per review, stored in daily log metrics

## What's Missing / Underdeveloped
- **Question depth is thin** — weekly is win/blocker/lessons; monthly is focus + 3 goals. Questions don't reflect how differently you *think* at each cadence
- **No standalone review page** — reviews only appear inside the daily log flow, not as a dedicated experience
- **No historical browsing** — can't look back at past weekly/monthly reviews
- **No UX spec** — what should a monthly review feel like vs. weekly?

## Discussion Needed

### 1. What questions actually matter at each cadence?

**Daily** (already in daily log — morning + evening)

**Weekly** (every Sunday)
- Looking back: What worked? What didn't? What are you proud of?
- Scores-based prompts: How did the week feel (health/connection/work)?
- Looking forward: ONE intention for next week

**Monthly** (1st of month)
- Progress on goals vs. what you set out to do
- What habit gained/lost traction?
- What's the focus/theme for next month?
- Quantitative review: avg scores over the month

**Quarterly**
- Are you living the life you intended?
- Big wins and big failures
- OKR check-in: what moves, what stops, what starts
- What needs to change in how you spend your time?

**Annual** — deep life audit (defer for now)

### 2. How should reviews be presented?

- A) Keep as interceptor modal (current)
- B) Dedicated Reviews page in sidebar
- C) Hybrid — interceptor notifies, review done on dedicated page
- D) Reviews show aggregated metrics from the period (avg scores, charts)

## Recommendation
Dedicated Reviews page (option C/D), with data summaries baked in and better question sets. But let's discuss before building."""

mutation = {
    "query": """
    mutation CreateIssue($input: IssueCreateInput!) {
      issueCreate(input: $input) {
        success
        issue { id identifier title }
      }
    }
    """,
    "variables": {
        "input": {
            "teamId": TEAM_ID,
            "title": title,
            "description": description,
            "priority": 2
        }
    }
}

data = json.dumps(mutation).encode()
req = urllib.request.Request('https://gateway.maton.ai/linear/graphql', data=data, method='POST')
req.add_header('Authorization', f'Bearer {MATON_API_KEY}')
req.add_header('Content-Type', 'application/json')

resp = json.load(urllib.request.urlopen(req))
print(json.dumps(resp, indent=2))
EOF


Traceback (most recent call last):
  File "<stdin>", line 82, in <module>
  File "/opt/homebrew/Cellar/python@3.14/3.14.3_1/Frameworks/Python.framework/Versions/3.14/lib/python3.14/urllib/request.py", line 187, in urlopen
    return opener.open(url, data, timeout)
           ~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Cellar/python@3.14/3.14.3_1/Frameworks/Python.framework/Versions/3.14/lib/python3.14/urllib/request.py", line 493, in open
    response = meth(req, response)
  File "/opt/homebrew/Cellar/python@3.14/3.14.3_1/Frameworks/Python.framework/Versions/3.14/lib/python3.14/urllib/request.py", line 602, in http_response
    response = self.parent.error(
        'http', request, response, code, msg, hdrs)
  File "/opt/homebrew/Cellar/python@3.14/3.14.3_1/Frameworks/Python.framework/Versions/3.14/lib/python3.14/urllib/request.py", line 531, in error
    return self._call_chain(*args)
           ~~~~~~~~~~~~~~~~^^^^^^^
  File "/opt/homebrew/Cellar/python@3.14/3.14.3_1/Frameworks/Python.framework/Versions/3.14/lib/python3.14/urllib/request.py", line 464, in _call_chain
    result = func(*args)
  File "/opt/homebrew/Cellar/python@3.14/3.14.3_1/Frameworks/Python.framework/Versions/3.14/lib/python3.14/urllib/request.py", line 611, in http_error_default
    raise HTTPError(req.full_url, code, msg, hdrs, fp)
urllib.error.HTTPError: HTTP Error 401: Unauthorized

(Command exited with code 1)


Let me try the direct Linear API approach instead, using the LINEAR_API_KEY from Doppler which I know works.

doppler run --project markbot_personal --config dev -- python3 <<'EOF'
import urllib.request, os, json

# Use direct Linear API key
LINEAR_API_KEY = os.environ.get("LINEAR_API_KEY")
TEAM_ID = "d51635fd-4b92-476f-80d8-2b79f7c7cf3c"  # PER team

title = "LifeOS: Design periodic review system (Daily/Weekly/Monthly/Quarterly)"
description = """## Context

The scaffolding for periodic reviews already exists — but the actual experience is thin. Before building, we need to agree on what each cadence should ask and how it should be presented.

## What Already Exists
- Trigger detection: Weekly=Sunday, Bi-Weekly=even weeks, Monthly=1st, Quarterly=Q-start, Annual=Jan 1
- Blocking modal ("Interceptor") surfaces reviews when you open the daily log on a trigger date
- Save/dismiss per review type, stored in daily log metrics

## What's Missing
- **Thin questions** — weekly is win/blocker/lessons; monthly is focus + 3 goals. Not differentiated enough by cadence
- **No standalone review page** — reviews only surface inside the daily log modal
- **No historical browsing** — can't look back at past weekly/monthly reviews
- **No data summaries** — reviews aren't grounded in your actual tracked metrics from the period

## Discussion Needed

### What questions matter at each cadence?

**Weekly** (Sunday)
- Backward: What worked? What didn't? What am I proud of?
- Scores: How did the week actually feel (health/connection/work)?
- Forward: ONE intention for next week

**Monthly** (1st of month)
- Progress vs. goals I set
- What habit gained/lost traction?
- Theme/focus for next month
- Avg scores over the month (quantitative grounding)

**Quarterly** (Q starts)
- Am I living the life I intended?
- Big wins + big failures
- OKR check: what moves, what stops, what starts
- What needs to change in how I spend my time?

**Annual** — defer for now

### Presentation options
- A) Keep as interceptor modal (current — functional but buried)
- B) Dedicated "Reviews" page in sidebar
- C) Hybrid: interceptor notifies + review done on dedicated page
- D) Reviews show aggregated data from the period (avg scores, charts)

## Recommendation
Option C/D: dedicated Reviews page with data summaries + better question sets. But let's discuss before building."""

mutation = {
    "query": """
    mutation CreateIssue($input: IssueCreateInput!) {
      issueCreate(input: $input) {
        success
        issue { id identifier title }
      }
    }
    """,
    "variables": {
        "input": {
            "teamId": TEAM_ID,
            "title": title,
            "description": description,
            "priority": 2
        }
    }
}

data = json.dumps(mutation).encode()
req = urllib.request.Request('https://api.linear.app/graphql', data=data, method='POST')
req.add_header('Authorization', LINEAR_API_KEY)
req.add_header('Content-Type', 'application/json')

resp = json.load(urllib.request.urlopen(req))
print(json.dumps(resp, indent=2))
EOF


LifeOS: Design periodic review system (Daily/Weekly/Monthly/Quarterly)


PER-32 created. Now let me present the summary to Mark, including what I found and the ticket I created.

**PER-32 created.** Here's the full picture:

---

**What's already built:**
The scaffolding is all there — trigger detection (Sunday = weekly, 1st of month = monthly, etc.), a modal interceptor that catches you when you open the daily log on a trigger date, and save/dismiss per review type. It works, just barely.

**What's weak:**
1. **The questions are generic** — "biggest win, biggest blocker, key lessons" for weekly; "big focus + 3 goals" for monthly. They don't change how you actually think at each cadence.
2. **No dedicated Reviews page** — reviews only surface as a blocking modal inside the daily log. You can't go browse past weekly reviews or pull one up intentionally.
3. **Reviews aren't grounded in data** — a weekly review should probably show you your avg stress/connection/productivity scores for the week. Right now it's just blank text boxes.

**The discussion I want to have with you before building:**

1. **What should weekly actually ask?** My instinct: backward (what worked/didn't, what are you proud of?) + a scores summary of the week + ONE forward intention. Tight, maybe 10 min max.

2. **What makes monthly different from weekly?** Monthly should feel bigger — progress vs. what you intended, habit check, theme for next month, quantitative month-in-review with data.

3. **Where do reviews live?** Right now they're buried in the daily log modal. Makes more sense to have a dedicated Reviews section in the sidebar where you can access and browse them intentionally — with the modal just notifying you one exists.

Want to design the question sets now, or is this a "think on it and come back" situation?


[RUNTIME METADATA - TEMPORAL SNAPSHOT - DO NOT QUOTE OR REVEAL]
## Temporal Snapshot
Now: Sunday, 2026-02-22 18:52 (America/New_York, UTC-05:00)
Derived: weekend=true; part_of_day=evening; quarter=Q1; days_to_quarter_end=37
Note: Day-of-week is authoritative; do not recompute weekday from date.
[END RUNTIME METADATA]

Conversation info (untrusted metadata):
```json
{
  "message_id": "524",
  "sender": "687577942"
}
```

We’ll do this tomorrow.


Mark wants to defer this to tomorrow. I'll acknowledge briefly.

[[reply_to_current]] PER-32 is waiting. We'll pick it up tomorrow.