Skip to content

feat(dashboard): make the live board actionable - #67

Open
RedStar071 wants to merge 30 commits into
mainfrom
feat/dashboard-live-control
Open

feat(dashboard): make the live board actionable#67
RedStar071 wants to merge 30 commits into
mainfrom
feat/dashboard-live-control

Conversation

@RedStar071

@RedStar071 RedStar071 commented Sep 5, 2026

Copy link
Copy Markdown
Member

Summary

  • Add live dashboard updates through a server-sent event stream and polling-backed overview refreshes.
  • Let operators create tasks, inspect live status, and approve or reject pending decisions from the board.
  • Add pull-request discovery without requiring a public webhook endpoint.
  • Add zero run --remote for dispatching work to a deployment's control plane.
  • Add dev:solo for running the dashboard with in-memory authentication and no Postgres.
  • Move audit-log access into the typed oRPC router and update the dashboard composables accordingly.

Why

This makes the dashboard a usable control surface instead of a read-only snapshot. Operators can see control-plane activity as it happens, create work, and resolve approval requests from the same interface.

The changes preserve the repository boundaries: the dashboard composes the API and authentication layers, runtime execution remains behind the runner, and remote CLI runs are submitted to the deployment control plane rather than executed locally. Polling provides a webhook-independent path for discovering pull requests while sharing durable delivery claims with webhook processing.

Verification

  • aube run check:repo
  • aube run lint:ci
  • aube run typecheck
  • aube test
  • aube run build

Safety and compatibility

  • I added or updated deterministic tests for changed behavior.
  • I preserved observe mode as read-only, or explained the policy change above.
  • Runtime commands and target-repository writes remain inside the runner boundary.
  • I did not expose secrets, tokens, personal data, or untrusted output in logs.
  • I updated documentation and Agent Skills when workflows or boundaries changed.

Agent context

  • Agent/tools used: AI coding agent session; repository inspection and patch review.
  • What the agent did, and what you changed or verified yourself: The agent helped author and review the change across the dashboard, API, CLI, source-control, documentation, and local-development configuration. Deterministic unit and component tests were added or updated for the changed behavior.

Reviewer notes

The change spans the dashboard control loop, audit routing, remote CLI execution, pull-request polling, and solo development setup. Particular attention is warranted for authentication and authorization behavior, polling delivery claims, remote-run repository allow-listing, and the restriction of unattended polling to non-writable modes.

Base branch: main

Confidence Score: 5/5

Safe to merge; no blocking issues remain.

No new findings were established. The previously reported issues are fully addressed in the current code: proactive polling and webhook delivery share commit claims, pull-request pagination declares and uses its bounded page limit, remote results require terminal states and plans, repository paths are normalized before storage, stream limits and initial-update ordering are enforced, error flags are explicitly boolean, and the documented solo setup and toolchain version are aligned.

Reviews (14): Last reviewed commit: "fix(cli): reject non-terminal remote tas..." | Re-trigger Greptile

Context used:

RedStar071 and others added 10 commits September 5, 2026 17:32
Records what was verified by building and running the app on 2026-09-05: the
dashboard renders and the control plane executes runs, but nothing feeds it,
nothing refreshes it, and starting it needs a database.

The plan restructures the edge rather than the packages, in five phases with a
demonstrable exit criterion each.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Seeing the dashboard required Postgres, a migration, a signing secret, and
control-plane tokens before the first page could render, so the quickest way to
look at it was not to.

`dev:solo` is `nuxt dev --dotenv .env.solo`: the same app, the same router, and
the same `/api/auth/**` endpoints, with Better Auth on the in-memory store the
Playwright preview server already uses. `.env.solo` is checked in because it
holds nothing worth keeping out of the repository — the session store dies with
the process, and the control-plane token is only accepted by a server started
this way. It is loaded only when a command names it with `--dotenv`, so `.env`
and every deployment are untouched.

