Documentation

fsds/linear-notification-system.md

Feature Specification: Linear Notification System

Overview

The Linear notification system enables bidirectional communication between humans and agents via Linear. Agents automatically see recent comments, feedback, and new assignments when they start a session, and can check for updates mid-session via a skill.

Status

Version: 1.0
Status: Phase 1 Complete
Components: Claude Code hooks, skills, state management
Linear Issue: SWE-369
ADR: ADR-060: Linear Notification System

Architecture

Phase 1: Polling (Current)

Session Start                              Mid-Session
─────────────                              ───────────

  session-start.sh                         /linear-notifications skill
       │                                        │
       ▼                                        ▼
  linear-notifications.sh              Linear MCP tools
       │                              (list_issues, list_comments)
       ▼                                        │
  Linear GraphQL API                            │
  (curl + OAuth token)                          │
       │                                        │
       ▼                                        ▼
  Formatted markdown                   Formatted markdown
  in session greeting                  shown to agent
       │                                        │
       └────────────┐          ┌────────────────┘
                    ▼          ▼
             .claude/state/linear-last-checked
              (shared persistent timestamp)

Phase 2: Webhooks (Future)

Linear (Cloud)          Cortex Server              Agent Session
──────────────          ──────────────             ──────────────

  Comment event ──────► /api/webhooks/linear       session-start.sh
                              │                         │
                              ▼                         ▼
                        Resolve agent            Read notification
                        by assignee/creator      files + poll API
                              │                         │
                              ▼                         ▼
                        Write JSON to            Formatted markdown
                        .claude/state/           in session greeting
                        linear-notifications/

Components

linear-notifications.sh (Hook Script)

Location: .claude/hooks/linear-notifications.sh
Invoked by: session-start.sh during SessionStart event

Standalone shell script that:

  1. Reads agent token from ~/.dplx/linear-tokens.json
  2. Reads last-checked timestamp from .claude/state/linear-last-checked
  3. Queries Linear GraphQL API for viewer identity
  4. Queries for recent comments on assigned/created issues (excludes agent's own)
  5. Queries for newly assigned issues since last check
  6. Formats results as compact markdown
  7. Updates the last-checked timestamp

Arguments: <worktree-path> <agent-name>
Output: Markdown string (empty on no activity or error)
Exit code: Always 0 (silent failure by design)

session-start.sh Integration

Location: .claude/hooks/session-start.sh
Changes: Calls linear-notifications.sh and includes output in the session greeting

The notification output appears after the collaborators section, giving agents immediate visibility into what they missed.

/linear-notifications Skill

Location: .claude/skills/linear-notifications/SKILL.md
Type: User-invocable

Instructs the agent to use Linear MCP tools (list_issues, list_comments) to check for activity since the last-checked timestamp. Uses the same state file as the hook script.

State File

Location: .claude/state/linear-last-checked
Format: ISO-8601 UTC timestamp (e.g., 2026-02-26T21:37:15Z)
Lifecycle: Created on first run, updated after each successful check. NOT cleaned up by agent-signoff.sh — persists across sessions.

Data Flow

GraphQL Queries

Viewer identification:

{ viewer { id name } }

Recent activity (comments + new assignments):

{
  recentComments: comments(
    first: 25, orderBy: createdAt
    filter: {
      createdAt: { gte: "$SINCE" }
      user: { id: { neq: "$VIEWER_ID" } }
      issue: { or: [
        { assignee: { id: { eq: "$VIEWER_ID" } } }
        { creator: { id: { eq: "$VIEWER_ID" } } }
      ]}
    }
  ) {
    nodes { body createdAt user { name } issue { identifier title } }
  }
  viewer {
    assignedIssues(
      first: 10
      filter: {
        createdAt: { gte: "$SINCE" }
        state: { type: { nin: ["completed", "canceled"] } }
      }
    ) {
      nodes { identifier title state { name } priority createdAt }
    }
  }
}

Output Format

## Linear Notifications

**3 new comment(s) on 2 issue(s) since Feb 25, 14:30 UTC**

### SWE-284: Complete Catalog shadow entity conversion
- [Feb 26, 16:22] **Daniel Castonguay**: I don't think we need to do anything else...
- [Feb 26, 16:29] **Bliss**: Agreed — the status gates/scopes approach is solid...

### SWE-331: Define container autoscaling policies
- [Feb 26, 19:18] **Daniel Castonguay**: We need more info about our situation before...

### Newly Assigned Issues
- **SWE-345**: Implement spatial zone hierarchy API (Normal priority)

Error Handling

The notification system is designed to be invisible on failure:

Scenario Behavior
Missing ~/.dplx/linear-tokens.json Silent exit, no output
Agent not found in token file Silent exit, no output
Linear API timeout (>10s) Silent exit, no output
Malformed API response Silent exit, no output
No new activity Updates timestamp, no output
First run (no state file) Defaults to 48-hour lookback window

The session-start.sh integration wraps the call in 2>/dev/null with an || LINEAR_NOTIF="" fallback, ensuring the script can never block or break session start.

Configuration

Setting Location Default
Agent tokens ~/.dplx/linear-tokens.json N/A (required)
Last-checked timestamp .claude/state/linear-last-checked 48 hours ago
API timeout linear-notifications.sh (CURL_TIMEOUT) 10 seconds
Max comments fetched GraphQL first parameter 25
Max new issues fetched GraphQL first parameter 10
Comment body truncation Script formatting logic 200 characters

Testing

Manual Script Testing

# Normal run (should show formatted output if there's activity)
.claude/hooks/linear-notifications.sh "$(pwd)" "AgentName"

# Verify silent exit on missing token
.claude/hooks/linear-notifications.sh "$(pwd)" "NonexistentAgent"

# Verify state file persistence
cat .claude/state/linear-last-checked

# Test with wider lookback window
echo "2026-02-01T00:00:00Z" > .claude/state/linear-last-checked
.claude/hooks/linear-notifications.sh "$(pwd)" "AgentName"

Session Integration Testing

  1. Start a new agent session via dplx session new
  2. Verify the notification section appears in the session greeting
  3. Leave a comment on an assigned issue from the Linear UI
  4. Start another session and verify the comment appears

Skill Testing

  1. Start an agent session
  2. Run /linear-notifications
  3. Verify it shows recent activity (or "no new activity")
  4. Verify .claude/state/linear-last-checked is updated

Future Work (Phase 2)

Phase 2 adds a webhook receiver in the Cortex server for real-time event capture:

  1. Webhook endpoint: POST /api/webhooks/linear in Cortex
  2. OAuth webhook toggle: Enable auto-webhook on the per-agent OAuth apps
  3. Event routing: Cortex resolves target agent(s) by issue assignee/creator
  4. File-based notifications: Write JSON files to {worktree}/.claude/state/linear-notifications/
  5. Session pickup: SessionStart hook reads and clears these files

This requires Cortex to be accessible from the internet. Until then, Phase 1 polling provides reliable coverage.