Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 10 additions & 3 deletions docs/code/dashboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ It starts automatically with `devintern worker`. Open [http://localhost:4400](ht
- Read the raw agent transcript for any harness execution under the run (clarity, implementation, review, and more)
- Open the task or pull request behind a run
- Understand why a run failed and retry it after fixing the cause
- Report a DevIntern bug for a run, with a preview of exactly what is sent
- Run a recurring automation immediately instead of waiting for its next scheduled time
- Hand the worker a one-off task with **New task**, without creating a tracker ticket
- Review recent worker logs when something needs attention
Expand Down Expand Up @@ -74,6 +75,12 @@ Retry after correcting the underlying problem, for example missing credentials,

Ad-hoc tasks created with `devintern worker add` show **Re-queue task** instead: the queue row is put back as `pending` under its existing key, so the worker resumes the interrupted attempt's branch and report rather than opening a second branch and pull request. The worker also auto-resumes an interrupted ad-hoc task once at startup when its prior local run recorded a resumable branch with either an implementation report or commits ahead of the repo's base (unless the interrupted attempt's child process is still alive; the row then stays `running` and is re-checked at the next startup, and re-queueing is refused with the pid to stop first); **Re-queue task** remains available for rows that were not resumable or whose automatic-resume budget is exhausted. If the row was already pruned (30 days by default) or is still queued/running, the run detail explains why it cannot be re-queued and points at `devintern worker add`.

## Report a problem with a run

When a run fails in a way the output does not explain, or completes with the wrong result, open it and choose **Report a problem**. It files the same redacted bug report as `devintern report --run <id>`; see [Reporting Bugs](reporting-bugs.md#report-from-the-dashboard).

Missing credentials, usage limits, and failed verification checks are configuration issues, not DevIntern bugs: fix those and retry instead.

## Run an automation now

Open **Automations**, find the automation, and choose **Run now**. When automations target more than one repository, the list groups them: automations without a `repo` come first under **General**, then one group per repository in name order, with enabled automations ahead of disabled ones and the soonest next run first. The worker runs it through the same pipeline as a scheduled occurrence, including its normal repository, environment, and overlap protection.
Expand Down Expand Up @@ -130,7 +137,7 @@ The dashboard and its data stay on the worker machine. It listens only on a loop
The API also refuses requests that did not come from the dashboard page itself, so a website open in your browser cannot use it behind your back:

- Every `/api` request must be addressed to `localhost`, `127.0.0.1`, or `[::1]` (any port, so remapped SSH tunnels work), or to an [allowed origin](#allowed-origins). Any other host name gets `403`. This blocks DNS-rebinding attacks, where a hostile domain is pointed at your loopback address.
- Requests that change something (retry, run now, new task, and any other non-GET action) must come from the same origin as the address they were sent to, for example `Origin: http://localhost:8440` for `localhost:8440`, and must send `Content-Type: application/json`. Cross-origin requests get `403`; other content types get `415`. A page served on another local port cannot read the API or act on it.
- Requests that change something (retry, run now, new task, sending a bug report, and any other non-GET action) must come from the same origin as the address they were sent to, for example `Origin: http://localhost:8440` for `localhost:8440`, and must send `Content-Type: application/json`. Cross-origin requests get `403`; other content types get `415`. A page served on another local port cannot read the API or act on it.

Worker run history lives in the worker's workspace, alongside its other local state. The embedded dashboard remains available when an expired Worker Pilot pauses acquisition. Starting a standalone dashboard requires an active Worker Pilot or automation license.

Expand All @@ -146,7 +153,7 @@ dashboard = false

Watching a long run from your laptop or phone does not require opening the dashboard port. Put an authenticating boundary in front of the loopback listener instead — your SSH client, your Tailscale tailnet, or a reverse proxy with its own login. All three options below leave the dashboard bound to `127.0.0.1`, and they work the same for the worker-embedded dashboard and for `devintern dashboard`.

**Treat the dashboard as sensitive.** Run transcripts, logs, and artifacts can contain credentials or customer data, and `POST /api/runs/:id/retry`, `POST /api/automations/:id/run`, and `POST /api/tasks` act with your repository credentials. That is why the recipes below add a login rather than just moving the bind address.
**Treat the dashboard as sensitive.** Run transcripts, logs, and artifacts can contain credentials or customer data. `POST /api/runs/:id/retry`, `POST /api/automations/:id/run`, and `POST /api/tasks` act with your repository credentials, and `POST /api/reports` sends a bug report as your signed-in DevIntern account. That is why the recipes below add a login rather than just moving the bind address.

### Option 1: SSH tunnel

Expand Down Expand Up @@ -217,7 +224,7 @@ An allowed origin is a full `scheme://host[:port]` the dashboard trusts in addit
| Environment variable (comma-separated) | `DASHBOARD_ALLOWED_ORIGINS=https://dash.example.internal,http://192.168.1.50:8088` |
| `devintern dashboard` flag (repeatable) | `--allow-origin https://dash.example.internal` |

A malformed entry stops the worker or dashboard at startup with an error naming it. `[dashboard]` is startup-only: restart the worker after changing it. When the list is not empty, the startup log prints it with a warning that anyone who can reach those addresses gets full dashboard access, including write actions.
A malformed entry stops the worker or dashboard at startup with an error naming it. `[dashboard]` is startup-only: restart the worker after changing it. When the list is not empty, the startup log prints it with a warning that anyone who can reach those addresses gets full dashboard access, including write actions such as retries and bug reports.

Allowed origins stop other websites from using your browser against the dashboard. They do not authenticate anyone. Who can reach the address is still decided by your Tailscale ACLs or by the proxy's login (for example Caddy `basic_auth`).

Expand Down
14 changes: 12 additions & 2 deletions docs/code/reporting-bugs.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
---
title: "Reporting Bugs"
sidebarLabel: "Bug Reports"
description: "File a redacted DevIntern bug report from the CLI, attach a relevant run, and preview the evidence before sending."
description: "File a redacted DevIntern bug report from the CLI or the dashboard, attach a relevant run, and preview the evidence before sending."
section: "Automation Reference"
order: 2
dateModified: 2026-10-21
dateModified: 2026-11-14
tags: ["troubleshooting", "bug-report", "diagnostics", "privacy"]
---

Expand Down Expand Up @@ -47,6 +47,16 @@ devintern report --last --log /tmp/devintern-run.log

Log attachment is bounded (64 KB per source) and credentials are masked.

## Report from the dashboard

The [worker dashboard](dashboard.md) can file the same report for a run without a terminal. Open the run and choose **Report a problem** (shown prominently for failed, escalated, and abandoned runs and as a secondary action for runs that completed with the wrong behavior).

1. Enter a title and describe what you expected and what happened. Do not paste secrets, credentials, or customer data. Leave **Share identifiers** off unless the task key and repository slug help triage (it matches `--share-identifiers`).
2. Choose **Preview report**. The dashboard collects the bundle exactly like `devintern report --run <id>` and shows what is attached, what is never sent, and the full redacted payload. Nothing has been sent yet.
3. Choose **Send report**. The dashboard submits the payload you previewed. Anonymous reports need a contact email on this step, which is added as the report contact; signed-in users follow up through their account.

The preview is held in the dashboard's memory for 15 minutes; after that, preview again. On success the dialog shows the report id. If the endpoint cannot be reached, the report is saved to `.devintern-code/reports/` like a CLI submission and the dialog shows the `devintern report --from <path>` command to retry later.

## Preview, save, and retry

```bash
Expand Down
22 changes: 22 additions & 0 deletions packages/code/src/dashboard-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ import {
} from "./lib/observability/dashboard-api";
import { handleOverview } from "./lib/observability/dashboard-overview";
import { handleCreateTask } from "./lib/observability/dashboard-tasks";
import { DashboardReports } from "./lib/observability/dashboard-report";
import type { DashboardReportDeps } from "./lib/observability/dashboard-report";
import { handleWorkspace } from "./lib/observability/dashboard-workspace";
import type {
ApiResponse,
Expand Down Expand Up @@ -202,6 +204,8 @@ export interface DashboardServerOptions {
tailscaleAutodetect?: boolean;
/** Replacement for `tailscale serve status --json` (tests). */
tailscaleProbe?: TailscaleStatusProbe;
/** Collaborator overrides for bug-report preview/submission (tests). */
reportDeps?: DashboardReportDeps;
}

/** Log trusted origins and the access warning they carry. */
Expand Down Expand Up @@ -258,6 +262,15 @@ async function createTaskFromRequest(data: DashboardData, request: Request): Pro
return handleCreateTask(data, payload);
}

/** Parse a JSON request body; undefined when it is missing or malformed. */
async function readJsonBody(request: Request): Promise<unknown> {
try {
return await request.json();
} catch {
return undefined;
}
}

/** Serve a file from the UI dir, confined to it; null on miss/traversal. */
function serveStatic(uiDir: string, pathname: string): Response | null {
const relative = pathname === "/" ? "index.html" : pathname.slice(1);
Expand Down Expand Up @@ -307,6 +320,7 @@ export function startDashboardServer(
automationActions: options.automationActions,
logDirs: options.logDirs,
});
const reports = new DashboardReports(data, options.reportDeps);
const uiDir = resolveUiDir();

const runDetailPattern = /^\/api\/runs\/([^/]+)$/;
Expand All @@ -316,6 +330,7 @@ export function startDashboardServer(
const runArtifactFilePattern = /^\/api\/runs\/([1-9][0-9]*)\/artifacts\/([a-f0-9-]{36})\/file$/;
const runArtifactPattern = /^\/api\/runs\/([^/]+)\/artifacts\/([^/]+)$/;
const runRetryPattern = /^\/api\/runs\/([^/]+)\/retry$/;
const runReportPreviewPattern = /^\/api\/runs\/([^/]+)\/report\/preview$/;
const automationRunPattern = /^\/api\/automations\/([^/]+)\/run$/;

const envOrigins = parseDashboardAllowedOriginsEnv(process.env.DASHBOARD_ALLOWED_ORIGINS);
Expand Down Expand Up @@ -354,6 +369,13 @@ export function startDashboardServer(
if (pathname === "/api/tasks") {
return createTaskFromRequest(data, request);
}
const reportPreview = pathname.match(runReportPreviewPattern);
if (reportPreview) {
return reports.preview(reportPreview[1], await readJsonBody(request));
}
if (pathname === "/api/reports") {
return reports.submit(await readJsonBody(request));
}
return { status: 405, body: { error: "method not allowed" } };
};

Expand Down
Loading
Loading