Verified: `turbo run dev:solo --filter=@code-zero/dashboard` from a checkout with
no database — `/api/v1/health` 200, `/login` 200, `POST /api/auth/sign-up/email`
returns a session, and `/` renders Control Plane with that cookie. check:repo,
format:check, and typecheck pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The trail had two homes it did not need. Recording went through a hand-rolled
`AuditRecorder` injected into the RPC context, while `evlog` — already installed,
already wrapping both transports — has an audit pipeline with the same shape.
Reading lived in a Nitro route outside the router, written when the router could
not authenticate a browser session; since it can, that reason is gone.

Recording is now `log.audit()` / `log.audit.deny()`. The persisted record is
evlog's own `AuditFields` plus a storage id and timestamp, so there is no second
audit vocabulary to keep in step, and `auditEnricher` fills in request context
(`requestId`, `traceId`, ip, user agent) no call site had been passing. The
identity is evlog's deterministic `idempotencyKey`, so a retried delivery lands
on the key it already wrote instead of appending a second copy. `auditLogPlugins`
carries the record to the same KV-backed store as before, filtered by
`auditOnly` and awaited so an audited mutation cannot answer 200 and lose its
record, and installed as `EvlogHandlerPlugin` plugins rather than as its `drain`,
so a deployment's own request logging is untouched.

Reading is `audit.list`, an authenticated procedure gated on a new explicit
`Principal.admin` rather than on a mode grant: what a caller may run and what a
caller may see are different questions. Operator tokens are never
administrators — the trail records their use, so letting one read it back would
let a token audit itself.

Verified against the built server: `tasks.create` success and denial each
persisted one record carrying `context.requestId` and the user agent; `audit.list`
answered FORBIDDEN for an operator token and for a signed-in non-admin over
`/rpc/**`. lint:ci, typecheck, and 151 api tests pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The board showed whatever the last fetch caught. A run records its lifecycle
events as it works, so a task appeared and then sat at the state it had when the
page loaded until someone pressed refresh — and a stalled page looked exactly
like a quiet one.

`/api/events` streams the overview over SSE, behind the same session the page
needs, pushing whenever a task record is written. The store wrapper that emits
lives in the composition root rather than in `packages/api`, so the store
contract stays plain persistence: every writer — the router, the webhook route,
and the run recording its own events — already goes through that one instance,
so a subscriber sees the whole lifecycle rather than the transitions one
transport happens to see. Writes are coalesced over 250ms, so a run that records
ten events in a burst sends one overview.

The client writes each message straight into the query cache the page already
reads, rather than invalidating and asking the server for what it just sent. The
query stays the loader for the first paint and for a client whose stream never
opens. A header indicator says whether the board is actually following, because
a stalled stream is otherwise indistinguishable from an idle one.

`useLiveOverview` takes the query key rather than reaching for `useNuxtApp()`,
which is also what keeps it out of the Nuxt runtime for the unit suite.

Verified against the built server: the stream answers 401 unauthenticated; with
a session it pushes the current overview on connect and again when a task
created through `/api/v1/tasks` reached the store. lint:ci, typecheck, i18n:report,
and the dashboard suites pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The router has accepted `tasks.create` and `approvals.decide` since the control
plane existed, but nothing in the UI called either: the board could watch a run
stop for a human decision and offer no way to give one. The sidebar meanwhile
listed nine sections that were inert buttons — a nav that promises surfaces the
app does not have teaches an operator that clicking does nothing.

The inspector offers Approve and Reject with an optional comment while a task is
`needs-human` and undecided, and shows the recorded decision once one exists,
because the control plane accepts exactly one. A `details`-based form queues a
task from the header. Both emit rather than mutate: the page owns the typed
client and the one place a failure is surfaced, so there is no second path to
keep in step. Neither writes into the query cache — the decision lands in the
store, and the store is what the live stream pushes back, so the board updates
from the same source every client sees rather than from a guess about what the
server did.

The repository is typed rather than picked from the allow-list: the list is
server-side checkout paths, which the persisted records deliberately keep out of
reach, and `tasks.create` already names the rule it refused on.

