Skip to content
xmiksayPublic

About

Agent to perform automatic claude triggers

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Repository files navigation

agent

CI

A Rust/Axum service that listens for git-forge webhooks, runs the claude CLI against the affected repo, and posts the result back as an issue/MR/PR comment. Supports GitLab and GitHub today; Codeberg (Forgejo) is planned. A Vue 3 SPA on the same port shows live task status, branch diff, captured stdout, and pending operator approvals.

Engineering rules live in .claude/CLAUDE.md and the full architecture map in docs/architecture.md. Read those first if you're going to modify the code.

Screenshots

The Tasks dashboard — every agent run across all connected repos, with provider, branch, trigger, status, and spend:

Tasks dashboard

The Stats view — time spent by the agent, grouped and windowed:

Stats view

Setup

1. Configure the host

Copy .env.example to .env and set:

Var Default Purpose
DATABASE_URL — (required) postgres://user:pass@host/db or sqlite://path/to/file.db?mode=rwc. ?mode=rwc is required — sqlx won't create a missing file otherwise. Slash count matters: sqlite://agent.db (two slashes) is a relative path; sqlite:///agent.db (three) is absolute (/agent.db at the filesystem root — almost never writable, and easy to type by mistake). sqlite::memory: also works but only persists for the process lifetime
REPO_BASE_PATH /tmp/claude-jobs working-tree base: <base>/<service_slug>/<project_slug>/<branch>
LISTEN_ADDR 0.0.0.0:3000 bind address
PUBLIC_BASE_URL unset externally reachable base URL (e.g. https://agent.example.com); used to auto-register provider webhooks. Unset → auto-registration skipped, wire hooks by hand
MAX_CONCURRENT_JOBS 3 task-admission ceiling — a per-turn semaphore plus the queue's count of live/in-flight tasks, not a fixed count of parallel claude processes: a warm-but-idle session (between turns) holds an admission slot but no process is actively running, so admitted work can exceed this many simultaneously-executing turns (#159 Docs)
TASK_TOKEN_BUDGET 1000000 per-task soft output-token budget; at 50 % the runner kills claude and finalizes the task failed (session_id kept, so the operator can Resume after the 5 h rate-limit window)
OPERATOR_APPROVAL_TIMEOUT_SECS 0 seconds an unanswered tool-approval waits before auto-denying. 0 = wait indefinitely (never auto-deny — the default). A turn parked on an approval releases its MAX_CONCURRENT_JOBS permit and re-acquires on resolve (#84), so even an indefinite wait never starves other turns
JOB_TIMEOUT_SECS 0 per-turn wall-clock cap; past it the runner SIGKILLs the agent and its whole process group (orphaned test processes included) and leaves the task resumable. 0 = disabled
WARM_IDLE_TIMEOUT_SECS 1800 keep_warm session_mode projects only — seconds a warm-but-idle session may sit between turns before the runner closes it gracefully (resumable completed, session_id kept — a later comment/message resumes it via claude -r). A warm agent holds a MAX_CONCURRENT_JOBS slot, so eviction keeps idle sessions from starving the queue. 0 = disabled (sessions stay warm indefinitely)
API_BEARER_TOKEN unset when set, gates /api/* and the SPA; paste it into the SPA on first load
NVM_DIR unset NVM install dir (e.g. /home/agent/.nvm). When set, the runner resolves the node toolchain NVM would activate (honouring the worktree's .nvmrc, else the default alias) and prepends its bin to the agent's PATH. Unset → disabled
PROJECT_DB_ADMIN_URL unset admin Postgres DSN with CREATE ROLE+CREATE DATABASE rights. When set, the runner provisions a throwaway role + database per task (agent_task_<id>), injects its DSN into the agent's env, and drops both at session end. Unset → disabled
PROJECT_DB_HOST_FOR_AGENT host from admin URL host:port the agent uses in the injected DATABASE_URL/PGHOST (may differ from the admin connection in containerized setups)
RUST_LOG agent=info tracing filter

The Vue SPA is baked into the release binary by rust-embed — no runtime path override.

Forge credentials (GitLab, GitHub, …) are not in .env — they live in the service table and are managed via the admin UI.

2. Run

Everything goes through the Makefile — the SPA must be built before the binary (rust-embed bakes frontend/dist/ in) and the targets enforce that ordering:

make run     # build SPA + cargo run (migrations run on startup)
make build   # release build (SPA + binary)
make dev     # hot-reload SPA: vite on 5173, API-proxied to the agent
make deploy  # release build, then install the binary + restart the systemd unit

Visit http://localhost:3000 and (if API_BEARER_TOKEN is set) paste the token. Node version is pinned in .nvmrc (nvm use first).

3. Add a git service

Go to Services → Add service and fill in:

Field Example
Kind gitlab (multiple allowed) / github (single)
Slug personal
Display name Personal GitLab
Base URL https://gitlab.com
Bot username group_NNN_bot_agent (the generated bot handle)
Access token GitLab: a Group/Project Access Token (glpat-…) with api + write_repository, Maintainer/Owner role — this gives the agent its own bot identity (a group_NNN_bot_* service account), so it acts as itself, not you. GitHub: a PAT with repo + admin:repo_hook. The token does git transport (token-HTTPS clone/push), note posting, and webhook registration
Webhook secret a random string

GitLab bot token. Mint the Group Access Token under the group's Settings → Access Tokens, or with the CLI:

glab token create --group my-group --name agent-bot \
  --scope api --scope write_repository --role maintainer --expires-at 2027-06-08

Access tokens expire within 365 days — rotate before expiry (manually, or via the access-tokens …/rotate API). See docs/application-integration.md.

After saving, the service detail page shows the Webhook URL to paste into the forge.

Webhook endpoints

Per service:

POST  /webhook/gitlab/<slug>     # X-Gitlab-Token = the service's webhook_secret
POST  /webhook/github/<slug>     # X-Hub-Signature-256 HMAC-SHA256 of body, key = webhook_secret

<slug> is the value you entered when creating the service. Service-detail page renders the full URL for convenience.

Auto-registration. When PUBLIC_BASE_URL is set, the agent registers the webhook itself the first time it sees a project (idempotently — re-runs update in place rather than duplicating), so the manual steps below are a fallback. The PAT needs the hook-admin scope (admin:repo_hook on GitHub; api + Maintainer/Owner on GitLab). GitHub App services use a single app-level hook instead.

GitLab side

Project → Settings → Webhooks → Add new webhook:

  • URL — https://<your-agent-host>/webhook/gitlab/<slug>
  • Secret token — the webhook_secret you saved
  • Trigger — Issues events, Comments, Merge request events (Confidential variants too if you want)
  • SSL verification — on

You can register the same webhook at group level (Settings → Webhooks on the group) so it applies to every project beneath it.

To register via API instead of UI:

curl --request POST \
     --header "PRIVATE-TOKEN: $GITLAB_PAT" \
     --header "Content-Type: application/json" \
     --data '{
       "url": "https://your-agent.example.com/webhook/gitlab/personal",
       "token": "the-webhook-secret",
       "issues_events": true,
       "note_events": true,
       "merge_requests_events": true,
       "confidential_issues_events": true,
       "confidential_note_events": true,
       "enable_ssl_verification": true
     }' \
     "https://gitlab.com/api/v4/projects/<project_id>/hooks"

GitHub side

Repo → Settings → Webhooks → Add webhook:

  • Payload URL — https://<your-agent-host>/webhook/github/<slug>
  • Content type — application/json
  • Secret — the webhook_secret you saved
  • Events — Issues, Issue comments, Pull requests, Pull request reviews

How it triggers

Event Action
Issue assigned to the bot user Implement, push branch, open MR/PR
MR/PR review with changes requested Fix and push
Any other MR/PR review Post a review note
Issue/MR/PR comment containing @<bot_username> Reply in-thread
Issue closed / MR/PR closed/merged Release the checked-out branch

Trigger mode. What counts as "assigned to the bot" is governed by the service's trigger_mode (overridable per event type): assignee (default — the bot is among the issue's assignees), label (the issue carries the configured trigger_label), or both. A GitHub App can't be an issue assignee, so App services must use label mode. Comments containing @<bot_username> reply in-thread regardless of mode, and a comment on an issue/MR that already has a task is always delivered to that task's live session. See docs/application-integration.md.

API

All routes under /api/* require Authorization: Bearer $API_BEARER_TOKEN when that env var is set. /webhook/*, /health, /github_app/callback, and /ws sit outside that middleware (/ws authenticates in-band — its first frame carries the token).

The core surface the SPA drives:

GET  /api/tasks                    # list; optional ?task_state= / ?agent_state=
POST /api/tasks                    # operator dispatch: { project_id, trigger } → pending task
GET|PATCH|DELETE /api/tasks/{id}   # detail / edit run inputs / delete (DELETE force-kills if running)
POST /api/tasks/{id}/confirm       # pending → queued
POST /api/tasks/{id}/kill          # SIGKILL, preserving session_id for Resume
POST /api/tasks/{id}/continue      # resume via `claude -r <session_id>`
POST /api/tasks/{id}/fresh_session # clear session_id, restart — next turn is a brand-new claude session
GET  /api/tasks/{id}/diff          # git diff of the worktree
GET  /api/tasks/{id}/events        # persisted agent timeline
GET  /api/auth_requests            # pending operator approvals (?status=, ?task_id=)
POST /api/auth_requests/{id}/resolve
GET  /ws                           # single app-wide live stream (in-band auth)

The full route table — task stats/bulk/token/message/retry, all /api/projects, /api/services (+ github_app/gitlab_token sub-routes), /api/providers, /api/models, /api/queues, and the webhook/callback routes — is the canonical docs/architecture.md#http-surface. Tokens and webhook secrets on git services are write-only — never returned by GET.

Operator approvals

Tool gating runs entirely over the stream-json control protocol on the stdin/stdout the runner already owns — there is no PreToolUse hook, no /internal/authcheck route, and no hook script. --permission-mode default --permission-prompt-tool stdio makes the CLI emit a can_use_tool request for every non-trivially-safe tool. handle_permission (src/jobs/permission.rs) allows edits/reads immediately, matches Bash against the project's allowed_operations glob list, and on a miss (or an AskUserQuestion) opens an auth_requests row, surfaces it to the SPA over the WS, and parks until the operator resolves it. OPERATOR_APPROVAL_TIMEOUT_SECS=0 (the default) waits indefinitely; >0 auto-denies on expiry.

Full protocol — the control_request/control_response framing and the AskUserQuestion answers path — is documented at docs/architecture.md#how-an-operator-approval-works.

Testing

make test runs the Rust suite against both DB backends (Postgres, then a sqlite:// pass — the DB-backed tests select the driver from the DSN scheme) followed by the frontend vitest suite. CI (.github/workflows/ci.yml) runs the whole make verify gate — lint + tests — against a Postgres service on every push.

About

Agent to perform automatic claude triggers

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages