Skip to content

talkenigs/auto-code

Repository files navigation

auto-code (Python / FastAPI)

A lightweight FastAPI service that listens for Jira webhooks and autonomously implements tickets using the Claude Agent SDK.

What it does

When a Jira issue webhook fires, this service:

  1. Pulls the latest branch from the remote
  2. Creates a git worktree on a branch ticket/<jira-key> (e.g. ticket/proj-1)
  3. Runs Claude Code to implement the ticket (up to 50 turns, acceptEdits permission mode)
  4. (optional) Runs a second Claude Code session to code-review the uncommitted changes
  5. (optional) If review/lint/test issues are found, resumes the session to fix them
  6. Commits, pushes the branch, and opens a PR via gh

The webhook handler responds with 202 Accepted immediately — all processing is fire-and-forget on the event loop.

Isolated git worktrees, not branch-switching

Every ticket gets its own git worktree under <WORKSPACE_DIR>/worktrees/<branch>, checked out from a single shared clone in <WORKSPACE_DIR>/repo. The shared clone is git fetched (clone-if-missing-else-fetch) so the object store is reused across runs, while each worktree is a fully isolated working directory branched off origin/main.

This means concurrent tickets never stomp on each other's files, there's no git checkout thrash on a shared tree, and cleanup is a single git worktree remove. Under DRY_RUN the worktree is deliberately left in place (with a copy-pasteable git worktree remove … --force hint) so you can inspect exactly what Claude produced.

Self-correcting pipeline: review → lint → test

Implementation is only step one. The pipeline runs opt-in quality gates, and each one feeds its failures back into the same Claude session to be fixed:

implement ──▶ code-review ──▶ (issues?) ──▶ resume & fix
          └─▶ run_lint     ──▶ (failed?) ──▶ resume & fix
          └─▶ run_tests    ──▶ (failed?) ──▶ resume & fix
          └─▶ commit ▸ push ▸ open PR

Because review-fix, lint-fix, and test-fix all resume the original session_id, Claude keeps full context of what it just wrote instead of cold-starting — it fixes its own lint and test failures the way a developer would. Lint and tests run against the target repo's toolchain (pnpm-based), not this service.

Latency: pre-warmed agent connection

The Claude Agent SDK doesn't run in-process — it spawns the @anthropic-ai/claude-code Node CLI over stdio. To hide that cold-start, claude_code.warm() opens the SDK connection before the first prompt is built, so the initial turn isn't paying connection-setup latency.

Idempotency & fire-and-forget

Jira retries created/updated events, so handle_ticket holds an in-flight lock (an _active_tickets set keyed by ticket key) — duplicate webhooks for a ticket already running are dropped, not double-processed. The webhook itself returns 202 instantly and hands the work to a FastAPI BackgroundTask, wrapped in a top-level try/except so a failure is logged against the ticket key rather than silently swallowed.

Defense-in-depth auth

The webhook is protected by three independent layers:

  1. Cloudflare Access service token at the edge (see deployment below).
  2. A static X-Webhook-Token shared-token header matching WEBHOOK_SECRET (required ≥16 chars in production, no-op in dev). A static token rather than a body HMAC because Jira Automation can only send fixed header values.
  3. Rate limiting via slowapi (10/minute).

config.py fails fast at startup in production if REPO_URL, ANTHROPIC_API_KEY, GH_TOKEN, or a valid WEBHOOK_SECRET is missing.

Clean service architecture

Each owned service (claude_code, github, checks, langfuse, runner) is a self-contained package: base.py (ABC) · client.py (concrete) · mock_client.py (offline/test) · factory.py (@lru_cache singleton) · __init__.py (re-exports). RunnerClient receives its collaborators by dependency injection rather than importing free functions:

Settings (config.py)
  → factories (@lru_cache singletons)
    → lifespan builds singletons onto app.state
      → api/dependencies.py injects them into routes (RunnerDep)

The mock_client variants let the whole pipeline run offline with zero network calls, which is exactly what the test suite exercises.

Subprocess safety & observability

Every git / gh / lint / test invocation uses argument-list subprocess form (no shell), eliminating shell-injection risk. All Claude SDK runs are traced end-to-end via Langfuse (instrumented in lifespan, flushed after each ticket) — no on-disk run logs, full traces in the dashboard.

Setup

uv sync --extra dev      # create .venv and install deps (incl. dev tools)
cp .env.example .env     # set REPO_URL (or REPO_DIR for local dev)

Commands

# Develop (hot-reload)
uv run uvicorn app.main:app --reload --port 3001

# Start (production)
uv run auto-code