Verified against the built server, over the same `/rpc/**` the page uses: a
session created a task, and got FORBIDDEN for a repository outside the allow-list
and for a mode a session may not request. The approval and form logic are covered
by 13 new component tests. Browser verification of the rendered result was not
possible — this environment has no automation host — so the visual pass against
`nuxt-frontend-review` is still owed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Work only reached the control plane when a provider delivered it, so a
self-hosted deployment behind no public URL had nothing feeding it: the board
stayed empty unless someone posted a task by hand.

`server/plugins/poller.ts` lists each watched repository's open pull requests on
an interval and starts a review for every head commit it has not started one
for. It is the pull-based half of the job the webhook route already does, and it
shares that route's durable `DeliveryClaimStore`, so a commit reviewed through
one path is never reviewed again through the other. The claim key carries the
head sha, which is what makes a new push earn a new review and an unchanged pull
request earn nothing.

Constraints that are enforced, not documented: it is off unless
`CODE_ZERO_POLL_REPOSITORIES` names something; it requests only `observe` or
`suggest`, so work nobody asked for cannot write to a checkout; and the checkout
comes from the path an operator paired with the repository rather than being
derived from the provider's answer, so a run can never target somewhere nobody
named. A failed start releases its claim so the next pass retries, and one
unreachable provider does not end the pass.

`listOpenPullRequests` is new on the GitHub adapter and returns both the base and
head commits, because a review reads the diff between them. It skips a record
missing either rather than losing the page it arrived in.

Verified against the built server: silent and healthy when unconfigured; refuses
to start naming the missing variable when configured without a token; and with a
token it reports the repository that failed without stopping the process or
putting the credential in the log. A pass against real GitHub is still owed —
this environment has no credentials, and the tests deliberately reach no network.
27 new tests; lint:ci, typecheck across the graph, and the build pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`accessFromEnvironment` returned `undefined` unless `CODE_ZERO_CONTROL_PLANE_TOKENS`
was set, and `mayTargetRepository` fails closed without a policy. So a deployment
that authenticates only browser sessions could never create a task: the
`CODE_ZERO_CONTROL_PLANE_REPOSITORIES` it had configured did not exist as far as
the router was concerned, and every target was refused.

The two variables answer different questions. Tokens say who a machine caller
is; the allow-list says what any authenticated caller may target, including a
person signed into the dashboard. Either one now produces a policy, and only
neither returns `undefined`, so an unconfigured deployment still rejects every
mutation and a deployment with no tokens still authenticates no machine caller —
`principals` is simply empty.

Found by running the dashboard with a session and an allow-list and nothing else,
which is what `dev:solo` and a self-hosted single-owner install both look like.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A local `zero run` and the dashboard kept separate histories: the CLI executed in
the checkout and recorded nothing the board could read, so work started from a
terminal was invisible to the surface built to watch it.

`--remote` hands the run to a deployment's control plane instead. It presents the
session `zero login` stored as a bearer token, so the run is attributed to the
person who signed in rather than to a shared operator token, and it goes to
`/rpc/**` because that is the only transport that resolves a session — stating
the `Sec-Fetch-Mode` header its CSRF guard reads, which a browser sends on its
own. The deployment therefore needs `AUTH_ENABLE_DEVICE_AUTHORIZATION=true`, the
same flag `zero login` already requires.

A flag rather than an inference from `CODE_ZERO_URL`: that variable already
selects which deployment `login` and `logout` act on, so treating its presence as
"run somewhere else" would silently move an operator's run to another machine and
another checkout the first time they set it. The plan called for the implicit
form; this is the deliberate departure from it.

The exit code comes from the same table a local run uses, so CI reads either the
same way, and an answer that is not a result is refused rather than allowed to
exit 0.

Verified against the built server end to end: from a checkout, `zero run
--proactive --remote --json` authenticated with a stored session, executed on the
deployment, printed the result, exited 0, and the task appeared in the board's
own `dashboard.overview`. 14 new tests; lint:ci and typecheck pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The whole live board rests on one property — every task write announces itself —
and nothing tested it. It could not be tested either: the notification was a
subclass of the KV-backed store, so reaching it meant reaching the deployment's
filesystem driver.

`observeWrites` is that subclass turned into a decorator over any `TaskStore`, so
a test drives it against an in-memory one. The tests state the parts that matter:
it announces every write, only after the write landed, and says nothing when the
write failed — a listener re-reading the store on a failed write would find
nothing changed and a listener told too early would read the previous state.

Forwarding `clear` went with it: `PersistentTaskStore` has none, so it was a
capability the wrapper invented for nobody.

`docs/architecture.md` gains the live-state paragraph the plan asked for, and
`docs/PLAN.md` records what was built, the three places the plan was departed
from and why, and the five things still owed — the browser review among them.

Verified: 997 tests across every package and app, lint:ci, typecheck, check:repo,
i18n:report, and the build all pass. The docs build needs a larger heap than this
sandbox allows by default and passes with one; nothing in this branch touches it
beyond a one-line table.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Add the `code` executable and VS Code CLI archive
@vercel

vercel Bot commented Sep 5, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
agent-zero-dashboard Error Error Sep 9, 2026 6:54pm UTC
2 Skipped Deployments
Project Deployment Actions Updated
agent-zero-docs Skipped Skipped Sep 9, 2026 6:54pm UTC
agent-zero-marketing Skipped Skipped Sep 9, 2026 6:54pm UTC

Comment thread apps/dashboard/server/utils/poller.ts Outdated
Comment thread packages/source-control/src/providers/github-pulls.ts Outdated
Comment thread packages/cli/src/remote.ts Outdated
Comment thread apps/dashboard/server/api/events.get.ts Outdated
@macroscopeapp

macroscopeapp Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This PR is a broad production feature spanning live dashboard behavior, automatic pull-request polling, remote execution, authorization, audit logging, and a database migration. Its authentication-sensitive paths, new automatic workflow, and unexplained root-level executable require human review.

Not approved because:

  • Credit balance exhausted. Approvability relies on correctness review in order to determine eligibility

Review your spending limits in Billing settings. You can add or adjust custom eligibility rules. Learn more.

- Add deployment YAML configuration for control-plane policy
- Store repository allow-lists in the database
- Add standalone dashboard, docs, and marketing dev commands
Comment on lines +113 to +116
export const repositoryStore: RepositoryStore =
process.env.AUTH_E2E_MEMORY === 'true'
? memoryRepositoryStore(process.env.CODE_ZERO_SOLO_REPOSITORIES)
: postgresRepositoryStore();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Solo repository setting is ignored

The in-memory repository store reads CODE_ZERO_SOLO_REPOSITORIES, while .env.solo and the README tell users to configure CODE_ZERO_CONTROL_PLANE_REPOSITORIES. As a result, following the documented dev:dashboard setup leaves the repository allow-list empty, so every task creation is refused.

Knowledge Base Used:

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/dashboard/server/utils/repositories.ts
Line: 113-116

Comment:
**Solo repository setting is ignored**

The in-memory repository store reads `CODE_ZERO_SOLO_REPOSITORIES`, while `.env.solo` and the README tell users to configure `CODE_ZERO_CONTROL_PLANE_REPOSITORIES`. As a result, following the documented `dev:dashboard` setup leaves the repository allow-list empty, so every task creation is refused.

**Knowledge Base Used:**
- [Dashboard application](https://app.greptile.com/wolfstar-project/-/custom-context/knowledge-base/wolfstar-project/code-zero/-/docs/dashboard-application.md)
- [Application service API](https://app.greptile.com/wolfstar-project/-/custom-context/knowledge-base/wolfstar-project/code-zero/-/docs/service-api.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code Fix in Cursor Fix in Cursor Cloud Agents

- Track the latest aube release in mise
- Add config and database workspace dependencies to the lockfile
Comment thread mise.toml Outdated
…t the environment

The repository and mode grants moved out of the environment in the previous
commit but nothing was rewired to the new sources, so the dashboard imported
two symbols `@code-zero/api` no longer exports. This closes that.

Both oRPC transports now resolve the operator tokens through
`controlPlaneAccess()` and answer "may a run target this checkout?" from the
`repository` table per request, so a repository configured a moment ago is
usable without a restart. `/api/v1/**` takes its CORS allow-list from
`control_plane.origins`.

The poller reads its watched repositories from the same table on every pass and
runs each in the mode that repository is configured with, rather than one mode
for the whole deployment. It reschedules itself when a pass finishes instead of
holding a fixed interval, which reads `poll.interval_seconds` each time and
removes the guard against a pass overrunning its own interval.

Five variables are gone: CODE_ZERO_CONTROL_PLANE_REPOSITORIES, _MODES and
_ORIGINS, CODE_ZERO_POLL_REPOSITORIES, _INTERVAL_SECONDS and _MODE. What stays
in the environment is what a deployment already keeps there: the operator
tokens, and CODE_ZERO_CONFIG to find the rest.

Also fixes the schema test, which the new `repository` table left failing, and
three type assertions the type-aware lint refuses.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comment thread package.json
"dev": "turbo run dev",
"dev:docs": "turbo run dev --filter=@code-zero/docs",
"dev:marketing": "turbo run dev --filter=@code-zero/marketing",
"dev:solo": "turbo run dev:solo --filter=@code-zero/dashboard",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Update stale command references

The standalone dashboard script was renamed to dev:solo, but apps/dashboard/.env.solo and several startup instructions in docs/PLAN.md still tell developers to run dev:dashboard. That command no longer exists and exits with a missing-script error, so developers following the tracked setup cannot start the dashboard. This is non-blocking, but the remaining references should be updated with the rename.

Artifacts

Package-script resolution harness

  • The authored harness checks the package script names and runs the documented command.

Package-script resolution output

  • The output shows that `dev:dashboard` is absent while `dev:solo` is present.

Documented command harness

  • The authored command harness invokes the stale dashboard startup command.

Documented command output

  • The output records the missing-script failure from the documented command.

View artifacts

T-Rex Ran code and verified through T-Rex

Prompt To Fix With AI
This is a comment left during a code review.
Path: package.json
Line: 17

Comment:
**Update stale command references**

The standalone dashboard script was renamed to `dev:solo`, but `apps/dashboard/.env.solo` and several startup instructions in `docs/PLAN.md` still tell developers to run `dev:dashboard`. That command no longer exists and exits with a missing-script error, so developers following the tracked setup cannot start the dashboard. This is non-blocking, but the remaining references should be updated with the rename.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code Fix in Cursor Fix in Cursor Cloud Agents

- Prevent stale overview snapshots and unbounded SSE connections
- Normalize saved checkout paths and make transport error flags explicit
`newTask.*` and its `modes`/`triggers` children landed in the dashboard
locales without a matching `aube run i18n:schema` run, so CI's schema
drift check failed on every push regardless of what else changed.
- Keep claims after reviews whose completion write fails
- Scan bounded GitHub pull-request pages
- Rename the standalone development command to `dev:solo`
Comment thread packages/source-control/src/providers/github-pulls.ts
…st discovery

`listOpenPullRequests` referenced `MAX_PAGES` in its pagination loop without
declaring it, so every call threw `ReferenceError: MAX_PAGES is not defined`
and broke polling entirely. Adds the constant (20 pages, 2,000 pull requests)
and rewrites the tests that covered the old single-page `perPage` parameter to
cover walking multiple pages and stopping at the cap instead.
isTaskResult only checked that id/state were strings, so a queued or
in-progress /rpc response (e.g. { id, state: 'queued' }) passed as a
completed TaskResult. JSON mode then printed it and exited 0 before the
review had actually finished, and normal-mode output threw while rendering
a plan that was never populated. Now requires state to be one of the three
terminal states and plan to be present.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant