DevAgent turns a GitHub issue into a draft pull request. Built with Python, FastAPI, LangGraph, Pydantic, OpenHands, Docker, and Gitleaks, it clones the repository and creates an isolated sandbox for each task. OpenHands uses the Agent Client Protocol (ACP) to run a local coding agent—currently Codex—inside that sandbox. DevAgent then builds, tests, and scans the changes before opening a draft PR for review.
Design: docs/superpowers/specs/2026-09-26-devagent-design.md.
sequenceDiagram
participant Client
participant API as FastAPI API
participant Worker
participant GitHub as GitHub adapters
participant Graph as LangGraph workflow
participant Git as GitOps
participant Sandbox as Docker sandbox
participant Codex as CodexEngine
Client->>API: POST /issues
API->>API: Validate bearer token and JSON
API->>Worker: submit_issue(issue_url)
Worker->>GitHub: Read issue
GitHub-->>Worker: Task
Worker->>Worker: Validate status, duplicates, repo, budget
Worker-->>API: RunRecord(status=queued)
API-->>Client: 202 + run_id
Worker->>Graph: Start queued run
Graph->>Graph: claim: assign branch and mark running
Graph->>Git: prepare: clone repository and create branch
Graph->>Sandbox: implement: start container
Sandbox->>Codex: Run coding agent
Codex-->>Sandbox: Edited files
Graph->>Git: verify: commit and inspect diff
Graph->>Sandbox: Run build and tests
Graph->>GitHub: publish: push branch and create draft PR
Graph-->>Worker: done or failed
The implementation chain remains claim → prepare → implement → verify → publish. Any failed step goes to
fail and records the error in the run API. Issues and their comments are read-only. A failed run can be retried manually.
The issue must be open and its owner/repository must appear in config/agent.yaml.
For local requests from your shell, load the same .env used to start the service:
set -a
source .env
set +a
curl -X POST http://127.0.0.1:8080/issues \
-H "Authorization: Bearer $DEVAGENT_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"issue_url":"https://github.com/acme/booking-api/issues/42"}'Accepted response:
{"run_id":"90ed2bdaf65a","status":"queued"}No GitHub label is required. Every run uses Codex.
The endpoint returns:
202when the run is queued.400for an invalid issue URL.404when GitHub cannot find the issue.409when the issue was already submitted in the current service process.422for a closed issue or an unconfigured repository.429when the daily run budget is exhausted.
Copy the examples, then put the secret values in the Git-ignored root .env file:
cp config/agent.example.yaml config/agent.yaml
cp .env.example .env
# Edit .env to set GITHUB_TOKEN, DEVAGENT_ADMIN_TOKEN, and CODEX_AUTH_JSON.
uv run --env-file .env devagent check-config --config config/agent.yaml
uv run --env-file .env devagent serve --config config/agent.yamlCODEX_AUTH_JSON is the one-line JSON content of ~/.codex/auth.json after codex login.
Keep the single quotes around that value in .env so its JSON quotes survive parsing.
uv run --env-file .env loads the file for that command; uv run alone does not load it.
Use a long random value for DEVAGENT_ADMIN_TOKEN. Never commit .env or paste token values into issues.
Minimal configuration:
github:
token: ${GITHUB_TOKEN}
service:
admin_token: ${DEVAGENT_ADMIN_TOKEN}
repos:
- repo: {owner: acme, name: booking-api}
base: main
commands:
bootstrap: "dotnet restore"
build: "dotnet build --no-restore"
test: "dotnet test --no-build"GITHUB_TOKEN should be a fine-grained token scoped only to configured repositories, with:
- Contents: read and write (clone and push).
- Issues: read (read the task and existing comments).
- Pull requests: read and write (find or create the draft PR).
- Metadata: read (included by GitHub for fine-grained tokens).
The GitHub token stays in the orchestrator. It is not passed to the coding-agent sandbox.
POST /issuesfetches the issue, rejects closed, duplicate, or unconfigured issues, applies the daily budget, creates a queued run, and returns its ID.- Claim marks the run as running and assigns a unique
agent/<owner-repo-issue>-<slug>-<run>branch name. - Prepare clones the configured base branch, creates the new local branch, detects .NET requirements, and selects or builds a cached toolchain image. The run retains its immutable Docker image ID.
- Implement runs the optional bootstrap on the unchanged base, then lets Codex edit the working tree in an OpenHands sandbox.
- Verify commits locally, rejects protected paths, symlinks, gitlinks, nested repositories and secrets, then repeats the optional bootstrap and runs the configured build and test commands in a fresh sandbox.
- Publish pushes only the new
agent/*branch, opens a draft PR, and records its URL in the run API. - Fail records the failed node and a redacted error in the run API. It never pushes a failed result.
- Never pushes to an existing branch or outside
agent/*; never force-pushes. - Never merges, approves, closes, or marks a pull request ready.
- Never writes to issues or their comments; it only reads them.
- Restricts GitHub HTTP calls to issue/comment reads and draft-PR reads/creation.
- Keeps GitHub credentials out of the sandbox, prompt, repository config, and logs.
- Rejects protected paths and secret-bearing diffs before push.
- Applies concurrent-run, daily-run, wall-clock, and optional cost limits.
Pushing an
agent/*branch can trigger repository CI. Configure branch policies and workflows so agent branches require review and cannot run untrusted changes with production secrets.
All run endpoints require Authorization: Bearer $DEVAGENT_ADMIN_TOKEN:
GET /runsGET /runs/{id}— includes per-node eventsPOST /runs/{id}/retry— only failed runs; creates a fresh run and branch
Runs, events, and LangGraph checkpoints are held in memory and are lost when the orchestrator restarts. Duplicate protection applies only within the current process; check existing PRs before resubmitting after a restart.
cp docker/compose.example.env docker/.env
cp config/agent.example.yaml config/agent.yaml
docker compose -f docker/compose.yaml --env-file docker/.env up -d --buildCompose builds the orchestrator and the common devagent-sandbox:latest image with OpenHands, Codex ACP, and ICU
(International Components for Unicode, used by .NET for culture-aware formatting, sorting, and casing).
After cloning, DevAgent reads global.json first and honors sdk.version, rollForward, and allowPrerelease.
Otherwise it chooses a stable SDK for the highest detected target framework. It reads TargetFramework and
TargetFrameworks from project files and the nearest Directory.Build.props, including simple property references,
empty-property defaults, and literal local property imports. Older target frameworks receive additional ASP.NET Core
runtimes, which also include the .NET runtime. SDK and runtime versions come from Microsoft's release metadata.
Toolchain layers are cached by base image ID, detected requirements, and build recipe. Cache hits need no metadata lookup or build. A cached resolution stays pinned until its image is removed or the base/requirements/recipe changes. Implementation and verification both use the selected image ID, even if a tag or project file changes during the run. Repositories without .NET projects use the common image directly.
Detection does not execute repository MSBuild code. Unresolved/dynamic properties, conflicting SDK policies,
explicit RuntimeFrameworkVersion, sdk.paths, and platform-specific or .NET Framework targets require a custom
image. Set sandbox.detect_dotnet: false alongside sandbox.image to use that image directly.
Remove the old DOTNET_CHANNEL and INSTALL_DOTNET build settings; toolchains are now selected per repository.
Codex uses a subscription login: run codex login and set CODEX_AUTH_JSON to the contents of
~/.codex/auth.json.
The API accepts an issue, the worker queues a run, and the workflow coordinates the adapters and runtime tools. The orchestrator owns GitHub access and Git operations; the sandbox provides a workspace where Codex can edit files.
main.pyimplementsdevagent serveanddevagent check-config. On startup it loads the configuration, cleans up orphaned sandboxes from this deployment, builds the dependencies, starts worker threads, and serves the API.app/api.pydefines the FastAPI routes. It checks the admin bearer token for issue and run operations, translates intake errors into HTTP responses, and exposes an unauthenticated/healthendpoint.app/worker.pyvalidates submitted issues against their GitHub status, earlier runs, configured repositories, and the daily budget. It stores accepted runs, queues them, and processes them with up tomax_concurrentworker threads. A retry creates a new run for a failed one.app/factories.pyconstructs the GitHub tracker, GitHub code host, and Codex engine. It also collects configured secret values for redaction.
workflow/state.pydefines the data passed between steps: the task, branch, checkout path, engine result, verification result, PR, and any failure. ItsDepsbundle supplies the adapters and runtime tools to each step.workflow/graph.pyconnects the steps with LangGraph, records start/finish/error events, checkpoints state in memory, and routes a step error tofail. It does not retry steps automatically.workflow/nodes/claim.pychooses the new branch and marks the run as running.prepare.pyclones the configured base branch and creates that branch locally.workflow/nodes/implement.pybuilds the prompt, optionally runs the repository bootstrap command, and asks Codex to implement the issue inside a sandbox.verify.pycommits the changes locally, checks paths, file types, and secrets, then repeats the optional bootstrap and runs the build and test commands in a fresh sandbox.workflow/nodes/publish.pypushes the verified branch, creates or reuses its draft PR, records the PR URL, and completes the run.fail.pyrecords the failure and a redacted error.shared.pycontains common repository and command helpers.
providers/trackers/defines the task-tracker interface and its GitHub issue adapter. The adapter parses issue URLs and provides read-only access to issues and comments.providers/code_hosts/defines repository and PR operations. Its GitHub adapter supplies the clone URL and push authentication, finds open PRs, and creates draft PRs.providers/engines/defines the coding-engine interface.CodexEngineruns Codex through OpenHands and ACP, applies time and optional cost limits, and returns a redacted result.
core/config.pyloadsagent.yaml, substitutes${VAR}values from the environment, and validates GitHub, Codex, repository, budget, guardrail, sandbox, Git identity, and service settings.core/models.pyholds the shared task, repository, PR, run, and engine data models.core/slug.pycreates branch-safe names from issue details.core/guardrails.pychecks push targets, changed paths and file types, daily budgets, and known secrets; it also redacts those secrets from output.runtime/sandbox.pystarts a separate OpenHands Docker container per sandbox session. The repository is writable there, but.gitis read-only; the orchestrator removes the container when the session ends and reaps orphaned containers at startup.core/dotnet.pyinspects repository toolchain requirements and applies SDK matching policies.runtime/sandbox_images.pyresolves official releases and builds cached toolchain layers from a trusted recipe with no repository files in the Docker build context.runtime/gitops.pyperforms clone, branch, commit, diff, and push operations in the orchestrator with isolated Git settings.runtime/http.pyallows provider HTTP requests only when their method and path match an explicit rule.runtime/secretscan.pyruns Gitleaks on the diff and fails verification if scanning fails.runtime/logging.pyemits structured JSON logs with known secrets masked.runtime/storage.pyopens the process-local run store and LangGraph's in-memory checkpointer.db/tables.pydefines the SQLite run and event tables, whiledb/store.pycreates, updates, lists, and retries runs. SQLite is in memory, so these records and checkpoints disappear when the process stops.prompts/combines the issue and comments with local coding rules. It marks issue text as untrusted task data before sending the prompt to Codex.
config/agent.example.yamlshows repository commands and service settings;.env.examplelists the environment variables used for secrets. Copy these to the ignoredconfig/agent.yamland.envfiles for a local service.docker/compose.yamlruns the orchestrator and Caddy and builds the sandbox image.docker/orchestrator.Dockerfilepackages the API, worker, Docker CLI, and Gitleaks;docker/sandbox.Dockerfilepackages the OpenHands agent server, Codex ACP adapter, and ICU common dependencies.docker/Caddyfileproxies only/issuesand/health.tests/covers configuration, the API, issue intake, and the GitHub tracker.docs/contains the design specification and the diagrams shown above.
These capabilities were intentionally removed from the first version and can be added after the GitHub-to-PR flow is stable:
- PostgreSQL persistence for runs, events, and LangGraph checkpoints, including restart recovery.
- GitHub webhooks or polling for automatic issue intake.
- Additional task and code-host providers: ClickUp, Trello, and Azure DevOps.
- Additional coding engines, including Claude Code, with explicit engine selection.
uv sync
uv run pytest
uv run pyright
uv run --env-file .env devagent check-config --config config/agent.yaml