Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
c103650
docs: plan the work that makes the dashboard usable
RedStar071 Sep 5, 2026
f61351d
feat(dashboard): start the dashboard alone with dev:solo
RedStar071 Sep 5, 2026
5d1bb66
refactor(api): move the audit trail onto evlog and into the oRPC router
RedStar071 Sep 5, 2026
fb5f20a
feat(dashboard): follow the control plane live on the board
RedStar071 Sep 5, 2026
72e43e6
feat(dashboard): let the board act on what it shows
RedStar071 Sep 5, 2026
eabb4e0
feat(dashboard): find pull requests to review without a webhook
RedStar071 Sep 5, 2026
282cc3c
fix(api): let a repository allow-list stand without operator tokens
RedStar071 Sep 5, 2026
cdeb447
feat(cli): run on a deployment with --remote
RedStar071 Sep 5, 2026
bf170ae
test(dashboard): cover the write signal the live board depends on
RedStar071 Sep 5, 2026
06968df
chore: add VS Code CLI binaries
RedStar071 Sep 5, 2026
46cd59f
feat(api): persist repository access in the control plane
RedStar071 Sep 6, 2026
c9433b5
chore: use latest aube and sync dashboard dependencies
RedStar071 Sep 6, 2026
94312d1
[autofix.ci] apply automated fixes
autofix-ci[bot] Sep 6, 2026
44124e0
refactor(dashboard): read control-plane policy from configuration, no…
RedStar071 Sep 6, 2026
9091487
chore: update mise.toml
RedStar071 Sep 6, 2026
6c534d2
feat(dashboard): broadcast live overview updates
RedStar071 Sep 6, 2026
10523be
feat(api): add repository configuration endpoints
RedStar071 Sep 6, 2026
c7dff6c
fix(api): preserve review safety across delivery and config parsing
RedStar071 Sep 6, 2026
904f1dd
fix(dashboard): dispatch poller reviews concurrently and share the we…
RedStar071 Sep 6, 2026
7df2e24
fix(dashboard): fix stuck claims, cross-provider dedup, and poller/ov…
RedStar071 Sep 6, 2026
fbf4d31
fix(dashboard): use evlog errors for transport failures
RedStar071 Sep 7, 2026
0fd2df6
test(dashboard): verify errors omit fatal and unhandled flags
RedStar071 Sep 7, 2026
73d4a63
chore(dashboard): rename standalone development command
RedStar071 Sep 7, 2026
a9d9fe6
fix(dashboard): bound live streams and normalize repository paths
RedStar071 Sep 7, 2026
e7f1173
[autofix.ci] apply automated fixes
autofix-ci[bot] Sep 7, 2026
d3fb5c5
fix(dashboard): avoid returning the stream push promise
RedStar071 Sep 7, 2026
ee8df24
ci(i18n): regenerate the dashboard schema for the new task-composer keys
RedStar071 Sep 7, 2026
877e0de
fix(dashboard): avoid duplicate reviews and paginate GitHub pulls
RedStar071 Sep 7, 2026
818df73
fix(source-control): define the missing pagination cap for pull reque…
RedStar071 Sep 9, 2026
cdb86f8
fix(cli): reject non-terminal remote task results
RedStar071 Sep 9, 2026
5b8396d
fix(api): recognize admin among comma-separated Better Auth roles
RedStar071 Sep 10, 2026
e0399e2
fix(dashboard): recover control-plane access after a failed config load
RedStar071 Sep 10, 2026
7a15d51
fix(database): stop saveRepository resetting fields a caller left out
RedStar071 Sep 10, 2026
9f22bfc
fix(dashboard): resolve solo-mode repository paths before storing them
RedStar071 Sep 10, 2026
bee6d7c
chore: remove committed VS Code CLI binaries
RedStar071 Sep 10, 2026
ca5f258
docs: remove dead control-plane env vars and fix stale aube pin
RedStar071 Sep 11, 2026
e629b6c
ci: stop seeding aube store cache from a stale lockfile hash
RedStar071 Sep 11, 2026
ae16db4
Revert "ci: stop seeding aube store cache from a stale lockfile hash"
RedStar071 Sep 11, 2026
4ff2b5d
fix(deps): repair truncated peer ids in the lockfile
RedStar071 Sep 11, 2026
1e1f392
fix(dashboard): keep the fields a memory-store save leaves out
RedStar071 Sep 11, 2026
c51178d
docs: point control-plane policy at the deployment file and the store
RedStar071 Sep 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,20 @@ test-report.junit.xml
.env
.env.*
!.env.example
# Checked in like the examples above: it configures `dev:solo`, whose whole point is that it holds
# nothing worth keeping out of the repository.
!.env.solo
.code-zero/
.data/
*.log
.DS_Store

