Uh oh!
There was an error while loading. Please reload this page.
feat(dashboard): make the live board actionable - #67
Conversation
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
The latest updates on your projects. Learn more about Vercel for GitHub.
|
| for (const pull of open) { | ||
| if (pull.draft) continue; | ||
| const key = pollClaimKey(repository, pull); |
There was a problem hiding this comment.
Polling claims a commit under poll:<repo>#<number>@<headSha>, but a GitHub pull_request synchronize delivery for that same commit starts another task without checking that claim. A poll followed by the matching webhook creates two reviews for one revision, duplicating model work and review activity. Use one atomic pull-request-and-commit claim across both intake paths.
Knowledge Base Used:
Artifacts
- Targeted source used to create one proactive poll task for PR 67 at the fixed head SHA, showing the poll claim scope.
- Executed poll-only command from `/home/user/repo` exited 0 and recorded one task with claim key `poll:acme/app#67@<headSha>`.
- Targeted reproduction source for the shared-store poll followed by the signed GitHub synchronize delivery.
- Executed poll followed by the same-commit synchronize webhook from `/home/user/repo/apps/dashboard` exited 0; the webhook returned HTTP-equivalent 200 accepted and task count rose from one to two.
Ran code and verified through T-Rex
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/dashboard/server/utils/poller.ts
Line: 74
Comment:
**Share pull request claims**
Polling claims a commit under `poll:<repo>#<number>@<headSha>`, but a GitHub `pull_request` synchronize delivery for that same commit starts another task without checking that claim. A poll followed by the matching webhook creates two reviews for one revision, duplicating model work and review activity. Use one atomic pull-request-and-commit claim across both intake paths.
**Knowledge Base Used:**-[Application service API](https://app.greptile.com/wolfstar-project/-/custom-context/knowledge-base/wolfstar-project/code-zero/-/docs/service-api.md)-[Dashboard application](https://app.greptile.com/wolfstar-project/-/custom-context/knowledge-base/wolfstar-project/code-zero/-/docs/dashboard-application.md)---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| async listOpenPullRequests(target: RepositoryTarget, perPage = 50): Promise<OpenPullRequest[]> { | ||
| const query = new URLSearchParams({ | ||
| state: 'open', | ||
| sort: 'updated', | ||
| direction: 'desc', | ||
| per_page: String(Math.min(Math.max(Math.trunc(perPage), 1), 100)), | ||
| }); | ||
| const payload = await this.send( | ||
| 'GET', | ||
| `/repos/${target.owner}/${target.repo}/pulls?${query.toString()}`, | ||
| ); |
There was a problem hiding this comment.
Paginate pull request discovery
Each polling pass requests the same first 50 open pull requests ordered by most recently updated, with no page, cursor, or rotation state. An open pull request ranked 51st or later is never sent to the poller unless it later becomes recent enough to enter that first page, so eligible reviews can be missed indefinitely. Advance through pages or persist a bounded rotating cursor.
Knowledge Base Used:Source-control provider adapters
Artifacts
- The exact TypeScript reproduction executed against the baseline commit and current GitHub pull-request adapter, showing that repeated calls omit pagination. The takeaway is that the proof is deterministic and directly exercises the adapter.
- Captured execution output from the parent of the introducing commit, where `listOpenPullRequests` did not yet exist and therefore made no HTTP request. The takeaway is that the before state has no comparable endpoint behavior.
- Captured execution output from two current adapter calls, each returning HTTP 200 OK for the identical first-page URL and leaving simulated PR docs: restructure AGENTS.md and add an AI contributions policy #51 undiscovered. The takeaway is that the endpoint does not paginate or rotate.
- Captured verbose Vitest output for the GitHub pull-request adapter tests, with all 15 tests passing. The takeaway is that existing tests do not cover advancing beyond the first page.
Ran code and verified through T-Rex
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/source-control/src/providers/github-pulls.ts
Line: 225-235
Comment:
**Paginate pull request discovery**
Each polling pass requests the same first 50 open pull requests ordered by most recently updated, with no page, cursor, or rotation state. An open pull request ranked 51st or later is never sent to the poller unless it later becomes recent enough to enter that first page, so eligible reviews can be missed indefinitely. Advance through pages or persist a bounded rotating cursor.
**Knowledge Base Used:**[Source-control provider adapters](https://app.greptile.com/wolfstar-project/-/custom-context/knowledge-base/wolfstar-project/code-zero/-/docs/source-control-providers.md)---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| function isTaskResult(value: unknown): value is TaskResult { | ||
| return isRecord(value) && typeof value.id === 'string' && typeof value.state === 'string'; |
There was a problem hiding this comment.
Reject incomplete remote results
The remote-result guard accepts { "id": "task-queued", "state": "queued" } as a completed task result even though downstream code requires a terminal state and complete result fields. In JSON mode, the CLI prints that partial response and exits successfully, allowing CI to report success before a review has completed; normal output instead throws while rendering the missing plan. Reject non-terminal or incomplete responses before reporting them.
Knowledge Base Used:Command-line client
Artifacts
- A local HTTP server and isolated credential store run the built CLI against complete and partial RPC responses, ending with the reproduced contract failure.
- The complete TaskResult `HTTP/1.1 200 OK` baseline was requested through the real built CLI and exited 0, establishing expected terminal behavior.
- The partial queued `HTTP/1.1 200 OK` result was accepted: JSON mode exited 0 and ordinary mode failed while rendering evidence, confirming the defect.
- The existing focused CLI suite was executed after the repro and passed 64 tests, showing current tests do not cover this partial-result contract.
Ran code and verified through T-Rex
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/cli/src/remote.ts
Line: 118-119
Comment:
**Reject incomplete remote results**
The remote-result guard accepts `{ "id": "task-queued", "state": "queued" }` as a completed task result even though downstream code requires a terminal state and complete result fields. In JSON mode, the CLI prints that partial response and exits successfully, allowing CI to report success before a review has completed; normal output instead throws while rendering the missing plan. Reject non-terminal or incomplete responses before reporting them.
**Knowledge Base Used:**[Command-line client](https://app.greptile.com/wolfstar-project/-/custom-context/knowledge-base/wolfstar-project/code-zero/-/docs/command-line-client.md)---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| const heartbeat = setInterval(() => { | ||
| if (!closed) void stream.push({ event: 'heartbeat', data: '' }).catch(() => undefined); | ||
| }, HEARTBEAT_MS); | ||
| taskChanges.on(TASK_CHANGED, schedulePush); |
There was a problem hiding this comment.
Any authenticated account can retain an unlimited number of live event streams. Each accepted request creates an open stream, a process-wide task-change listener, and heartbeat scheduling, while the listener warning limit is disabled. Repeated connections can exhaust application resources and degrade service for other users; enforce per-user and global connection limits or rate limiting.
How this was verified: Retaining twelve authenticated streams created twelve listeners and twelve heartbeat allocations without an application-level connection cap.
Artifacts
- Executable source that captured the route response before the event-stream endpoint was added.
- Captured output showing that GET `/api/events` returned 404 before this event-stream endpoint was introduced.
- Executable source that opens and retains authenticated event streams while measuring listener and timer allocations.
- Captured output showing twelve accepted streams for one authenticated user with twelve retained listeners and heartbeat allocations.
Ran code and verified through T-Rex
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/dashboard/server/api/events.get.ts
Line: 56-60
Comment:
**Limit live event connections**
Any authenticated account can retain an unlimited number of live event streams. Each accepted request creates an open stream, a process-wide task-change listener, and heartbeat scheduling, while the listener warning limit is disabled. Repeated connections can exhaust application resources and degrade service for other users; enforce per-user and global connection limits or rate limiting.
**How this was verified:** Retaining twelve authenticated streams created twelve listeners and twelve heartbeat allocations without an application-level connection cap.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.ApprovabilityVerdict: Not approved Macroscope's review found this PR not approvable — This PR adds several production workflows—live SSE updates, background pull-request polling, remote execution, audit access, and dashboard mutations—alongside authentication and access-policy changes. Unresolved findings identify duplicate or missed reviews, incomplete remote results, and unlimited live connections, so the scope and risk require human review. Not approved because:
Review your spending limits in Billing settings. You can add or adjust custom eligibility rules. Learn more. |
Summary
zero run --remotefor dispatching work to a deployment's control plane.dev:solofor running the dashboard with in-memory authentication and no Postgres.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:repoaube run lint:ciaube run typecheckaube testaube run buildSafety and compatibility
observemode as read-only, or explained the policy change above.Agent context
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: 0/5
Unsafe to merge: four independently reproduced failures affect task execution, review coverage, CLI correctness, and service availability.
The reproduced findings include duplicate task creation, incomplete pull-request discovery, successful CLI completion for incomplete remote responses, and an authenticated resource-exhaustion path.
Files Needing Attention: apps/dashboard/server/utils/poller.ts, packages/source-control/src/providers/github-pulls.ts, packages/cli/src/remote.ts, apps/dashboard/server/api/events.get.ts, code, and vscode_cli.tar.gz.
Security Review
Authenticated users can open and retain unlimited live-event streams. Each stream allocates a listener and heartbeat scheduling resources, allowing repeated connections to consume application resources and degrade service for other users.
What T-Rex did
Prompt To Fix All With AI
Reviews (1): Last reviewed commit: "chore: add VS Code CLI binaries" | Re-trigger Greptile
Context used (5)