alden

Architecture

Alden is a TypeScript monorepo. The engine does the whole review locally; the CLI, the local UI, the desktop app and alden mcp render its output. Deterministic analysis handles what can be proven, and the model judges on top of it.

Packages

Package What it is
packages/schema The shared formats, as zod schemas: config, ranked queue, briefing. JSON Schemas are generated into packages/schema/json, and a test fails when they're stale.
packages/core The engine: config, GitHub, the queue, diffs, checks, the code graph and the model layer.
packages/cli The alden command and terminal rendering, the web UI's server (src/ui) and the MCP server for AI coding tools (src/mcp: the protocol over stdio, the tools and their Markdown output). The only published package; tsup bundles core and schema into it.
packages/ui The web UI: React and Vite, talking only to the local API, so the desktop app can wrap it. The CLI build copies it into dist/ui.
packages/desktop The macOS app: a Tauri 2 shell that runs the standalone binary as alden ui --json and shows it in a window and the menu bar. See its README.

Inside core

Directory What it does
github/ Octokit client with retries and rate-limit handling, token resolution (env, keychain, gh), device sign-in, CODEOWNERS, PR details and the review-request search.
queue/ File classification (code, test, docs, lockfile…) and ranking into "needs you" and "fast-track".
diff/ A unified-diff parser, and diff sources: GitHub's per-file patches, and git diff for local changes.
review/ The checks, "needs your eyes" picking, briefing assembly, and reviewPullRequest / reviewLocalChanges.
review/llm/ The model's prompt (diff packed by priority to fit the budget), validation of its findings against the diff, and merging them into the briefing.
llm/ Model aliases and prices, providers (Anthropic, Bedrock, Vertex, Foundry, OpenAI-compatible), budgets and the spend ledger.
graph/ tree-sitter parsing and symbol extraction, the repo index and its cache, changed symbols, import resolution, caller queries, and PR worktrees.
feedback/ The feedback log (verdicts and misses), its summary, and the redacted export.
learn/ What Alden learns from one reviewer in one repo, as plain counts, and how it moves flags and queue rows.
stats/ Time to first review and cycle time for alden stats, the first use of Alden per repo, and review sessions for time to review (opened on a briefing, ended by sendReview).
store/ The SQLite database, ~/.alden/alden.db (node:sqlite, or Bun's in the standalone binary), and its migrations.
telemetry/ Opt-in usage stats: consent, the salted repo hash, the queue in the database and sending to PostHog.

The web UI's server

alden ui runs packages/cli/src/ui/server.ts: a node:http server on 127.0.0.1 that serves the built UI and a JSON API. api.ts wires the API to the engine with the same calls the CLI commands make. Every API call needs the session token and the server's own Host, and reviews stream newline-delimited JSON: a stage event per step (from core's onStage callback), then the briefing or an error. Any path outside /api/ serves the built UI.

Method and path What it does
GET /api/session Alden's version, the folder and repo it runs in, the GitHub user (or, signed out, why: the keychain refused or there's no sign-in), the model and where its key comes from, the budget, and whether it's the desktop app and background briefings are on.
GET /api/queue The ranked queue. refresh=1 skips the cache; cached=1 answers with the last queue (up to a day old) at once, if there is one; repo and org (repeatable) narrow it. A fresh one runs the follow-up check first (as GET /api/follow-ups does, sharing its once-a-minute limit; at most 8 seconds) and carries Your turn as yourTurn.
POST /api/review Runs a review of { pr?, llm?, graph?, model?, effort? } and streams it as above. No pr reviews local changes. A PR's model briefing also makes Alden's drafts unless drafts.auto is off.
POST /api/prepare Briefs the next of the top background.prepare (3) PRs in Needs you, in your ranked order, in the background. Acts only when started with --json (the desktop app) and background.enabled is on; asks the model only if background.model is true in the user config.
GET /api/prepared Briefings prepared in the background; with pr=owner/repo#12, that PR's briefing, unless the PR changed since.
POST /api/freshness Whether a briefing still matches its PR: { target } (a briefing's target) → fresh, pr_changed, base_moved (with the base and how many commits) or unknown. One GitHub request.
GET /api/spend Today's and recent spend against your caps.
GET /api/config The merged settings.
PUT /api/config Sets { key, value } in the user config; a null value removes it.
GET /api/stats alden stats for repo, over days (90), split at since; refresh=1 skips the cache.
GET /api/review-time Time to review over the last 14, 30 and 90 days, from this machine's review sessions.
POST /api/review-time Active time on a PR's pane: { pr, activeMs } (at most 5 minutes a report), added to its review session under way.
GET /api/feedback The feedback summary.
POST /api/feedback Records a verdict on a flag, or a miss.
GET /api/feedback/export Your feedback, always redacted (see Privacy and data).
POST /api/reaction Records { pr, reaction } (up, down, dismiss or clear) on a row of the last queue.
GET /api/memory What Alden knows about you in repo (default: the clone it runs in).
POST /api/memory Adds a note: { text, repo? }.
GET /api/memory/repos Each repo Alden has memory for.
POST /api/memory/change { action, repo? }: edit (with id, text), restore (with id), learning (with on) or forget-all.
POST /api/memory/forget Forgets one item: { id, repo? }.
GET /api/drafts pr=owner/repo#12's pending review: { drafts, unaddressed }, the drafts brought to the PR's current head (moved with their lines, or outdated), and the places its latest briefing flagged that no draft is near.
POST /api/drafts Adds a draft at the PR's head: { pr, body, path?, line?, startLine?, side?, suggestion? }; without path, a general note. body may be left out with a suggestion. A line or file GitHub won't take an inline comment on becomes a linked note. Always yours (source: "you"). 422 for a range the wrong way round, an old-side line outside the diff, or a link to code that isn't at the head.
PATCH /api/drafts/:id Changes a draft's { body?, suggestion? } (suggestion: null removes it), or places it again with { path, line?, side?, startLine? }, which brings an outdated draft back.
DELETE /api/drafts/:id Deletes a draft: { ok: true }. On one of Alden's drafts, that's Drop, which teaches a little (see Alden's drafts).
POST /api/drafts/keep Keeps Alden's drafts: { ids } or { pr } (all of them on it) → { kept }. Each becomes yours (source: "you", keeping drafted) and is sent with the review; untouched, they never are. Editing one with PATCH keeps it too.
POST /api/drafts/alden Draft my review: { pr, tone? } → { made, skipped, asked, costUsd, drafts }. tone (terse, concise, detailed, teaching) is for this call; without it, the repo's tone. 409 without a model, a briefing of the PR's head, or on a PR with text aimed at AI reviewers, or over budget; 502 when the provider fails.
POST /api/drafts/suggest-text Draft with Alden: { pr, text?, tone? } with { path, line?, side?, startLine? } for a comment on code or { threadId } for a reply → { text, costUsd }. Saves nothing; closing the request stops it.
GET /api/tone The tone Alden drafts in for repo=owner/name: { tone, source: "repo" | "default" } (ToneStateSchema), the repo's own on this machine or drafts.tone. Without repo, the default.
PUT /api/tone { repo?, tone }: with repo, that repo's own tone on this machine (tone: null removes it, back to the default); without, sets drafts.tone in the user config. Answers as GET.
POST /api/ask Ask about this PR: { pr, question, model? } streamed as newline-delimited JSON (AskEventSchema): { type: "delta", text } as the answer is written, then { type: "done", costUsd, model } or { type: "error", message, status } (409: no model, no briefing of the PR's head, text aimed at AI reviewers, over budget; 502: the provider failed). 400 (plain JSON) for a bad body. Closing the request aborts the model call. See Ask about this PR.
GET /api/diff The Diff tab: pr=owner/repo#12's diff at its current head, or without pr the local changes a briefing of them covers, as { headSha, files: [{ path, previousPath?, status, additions, deletions, binary?, patchMissing?, hunks: [{ header, lines: [{ kind: add | del | ctx, oldLine?, newLine?, text }] }] }] } (PrDiffSchema). Built from the PR files API's patches or git diff HEAD plus untracked files; a file whose patch was too large has hunks: [] and patchMissing: true. Control sequences are stripped from every string, as on every route. A PR's diff is kept per head commit while the server runs, so asking again costs one GitHub request; local changes aren't kept.
POST /api/review/send Sends the PR's drafts (except Alden's you haven't kept) to GitHub as one review: { pr, event: COMMENT | APPROVE | REQUEST_CHANGES, body? } → { url, posted, review }. The only route that writes to GitHub. Refuses (409) when a draft is outdated or the PR moved since the drafts were listed, 422 when a line isn't in the diff, 403 with the fix when the sign-in can't write reviews. Sent drafts are deleted.
POST /api/briefing/markdown { briefing } as Markdown, as alden review --markdown prints it: { markdown, fileName }.
POST /api/briefing/save Desktop app: saves { briefing } as Markdown to the Downloads folder and says where.
POST /api/feedback/save Desktop app: saves the redacted feedback export to the Downloads folder.
POST /api/open Desktop app: opens { url } (http or https, never Alden's own page) in the default browser.
GET /api/follow-ups Desktop app: checks the open PRs you reviewed in the last 14 days (one GitHub request to find them, then one per PR whose updatedAt moved; at most once a minute) and answers { notifications: [{ pr, title, url, text, events }], sound }: one per PR with news not yet shown, at its level. Read only.
POST /api/follow-ups/ack Marks { events } (the ids a notification covered) shown, so they're never shown again.
POST /api/follow-ups/seen Marks everything on { pr } seen (and shown): you opened it, so it leaves the queue's Your turn.
GET /api/watch pr=owner/repo#12's notification level: { pr, override?, level } (PrWatchSchema).
PUT /api/watch Sets pr's own level, { level: off | rereview | your-turn | every-reply }, or { level: null } for the global one.
GET /api/rank Settings → Ranking: your weights, rules and background.prepare.
PUT /api/rank Saves { weights?, rules?, prepare? } to the user config ([queue.rank], background.prepare); a null weight or prepare goes back to the default.
POST /api/rank/preview The current queue ranked with { weights?, rules? }, unsaved, beside where each PR sits now.
POST /api/auth/device Starts GitHub sign-in with a one-time code (the device flow); the server waits for approval.
GET /api/auth/device How that sign-in is going: waiting (with the code), done, or failed.
POST /api/auth/device/open Opens GitHub's page for the code in the default browser.
POST /api/auth/gh Drops Alden's stored sign-in, so it uses the GitHub CLI's.
POST /api/auth/sign-out The avatar menu's Sign out: removes Alden's stored sign-in, as alden auth logout does, and says what still signs you in.
PUT /api/model-key Stores { key } (an Anthropic or OpenAI API key, told apart by its shape) in the keychain, as alden auth model does; { key: null, provider? } removes the stored key of provider (anthropic unless said). Write-only: answers { provider, key } with where that key comes from, never the key.
GET /api/model-setup What "Set up a model" offers: what reviews use now, the Anthropic and OpenAI keys Alden finds (key, openaiKey: where from, never the key), whether Ollama runs and what it has pulled, this machine's memory and the local model recommended for it.
POST /api/model-setup/test-key Tests { key }, Anthropic's or OpenAI's, with one tiny request to its provider (Haiku for one output token, or gpt-6-luna for a few) and stores it if it works, making that provider the model; untested: true stores it without the request. Answers { ok, provider, key } or { ok: false, reason, message }, never the key.
GET /api/models The model picker (ModelChoicesSchema): { current: { provider, model }, groups: [{ provider, label, via, available, items: [{ id, note }] }] }. Groups: Anthropic and OpenAI ("your key"), Local ("Ollama", the models it has pulled) and the configured Bedrock, Vertex or Foundry; each available only when set up here. current.provider is ollama for a local model, openai-compatible for another endpoint.
PUT /api/models { provider, model } (ModelPickSchema): makes it the model in your user config (model.provider, model.name; Ollama as openai-compatible) and answers as GET does. A provider that isn't set up is a 409, a name that isn't a model (or an Ollama model not pulled) a 400.
POST /api/model-setup/local Makes { model } (an Ollama model name) the model, pulling it first if Ollama hasn't; streams progress lines, then done or error, as a review does. Closing the request stops the download.
POST /api/model-setup/open Opens one of the setup step's pages (keys, openaiKeys, cloud or ollama) in the default browser, for the desktop window. Only those four.
GET /api/clone The local clone PR briefings on repo find callers in (here, remembered, or under clones.folders), or null.
GET /api/cli Desktop app: whether the alden command is installed, and where.
POST /api/cli/install Desktop app: links its alden into /usr/local/bin (macOS may ask for a password).
GET /api/telemetry Whether usage stats are on, and what blocks them, if anything.
PUT /api/telemetry Turns usage stats on or off: { on }.

How a review flows

  1. Target. A PR (URL, owner/repo#n, or a queue number) or the local working tree.
  2. Diff. Fetched from GitHub or read from git, then parsed into files, hunks and lines.
  3. Code graph (unless --no-graph). The repo is indexed from the working tree or a PR worktree. Changed lines are mapped to symbols, and the callers of changed public symbols are found. Failures become a "Not checked" note.
  4. Checks. Each check gets the diff, PR details and graph, and returns passed, flagged or skipped, with evidence.
  5. Pick. Up to review.eyes (3 by default) "needs your eyes" places are chosen from the flagged checks.
  6. Model (unless --no-llm or unconfigured). The prompt is built from the diff, the check results and caller snippets, and sized to the budget. budgetedGenerate is the only way to call a model: it checks the worst-case cost against both caps, then records the actual cost in the ledger. Findings are checked against the diff, then merged: deterministic high and critical picks keep their slots. The model step never throws; failures become a status and a note.
  7. Render. The briefing, a plain object matching briefing.v1, is printed as text or JSON.

Principles