# Skilld references (recreated by `skilld install`)
.skilld

# Local editor tooling; never something a checkout should carry.
/code
/vscode_cli.tar.gz
code-zero.deployment.yml
!code-zero.deployment.example.yml
!apps/dashboard/code-zero.deployment.solo.yml
5 changes: 4 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Code Zero is an open-source autonomous engineer that finds, fixes, and verifies
**Key information:**

- Node version: `24.19.0` (`>=24.2` supported; see `mise.toml` and `engines`)
- Package manager: `aube@1.38.0` (pinned in `package.json` via `packageManager`)
- Package manager: `aube@1.41.0` (pinned in `package.json` via `packageManager`)
- TypeScript: `^5.9.2`, overridden to `typescript-native-bridge` so checks run on tsgo
- Main branch: `main`

Expand Down Expand Up @@ -85,6 +85,9 @@ cp apps/dashboard/.env.example apps/dashboard/.env

```bash
aube run dev # watch workspace development tasks
aube run dev:solo # dashboard alone, no database (apps/dashboard/.env.solo)
aube run dev:docs # docs site alone
aube run dev:marketing # marketing site alone
aube run zero doctor # inspect the local environment
aube test # deterministic Vitest suites
aube run test:browser # dashboard and marketing browser suites
Expand Down
36 changes: 30 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,27 @@ aube run dev
The root `.env` configures the CLI. Each app loads its own file: the dashboard uses
`apps/dashboard/.env`, while the docs app optionally uses `apps/docs/.env` for `NUXT_APP_BASE_URL`.

To see the dashboard before configuring anything, start it on its own instead:

```bash
mise install
aube ci
aube run dev:solo # http://localhost:3000, then sign up at /signup
```

`dev:solo` is `nuxt dev` reading [`apps/dashboard/.env.solo`](./apps/dashboard/.env.solo) in
place of `.env`: Better Auth runs on an in-memory store, so there is no Postgres to install and no
migration to apply, and the account you create lives until you stop the process. Nothing else
about the app changes — it is the same UI, the same router, and the same authentication endpoints
a deployment serves. Tasks still need a checkout to target, so add one to
`CODE_ZERO_SOLO_REPOSITORIES` in that file; `observe` runs no model, so a task can be
created and inspected without a provider credential. Use `aube run dev` and `apps/dashboard/.env`
for anything that has to persist.

`dev:docs` and `dev:marketing` start those two apps on their own the same way `mail:preview`
already does for `apps/mail-preview` — a plain `turbo run dev` filtered to one app, with no
alternate env file.

`aube run <script>` and `aube test` check install freshness first, so a separate install step is rarely needed. aube reads and writes the existing `pnpm-lock.yaml` and `pnpm-workspace.yaml` in place — the lockfile stays in pnpm's v9 format for anyone who still runs pnpm.

---
Expand All @@ -118,10 +139,13 @@ zero logout [--url X] forget a stored session
zero review (--feedback X | --proactive) inspect without editing
zero fix (--feedback X | --proactive) validate, edit, and verify (policy permitting)
zero run (--feedback X | --proactive) run using the configured mode
zero run --remote [--url <origin>] run it on a deployment instead of here
```

The CLI parses arguments with [`@bomb.sh/args`](https://github.com/bomb-sh/args) and renders with [`@clack/prompts`](https://github.com/bombshell-dev/clack). Use `--proactive` to inspect the working-tree diff without reviewer feedback. When neither trigger is provided in a terminal, it asks for the task interactively; use `--feedback` or `--proactive` with `--json` for scripts and CI.

`--remote` hands the run to a deployment's control plane instead of executing it in this checkout, so it lands in the same task history the dashboard reads and appears on the board while it runs. It uses the session `zero login` stored, presented as a bearer token, so the run is attributed to the person who signed in rather than to a shared operator token — which means the deployment needs `AUTH_ENABLE_DEVICE_AUTHORIZATION=true`, the same flag `zero login` already requires. The deployment is chosen with `--url` or `CODE_ZERO_URL`; the flag is deliberate rather than inferred from that variable, which already selects which deployment `login` and `logout` act on. The repository sent is this checkout's path, and the deployment's own configured repositories decide whether it may be targeted. Exit codes are the same table a local run uses, so CI reads either the same way.

`zero login` runs the [RFC 8628](https://datatracker.ietf.org/doc/html/rfc8628) device flow: it prints a short code, you approve it at the deployment's `/device` page in a browser you are already signed into, and the CLI stores the resulting session token in `$XDG_CONFIG_HOME/code-zero/credentials.json` (owner-readable only). The same command serves a cloud-managed deployment and a self-hosted one — pick which with `--url`, or set `CODE_ZERO_URL`; without either it targets `http://localhost:3000`. Tokens are kept per origin, so signing into one deployment never evicts another, and `zero logout` without `--url` forgets all of them. The deployment must have `AUTH_ENABLE_DEVICE_AUTHORIZATION=true`; it is off by default. That flag also registers Better Auth's `bearer` plugin, which is what lets the stored token be presented as `Authorization: Bearer <token>` — without it the flow would mint a session that only a cookie could carry. `zero doctor` lists which deployments have a stored session and whether it has expired, never the token itself.

---
Expand All @@ -130,13 +154,13 @@ The CLI parses arguments with [`@bomb.sh/args`](https://github.com/bomb-sh/args)

`aube run dev` starts the single deployable app on `http://localhost:3000` (override with `PORT`). It is the only adapter that composes a runner for hosted work, and the same Nuxt app serves the UI, the control plane, and authentication from one origin:

| Surface | Purpose |
| -------------- | ----------------------------------------------------------------------------------------------------- |
| `/rpc/**` | Typed oRPC router: `health`, `dashboard.overview`, `tasks.list/get/create`, `approvals.decide` |
| `/api/v1/**` | The same router over OpenAPI/REST; interactive docs at `/api/v1/docs`, spec at `/api/v1/openapi.json` |
| `/api/auth/**` | The Better Auth handler (mounted by `@onmax/nuxt-better-auth` from `server/auth.config.ts`) |
| Surface | Purpose |
| -------------- | ------------------------------------------------------------------------------------------------------------ |
| `/rpc/**` | Typed oRPC router: `health`, `dashboard.overview`, `tasks.list/get/create`, `approvals.decide`, `audit.list` |
| `/api/v1/**` | The same router over OpenAPI/REST; interactive docs at `/api/v1/docs`, spec at `/api/v1/openapi.json` |
| `/api/auth/**` | The Better Auth handler (mounted by `@onmax/nuxt-better-auth` from `server/auth.config.ts`) |

`/rpc/**` and `/api/v1/**` are the same `rpcRouter` from [`packages/api`](./packages/api) served over two wire protocols, so authorization behaves identically either way. Reads are open for the dashboard; mutations (`tasks.create`, `approvals.decide`) fail closed. `CODE_ZERO_CONTROL_PLANE_TOKENS` holds comma-separated `name:token` bearer credentials, and `CODE_ZERO_CONTROL_PLANE_REPOSITORIES` allow-lists the repository paths `tasks.create` may target; without them every mutation is rejected. `CODE_ZERO_CONTROL_PLANE_MODES` holds comma-separated `name:mode|mode` grants for the execution modes each principal may request; without a grant a principal may only request the non-writable `observe` and `suggest` modes, so `fix` and `autonomous` require an explicit operator grant. The approval actor is the authenticated principal's name, never a wire-supplied value. This bearer-token scheme authorizes the control-plane API and is independent of the Better Auth session that protects the dashboard UI.
`/rpc/**` and `/api/v1/**` are the same `rpcRouter` from [`packages/api`](./packages/api) served over two wire protocols, so authorization behaves identically either way. Reads are open for the dashboard; mutations (`tasks.create`, `approvals.decide`) fail closed. `CODE_ZERO_CONTROL_PLANE_TOKENS` holds comma-separated `name:token` bearer credentials — the one half of this policy that is a secret, and so the one half still in the environment. Which execution modes each principal may request is `control_plane.modes` in `code-zero.deployment.yml`; without a grant a principal may only request the non-writable `observe` and `suggest` modes, so `fix` and `autonomous` require an explicit operator grant. Which checkouts `tasks.create` may target is neither a secret nor fixed for the life of the process, so it is the `repository` table an operator edits from the dashboard; with none configured every task creation is refused. The approval actor is the authenticated principal's name, never a wire-supplied value. This bearer-token scheme authorizes the control-plane API and is independent of the Better Auth session that protects the dashboard UI.

Typed clients infer their shape from the router rather than redeclaring request and response types:

Expand Down
39 changes: 28 additions & 11 deletions apps/dashboard/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -22,24 +22,41 @@ CODE_ZERO_MODEL=gpt-5
GITHUB_TOKEN=

# Control-plane API (packages/api), served at /rpc/** and /api/v1/**. Fails closed: without
# CODE_ZERO_CONTROL_PLANE_TOKENS every mutation is rejected while reads stay open.
# Comma-separated name:token bearer credentials.
# CODE_ZERO_CONTROL_PLANE_TOKENS every mutation by a machine caller is rejected while reads stay
# open. Comma-separated name:token bearer credentials — the only half of this policy that is a
# secret, which is why it is the only half still spelled here.
#
# What each principal may run, and which origins may read /api/v1/** cross-origin, are policy
# rather than secrets and live in `code-zero.deployment.yml` (see code-zero.deployment.example.yml).
# Which checkouts a task may target is neither: it changes while the process runs, so it is a table
# an operator edits from the dashboard.
CODE_ZERO_CONTROL_PLANE_TOKENS=
# Comma-separated repository paths tasks.create may target.
CODE_ZERO_CONTROL_PLANE_REPOSITORIES=
# Comma-separated name:mode|mode execution-mode grants. Without a grant a principal may only
# request the non-writable observe and suggest modes.
CODE_ZERO_CONTROL_PLANE_MODES=
# Comma-separated origins allowed to read /api/v1/** cross-origin via CORS. Empty by default:
# tasks.list/tasks.get/health are unauthenticated by design, so letting a browser read their
# responses from another origin is an explicit opt-in, not the default.
CODE_ZERO_CONTROL_PLANE_ORIGINS=
# Path to the deployment configuration file. Defaults to `code-zero.deployment.yml` in the working
# directory; a deployment that has changed nothing ships no file and gets the defaults.
CODE_ZERO_CONFIG=

GITHUB_WEBHOOK_SECRET=
# Checkout the webhook route binds incoming events to. POST /webhooks/github fails closed
# (503, nothing ingested) until both this and GITHUB_WEBHOOK_SECRET are set.
CODE_ZERO_CHECKOUT_PATH=

# Polling (server/plugins/poller.ts), the pull-based half of the same job the webhook does: it
# finds open pull requests to review without this deployment needing a public URL. It needs
# GITHUB_TOKEN above, and watches nothing until a repository is configured with polling on.
#
# It runs an interval inside the server process, so it belongs to a deployment that stays up; a
# serverless target freezes between requests and would poll only by accident. It shares the durable
# delivery claims with the webhook route, so the two never review the same commit twice.
#
# Which repositories it watches, and the mode each one runs in, are rows in the `repository` table
# rather than variables here: an operator turns polling on for a repository from the dashboard and
# the next pass picks it up, with no restart. The interval between passes is in
# `code-zero.deployment.yml`. Each row pairs a repository on the provider with the checkout on this
# host a run may execute against; the path is never derived from the slug, so a run can only ever
# target a checkout an operator named. That checkout has to be kept current (a periodic
# `git fetch`) — a review reads the diff between the pull request's base and head commits, so a
# checkout missing them fails the run rather than reviewing the wrong thing.

# Database (packages/database). Owns the schema and the migrations; this app is the only process
# that opens it. The pre-split name AUTH_DATABASE_URL is still read when this is unset.
# `packages/database/.env` carries the same connection string for drizzle-kit: keep the two
Expand Down
36 changes: 36 additions & 0 deletions apps/dashboard/.env.solo
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Environment for `aube run dev:solo` — the dashboard on its own, with no Postgres and no model
# credentials. Loaded only by that script (`nuxt dev --dotenv .env.solo`); `nuxt dev`, `nuxt build`,
# and a deployment all keep reading `.env`, which this file never touches.
#
# Checked in on purpose: it holds no secret worth keeping. The session store is in memory, so the
# signing secret below protects nothing that outlives the process, and the control-plane token is
# only accepted by a server started this way.
#
# Edit CODE_ZERO_SOLO_REPOSITORIES to point at a checkout you want to run tasks against.

# Better Auth runs on an in-memory store, so the dashboard starts without a database. Same flag the
# Playwright preview server sets (see playwright.config.ts); `server/auth.config.ts` documents why
# it is not guarded by NODE_ENV. Never set it in `.env` or in a deployment's environment.
AUTH_E2E_MEMORY=true
# Nothing is ever dialed once the memory adapter replaces the database, but the options still have
# to build, so the resolver needs a value here.
DATABASE_URL=postgres://solo:solo@127.0.0.1:5432/solo
# Sessions live and die with the process, so this is a fixed placeholder rather than a real secret.
NUXT_BETTER_AUTH_SECRET=dev-only-solo-secret-not-used-in-any-deployment
# The store is empty at every start, so the first thing to do is create an account.
AUTH_ENABLE_SIGNUP=true

# Control plane. `observe` runs no model, so a task can be created and inspected without any
# provider credential; `suggest` and `fix` need one from `.env`.
CODE_ZERO_CONTROL_PLANE_TOKENS=solo:solo
# What the `solo` token may run is policy, so it comes from the deployment configuration rather
# than from here: `code-zero.deployment.solo.yml`, named by CODE_ZERO_CONFIG below.
CODE_ZERO_CONFIG=code-zero.deployment.solo.yml

# The repository table is in memory too and starts empty on every boot, and the procedures that
# would fill it require an administrator, which a freshly created throwaway account is not — so
# this seeds it. Read only when AUTH_E2E_MEMORY is true; a real deployment configures repositories
# from the dashboard instead. Entries are `owner/name=/path` (watched by the poller) or bare
# `/path` (allow-listed only), comma-separated. Empty means every task creation is refused, which
# is the safe default; add your own checkout to run one.
CODE_ZERO_SOLO_REPOSITORIES=
8 changes: 6 additions & 2 deletions apps/dashboard/app/pages/(dashboard)/audit.vue
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,12 @@ import { useHotkeys } from '@tanstack/vue-hotkeys';

import { useShortcutsDialog } from '../../../modules/shared/composables/useAppShortcuts';

const { rows, pending, error, authError, authEnabled, hasMore, refresh, loadMore } =
useAuditTrail();
// The typed client lives on the Nuxt app; the composables take the one call they need rather than
// the app instance, so they stay drivable from a plain unit test.
const { $orpc } = useNuxtApp();
const { rows, pending, error, authError, authEnabled, hasMore, refresh, loadMore } = useAuditTrail(
(query) => $orpc.audit.list(query),
);
const dialogOpen = useShortcutsDialog();

// Client-side only: both trails authenticate the browser's session, and this page is not
Expand Down
Loading
Loading