Linear Auto-Processing Pipeline
Linear Auto-Processing Pipeline Automatically picks up Linear tasks labeled , , or , spawns persisted OpenCode Web chats, tracks their lifecycle, and cleans ...
Linear Auto-Processing Pipeline
Automatically picks up Linear tasks labeled
claude-fix,claude-research, orclaude-plan, spawns persisted OpenCode Web chats, tracks their lifecycle, and cleans up when done. The legacy label names remain for compatibility. All runs on the dev server only.
Why This Exists
Manual task pickup is slow — labeling a Linear task should be enough to kick off work. This pipeline turns Linear labels into running OpenCode chats, monitors their progress, and reclaims legacy resources when tasks complete or crash.
How It Works
Lifecycle Overview
1. User adds "claude-fix" label to a Linear task
|
2. Poller detects it (every 30s)
|
3. Starts a persisted OpenCode Web chat
- Passes prompt directly through the running OpenCode server
- Tracks in poller-sessions.json
- Swaps labels: claude-fix -> claude-is-working
- Sets status: In Progress
|
4. OpenCode works (reads prompt, investigates, implements)
- Posts progress comments to Linear via MCP
|
5. OpenCode finishes
- Sets status: In Review (or Todo if blocked)
- Removes claude-is-working label
|
6. Poller detects completion (next cycle, <=30s)
- Removes tracking record
- Removes any legacy Zellij session with the same name
Legacy records created before sidebar chat spawning may still point at Zellij sessions. Cleanup keeps reclaiming those records, but new work launches into the existing OpenCode Web project sidebar instead of creating terminal sessions.
Labels
| Label | Mode | OpenCode Permissions | What Happens |
|---|---|---|---|
claude-fix |
execute | Full read/write | Investigate + implement fix + deploy |
claude-research |
research | Read-only | Codebase + web research, post findings as comment |
claude-plan |
plan | Read-only | Investigate + propose approach, no implementation |
Priority order: fix > research > plan (if multiple labels exist).
Worker Limits
- New poller workers launch as persisted OpenCode Web chats instead of consuming Zellij slots.
- Legacy Zellij cleanup still enforces the historical session cap for older records and manual sessions.
- If a future chat-worker cap is added, queued tasks should keep the existing deduped “Queued” comment behavior.
- There is intentionally no blanket nightly enricher: open Todo/Backlog tasks only spawn OpenCode chats when explicitly labeled for automation.
Cleanup Layers
| Trigger | What | When |
|---|---|---|
| Poller (30s) | Detects tasks moved to “In Review”/“Done”, removes tracking, and kills any legacy Zellij session with the same name | Every poll cycle |
| session-cleanup.py (5 min) | Catches crashed sessions (EXITED/disappeared in Zellij), updates Linear, removes tracking | Every 5 min via systemd timer |
| enforce_session_limit() | Kills EXITED sessions first, then oldest idle sessions if over 6 | Called by both poller and cleanup |
Key Files
scripts/linear-poller.py
Main engine. Runs every 30s via systemd. Each cycle:
_cleanup_completed_sessions()— clean tracking for tasks whose work is doneenforce_session_limit()— free slots if over the cap- Collect candidates from
claude-fix,claude-research,claude-planlabels - For each candidate: spawn an OpenCode Web chat, track it, swap labels
scripts/_linear_client.py
GraphQL client for the Linear API. All reads/writes go through _graphql(). Key functions:
list_issues_with_label()— find tasks to processget_issue_with_comments()— fetch context for prompts (usescomments(last: 10)for newest)post_comment(),add_label(),remove_label(),update_issue_status()
scripts/_zellij_utils.py
Legacy Zellij session management plus OpenCode chat launch. Key functions:
spawn_opencode_session()— launches a persisted OpenCode Web chat with explicit plan or execute modecount_active_sessions()— counts all non-EXITED legacy Zellij sessionsenforce_session_limit()— kills excess legacy Zellij sessionslist_sessions_with_state()— returns{name: "ACTIVE"|"EXITED"}for cleanup
scripts/session-cleanup.py
Runs every 5 min via systemd timer. Three cleanup passes:
cleanup_stale_sessions()— kills sessions.json entries with stale Linear activity (>2h)cleanup_dead_poller_sessions()— detects EXITED/disappeared poller sessions, updates Linearenforce_session_limit()— enforces the global cap
scripts/linear-cron-setup.sh
Installs all systemd services. Run once on the dev server:
bash scripts/linear-cron-setup.sh
Session Tracking
Poller sessions are tracked in scripts/.tmp/poller-sessions.json:
{
"fix-OPE-123": {
"issue_id": "<linear-uuid>",
"identifier": "OPE-123",
"mode": "execute",
"started": "2026-04-02T12:00:00Z",
"claude_session_id": "<claude-jsonl-uuid>"
}
}
The claude_session_id maps to the JSONL transcript at ~/.claude/projects/-home-superdev-projects-OpenMates/<uuid>.jsonl, enabling the /task-status skill to show what each session is doing.
Systemd Services
| Service | Schedule | Purpose |
|---|---|---|
linear-poller.service |
Every 30s (loop with flock) | Poll for labeled tasks, spawn sessions, cleanup completed |
session-cleanup.service + .timer |
Every 5 min | Catch crashed sessions, enforce limits |
linear-archive.service + .timer |
Daily | Archive old closed issues (free plan limit) |
Check status:
systemctl --user status linear-poller.service
systemctl --user list-timers
journalctl --user -u linear-poller.service -n 20
Prompt Structure
Each spawned session reads a prompt file from scripts/.tmp/poller-prompt-OPE-XXX.txt containing:
- Mode instructions — what the session can/cannot do (execute vs read-only)
- Task context — identifier, title, description, recent comments
- Work instructions — research, identify root cause, implement (or summarize)
- Linear tracking instructions — post progress comments, update status on completion, handle failures
Troubleshooting
Sessions not spawning
# Check poller logs
journalctl --user -u linear-poller.service -n 30
# Check session count
python3 -c "import sys; sys.path.insert(0,'scripts'); from _zellij_utils import count_active_sessions; print(count_active_sessions())"
# Manual dry run
python3 scripts/linear-poller.py --dry-run
Sessions stuck as “In Progress”
# Check tracking file
cat scripts/.tmp/poller-sessions.json
# Run cleanup manually
python3 scripts/session-cleanup.py --dry-run
python3 scripts/session-cleanup.py
OOM / too many sessions
# Check memory
free -h
# Check all sessions
zellij list-sessions --no-formatting
# Force cleanup
python3 -c "import sys; sys.path.insert(0,'scripts'); from _zellij_utils import enforce_session_limit; enforce_session_limit()"
Systemd user bus crashed (after OOM)
systemctl --user daemon-reexec
# Or reboot the server