# Test
uv run pytest

# Lint
uv run ruff check .

Architecture

app/
├── main.py                       # FastAPI app bootstrap, /health endpoint
├── config.py                     # pydantic-settings env validation
├── logger.py                     # Minimal structured JSON logger (stdout/stderr)
├── models.py                     # Pydantic models (Ticket, PipelineOptions, Jira payload)
├── routes/
│   └── jira.py                   # POST /webhook/jira — parses payload, fires runner
├── api/
│   └── dependencies.py           # DI providers — injects app.state singletons (RunnerDep)
├── services/                     # each is a base/client/mock_client/factory package
│   ├── runner/                   # Orchestrates the full implementation pipeline
│   ├── claude_code/              # Wraps Claude Agent SDK query() / ClaudeSDKClient
│   ├── github/                   # subprocess wrappers for git + gh (clone/worktree/commit/PR)
│   ├── checks/                   # run_lint / run_tests against the target repo
│   └── langfuse/                 # Claude SDK tracing instrumentation
└── prompts/
    └── ticket_prompts.py         # build_implementation/review/fix prompts

Environment variables

Variable Default Description
CLAUDE_CODE_API_PORT 3001 HTTP port the server listens on
NODE_ENV development development or production
WORKSPACE_DIR /workspace Volume root; repo cloned to <WORKSPACE_DIR>/repo, worktrees under <WORKSPACE_DIR>/worktrees
REPO_URL (unset) HTTPS clone URL of the target repo. Required in production
REPO_DIR current dir Legacy dev/dry-run fallback: path to a pre-existing local repo
ANTHROPIC_API_KEY (unset) Read by the spawned Claude Code CLI. Required in production
GH_TOKEN (unset) GitHub token for HTTPS clone/push. Required in production
WEBHOOK_SECRET (unset) Static X-Webhook-Token value. Required in production (≥16 chars); no-op in dev
DRY_RUN false Skip commit/push/PR; leave worktree in place for inspection
LANGFUSE_PUBLIC_KEY (unset) Langfuse public key (set with secret key to enable tracing)
LANGFUSE_SECRET_KEY (unset) Langfuse secret key (both keys required to enable tracing)
LANGFUSE_BASE_URL https://cloud.langfuse.com Langfuse host

Endpoints

Method Path Description
GET /health Liveness check — returns { "status": "ok" }
POST /webhook/jira Jira issue webhook — triggers ticket processing

Jira webhook payload (minimum required fields)

{
  "issue": {
    "key": "PROJ-1",
    "fields": {
      "summary": "Add dark mode toggle",
      "description": "Optional detailed description"
    }
  }
}

Claude Agent SDK integration

services/claude_code/client.py uses query() and ClaudeSDKClient from claude_agent_sdk. Runs are traced via Langfuse.

Two modes:

  • run_implementationpermission_mode="acceptEdits", up to 50 turns. Writes code but does not commit. Optionally reuses a pre-warmed client or resumes a session.
  • run_code_reviewpermission_mode="bypassPermissions", up to 10 turns. Output is parsed for HAS_ISSUES: true|false and a FINDINGS: block.

The SDK does not run the agent in-process — it spawns the @anthropic-ai/claude-code (Node) CLI over stdio, so the container ships both Python and Node.js + the Claude Code CLI.

Git operations

services/github/client.py and services/checks/client.py shell out to git / gh / the target repo's lint+test tooling using argument-list subprocess calls (no shell). Requires git in PATH and gh authenticated (gh auth login / GH_TOKEN).

Deployment (Cloudflare Tunnel + Access)

The service is containerized (Dockerfile — Python + uv + Node + Claude Code CLI + git + gh, non-root) and deployed via compose/auto-code.yml, which runs a cloudflared tunnel alongside the app with no published ports. Inbound Jira webhooks reach the service only through the Cloudflare edge, gated by a Cloudflare Access service token — the host itself exposes nothing to the public internet.

docker build -t auto-code:latest .
docker compose --env-file .env -f compose/auto-code.yml up -d

See deployment-plan.md for the full isolated-host + Cloudflare Tunnel/Access setup.

Local testing with ngrok

For local development without the tunnel, expose the dev server directly:

# 1. Start the service
uv run uvicorn app.main:app --reload --port 3001

# 2. Expose it
ngrok http 3001

# 3. Register the public URL in Jira as a webhook:
#    https://<your-subdomain>.ngrok-free.app/webhook/jira

About

A lightweight FastAPI service that listens for Jira webhooks and autonomously implements tickets using the Claude Agent SDK.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors