Skip to content

Repository files navigation

execbro-runner

⚠️ Early-alpha proof of concept. APIs, config schema, CLI flags, and internal behavior will change without notice between versions. Don't depend on it in anything you can't afford to re-wire. Feedback and bug reports are welcome.

Queue-based autonomous task runner for React Native apps. Drop in a markdown prompt, and a background daemon spins up an isolated sandbox (git worktree + iOS simulator or Android emulator + Metro), runs Claude Code headlessly against it, has the agent verify the change live on the device via the ExecBro MCP server, then optionally pushes the resulting branch and surfaces a PR URL.

Fire-and-forget Claude Code runs for your React Native app, with real device verification baked in.

Why this exists

Running Claude Code by hand on a React Native app means babysitting it: starting Metro, picking a simulator, watching it work, then reminding it to actually exercise the UI before declaring victory. This runner automates all of that so you can queue up several tasks and walk away.

Each task gets its own:

  • git worktree (so concurrent tasks don't stomp on each other's files)
  • iOS simulator UDID or Android emulator (so they don't fight over the same device)
  • Metro port (so bundlers don't collide)
  • headless Claude session you can resume with claude --resume <session-id> if you need to inspect or take over

The agent runs under a composed prompt: a preamble describing its sandbox and the ExecBro tools available, the user's task, and a verification suffix that forces it to reload the app, take a screenshot, exercise the affected flow, and check logs before signaling done. When the agent commits, the runner can push the branch and fire a macOS notification with the PR URL.

Requirements

  • macOS with Xcode (for xcrun simctl) for iOS tasks
  • Android SDK with adb and emulator on PATH for Android tasks
  • Node ≥ 18
  • claude (Claude Code CLI) on PATH and authenticated
  • A target React Native app — either bare RN or Expo managed workflow (both are supported: Metro, native build/install, and app launch all auto-detect Expo via an expo dependency and use expo start/expo run:android/run:ios/a dev-client deep link instead of assuming @react-native-community/cli)
    • iosBundleId and androidPackageName are auto-discovered from the native ios//android/ projects, but Expo managed-workflow apps typically don't commit those — set "execbro": { "iosBundleId": "...", "androidPackageName": "..." } in package.json to override
    • For Expo apps, also declare a URL scheme in app.json (expo.scheme) or app.config.js/.ts — needed so a rebuilt/relaunched app connects straight to the task's Metro instead of stalling on the dev-client's "Development servers" picker screen
  • A GitHub or Bitbucket remote on the target repo if you want auto-push and a PR URL (opt-in via pushOnDone)

Install

npm install
npm run build
npm link    # puts execbro-task and execbro-worker on your PATH

To uninstall the global symlinks: npm unlink -g from this directory.

Usage

1. Generate the config. init discovers your booted simulators and Android AVDs and writes them as slots into ~/.execbro/config.json:

execbro-task init           # interactive — shows the proposed config and asks to confirm
execbro-task init --yes     # non-interactive — write without confirmation

Re-run it any time you add or remove devices; it merges new slots in and preserves your other config fields (including a slot's enabled state — see below). If you need to inspect what's available without writing the file, use execbro-task devices; the listing is annotated with each device's current config state (enabled, disabled, or - if unconfigured).

init validates the merged result against the config schema before writing it — a config.json that parses as JSON but fails validation (e.g. a hand-edited field of the wrong type) makes init fail with a clear message instead of silently rewriting it.

Reserving a device for manual use. Every slot has an enabled field (default true). A disabled slot stays in config.json — still listed, still visible in execbro-task devices — but the scheduler never picks it. This is how you keep a device configured for reference while dedicating a different one to automation, without deleting its slot:

execbro-task devices --disable "Pixel_9_API_34"   # never scheduled from now on
execbro-task devices --enable "Pixel_9_API_34"    # re-activate it

(No restart needed — a running execbro-worker watches config.json and picks this up automatically before its next task pickup; a task already running keeps the config it started with.)

2. Start the daemon in a terminal you can leave running:

execbro-worker

3. Write a task file. It's just a markdown prompt — the same thing you'd paste into Claude Code interactively:

Add a pull-to-refresh to the orders screen. The list lives in
src/screens/Orders.tsx and is backed by the `useOrders` hook.

4. Enqueue it. The repo is auto-detected from the prompt file's git root:

execbro-task add prompts/pull-to-refresh.md                 # default: iOS
execbro-task add prompts/foo.md --devices android           # Android
execbro-task add prompts/foo.md --devices ios,android       # both
execbro-task add prompts/foo.md --parallel                  # let it run alongside other parallel tasks
execbro-task add prompts/foo.md --devices android --device "Pixel_8_API_34"  # pin one specific device
execbro-task add prompts/foo.md --force-device               # skip the "is this device already in use" check
execbro-task add prompts/foo.md --force-rebuild               # rebuild the native app even if nothing changed
execbro-task add prompts/foo.md --eager                       # boot the device and build the app up front

Deferred by default

A task starts deferred: it gets a worktree and node_modules and nothing else — no simulator, no Metro, no pod install, no native build. Most tasks are code changes that never touch a device, and skipping bring-up saves several minutes each.

If the agent decides it needs to verify on a device, it runs:

execbro-task provision <id>

That blocks while the runner claims a device slot, boots it, starts Metro, and builds and installs the app pinned to that Metro. If nothing frees up within deferredProvisionWaitSec (default 900), the command exits non-zero, the agent finishes code-only, and it says so in its final message — the task is not failed. A verification that couldn't be attempted is a finding in the report, not a reason to discard a branch.

Pass --eager for a task you know needs a device from its first step; it provisions before the agent starts, which is what every task did before this.

Triage: what the task turns out to be

A task no longer has to arrive as a worked-out plan. It can be a prompt and a link to a bug tracker, and the agent works out the rest — so before it edits anything, it declares what it found:

execbro-task verdict <id> --proceed --file plan.md
execbro-task verdict <id> --needs-decisions --file findings.md

--proceed attaches a short plan and carries on. Every task writes one, however small; it's the statement of intent a reviewer reads before the diff.

--needs-decisions records that the task turns on choices the codebase cannot settle — not that it was hard, but that it was underdetermined. The command validates that the document names at least two of them, each with its options and why nothing in the repo, the ticket or the conventions decides it. (One is enough when the ticket itself was unreachable.) That bar exists because writing a report is always cheaper than fixing a bug, so the easy exit has to cost something.

Such a task finishes done, not failed — nothing broke, and the agent did the right thing. Its report carries a Decisions required section, the queue list and the macOS notification both flag it, and outcome.json records resolution. Answer the decisions and re-queue the prompt with your answers appended; there's deliberately no channel back into a finished session.

The report also notes when a verdict was declared after the edits it claims to plan — a plan written afterwards is a rationalisation, and that's worth seeing.

Two consequences worth knowing:

  • Device-free tasks are exempt from the serial-blocking rule and may start alongside a device task, and past a queue head that's blocked waiting on a busy device. maxDeviceFreeTasks (default 2) caps how many run at once — each is still a full headless Claude session.
  • Whether to request a device is the agent's judgement call. Nothing forces it.

By default the scheduler picks the first enabled device slot for the requested platform(s) that isn't already in use (another Metro paired with it, or the app already running there — protecting a manual dev session from being stomped on). --device narrows that search to one specific slot for a single task; --force-device skips the in-use check when you know it's a false positive (it never skips a disabled slot — that's a deliberate exclusion, not a busy heuristic).

Preferring a device by default. Set defaultDevice in config.json to steer the scheduler toward one slot per platform without pinning every task to it:

{
    "defaultDevice": {
        "ios": "iPhone 15",
        "android": "Pixel_9_API_34"
    }
}

For iOS, the value matches either the simulator's UDID or its display name (case-insensitive); for Android, the AVD name — both compared against each slot's deviceId. It's a soft preference, not a pin: the scheduler tries that slot first, but falls back to the rest in id order when it's disabled, in-flight, or already busy. A task's --device still overrides it outright. An unrecognized name is logged and ignored rather than failing the worker. (No restart needed — like any other config.json edit, a running worker picks this up automatically before its next task pickup; a task already running keeps the config it started with.)

5. Check progress:

execbro-task list                       # all tasks and their status
execbro-task show <id>                  # descriptor + log path + session id
tail -f ~/.execbro/logs/<id>.jsonl      # follow the agent's transcript live (raw stream-json, one event per line)
cd ~/.execbro/worktrees/<id> && claude --resume <session-id>   # attach interactively

If pushOnDone is enabled in your config, the finished branch is pushed and you'll get a macOS notification with a PR URL (GitHub or Bitbucket).

If a running task needs a native rebuild — the agent added a pod, a Gradle dependency, or an Expo config plugin — Metro alone won't pick it up, because Metro only reloads JavaScript:

execbro-task rebuild <id>        # pod install + rebuild + reinstall + re-pin to the task's Metro port

Run this rather than pod install / expo run:ios by hand: reinstalling the app outside this command drops the setting that pins it to the task's Metro port. The agent is told about it in its preamble, and the runner also rebuilds automatically before committing if the native fingerprint changed during the session.

6. Clean up when you're done with a task:

execbro-task retry <id>          # re-queue a FAILED task as a fresh one, removing the original
execbro-task clean <id>          # one task
execbro-task clean --all-done    # everything in the done bucket
execbro-task clean --all-failed  # everything in the failed bucket
execbro-task clean --all-running # stop and remove running tasks (then restart execbro-worker)

Retrying a failed task. execbro-task retry <id> — or the retry button on a failed task in the dashboard — reads the failed task's prompt and settings, queues them again as a new task, then removes the original. The rerun gets a fresh worktree branched from the same baseBranch, exactly as the first attempt started.

The original's log, report and worktree go with it; its branch is kept, so any commits the failed attempt made are still there. The new task records retryOf, so it stays identifiable as a rerun after the original is gone. Retry refuses anything not in failed/, and refuses if the prompt file has disappeared (a CLI-added task whose file lived in your repo and was since moved) rather than queueing an empty prompt.

Deferred provisioning tuning

{
    "maxDeviceFreeTasks": 2,
    "deferredProvisionWaitSec": 900
}
  • maxDeviceFreeTasks (default 2) — how many tasks holding no device may run at once. Device-free tasks are exempt from the serial rule, so this is the only bound on them, and it bounds headless Claude sessions rather than devices.
  • deferredProvisionWaitSec (default 900) — how long execbro-task provision waits for a device before giving up. Generous on purpose: the wait is usually another task finishing, and an agent that gives up early falls back to shipping unverified.

Worktree cleanup

A worktree carries node_modules plus, for repos that gitignore their native dirs, cloned ios/ and android/ trees — several GB per task. The worker therefore deletes a task's worktree once it reaches a terminal state, after the report pipeline has read it. The task/<id> branch and its commit are never touched: the commit lives in the ref, not in the directory it was made in.

The trade-off is that cd ~/.execbro/worktrees/<id> && claude --resume <session-id> no longer has a directory to run in for a cleaned task. Control it in ~/.execbro/config.json:

{
    "cleanupWorktree": "always"
}
  • "always" (default) — delete on success and failure.
  • "onSuccess" — keep failed worktrees so you can inspect them and resume the session.
  • "never" — the old behaviour; worktrees accumulate until execbro-task clean.

Prompt templates

The agent's preamble, system prompt, verification suffixes, and analysis prompt live in ~/.execbro/templates/ so you can edit them. They're seeded from the packaged copies on first use, and a .shipped.json manifest records the hash of whatever this tool last wrote — that's what lets an upgrade tell "an old copy you never touched" from "a copy you deliberately edited". Untouched copies are refreshed automatically; edited ones are never overwritten, only reported.

execbro-task templates             # current / stale / customized / missing, per file
execbro-task templates --refresh   # adopt the packaged copies (yours saved to <name>.bak)

A file present with no manifest entry (an install predating the manifest) is treated as customized, since it can't be proven unmodified — use --refresh to adopt the packaged version.

Serial vs parallel tasks

By default each task runs serial: it waits until nothing else is running, then runs alone. Pass --parallel to let a task run alongside other parallel tasks (subject to free slots and Metro ports). A serial task acts as an implicit barrier: parallel tasks queued behind it wait until it finishes.

Driving it from Claude Code

SKILL.md in this repo documents when and how Claude Code itself should use execbro-task/execbro-worker — when to enqueue vs. run interactively, how to read task status and logs, how to diagnose a "device busy" skip, and common mistakes to avoid. Copy or symlink it into ~/.claude/skills/execbro-runner/SKILL.md (Claude Code's user-level skills directory) so it's auto-discovered; a plain relative reference from this repo isn't picked up on its own. If you edit SKILL.md here, remember to re-sync the installed copy — the two aren't linked.

How a task flows through the system

  1. execbro-task add foo.md writes a JSON descriptor to ~/.execbro/queue/inbox/.
  2. execbro-worker sees it and claims a slot via flock, skipping any enabled slot it can't confirm is free (already running the target app, or paired with another live Metro) unless the task set --force-device.
  3. The provisioner creates a worktree at ~/.execbro/worktrees/<task-id>. If the repo gitignores ios//android/ (the Expo CNG default), a worktree contains no native dirs at all — so the provisioner copy-on-write clones them from the source repo, which is near-instant on APFS and gives the task its own isolated copy. It then runs npm install (and pod install if needed), boots the assigned iOS simulator or Android emulator, installs the app, and waits for Metro to come up.
  4. The runner composes the prompt and launches claude headlessly in the worktree with the ExecBro MCP server pre-configured to talk to this task's Metro. The session id is recorded so you can --resume it.
  5. The runner streams the transcript to ~/.execbro/logs/<id>.jsonl and waits for the agent to exit.
  6. Before committing, the runner re-fingerprints the native side. If the agent changed it, the app is rebuilt and reinstalled so the device matches the branch; a failure here is reported, not fatal.
  7. On success: the agent's commit is on task/<id>. If pushOnDone is set, the branch is pushed and a PR URL is composed; a macOS notification fires either way.
  8. After the report pipeline finishes, the worktree is deleted (see Worktree cleanup); the branch stays.

Session reports

Every finished task (success or failure) gets a report directory at ~/.execbro/reports/<taskId>/, built by a post-processing pipeline that runs in the worker daemon right after the task's slot is released — it never holds a simulator or emulator busy. Artifacts:

  • outcome.json — a deterministic per-task outcome: status, timings, the agent's closing message, errored tool calls, the git changes on its branch, and the tail of the runner log. Written for every task, including ones that fail in provisioning and never produce a transcript
  • task-report.md — a short human summary built from outcome.json: what was done, what blocked it, what to do next. Rendered deterministically first, then rewritten as prose by one claude -p pass; if that pass fails the deterministic version stays, so there is always a report
  • transcript.jsonl — the agent's tool-call stream, split out of logs/<taskId>.jsonl so that log file stays pure human-readable text
  • analysis.json — a deterministic extract: per-tool call counts, error counts, latency, and detected act-then-observe round trips
  • report.md — a short markdown write-up generated by one claude -p pass over analysis.json. This one reviews the ExecBro tool surface, not the task — task-report.md is the "what happened to my task" artifact
  • filmstrip.html — a self-contained page pairing each screenshot the agent took with the actions that led to it; opens standalone in a browser, no server needed
  • <udid>.mp4 + <udid>.chapters.vtt — a full-session screen recording per device (when video capture is enabled) with a WebVTT chapter track indexing it by ExecBro tool call

Controlled by the sessionReport block in ~/.execbro/config.json:

{
    "sessionReport": {
        "enabled": true,
        "video": "all",
        "model": "sonnet"
    }
}
  • enabled — turn the whole pipeline off for a leaner run
  • video — which platforms get a screen recording: "all" | "ios" | "android" | "off". This only gates the .mp4/.chapters.vtt files — analysis.json, report.md, and filmstrip.html are built regardless, on both platforms, since they cost nothing but a JSON parse and a Claude call.
  • model — the model both claude -p report passes run on (task-report.md and report.md). Accepts an alias ("sonnet", "opus") or a pinned id ("claude-sonnet-5"); defaults to "sonnet". Override for one run with execbro-task report <id> --model <model>.

Every stage (outcome, analysis, report, filmstrip, chapters) is independently fail-open: a failure in one is logged and the rest still run, so a task that already succeeded is never affected by report generation going wrong. The outcome stage runs first and does not need a transcript — that is what makes a provisioning failure produce a report instead of nothing.

Re-run post-processing for a finished task, optionally starting partway through:

execbro-task report <id>                    # re-run every stage
execbro-task report <id> --from outcome     # re-run the task report only, then everything after
execbro-task report <id> --from filmstrip   # re-run filmstrip and chapters only
execbro-task report <id> --model opus       # one-off model override

execbro-task show <id> prints task-report.md inline once it exists, and the dashboard shows it as a panel on the task detail screen.

Dashboard

execbro-dashboard serves the same queue you'd otherwise drive with execbro-task over HTTP — read task status/logs/artifacts, and enqueue new tasks — for a browser-based UI or a script to talk to. It's node:http only (no framework), and phase 1 is read-and-enqueue: there's no cancel, pause, or reorder yet, and DELETE /api/tasks/:id refuses anything whose status isn't done or failed (queued and running tasks can only be stopped today via execbro-task clean --all-running).

⚠️ This API can execute arbitrary code on the host. Enqueueing runs Claude Code with --dangerously-skip-permissions against a real git worktree. Binding dashboard.bind to anything beyond 127.0.0.1 exposes that to whatever network can reach it. Only do so behind a private overlay network (e.g. Tailscale) or a tunnel that provides its own authentication — the bearer token here is the only other line of defense.

1. Generate an API token. The dashboard refuses to start without one:

openssl rand -hex 32 > ~/.execbro/dashboard-token && chmod 600 ~/.execbro/dashboard-token
  • Use an ASCII token (hex or base64, as openssl rand -hex produces). Node decodes request headers as latin1 while the token file is read as utf8, so a token containing non-ASCII bytes can never match the one you generated — auth fails closed, but silently, and you'll see nothing but 401s.
  • chmod 600 is not optional. This file is the key to arbitrary code execution on the host; a world-readable token is a full compromise. The server checks the file's mode at startup and logs a warning (not a refusal) if it's wider than 0600.
  • EXECBRO_DASHBOARD_TOKEN in the environment works instead of (and takes priority over) the file, useful for a systemd unit or CI. It's still held to the same 32-character minimum.

2. Configure dashboard and repos in ~/.execbro/config.json:

{
    "dashboard": {
        "bind": "127.0.0.1",
        "port": 8770
    },
    "repos": [
        { "name": "myapp", "path": "/Users/you/code/myapp" }
    ],
    "repoRoots": [
        "/Users/you/code"
    ]
}

dashboard defaults to 127.0.0.1:8770 if omitted.

repos and repoRoots together form the enqueue allowlist — a client never supplies a filesystem path, only a name resolved against it, so this is exactly what the API is trusted to run agent tasks against. With neither configured, reads still work but enqueueing is disabled.

  • repos names individual repositories. Use it when you want to be precise about which projects are runnable.
  • repoRoots names directories, and every immediate child that is a git repository is allowlisted under its own directory name. A project you clone later is enqueueable without editing config or restarting the dashboard — the roots are re-scanned on each request. Children that aren't git repos, and dotdirs, are skipped.

⚠️ repoRoots widens the boundary to "anything that is, or later becomes, a git repo directly inside these directories." That is the point of it, but it means anything able to write into one of those directories can make itself a target for an agent running with --dangerously-skip-permissions. Point it at directories you control; never at ~/Downloads or a shared/synced location.

Names are unique and first-wins: an explicit repos entry beats a discovered one, earlier roots beat later ones, and within a root the order is alphabetical. GET /api/repos returns the resolved list, so what the UI offers is always what POST /api/tasks will accept.

3. Build the browser UI (optional but recommended):

cd web && npm install && npm run build

This produces web/dist. The dashboard server serves it automatically at / once it exists — no separate step, no config flag to flip. Without it, / 404s cleanly rather than crashing, and the JSON API under /api/ still works on its own (e.g. from a script, or curl).

The UI itself has no login flow of its own: on first load it shows a form asking for the same token from ~/.execbro/dashboard-token (or EXECBRO_DASHBOARD_TOKEN), and once submitted, stores it in the browser's localStorage and attaches it as Authorization: Bearer <token> on every request from then on. A 401 (wrong or revoked token) clears the stored token; reloading the page brings the form back. There's no server-side session — clearing site data or using a different browser just means pasting the token again.

Like the API it talks to, the UI is read-and-enqueue only: it lists tasks, tails logs, plays back recordings, and can submit a new task from #/new, but there's no cancel, pause, or reorder button anywhere in it — that matches the API's phase-1 scope above.

4. Run it:

execbro-dashboard

It serves web/dist at / when that directory has been built and the JSON API under /api/.

5. Keep it running (macOS). execbro-worker and execbro-dashboard are both plain foreground processes, so on a machine meant to stay up — a Mac mini acting as a home server — install them as launchd agents instead:

npm run autostart            # write both plists and start them
npm run autostart:status     # show whether launchd has them running
npm run autostart:stop       # stop both and remove the plists

Both are started with RunAtLoad and KeepAlive, so they come back after a crash and after a reboot, logging to ~/.execbro/logs/com.execbro.{worker,dashboard}.log. Installing takes ownership: any hand-started worker or dashboard is stopped first, because a second worker would exit on the worker.pid conflict and be restarted forever.

Two things specific to this being a LaunchAgent rather than a LaunchDaemon:

  • A user must be logged in. The worker drives iOS Simulators and screen recording, which need a real GUI session — so a headless mini wants automatic login enabled, and sleep disabled (sudo pmset -a sleep 0).
  • PATH is rebuilt at install time. launchd hands processes a bare PATH, which would leave claude and adb unreachable; the script resolves them when it writes the plist. Re-run npm run autostart if you later move or reinstall those tools — and note the plists point at this checkout's build/, so re-run it if you move the repo.

Endpoints (all except /api/health require Authorization: Bearer <token>):

Method & path Purpose
GET /api/health Liveness check, no auth required
GET /api/tasks All tasks, bucketed by inbox/running/done/failed
POST /api/tasks Enqueue: { repo, baseBranch, title, prompt, devices?, parallel?, forceRebuild?, forceDevice? }. There is no eagerDevice field, so dashboard-enqueued tasks are always deferred and provision on request
GET /api/tasks/:id One task's descriptor, duration, task-report.md body, and signed artifact URLs
DELETE /api/tasks/:id Clean a task — 409 unless it's done or failed
POST /api/tasks/:id/retry Re-queue a failed task as a new one and remove the original
GET /api/tasks/:id/log?offset=N Byte-offset tail of the raw log, for polling
GET /api/tasks/:id/transcript?offset=N Byte-offset tail of the tool-call transcript
GET /api/tasks/:id/analysis analysis.json, whole file
GET /api/tasks/:id/report report.md, whole file
GET /api/tasks/:id/task-report task-report.md, whole file
GET /api/tasks/:id/artifacts Signed, time-limited URLs for every file in the task's report dir
GET /api/tasks/:id/artifacts/*?t=<token> Streams one artifact (images, video with byte-range support) using the signed token from the listing above, not the bearer header — this is what an <img>/<video> tag hits directly
GET /api/worker `{ status: "running"
GET /api/repos The configured repo allowlist
GET /api/devices Configured slots, each with busy and heldBy
GET /api/devices/discovered Simulators and AVDs found on this machine, each flagged configured if already in a slot
POST /api/devices Add a slot: { platform, deviceId }, where deviceId must be one the discovery scan just reported
PATCH /api/devices/:id { enabled?, headless? } — partial update, an omitted field is left alone; 409 if the slot is busy
DELETE /api/devices/:id Remove a slot — 409 if busy; returns 200 { ok: true } (not 204)
GET /api/repos/:name/branches Local branches of an allowlisted repo, newest first

Slot changes are written to ~/.execbro/config.json and picked up by a running worker automatically — it watches the file and swaps its config between task pickups, so no restart is needed. A task already running keeps the config it started with. A malformed edit is logged and ignored rather than stopping the worker.

Any change to a slot that is currently running a task (PATCH or DELETE) is refused with 409. If a worker was killed mid-task, its descriptor stays in the running bucket and its slots read as busy indefinitely — clear them with execbro-task clean --all-running.

deviceId is never free text: POST /api/devices only accepts an identifier the discovery scan just returned, because that value reaches simctl/adb directly. To change a slot's device, remove it and add the new one.

Android slots have a headless flag, default true — the emulator boots with -no-window, invisible, matching the default before this flag existed. Setting it to false opens a visible emulator window so a task can be watched; the change takes effect the next time that slot boots an emulator, not one already running. The flag is accepted on iOS slots too but ignored: simulator visibility depends on whether Simulator.app is open, not a simctl flag.

Artifact responses (/api/tasks/:id/artifacts/*) carry X-Content-Type-Options: nosniff and a sandboxing Content-Security-Policy, because report artifacts are written by the autonomous agent task itself, not the operator — an .html report or .svg frame is untrusted content and is served defensively.

Testing

npm test

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages