AI Decision System
Replayable, offline-verifiable records for AI-assisted go/no-go decisions: experiment promotions, typed model answers and human-approved actions.
Fabrik이 GitHub 보드를 감시해 Claude Code를 설정 가능한 SDLC 파이프라인으로 자동 실행합니다
Fabrik watches your GitHub Project board and drives Claude Code through a configurable SDLC pipeline. File an issue, drag it across the board, let the factory run.
개발 도구Go · ShellGitHub @handarbeit온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
Drive Claude Code through an SDLC pipeline on your GitHub Project board — Specify → Research → Plan → Implement → Review → Validate. Human in the loop, or on it. A Go CLI for automated, issue-driven development. Docs → fabrik.handarbeit.io
저장소 README에서 가져옴
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Automated Claude Code SDLC driver powered by GitHub Issues and Projects.
Fabrik watches a GitHub Project board and drives Claude Code through configurable
workflow stages. Issues are the unit of work — the issue body is the spec, comments
are user input, and the board columns define the workflow.
📖 Documentation, guides, and examples → fabrik.handarbeit.io
Requires Go 1.26.1+, the Claude Code CLI, and a GitHub token with repo and project scopes.README 전문 보기 ↗Automated Claude Code SDLC driver powered by GitHub Issues and Projects.
Fabrik watches a GitHub Project board and drives Claude Code through configurable workflow stages. Issues are the unit of work — the issue body is the spec, comments are user input, and the board columns define the workflow.
📖 Documentation, guides, and examples → fabrik.handarbeit.io
Requires Go 1.27.3+, the Claude Code CLI, and a GitHub token with repo and project scopes.
# Install
go install github.com/handarbeit/fabrik@latest
# Initialize stage configs, plugin, and project config template
# Pass your GitHub Project URL to auto-populate owner, project, and owner_type:
fabrik init --user you https://github.com/orgs/your-org/projects/5
# Or run without a URL for full interactive prompts (TTY) / blank template (non-TTY)
fabrik init
# Creates .fabrik/stages/, .fabrik/plugin/, and .fabrik/config.yaml
# Edit .fabrik/config.yaml with your project settings (commit this file)
# Add your GitHub token to a gitignored .env:
echo 'FABRIK_TOKEN=ghp_...' >> .env
echo '.env' >> .gitignore
# Run (settings come from .fabrik/config.yaml)
fabrik
# Or override specific values with flags
fabrik --stages ./.fabrik/stages --yolo
Configuration summary:
.fabrik/config.yaml — non-secret project settings (commit to git).env — secrets only (FABRIK_TOKEN / GITHUB_TOKEN; must be gitignored)CLI flag > env var > .env > config.yaml > defaultsUse --auto-upgrade to have Fabrik self-upgrade at startup and when idle:
fabrik --auto-upgrade ...
At startup and after 2 idle polls, Fabrik upgrades itself automatically and re-execs:
go build): detects local or remote commits
ahead of the running binary and rebuilds in place from origin/main.When Fabrik is upgraded, it also compares your .fabrik/stages/*.yaml files against the embedded defaults and prints a startup warning for any stage file that is missing fields added in the newer binary. Run fabrik refresh-stages --apply to add the missing keys, then review with git diff and commit. See Stage YAML Drift Warning in the User Guide for details.
Install the Fabrik PM plugin in your interactive Claude Code session and Claude answers "what's the status of #42?", "what needs me?" and "is the pipeline healthy?" from your running Fabrik daemon — column, time in column, worker, PR and CI, blockers and what the engine will do next — instead of scraping GitHub.
Run these in any Claude Code session (the fabrik binary must be on your PATH):
/plugin marketplace add handarbeit/fabrik
/plugin install fabrik@fabrik
fabrik init prints the same two commands. After install you get:
fabrik mcp: fabrik_status, fabrik_board, fabrik_health and the rolled-up fabrik_situation for state, plus audited overseer actions (promote, advance, set autonomy, re-run Validate, clear a Claude usage-limit hold). No action lifts a pause; a human comment does.--subscriber, FABRIK_SUBSCRIBER or CLAUDE_TOPIC set (the OS-username default names audit comments but starts no push stream), fabrik_subscribe / fabrik_unsubscribe have the daemon push events into your session as they happen (a settled Validate, a pause, a stalled poll loop, a merge) instead of you polling.fabrik:fabrik skill — auto-activates when .fabrik/ is detected or Fabrik is mentioned: the PM mental model, the attention states and the steering labels.fabrik:fabrik-setup skill — one-time onboarding walkthrough for setting up Fabrik from scratch./fabrik:status command — what needs you, what is in flight and daemon health, from the daemon.Each Fabrik release pins the marketplace to its own tag and sets the plugin's version to the binary's, so you only ever get released changes; a session-start notice tells you if your plugin and binary versions differ. See Install the Fabrik PM Plugin in the User Guide for settings, auto-update and the optional push-events opt-in.
Distinct from
fabrik-stages: the engine's stage skills are embedded in the binary and deployed automatically to.fabrik/plugin/byfabrik init. You do not — and should not — install them manually, and Fabrik keeps the PM plugin out of every worker it spawns.
GitHub Project Board (source of truth)
| GraphQL poll
Fabrik (Go CLI, runs locally)
| stage config match
Claude Code (invoked per stage, in isolated worktree)
| output
GitHub Issue comments + labels + body updates
fabrik/issue-N branch).stage:<name>:complete.Big-board efficiency (v0.0.57): On large project boards, the per-poll GraphQL cost drops ~5–10× via a lightweight
updatedAt-only probe that gates the full deep-fetch — only items that changed since the last poll trigger a full query. Terminal items (issues in a cleanup/Done column withstage:<name>:completeand no active lifecycle labels) are skipped entirely from both the probe and deep-fetch evaluation.Cold-start optimization (v0.0.58): On startup, closed Done items are seeded from probe data as terminal without a deep-fetch, reducing cold-start GraphQL cost by ~80–90%. Combined with probe-driven polling, this makes running two Fabrik instances on a single token budget practical.
Anthropic auth namespace scrub (v0.0.77): Every worker invocation's environment is scrubbed of the Anthropic/Claude-Code auth namespace by default (
ANTHROPIC_*wildcard, plus an enumeratedCLAUDE_CODE_*auth-selector list) — an ambientANTHROPIC_API_KEYin the engine's environment can no longer silently redirect a stage from subscription billing to metered API billing.FABRIK_ANTHROPIC_API_KEYis the only supported opt-in for API billing;apiKeyHelperis refused outright, both at startup and per-invocation. See Anthropic Auth Namespace Scrub &apiKeyHelperRefusal in the User Guide.GitHub Enterprise Server support (v0.0.78): Fabrik can run against a GHES instance instead of github.com — pure endpoint/host configuration, no adapter layer. Set
--ghes-host/FABRIK_GHES_HOST(orghes_hostin.fabrik/config.yaml) and Fabrik independently derives the REST (/api/v3/...) and GraphQL (/api/graphql) endpoints, points bare-clone URLs and commit noreply emails at your host, and propagatesGH_HOSTto every stage worker'sghCLI calls so they target the same instance. Absent any GHES configuration, behavior is unchanged. See GitHub Enterprise Server Support in the User Guide.v0.0.84 highlights: an overseer MCP server (
fabrik mcp, bundled in the PM plugin) lets a Claude Code session read board and daemon state from the running daemon with no GitHub calls, act through audited tools, and receive pushed events — see . Stage skills are addressed as (); the built-in plugin is now , and moves your edits to — see and . The gains singleton catch-up, red-singleton auto-repair, overlap-aware batching and restart-safe runs. Three display-only board fields (, , ) show what each item is doing — add them with () — and a TUI warnings panel lists operator-level faults. Upgrading? Follow the ordered steps in ; GitHub App installations now require .
v0.0.85 highlights: a stage now runs before a comment that was waiting when the item entered it, so the stage's skill and stage comment are never skipped by a comment pass — see Steering with Comments. SIGHUP (or
ctrl+r) is a graceful restart: the first signal stops starting new work and waits for running workers to finish, and a second restarts now — see Restarting. The TUI's In Progress panel lists items Fabrik is waiting on (CI, reviews, a retry, a rebase, the merge train, a human reply) instead of saying "nothing to do". No configuration changes are needed; a SIGHUP restart can now take as long as the longest running worker.
v0.0.86 highlights: a bug-fix release that makes several gates stricter and safer. A cancelled (or stale) check no longer counts as a pass at the CI gate or in the merge train — it is re-run once, then treated as a CI failure (stage) or an uncharged infrastructure abandon (train) — see CI Gate and CI-Fix Workflow. The train also rebuilds a trial whose base moved mid-CI instead of blaming the members, and waits out a transient
blockedstate after marking a green trial ready. A diverged issue branch pauses instead of being force-pushed (details), an unknown merge state pauses after 30 minutes (--merge-state-unknown-timeout), and a denied edit Fabrik cannot grant pauses on its first occurrence.CLAUDE_CONFIG_DIRnow has one precedence order for Fabrik and Pruefer (Alternate Claude Profile), and dev-build self-upgrade requires an exactgithub.comorigin. No configuration changes are needed.
v0.0.87 highlights: the
fabrik mcpsubscriber name now resolves as--subscriber, thenFABRIK_SUBSCRIBER, thenCLAUDE_TOPIC, then your OS username (blank values and unexpanded${...}placeholders count as unset), so overseer actions work with no configuration. The PM plugin no longer has a Subscriber name setting — a plugin-wide value was shared by every session and made them supersede each other; an old value is no longer read.FABRIK_SUBSCRIBERorCLAUDE_TOPICstill start the push stream, but the OS-username default names audit comments without starting one — see Install the Fabrik PM Plugin. No configuration changes are needed.
v0.0.88 highlights: two themes. Pruefer reviews through plugins and reads the whole PR: the default review is an embedded
pruefer-review:reviewskill, operators can load their own review plugins and select one byplugin:skill(a skill that fails to load falls back to the default, and the posted review says so), repo-supplied plugins are available but off by default behind three opt-ins, review tools are coded within an operator ceiling and outside authors get a read-only review, and a coverage resume loop (max_coverage_passes, default 2,0disables) resumes the review until every staged diff entry is read — each extra pass spends Claude quota. Transcripts now live in.pruefer/transcripts/(kept 7 days), not the Claude profile — see Pruefer review notes. The engine closes state-audit gaps:fabrik:pausednow wins at every automatic advance and landing, including the merge train; every merge is pinned to the head the gates judged and only the head Validate verified lands; and the Validate completion SHA survives a restart, so a later push still re-runs Validate — see SHA-change Auto-Revalidation and Steering with Comments. Upgrading Pruefer: areview_toolsrule that is now refused (for example a test runner or a shell-capable tool) stops Pruefer from starting at startup, so remove or narrow it first. Fabrik engine operators need no configuration changes.
v0.0.89 highlights: three themes. The engine stops looping on work it cannot finish: a stage that finished but could not post its output is recorded and published by retry instead of being run again, no worker starts while the
fabrik:editinglock label cannot be written or while GitHub is unreachable, and one stage of one item is dispatched at mostmax_stage_dispatchestimes (default 25) before the item is paused — see Recorded stage outcome and publication retry. State holds up under webhooks, restarts and concurrent writes: an older read never overwrites a newer fact, merge-train caps and the usage-limit hold survive a restart, all webhook transports share one intake with persisted delivery dedupe, and more webhook events are applied directly (issue_dependencies, sub-issues, review threads, labels, milestones, comment edits and deletes) — see Enabling Webhook Mode and In-Memory Board Cache. A new overseer action:fabrik_set_blocked_byadds or removes blocked-by edges from an MCP session, refusing self-blocks, pull-request blockers and cycles. Upgrading: an item is now paused after 25 dispatches of one stage — raise it with--max-stage-dispatches/FABRIK_MAX_STAGE_DISPATCHES/max_stage_dispatches, or set0to disable; a negative or non-integer value is refused at startup (see Retry and Escalation). A webhook you created yourself (an organisation or repository hook, a GitHub App subscription or a Hookdeck source) keeps its own event list: addsub_issues,pull_request_review_thread,labelandmilestoneto receive the new facts directly, otherwise Fabrik learns them from its normal fetches.
v0.0.90 highlights: one theme, webhooks without a relay. Under GitHub App authentication the daemon can now receive App webhooks itself:
event_source: directis a plain HTTP receiver you place behind your own tunnel or proxy, andevent_source: tailscaleembeds a Tailscale node so each daemon joins your tailnet as its own machine and serves its own Funnel endpoint — see Direct App Webhook Receiver and Embedded Tailscale Funnel Receiver. Nothing is lost while the daemon is down: on start and on every reconnect the two receivers find deliveries GitHub failed to deliver and ask it to redeliver them; duplicates are dropped by delivery ID (replay_failed_deliveries, default on). Webhook health you can read:fabrik_healthreports, per receiver, whether GitHub can reach it (route:reachable,unreachablewith GitHub's reason, orunknown) and counts the deliveries the intake accepted, dropped and applied (webhook.intake); a route unreachable for 10 minutes raises thewebhook_route_unreachablewarning. Upgrading: nothing changes for poll-only setups apart from the binary, which is now about 35 MB because every build carries the Tailscale node, and building from source needs Go 1.27.3. The receivers are opt-in and need App auth: setevent_sourcetodirect(withdirect_listen) ortailscale(withtailscale_hostnameand, on first start, a Tailscale auth key inFABRIK_TS_AUTHKEY), and put the App's webhook secret inFABRIK_GITHUB_WEBHOOK_SECRET. Replay is on by default for those two receivers only; setreplay_failed_deliveries: falseto turn it off. Hookdeck is deprecated in v0.0.90 and removed in v0.0.92 (event_source: hookdeckworks unchanged until then): if you use it, move totailscaleordirectby following Migrating off Hookdeck in the User Guide.
Fabrik can receive GitHub events in near-real-time (within ~2 seconds) instead of waiting for the next poll tick by spawning gh webhook forward as a background subprocess. Enable with --webhooks. With the default in-memory board cache active, the webhook stream also drives a reconcile-based health check (default every 3 minutes) that reduces full-board polling to at most once every 60 minutes.
Single-user constraint: GitHub allows only one active
gh webhook forwardsubscription per repository or organization at a time. Coordinate across your team so only one Fabrik instance runs with--webhooks; other instances can run without it and will still process issues via polling. See §10 of the User Guide for full setup, configuration, and troubleshooting details.
GitHub App auth:
--webhookscannot be combined with GitHub App authentication. App-auth setups — including multi-repo ones — can get near-real-time delivery withoutgh webhook forwardin two ways: through an embedded Tailscale Funnel node (--event-source tailscale, each daemon serving its own endpoint), or by receiving App webhooks directly behind your own tunnel or proxy (--event-source direct); polling stays the correctness backstop in every case. See Embedded Tailscale Funnel Receiver and Direct App Webhook Receiver. The Hookdeck relay (--event-source hookdeck) is deprecated in v0.0.90 and removed in v0.0.92; see Migrating off Hookdeck.
On a repo with strict branch protection, landing several ready PRs serially produces an O(N²) rebase-and-retest cascade — each merge invalidates every other ready PR's "up-to-date + green" status, forcing a rebase and a full required-check re-run before the next one can land. Fabrik solves this by batching ready PRs and validating the combined result once instead of retesting each one against every prior merge.
Two landing engines share one Queued board column, and Fabrik picks the engine per repo:
--merge-queue off.(repo, base) partition — a single repo can run multiple concurrent trains when base:<branch> items target different branches — runs one combined Validate per partition, and lands the batch atomically; a red batch is isolated via halving bisection so a single poisoner doesn't block the rest. When exactly one item is queued for a partition, Fabrik skips the trial and lands that item's own PR directly once it's a fast-forward of its base, mergeable, and CI is green. It is opt-in via --merge-train on, FABRIK_MERGE_TRAIN=on, or merge_train: on in .fabrik/config.yaml.Native queue routing defaults to auto (it only engages on repos where GitHub's native queue is already enabled); the merge train defaults to off. Both leave the existing serial auto-merge path untouched unless explicitly turned on. Since v0.0.84 a lone queued PR that is merely behind base is caught up in place rather than re-validated in a trial (singleton_catch_up), a red lone PR is sent back through Validate once before it is paused (max_train_auto_repair_attempts), PRs touching the same path are kept out of the same batch (list lockfiles and changelogs in merge_train_overlap_ignore), and trials survive a restart. These are on by default when merge_train is on; singleton_catch_up: off and max_train_auto_repair_attempts: 0 turn the first two off.
See Merge Queue and Merge Train / Queued in the User Guide for setup (including the required Queued board column) and tuning knobs.
When anyone comments on an issue in an active stage:
fabrik:editing label to lock the issuefabrik:editing labelComments from all authors trigger processing — human colleagues, code-review bots
(Copilot, Gemini), and other Fabrik instances alike. Only two categories are skipped:
comments that start with 🏭 **Fabrik (Fabrik's own output) and comments that already
carry a 🚀 rocket reaction (already processed).
If you comment on an item before it reaches a stage, the stage runs first and one comment pass applies your comment afterwards; the item does not advance until that pass has run (see Steering with Comments).
This allows iterative refinement — comment to answer questions, provide feedback, or steer the work, and Fabrik incorporates the input into the issue.
Stages with post_to_pr: true (like the default Review stage) post detailed
output on the linked PR and a brief summary on the issue. The Review stage
also rebases onto the configured base branch and resolves merge conflicts before reviewing,
keeping the PR branch clean.
The Review and Validate stages ship with wait_for_reviews: true enabled by
default. When this option is set, Fabrik uses a three-phase reviewer gate
(re-invocation fires unconditionally; auto-advancement to the next stage still
requires auto-advance to be active):
fabrik:awaiting-review
and holds auto-advance until the gate clears.fabrik:awaiting-input.DISMISSED/PENDING
(CHANGES_REQUESTED, COMMENTED, and APPROVED bodies are all treated
as potentially actionable — this is what lets a bot reviewer like Pruefer,
which submits its entire finding set as a COMMENTED review, get picked
up and addressed instead of silently dropped), then waits for the next
review round. Only a truly empty-bodied APPROVED review with no inline
comments has nothing to route, so re-invocation is skipped and the issue
advances normally. After each re-invocation cycle, Fabrik posts a PR
summary comment with a per-thread footer listing how each inline review
thread was addressed.Configuration:
--max-review-cycles/FABRIK_MAX_REVIEW_CYCLES(default5) caps the number of re-invocation cycles per session.--review-wait-timeout/FABRIK_REVIEW_WAIT_TIMEOUT(default15minutes) sets the per-cycle wait before pausing. The env var is only consulted when the corresponding CLI flag is absent; if--review-wait-timeout=0or--max-review-cycles=0is explicitly passed, the built-in default is used and the env var is bypassed. See USER_GUIDE.md §10 for details.
The Validate stage also ships with wait_for_ci: true enabled by default. Fabrik gates auto-advance and, when applicable, auto-merge on CI checks passing; if checks fail, it re-invokes the stage agent with a structured CI-fix report to address new-regression failures. Non-required check_run failures do not block the gate — Fabrik trusts GitHub's mergeable_state as the authoritative merge-readiness signal, so only failures that branch protection itself considers blocking will prevent auto-merge.
If a stage doesn't complete (e.g., unfixable issues found), it retries after a cooldown period rather than being permanently skipped.
Fabrik responds to natural language. Comment on an issue to steer the work:
Each stage sees the full conversation history — previous stage outputs, user comments, and all — so context carries forward through the pipeline.
When a stage needs user input (FABRIK_BLOCKED_ON_INPUT), Fabrik posts a
🏭 **Fabrik** — @<user>: comment so GitHub delivers a mobile push notification
to the configured operator. For pushes to arrive, Fabrik must run as a different
account than the user being @mentioned — see
USER_GUIDE.md §Stages Waiting for Input
for setup details and the self-mention caveat.
Sending SIGHUP to the Fabrik process stops new work, lets in-flight Claude runs
finish, then re-execs the binary in place (a second SIGHUP restarts immediately) — no terminal disruption, no lost state. Use it
to pick up a new binary after go install without restarting the session.
The terminal alt-screen is properly restored on signal-driven exit, so no
reset is needed after a SIGINT, SIGTERM, or SIGHUP. Plugin-skills are checked and
refreshed non-interactively during restart when no local customizations are detected —
the process never hangs waiting for a prompt. When local customizations are present,
the refresh is skipped to preserve your edits and the [u] custom workflow badge
appears on next TUI startup instead, so operator customizations are never silently
overwritten. See
USER_GUIDE.md §Recovering from Wedged State
for the full escape-hatch procedure.
Sending SIGINT or SIGTERM (Ctrl-C on the bare CLI, or quitting the TUI)
triggers a clean stop, not a kill: Fabrik stops admitting new work, pauses
every in-flight issue durably (fabrik:paused + fabrik:awaiting-input,
plus an audit comment naming the interrupted stage), and commits and pushes
any uncommitted worktree progress before winding down. The entire drain —
Claude worker kill-grace escalation plus the pause-write phase — is bounded
by --drain-deadline (default 30 seconds), so Fabrik exits rather than
hanging if something gets stuck. Force-quit remains available as an escape
hatch: press Ctrl-C (or send SIGINT/SIGTERM) a second time while the clean
stop is in progress to exit immediately with no further label writes or
worktree commits. See
USER_GUIDE.md §Graceful Shutdown
for the full sequence.
The example stages implement a full SDLC pipeline:
| Stage | Purpose |
|---|---|
| Backlog | Parking lot; declared unmanaged: true (no processing) |
| Specify | Refine rough issues into clear, unambiguous specs (Q&A with user) |
| Research | Explore codebase, identify scope, surface questions |
| Plan | Design approach, break into tasks, document decisions |
| Implement | Write code, tests, commit to issue branch |
| Review | Rebase, review, fix, and push to PR |
| Validate | Run tests, verify requirements met |
| Done | Terminal state (no processing) |
Each stage is a YAML file in your stages directory:
name: Research # Must match a Project board column name
order: 1 # Processing order (lower = earlier)
skill: fabrik-stages:research # Plugin skill (recommended; alternative to inline prompt)
prompt: | # Inline prompt (used when skill is not set)
You are a research agent...
comment_skill: fabrik-stages:research-comment # Skill for comment review (overrides comment_prompt)
comment_prompt: | # Inline comment-review prompt (used when comment_skill is not set)
You are reviewing user comments...
model: sonnet # Optional: claude model to use
max_turns: 50 # Optional: max conversation turns per stage invocation
comment_max_turns: 15 # Optional: max turns when processing user comments (default: min(max_turns,15))
read_only: false # Optional: stash/restore worktree; use for analysis stages (Specify, Research)
persist_spec: true # Optional: engine commits the issue-body spec to specs/<issue>-<slug>/spec.md (Specify)
post_to_pr: true # Optional: post output on linked PR instead of issue
create_draft_pr: true # Optional: push branch and create draft PR before Claude runs
mark_pr_ready_on_complete: true # Optional: mark PR ready after stage completes
auto_advance: false # Optional: override global yolo setting for this stage
# true = always auto-advance; false = never; omit = inherit yolo
cleanup_worktree: false # Optional: remove worktree instead of invoking Claude (terminal stages)
disable_adaptive_thinking: true # Optional: disable Claude Code's adaptive thinking budget (default: true)
effort_level: high # Optional: Claude thinking effort: low, medium, high, max (default: high)
allowed_tools: # Optional: replaces the default tool set — not additive
- Read
- Grep
- Glob
completion:
type: claude # "claude" (default and only supported type)
Every Claude Code invocation receives --permission-mode dontAsk and an explicit --allowedTools list. Tools outside the allowed set are silently denied — no interactive prompts. This means Fabrik works correctly in headless and shared environments without requiring users to pre-configure permissions in ~/.claude/settings.json.
When a stage omits allowed_tools, Fabrik uses a built-in default covering file operations, git, GitHub CLI, common build systems, and shell utilities. See §7 Permissions in the User Guide for the full list.
When allowed_tools is set, it replaces the default set entirely — it is not additive. Only the listed tools are permitted for that stage.
The fabrik:unrestricted label bypasses this posture entirely, passing --dangerously-skip-permissions instead. Use only when a stage needs tools outside the default set.
Fabrik resolves settings in this order (highest to lowest priority):
CLI flag > shell env var > .env file > .fabrik/config.yaml > built-in default
Run fabrik init to generate .fabrik/config.yaml in your project. This file holds
all non-secret project settings and should be committed to git. See the
Configuration Reference in the User
Guide for a full field-by-field description with examples and defaults, including
required_status_contexts — per-repo required context names the CI gate must see
confirmed success for, needed only when a repo's required CI signal is a classic
commit status rather than a normal GitHub Actions check run.
Secrets (GitHub tokens) belong in a gitignored .env file only:
# .env (gitignored — keep secrets here)
FABRIK_TOKEN=ghp_... # Preferred
GITHUB_TOKEN=ghp_... # Fallback
Safety: When a .git/ directory is present, Fabrik refuses to start if .env exists but is not listed in .gitignore (prevents accidental token leaks). In directories without .git/ — containers, CI workspaces, or bare directories — this check is skipped and .env is loaded normally.
Unrecognized config.yaml keys: At startup, Fabrik now reports any .fabrik/config.yaml key that doesn't match a real config field — a typo, a stale key, or an entry for a knob that's actually CLI/env-only — instead of silently discarding it, and names the corresponding --flag/FABRIK_ENV_VAR to use when one exists. See Unrecognized config.yaml Key Warnings in the User Guide for details.
Fabrik can authenticate as a GitHub App installation instead of a personal access token. This is a co-equal alternative, not a replacement: a PAT remains fully supported and is the default, and is permanent. App auth requires an organization-owned project board — user-owned boards cannot use it. Set github_app_id, github_app_private_key_path and github_app_installation_id together (or the matching --github-app-* flags / FABRIK_GITHUB_APP_* env vars), or let fabrik init --github-app --owner <org> register the App and write the config for you. See GitHub App Authentication in the User Guide.
| Flag | Description | Default |
|---|---|---|
--owner | GitHub repo owner | required |
--repo | GitHub repo name; omit for multi-repo mode | optional |
--project | GitHub project number | required |
--user | Your GitHub username | required in PAT mode; not needed under GitHub App auth |
--token | GitHub token | $GITHUB_TOKEN |
--stages | Stage configs directory | ./.fabrik/stages |
--yolo | Auto-advance issues through stages without human approval; also auto-merges the linked PR when Validate completes | false |
--convergence-budget | Wall-clock budget for post-Validate yolo PR convergence (Go duration: 30m, 1h; 0 disables the budget entirely; also FABRIK_CONVERGENCE_BUDGET) | 30m |
--auto-merge-strategy | Merge method Fabrik attempts first, for both GitHub native auto-merge (yolo PRs) and direct merges (e.g. merge-train landings): MERGE, SQUASH, or REBASE; falls back to a repo-allowed method if the configured one is disallowed (also FABRIK_AUTO_MERGE_STRATEGY) | MERGE |
--auto-upgrade | Self-upgrade from handarbeit/fabrik GitHub Releases at startup and when idle (after 2 idle polls) | false |
--poll | Poll interval in seconds | 30 |
--retry-backoff | Seconds before re-dispatching a stage that did not complete; independent of --poll (also FABRIK_RETRY_BACKOFF; minimum 1) | 60 |
--notui | Disable the interactive TUI dashboard | TUI on by default |
--max-concurrent | Maximum number of concurrent issue workers |
Prerequisite for yolo auto-merge: GitHub's
allow_auto_mergesetting must be enabled on the repository before Fabrik can enable native auto-merge on yolo PRs. Enable it under Settings → General → Pull Requests → "Allow auto-merge", or run:gh api -X PATCH repos/{owner}/{repo} -F allow_auto_merge=trueFabrik emits a
[startup] WARNINGif this setting is disabled on any managed repo.
Releases: New releases are announced in the handarbeit/fabrik Discussions "Announcements" category after each successful release.
| Command | Description |
|---|---|
fabrik init | Initialize .fabrik/stages/, .fabrik/plugin/, and .fabrik/config.yaml in the current repo |
fabrik init --github-app --owner <org> | Guided GitHub App setup: register a new App (or adopt one with --github-app-id/--github-app-private-key-path), verify its permissions and write github_app_* into .fabrik/config.yaml. Organization-owned targets only |
fabrik init --create-board --owner <org> [--repo <repo>] [--title <title>] | Create a fully-configured GitHub Projects v2 board from your stage configs (Status columns in stage order, linked to the repo) and write it into .fabrik/config.yaml. Organization-owned targets only |
fabrik repair-board [--apply] | Diagnose an already-configured board for missing Status columns and display-only fields (Fabrik, Last activity, Last run); with --apply, add them (never removes or reorders existing columns, never touches an existing field) |
fabrik watch <issue-number> | Open a real-time TUI for a single issue — live Claude output, stage history, PR/CI status |
fabrik stream-filter | Read NDJSON Claude output from stdin and render it as human-readable text |
fabrik resume <issue-number> | Resume an interrupted Claude session for an issue |
fabrik upgrade | Refresh the plugin skills in .fabrik/plugin/ from embedded defaults and qualify stage skill references (fabrik-stages:review). Local edits are no longer refused: they move to .fabrik/plugins/local-workflows/, which is where to customise from now on. Stop the daemon first and commit the result. --no-migrate keeps the old refuse-on-customization behaviour; --force overwrites unconditionally; an up-to-date install prints fabrik upgrade: plugin already up to date. See Workflow migration in the User Guide. |
fabrik mcp [--dir <fabrik-dir>] |
Fabrik ships two user-invocable Claude Code skills (type /skill-name in any Claude Code session in the Fabrik repo):
| Skill | Description |
|---|---|
/cut-release | Pre-flight checks, curated release notes, commit/tag/push, and auto-files a doc-update issue — see §5 of the User Guide |
/audit-documentation | Scans recently shipped issues, identifies documentation gaps, closes covered issues, and files new gap issues — see §5 of the User Guide |
Operator customizations to built-in skills are protected — upgrades detect local edits and offer guided reconciliation rather than silently overwriting. See Upgrade protection for customized skills.
The main Fabrik dashboard (enabled by default; disable with --notui) shows a live turn counter ([N/M turns]) for each active job in the In Progress pane.
fabrik watchMonitor a single issue in real time without running the engine:
fabrik watch 42
# or with explicit credentials if not in .fabrik/config.yaml:
fabrik watch 42 --owner myorg --repo myrepo
The watch TUI displays: issue title and labels, live Claude output (streamed from the log file), stage history with duration and cost, linked PR status, CI check results, and comment count. Press i to open an interactive Claude session in the issue's worktree (resumes the current stage's session). Press q to exit.
Fabrik uses labels to track state:
| Label | Purpose |
|---|---|
fabrik:locked:<user> | Issue is being processed by this user's Fabrik instance |
fabrik:editing | Issue body is being updated (prevents concurrent processing) |
fabrik:paused | Issue is skipped entirely — no stage processing or comment processing occurs |
fabrik:awaiting-input | Stage is paused waiting for user input; auto-clears when a new human comment is posted after the pause |
fabrik:blocked | Issue is waiting for one or more blocking issues to close; managed automatically by the engine |
fabrik:awaiting-review | Applied optimistically by the engine whenever a wait_for_reviews: true stage completes (reviewer request data may still be stale at that moment); removed when no requested reviewers are outstanding and at least one review has been submitted (the dual condition catches bot reviewers like Copilot and Gemini that self-trigger via webhook without appearing in the formal reviewer list), or when the reviewer-wait timeout elapses as a fallback (--review-wait-timeout / FABRIK_REVIEW_WAIT_TIMEOUT), at which point the issue is paused with fabrik:awaiting-input |
fabrik:awaiting-ci | Applied immediately when a wait_for_ci: true stage emits FABRIK_STAGE_COMPLETE; means "CI gate active" and covers both pending and failed CI states; stage:X:complete is deferred until CI passes (conjunctive gate). Triggers cache bypass so CI results are re-evaluated on every poll. Cleared when all checks pass or the CI wait timeout elapses, at which point the issue is paused with fabrik:awaiting-input |
fabrik:rebase-needed | Applied when GitHub reports the linked PR as mergeable: false on a wait_for_ci: true stage (usually triggered by another PR merging into the base branch while this branch sits in the CI-await window); the engine dispatches a rebase re-invocation for Claude to resolve the conflict, and the label is cleared when the rebase lands and the PR becomes mergeable again |
fabrik:awaiting-runaway-alert | Applied when the merge-train runaway guard (ADR-059 D8) pauses a Queued member (fabrik:paused + fabrik:awaiting-input) but its alert comment fails to post; retried every poll by a settle scan until the alert succeeds or a fallback comment lands. See in the State Machine spec. |
Multiple people can run Fabrik against the same project board. Stage work is scoped
to each instance's --user — an instance only picks up and runs stages for issues
locked to its configured user. Comment processing, however, is any-author: comments
from colleagues, bots, and other Fabrik instances all trigger processing regardless
of who wrote them. Labels provide lightweight locking to prevent conflicts.
Fabrik always bare-clones each managed repository on first access. Run Fabrik from any directory — no need to be inside a git checkout of a managed repo:
.fabrik/repos/<owner>-<repo>.git on first access.fabrik/worktrees/<owner>-<repo>/issue-N/ on branch fabrik/issue-NThis means:
mkdir my-fabrik-dir && cd my-fabrik-dir
fabrik init
fabrik --owner myorg --project 5 --user me
# Fabrik bare-clones each repo as needed into .fabrik/repos/
Omitting --repo (or leaving repo: commented out in .fabrik/config.yaml) enables multi-repo mode — Fabrik processes all repos discovered on the board. Pass --repo owner/repo to restrict processing to a single repository.
.fabrik/repos/ is gitignored by default — bare clone directories are large and should not be committed.
By default, Fabrik bare-clones repos using HTTPS. To use SSH instead ([email protected]:<owner>/<repo>.git), pass --ssh or set git_ssh: true in .fabrik/config.yaml. The FABRIK_GIT_SSH=true environment variable is also supported.
If no git credential helper is configured, Fabrik prints an advisory warning at startup (non-fatal) directing you to configure one or switch to SSH. Existing bare clones are not re-cloned when you change this setting — only new clones are affected.
See USER_GUIDE §3 — Git Clone Protocol (HTTPS vs SSH) for full details.
Fabrik supports dependency-based sequencing of issues using GitHub's native "Blocked by" relationships. A formation is a coordinated set of issues that execute in parallel where possible and respect ordering constraints automatically — no polling, no manual unblocking.
When all blocking issues are closed, Fabrik guarantees re-evaluation of a blocked issue within ~10× the poll interval (~5 minutes at default settings), then resumes it automatically once dependencies are satisfied. The fabrik:blocked label is managed entirely by the engine (created on first use, no pre-creation needed). The first stage (Specify) always runs regardless of blockers, so an entire formation can be fully specified before execution begins.
Recipe: file issues → add blocked-by edges in GitHub → label all fabrik:yolo → move to Specify → watch it run.
Plan can also decompose an oversized issue autonomously. It emits FABRIK_SPAWN_CHILD_BEGIN/END blocks in its output declaring each sub-issue to create; when the parent advances to Implement, the engine's preImplement step creates each child issue in its target repo, adds it to the board, and links it as a blockedBy dependency of the parent. The sub-issues then flow as a formation automatically.
Validated on the Ambient project: 9 issues, 4 parallel starts, 7 dependency edges — all pipeline constraints respected automatically, ~88 minutes wall-clock, $31 total cost.
See the USER_GUIDE §3 — Dependency-Based Sequencing (Formations) for the full recipe, diagram, and behavior callouts.
When an issue reaches Done, it remains on the board in the Done column until auto-archive removes it. Auto-archive is on by default: once an item has sat in Done for at least --archive-after (default 168h / one week, also FABRIK_ARCHIVE_AFTER), anchored to when the completion label (stage:<Done>:complete) was applied, Fabrik archives it off the project board. Archived items are not deleted — they remain accessible and recoverable via the project board's "Archive" view in GitHub. Set --archive-done off (also FABRIK_ARCHIVE_DONE=off) to disable auto-archival and leave Done items on the board indefinitely.
Fabrik is used to develop Fabrik. Issues filed against this repo go through the same Specify → Research → Plan → Implement → Review → Validate pipeline that Fabrik orchestrates. When we filed an issue to add PR comment processing, Fabrik researched its own codebase, planned the GraphQL changes, and will eventually implement the feature that lets it read PR comments — gaining a capability it needs by building it for itself. Ouroboros-as-a-service.
The human's role is product manager, architect, and UX designer all in one: file issues, answer questions when the Research stage surfaces them, drag cards across the board, and occasionally comment "please process PR feedback" when Copilot has opinions. The factory does the rest.
./stagesIf you used Fabrik before v0.2, your stage configs were in ./stages (the old default)
or ./stages/mystages/ if you used that convention.
mkdir -p .fabrik/stages
# If you used the default ./stages path:
cp ./stages/*.yaml .fabrik/stages/
# If you used ./stages/mystages/:
cp ./stages/mystages/*.yaml .fabrik/stages/
# Or keep the old path by setting FABRIK_STAGES in your .env:
# FABRIK_STAGES=./stages
./stages workflow (see "Quick Start" and "Migration from ./stages" above for the current ./.fabrik/stages default), workflow patterns, stage details, labels, and troubleshootingSee adrs/ for documented decisions and their rationale. Of particular note:
ghp_...) with repo, project, and workflow scopes
github_pat_...) are not supported — GitHub Projects v2 GraphQL requires a classic PATApache License 2.0. Contributions are accepted under the same license (see CONTRIBUTING.md) — no CLA required.
plugin:skillfabrik-stages:reviewfabrik-stagesfabrik upgrade.fabrik/plugins/local-workflows/FabrikLast activityLast runfabrik repair-board --applyactions: write5 |
--max-retries | Max failed stage attempts before pausing the issue (0 = unlimited) | 3 |
--max-tools-denied-retries | Maximum number of consecutive tool-permission-denial exits per stage before pausing (0 = default 3; also FABRIK_MAX_TOOLS_DENIED_RETRIES). Independent of --max-retries — a permission misconfiguration is not a stage failure. | 0 (3 denials) |
--claude-wait-delay | Seconds to wait after Claude exits before recovering buffered output when grandchild processes hold stdout pipe open (0 = use built-in default of 30 sec; env: FABRIK_CLAUDE_WAIT_DELAY). Prevents worker goroutines from blocking indefinitely when Claude uses run_in_background or the Monitor tool. | 0 (30 sec) |
--review-wait-timeout | Minutes to wait per reviewer-gate cycle before pausing. Explicitly passing 0 uses the built-in default of 15 and bypasses FABRIK_REVIEW_WAIT_TIMEOUT. When absent, FABRIK_REVIEW_WAIT_TIMEOUT is consulted first; falls back to 15 if unset. | 0 |
--max-review-cycles | Max re-invocation cycles per reviewer-gate session. Explicitly passing 0 uses the built-in default of 5 and bypasses FABRIK_MAX_REVIEW_CYCLES. When absent, FABRIK_MAX_REVIEW_CYCLES is consulted first; falls back to 5 if unset. | 0 |
--max-rebase-cycles | Maximum rebase re-invocation cycles per issue before pausing (0 = default 3; also FABRIK_MAX_REBASE_CYCLES) | 0 (3 cycles) |
--max-enqueue-cycles | Maximum merge-queue re-enqueue cycles per issue before pausing (0 = default 5; also FABRIK_MAX_ENQUEUE_CYCLES) | 0 (5 cycles) |
--merge-queue | Merge queue routing for the yolo path: auto (enqueue when the repo requires GitHub's native merge queue) or off (skip enqueue; direct merge may fail on queue-required repos; also FABRIK_MERGE_QUEUE) | auto |
--merge-train | Fabrik-internal merge train (ADR-059): on advances yolo Validate completions into the Queued column for batched landing with a single combined Validate, or off to keep the existing per-PR auto-merge path (also FABRIK_MERGE_TRAIN) | off |
--max-train-trials-per-window | Merge-train runaway guard: maximum trial-branch creations with zero successful lands within the window before pausing all Queued members (0 = default 20; also FABRIK_MAX_TRAIN_TRIALS_PER_WINDOW) | 0 (20) |
--train-trial-window | Merge-train runaway guard: rolling window in minutes over which --max-train-trials-per-window is measured (0 = default 60; also FABRIK_TRAIN_TRIAL_WINDOW) | 0 (60 min) |
--archive-after | Grace period since an item settled into Done before it is auto-archived off the project board (Go duration: 168h, 24h; also FABRIK_ARCHIVE_AFTER) | "" (168h = 1 week) |
--archive-done | Auto-archive Done items after --archive-after elapses: on or off (also FABRIK_ARCHIVE_DONE) | "" (on) |
--debug-output | Save Claude stage output to .fabrik/debug/ for debugging | false |
--symlink-env | Create a relative symlink at <worktree>/.env pointing to the fabrikDir .env at worktree setup time. Enables stage code to read project secrets without copying them. Also FABRIK_SYMLINK_ENV. | false |
--plugin-dir | Path to Fabrik plugin directory (replaces the base plugin) | "" |
--plugin-dirs | Comma-separated additional workflow plugin directories loaded after the base plugin | "" |
--webhooks | Enable real-time webhook event delivery via gh webhook forward (requires cli/gh-webhook extension; also FABRIK_WEBHOOKS) | false |
--github-app-id | GitHub App ID for App-installation auth — set together with --github-app-private-key-path and --github-app-installation-id, or not at all (also FABRIK_GITHUB_APP_ID) | 0 |
--github-app-private-key-path | Path to the GitHub App's private key PEM (also FABRIK_GITHUB_APP_PRIVATE_KEY_PATH) | "" |
--github-app-installation-id | GitHub App installation ID to authenticate as (also FABRIK_GITHUB_APP_INSTALLATION_ID) | 0 |
--no-browser | Suppress automatic browser-opening for GitHub App guided-install prompts; use --no-browser=false to re-enable (also FABRIK_NO_BROWSER) | true |
--event-source | Event ingestion transport: poll, tailscale, direct or hookdeck (all but poll require GitHub App auth; not combinable with --webhooks; also FABRIK_EVENT_SOURCE). hookdeck is deprecated in v0.0.90 and removed in v0.0.92; use tailscale or direct. The deprecated --hookdeck-api-key-env / --hookdeck-webhook-secret-env flags are described in the User Guide | poll |
--direct-listen | host:port the direct receiver listens on (plain HTTP; bind loopback). Required with --event-source direct (also FABRIK_DIRECT_LISTEN). See Direct App Webhook Receiver | "" |
--app-webhook-path | URL path the direct and tailscale receivers serve (also FABRIK_APP_WEBHOOK_PATH) | /webhook |
--tailscale-hostname | Tailnet hostname of the embedded Funnel node. Required with --event-source tailscale (also FABRIK_TAILSCALE_HOSTNAME). See Embedded Tailscale Funnel Receiver | "" |
--replay-failed-deliveries | Ask GitHub to redeliver App webhook deliveries that failed while the daemon was not receiving (direct or tailscale only; also FABRIK_REPLAY_FAILED_DELIVERIES and replay_failed_deliveries) | true |
--reconcile-interval | Seconds between periodic light-reconcile health checks when webhooks are enabled (0 = default 180; also FABRIK_RECONCILE_INTERVAL) | 0 (180 s) |
| Stdio MCP server backed by the running daemon: read tools, audited overseer actions and pushed events. The Fabrik PM plugin registers it for you. See Overseer Tools. |
fabrik:api-key-helper-detected | Applied when a stage invocation is skipped because the worktree's own .claude/settings.json sets apiKeyHelper; does not count against max_retries. Clears automatically once apiKeyHelper is removed and a later invocation reaches Claude successfully. See Anthropic Auth Namespace Scrub & apiKeyHelper Refusal in the User Guide. |
fabrik:skill-unresolved | Applied when a stage's skill reference (<plugin>:<skill>) matches no plugin loaded for the worker, so the stage is not run; does not count against max_retries. Clears automatically once the reference resolves. See Skill references in the User Guide. |
fabrik:tools-denied | Applied when Claude's own permission layer denies one or more tool calls during an invocation; does not count against max_retries, bounded instead by its own --max-tools-denied-retries counter (default 3). Clears automatically on the next invocation that isn't itself denied. See Retry and Escalation in the User Guide for the "Troubleshooting: an issue carries fabrik:tools-denied" note. |
fabrik:awaiting-done | Set when a stage emits FABRIK_NO_WORK_NEEDED; the Done move / issue close is retried every poll until it succeeds. See Labels Reference |
fabrik:awaiting-close | Set when a landed issue is still open after its PR merged (non-default base: branch, or the default base when GitHub's Closes #N auto-close did not fire); Fabrik closes it on a later poll with a "closed after merge" comment. See Closing a Landed Issue |
fabrik:awaiting-pr-ready | Set when marking the draft PR ready for review failed; retried every poll without holding the item back. See Labels Reference |
fabrik:awaiting-advance | Set when a terminal advance failed to move the board Status (e.g. a missing column); retried every poll. See Labels Reference |
fabrik:reworking:<Stage> | Transient marker while a comment re-enters an already-completed stage; removed when the rework finishes. See Labels Reference |
fabrik:toolchain-stale | Warn-only: the worktree declares a toolchain version the daemon's PATH does not satisfy; clears automatically. See Labels Reference |
fabrik:claude-limit | Claude account usage limit hit; does not count against max_retries. See Claude Usage-Limit Hold |
fabrik:children-spawned / fabrik:spawned-child:<index>:<number> / fabrik:sub-issue | Sub-issue spawn bookkeeping: batch-complete guard on the parent, resume markers for an interrupted spawn, and a filter label on each child. See Labels Reference |
stage:<name>:complete | Stage has been completed |
stage:<name>:in_progress | Stage is actively running |
stage:<name>:failed | Stage hit max retries and was paused |
model:<name> | Use this model for the issue (e.g. model:opus overrides the stage YAML default) |
effort:<level> | Override thinking effort for this issue only — valid values: low, medium, high, max; precedence: max > high > medium > low. Complements model: label. |
fabrik:yolo | Force auto-advance even when auto_advance: false; also triggers auto-merge of the linked PR when Validate completes |
fabrik:auto-merge-enabled | Applied when Fabrik enables GitHub's native auto-merge on a yolo PR after Validate completes; serves as the idempotency guard and convergence-budget start anchor; removed when the PR merges or is closed |
fabrik:cruise | Auto-advances through all stages like fabrik:yolo but stops at Validate — no auto-merge, no move to Done. If both fabrik:cruise and fabrik:yolo are present, cruise takes precedence. |
fabrik:unrestricted | Pass --dangerously-skip-permissions to Claude Code for this issue only, bypassing the default --permission-mode dontAsk posture and the entire tool allowlist. Use only when a stage needs tools outside the default set (e.g. deno, bun, or other non-standard toolchains). Caution: removes all tool restrictions. |
review-authority:<mode> | Override the stage's configured review_authority for this issue only (advisory or authoritative); only meaningful alongside wait_for_reviews: true. See Authoritative Mode in the User Guide. |
expected-reviewers:<mode> | Override the stage's configured expected_reviewers for this issue only (none or declared); only meaningful alongside wait_for_reviews: true. See Declaring Expected Reviewers in the User Guide. |
fabrik:extend-turns | Pre-grant 2× max_turns budget for the next invocation; auto-extends to 3× when stage progress is detected; auto-removed on successful stage completion. |
fabrik:revalidate | Force re-entry of the Validate stage — the engine clears stage:Validate:complete, stage:Validate:failed, fabrik:paused, fabrik:awaiting-input, fabrik:awaiting-ci, fabrik:auto-merge-enabled, and itself, then re-runs Validate on the current HEAD SHA. Use as a one-action recovery for stuck-Validate, or as the manual fallback for items that completed Validate before SHA-change auto-revalidation was deployed. |
base:<branch> | Override the base branch for this issue — Fabrik forks from, rebases onto, and targets PRs at <branch> instead of the repository default. Must be set before Research. If the branch does not exist on the remote, Fabrik falls back to the default branch and posts a comment. |