--- name: agent-pipeline description: Operate a ticket-to-agent pipeline — Linear (free plan) + self-hosted Cyrus running Claude Code on your own machine. Use when creating or updating tickets, handing a ticket to the agent, checking why a ticket did not start, reading the runner's status or logs, adding a repo to the pipeline, changing routing, or any Linear admin change. Also when the user says "make a ticket", "give it to the agent", "is the agent working", "add my repo". --- # Agent pipeline (Linear + Cyrus) The user writes tickets in Linear. Cyrus on the always-on machine hands each one to Claude Code, and the result comes back as a PR, a commit or a deploy, depending on the repo's rule. Set-up is in `workflows/agent-pipeline.md`. This skill is the operator's manual. ## The shape (fill in for your setup) | Piece | Value | |---|---| | Linear workspace | `https://linear.app/`: **one team** ``, **one project per repo** (free plan: 2 teams, unlimited projects) | | Routing | project name → repo, via `projectKeys` in `~/.cyrus/config.json` | | Per-repo rule | written in each repo's `CLAUDE.md`: PR-and-stop / commit to develop / agent deploys | | Runner | service (launchd/systemd) on port 3456, public `https://.` via Cloudflare Tunnel | | Config | `~/.cyrus/config.json` (routing + OAuth tokens) · `~/.cyrus/.env` (secrets) | | Logs | `~/.cyrus/cyrus.log` · per ticket `~/.cyrus/logs//` | | Claude | the machine's own `claude` login; usage counts against the user's plan | **Secrets rule:** never `cat` `config.json` or `.env`. Print key names only (`cut -d= -f1 ~/.cyrus/.env`), and read tokens straight into a variable (see `references/linear-api.md`). ## Quick reference | Want to… | Do | |---|---| | Create a ticket and start the agent | `issueCreate` with `projectId` + `delegateId` = the agent's app user (`references/linear-api.md`). Delegate set on create → the session starts in the same second | | Hand an existing ticket over | `issueUpdate(delegateId)`. Agents live in **delegate**, not assignee | | Check it started | `curl localhost:3456/status` → `busy`; `grep ~/.cyrus/cyrus.log`; `~/.cyrus/worktrees/` exists. First thought ≈10 s, real work ≈2 min | | Watch it finish | `gh pr list --repo / --search `; summary comment on the ticket | | Send it back | reply in the ticket thread (same session, same PR) | | Close a ticket with no PR | a commit on the default branch with `Fixes ` (verify; not 100% reliable) or set state Done | | Re-run a ticket that errored | re-delegating does **nothing**; reply in the thread or file a new ticket | | Diagnose "didn't start" | `references/cyrus.md` → Diagnosing | | API returns 403 | do it in the Linear UI (rename/delete team, team labels) | ## Adding a repo (~2 min) 1. `gh repo view /` resolves. Project name = repo name title-cased (`my-app` → `My App`). State the name you chose. `projectKeys` must match it exactly. 2. `cyrus self-add-repo https://github.com//.git` (clones and hot-reloads). 3. Create the Linear project with that name in team `` (`projectCreate`). 4. Set `projectKeys` and **delete the auto-added `routingLabels`** (labels beat projects): ```bash jq '.repositories |= map(if .name=="" then del(.routingLabels) | . + {projectKeys:[""]} else . end)' \ ~/.cyrus/config.json > /tmp/c.json && mv /tmp/c.json ~/.cyrus/config.json ``` 5. If the repo has no `CLAUDE.md` with a deploy rule, the first ticket should add one. It is the agent's only briefing besides the ticket. ## Writing a good ticket Title = the change. Description = what to change, then `Done when:` with 2–4 checkable lines (including which test or browser flow must still pass), then what **not** to touch. Say "deploy" only if the user wants it live. For apps with real users, say "no schema change" or "docs/tests only" when that is the intent. **One small, checkable change per ticket.** Multi-hour phases handed to an unattended agent failed on usage limits and restarts. ## Common mistakes - Querying `assignee` to see who works a ticket. Agents are in `delegate`. - Hitting `https:///callback` while `cyrus self-auth-linear` waits: it exits and the OAuth redirect 502s. - Starting a second Cyrus by hand: port 3456 is taken. Restart the service instead. - "Blocked by" relations **park** sessions in memory. They wake only when that exact blocker is Done or a *human* replies, and a restart drops them. Fix relations before delegating. - Parallel sessions hit the shared Claude usage limit (`Session error` in the log). Run one at a time. - Merging a PR is not deploying. Someone still pulls, migrates the test DB, and runs the deploy script.