Skip to content

ack-ventures/critters

Repository files navigation

Critters

Critters is a TypeScript daemon that polls issue trackers (Linear and Jira) for issues labeled "Critter", spawns coding CLI instances (Claude Code and/or Codex) to plan and implement the work, and opens draft pull requests for human review. It runs on Bun and orchestrates everything through tmux panes.

Inspired by Stripe's Minions.

Install

curl -fsSL https://raw.githubusercontent.com/ack-ventures/critters/main/install.sh | bash

This downloads the latest binary, installs it to your PATH, and walks you through initial setup.

Commands

  • critters — start the daemon
  • critters version — show version
  • critters update — check for and apply updates
  • critters stop — stop the daemon gracefully
  • critters init — (re-)configure ~/.critters/
  • critters status — show daemon status (active/queued critters, today's stats)
  • critters retry <ID> — retry a failed critter (reset to Todo)
  • critters logs <ID> — show logs for a critter run
  • critters kickoff — trigger an immediate poll (instead of waiting for the next interval)
  • critters list-types — show configured critter types
  • critters init-repo — scaffold .critters.yaml in the current repo
  • critters prompt-help — launch Claude to help design critter types and prompts
  • critters clean — clean up stale work directories (--all, --dry-run, --branches, --panes)
  • critters release-notes — show release notes for recent versions
  • critters validate — validate config file without starting daemon
  • critters help — show usage

Flags

  • --dry-run — poll once, show what would happen, and exit
  • --no-tmux — daemonize to background without tmux (logs to ~/.critters/critters.log, writes PID to ~/.critters/critters.pid)
  • --no-watch — disable config file watching (no hot-reload)
  • --skip-update — skip auto-update check on startup
  • --config PATH — use a custom config file
  • --type NAME — filter dry-run to a specific critter type
  • --json-logs — output structured JSON logs (one object per line)

Self-Hosting

See docs/self-hosting.md for the full deployment guide.

Quick start (binary):

curl -fsSL https://raw.githubusercontent.com/ack-ventures/critters/main/install.sh | bash
critters init
tmux new -s critters
critters

Development quick start

Prerequisites: Bun, Claude Code CLI (authenticated) and/or Codex, gh CLI (authenticated), tmux, jq

# Clone and install
git clone https://github.com/ack-ventures/critters && cd critters
bun install

# Configure
cp .env.example .env
# Edit .env and set LINEAR_API_KEY (for Linear) and/or JIRA_HOST, JIRA_EMAIL, JIRA_API_TOKEN (for Jira)
# Optionally set SLACK_WEBHOOK_URL for notifications, or SLACK_BOT_TOKEN + SLACK_CHANNEL for threaded notifications

# Optionally tweak critters.config.yaml

# Run
bun start
# or: bun run src/index.ts

Docker

# Clone and configure
git clone https://github.com/ack-ventures/critters && cd critters
cp .env.example .env
# Edit .env — set ANTHROPIC_API_KEY and LINEAR_API_KEY (and/or JIRA_* vars)

# Start
docker compose up -d

# View logs
docker compose logs -f

# Dashboard at http://localhost:3847

The Docker image includes all runtime dependencies (Claude Code CLI, gh, git, jq).

Auth requirements:

  • CLAUDE_CODE_OAUTH_TOKEN — Claude Max/Team subscription token (run claude setup-token on the host, then add to .env or export in your shell). Alternatively, set ANTHROPIC_API_KEY for direct API access.
  • ~/.ssh — SSH keys for git clone (mounted read-only)
  • GITHUB_TOKEN — GitHub CLI auth (run gh auth token on the host, then add to .env or export in your shell)

For the pre-built image, replace build: . with image: ghcr.io/ack-ventures/critters:latest in docker-compose.yaml.

To use the latest released binary instead of building from source, set the build target to prod:

# docker-compose.yaml
services:
  critters:
    build:
      context: .
      target: prod  # downloads released binary instead of building from source

Or build directly: docker build --target prod . (append --build-arg CRITTERS_VERSION=1.6.1 to pin a version).

How it works

  1. Watcher polls your issue tracker (Linear and/or Jira) every 120 seconds for issues with the "Critter" label in "Todo" status.
  2. Spawner shallow-clones the target repo into a temp directory and creates a feature branch.
  3. Phase 1 (Planning): A configured coding CLI instance explores the codebase and writes an implementation plan.
  4. Phase 2 (Execution): A configured coding CLI instance implements the plan, commits, pushes, and opens a draft PR.
  5. Status updates flow back to the issue tracker: Todo → In Progress → In Review (on PR) or Critter Failed (on error).

Critter lifecycle

The built-in critter types form a pipeline:

Todo → In Progress → In Review → review ──┬──► Done (merged)
         (create)                          │
                        ┌──────────────────┘
                        ▼
                   Human Review
                        │
                        └──► fix-review-comments ──► In Review (loops back to review)
  1. create picks up "Todo" issues, plans and implements the work, opens a draft PR, and moves the issue to "In Review".
  2. review picks up "In Review" issues, reviews the PR diff, and either merges it (→ "Done") or requests changes (→ "Human Review").
  3. fix-review-comments (optional, user-configured) picks up "Human Review" issues, reads the review feedback from the PR, addresses it, and moves the issue back to "In Review" — sending it through review again.

Without fix-review-comments, issues that need changes stop at "Human Review" for manual intervention (or critters retry --force).

To enable the loop, add a fix-review-comments type to your config:

critterTypes:
  # ... create and review types ...

  fix-review-comments:
    trigger: { label: "Critter Review", status: "Human Review" }
    repo: { clone: true, branch: true }
    phases:
      - name: fix
        prompt: ~/.critters/prompts/fix-review.md
        model: opus
        maxTurns: 50
        tools: default
    outcomes:
      success: { status: "In Review" }
      failure: { status: "Critter Failed" }
    concurrency: 2
    timeoutMinutes: 20
    enrichment: extractPrUrl
    claimStatus: "In Progress"

The prompt file should instruct Claude to read the PR review feedback (for example with gh pr view --comments plus gh api repos/:owner/:repo/pulls/:number/comments for inline review comments), make the requested fixes, commit, and push. Use builtin:execution prompts as a starting point. claimStatus prevents duplicate dispatch if two poll cycles trigger before the fix completes.

Configuration

Settings live in critters.config.yaml:

Field Default Description
provider "linear" Default issue tracker: "linear" or "jira"
pollIntervalSeconds 120 How often to poll for issues
concurrency 2 Max parallel critters
timeoutMinutes 30 Total timeout per task (both phases)
workDir /tmp/critters-work Temp clone directory
triggerLabel "Critter" Label that triggers pickup
maxPlanningTurns 50 Max turns for planning phase
maxExecutionTurns 75 Max turns for execution phase
defaultAllowedTools see file Tools critters can use
repos {} Project ID → repo URL + extra tools
teamRepos {} Team ID → fallback repo URL
defaultRepo Final fallback repo URL when not in description, project config, or team config
tmuxSession "critters" Name of the tmux session to use
branchPrefix "critter" Prefix for feature branch names (<prefix>/<ID>-<slug>)
planningModel "opus" Model for planning phase
executionModel "opus" Model for execution phase
healthPort 3847 HTTP server port for dashboard and health checks (0 to disable)
dashboardToken Shared secret for dashboard POST endpoints (also reads DASHBOARD_TOKEN env var)
maxLogSizeMb 10 Max log file size in MB before rotation (with --no-tmux)
cleanupIntervalMinutes 60 How often periodic work directory cleanup runs (minutes)
cleanupStaleMinutes timeout + 30 Age threshold (minutes) for considering work directories stale
minDiskSpaceMb 1024 Minimum free disk space (MB) required before cloning a repo
jiraStatusMap {} Map critter status names to Jira workflow status names
hooks {} Shell commands run on lifecycle events (see below)
costAlertThreshold Cost (USD) per task that triggers a Slack alert
costBudget Cost (USD) per task that triggers a kill
reviewTriggerLabel "Critter Review" Label that triggers review pickup
reviewModel "opus" Model for reviews
reviewConcurrency 2 Max parallel review critters
reviewTimeoutMinutes 15 Timeout per review
maxReviewTurns 30 Max turns per review
cli "claude" Default CLI adapter: "claude" or "codex"
phases[].sandbox Optional phase-level sandbox override for adapters that support it, e.g. Codex read-only, workspace-write, or danger-full-access
autoRetry Auto-retry config: { maxRetries, baseDelaySeconds, maxDelaySeconds }
circuitBreaker Circuit breaker config: { failureThreshold, maxBackoffMinutes }
mcpConfig Path(s) to MCP config JSON file(s), applied to all critters
strictMcpConfig false When true, passes --strict-mcp-config to prevent inheriting operator's MCP servers
metricsRetentionDays 90 Days to retain metrics data before pruning
autoUpdate Auto-update config: { enabled (default: true), intervalMinutes (default: 1440) }
tunnel Tunnel config: { enabled, auth, domain } for ngrok remote access
linearWebhookSecret Linear webhook signing secret (env: LINEAR_WEBHOOK_SECRET)
jiraWebhookSecret Jira webhook secret (env: JIRA_WEBHOOK_SECRET)

Per-repo tool overrides merge with the defaults:

repos:
  "project-uuid":
    url: "git@github.com:org/repo.git"
    extraAllowedTools:
      - "Bash(python:*)"
      - "Bash(pip:*)"

You can choose the CLI globally, per critter type, or per phase:

cli: claude

critterTypes:
  create:
    phases:
      - name: planning
        prompt: builtin:planning
        cli: codex
        model: gpt-5.2
        maxTurns: 50
        tools: readonly
      - name: execution
        prompt: builtin:execution
        cli: claude
        model: opus
        maxTurns: 75
        tools: default

Critters preserves the existing tools config for both adapters. Claude enforces the allowlist directly; Codex uses sandboxing plus prompt-level restrictions, so enforcement is capability-based rather than identical.

For Codex phases that need outbound GitHub access through gh, set a phase sandbox explicitly. In practice, PR review and review-fixing phases often need danger-full-access, because Codex workspace-write can block the GitHub API calls that gh relies on.

Hooks

Shell commands that run on lifecycle events. Environment variables available: CRITTER_ISSUE_ID, CRITTER_IDENTIFIER, CRITTER_TITLE, CRITTER_REPO_URL, CRITTER_BRANCH, CRITTER_PR_URL (when applicable).

hooks:
  onTaskStarted: "echo 'Task started'"
  onPrCreated: "curl -X POST https://example.com/notify"
  onMerged: "./scripts/post-merge.sh"
  onTaskFailed: ""
  onReviewStarted: ""
  onNeedsChanges: ""

Web Dashboard

The daemon runs an HTTP server on port 3847 (configurable via healthPort, set to 0 to disable).

Route Method Description
/ or /dashboard GET Live dashboard with task stats, charts, and recent activity. Includes a "New Critter" button for creating tickets
/healthz GET JSON health check (uptime, version, per-type active/queued counts, active critter details, metrics summary)
/metrics GET JSON array of recent metric events
/poll POST Trigger an immediate poll cycle
/review-poll POST Trigger an immediate review poll cycle
/restart POST Restart the daemon process
/stop POST Stop the daemon gracefully
/api/v1/metadata GET Provider teams, critter types, and repo info
/api/v1/issues POST Create a critter issue ({ provider, teamId, title, description, critterType })
/api/logs/<id> GET Processed log tail for a critter run
/webhook/linear POST Linear webhook endpoint (requires linearWebhookSecret)
/webhook/jira POST Jira webhook endpoint (requires jiraWebhookSecret)

When dashboardToken is configured, POST endpoints require a bearer token. critters status queries the health endpoint to display a quick summary in the terminal.

Webhooks

Webhook endpoints provide near-instant issue pickup instead of waiting for the next poll. Webhooks are additive — polling continues as the fallback. Set LINEAR_WEBHOOK_SECRET and/or JIRA_WEBHOOK_SECRET to enable. The daemon must be reachable from the internet — use the tunnel config for ngrok or a reverse proxy.

Creating tickets

Works with both Linear and Jira. For a critter to pick up an issue, it needs:

  • Label: "Critter" (exact match, configurable)
  • Status: "Todo" (Linear) or the mapped status via jiraStatusMap (Jira)
  • Description: must include repo: git@github.com:org/repo.git on its own line (unless a project or team mapping exists in the config)

Optionally, assign the issue to the relevant project and include implementation guidance in the description -- the critter reads it as its task spec.

Custom critter types

Beyond the built-in create and review flows, you can define custom critter types in critters.config.yaml. Each type gets its own trigger label, phase pipeline, model, tools, and outcome statuses.

critterTypes:
  code-audit:
    trigger: { label: "Code Audit", status: "Todo", statusType: "unstarted" }
    repo: { clone: true }
    phases:
      - name: audit
        prompt: ~/.critters/prompts/code-audit.md
        model: sonnet
        maxTurns: 20
        tools: [Read, Glob, Grep, "Bash(git:*)", "Bash(ls:*)"]
    outcomes:
      success: { status: "Done", comment: true }
      failure: { status: "Critter Failed", comment: true }
    concurrency: 3
    timeoutMinutes: 10

Custom types automatically:

  • Prompt Claude to write a .critter-report.md file
  • Upload the report as a .md attachment on the issue
  • Post the report as an inline comment

Prompt files support {{identifier}}, {{title}}, {{description}}, and other variables. See CLAUDE.md for the full reference.

Assignee filtering: Add trigger.assignee to only pick up issues assigned to a specific user. Use an email address (e.g., "alice@company.com") or "me" for the authenticated user. Useful in shared projects where you don't want critters picking up every labeled issue.

Quiet mode: Add quietComments: true to suppress status/progress comments (e.g., "picking up task...", "phase completed in..."). Report content and error comments are still posted.

Claim status: Critter types with trigger.statusType: "unstarted" automatically transition issues to "In Progress" when picked up, preventing duplicate dispatch. Override with claimStatus: "Reviewing" (or any status), or set claimStatus: null to disable. Types without statusType: "unstarted" (e.g., review types) don't claim by default.

Model guidance: Use sonnet or opus for custom types. Haiku often ignores tool-use instructions and produces shallow output.

If critterTypes is omitted from config, the daemon synthesizes the default create and review types from the flat config fields — fully backward compatible.

Use cases

Critters is a general-purpose agent orchestrator — the built-in create and review types are just the start. Any workflow matching "watch for trigger → run Claude with a prompt → produce an outcome" can be a critter type:

  • Issue triage bot — automatically labels, prioritizes, and summarizes new issues
  • Documentation writer — reads code and generates or updates markdown docs, opens a PR
  • Security auditor — scans for OWASP top 10 vulnerabilities, dependency issues, and hardcoded secrets
  • Test generator — reads implementation code and writes missing test cases, opens a PR
  • Multi-step workflows — chain critter types using Linear/Jira blocking relationships (e.g., a "plan" critter spawns sub-tickets, then "implement" critters pick up each one)

See CLAUDE.md for complete config examples.

Multi-provider (Linear + Jira)

A single daemon can poll both Linear and Jira. Set the default provider at the top level, then use provider on each critter type to override:

provider: linear  # default

jiraStatusMap:
  "Todo": "To Do"
  "In Progress": "In Progress"
  "In Review": "In Review"
  "Done": "Done"
  "Critter Failed": "Failed"

critterTypes:
  create:
    provider: [linear, jira]   # polls both trackers with the same config
    trigger: { label: "Critter", status: "Todo", statusType: "unstarted" }
    repo: { clone: true, branch: true }
    phases:
      - name: planning
        prompt: builtin:planning
        model: opus
        maxTurns: 50
        tools: readonly
      - name: execution
        prompt: builtin:execution
        model: opus
        maxTurns: 75
        tools: default
    outcomes:
      success: { status: "In Review" }
      failure: { status: "Critter Failed" }
    concurrency: 2
    timeoutMinutes: 30

provider accepts a single value ("linear", "jira") or an array ([linear, jira]). When an array is used, the type is expanded internally so each provider is polled independently — no need to duplicate config.

Only the env vars for providers you actually use are required. A Linear-only config doesn't need JIRA_* vars.

See CLAUDE.md for full multi-provider docs, Jira differences, and more config examples.

Documentation


See CLAUDE.md for detailed developer docs, architecture diagrams, and contributor conventions.

About

No description, website, or topics provided.

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages