- TypeScript 98.1%
- CSS 0.8%
- Just 0.5%
- Shell 0.3%
- Dockerfile 0.1%
|
|
||
|---|---|---|
| .cursor/skills/forgejo-deps-backfill | ||
| .forgejo/workflows | ||
| .husky | ||
| apps | ||
| design | ||
| docs | ||
| flows/defaults | ||
| ops/audit | ||
| packages/shared | ||
| scripts | ||
| skills | ||
| specs | ||
| vendor/penpot-mcp-server | ||
| .cursorignore | ||
| .dockerignore | ||
| .gitignore | ||
| biome.json | ||
| bun.lock | ||
| claude-hooks.service | ||
| CLAUDE.md | ||
| Dockerfile | ||
| justfile | ||
| package.json | ||
| README.md | ||
| tsconfig.base.json | ||
| tsconfig.json | ||
| turbo.json | ||
claude-hooks
Webhook-driven orchestration service that ingests Forgejo / GitHub / GitLab events, routes them through a flow-graph dispatcher, and spawns AI agent workers inside per-role Docker containers — turning issues into pull requests, and PRs into merged code, automatically.
Architecture
┌──────────────────────────────────────────────────┐
│ Forge (Forgejo / GitHub / GitLab) │
│ system webhook → POST /webhook/forgejo │
└────────────────────┬─────────────────────────────┘
│ HMAC-verified
▼
┌──────────────────────────────────────────────────┐
│ claude-hooks │
│ │
│ ┌────────────┐ ┌──────────────┐ ┌──────────┐ │
│ │ HTTP API │ │ Flow runner │ │ SQLite │ │
│ │ (Bun) │ │ (dispatch) │ │ (state) │ │
│ └────────────┘ └──────┬───────┘ └──────────┘ │
│ │ FIFO queue per agent │
│ ┌──────────────────────┴──────────────────────┐ │
│ │ Worker registry │ │
│ │ code-lead · dev · reviewer · designer │ │
│ │ design-reviewer · architect │ │
│ └──────────────────────┬──────────────────────┘ │
└─────────────────────────│────────────────────────┘
│ docker exec
▼
┌──────────────────────────────────────────────────┐
│ Agent containers │
│ │
│ ┌──────────┐ ┌─────┐ ┌──────────┐ ┌──────────┐ │
│ │code-lead │ │ dev │ │ reviewer │ │ designer │ │
│ └──────────┘ └─────┘ └──────────┘ └──────────┘ │
│ │
│ ┌────────────────────────────────────────────┐ │
│ │ AI provider (per agent, operator choice) │ │
│ │ Anthropic · DeepSeek · Ollama · Cursor │ │
│ └────────────────────────────────────────────┘ │
│ Forge MCP · per-agent credentials │
└──────────────────────────────────────────────────┘
The service runs under systemd user scope (systemctl --user) on the host
machine, listening on port 4500. Every agent — including the architect —
runs in its own long-lived Docker container via docker exec.
Each agent type can be configured to use a different AI provider (Anthropic,
DeepSeek, Ollama, or Cursor) via the dashboard or agents.json. The
orchestration layer is provider-agnostic — only the container's env vars change.
Agent roles
| Role | Forge user | Model | Purpose |
|---|---|---|---|
code-lead |
code-lead |
Opus 4.7 | Senior implementation — architecture-heavy tasks. Owns the breakdown skill. |
dev |
dev |
Sonnet 4.6 | Implementation — well-scoped, straightforward tasks. |
reviewer |
reviewer |
Sonnet 4.6 | Code review. Squash-merges when APPROVED + CI green + mergeable. |
designer |
designer |
Opus 4.7 | UI/UX mockups in Penpot (MCP-connected). |
design-reviewer |
design-reviewer |
Sonnet 4.6 | Visual-defect review of Penpot frames and dashboard PRs. |
architect |
architect |
Opus 4.7 | Planning + spec-writing. Driven via /architect/* chat endpoints. Singleton. Cannot merge PRs. |
Tech stack
| Layer | Choice |
|---|---|
| Runtime | Bun |
| Agent SDK | @anthropic-ai/claude-agent-sdk (programmatic Claude Code) |
| ORM | Drizzle on bun:sqlite |
| Frontend | Vite + React 19 + TanStack + Tailwind 4 |
| Linter / Formatter | Biome |
| CI | Forgejo Actions |
| Monorepo | Bun workspaces + Turbo |
| Agent runtime | Per-instance Docker container |
Workspace layout
apps/
server/ # Bun HTTP service — webhook ingress, flow runner, worker registry
web/ # React 19 SPA dashboard
forge-mcp/ # Forge-neutral MCP server baked into every agent container
packages/
shared/ # Cross-app TypeScript types (TaskRecord, ResolvedAgent, SSE envelopes)
Both apps depend on @claude-hooks/shared via workspace:*. The server boots
on port 4500 and serves the React SPA on /app/*. Requests to / and
/dashboard receive 302 → /app/ so the SPA boots and runs its entry
beforeLoad hook — it probes /onboarding/should-redirect for a fresh fleet,
then redirects to /$locale/planner/board using the active locale from
settings (default en).
Quick start
just setup # bun install at the root — links every workspace
just dev # turbo run dev — server (:4500) + web (:5173) in parallel
just start # run server in production (matches systemd unit)
just qa # typecheck + lint + format check + tests across the workspace
If /app/* returns a 503 with a "run cd apps/web && bun run build" hint,
the SPA bundle hasn't been built yet. just dev builds as a side effect;
just start expects the dist bundle to already exist.
Dev loop (Vite on port 5173, proxies REST/SSE to 4500):
just dev # turbo runs server + Vite together
# then open http://127.0.0.1:5173/app/planner/board
Dashboard
The React 19 + TanStack + Tailwind 4 dashboard is the sole UI. It carries a
hardened SSE reliability path (heartbeat + debounced "disconnected" banner +
/health probe).
SPA pages
All routes live under the locale prefix (/app/<locale>/…; en is the default).
The /$locale/ landing redirects to /$locale/planner/board.
| Path | Description |
|---|---|
/app/planner/board |
Assignment board — drag-to-assign Kanban, one column per agent type + an Unassigned gutter. Live via SSE. |
/app/planner |
Architect chat — sessions list + streaming turns. Slash-command palette (/spec, /breakdown, /assign) and @file / @specs/foo.md autocomplete. |
/app/workspace |
Unified workspace — combined specs editor + architect chat. URL params: ?session=<id>, ?spec=<name>, ?repo=<owner/name>. |
/app/specs |
Spec browser — list and open specs/*.md from watched repos. |
/app/flows |
Flow-graph editor — view, create, and edit dispatch flow definitions. |
/app/agents |
Agent CRUD — create / edit / delete instances with volume-wipe confirm. |
/app/issue/:owner/:repo/:issueNumber |
Issue detail — per-issue pipeline stages, task history, and event log. |
/app/settings |
Settings hub — entry point for all configuration surfaces. |
/app/settings/service |
Service config (forge URL, container image, pipeline thresholds, auth). |
/app/settings/service/watchdogs |
Per-stage stall thresholds. |
/app/settings/service/ai-providers |
AI provider configuration and failover. |
/app/settings/service/forge |
Per-forge OAuth credentials. |
/app/settings/service/container |
Container image and runtime settings. |
/app/settings/service/design |
Penpot / design-reviewer settings. |
/app/settings/agents |
Agent-type config and per-instance overrides. |
/app/settings/agents/per-agent-secrets |
Per-agent secret injection. |
/app/settings/agents/config-history |
Agent config change history. |
/app/settings/agent-types |
Agent-type defaults (models, skills, plugins). |
/app/settings/agent-config |
Global agent configuration. |
/app/settings/repos |
Watched repos — add/remove, rotate webhook secrets. |
/app/settings/labels |
Label catalog management. |
/app/settings/secrets |
Encrypted secret store. |
/app/settings/language |
UI language selection (Paraglide i18n). |
/app/settings/appearance |
Theme / colour-scheme preferences. |
/app/settings/voice-input |
Voice input configuration (requires local speaches service). |
/app/login |
Login page (Authelia-backed). |
/app/onboarding |
First-run wizard — forge credentials, webhook registration, initial agent setup. |
Assignment board
/app/planner/board is the drag-to-assign Kanban view. One column per agent
type, plus a synthetic Unassigned gutter that surfaces open type:user-story
issues without an assignee. Cards group by status (Running → Queued →
Idle-assigned → Unassigned); the column header reports busy/capacity
saturation.
Drag-drop is plain HTML5: dropping a card invokes POST /board/assign, which
PATCHes the Forgejo issue's assignees — Forgejo then fires the normal
issues.assigned webhook so dispatch routing stays in one place. Optimistic
UI: the card moves immediately and rolls back on a non-2xx response. Filters
(repo / milestone / label / only-unassigned) bind to URL search params so
views are shareable. Live updates via /events invalidate the TanStack Query
cache; a 30 s poll backstops idle tabs.
Pipeline stages
The issue detail view (/app/issue/:owner/:repo/:issueNumber) surfaces the
canonical stage pipeline — Breakdown → Implement → PR → CI → Review ↺ → Approved → Merge → Closed — derived from GET /issues/pipeline.
| State | Description |
|---|---|
success |
Stage completed cleanly |
running |
Agent actively working |
pending |
Waiting to start |
failure |
Stage errored |
stalled |
Running/pending past the configured threshold |
round > 1 |
Reviewer REQUEST_CHANGES loop |
force-merge |
Terminator fired after max review rounds |
Per-stage stall thresholds are configurable in Settings → Service → Watchdogs
(ci_threshold_ms default 15 min, review_threshold_ms default 10 min,
implement_threshold_ms default 30 min).
Quick actions for a stalled PR:
- Bounce — POSTs to
/pipeline/bounce-review; the server DELETE + re-POSTs every requested reviewer on the PR so Forgejo fires a freshpull_request_review_requestevent and normal routing takes over. Auth-gated (operator-only). - Re-trigger CI is a deep-link to the Forgejo Actions run, not an auto-retrigger — safety rail.
Containerised workers
Each code-flow agent runs inside a dedicated long-lived Docker container:
| Container | Volume | Purpose |
|---|---|---|
claude-hooks-code-lead |
claude-hooks-code-lead-state |
Code Lead agent |
claude-hooks-dev |
claude-hooks-dev-state |
Dev agent |
claude-hooks-reviewer |
claude-hooks-reviewer-state |
Reviewer agent |
claude-hooks-designer |
claude-hooks-designer-state |
Designer agent (Penpot MCP) |
claude-hooks-design-reviewer |
claude-hooks-design-reviewer-state |
Design-reviewer agent (Penpot MCP) |
The designer / design-reviewer containers embed the Penpot MCP server.
Code-only agents (code-lead / dev / reviewer) deliberately do not
embed it.
Credentials and isolation
- Agent code (Claude Agent SDK) runs inside the container via
docker exec. - Each container has an empty
$HOME— no host credentials are visible inside. - The Forge token for each agent is injected as
FORGEJO_ACCESS_TOKENat container start, read from~/.config/claude-hooks/tokens/<name>on the host. Token files are never bind-mounted into containers. - Claude Code OAuth credentials are bind-mounted read-only from a dedicated host
path into
$CLAUDE_CONFIG_DIR/.credentials.jsoninside the container. Rotating credentials on the host propagates without a container restart.
State persistence
Each agent's cache/, worktrees/, and session data live in the named volume.
Worktrees and sessions survive container restarts; an in-flight task will fail on
a container stop/start and can be retried.
Forge MCP (apps/forge-mcp/)
forge-mcp is the MCP server baked into every agent container. It replaced the
upstream forgejo-mcp Go binary and is compiled to a standalone binary at
/usr/local/bin/forge-mcp via bun build --compile.
Provider-neutral by design. Every tool is named mcp__forge__<verb>
regardless of which forge backs the dispatch. The right adapter is selected at
container start from FORGE_TYPE — agents never branch on forge type.
agent (AI CLI)
└── stdio JSON-RPC
└── forge-mcp (mcp__forge__* tool surface)
└── ForgePort adapter (forgejo / github / gitlab)
└── REST API
The same ForgePort + adapter code used by the service's own dispatch path
backs the MCP — one implementation, no duplicated REST logic.
Tool surface (full list in apps/forge-mcp/src/tools.ts):
| Category | Tools |
|---|---|
| Issues | get_issue, list_issues, create_issue, update_issue, add_issue_labels, remove_issue_labels |
| Comments | list_comments, create_comment, get_comment, edit_comment, delete_comment |
| Pull requests | get_pull_request, list_pull_requests, create_pull_request, update_pull_request, get_pull_request_diff, list_pull_request_files, merge_pull_request |
| Reviews | list_reviews, get_review, submit_review, dismiss_review, list_review_comments, request_reviewers, remove_review_request |
| Files | read_file, list_dir, write_file |
| CI | list_workflow_runs, get_workflow_run, get_aggregate_ci_status, get_job_logs |
| Dependencies | get_blockers, get_blocked, add_blocker |
| Labels / milestones | list_repo_labels, create_label, list_repo_milestones |
| Account | list_my_repos |
See docs/forge-mcp.md for the full tool reference and
migration guide from the legacy mcp__forgejo__* namespace.
Architect agent
The architect is a singleton planning + spec-writing agent that runs in the
claude-hooks-architect-default container via docker exec, the same mechanism
as every other agent. It writes specs, dry-runs /breakdown, and hands work off
to the code-lead / dev / reviewer pool.
File operations (spec reads/writes, attach picker) go through the Forge REST adapter — the architect never touches the host filesystem directly. All orchestration (HTTP endpoints, SSE, SQLite session table, steer / abort channels, tool policy) lives in the service process; the Claude Agent SDK subprocess runs inside the container.
What it cannot do (enforced by canUseTool in apps/server/src/domain/agent/architect.ts):
- Push directly to
main/master—git push origin mainpatterns in a Bash tool call are denied. - Merge PRs —
mcp__forgejo__merge_pull_requestis refused outright.
Dispatch surface. Forge webhook paths are blocked from reaching the
architect — resolveAgentByUser / resolveAgentByType skip non-dispatchable
types, so assigning architect as an issue assignee is a silent no-op. The
only entry point is the /architect/* chat endpoints.
Chat API:
| Method | Path | Description |
|---|---|---|
| POST | /architect/chat |
{ session_id?, message } — one turn. Returns { session_id, task_id }. |
| GET | /architect/sessions |
List sessions (id, title, turn count, preview). |
| GET | /architect/sessions/:id |
Full transcript (user + assistant messages). |
| DELETE | /architect/sessions/:id |
Drop a session permanently. |
| GET | /architect/stream/:task_id |
SSE stream filtered to the given task's events. |
Example — start a new session, stream the response:
curl -s -X POST http://localhost:4500/architect/chat \
-H 'Content-Type: application/json' \
-d '{"message":"Draft specs/my-feature.md from these notes…"}'
# → { "session_id": "<uuid>", "task_id": "<uuid>", "worker": "architect-default" }
curl -N http://localhost:4500/architect/stream/<task_id>
# → streams: init → progress → tool_call → assistant → result
Provisioning on first deployment:
-
Create the Forge account (
architect), issue a personal access token withwrite:repositoryscope, and write it to~/.config/claude-hooks/tokens/architect(mode0600). -
Seed the architect's Claude Code credentials:
just agent-env-sync architect -
Restart the service —
ensureDefaultForTypescreates thearchitect-defaultrow automatically on first boot.
Singleton guarantee: POST /agents refuses a second architect instance
with 409 Conflict. The first row is seeded automatically.
Multi-forge & multi-repo
The service watches every row in the watched_repos table (editable via
Settings → Repos). Every webhook payload's repository.full_name is matched
against that list — unknown repos are rejected with 404. Each row carries
owner/name and a forge enum (forgejo / github / gitlab).
An empty watched_repos table is the back-compat escape hatch — the guard is
skipped and every repo is accepted. New deployments should always populate it.
Adding a repo
-
Ensure the agents' Forge tokens can see the repo. Tokens are read from
~/.config/claude-hooks/tokens/<type>at container start. -
Add the repo via Settings → Repos → Add with the matching forge enum.
-
Bootstrap labels:
just labels-bootstrap <owner>/<repo> # ad-hoc, no restart needed -
Register the webhook on the new repo pointing at
http://<host>:4500/webhook/forgejowith the shared webhook secret. -
Verify routing: assign an issue to an agent user (e.g.
dev). The service logs should show[webhook] event=issues action=assigned repo=<owner>/<repo>.
Label-based routing
issue labelled area:design → designer
designer posts handoff comment
→ issue gets area:design-review → design-reviewer
PR labelled area:dashboard → design-reviewer (beats author mapping)
everything else → assignee-based routing
Tickets must not carry area:design / area:design-review if they are
implementation tasks — use area:agents or a relevant functional label instead.
Design review flow
Two triggers, one agent (design-reviewer), one skill (skills/design-review.md):
-
Dashboard PR review — trigger: PR labelled
area:dashboardor authored bydesigner. The skill reads the PR diff and verifies every hex literal in the changed files againstdesign/tokens.json. Verdict ships as a Forge pull review (APPROVEDorREQUEST_CHANGES). -
Penpot handoff review — trigger: issue labelled
area:design-reviewafter adesignerhandoff comment. The skill exports every Penpot frame as PNG, inspects each visually, posts a grouped-findings comment, and removes thearea:design-reviewlabel.
Systemd service
ExecStartPre → just agents-sync (reconcile fleet to SQLite agents table)
ExecStart → bun run apps/server/src/main.ts
ExecStopPost → just containers-down (stop all agent containers)
agents-sync walks every row in the agents SQLite table and brings Docker in
line: creates missing containers, removes orphans, recreates any whose image or
credentials source drifted.
Creating a new agent instance
Creating a row from the dashboard triggers reconcileOne automatically — within
seconds the container and state volume are created. If you bypass the dashboard
(direct SQL insert, service down):
just agents-sync # idempotent, safe to run while the service is up
Graceful shutdown
systemctl --user restart claude-hooks (or any SIGTERM / SIGINT) triggers
the graceful-shutdown handler before systemd's grace timer expires:
- Closes
Bun.serve— new requests are refused at the TCP layer. - Waits up to
shutdown.drain_ms(default 60 s) for in-flight tasks. - On timeout, calls
abort()on every still-busy worker and marks itstask_historyrowcancelledwith reasonshutdown. - For container workers, signals the in-container Claude CLI to stop.
Tune the drain budget via Settings → Service:
{ "shutdown": { "drain_ms": 120000 } } // clamped to [1000, 270000] ms
TimeoutStopSec=300 gives the process up to five minutes before systemd
force-kills it.
Manage
just install # copy unit, daemon-reload, enable, start
just status # systemctl --user status claude-hooks
just logs # journalctl --user -u claude-hooks
just uninstall # stop, disable, remove unit
Container management
just containers-up # start all agent containers
just containers-down # stop all (volumes persist)
just containers-rebuild # pull latest image, recreate all containers
just containers-rebuild dev # recreate only the dev container
just containers-logs # last 50 lines from every container
just containers-logs dev # tail -f logs for dev
just containers-shell dev # docker exec -it bash into dev
First-time container setup
-
Provision per-agent credentials:
mkdir -p ~/.config/claude-hooks/claude-credentials cp ~/.claude/.credentials.json \ ~/.config/claude-hooks/claude-credentials/.credentials.json chmod 600 ~/.config/claude-hooks/claude-credentials/.credentials.json -
Build the image locally (if no published image yet):
docker buildx build --platform linux/amd64 -t <registry>/<owner>/claude-hooks:latest . -
Create and start a container:
just containers-rebuild reviewer -
Smoke-test:
just containers-smoke reviewer -
Enable container mode for the agent type in Settings → Agent types → reviewer — flip
agent_type_container.enabledtotrue. -
Restart the service.
Token rotation
echo '<new-token>' > ~/.config/claude-hooks/tokens/<name>
just containers-rebuild <name> # only that container is recreated
Patched forgejo-mcp in the image
The image builds forgejo-mcp from source with local patches under
patches/. They fix upstream bugs where
merge_pull_request silently returns success on HTTP rejects and
update_issue drops the assignee argument. The patched binary reports
version ${FORGEJO_MCP_VERSION}+claude-hooks.<n>. Remove the patches once
upstream ships the fixes.
Security
Claude-hooks trusts an external Authelia instance (or any proxy that sets
Remote-User) rather than a homegrown token scheme.
- Mutating endpoints (
/task,/cancel,/breakdown,/agentsPOST/PATCH/DELETE,/architect/*) require an authenticated operator. - Read-only monitor endpoints (
/health,/events,/queue,/history,/stats,/usage) remain open on the LAN.
Reverse proxy setup
Create a vhost in your proxy stack pointing at the service and pass requests through Authelia:
server {
server_name claude.<your-domain>;
location / {
auth_request /authelia;
auth_request_set $user $upstream_http_remote_user;
proxy_set_header Remote-User $user;
proxy_pass http://<host>:4500;
proxy_set_header Host $host;
}
location = /authelia {
internal;
proxy_pass http://127.0.0.1:9091/api/verify;
}
}
Authelia access control:
rules:
- domain: claude.<your-domain>
policy: one_factor
subject: "user:<username>"
Service-side auth config
Edit Settings → Service or write a service_config row directly:
{
"auth": {
"operator_user": "<username>",
// IPv4 CIDRs of the reverse proxy that sets Remote-User (must be non-empty)
"trust_proxy": ["10.0.0.0/24"],
"authelia_logout_url": "https://auth.<your-domain>/logout"
}
}
Without authentication: omit the auth block to leave mutating routes open
on the LAN. Add the block only after the vhost + Authelia rule are in place.
How Remote-User is trusted
Remote-User is a plain HTTP header — any LAN client could forge it. The
server counters this by verifying the TCP connection source IP against
trust_proxy before honouring the header. User-supplied x-claude-client-ip
headers are stripped first so external callers cannot spoof a trusted IP.
Verify the chain:
curl http://localhost:4500/whoami
# → {"user":null,"auth_enabled":true,"logout_url":"https://auth.<your-domain>/logout"}
Debugging with auditd
When a claude-hooks-* container vanishes without a service-side
reconcileOne call, the repo ships an auditd rule that logs every docker CLI
invocation by uid 0 or uid 1000 with its PID, argv, exe, and cwd, tagged
claude-hooks-docker.
Requires the audit package and a running auditd service.
just audit-install # install rule (requires sudo)
just audit-tail # follow new events in near real time
just audit-uninstall # remove rule and reload
One-shot inspection:
sudo ausearch -k claude-hooks-docker -ts today -i
API reference
Core task
| Method | Path | Description |
|---|---|---|
| POST | /task |
Submit a coding task (returns task_id) |
| GET | /task/:id |
Get task result (success/failure/running/queued) |
| POST | /task/:id/steer |
Mid-flight operator steering — inject a message into the running agent |
| POST | /task/:id/redispatch |
Re-dispatch a failed or cancelled task |
| GET | /health |
Service health + queue depth |
| GET | /queue |
Current task + queued tasks per worker |
| POST | /cancel |
Abort the running task |
| GET | /history |
Recent task history (last 50, in-memory) |
| GET | /storage |
Disk usage for cache clones / worktrees / sessions |
| GET | /events |
SSE stream of real-time task events |
| POST | /reset |
Drop worktree + session for a (agent, repo, issue) tuple |
| POST | /sweep |
Run one pass of the expiration sweeper on demand |
Agent management
| Method | Path | Description |
|---|---|---|
| GET | /agents |
List every SQLite row + type defaults + live worker state |
| POST | /agents |
Create a new instance row + reconcile container |
| PATCH | /agents/:name |
Update override fields + reconcile container |
| DELETE | /agents/:name |
Cancel task, remove container, drop row |
| GET | /agents/health |
Aggregate fleet saturation, queue depth, cost |
| GET | /agents/:name/provider-events |
Failover ledger for one instance |
| POST | /agents/:name/reset-tier |
Reset to tier 1, clear paused, clear failure metadata |
| POST | /agents/:name/pause |
Pause the agent (no new tasks accepted) |
| POST | /agents/:name/unpause |
Resume a paused agent |
| GET | /agents/models |
Provider-supplied model catalogue (?provider=<id>) |
Breakdown
| Method | Path | Description |
|---|---|---|
| POST | /breakdown |
Run the breakdown skill on a specs/*.md file |
POST /breakdown dispatches the breakdown skill against a spec document
(specs/<name>.md) in a watched repo. The code-lead pool turns each logical
## section into a type:user-story issue.
curl -X POST http://localhost:4500/breakdown \
-H 'Content-Type: application/json' \
-d '{"repo":"<owner>/<repo>","spec_path":"specs/my-feature.md","tracking_issue":47}'
| Field | Required | Description |
|---|---|---|
repo |
yes | Must match a row in watched_repos |
spec_path |
no | Defaults to specs/ (walk all .md) |
tracking_issue |
no | Issue on which the skill posts its summary comment |
milestone |
no | Milestone title attached to every created issue |
dry_run |
no | true ⇒ post proposed issues as a checklist, don't create |
max_issues |
no | Per-dispatch cap; defaults to 15 |
Response: 202 { task_id, worker, repo, tracking_issue }.
The same guardrails apply to the webhook /breakdown slash command — posting
a comment starting with /breakdown on any watched-repo issue dispatches the
same skill. Trailing args: positional (/breakdown specs/foo.md) or key=value
(/breakdown spec_path=specs/foo.md milestone="v1.0").
Stats and pipeline
| Method | Path | Description |
|---|---|---|
| GET | /stats |
Cost / turn / success-rate aggregates |
| GET | /usage |
Token-consumption rollup against the weekly quota |
| GET | /issues/pipeline |
Per-issue canonical stage model |
| GET | /issues/ready |
Open issues with all blockers closed + no /hold |
| GET | /issues/deps |
k-hop dependency DAG for one issue |
| POST | /pipeline/bounce-review |
Rekick a stalled reviewer assignment |
Watchdog
| Method | Path | Description |
|---|---|---|
| GET | /watchdog/status |
Dead-letter PRs and watchdog state |
| POST | /watchdog/retry/:repo/:number |
Remove a dead-letter entry (auth-gated) |
| POST | /watchdog/snooze/:repo/:number |
Snooze a dead-letter PR for 24 h (auth-gated) |
| GET | /watchdog/recovery/diff |
Git diff for a stash branch (auth-gated) |
| DELETE | /watchdog/recovery |
Force-delete stash branch (auth-gated) |
Settings
| Method | Path | Description |
|---|---|---|
| GET | /settings |
Read service settings |
| PATCH | /settings |
Update service settings |
| GET | /whoami |
Inspect what the server sees for auth |
| GET | /watched-repos |
List all watched repos |
| POST | /watched-repos/:owner/:name/rotate-secret |
Rotate the webhook secret for a repo |
Recipes reference
just dev # Run service with hot reload
just start # Run in production
just qa # Typecheck + lint + format check + tests
just agents-sync # Reconcile per-agent containers vs. SQLite agents table
just containers-rebuild [NAME] # Pull/build image and recreate containers (one or all)
just deps-backfill REPO [TOKEN] # Promote prose-only issue dependencies to native Forge API
just redispatch-interrupted # Re-queue tasks stuck in 'running' after an unclean restart
just flows-apply FILE # Apply a flow-graph definition file to the service
just render-agent-env [NAME] # Print the resolved env for one (or all) agent instances
just oauth-register-forgejo # Register OAuth app credentials for a Forgejo instance
just oauth-register-github # Register OAuth app credentials for GitHub
just audit-install # Install auditd rules that log docker stop/rm on containers
just audit-tail # Follow new events tagged claude-hooks-docker
just audit-uninstall # Remove the auditd rules and reload
Migration note
If you have an existing installation using the on-host cache (pre-containerisation), that state is not migrated into Docker volumes. Volumes start empty and are populated as agents pick up new tasks.
systemctl --user stop claude-hooks
rm -rf ~/.cache/claude-hooks # abandon old state
just containers-rebuild # create containers (image + tokens + credentials)
just install # install updated unit
Release pipeline
Tag pushes (v*) fire .forgejo/workflows/release.yml. The pipeline:
- Builds both arch binaries and attaches them to a Forgejo release.
- Builds + pushes a multi-arch container image.
- Runs runtime smoke tests (
bun,git,forgejo-mcp,claude --version) plus a credential-file audit against the just-pushed image.
On failure the image tags are deleted from the registry and the release is flipped to prerelease with a "DO NOT USE" banner.
The publish-image job requires Docker daemon access and is gated on a
dedicated release-only runner label (runs-on: [docker, release]). PR CI
(qa.yml) stays on plain docker and cannot land on the socket-mounted runner.
See docs/runner-setup.md for runner registration,
config.yml snippets, and the release-candidate dry-run procedure.
Further reading
| Doc | Topic |
|---|---|
docs/api.md |
Complete HTTP route reference |
docs/agents-architecture.md |
Agent types vs. instances |
docs/architect.md |
Host-mode architect — gate, dispatch, security rails |
docs/containers.md |
Container reconciliation + watchdog |
docs/credentials.md |
Credentials mount + agent-env-sync |
docs/database.md |
Drizzle + bun:sqlite, migration runner |
docs/label-routing.md |
Label routing + pipeline stall thresholds |
docs/design-review.md |
Design review flow detail |
docs/providers.md |
AI provider failover + routing |
docs/multi-repo.md |
Multi-repo dispatch gate + bootstrap |
docs/multi-forge.md |
Multi-forge (Forgejo / GitHub / GitLab) |
docs/configuration.md |
Service configuration reference |
docs/breakdown.md |
Breakdown skill detail |
docs/dependencies.md |
Issue dependency propagation |
docs/reviewer-instances.md |
Specialised reviewer pool |
docs/plugins.md |
Per-agent Claude Code plugins |
docs/penpot.md |
Penpot MCP integration |
docs/forge-mcp.md |
Forge MCP (agent-side) |
docs/workspace.md |
Workspace + web app detail |
docs/shutdown.md |
Graceful shutdown |
docs/i18n.md |
i18n (Paraglide) |
docs/modules.md |
Every file in apps/server/src/ |