)7jGK%mptItvr
z5)8cZk-$s2wa9`(^mDm~ga(hds?Q`Pi5J>AVC1{3E#F=j%;(Lq~AO|{vqQaU=)
zpOtJMCaCFEDW26l^C}i6XlL2KJh!o^`Uxzg_n;jNXiOY+Nf5%Vt;ayUO(Bw##
z7;q=m2Dr5r1`pE@u&-*13x^>(FXl&yfjtZmDvb_H@w;
zI!q_f|7;0;U1XuE&4i4a)lUtXbf#!OpNHN
zvoj`N+|P%Ri3uY}B*&V)oYpEZs9B~0BI@wZz<=gd=_qi#!bmW2if={M9~^yIrRfN_
zq91Dd&~!)8Tl~pV3PYp#kEr$p&*%1r2Zd8FLKrB33O{=NW0g)5(VqdEmrnZ)+Ua
z6sb^_wMY-bA1wiv)Yn3|daM!SHf
zNX6l)6}Qvh>U}22iZ%I|-^15}oz>EEnb4DT2)d)*93x%>p>k8j+1T9C3HS)3nV(U^
z;;0%nf=ytA1>Ad`6WNm$OEbe-;Qvw5e>KhYCy?|vtb=?<)Rd>&
ztdAN%0U5LvL%$E!J}4*O)2nqtIGH|yQnqk05tCPf$IaNpdi^8Gk9zu273A0d7nI-*
z=(ni_nJG+_P;W03elCq0y(a*ppCp
zq(|B9(z3IA(+Ahx+DZI7;i@$xI+GvwdvBq=)ncQ8|D^)xOCs=ND%LGJia;?`dx8Ga
zWg%w9gn+mg?eDHr)6thQdWcTFWk4AR?edb*<^SeC+BC
zjZEbUJ}6VAZ5^k%GWRgHUX>de8?Zn#G32A94tEXu(}P|u=#{6(FUe!2O#
zjJ~3I08rThrHSrNbc9rWiRTAPDKRY@QCA!2tR8Ew&l27P@;P98>5iVjFQae$k|H2x
zqAu~0aqhy!i%f^C*?Xg#n?T`Grc&uJ
z9Y^yH1l=x(&9`Zapo$F&X=K}S4Gj$--(Z32PWtdpJNx_lz9xpU(rE=*ykTusCXM
zeWs?CBl_EV>&6D~Md2oCw_@TD;}xEhqhkV<(9J&t^4OQM%>6B|b1Jnb#!&I{7Bdc}
zA?zDqpYhH4hU~fqb)el0%|96vNfVBbUU=WUh82&x%1`t*HagVefXl_U
zy`?^>A}2LZX+l-GR!J9f9zM}zd;6d~zvGcTAN#t4YP4iNt&7E=hIj0TmB>`z|zhGbTngpf#oBr
zPPoQ?OVDzeqMV3?$ZM|)4LayL#RwDCk-q%hUAb>hK3mJxZ00`QTbn?>C<#KLZG)XzI)C!QB#>`ToWG4!YKl
zYku8Sun~HOjRRGTA>t(REHNFD|FHtp^K&xz(Dzxk2Guhf-_@~xCF@@w$+1UpvDf2G
zM&)p1Mn*&4lZQt~x`00;TrT%&06xzqGLn;pFhglA<>2f+-#IhjD
zg^$EUk%_heBH7dRAmms^N@~0f6=ZHMUY}^DSHp9j)gV^-4jlRT9GR4u?CXAAb;3Ww@dMj%rTW1GVz1*~pj?noGE^hRF%WF;
z=EZ^r_ehmj}Y45m6$4{W202c
z_$Cyo&QyBaLvAw#m<5M-jsQ@=#AMc3Y3pPAf`Zj@b!bbqdE%p?4_o8*US8t3TLXb!
z`td3r=W@S2>HsPu+uTgq>`?r#duX6PFQO3+bVBcC)&ak{)%EpAMTPE^V_>%5(Oa*d
zze27au`=At02{IkCQsTOvE;?$6K@3x2{R7t>R%`sJ!exf&!ReZTj1+nrg9B_%aE9G
zTS-Nn_LgR9FA#FI_vDmPlWxoPdEQ&I&&%p$KjW#>-XRsS?E)|}p=Rj&_ZSOPp_>_O
z^kdbC9i)1Hm%K6$tH{%s0+9J1ZPA~=^u#Ce$JQOb#x&}8?#a68z+wDUm=?Wy2f3?8
zM)uirqXPmsOD0NT-I(@~Q0=1CE+ruG#rpZHd}>AXxyxT(n~2xd=1MHbODF-s6*38U
zvL!Nhg3KM)kLicJmcRENj#8Oz2{uX1>5F18Wou&$9=t0pg=AocasjX5&2I#LJ^=db
z#zn7)W`CG7wYIXp$E6z+9GpexvZKhgB0U=mJ8z(dG|+9rIQBWo0^C?@e3U
zKPZ_!{qXl4L-#8ya*?TYW@$?PfDHE^Al;b29jsT44Em-*;py(Q`lr26u*w#E*|Nhp>06>v
zfO=EMd3AV6x#)0%#x53xI12K!7n{CD+OKf~38E;VbC+`V)c{R@vVODkP4)pJ+D%=s|D4Tgtt
z<#|4{pBbiHrqXv(1lRg21J
z_LW*_aAgef_RjtHRoDNFm8U+W5)`nXX&DI)!#`M&0#AxYSOTHg<)X;+D2?
zfr8sA?DwIY^z?SNR?p(*8Uc~R2y}-dz4&+Tsdaw<4J{HMu9#*6yaVDdfKZGLKuC&;
z$n9{T!^@`#KbY;Bl`!Kj;f%UqmyRlzp_Lea)SXaMlo)TN$n;_914qUjL%hw5KER2p
zn?cEr(P!L%VPx{$xy$@#Hz5o^%jDqZ(tz{;%Sw!h{Oh01{Azgq22xo4LPc3d`zAkM
zL2y7o;JuBf?p`VN^^mJ;h3Pwhmx+FNG7KWie)KDuICpOB3CT+i4&Z4wJ4@zbJO1oV
zq>SqmV3}4kD8GXt1pE%geo9%{P;~UIJgsWWj@|yrcp6$a=6-t0M5w@_t12pLI61lD
zL%%BZUw?5!{o#e1*E%*)f%m?4Ff%YXV)ARtRzL%nPBKSh%Aaxe2uwxk+
zX>dCj!J#|7WWMzm6o}omrgU{;sg9NOztTcL-Xwr1y2tj{pug7-bc+4D9_`nwz-|3`
zImoYv&q=(!#C`7H@Bi5co=bl{0=`W#$o_fB$*-Th3A%lr@ZTSlcn6;Oudn~o6aRhu
z|FI$e^}^3ag#JhV%YO{{j}f8&t~d67Kjc3~g#JH%mj8!C{$oVw+<$5M{J#wOj}gKA
z`=6TX|LJQzoB#iPtLr}<@*gAqT7UoeNdEEu{CB@Sy1!oH|IH+rIR)ye)6?QI>t^ue
e%V&ql#dGRK&PK~WY-i5?oZM68C;5+GzWX;
zg)+uY1$UKpsiF^8X%I4Ca<)}NKr_@tSV3SM!dhvNa#36L3@}CKab`
zDu3ku6Vs#hqZ0-}`bI{)+ClapikHQ!olsU*j?ZN((Tj%AQ+<#87&$&k_cp8M$x)KS
zZUIPao3w7^2i?b+F86|-pnDg088Cei>%qP4>>zmV=P7?iZI#JOQg89faMRtiB_0b^
zGZ*4}N5jLV`c`)tjg_qrpDIsOT{H9xye+_EGK-B1yB_iRL2@HV?@x@BHS=)WoJ-41
zoR*f(Mcl>lRR?l6q_IHTDL-eiEe7QXl((iZ;w$GJ4$g!?Ela)GP%mxwWB#3SB=@b`
zbDutadeS=X(4oQ}PDmhZhJdp-`PoM_9g}F^(XQW%kFA96EYNmTHz;l#<=A_Kja;E?
z=6dGhs!tf~3|?-it{U1F>i(NVuBS!7Kqk8L{q)-qFM&~tIo*7LT2Jw~&B43`lrVeM
zDzE2dE(zD_r;7yRBoAiAwQz5fM1Aa7Iro`S;2yKE>B(cG4LY0yhcCaMqQAPdtKq@e
zRbOM|Ue}LaG(v{kIxm{eLu5y8VA){k{K;79McP@Lqn}uDo52du-WI=&j
z^lPC~J|;aH(5S2YJi}ro8VzXQcL99AMp%q88k9fNm2i1v)~{`fW|%duUlEB=xu6*4
z#=sC~*wz$Q=xX+8i_;sQ^|+*@yaxQt0uW1XP7db<_@KBCwq;kO%Q0JLtG@@aO6K_1K
z-|4d&YxEL7ucVT?R=SCui_u587@0Qx%&jJE!}$jU2N>99s(p$ald%z-{BlVgAfe9;l+!t>}t%8)R>D_
z)ryASX2#4BF#O5X_UR85sFu#3Iyw?D7rdi<4@UvyDL~Gt!SZD{`f^3Z5-(IC;kRKTYpS1j9(X~oUZ3TYoLvn8D-*5M78uUP22RmxpL@)_`yofn
zM4b0j&BNy#(iUL=7APXMa#08GPv3fBx!Fj-#a%&;v)I
z`#ro;LQr*G6pbeU!n+=$P@;aeX^7hzynJ0`^8CwSEZZ5$eqMzh!lxUzsu1-R16iYncj<3;1srSlas0j+6)7I(C8okTXU5WAs)eR8tc|%XDnLNuy
z$7WhdGkGCb2NTkf>C026wIkTjl33n4%yX*)q+3kWx``8zI|-e`xbUB5g-GEel4$OPQss
zcI_QoR29r4X3GTAEt4fDkHhPNloixXZ}iU&3{X9?H8!oUD#F-EK9Jgnx2HdR0wo1f2I*=71z5%3ppPqQA{w^bDsub$q+i$uXP;)c^#Jla;m>Vn2fxn4=mn0(=wzi#b;pH?uZ?>F(N<#
zY5DD%gVf~W|Fnc3}$25ohoNsTTja=O<^$<-A|e&aty
z3+GP{mIe6mv9hwVElyd-!@G*S!dm&dGGhf1lhpHhyG3ukBJEKqOA(T|NnDev3;P9S
z?v{~4j2cf87Zo3--Dk7TfF>{CT?IKSO0LQIvb~Ouj$gi@i*$8ILa=suJ)8!KO`FRr
zD~n-e{xoQi_B*4jQ8f?%0!1Taq9rsBWw6;vy9`1tzl=TX=?NKmp87*J!v99TMK_gN
z3b2NYE}IvW6xv50Fdmi=cQ9!ucJ^y*z1y+<8v?!11a1BX@5F<4enXnuC9ix#(PyNi
zH=c@PFZi!S;Q!A2K8a%e8}TX%|
zAom9KrT^H7ztcUpVVOvzuw#ZQ2Age9opjv` by fleet-host |
+| The device | Roku Express 4K+ at `192.168.50.218`, Roku OS 15.3.4, developer mode on. Registered as `roku-` by fleet-host. An executor older than 0.9 registers it as `platform: android` and leaves an offline `roku-ip-192-168-50-218` beside it; from 0.9 it says `roku`, and the serial-named row retires the IP-named one |
| The checkout | `~/tvloop` on fleet-host, **synced from the dev MacBook**, not cloned: the repo is private and fleet-host has no GitHub credential. Re-sync with `rsync -a --delete --exclude node_modules --exclude 'packages/*/dist' --exclude tvloop.toml / fleet-host:tvloop/`, then `corepack pnpm install --frozen-lockfile && corepack pnpm build` there |
| `~/tvloop/tvloop.toml` | Not in the repo. Points `[project] source` at `examples/sample-channel`, the channel the flows expect. Keep it out of the rsync |
-| The password | Keychain item `fleet-roku-dev` / account `rokudev` on fleet-host, the same one the dev installer reads. Missing means every run is `skipped:` and says how to add it |
+| The password | Keychain item `fleet-roku-dev` / account `rokudev` on fleet-host, the same one the dev installer reads. Missing means every run is `skipped:` and says how to add it. On a 0.7 or 0.8 brain those runs show as `done`; from 0.9 they are `skipped`, and three in a row raise a `schedule-skipping` alert |
| The steps | `doctor`, `install` (sideloads the sample channel, **replacing whatever is in the dev slot**), `hardware`, `replay`. A failed doctor or install stops the run |
## Real phones from CI
@@ -148,7 +148,8 @@ Three things have to be true for a status to appear:
`fleet mcp` serves the lab as MCP tools; register it once with
`claude mcp add --scope user fleet -- ~/.fleet/bin/fleet mcp`. From the dev
MacBook it reaches the brain through the tunnel, the same as the agent does.
-A result whose error starts with `skipped:` means nothing ran.
+A result whose error starts with `skipped:` means nothing ran. The 0.7.2 brain
+closes those jobs as `done`; from 0.9 their status is `skipped`.
From 0.8.0 there is also `fleetr`, whose exit code is the verdict, and a skill
(`fleet skill install`) that tells an agent which commands to run in what
@@ -194,7 +195,7 @@ Five enabled. All times local to fleet-host.
| `nightly-self-check` | 02:15 | fanout, pool `machines` | Disk, clock drift, a tool that vanished, or the agent is not supervised |
| `nightly-fleet-ui-smoke` | 02:30 | `fleet-host`, an Android device | **Red since 2026-08-21.** From 09-16 to early October it was `no targets attached`. The Galaxy S8+ is plugged in again and the suite now runs on it, and fails, every night; why is not diagnosed yet |
| `nightly-aliquant-shots` | 04:15 | `fleet-host`, `chromium` and `mobile-chrome` | Green. The baseline was re-accepted on 2026-09-22 from fleet-host's own captures, after Aliquant's sign-in page gained a "Forgot password?" link; the old one had diverged 2.7% on desktop and 6.1% on mobile since 09-10. The `webkit` and `mobile-safari` baselines are still the August ones, because nothing runs those projects now |
-| `nightly-tvloop-hardware` | 05:00 | `fleet-host`, the Roku, exclusive | tvloop's doctor, install, contract tests and flow replay. `skipped:` until the Roku password is in fleet-host's Keychain. Red from doctor means the Roku is asleep, off the network, or out of developer mode |
+| `nightly-tvloop-hardware` | 05:00 | `fleet-host`, the Roku, exclusive | tvloop's doctor, install, contract tests and flow replay. `skipped:` until the Roku password is in fleet-host's Keychain (status `done` on the 0.7.2 brain, `skipped` from 0.9). Red from doctor means the Roku is asleep, off the network, or out of developer mode |
**`nightly-aliquant-web` and `nightly-aliquant-audit` are retired** (disabled
2026-09-29). Aliquant's own CI already runs the same Playwright suite and
diff --git a/docs/protocol.md b/docs/protocol.md
index f96a45c..1b1046d 100644
--- a/docs/protocol.md
+++ b/docs/protocol.md
@@ -49,13 +49,13 @@ means "nothing", and they are different on purpose.
### `platform` and `kind`
**`platform`** is which OS family you run. An open string: `android`, `ios`,
-`tvos`, `watchos`, `visionos`, `macos`, `linux`, `windows`, `web` are the ones
-in use. Send it. An agent that omits it gets the old rule — iOS if `os` looks
+`tvos`, `watchos`, `visionos`, `macos`, `linux`, `windows`, `roku`, `web` are
+the ones in use. Send it. An agent that omits it gets the old rule — iOS if `os` looks
like iOS, Android otherwise — which is kept so that a collector upgrade does
not relabel agents written before the field existed. That rule is also why the
field had to exist: it does not report a platform, it reports a guess, and
every desktop agent registered as an Android phone until it could say
-otherwise.
+otherwise, and so did the host executor's Rokus until 0.9.
**`kind`** is the form factor: `phone`, `tablet`, `laptop`, `desktop`, `tv`,
`watch`, `headset`, `sbc`, `browser`, `ci`, `container`. Null when you do not
@@ -89,6 +89,12 @@ results you posted stay attributable and `GET /api/devices/:id` still resolves
you. What changes is that the queue stops offering you work and you leave the
shelf.
+One exception, decided by the collector rather than the agent: a row whose
+descriptor says `kind: "simulator"` or `soc: "simulator"` is **deleted** once
+it has been offline for `FLEET_SIMULATOR_TTL_DAYS` (default 7), because a
+simulator is recreated under a new UUID and never comes back. Its results and
+beacons are kept, as with the dashboard's Forget.
+
## 2. Claim work
```
@@ -243,6 +249,14 @@ The last row for a device carries `final`:
the verdict. `error` carries the reason on a failure, and it is what the
dashboard shows against the job — so make it a sentence a person can act on.
+**If you could not run the job at all**, say `"ok": true` with an `error` that
+begins `skipped:` and names why (`skipped: com.example not installed`). The job
+closes as `skipped` rather than `done`, nothing waiting on it runs, and no
+commit status is posted. A host row of plain `ok: true` over device rows that
+all said `skipped:` counts the same. See
+[Skipped is not done](concepts.md#skipped-is-not-done). Nothing about the row's
+shape changed; a runner that already said `skipped:` needs no update.
+
### Metric names are not free-form
`metrics` keys come from
diff --git a/docs/workloads/host.md b/docs/workloads/host.md
index b61db8b..499df54 100644
--- a/docs/workloads/host.md
+++ b/docs/workloads/host.md
@@ -270,6 +270,15 @@ directory (`FLEET_FLOWS_DIR` to change). `exclusive`
takes a device lock for the run, because two suites tapping the same phone is
not a test.
+**A failing Maestro run says which step broke.** The device row's `error` is
+the first failing flow and its assertion, read from the JUnit report
+(`smoke: Assertion is false: "Fleet Runner" is visible`), `test.failing_flow`
+names the flow, and the report itself is the row's artifact
+(`GET /api/v1/artifacts//content`). When Maestro produced no report at
+all, the error is its exit code and the last line it printed to stderr, never
+its command line, which carries the test account's password. Until 0.9 the
+reason was in the uploaded report and nowhere else.
+
**iOS UI tests need a Mac with full Xcode.** `simctl` and `devicectl` ship with
Xcode, not the Command Line Tools, so they run on a different executor from the
Android work. [The iOS executor](../deploy/ios-executor.md) covers standing one
From 3ab79c9d20e32c0a3ef0a4d5f35e5215b28c012a Mon Sep 17 00:00:00 2001
From: Taylor Addison
Date: Thu, 15 Oct 2026 21:34:54 -0400
Subject: [PATCH 2/2] describe.test: one evalMatch import after merging the
tvOS and Roku checks
Co-Authored-By: Claude Opus 5.5
---
collector/src/describe.test.ts | 1 -
1 file changed, 1 deletion(-)
diff --git a/collector/src/describe.test.ts b/collector/src/describe.test.ts
index 5d50442..9fcd36f 100644
--- a/collector/src/describe.test.ts
+++ b/collector/src/describe.test.ts
@@ -74,7 +74,6 @@ export async function runDescribeChecks(check: Check) {
JSON.stringify([r1.platform, r2.platform]));
const { devicePlatform } = await import("./api/shared.js");
check("and the collector believes it", devicePlatform(r1) === "roku", devicePlatform(r1));
- const { evalMatch } = await import("./match.js");
check("os ~ 'roku' still selects it", evalMatch("os ~ 'roku'", r1) && evalMatch("os ~ 'roku-15'", r1));
const { rokuPresenceHere } = await import("./drivers/roku.js");