# Changelog
OxygenHub runs at https://oxygenhub.io; each deploy's build is on the About page and at `/api/v1/version`.
## 0.9.16 (2026-10-29)
- **A release's asset at a fixed address (oxygen#157).** `/OWNER/NAME/releases/download/TAG/FILE` redirects to the asset, an address known before the release exists (as GitHub's), so a packager's recipe can name it.
- **Releases carry their source (oxygen#157).** Each release now has `oh-VERSION-src.tar.gz`: the oxygenhub tree with its subrepos inside, so oh builds from it alone, reproducible and listed in SHA256SUMS, for packagers (haikuports' oh recipe).
- **A phone's runner follows the app's releases (oxygenflow#84).** A runner on Android is part of the OxygenHub app, so the hub never offers it a runner build: with updates on, it is told it follows the app's releases (update the app to update it), and Admin › Runners › Updates says so.
## 0.9.15 (2026-10-28)
- **Run a pipeline's WASM jobs in your browser (oxygenflow#80, first part).** Flow › *run in this tab* (`/OWNER/NAME/flow/tab`) lists the jobs of a bookmark's pipeline whose sandbox is `runtime "wasm"` and runs them in this tab, as a runner's WASM tier does: a worker fetches the bookmark's files as a zip once, unpacks them in memory and runs each job's modules with a WASI preview 1 shim, the files at `/src` and as the current directory, the job's arguments and environment, its output shown as it comes, its exit code and time; Run all, Run and Stop per job. Nothing is claimed or reported: it shows the tier in a browser before a tab can take jobs as a runner.
- **Runner builds through the swarm (oxygenflow#84).** The first time the hub offers a runner build, it cuts the release asset into its image chunks, as an image named by the build's BLAKE3, so a runner in a swarm (OxygenFlow 0.5.2) fetches it from its site's peers and the hub together and the hub serves it about once per site; Admin › Runners › Images lists it as "runner build flow VERSION".
## 0.9.14 (2026-10-28)
- **Read a Flow job's report over the API and with `oh` (#348).** A report a job published could only be read on its web page. Now `GET /api/v1/repos/OWNER/NAME/flow/runs/N/jobs/JOB/report` gives it with its metrics, context and files, `…/report/PATH` one file as published (`data/frames.csv`), `…/jobs/JOB/reports` every report the job published, `GET …/reports` the newest run of each report (`?bookmark=`), and `GET …/reports/NAME` one run by `?id=` (the id the web page carries) or the newest on a bookmark, with `…/files/PATH`. Files come sandboxed, so a page in one runs nothing on the hub; errors name the jobs, reports and files there are. `oh run report RUN JOB` prints its `report.toml`, `--list` what it holds, `--file PATH [-o FILE]` one file; `oh report list` and `oh report show NAME` do the same by name.
- **A job's timings over the API (#348).** A run's jobs now say when each was queued and claimed, the phases its runner reported (image, prepare, run, finish, in milliseconds) and its peak disk and memory; `oh run view N --timings` prints them under each job ("waited 12.0 s · image 850 ms · run 2 min 5 s · peak disk 2.1 GiB, memory 900 MiB").
## 0.9.13 (2026-10-28)
- **An assistant drafts a new repository's starting files (#347).** Creating a repository can come with a description of what it is for: `POST /api/v1/repos/OWNER/NAME/starter {"description"}` asks the owner's assistant (the site's, or the organisation's) for a small working starting point (a README, the build files, a source layout and a first test), checks what comes back (relative paths, text, at most 40 files and 256 KiB), and commits it to the empty repository's default bookmark as the person who asked, signed by the hub as their delegate, the change naming the assistant and what was asked. It runs in the background, since a model may take longer than a proxied request may last; `GET …/starter` says how it went, and `GET /api/v1/user/starter?owner=X` which assistants may be asked. Never over files that are there. The Android app's New repository uses it.
- **Your Flow runs over the API (oxygenhub-android#26).** `GET /api/v1/user/flow/runs` gives the newest runs of the repositories you belong to, each with its repository and jobs (`?before=MS` pages back), for the Android app's Flow monitor.
## 0.9.12 (2026-10-28)
- **A repository's sky over the API (oxygenhub-android#23).** `GET /api/v1/repos/OWNER/NAME/sky` gives the backdrop a repository's pages draw (#196), its events of the last 30 days, in the shape of `/user/sky`, for the Android app's repository screen; read access, as the repository's pages.
- **Runner updates in waves (oxygenflow#84).** A test group can be given a wave (1 to 9) on Admin › Runners › Updates or an organisation's settings: a new version reaches wave 2's runners only once every runner of wave 1 that took it has finished a number of jobs on it (5 by default) with none ending in error, or a number of hours (24) has passed since its first job there; runners in no wave come last. A job that ends in `error` on a wave's runner (the runner's or the machine's fault; failed tests do not count) stops the version: the next runner to ask holds it for every runner, and the page names the job, until an administrator lifts the hold. Each job now keeps the runner build it was claimed by (migration 0102). A runner waiting says so: "flow 0.5.1 waits for wave 1 to prove it: 1 of 2 runners on it, 3 jobs". First slice: errors are not yet weighed against the version before, and a stopped wave's runners are not rolled back by the hub.
## 0.9.11 (2026-10-28)
- **Your sky over the API (oxygenhub-android#23).** `GET /api/v1/user/sky` gives what the dashboard's backdrop draws: the caller's repositories and their pushes, proposal and issue events, releases and Flow runs of the last 30 days, so the Android app draws the same sky from the same data.
- **Another repository's pass is taken only on its signed receipt (#296).** A pass from upstream or from any other repository on the hub now stands only if its runner's signed receipt was kept (the hub checks it against the runner's bound key when the job finishes) and, read again from that repository's store, still verifies and its key is not revoked; otherwise the job runs here. Only a pass that ran in the repository offering it is taken, so the receipt is where it is checked.
- **A pass on the same state is reused from any repository on the hub (#296).** After the repository's own runs and the line it was forked from, a push's job with no pass of its own on that state reuses one from any other repository holding the same state, public or with the same owner, newest first: a state is its changes, so whoever proved it, the proof holds. The run page and the check say whose pass it was.
## 0.9.10 (2026-10-27)
- **Approval gates and expedite for clients (oxygenhub-android#5).** A run's jobs over the API say `approval` (`waiting` while one waits for a person, `approved` or `rejected` after), and `POST /api/v1/repos/OWNER/NAME/flow/runs/N/expedite {"reason": …}` makes a run urgent as the run page's Expedite does (a maintainer, with a reason, within the owner's cap of urgent runs); the page and the API share it.
- **A proposal's diffs over the API (oxygenhub-android#4).** `GET /api/v1/repos/OWNER/NAME/proposals/N/diff` gives each file the proposal changes with its diff (three lines of context, in the shape a change's files have), as the proposal page shows them, for the Android app's proposal view.
- **Images that point at a file's page show (#346).** Markdown left an image's full or root-relative address alone, so `` (the address a file's page shows, as the doomsdayone/tetriblox wiki's screenshots use) pointed an `
` at an HTML page and showed broken. An image of one of this hub's file pages now renders as that file's raw bytes (`…/raw/REF/PATH`), as GitHub does for `blob/` images; links keep the page, and other sites' addresses are untouched.
- **Phone alerts (oxygenhub-android#16, oxygenhub-gcp decision 0008).** A phone registers its Cloud Messaging token and an X25519 public key with `PUT /api/v1/user/push`, using the token it signed in with (`DELETE` unregisters; revoking the token ends it too, migration 0101). Each notification an account is told then also goes to its phones, its title and link sealed to each phone's key (`flow_core::alert`), as a data-only, high-priority message, so Google carries only ciphertext. The hub sends as the service account its VM runs as, with the metadata server's token: no key. Off until `[push] fcm_project` (`OXYHUB_PUSH_FCM_PROJECT`) is set; a registration Cloud Messaging no longer knows is forgotten. Pins OxygenFlow main (sealed alerts).
- **A fork starts green where upstream proved the same state (#296, first slice).** Pass reuse (#265) looked in the repository's own runs only. A push's job with no pass of its own on that state now reuses one from the repositories it was forked from, nearest first (up to five up): the same state is the same changes, so upstream's receipt stands for it. Only from an upstream that is public or has the fork's owner. The run page says whose pass it was and links to it, and its check reads "passed in OWNER/NAME run #N on this state, reused".
- **A proposal's conflicts are resolved on the web (#337).** A proposal whose landing would conflict has RESOLVE ON THE WEB for those who may push to its source: the hub brings the target into the source bookmark, as `oxy merge TARGET` would there (signed as the user's delegate, or unsigned where no trust policy asks for more), and opens the resolver on the first conflicted file. The resolution is committed to the source, so the proposal follows it and lands. A proposal from a fork is resolved in the fork.
- **A default bookmark must exist, and bookmarks are made on the web (#345).** Settings refused nothing, so a default named before it existed (doomsdayone/tetriblox's `develop`) left the repository's pages saying `"develop" is not a change`. Settings now refuse a new default that is not a bookmark (an empty repository may name any, for its first push) and list those that are; a missing default leaves the overview showing main, or the first bookmark, with a note; an unknown name is "not a bookmark, tag or change of this repository". Bookmarks › New bookmark makes one from a bookmark, tag or change, as the user's delegate or, without a trust policy, unsigned as a push would be, so a proposal can then target it.
- **Iterations on project boards (#343).** A board can run an iteration: a name and its first and last days. The board shows it above the columns, day N of M and the cards done so far, with a burn-up of cards done per day. Closing it archives the cards in the Done column (the one that takes finished work, or else the last) under it; its page, linked from "past iterations", lists them, restorable, with its burn-up. One iteration is open at a time. Migration 0100.
- **`oh` on Haiku (#344).** `oh` builds and runs on Haiku R1/beta6 (x86_64); `oh-VERSION-haiku-x86_64.tar.gz` is on the releases page from 0.9.9, with its own `SHA256SUMS-haiku`, and the README says how to install it.
## 0.9.9 (2026-10-27)
- **`oh` works in PowerShell (#319).** PowerShell has a built-in alias `oh` (for `Out-Host`), so `oh --version` ran that and failed. The Windows zip now carries `ohub.exe`, the same program under a name nothing hides, and `install.ps1`, which installs both for the current user and puts them on `PATH`. Where PowerShell runs scripts it also removes the alias in the profile; under Windows' default policy, where a profile cannot run and would only print an error, it says to use `ohub` and never changes the policy. Tried on the Windows VM under both policies.
- **Admin › Runners › Images tells a peer-held image from the hub's own (#342).** An image only runners hold shows as "manifest" (sent by which runner) rather than "hub", and one no runner holds now is marked nowhere, as an image the hub lacks is.
- **A queued job's reason names CPUs too.** A runner with a free slot but not the room for a job was said to lack "the memory or disk"; earth-1's limit is often its CPUs (5, with a 4-vCPU job running), so it says "the memory, CPUs or disk".
- **The Content Security Policy is enforced (#227).** The pages carry no inline scripts or handlers, and a week of reports showed it would block only Cloudflare's Web Analytics beacon, which the policy now allows (`static.cloudflareinsights.com`, reports to `cloudflareinsights.com`). Violations are still reported and logged.
- **Where a job's time went (oxygenflow#35).** A runner's finish report carries its phases (image, prepare, run, finish); the run page shows them under each job, and `/metrics` has `oxyhub_flow_job_phase_seconds` by phase. Migration 0099 (`flow_jobs.phases`).
## 0.9.8 (2026-10-27)
- **Images only runners hold (#342, oxygenflow#94).** A runner that serves its peers sends the manifest of each image it holds that the hub lacks (`POST /flow/images/held`, `PUT /flow/images/{hash}/held`, runner tokens only); the hub keeps it as held by runners and offers the image's jobs to runners of the same swarm site, which fetch it from their peers (the announce says the hub is no seed for it). A pushed image replaces such a record. On 2026-10-25 a re-pinned image that was never pushed kept every Rust job on one runner for a day. Migration 0098 (`jet_images.source`, `holder`).
## 0.9.7 (2026-10-27)
- **A push to a landed proposal's bookmark says so (#329).** The changes reach no target until a new proposal takes them, and the push was silent: now it is answered `its proposal #N has landed in TARGET, and these changes are not on TARGET: open a new proposal with oh proposal create --from BOOKMARK` (oxygen's `Backend::push_notes`). The landed proposal's page lists them as changes since it landed, with a link to a new proposal; it no longer shows them as its own, and no longer offers to delete a bookmark that holds them.
- **Creating a repository no longer stalls the hub on a busy disk (#335).** A bare init makes about sixteen fsyncs, and on Earth's shared disk each waited behind every other write: one create took 26 s and held an async worker the whole time. Creates from the API, the web and drops now run on a blocking thread, as forks and bucket creates already did.
- **A Diffusion bump pins the upstream's default bookmark (#332).** A bump kept the bookmark name of the pin it replaced, so a subrepo first pinned at an upstream proposal's bookmark still named it after the proposal landed and Diffusion moved the pin to the default bookmark's state; a checkout then counted the subrepo as on another bookmark. A bump now names the upstream's default bookmark, unless it keeps pinned changes that bookmark lacks.
- **Listing landed proposals is quick, and pages (#331).** `oh proposal list --state landed` on a repository with a few hundred proposals timed out: the API worked out every proposal's projected state again, landed and closed ones too. Those now come from what was recorded, and the list takes `limit` (`oh proposal list --limit N`, 30 by default), newest first.
- **A big image's push no longer fails at the last step (#341).** The hub reads a whole image before it accepts the manifest, which on a busy disk outlasted `flow rootfs push`'s wait. The image could then be kept but never offered, and a retry read it all again. The hub now finishes and records the image even if the sender gives up, and a manifest it already keeps is recorded without reading the image again.
- **The API refuses a field an edit does not take (#330).** `PATCH` on an issue, proposal, project, milestone, release or drop answered 200 to a field it does not know and changed nothing (`{"ready": true}` on a draft proposal, say), which looked like success. It now answers 422 naming the field and the ones it takes.
- **Mail reads `[OxygenHub]`, in a wider message (#334).** A notification's subject was tagged with the site's name (`[Oxygenhub on Earth]`); mail now goes by the From address's display name (`OxygenHub `) in the subject and the footer, falling back to the site's name, so the site keeps its own name (which its apt repository is labelled with). The message card is 720px wide rather than 560.
- **An agent follows its owner's newest identity (oxygen#82).** A keyless certificate's answer now carries `author_chain`: the grant owner's newest linked identity and its predecessors back to the grant's key, so an agent home with `author.for` authors as the owner's current identity, not one several rotations old.
- **The profile page's actions are one tidy group (#328).** Edit profile, Projects and Drops sat on two rows, touching each other and the panel below; they are now one evenly spaced block, Edit profile across the top and the two pages side by side.
- **Copy buttons are a glyph joined to their field (#338).** Every copy button is now a clipboard glyph on the end of the box it copies, with the tooltip "Copy to clipboard"; pressed, it turns to a tick saying "Copied!" for two seconds. A drop's Embed and Clone boxes line up with each other.
- **Changelog entries no longer block a landing (oxygen#145).** Two proposals that each add a line at the same place in a file marked `merge=union` in `.oxyattributes` (`CHANGELOG.md merge=union`) used to leave the second unlandable where conflicts are refused. When a proposal's only conflicts are such adjacent insertions, landing it now adds the resolution, both sides kept, the earlier change's first, authored by whoever lands it and signed by the hub as their delegate. Any other conflict blocks as before.
## 0.9.6 (2026-10-26)
- **A highlighting test no longer fails a busy machine's gate (#327):** it timed a debug build against a release build's 500 ms; debug builds now have 3 s, still far below what highlighting the line would take.
- **`oh runner-updates` and its API (#322):** site administrators read every level's update settings, the test groups, each runner's policy and offer, and the held builds (`GET /api/v1/admin/runner-updates`); `oh runner-updates set group:canary --updates on --window 02-05` changes one level, keeping what is not given (`PUT …/settings`, 422 saying what is wrong); `oh runner-updates lift 0.4.0` offers a held build again (`POST …/holds/{version}/lift`).
- **A build that rolls back on one runner is held for the rest (#326):** when a runner reports it rolled back from flow X (it did not reach the hub after updating, or would not start), the hub offers X to no other runner, saying why, until an administrator lifts the hold on Admin › Runners › Updates, which lists each held build with the runner and the reason. One failed canary halts the rollout. Migration 0097.
- **Finished work moves to Done (#325):** a card whose issue closes (on its page, through the API, or by a change's `closes #N`), or whose proposal lands or closes, moves to its board's column for finished work. A new board's Done is that column, and so is each existing board's column named Done; a column's menu turns it on or off (one column of a board). Archived cards stay archived.
- **A WASM job's placement names no image (#323):** a job on the WASM tier was placed saying its runner "fetches the image from the hub"; it has none, so the line now starts with what the runner is doing (andymccall/windows-runner-demo run #1).
- **Runner updates: policy, test groups and the offer (#322):** Admin › Runners › Updates sets where runner builds come from (a repository whose releases carry `flow-linux-ARCH` files, and `flow-windows-x86_64.exe` for Windows runners) and who takes them. Updates are off unless chosen; the site, an owner, a test group and a runner may each set updates on or off, a channel (`stable`, `canary` or a version), a window of hours (UTC) and dry run, and the nearest setting wins, each setting on its own. A test group is chosen runners or a percentage of the site's (or an owner's) runners, by a stable hash of each runner's id, so a share is the same machines every rollout and 10% sits inside 50%. `GET /api/v1/flow/runner/update` tells a runner its policy, where each setting came from, and the build to install (its BLAKE3, to check the download against); `POST` takes what the runner did, shown on the page and in the audit log. An organisation's owners set the same for their own runners in the organisation's Settings › Runner updates: the organisation's setting, test groups of its runners (chosen, or a percentage), and each runner's own; nothing of the site's or another owner's. Pins OxygenFlow's main.
## 0.9.5 (2026-10-26)
- **No failed run every minute for a pipeline that does not parse (#321):** the scheduler asked each repository's pipeline every minute and, for one it could not read (a mistake, or no jobs yet), recorded a failed `schedule` run each time: oxygenhub-android had about 1,500. A scheduled tick now starts nothing for such a pipeline; its push's run still says what is wrong.
- **A change's diff through the API (#320):** `GET /api/v1/repos/{owner}/{name}/changes/{hash}` gives each file's diff lines too (`lines`: `kind` context, add, del or gap, `old` and `new` line numbers, `text`), with three lines of context, up to 2000 lines a file (`truncated` says when it stops short), so the Android app can show a diff itself.
- **JaCoCo coverage is read (#308):** Gradle's `jacocoTestReport` and Kover write JaCoCo XML, which a `coverage` artifact (or `POST /coverage`) may now be, beside LCOV and Cobertura: lines hit when an instruction ran, branches, methods. Its paths are relative to a source root (`io/oxygenhub/…/X.kt`), so a report's path not in the state is matched by its ending to the one file it can be.
## 0.9.4 (2026-10-25)
- **Checkpoints, so a handed-back job resumes (oxygenflow#77):** `POST /api/v1/flow/jobs/{id}/checkpoint` keeps the directories a job declares with `checkpoint "/src/…"` as its runner hands it back; the next attempt's claim names the blob (`checkpoint`) and its runner fetches it (`GET`) and restores it before running. A checkpoint is kept beside the job's artifacts but is none of them (no receipt names it). Pins OxygenFlow's main.
- **Boards keep the last fortnight of finished work (#293):** by default every column archives its finished cards (closed issues, closed or landed proposals) 14 days after they reached it, hourly, so a Done column stops growing however long a project runs; open work and notes stay. A column can instead archive every card after N days, or never ("Archive automatically" in its menu). Archived cards stay on the board's archive view, searchable and restorable. On the next hourly pass, boards with years of Done cards (Super Mario Land 2's 327) shrink to their last two weeks.
- **An issue's references: no line through the chips, each change once (#318):** an opened "referenced by N changes" box drew the proposal timeline's vertical rule through its first column (the events shared its class); and a change was recorded again for every bookmark it was pushed on, and for every mention in its message, so one change filled several chips. Now one reference per change per issue or proposal, and the page shows each change once, including those recorded before.
- **Claims say how long a job is expected to take (oxygenflow#76):** `expected_ms`, the median of the job's last five passing runs in the repository (reused passes left out), so a runner leaving with notice finishes a job that fits it and hands back the rest. Pins OxygenFlow's main.
- **The hub's CI writes a test report and coverage (#307):** `test` runs under cargo-nextest and keeps a JUnit report (whether or not the tests pass); a new `coverage` job runs the tests instrumented beside it, with its own cached target, and keeps LCOV coverage. `coverage` is not a required check.
- **The new proposal form lines up (#315):** as wide as the changes above it; Draft with Claude a secondary button under the editor (it fills the fields, it does not open the proposal); the draft checkbox and Open proposal on one row.
- **Draft with Claude proposes a title too (#316):** for a proposal not yet opened, the assistant begins its draft with a title in the style of the changes' own titles; it fills the title field unless you wrote one (the page's own suggestion does not count), and stays out of the description.
- **A waiting WASM job no longer says runners lack its image (#317):** a job with no image (the WASM tier) waited saying every runner "does not hold image" with an empty name; the explanation now skips the image check, as the claim always did.
- **A draft proposal can be made ready from `oh` and the API (#313):** `oh proposal ready N` (and `--undo` to turn it back into a draft); `PATCH …/proposals/{n}` takes `state: "open"` for a draft and `state: "draft"` for an open one, as the page's buttons do, and `draft: false|true` in the words a proposal is created with. `{"draft": false}` used to answer 200 and change nothing.
## 0.9.3 (2026-10-25)
- **Runners hand jobs back, and the queue takes them first (#312, oxygenflow#76):** `POST /api/v1/flow/jobs/{id}/release` {reason} lets a draining runner hand back a job it holds as it leaves (a phone off Wi-Fi, a restart, a spot reclaim). The job is queued again **ahead of new work**, whatever its priority; its log says who handed it back and why, and its next attempt's log follows on; it is not a decline. A job whose runner the hub has not heard from for 3 minutes (claims, 30-second heartbeats, logs) is handed back the same way, rather than running on until its time limit and ending as lost. A job handed back more than 5 times ends as an error saying so. Migration 0094 adds `flow_jobs.handed_back_ms` and `handbacks`.
## 0.9.2 (2026-10-25)
- **`GET /api/v1/user/proposals?landable=true` (#311):** also lists the open proposals the caller could land now (in repositories they can write to, with no blockers, as each proposal's `landable` says), for the Android app to say when there is something to land. Only when asked for, since it works out each one's projected state.
- **One logo, with its arms (#306):** the Oxygen mark's two arms were never drawn: their gradient was in bounding-box units, and a horizontal line's box has no height, so SVG paints nothing. The gradient is now in user space everywhere (header, footer, favicon, `mark.svg`; the VS Code extensions and the Android app too), and while a page loads the arms slide back and forth in turn beside the turning rings (still with reduced motion).
- **Admin › Runners: the Serves column lines up (#309):** the scope sits above its input, so the inputs line up whatever the scope's length.
- **A job's RE-RUN THIS JOB button no longer touches its log (#310).**
## 0.9.1 (2026-10-25)
- **Pipelines may use the WASM tier:** OxygenFlow's main pinned (oxygenflow#75), so `runtime "wasm"` sandboxes are accepted and their jobs placed on runners that offer the tier (every runner from flow 0.3.1-3; an Android phone from the app).
- **`POST /api/v1/user/runner-token` (repo:write):** a one-time registration token for a runner that serves the caller's own repositories and no one else's, as the Android app offers the phone as a runner (oxygenhub-android#8); it lasts a day, like the settings pages' tokens, and is audited.
## 0.9.0 (2026-10-25)
- **`GET /api/v1/user/proposals` (repo:read):** the proposals the web dashboard lists, for apps: `review` (open ones by others in repositories the caller belongs to, not yet reviewed by them) and `authored` (their own open and drafts, in repositories they can still see), each with its repository, state, conflicts and web address. The Android app's dashboard shows them (oxygenhub-android#18).
- **Board cards can be archived (#293, archive):** off the board but kept, on the board's archive view (`?archived=1`, with the same filter) and restored to the end of their column. A column archives its cards that reached it more than N days ago at once ("Archive cards here more than 14 days"), or on its own once set to (checked hourly, `projects.archiver`), so a Done column stops growing. An issue's or proposal's page says its card is archived. The API has `POST …/cards/archive` (#289's selection plus `older_days`, with `dry_run`), `GET …/cards?archived=true` and `POST …/cards/{id}/restore`; `oh project card archive N --from Done --older 14d`, `oh project card restore N CARD` and `oh project view N --archived`. Migration 0093 adds `project_cards.archived_ms` and `moved_ms` (when a card joined its column) and `project_columns.archive_after_days`.
- **Board cards show their labels and assignees, and the board filters by them (#293, filters):** an issue's or proposal's card shows its labels (each a link to the board filtered by it) and an issue's assignees; the filter takes `label:NAME`, `assignee:NAME` and `state:STATE` with words, as the issue list does, every term holding. The board reads what its cards show once per repository rather than once per card (a board of 300 cards made 300 queries). The API's cards and `oh project view` carry the labels and assignees too.
## 0.8.0 (2026-10-25)
- **Built on oxy 0.6.0 and OxygenFlow 0.3.1:** tag withdrawals, keyless grants that survive a key rotation, working depth-0 clones of big repositories (oxygen#134, #135, #137; oxygenhub#301).
- **The API for apps (#302-#305), found building the Android app:** `GET /api/v1/user/repos` lists the caller's own repositories, their organisations' and those they collaborate on (`?affiliation=owner,organization,collaborator`, paged with `?after=owner/name&limit=`), and `GET /api/v1/user/orgs` their organisations and role (#302); a call with no token now answers 401 (sign in) rather than 403 (#303); `DELETE /api/v1/device/code` withdraws a device sign-in the app gave up on, at once (#304); each notification says its `repository`, `reason` (participating, watching), `thread` and the kind's `label` (#305).
- **Boards that keep growing stay readable (#293, first part):** a column of more than 30 cards shows its newest 30 (cards join a column at its end), says how many older ones it leaves out and links to "show all"; a filter above the board finds cards by title or reference, and by state (open, closed, landed, draft), in every column, counting "3 of 312".
- **An issue's references come in a box per day (#292):** an issue many changes refer to listed a line per change and ran on down the page. Two or more on one day are now one box, "referenced by 12 changes on 21 Oct 2026", their changes as wrapping chips, collapsible, the newest day's open.
- **Move a board's column at once (#289):** `oh project card move N --from Doing -c Done` moves every card of a column, `--except 317,#320` leaves some, `--state closed,landed` takes only cards whose issue or proposal is done, and `--dry-run` lists what would move. One call (`POST …/projects/{owner}/{n}/cards/move`), all or none, one audit entry: tidying a board of 115 cards took 115 commands.
- **Expediting a run moves the queue at once (#291):** a run's jobs kept the place they had ("9 of 9 jobs come before") until the next placement pass and some other change on the page. Expediting now places every queued job again straight away, every run whose jobs' notes changed updates live, and a job an expedited run moved down says so ("2 ready jobs come first (after expedited run andy/demo #12)").
- **Create and withdraw tags on the website (#295, #301):** a writer creates a tag on the Tags page (or `oh tag create`, `POST …/tags`), signed by the hub as their delegate, under the policy, and its tag jobs run; a maintainer withdraws one there (or `oh tag delete`, `DELETE …/tags/{tag}`, or `oxy push --delete tags/NAME`) with a signed withdrawal, never while a release uses it. The withdrawal is logged, audited and listed on the page with why, and clones drop the tag on their next fetch. Tags never move, and a withdrawn name is not used again. Pins oxygen with tag withdrawal and with keyless grants that survive their owner's key rotation (oxygen#135).
- **An agent's changes verify in a new repository (oxygen#134):** a keyless agent's change in a repository its owner had never pushed to read INVALID ("grant is by an unknown author"). The hub now sends the grant's signer with each certificate, and with oxy's fix the agent keeps it and pushes it beside the certificate, so the repository can check the grant.
- **A mistyped address shows the not-found page (#294):** an address the hub could not read, such as a project or issue number that is not a number, answered with a raw parse error ("Invalid URL: Cannot parse value …"); it now shows the site's not-found page, signed-in header and all.
- **Embed a drop (#280):** a drop's page offers an `