Skip to content
Merged
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
5 changes: 4 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@ from an agent, see [`skills/fleet-runner/SKILL.md`](skills/fleet-runner/SKILL.md
| Path | What |
|---|---|
| `collector/` | The brain: Fastify + `node:sqlite`, the queue, the dashboard (`dash/`), the host executor |
| `collector/src/workloads/explore/` | Exploratory QA: a vision model drives an app from mission cards (`collector/examples/missions/`); see `docs/explore/internals.md` |
| `collector/src/api/v1/` | The control API. `routes.ts` is the single route table: it registers the routes, generates `openapi.json` and the typed client |
| `collector/src/client/` | The generated client (`api.gen.ts`) and its transport. The CLI and the MCP server use nothing else |
| `fleet/` | The `fleet` / `fleetr` command: runs a brain or a runner, plus the work commands, `fleet mcp` and the remote gateway |
| `runner-machine/`, `runner-android/`, `runner-ios/`, `runner-roku/`, `desktop/` | Runners. They speak the runner protocol (`/devices/register`, `/devices/:id/next-job`, `/results`), not `/api/v1` |
| `desktop/` | A Tauri menu-bar app around `fleet`. A front door, not a runner; compiled in CI and never launched |
| `runner-machine/`, `runner-android/`, `runner-ios/`, `runner-roku/`, `collector/runner-web/` | Runners. They speak the runner protocol (`/devices/register`, `/devices/:id/next-job`, `/results`), not `/api/v1` |
| `skills/fleet-runner/` | The agent skill. Its tables are generated |
| `bug-garden/` | An Android fixture app with twenty planted defects, for scoring exploratory QA |
| `docs/` | The mkdocs site. Built with `--strict`, so a broken link fails CI |

## Suites
Expand Down
62 changes: 62 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ a CI job fails when they disagree.
the bundle gains a directory the installers do not copy. The installers are
fetched from `main`, so re-running the install command fixes an existing
0.8.0 install; no new release is needed.
- **`bug-garden/tools/check-bench.mjs` passed `bench-delete-plant` without
checking the plant was gone.** The card's `check` gained `absent_text`, which
the harness enforces and the standalone checker ignored; it also matched
case-sensitively where the harness does not. It now reads `absent_text` and
matches case-insensitively, as the bug garden's README says.

## [0.8.0] — 2026-10-15

Expand Down Expand Up @@ -126,6 +131,63 @@ pull request that touches them:
unversioned routes with hand-written types
([#83](https://github.com/addisdev/fleet-runner/issues/83)).

### Also in this release

These merged into `develop` before 0.8.0 was cut and shipped in it, but were
left out of the notes above on release day.

- **Exploratory QA, the `explore` workload.** A vision model works through
mission cards on tonight's build, driving a device the way a person would,
while the harness checks every step for crashes, ANRs, frozen and blank
screens, dead controls, accessibility and visual defects. Each candidate is
replayed twice on a clean install and only what reproduces is filed; a crash
stands on its log. See [Exploratory QA](docs/explore/index.md).
([#70](https://github.com/addisdev/fleet-runner/pull/70))
- **Findings, verdicts and a morning digest.** `POST /findings` merges repeats
by fingerprint; the dashboard's Findings page has Real, Duplicate, Not a bug
and Agent's mistake. A check under 30% precision after ten verdicts is
switched off for that app (crashes and ANRs never are). GitHub issues stay a
dry run unless `FLEET_GITHUB_ISSUES=1` and a token are set, and the collector
only ever creates them. ([#70](https://github.com/addisdev/fleet-runner/pull/70))
- **What it drives:** Android phones, tablets, Android TV and Fire TV over adb;
iOS and tvOS through FleetDriver, a new XCUITest command server in
`runner-ios`; a Roku through tvloop. The leash refuses delete, purchase,
invite and sign-out controls unless a card allows them and never types into
a password field. ([#70](https://github.com/addisdev/fleet-runner/pull/70))
- **The bug garden**, an Android fixture app with twenty planted defects, a
scoring rule and scorer, and twenty bench cards for scoring a model without
a judge. Its screenshot walks capture each screen in the base, dark, Spanish
and large-text settings. ([#70](https://github.com/addisdev/fleet-runner/pull/70),
[#74](https://github.com/addisdev/fleet-runner/pull/74))
- **Nights on ultra.** `scripts/explore-night.ts` is the night-queue command:
it boots the devices, opens its own tunnel to the brain, runs one job and
exits with its result. `nightly-builds.sh` builds GreenFolio iOS and
Dozehound tvOS from `origin/develop`; the tvOS and iOS nights run after the
products' own night jobs. Holo4 35B-A3B measured 100 of 100 on pointing and
a median step of 6.8 s. Fire TV is parked and the TV question is answered on
tvOS. ([#73](https://github.com/addisdev/fleet-runner/pull/73),
[#77](https://github.com/addisdev/fleet-runner/pull/77),
[#78](https://github.com/addisdev/fleet-runner/pull/78))
- **The dashboard draws each device as the object it is**: 22 drawings
(phones, tablets, TV boxes and sticks, computers, watches, a headset, a
board, a browser) instead of seven silhouettes, chosen from the model string
first because older agents report a Roku as `android` and an Apple TV or a
watch as `ios`. ([#69](https://github.com/addisdev/fleet-runner/pull/69))
- **The Android runner starts on Android 7.0 and 7.1.** `RunnerService` called
`startForegroundService`, which exists from API 26, so API 24 and 25 threw
`NoSuchMethodError`; it uses `startService` there now.
([#75](https://github.com/addisdev/fleet-runner/pull/75))
- **The Android runner is linted in CI.** The `android.hardware.type.watch`
feature line that failed `lintDebug` is gone; the runner still detects a
watch at run time. ([#76](https://github.com/addisdev/fleet-runner/pull/76))

Not verified, from those pull requests: a physical iPhone or Apple TV through
FleetDriver; a real Roku (its developer password is not on the host yet); any
model-driven night on an Apple surface (the iOS and tvOS simulators were driven
without a model); the GreenFolio Android cards; the Android 7 fix on a real
API 24 or 25 device (Robolectric only); and any Wear device, before or after
the lint change.

## [0.7.2] — 2026-09-30

### Fixed
Expand Down
8 changes: 5 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ that is not on this shelf is data this repository does not have.

## Running the suites

Four components, four suites. Only run the one you touched.
Five components, five suites. Only run the one you touched.

=== "collector"

Expand Down Expand Up @@ -80,7 +80,7 @@ Four components, four suites. Only run the one you touched.
npm install && npm test
```

122 tests, about a second. This is the easiest component to contribute to
About a second. This is the easiest component to contribute to
and the reference implementation of the protocol.

## Things this project learned the hard way
Expand Down Expand Up @@ -162,7 +162,9 @@ the others.
One version covers every component, so a release is one number everywhere.

1. On a branch from `develop`, run `node scripts/version.mjs set X.Y.Z`. It
writes `VERSION` and every component's own version. Then run
writes `VERSION` and every component's own version except the desktop
app's (`desktop/src-tauri/Cargo.toml` and `tauri.conf.json` are not among
the places it writes, and still say 0.5.0). Then run
`npm --prefix collector run gen:api`, because `openapi.json` carries the
version and CI fails when it is stale. Android's `versionCode` is not
derived from it; bump that by hand if the Play Store build needs it.
Expand Down
38 changes: 25 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ One queued job installs a build on every attached device, runs a UI suite
across them, benchmarks llama.cpp on real silicon, classifies a few hundred
images through Core ML and LiteRT, screenshots a website on two real phone
screens and diffs it against a baseline, or drains a battery on purpose and
plots the curve. Everything lands in one results database with one dashboard
in front of it.
plots the curve. Overnight, a local vision model can use tonight's build the
way a person would and leave a short list of bugs that reproduced on a clean
install. Everything lands in one results database with one dashboard in front
of it.

Built to answer a question I could not otherwise answer: **would on-device
machine learning actually be good enough to ship in my apps, and on which
Expand All @@ -27,7 +29,7 @@ hardware?** It turned out to be yes, and the fleet is how I know.
| **Problem** | Mobile tests and ML benchmarks are only useful when they run on the hardware, under conditions, and with the exact artifacts a product will ship. |
| **System** | One collector owns the registry, queue, leases, artifacts, results and dashboard; independently implemented agents claim work through one tested JSON protocol. |
| **Reach** | Five runner implementations across TypeScript, Swift, Kotlin, JavaScript and BrightScript, plus host-driven iOS and Android workloads. |
| **Workloads** | 28 workload types spanning UI tests, visual regression, llama.cpp, Core ML, LiteRT, battery experiments, builds and deployment. |
| **Workloads** | 34 workload types spanning UI tests, visual regression, exploratory QA by a vision model, llama.cpp, Core ML, LiteRT, battery experiments, builds and deployment. |
| **Proof** | The first production question found an int8 CPU model faster to load and run than its fp32 GPU alternative while preserving useful accuracy. |

**What this demonstrates:** distributed systems, mobile platforms, protocol
Expand All @@ -40,8 +42,8 @@ trade-offs.

![The Overview while one fan-out benchmark is claimed by three agents at once: the queue fills, three rows appear under running now, and the done count climbs as each reports back](docs/img/fanout.gif)

One `POST /jobs` with `"fanout": true`, and every agent the expression matches
claims its own child job. Above: a laptop and two browser runners on a collector
One job with `"fanout": true`, and every agent the expression matches claims
its own child job. Above: a laptop and two browser runners on a collector
that did not exist a minute earlier. Nothing there is staged — it is the
shipping agents against a real collector, recorded a frame every 250 ms.

Expand All @@ -63,8 +65,9 @@ curl -fsSL https://raw.githubusercontent.com/addisdev/fleet-runner/main/install.
> The installer verifies the download against the release's `SHASUMS256.txt` and
> refuses to install if it does not match. It needs Node 22.13 or newer and
> carries no runtime of its own. **`fleet up` itself has been watched to work on
> macOS/arm64 only** -- CI runs the suites on Windows and Linux, which is not the
> same as somebody having run a fleet on one.
> macOS only**, on Apple silicon and on a 2016 Intel Mac that runs this lab's
> brain -- CI runs the suites on Windows and Linux, and starts the container and
> the systemd unit, which is not the same as somebody having run a fleet on one.
> **[docs/install/](https://addisdev.github.io/fleet-runner/install/)** is honest
> per platform.

Expand Down Expand Up @@ -137,15 +140,16 @@ something a runner can do without the collector shipping a release.

![Twenty-eight workloads in three columns by who claims them: on the device, on a host, and on a machine](docs/img/workloads.png)

Twenty-eight workloads, and the column a workload sits in is the answer to
"who can physically do this". A benchmark runs inside the app on the phone. An
Thirty-four workloads (the figure predates six of them, `explore` and `tvloop`
among them), and the column a workload sits in is the answer to "who can
physically do this". A benchmark runs inside the app on the phone. An
install needs a cable and a Mac. A build needs a checkout and a toolchain.
Each has [its own page](https://addisdev.github.io/fleet-runner/workloads/),
saying what it measures and what it refuses to guess.

## What is in here

Seven projects in one repository, in three groups: **one brain**, **five
Eight projects in one repository, in three groups: **one brain**, **five
runners** that speak its protocol and share no code with each other or with it,
and **two front doors** onto the whole thing. They ship independently but they
are versioned together, because the protocol is the thing that breaks and a
Expand All @@ -155,7 +159,7 @@ change to it touches three of them at once.

| | What it is |
|---|---|
| **[collector/](collector)** | Device registry, job queue with leases, artifact store, results database, scheduler, alert engine, and the dashboard above. Node + Fastify + SQLite, no broker, no cloud. It also holds the host executor, which drives phones from outside over adb, simctl and devicectl. |
| **[collector/](collector)** | Device registry, job queue with leases, artifact store, results database, scheduler, alert engine, and the dashboard above. Node + Fastify + SQLite, no broker, no cloud. It also holds the host executor, which drives phones from outside over adb, simctl and devicectl, and the [exploratory QA](https://addisdev.github.io/fleet-runner/explore/) workload a vision model drives. |

**The runners** -- five hand-written implementations of one protocol, in five
languages, sharing not one line
Expand All @@ -175,6 +179,13 @@ languages, sharing not one line
| **[fleet/](fleet)** | The CLI. `fleet up` runs this machine's components under a supervisor that backs off and gives up loudly rather than restarting a broken component forever; `fleet doctor` says what the machine can run and why not; `fleet service install` keeps it up. It wraps the three programs above and reimplements none of them. |
| **[desktop/](desktop)** | A Tauri menu-bar app around that CLI: a tray menu, role switches, the dashboard in a window, and a notification when a component gives up. It exists to test one hypothesis about macOS local-network permissions, and **it has never been launched**: CI compiles its Rust and that is all -- read its README before believing anything in it. |

**Alongside them**

| | What it is |
|---|---|
| **[skills/fleet-runner/](skills/fleet-runner)** | The agent skill `fleet skill install` copies into a coding agent: which commands to run in what order. Its tool and exit-code tables are generated from the code. |
| **[bug-garden/](bug-garden)** | An Android app with twenty planted defects, so exploratory QA has something with a known answer to be scored against. |

Each directory has its own README, its own tests and its own CI job, filtered by
path so a change to a phone runner does not build the dashboard.

Expand Down Expand Up @@ -272,8 +283,9 @@ that is not on this shelf is data this repository does not have.
the handful of things this project learned the hard way. Behaviour is covered by
the [Contributor Covenant](CODE_OF_CONDUCT.md).

**There is no authentication, by design** — the collector is built for a home
LAN or a tailnet. [`SECURITY.md`](SECURITY.md) says what that does and does not
**By default there is no authentication** — the collector is built for a home
LAN or a tailnet, and [scoped tokens](https://addisdev.github.io/fleet-runner/deploy/auth/)
are opt-in. [`SECURITY.md`](SECURITY.md) says what that does and does not
cover before you report it.

## License
Expand Down
27 changes: 17 additions & 10 deletions bug-garden/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ mission and before a replay) puts it back exactly where it started.
| `android/` | The app. Kotlin, Jetpack Compose, minSdk 26, AndroidX only. |
| `defects.json` | The twenty defects: class, which check should catch it, screen, how to reach it, what it looks like, and a match pattern. |
| `tools/check-bench.mjs` | Checks a bench mission's `check` block against a `uiautomator dump`, with no model. |
| `tools/verify-on-device.sh` | Walks to every defect on a device and checks each one still fires; see [Proving it on a device](#proving-it-on-a-device). |
| `../collector/examples/missions/bug-garden/` | Seven exploration missions and twenty bench missions (`bench-*`). |
| `../collector/examples/flows/bug-garden/` | `sign-in.yaml` (the missions' setup flow), `smoke.yaml`, and `bench/`, a scripted answer to each bench mission. |
| `../collector/examples/flows/bug-garden/` | `sign-in.yaml` (the missions' setup flow), `smoke.yaml`, `bench/` (a scripted answer to each bench mission), `probes/` (a walk to each defect), and the `t1-*.yaml` walks that take one screenshot of each screen in the base, dark, Spanish and large-text settings. |
| `../collector/scripts/explore-bench/garden-score.ts` | Scores a night's findings against `defects.json` by the rule under [Scoring](#scoring). |

## Build and install

Expand Down Expand Up @@ -89,7 +91,7 @@ in any of the three languages.

## The defects

Twenty, in eight classes. `defects.json` has the steps for each; in brief:
Twenty, in nine classes. `defects.json` has the steps for each; in brief:

| Id | Class | Check | Screen | Defect |
|---|---|---|---|---|
Expand Down Expand Up @@ -202,9 +204,15 @@ with quantity 3", "Change the language to Spanish"), defects off
(`launch_args: ["garden_defects=false"]`), and a `check` block a harness
evaluates against the final `uiautomator dump`:

- `text`: every string is a case-sensitive substring of some node's `text` or
`content-desc`. (Content-desc counts: the Settings icon on Home is labelled
"Ajustes" in Spanish, so the Spanish check passes on Home or on Settings.)
- `text`: every string is a case-insensitive substring of some node's `text`
or `content-desc`. (Content-desc counts: the Settings icon on Home is
labelled "Ajustes" in Spanish, so the Spanish check passes on Home or on
Settings.)
- `absent_text`: no node's `text` or `content-desc` contains any of these
strings. `bench-delete-plant` uses it to check that Aloe Vera is gone, not
just that the list is one plant shorter.
- `focused_label`: the focused node's label contains the string. The TV cards
use it, since a D-pad screen's state is where focus sits.
- `screen`: some node has `resource-id="screen_<screen>"`.

The checks only use text that the seed data cannot produce by itself. No seeded
Expand All @@ -213,11 +221,10 @@ the Office, so "Qty 3" can only come from the plant the mission added. Where a
check names no screen, it holds on both the plant list and the plant's detail
page, which is where a model doing the task naturally ends.

One limit: the check block has no way to say text must be absent, so
`bench-delete-plant` checks for "8 plants" (one fewer than the seed) rather
than for Aloe Vera's absence.

`tools/check-bench.mjs` implements exactly these rules:
`tools/check-bench.mjs` implements these rules. The one difference from the
harness is `screen`: the harness matches the screen map's name for the final
screen, while the tool, which has only a dump, looks for the bug garden's
`screen_<screen>` tag. To check a dump by hand:

```sh
adb exec-out uiautomator dump /dev/tty > /tmp/dump.xml
Expand Down
Loading
Loading