Skip to content

MatterAIOrg/linear-coding-agent

Repository files navigation

Linear Coding Agent

A small Node.js + Hono service that turns Linear tickets into orbcode coding sessions.

Flow

  1. Create a Linear ticket with metadata in the description (repo, model, optional goal/base). The agent registers a parent job and posts a heads-up comment. Nothing is built yet.
  2. Move the ticket to In Progress. The agent fans out one child job per repo and starts working.
  3. Periodic progress comments (throttled to 1/minute, plus on phase changes) keep you up to date on what the agent has done.
  4. If the agent calls ask_followup_question, the agent posts a Linear comment with the question and pauses.
  5. Reply to the ticket with your answer. The agent resumes via orbcode --resume <sessionId>.
  6. The agent runs tests, commits, pushes, and opens a PR (all commits attributed to matterai-app[bot], all generated files excluded from the commit). Any errors collected during the run are listed in a single final summary comment.

Tool errors never abort the run. They are accumulated and surfaced at the end in one comment; the agent keeps iterating.

Quick start (Docker)

The container image bundles the agent, orbcode, git, and the GitHub CLI. Required environment variables are validated at startup.

cp .env.example .env
# Edit .env - you MUST set MATTERAI_API_KEY (used by orbcode) plus the
# Linear and GitHub credentials. The container refuses to start without
# MATTERAI_API_KEY.
docker compose up -d
docker compose logs -f agent

Point a Linear webhook at http://<host>:<port>/webhook/linear and subscribe to Issue (create + update) and Comment (create). On Linux without a reverse proxy, the default port is 3000; in .env.example the default is 4444 to avoid clashing with other local dev servers.

Quick start (bare metal)

npm install
# Install orbcode globally so it's on $PATH
npm install -g @matterailab/orbcode
cp .env.example .env  # fill in MATTERAI_API_KEY and the rest
npm run dev

Required environment variables

Var Used by Where to get it
MATTERAI_API_KEY orbcode (claude-code-style auth) https://matterai.so — account settings
LINEAR_API_KEY the agent's GraphQL client https://linear.app/settings/api
LINEAR_WEBHOOK_SECRET webhook signature verification the webhook config in Linear
GITHUB_TOKEN clone / push / gh pr create https://github.com/settings/tokens (scopes: repo)
INTERNAL_TOKEN protects /jobs/* any random 16+ char string

Without MATTERAI_API_KEY the agent and the orbcode CLI both fail to authenticate. The Docker Compose file fails fast on docker compose up if it's missing, with a clear error message.

Status gating

The agent does not start work on Issue.create. It only kicks when the status changes to In Progress (case-insensitive). This is wired through Issue.update webhooks. You can rename the status in Linear as long as name matches "in progress".

Multiple repos

The repo: key accepts one or more repos, space or comma separated. Each can be owner/name or a full GitHub URL (https://github.com/owner/name[.git], git@github.com:owner/name.git).

repo: matterai/orbcode https://github.com/matterai/axon
model: axon-eido-3-code-mini

Update both repos to use the new error model.

This fans out to two child jobs, one per repo, each with its own branch and its own PR. The first one is the "primary" only in that it appears first in the fan-out order; all children run concurrently.

Linear issue format

Front-matter (YAML-ish) takes precedence over inline key: value lines. Only repo, model, base/baseBranch, and goal are treated as metadata — every other line is part of the goal.

---
repo: matterai/orbcode matterai/axon
model: axon-eido-3-code-mini
base: main
goal: Update both repos to use the new error model
---

Or inline:

repo: https://github.com/matterai/orbcode.git,matterai/axon
model: axon-eido-3-code-mini

Update both repos to use the new error model.

The agent refuses to register and posts a comment if repo: or model: is missing.

Webhook configuration

In Linear, point a webhook at https://<your-host>/webhook/linear and subscribe to:

  • Issue -> create (registers the job, waits for In Progress)
  • Issue -> update (kicks when status changes to In Progress)
  • Comment -> create (resumes a waiting job with the comment body as the answer)

The agent verifies the linear-signature header with HMAC-SHA256 of the raw body using LINEAR_WEBHOOK_SECRET.

How questions work

  1. Orbcode calls ask_followup_question with {question, follow_up}.
  2. The hook (hooks/ask-question-hook.sh) writes .linear-coding-agent/ipc/question.json and exits 2.
  3. The runner sees the file, kills the child, and resolves with {kind: "question"}.
  4. The orchestrator posts a Linear comment with the question and any suggested answers, sets status: waiting_for_answer, and pauses.
  5. When a user (not the bot) comments on the issue, onIssueComment is called.
  6. The orchestrator sets status: resuming and re-invokes the runner with orbcode --resume <sessionId> -p "<prompt with answer>".

How tool errors are collected

A second hook (hooks/error-collector-hook.sh) is wired on PreToolUse, PostToolUse, and PostToolUseFailure. When a tool error is observed it appends a JSON line to .linear-coding-agent/ipc/errors.jsonl. The runner streams these to the orchestrator via an onToolError callback, which records them on the job. The agent itself is not told to stop — orbcode continues. Only at the end, when the job reaches done or error, does the orchestrator post a single summary comment listing all collected errors.

The two places where errors ARE fatal:

  • Clone failure (no worktree to operate on)
  • gh pr create failure (recorded, but the branch is still pushed and a warning comment is posted)

Everywhere else, the loop keeps going.

How progress updates work

A third hook (hooks/progress-reporter-hook.sh) is wired on SessionStart and PostToolUse (matcher *). It overwrites .linear-coding-agent/ipc/progress.json with a one-line summary of the most recent activity (read path/to/x, edited path/to/y, ran: command…). The runner streams these to the orchestrator, which:

  1. Appends the entry to a per-job activity buffer (capped at 50).
  2. Tries to claim a "progress comment" slot — at most one per minute per job.
  3. If the slot is claimed, posts a Linear comment showing the current phase, session id, error count, and the most recent 5 activities.

A phase change (e.g. "implementing" -> "running tests") forces a comment immediately, bypassing the throttle.

Architecture

Linear webhook -> Hono server -> Orchestrator (state machine)
                                      |
                                      v
                                    JobStore (JSON file + mutex)
                                      |
                                      v
                                  Worker per job:
                                    - git clone + branch (gh CLI)
                                    - orbcode -p --model
                                    - .orbcode/settings.json + 3 hook scripts
                                    - test runner (auto-detect pnpm/bun/npm/yarn)
                                    - gh pr create

Layout

src/
  config.ts             # zod-validated env loading
  server.ts             # Hono app: webhook + internal endpoints
  orchestrator.ts       # per-job state machine, error collection, progress
  store.ts              # JSON-file job store with mutex
  types.ts              # shared domain types
  linear/
    client.ts           # GraphQL queries/mutations
    parser.ts           # extract goal/repos/model from issue body
    webhook.ts          # HMAC-SHA256 signature verification
  github/
    client.ts           # clone/branch/commit/push/PR
  orbcode/
    runner.ts           # spawn orbcode, watch IPC dir, stream tool errors + progress
  projects/
    test-runner.ts      # auto-detect package manager, run tests
    gitignore.ts        # ensure .gitignore excludes our generated files
  util/
    process.ts          # spawn wrapper with timeout + stream
    logger.ts           # structured JSON / pretty
    slug.ts             # branch name helpers
hooks/
  ask-question-hook.sh        # PreToolUse + SessionStart
  error-collector-hook.sh     # PreToolUse + PostToolUse + PostToolUseFailure
  progress-reporter-hook.sh   # SessionStart + PostToolUse
tests/                       # vitest

Setup

npm install
cp .env.example .env  # then fill in secrets
npm run typecheck
npm test
npm run dev

Commits and PRs

The worktree is configured with:

user.name = matterai-app[bot]
user.email = 162532116+matterai-app[bot]@users.noreply.github.com

so every commit the agent makes shows up on GitHub as authored by the bot. PRs are created via gh pr create with GH_TOKEN set to the GitHub token; the PR body links back to the Linear issue and includes any collected errors.

If the agent hit errors during the run, the PR is opened as a draft so the human can review before requesting review.

Generated files (.orbcode/, .linear-coding-agent/) are never included in the commit:

  • The runner injects a managed block into the project's .gitignore (idempotent, behind a # >>> linear-coding-agent ... # <<< linear-coding-agent marker so it's easy to find and remove).
  • commitAll does git add -A and then explicitly git restore --staged the excluded paths, defending against pathological .gitignore files that use ! to force-include them.

Internal endpoints

All require the INTERNAL_TOKEN header.

  • GET /health — liveness
  • GET /jobs — list all jobs (parent + children)
  • GET /jobs/:id — get one job
  • POST /jobs/:id/retry — re-kick a job (useful after manually fixing the worktree)

Idempotency

registerIssue is idempotent on the Linear issue id. Re-delivering the same webhook returns the same parent job. Children are created at registration time and re-fanning-out on a second onIssueStatusChange is a no-op for already-running children.

Jobs survive a process restart via the JSON store; the POST /jobs/:id/retry endpoint can be used to resume.

Docker deployment details

The image is multi-stage:

  • builder: installs all deps and compiles TypeScript -> dist/.
  • runtime: minimal node:20-slim with git, jq, gh (from the official apt repo), the compiled app, the hook scripts, and orbcode installed globally via npm install -g @matterailab/orbcode.

The container:

  • Runs as the non-root node user.
  • Writes job state and worktrees to /data (bind-mount ./data:/data).
  • Listens on :3000 (configurable via PORT).
  • Has a HEALTHCHECK that probes /health every 30s.

docker compose config and docker compose up both fail fast with a clear message if MATTERAI_API_KEY is missing. Set it in .env and you're done.

Limitations / next steps

  • Single-process orchestrator. Swap JobStore for Postgres + advisory locks to scale horizontally.
  • Error sidecar is best-effort (we truncate after draining). A future iteration can use fs.watch's persistent mode and an offset cursor to avoid races.
  • The hook only special-cases ask_followup_question for blocking. All other tools are allowed through; errors are recorded but not blocked. If you need to deny specific tools, add matchers in src/orbcode/runner.ts#writeWorktreeConfig.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors