Wright is a self-hosted daemon that turns well-scoped GitHub and GitLab issues into verified, ready-to-review pull requests — unattended, around the clock — while treating cost per resolved issue as the design constraint, not an afterthought.
Every backlog has a long tail of issues that are clearly scoped but not worth a senior engineer's time: small bug fixes, minor features, chores, mechanical refactors. General-purpose AI coding assistants — chat-driven, IDE-embedded, cloud "AI engineer" products — can technically do this work, but they're built for a human sitting in the loop, paying for a broad, flexible tool on every turn. That cost structure doesn't fit the job of "take a scoped issue, produce a merged PR, unattended, at scale, cheaply."
Wright is narrow on purpose. It does one job — issue in, verified PR out — and spends an LLM call only where an LLM call is actually required: understanding the issue and writing or adjusting code. Everything around that — polling, gating, sandboxing, git operations, running the repo's own test suite, retrying against real failures, opening the PR — is deterministic Go, not more model calls. That's the whole efficiency story: not a cheaper model, a smaller loop.
It's built first for individual developers and small teams who want their backlog worked down without a large AI spend or a third party holding push access to their code — which is also why it's self-hosted: a single static binary, small enough to read end to end, so you know exactly what it does with your credentials.
- Not a general-purpose coding assistant. No chat, no IDE plugin, no "help me design this system." It takes a scoped issue and produces a patch — nothing broader.
- Not interactive. There's no pairing session to drive. It runs unattended; you interact with it through issue comments, PR review, and config, not a conversation.
- Not a product manager. It doesn't decide what should be built. Scoping the work — writing and labeling the issue — is still yours; Wright implements.
- Not a hosted service. Self-hosted first, by design — your token, your container, your infrastructure.
- Not (yet) proven at scale. It's pre-alpha, see below — the pipeline runs end to end, but the reliability track record is still being built.
Status: pre-alpha (Phase 1). The end-to-end single-repo pipeline is now implemented: poll labeled issues, run a gate, execute an agent in a Docker sandbox, verify with the repo's own tests, and open a PR. Interfaces/config may still evolve while the project hardens toward Phase 2.
⚠️ Security: the trigger label is a trust boundary. Wright feeds issue text — from anyone who can file an issue — to an LLM agent that writes code and opens PRs against your repo. Only applytrigger_labelto issues from people you trust. See SECURITY.md for the full threat model, token scope guidance, and hardening recommendations.
- A
Providerinterface with GitHub and GitLab adapters covering the write path (including issue-label add/remove and PR operations). - A Phase 1 pipeline: poll → gate → sandboxed agent tool loop → verifier retry loop → git push → PR creation.
- Optional cross-issue dependency stacking (
stacking.enabled): if an issue references a dependency that already has an open Wright PR, Wright stacks the new work on that PR's branch instead of blocking until a human merges it, then retargets the stacked PR once the dependency merges. - Per-issue token and turn accounting.
- A YAML config format (
wright.yaml) describing one or more repos. - A CLI:
wright validate— load and validate config offline, and confirm required token env vars are set.wright once— prove provider access and list labeled issues.wright run— run one full Phase 1 pipeline pass for one repo.wright smoke— manual provider write-path smoke test.wright version— print the build version.
Wright reads a wright.yaml. See wright.example.yaml
for a fully commented example, and
docs/CONFIGURATION.md for the full reference of
every field, default, and validation rule.
version: 1
repos:
- provider: github # "github" | "gitlab"
repo: your-org/your-repo # GitLab: full project path, nested groups OK
# api_base_url: https://gitlab.example.com # GHE / self-hosted GitLab only
# token_env: MY_TOKEN_VAR # env var NAME; see table below
trigger_label: wright # default "wright"
# base_branch: main # default: repo's default branch via API
auto_merge: false # default false — explicit opt-in
budget:
max_turns: 30
llm:
provider: claude # "claude" (default) | "openrouter"
auth: api_key # api_key (Phase 1); oauth is deferred to Phase 2
agent_model: claude-sonnet-5
gate_model: claude-haiku-4-5
effort: high
# prompt:
# system_append: "Always update CHANGELOG.md when you change public behavior."
# # system_override: ... # advanced — see wright.example.yaml
promptcustomization.system_appendadds repo-specific instructions after Wright's default behavior guidance;system_overridefully replaces it (mutually exclusive withsystem_append;wright validaterejects setting both). Either way, Wright's operational contract — don't self-commit/push, tool path rules, verify-retry behavior — is a separate, always-enforced block that neither field can touch. Only usesystem_overrideif you know exactly what default guidance you're discarding.
LLM providers.
claudeis the default and the path Phase 1 is built around and tested against.openrouteris an optional extension beyond that: it targets OpenRouter's OpenAI-compatible API, supportsauth: api_keyonly, and reads its key fromWRIGHT_OPENROUTER_API_KEY/OPENROUTER_API_KEY.
Credentials are never stored in the config file. Tokens are read from environment variables, resolved in this order:
| Order | Source | Notes |
|---|---|---|
| 1 | token_env from the repo entry |
Explicit override; you name the var. |
| 2 | WRIGHT_GITHUB_TOKEN / WRIGHT_GITLAB_TOKEN |
Wright-specific, by provider. |
| 3 | GITHUB_TOKEN / GITLAB_TOKEN |
Conventional fallback, by provider. |
make build
export WRIGHT_GITHUB_TOKEN=ghp_... # or WRIGHT_GITLAB_TOKEN=glpat-...
cp wright.example.yaml wright.yaml # then edit for your repo
./wright validate --config wright.yaml
./wright once --config wright.yaml
./wright run --config wright.yamlmake build # compile ./cmd/wright with version ldflags
make test # go test ./... (no live API calls)
make lint # golangci-lint run
make tidy # go mod tidyThe test suite makes zero live API calls — adapters are tested against
httptest servers with canned fixtures. Live verification is done manually via
wright smoke against your own scratch repos.
wright run uses real git inside the sandbox clone (checkout/commit/push).
PushCommits remains available primarily for deterministic provider-level smoke
and adapter testing.
Current implementation centers on internal/{poller,gate,sandbox,agent,verifier,gitops,pipeline}
plus internal/{config,provider,cli,stack}.
Phase 2 work (queueing/concurrency/reliability hardening) is planned but not yet implemented.
Apache License 2.0 — see LICENSE.