ADP is a self-hosted, GitHub-compatible forge for AI coding agents. Keep using git, gh, and
existing CI integrations while ADP binds every change to its intent, agent provenance, approvals,
and signed verification evidence.
Why ADP? An agent saying “tests pass” is not proof, and the transcript that could show its work is gone when the session ends. ADP keeps that context on the change itself, and refuses to land a change that does not meet your evidence requirements.
Try it · On a repository you already have · How it works · Self-hosting
New here? Start with the site — what ADP is, why it exists, and the AI-native SDLC stage by stage.
MIT · TypeScript · Fastify · PostgreSQL · the real git binary for all plumbing.
Two paths, answering different questions. The first shows you what ADP does; the second puts it under the work you already have. Take them in that order — the demo is much the better answer to the first question, and it takes a minute.
Neither creates an account. If you want to see the client without installing anything at all,
npx @deduva/adp --help is the whole CLI and no tree — it talks to an instance, so one of the two
paths below is what makes it do something.
One command. It starts a throwaway ADP against an ephemeral PostgreSQL, then uses ordinary git
and an unmodified gh to clone, push, open a proposal, report a gate, and land the change —
narrating each step. Nothing is installed, no account is created, and everything is torn down when
you press Enter.
git clone https://github.com/DeDuva/adp.git && cd adp
make demoIt ends where the point is. The merge is refused while the change has no gate result and no approval, and allowed once it has both. You then read the signed evidence bundle and the operation log that record why.
Needs Docker and Node 22 (make doctor checks). About a minute, most of it the first npm ci.
Liked it and want one that is still there tomorrow?
make local # then make local-down / make local-destroySame server, same proxy, longer lifetime: Postgres on a named volume, a self-signed certificate for
localhost written where you can add it to your trust store, and the token printed. gh will talk
to it, which plain HTTP never allows for a non-github.com host. It is for evaluation and
development, not a deployment — docs/self-hosting.md §3b says exactly
where that line is.
Or run the full verification suite
Every tier, including conformance against the real gh binary and the browser-driven acceptance
walkthrough.
Minutes rather than seconds, and it proves the parts a demo skips:
bash scripts/dev/bootstrap.sh
make up && make test-all && make downCompanion mode: your repository stays on GitHub and you do not change how you work. Issues, branches, pull requests, reviews, Actions and the merge button all stay where they are. ADP sits underneath, records what each of them means, and publishes its verdict back onto the pull request as a check GitHub can require.
You need an instance to point at. make local above is enough — it prints a URL and a token, and
it is still there tomorrow. Then, from inside your own checkout:
npx @deduva/adp login --server http://localhost:8420 --token <the token make local printed>
npx @deduva/adp init --repo local/<your-repo> --credential <a GitHub token ADP can push with>Three things that save a wrong turn on a make local instance:
- The CLI uses the plain HTTP port, not the TLS one.
ghis the tool that needs the certificate, because it refuses plain HTTP for any host but github.com;adphas no such rule. - Name the org.
adp initinfersexample-org/widgetfrom your GitHub remote and your token is not a member of an org by that name, so it refuses and tells you to name one. On amake localinstance the org you have islocal. - The mirror needs a credential ADP can push with —
--credential, orGITHUB_TOKENin the environment.
adp init creates the repository on your instance, records which repository this clone is,
configures the mirror in both directions, and writes an adp.yaml detected from what your
repository already says about itself — for a Node repository, the gate it finds is your own
npm run test. It shows you the file rather than committing it.
repo: created
identity: local/widget, recorded for this clone (mirror mode)
remote: unchanged — you push to your existing remote, ADP observes
mirror: https://github.com/example-org/widget.git
adp.yaml: written — node (package.json)
gate test: npm run test
| What changes in your repository | One new file, adp.yaml, which you review before committing |
| What does not change | Your remote, your pull requests, your reviewers, your CI, your merge button. git clone keeps working throughout |
| What you give up while ingest is on | Creating proposals and issues natively on that repository, and adp land — GitHub stays the merge authority |
docs/companion-mode.md is the whole of it: the loop with what ADP records
beside what you do, the two checks that appear on the pull request, and what it deliberately refuses
to do.
For a real deployment, see docs/self-hosting.md; to run from source,
Running it.
Agents write most new code now, and the pattern is serial rather than a swarm: one capable agent iterates against CI until it believes the work is done, then submits. Fan-out is reserved for hard problems and fleet-wide remediation.
The belief is the problem. When the author decides when it's finished, the only oversight surface is a test suite the author can see — and often edit.
Git was built for a different job. Its conflicts halt automation, and it records what changed while discarding why and whether anyone checked. So when a change lands wrong, everything that follows — revert it, validate it, continue it with a different model or harness — needs a record that today exists only as a transcript in one vendor's format. Every harness privately reinvents the same primitives: checkpoint and rewind, session persistence, workspace orchestration. None is visible to the repository, and none can read another's.
ADP's bet is that the durable primitive is neither storage nor the change model, but binding context to verification evidence at merge time. Plenty of systems capture provenance; the point here is to make it enforceable — to turn the agent's belief into evidence someone else can check, revert, or build on.
git clone keeps working throughout.
The long version, with the field data and how every other entrant scores: Why ADP exists.
ADP serves two APIs over the same data. One imitates GitHub, so tools you already have keep working. The other exposes what GitHub has no equivalent for.
The compatibility plane is GitHub's surface: the git wire protocol, REST at /api/v3, and
GraphQL at /api/graphql. An off-the-shelf agent or CI tool uses it with no knowledge that ADP
exists. This is deliberately not an emulation layer bolted on top — the domain model is issues,
proposals, reviews, and merges, so GitHub's shapes project onto it directly.
The native plane at /api/adp (and over MCP) exposes what has no GitHub analogue: the
operation log, undo, evidence bundles, and workspaces.
Pushing a commit produces a signed changes row binding four things together:
| Field | What it captures |
|---|---|
| Intent | the issue the work answers — filed as a typed intent, not free text |
| Diff | the git commit itself; git remains the store |
| Evidence | gate results for the commit, as DSSE-signed in-toto attestations |
| Provenance | the pushing identity, plus harness / model / session where supplied |
Schemas live in spec/schemas/ (change, evidence, provenance, operation);
the REST surface is described in spec/openapi.yaml.
Two mechanisms run at the point where code enters the system, both as real git hooks invoked by
git receive-pack:
-
pre-receiveruns push protection. A bundled regex-plus-entropy secret scanner rejects the push at the wire with a typed error naming the line and pattern. Becausepre-receiveruns while pushed objects are still in git's per-push object quarantine, the hook computes its diff locally and ships the text to the server, rather than shipping shas the server cannot yet resolve.That guards the diff, and a trajectory is a different object. It records what the agent read — file contents pulled into context, environment inspected, tool output returned. A
.envthe agent opened and correctly decided not to commit never appears in any diff, and would appear verbatim in atool_callpayload. So the same scanner runs at the trajectory ingest path too, before anything is hash-chained: a detected secret has its span replaced by a visible[redacted:<pattern>]marker, the event recordsredactionsnaming where and what fired, and the chain commits to the redacted form — so what verifies is what is stored, and a redaction can never be edited away afterwards. It is the same engine in a second place, not a second engine.A detector removes what it recognises, and the larger surface is everything else — source no pattern covers, customer data in a tool result, a prompt someone typed. So a trajectory stores the shape of a payload by default and not its content: objects, arrays, keys, numbers and booleans survive, and each string becomes
[adp:str bytes=N]. The event carriespayload_digest, sha256 of the canonical JSON of what was supplied, so a producer holding its own copy can still prove the record corresponds to it — "verified, payload not retained" as a real verification state rather than a hole. What answers what the agent did is untouched: the kind, the tool name, the outcome, the model, the token counts, the timings, the commit sha.Both settings live in
adp.yaml, per repository:trajectory: on_secret: redact # the default; `refuse` rejects the whole batch instead payloads: structure # the default; `full` stores payloads as supplied
redactis the default deliberately. Both modes keep the secret out of the database —refuseadditionally throws the trajectory away, and a lost trajectory teaches a user to turn recording off, which costs the record everything and costs the secret nothing.structureis the default on a different argument, and it is an asymmetry rather than a judgement about how much anyone should record: widening tofullis available to a repo that has read what a trajectory holds, and unsending what already arrived is available to nobody. Afullevent carries nopayload_digest— null is what says the payload is verbatim. -
post-receiverecords a signed change per new commit, deduplicated by(repo, sha). A commit that carries anADP-Intenttrailer — the intent's id, or the issue number as#41— is bound to that intent, andADP-Sessionlinks it to a session; both are covered by the signature. The binding rides ongitrather than on an API call, so it works from any harness, and a trailer naming something this repo does not have leaves the change unbound rather than failing the push.
Landing is governed by a three-level land policy: an instance floor (LAND_POLICY_FLOOR,
admin-owned), unioned with the org's floor, unioned with the repo's own adp.yaml — each level can
add requirements, never remove one. Both gates_green and one_approval are enforced identically
on the REST and GraphQL merge paths, and a malformed adp.yaml fails closed. Merges are
fast-forward only.
A fresh instance floors at gates_green alone. A merge is refused until the change has a
green gate result, and that is the whole default — so one developer with one token can clone,
push, gate and land without arranging anything. Turn approval on when there is somebody to give
it, at whichever level owns the decision:
LAND_POLICY_FLOOR=gates_green,one_approval # the whole instance, admin-ownedland:
require: [one_approval] # one repo's adp.yaml, or an org's policy repoBecause the three levels union, a repo or an org can add one_approval to an instance that does
not require it — but nothing below the instance can take it away again.
one_approval is author-independent: an approving review from the principal that authored the
proposal does not satisfy it, so an agent cannot clear the requirement that exists to check it by
approving its own work. Landing under it takes two principals, which is why it is not the default —
a solo evaluator has one, and would be shown a refusal only somebody else could clear. Mint a second
with tsx src/bootstrap.ts reviewer --org <owner>; an agent acting as an independent reviewer wants
its own token for the same reason.
# adp.yaml, read off the base ref — as GitHub reads branch protection off the target branch
gates: [test, lint]
land:
require: [gates_green, one_approval]The server receives and attests gate results; it never executes them.
POST /api/v3/repos/{o}/{r}/gates signs and stores a result, GET .../commits/{sha}/gates lists
them, and they project onto the compatibility plane as Commit.statusCheckRollup. This is the same
division of labor as GitHub's Checks API: external systems report, the forge records and gates. No
first-party scanner is built, by design — the bundled secret engine is the only in-tree detector.
Execution is a separate process. The gate runner in runner/ polls
/api/adp/gate-jobs/claim, executes the job in an isolated container, and reports through the same
signed path any external reporter uses. It is a pure HTTP client — no server import, no database
credential, no signing key — and it belongs on its own host, because a mounted Docker socket is
root on that host. What the isolation actually guarantees, what proves it, and what it explicitly
does not claim are asserted in server/test/ and the runner's own suite.
Repos live in orgs, and the org is the tenancy boundary. Repo access authorizes against the
caller's org on every plane alike — REST, git wire, GraphQL and /api/adp — and the matrix that
proves it is in server/test/e2e-org-isolation.test.ts.
An org carries four more things: its policy floor; a kill switch that refuses every land while set; quotas on repos, concurrent workspaces, concurrent gate jobs and stored bytes; and an audit-log export. That export is a projection of the operation log, not a second system. Org administration is itself audited: quota and policy-repo changes write operations, and the policy floor is a file in a repo, so changing it travels the same signed, reviewable path as code.
Point gh at a running server. Note GH_ENTERPRISE_TOKEN, not GH_TOKEN — that is what gh reads
for any non-github.com host:
export GH_HOST=adp.example.com
export GH_ENTERPRISE_TOKEN=<token>gh treats any unknown host as GitHub Enterprise Server and derives https://HOST/api/v3/, which
is where ADP mounts. The same is true for Octokit and most CI libraries.
Clone and push with a token as the git password:
git clone https://x-access-token:<token>@adp.example.com/<owner>/<repo>.gitSmart HTTP is delegated to the real git http-backend CGI behind auth middleware, so
clone, fetch, pull, push, ls-remote, and shallow, partial, and force-push variants behave exactly as
git does. Delegating to git itself makes fidelity free. SSH is not served; sandboxed agents use
HTTPS and a token.
Functional means the command does real work against the domain model end to end. Partial
means it is callable and answers honestly, but some of what GitHub would return is not backed by
data here. The issue create/view and pr create/view/merge paths are driven by a real, unmodified
gh binary against a live server on every CI run.
| Command | Status | Notes |
|---|---|---|
gh auth status |
Functional | |
gh repo view / clone |
Functional | |
gh repo create <owner>/<name> |
Functional | resolves the owner through GET /api/v3/users/{owner}, then creates through the GraphQL createRepository mutation. The conformance suite runs it, so this row is enforced rather than asserted. The bare gh repo create <name> form is refused: ADP has no personal namespace, and every owner is an org |
gh issue create / list / view / close |
Functional | |
gh issue comment |
Functional | |
gh pr create / list / view [--json] |
Functional | |
gh pr checkout |
Functional | resolves the head ref, then a real git fetch |
gh pr diff |
Functional | REST Accept: …diff / …patch |
gh pr review |
Functional | |
gh pr merge |
Functional | subject to the land policy; refuses with a typed 422 naming each unmet requirement and the command that satisfies it |
gh pr close / reopen |
Functional | |
gh pr comment |
Partial | stored as an issue comment; PR conversation comments are not a separate subject |
gh pr checks |
Functional | each gate result is a StatusContext — name, verdict, and a link to its evidence bundle. Not a CheckRun: that shape implies a workflow run, which ADP deliberately does not have |
gh pr ready |
Partial | recorded as a no-op — there is no draft state; PRs are ready from creation |
gh api <endpoint> |
Functional over the implemented surface | see below |
gh run / release / project / search |
Not supported | returns a clear error |
Unimplemented REST endpoints return 404 with a body naming the ADP equivalent. A broken call that
explains itself costs an agent one turn; a hang or a 500 costs it the trajectory. Not served: search,
Actions, releases, packages, orgs/teams, projects, deployments, branch protection, code scanning,
Dependabot, notifications, gists. Branch protection, code scanning, and Dependabot are absent as
API surfaces on purpose; their capabilities arrive natively through the land policy and push
protection instead of endpoint emulation.
GraphQL loads GitHub's real published SDL (spec/graphql/github.graphql) unmodified into
graphql-js and resolves only the fields ADP backs, including nine mutations. Everything else fails
as a resolver error, never a schema validation error — which is what keeps a partial implementation
from being worse than none, since gh's queries validate against the real schema.
REST under /api/adp, and the same operations over MCP — 21 tools:
| Capability | REST | MCP tool |
|---|---|---|
| Operation log | GET .../operations, .../operations/{id} |
adp_op_log, adp_history_query |
| Undo | POST .../operations/{id}/undo |
adp_undo |
| Evidence bundle | GET .../evidence/{sha} |
adp_evidence_get |
| Workspaces | GET/POST .../workspaces, DELETE .../workspaces/{id} |
adp_workspace_create, adp_workspace_destroy |
| Candidate sets | GET/POST .../candidate-sets, POST .../candidate-sets/{id}/select |
adp_candidates_open, adp_candidates_select, adp_candidates_resolve |
| Sessions and checkpoints | POST .../sessions, .../sessions/{id}/checkpoints, .../sessions/{id}/resume |
adp_session_start, adp_session_get, adp_checkpoint_create, adp_session_resume |
| Trajectories | POST .../sessions/{id}/events, GET .../runs/{id}/trajectory |
adp_trajectory_append, adp_run_trajectory |
| Runs and evals | GET .../runs/{id}/stats, GET .../runs/compare |
adp_run_stats, adp_runs_compare |
| Proposals | POST /api/v3/.../pulls, .../pulls/{n}/reviews, PUT .../pulls/{n}/merge |
adp_proposal_open, adp_proposal_review, adp_proposal_merge |
| Intents | GET /api/v3/.../issues/{n} |
adp_intent_get |
The operation log is filterable by actor, verb, date range, and file path — path filtering resolves the commit behind an operation and asks git which paths it touched. Undo currently covers reverting a landed fast-forward merge, moving the base ref back by the same compare-and-swap the merge used; it refuses if the branch has moved since, rather than silently discarding what landed after. Other verbs return a 422 instead of a no-op that pretends to have worked.
A workspace is deliberately just a git branch with lifecycle metadata, not a new isolation mechanism. Destroying one deletes the ref and marks the row destroyed, so the log stays complete.
Candidate sets are the one primitive here with no GitHub analogue: N competing solutions to a
single intent. A set is opened against an intent, proposals join it by passing candidate_set_id
at creation, and one is eventually selected as the winner — the fan-out/compare/pick shape a fleet
of agents actually produces, which a merge queue does not express.
The last two rows wrap /api/v3 rather than /api/adp, and that is deliberate: a proposal is
the GitHub-shaped object, and an intent comes from an issue — nothing under /api/adp mints one.
Giving either a second native spelling would be a fidelity problem, not a feature. Without these
tools an agent driving the native plane had to break out to a raw curl to open a proposal or read
its own task, which is one command against gh's one command and a hand-assembled HTTP request
against the native plane's.
adp_proposal_merge returns a refused land as the typed policy result — each unmet requirement
with what is missing and the command that satisfies it — rather than an error string. A refusal is
the one response an agent is guaranteed to see on a well-configured instance, and an agent that
cannot read it burns a turn guessing.
The MCP server is a thin wrapper over these same REST endpoints, so behavior is defined in one place rather than duplicated per protocol. Run it over stdio:
ADP_SERVER_URL=https://adp.example.com ADP_TOKEN=<token> npm run mcpA thin command-line wrapper over the REST endpoints above, for scripting and CI steps that would
otherwise be a raw curl. Published to npm as @deduva/adp, with no runtime dependencies:
npx @deduva/adp --help # no install at all
npm install -g @deduva/adp # or put `adp` on your PATH
adp login --server https://adp.example.com --token <token> # writes ~/.adp/config.jsonUntil #235 the package was private: true and the only documented install was a source build, so
ADP could not be obtained without cloning it — every front door this project widened opened onto a
building with no road. The source build still works and is what you want when developing the CLI
itself:
cd cli && npm ci && npm run build
npm link # puts this checkout's `adp` on your PATH; undo with `npm unlink -g @deduva/adp`| Command | Wraps |
|---|---|
adp init [--mirror <url>] [--credential <token>] |
attaches ADP to the repository you are standing in: org, repo, mirror, and an adp.yaml detected from what the repo already says |
adp login --server <url> --token <token> |
writes ~/.adp/config.json (or set ADP_SERVER_URL/ADP_TOKEN) |
adp repo mirror <owner>/<repo> --remote-url <url> --secret <secret> --credential <credential> [--direction outbound|inbound|both] |
POST .../mirror |
adp gate report --repo <owner>/<repo> --sha <sha> --name <name> --status <success|failure|pending> |
POST .../gates |
adp pr list --repo <owner>/<repo> |
GET .../pulls |
adp pr merge --repo <owner>/<repo> --number <n> [--method merge|squash|rebase] |
PUT .../pulls/{n}/merge |
adp pr review --repo <owner>/<repo> --number <n> --state <approved|changes_requested|commented> |
POST .../pulls/{n}/reviews |
adp watch --repo <owner>/<repo> [--pr <n>] |
the proposal, its gates, its runs, and whether it would land |
adp undo <sha> --repo <owner>/<repo> |
finds the merge that produced the commit, and says which undo path it took |
adp reimplement <sha> [--harness <name>] [--model <name>] [--launch] [--compare] |
the intent, the base, a second run related to the first, and the comparison |
adp bakeoff --repo <owner>/<repo> --intent <uuid|#issue> --harness <a,b,c> [--launch] |
a candidate set, one labelled run per harness, and the comparison — --launch runs them, each in its own worktree |
adp runner up --here |
starts a gate runner, or refuses and says why not here |
adp connect <claude-code|codex|gemini-cli> [--model <name>] |
not a wrapper — see below |
adp disconnect <harness> |
undoes exactly what connect wrote |
One command per harness, in the checkout you want connected:
adp connect claude-codeIt mints a token carrying that harness's name, writes the harness's own MCP configuration in its own
format at its own path, installs a prepare-commit-msg hook that adds the ADP-Intent trailer from
the branch you are on, and wires up recording. Then it proves it worked by opening and closing a
real session with the credential it just wrote — because a config written to the wrong path fails
silently and looks exactly like success.
Everything it writes is inside the repository: .mcp.json for Claude Code, .codex/config.toml for
Codex, .gemini/settings.json for Gemini CLI, and .adp/ for the recorder launcher. Those files
hold a live token, so connect adds them to .git/info/exclude — a per-clone ignore, not a
.gitignore entry, because telling every other contributor about one developer's harness is not its
business. adp disconnect <harness> removes exactly what connect wrote, including that block.
Minting needs an admin scope. Without one connect still works and says which half you did not get:
the harness reuses your own token, and the provenance on a signed change names no harness.
The trajectory producer. It records what an agent actually did — every message, model call and tool call, in order and hash-chained — by reading a stream the harness is already producing, from a separate process. Nothing runs inside the agent's context window, so recording costs the agent nothing: 20 paired trials put it at −$0.0022 per trial, 95% CI [−$0.0073, +$0.0029].
Lives in recorder/, built like the CLI, and needs only repo:write — the scope a developer's own
token already carries, so it runs as the developer rather than as infrastructure:
cd recorder && npm ci && npm run build && npm link # puts `adp-recorder` on your PATH
export ADP_SERVER_URL=https://adp.example.com ADP_TOKEN=<token>
# follow a transcript the harness is already writing — it needs no flag and no
# knowledge that anything is watching
adp-recorder tail --repo <owner>/<repo> --file ~/.claude/projects/.../session.jsonl
# or run the harness through it
adp-recorder wrap --repo <owner>/<repo> --harness codex -- codex exec --json "fix the flake"
# and finish anything a previous recorder left undelivered
adp-recorder flushThe session lifecycle needs nothing typed. A session opens when the recorder attaches, bound to
the intent HEAD's ADP-Intent trailer names; it checkpoints at boundaries — a commit, a handoff, a
quiet stretch, the end — rather than on a timer, because a checkpoint is worth having when it is
somewhere you would want to return to; and it ends as closed when the harness finished or
suspended when it did not. Those last two are different facts, and an unclosed session is
indistinguishable from an abandoned one.
A checkpoint names a commit, so ADP has to have that commit: one taken against work you have not
pushed is deferred to the next boundary and the recorder says so. --continue picks up where this
machine's last suspended session in the repository left off, across harnesses — the lineage chain
that results was assembled by nobody:
adp-recorder wrap --repo <owner>/<repo> --harness codex --continue -- codex exec --json "carry on"Which harnesses are covered. A reader translates one harness's private event names into ADP's
fixed vocabulary. One ships for every harness adp connect offers, chosen for having a stable
machine-readable event stream rather than for being the most popular:
--harness |
Reads | Gets |
|---|---|---|
claude-code (default) |
claude --output-format stream-json, or the session transcript it writes |
messages, model calls, tool calls with their outcomes, denied calls as rejected, per-session cost |
codex |
codex exec --json |
messages and reasoning, shell and apply_patch and MCP calls with their outcomes, declined calls as rejected, per-turn tokens (Codex reports no cost) |
gemini-cli |
gemini --output-format json, and the streaming form where the CLI offers one |
the answer, per-model token and latency totals, and tool calls with their outcomes where the stream reports them individually — where it reports only counts, the counts, said to be counts |
That last cell is the interesting one, and it is why a third reader was worth having. Gemini's non-interactive run reports aggregate tool statistics rather than one event per call, and the reader records nine calls that arrived as the number nine as the number nine — a trajectory that claims detail it never had is worse than one that says what it has.
And what an uncovered harness still gets, which is most of it. Commit-level provenance, intent
binding, gates, evidence bundles and land policy all ride on git and the commit trailer rather
than on the harness, so they work from anything that can push. What is missing without a reader is
turn-level detail — the trajectory itself, and the evals and checkpoints that hang off it.
Two ways to close that gap. Write a reader: it is a module exporting createReader() returning an
object with read(line) and end(), documented in
recorder/src/readers/index.ts, and it is loaded with
--reader ./my-reader.js without this package changing. Or emit events directly to
POST /api/adp/repos/{owner}/{repo}/sessions/{id}/events, which is what the recorder itself does.
The server stores harness as a string it never branches on — translation is the recorder's job,
which is what keeps the protocol harness-neutral. An unknown --harness with no --reader is
refused rather than defaulted, because recording one harness's stream through another's parser
succeeds, looks like a recording, and is worthless.
A read-only React SPA served at /ui/* by the same server. It shows issues and pull requests with
their reviews, gate results, and diffs; the evidence view for a commit (signed provenance plus every
DSSE gate attestation); and the operation log with filters. Its one interactive control is an
Undo button on merge operations, calling the same endpoint the MCP tool and a direct API caller
would.
| Variable | Default | Purpose |
|---|---|---|
DATABASE_URL |
— | PostgreSQL connection string |
GIT_ROOT |
— | directory holding bare repositories |
SIGNING_KEY |
— | any secret string; the Ed25519 key is derived via SHA-256 |
PUBLIC_URL |
— | externally reachable base URL |
PORT |
3000 |
listen port |
GIT_MAX_PACK_BYTES |
500 MB |
bounds the git smart-HTTP request body only |
LAND_POLICY_FLOOR |
gates_green |
instance floor; add one_approval for an instance with a second principal, empty string disables |
OIDC_ISSUER, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET |
— | OpenID Connect login. Both client values must be present or the routes do not mount at all |
OIDC_ALLOWED_DOMAINS |
empty | empty means no auto-provisioning: a verified account with no existing link is refused, not welcomed |
STORAGE_METER_INTERVAL_MS |
600000 |
how often each org's storage is re-measured; also the overshoot an org can achieve past its quota |
Auth is bearer tokens with repo:read / repo:write / admin scopes, enforced on every REST
route, the GraphQL endpoint, and the git route. Reads are private by default. Token lookup is by an
indexed sha256 key, with scrypt verification doing the actual authentication.
The server runs locally or under Docker Compose. Setup, bootstrapping the first token, and the
three-tier test suite are documented in server/README.md.
cd server
npm install
npm run migrate
npm run devOn a machine that has never seen this project, one command provisions it and one loop runs everything against a throwaway database that is destroyed afterwards:
bash scripts/dev/bootstrap.sh # toolchain, Docker, dependencies
make up && make test-all && make down # bring up, run, tear down, assert cleanmake down asserts the machine is clean rather than assuming it — no leftover containers, volumes,
server processes or temp directories. On Windows, tools/win/Run-CleanTest.ps1 runs the same loop
inside a throwaway WSL distro and deletes it afterwards, so a full verification leaves nothing
behind at all.
Self-hosting is documented in docs/self-hosting.md: a Helm chart
(helm/adp) and the Docker Compose path in deploy/, what the two decisions
are that cannot be defaulted for you, and why the gate runner needs a node of its own.
CI runs typecheck, build, migrations against a fresh Postgres, the full unit/integration/e2e suite —
including a real clone → push → propose → review → merge cycle — and the gh conformance gate, on
every pull request. A separate clean-room workflow provisions a bare container from scratch and runs
the same loop, so the "brand new machine" path stays verified rather than assumed.
| Document | What it is |
|---|---|
docs/companion-mode.md |
ADP underneath GitHub: what stays on github.com, what ADP records without a command being run, the two checks that appear on the pull request, and what it deliberately refuses to do. |
docs/self-hosting.md |
Running your own instance: the Helm chart, the Compose path, what the chart refuses to guess and why, and where the sharp edges are. |
docs/api-compatibility.md |
What the contract version promises, and what a bump means for a generated client. |
docs/server-stack-tutorial.md |
The server stack explained piece by piece, no prior familiarity assumed. |
docs/observability.md |
What is measured, what pages, and what to do when it does. |
docs/ecosystem.md |
Who depends on ADP and how — the dependency graph, and what a change here requires elsewhere. Read this before changing the wire contract. |
CHANGELOG.md |
What shipped, per released version. |
PLAN.md |
The backlog: what is left, in what order, and the open decisions. |
Contributing to ADP itself needs two more. AGENTS.md has the branch and review
conventions, the invariants that look wrong until you know why, and the commands that actually run.
docs/test-environment-automation.md covers how the test
environment is brought up and torn down.
MIT — code, spec, conformance suites, and prose alike. Nothing in this repository needs a second look from legal before you run it, fork it, or build on it.