OXYGENHUB
◆ main oxygenhub / CHANGELOG.md
107.7 KiB · Markdown · last changed Release 0.9.24 22 hours ago renderedsource blame history raw

Changelog

OxygenHub runs at https://oxygenhub.io; each deploy's build is on the About page and at /api/v1/version.

0.9.24 (2026-11-04)

  • A fresh New repository page (#360): owner and name side by side with a live check that the name is free and valid and the address it will have; visibility as three described choices; Start with a README, a licence (MIT, BSD 2-Clause or ISC, with what each allows) and an .oxyignore for a language, committed as you with the hub signing as your delegate (it says what to do first when you have not delegated, before anything is made); and a project board with it: a new one (Backlog, Doing, Done, on by default for an organisation) or one of the owner's, which takes the repository's issues and proposals as they open. Storage moves under its own heading. The API's create takes the same (project: {"new": TITLE} | {"number": N}, readme, licence, ignore), and oh repo create OWNER/NAME [--project new:TITLE|N] [--readme] [--licence MIT] [--ignore Rust].
  • oxygenhub.io links open the Android app (#370): /.well-known/assetlinks.json vouches for the app (its package and release key's fingerprint, [push] android_apps to change), so Android opens the hub's links in it once the app claims them (oxygenhub-android#36).
  • The changelog is checked on the WASM tier: a changelog job in the pipeline runs OxygenFlow's checks module (no image, no KVM; a phone or plex takes it) and fails when the changelog's sections are out of shape or Cargo.toml's version has none.
  • Board views (#361): a board shows what matters now and other views of the same cards.
    • Saved views as tabs across the top: a layout with its filter, lanes and order, saved under a name and shared by its address (?view=NAME).
    • One filter bar for every view: words, repo:, assignee:, label:, milestone: (quote a title with spaces), state:, is:open|closed, age:>14d and age:<7d.
    • Board: swimlanes by repository, assignee, label or milestone; a work-in-progress limit per column, whose header turns to the warning colour past it; Done shows this iteration's work while one is open, saying how many cards finished before it; and compact cards (labels as dots).
    • Table: every card as a row (title, reference, state, column, assignees, labels, milestone, opened), sortable, grouped, a column changed in place, 50 rows a page.
    • Keys: j/k between cards, Enter opens one, m moves it, / filters.
    • History: every card's moves (added, moved, archived, restored, removed) are kept from now on, for the charts and forecasts of #357.
    • The API answers GET /projects/{owner}/{n}?q= or ?view=NAME with the filter applied, and lists the board's views and each column's limit; oh project view N --filter TEXT or --view NAME.
  • A hung test no longer holds a runner (#365): the PostgreSQL tests run under nextest like the rest, and CI stops any test still running after ten minutes; the history scan's tests have four worker threads and a two-minute bound of their own. Two of them once sat for an hour on plex-1.
  • Charts and forecasts from the board's own history (#357): a Charts tab on every board, with no estimates to keep.
    • Forecasts: a Monte Carlo of the board's weekly throughput (the last 12 weeks, or its life if shorter) says when the open cards will be done, at 50% and 85%: for the open iteration or the whole board, for each milestone, and for any filter. It says how many weeks it rests on, and says plainly when there is nothing to go on. Seeded by the board and the day, so it does not change between page loads.
    • Cycle time (leaving the first column to reaching Done: median and 85th percentile), throughput (cards done each week), the open iteration's burn-down with its scope, and cumulative flow (cards per column per day over 60 days, a widening band showing where work waits).
    • Timeline (#361): iterations as spans and milestones with their due dates and forecast bands on one time axis, today marked.
    • Drawn server-side as SVG in the hub's chart style, readable in both themes, each with its numbers in a table. GET /projects/{owner}/{n}/charts (?q=) gives the series and forecasts; oh project charts N [-q FILTER].

0.9.23 (2026-11-03)

  • A push no longer stops main's running run (#369): a push cancels its bookmark's older push runs, and those already running too unless Settings › OxygenFlow keeps them; on the default bookmark, where releases are cut from the run of their commit, a running push run now always finishes, and only queued ones are cancelled. A release commit's run lost to the next push is what prompted it.
  • A board's columns from the API and oh (#366): POST /projects/{owner}/{n}/columns adds one at the right, PATCH …/columns/{column} (by id or name) renames it or moves it to a position, and DELETE removes it, its cards going to the first remaining column; each answers the board. oh project column add|rename|move|remove BOARD … (move --to N, counting from 1 at the left, --left, --right), so a sprint column is renamed without the web page.
  • oh run watch and oh run view take the newest run (#367): with no number they follow or show the newest run, or the newest of --bookmark NAME, so the run a push just started needs no lookup.
  • oh issue close and reopen say why (#368): -b, -F and --attach post a comment first, in the same command.

0.9.22 (2026-11-02)

  • Pictures in issues and comments fit their box (#364): an attached screenshot was shown at its own size, running out of the card on a phone and dwarfing the text on a desktop; pictures in all rendered Markdown now scale down to the width they have.

0.9.21 (2026-11-01)

  • Added: your phone as the second factor, and the sudo step (#356). With two-factor sign-in on, the OxygenHub app can add the phone as a second factor (its Keystore key, opened by your fingerprint, face or PIN; adding it needs a current app code). Signing in then shows a two-digit number and waits: the phone offers three, you pick the one shown and confirm, and the browser carries on, with "Use a code instead" always there. A wrong number is a no. Before sensitive changes (password, two-factor settings, keys, tokens, secrets, webhooks, collaborators, deleting or transferring a repository, anything under Administration) a session that has not proved a second factor in 15 minutes is asked again at /sudo: the phone, a code, or the password for an account without two-factor. Settings › Security lists the phones; turning app codes off removes them. API: /api/v1/user/authenticators, /api/v1/user/approvals; migration 0104.

0.9.20 (2026-11-01)

  • Added: GET /api/v1/user/issues (#359). The open issues assigned to you, in repositories you can see, newest-updated first, as the dashboard's "Assigned to you" lists them, each with its repository and address; for apps, first the Android widget's issue count (oxygenhub-android#31).

0.9.19 (2026-11-01)

  • Added: attachments on issues and comments (#350). Screenshots and any other file: POST /api/v1/repos/OWNER/NAME/attachments?name=FILE with the file's bytes answers with its address and the Markdown to use, and oh issue create and oh issue comment take --attach FILE (repeatable), so a screenshot pasted into Claude Code can go onto the issue it files. An image (by its bytes: PNG, JPEG, GIF, WebP) shows in place; anything else, SVG included, is a download. Attachments are served under the repository's read rules and can run nothing (no sniffing, a policy that allows nothing, never in a shared cache). Pasting and dragging into the web editors come later.

0.9.18 (2026-10-31)

  • Added: oh mcp, OxygenHub for AI agents (#353). An MCP server on stdio for the repository oh resolves: tools to read issues, proposals with their files and checks, OxygenFlow runs, job logs, reports, test results, releases, drops and the inbox; with --allow-write, to open, comment on and close issues, comment on proposals and re-run Flow runs. What people wrote on the hub comes back under untrusted, so an agent reads it as data, never as instructions. Built on oxy-core's MCP module (oxygen#159). See docs/oh.md.
  • Fixed: agents show on the website (#352). A change a keyless agent sealed (Kimi, Antigravity, Claude: author.signer = keyless seals at push) showed as unsigned on change pages, history, proposals and the API, because the hub read only a change's own signature. It now reads seals too: the change says sealed, with "via agent:NAME@you" and "keyless, under your grant", and the Contributors panel names the agent under you. Help › AI identities now sets the agent's author (your name, email and the key you make its grant with, filled in when you are signed in) and copies your public identity into its home; without them an agent's changes were unattributed.
  • Kimi has its own avatar (#351). Changes signed via an agent named Kimi (agent:kimi@…, or Moonshot) now show a Kimi mark, a white K with a blue dot on a dark disc, instead of an identicon, as Claude, Gemini and the others have theirs.

0.9.17 (2026-10-29)

  • Drops over the API: fork, star and comment (#349). What a drop's page does, for the VS Code extension and other clients: POST /api/v1/drops/{id}/fork, PUT/DELETE /api/v1/drops/{id}/star, GET/POST /api/v1/drops/{id}/comments and DELETE /api/v1/drops/{id}/comments/{n}, by the web's rules (the owner told of a comment; its author, the owner or an administrator deletes it). A drop over the API now says its stars, whether you starred it, what it was forked from and its embed address. oh drop fork|star|unstar|comments|comment. The API's TypeScript types now include the dashboard's proposals, the starter and test reports.

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 ![…](https://oxygenhub.io/OWNER/NAME/file/main/x.png) (the address a file's page shows, as the doomsdayone/tetriblox wiki's screenshots use) pointed an <img> 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 <noreply@…>) 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 <iframe> snippet; /{owner}/drops/{id}/embed shows its files with a link back, framed by any site, with no scripts and no forms.
  • Comments on drops (#280): a drop's page has comments: signed-in readers write them in Markdown, the drop's owner is told, and the author, the owner or a site administrator deletes one.
  • Star a drop (#280): a signed-in visitor stars a drop, or takes the star back, from its page, which shows how many it has.

0.7.0 (2026-10-21)

  • Fork a drop (#280): a signed-in visitor's FORK makes a drop of their own from someone else's, with its files, history, title and secrecy; the fork's page says which drop it came from.
  • Flow's job times in /metrics (oxygenflow#35, #212): oxyhub_flow_job_queue_seconds (ready to started) and oxyhub_flow_job_run_seconds histograms by runner, finished jobs by runner and result, reused passes, and the work queue's waiting and dead items, all over the last day, for the Grafana dashboard.
  • A durable work queue (#276): work that fails where it starts is queued and retried instead of lost: a notification's email the mail server refused, and a Git mirror export after a push. Any worker process takes due items (on PostgreSQL several at once, each skipping what another holds), retries back off from 30 seconds to an hour, and an item still failing after eight attempts is kept as dead.
  • One auto-land and one Diffusion bump at a time across hub processes (#275): auto-land's check of a proposal, and Diffusion's bump and landing of a consumer's pin, were serialised only within a process, so two web processes could land one proposal twice. Each now holds a lock named for the proposal or the pin across every process sharing the database (a PostgreSQL advisory lock; on SQLite, one process, as before).
  • Several web processes can share a data directory (#274): the transparency log took appends from one process only, so only one process per data directory could have the web role. Each append now holds a lock in the log's directory and starts from the log as the files hold it, readers see every process's entries, and every process serves the newest checkpoint, whichever signed it.
  • A push reuses a job's pass on the same state (#265): when main reaches a state a proposal's run already tested (landing it), a job that passed there with the same spec and sandbox is not run again: it shows passed, reused from that run, with its receipt, and its check says so. Jobs with an approval gate, an environment, secrets or identity tokens, or whose artifacts another job takes, always run. Maintainers can turn reuse off under Settings › Runs.
  • Drops are backed up (#280): an owner's backups now include its drops, each copied like a repository and restored as a drop (secret or public, with its title and description); Admin › Backups counts them with the repositories they cover.
  • A skip marker quoted in code does not skip: a change message that mentions [skip ci] between backticks or in a fenced block (documenting the marker, say) now starts its run as usual; only a marker in the text itself skips it.
  • Administration has a sidebar, one page per section (#288): the admin area was one long page plus separate pages for runners, images, Diffusion, backups and storage. Every page now has the same sidebar, grouped as Overview, People (accounts, registration and invitations, organisations), Sign-in (providers, security contact), Mail, Flow (runners, images, runner settings, insights, variables, secrets), Integrations (AI assistants, Diffusion), Data (storage and maintenance, backups) and Audit, each at its own address (/admin/mail, /admin/audit, …). /admin is an overview of the site's numbers. Forms keep their addresses and answer with their own page, and a kept /admin#mail link opens the Mail page.
  • Help: AI identities (#287): a section on letting an AI agent work for you: keyless agents (a grant with oh agent grant, an agent home with oxy auth grant, ten-minute certified keys, nothing secret on disk), how its work shows (via agent:NAME@YOU), admitting it to a repository's trust policy (oxy policy grant-subject), agents with keys of their own (delegation), agent-marked tokens, approval gates, and revoking. "Letting the hub act for you" now says to export your identity again after delegating and upload that.
  • Settings › Keys says when each key was last used (#286): like tokens, each linked key shows when it was linked, when the hub last accepted a push it signed (a change or a seal; "never" until then) and when the identity expires.
  • Settings › Security is balanced (#285): every box runs the page's width: two-factor shows whether it is on in its head, with its field and button on one row; connected accounts are rows with Connect or Disconnect; signed-in browsers are rows with Sign out, and Sign out everywhere else sits in that box's head with the session limits under it.
  • Push without starting a run (#284): [skip flow], [skip ci] or [ci skip] in the newest pushed change's message starts no jobs; the run list shows a skipped run naming the change and who pushed it, and the next run still tests what it brought. Tags always run. Maintainers choose where it is allowed under Settings › Runs: any bookmark (the default), any but the default bookmark, or never.
  • A drop's page is centred and its header clearer (#283): the page ran the full width with everything against the left edge; it is now centred like other pages, with the title, description and details on the left, EDIT and DELETE together on the right (DELETE only for whoever may delete it), the clone line with a copy button under them, and a Markdown file filling its box.
  • A started job no longer says it is waiting (#282): a run page opened while a job was queued kept "Waiting for a runner and for build." under it after it started, as the live update changed its status and streamed its log; the note now goes once the job leaves the queue.
  • Linking the identity that delegates to the hub (#281): the notice on pages that commit for you (New drop, web edits) read as if the hub's key file was what to link under Settings › Keys, which then asked for a signature nobody but the hub can make. It now says to export your own identity again after oxy identity delegate add and upload that; uploading the hub's own key is refused saying so, uploading an already linked key says to export again after delegating, and a challenge signed by another key than the one uploaded names both keys. The New drop form runs the page's width, and CREATE DROP and CANCEL are the same height in one row.

0.6.1 (2026-10-20)

  • Tidier forms and headers: the New drop page is laid out like every other page (it ran the full width when its notice did), a form's Cancel lines up with its buttons (drops and web edits), and the Drops page's + NEW DROP sits beside its title instead of against the text above.
  • Released oxy can clone repositories with an agent's release tag again (oxygen#131): since keyless agents could tag releases with ten-minute certified keys, oxy 0.5.0 and older refused such a tag when cloning or fetching a repository with a trust policy (key … may not create tags/v0.5.0 under this repository's policy), and with it the whole clone, so andymccall/oxygen could not be cloned by any released client. The hub now leaves those tags out of its answer to a client that does not declare certified-tags in the new Oxy-Features request header (oxygen#131), which sees the repository as if the tag did not exist; current clients get it as before. Needs the oxygen pin with oxygen#131.
  • Drops: snippets of files, each its own small repository (#279): a drop is one or more files, as a gist is, kept as an Oxygen repository of its own at /{owner}/drops/{id} (a 24-hex-digit id), so every edit is a signed change with history and diffs, and oxy clone and oxy push take its address. Make one at /drops/new (title, description, files, public or secret), with POST /api/v1/drops, or with oh drop create FILE… [--secret] [-d TEXT] [-t TITLE]; the hub commits it as your delegate, as web edits are, so you delegate to the hub's key first. Your Drops page (/drops, under your profile and in the site band) lists and manages them; someone else's shows their public drops only. A secret drop is listed nowhere but readable by anyone with its address. Drops never appear among repositories (profiles, the dashboard, Explore, search, GET /api/v1/repos); Admin › Storage labels them. Only the owner edits a drop; the owner or a site administrator deletes it. Push protection covers drop pushes and what the web and the API commit. API: GET/POST /drops, GET /users/{name}/drops, GET/PATCH/DELETE /drops/{id}; oh drop list|view|delete. drops is now a reserved name, for owners and for repositories. Not yet: forks, stars, comments, embedding, and drops in backups.
  • Repository settings are one page per setting (#278): a repository's Settings was one long page; it is now a sidebar of grouped pages (General, Code, Flow, Integrations, Data, Danger zone) with the page shown beside it, the current one marked, each at its own address (/{owner}/{name}/settings/push-protection, /settings/runs, …; /settings is General). Pages a role cannot use are not listed and are not found. Every form keeps its address and now returns to its own page with what happened, so reloading does not send it again. Links elsewhere point at the new pages (the push protection refusal names /settings/push-protection), and a kept /settings#section link opens the page that has it. On a narrow screen the sidebar becomes a compact list above the page. An archived repository's settings open for its admins again, so they can bring it back.
  • Proposal lists and the proposals API are much faster on large files: listing a repository's proposals rendered every open proposal's full diff and looked up each new line's history separately, recomputing a whole file's lines for every line; lists, the API, auto-land and the checks after a push now skip the rendering, and the proposal page computes each file's lines once.
  • Keyless agents can push release tags (andymccall/oxygen#129): A tag signed by a key the hub certified (an agent's or a Flow job's ten-minute key) was refused "unknown key", and then refused by the policy. With oxy's fix the hub accepts it under the policy's subject grant and countersigns when it accepted it, as it does a bookmark move. It logs the tag's acceptance in the transparency log beside the tag, and serves both with the tag in every refs answer, so a client fetching the tag months later checks the certificate at the hub's time. Runners read repositories with their own oxy, so they need rebuilding to check such tags; older ones ignore the new fields.
  • Secret scanning finds what is already in history (#277): push protection's rules now also run over each repository's existing history in the background (every change reachable from its bookmarks: the lines it inserts and the text files it replaces), so a credential committed before push protection existed, or while it was off, is found too. Every repository's whole history is scanned once after this deploy, then only what each push adds, a budget at a time ([limits] history_scan_limit, 64 MiB, and history_scan_time_ms, 5 s, per repository per pass; 0 turns it off), at low priority, resuming where it stopped. Maintainers see the findings, with the change, bookmark, file and line, under Settings › Push protection › Found in history, and either allow one (not a secret) or mark it resolved with how it was revoked (audited as secret.resolved); "Scan history now" scans the whole history again. An allowed fingerprint can now be removed (audited as secret.bypass_removed), so pushes adding that value are refused again. Repositories with push protection off are not scanned.

0.6.0 (2026-10-20)

  • An event bus between the hub's processes (#273): on PostgreSQL, what one process publishes (a push, a check, a run's jobs, a comment, a waiting webhook delivery, a choice in the conflict resolver) reaches every other process over LISTEN/NOTIFY, so live pages on any web process see a push made through another, and a separate worker delivers a push's webhooks at once instead of on its once-a-minute pass. Nothing to configure; on SQLite the bus stays in process. Events say "look now" and are delivered at most once; a process that loses its listening connection listens again on a new one and logs that events may have been missed. A live page that reconnects with an event id its process never gave (another web process's, or one from before a restart) is now told to reload instead of waiting for newer ids. In the conflict resolver, choices and landings cross processes; who is present stays per process. See docs/roles.md.
  • Run the hub as web and worker processes (#272): oxygenhub serve --role web,worker (or [server] roles, OXYHUB_SERVER_ROLES; both by default, so one process works as before). A web process serves pages, the API, the wire protocol and the runner API; a worker runs the background tasks and serves only /health and /metrics. On PostgreSQL each singleton task (Flow's scheduler, reaper and placement pass, webhooks, nightly upkeep, backups, Diffusion's index, the log's checkpointer) runs in one process at a time under an advisory lock, and another takes it over within seconds when that process stops or loses its connection. /health reports the process's roles and the locks it is leading. Until the transparency log has a cluster-wide writer (#274), run one web process per data directory. See docs/roles.md.
  • Repositories kept in a bucket need far fewer requests (oxygen#116): the hub reads a repository's signatures and index entries from a journal and entry lists instead of a LIST per lookup, and stops asking for the old one-list keys once its nightly gc has folded them. Another writer's new entry is seen within 30 seconds. Every writer of a bucket repository should run oxy 0.5.0 or later: until it does, entries it writes after the first fold are seen only after the next nightly gc.
  • Push protection (#268): the hub refuses a push that adds what looks like a credential (cloud keys, GitHub, GitLab and OxygenHub tokens, Slack and Stripe keys, private keys; rules v1), naming each by file, line and a fingerprint, never the secret itself, and saying how to take it out (oxy amend) and to revoke it if real. Nothing of a refused push is stored. A maintainer can allow one fingerprint with a reason (Settings › Push protection, audited); the setting is on for every repository and can be turned off there. Only what a push adds is scanned, within [limits] scan_limit (16 MiB), scan_file_limit (1 MiB) and scan_time_ms (2 s); a push past them is accepted and its push log says how much went unscanned. Existing history is not scanned yet.
  • Land when checks pass, or land now over them (#261): anyone who may land a proposal can set it to land by itself, on the proposal, through POST/DELETE …/proposals/{n}/auto-land or with oh proposal land N --when-ready (--off to clear it). Once every required check has passed and nothing else blocks it, it lands as the person who set it, if they may still land. A required check that fails or is cancelled turns it off with a note on the timeline; a push to the source makes it wait again. When required checks are the only thing in the way (pending, or failed with the runners down), a maintainer may land now with a reason: on the proposal, with {"over_checks": "reason"} on the API's land, or oh proposal land N --over-checks REASON. Only the checks are passed over; approvals, code owners, requested changes, drafts, dependencies and the trust policy still apply. Both are audited, and the proposal, its timeline and the pushes page say how it landed.
  • The repository bar has an Artifacts menu and a More menu (#254): the bar is now Code, Changes, Proposals, Issues, Flow, Releases, Wiki, Artifacts, More and Settings, and no longer wraps. Artifacts holds Releases, Build artifacts (every run's outputs in one list, new), Images (the runner images the repository's jobs used, new), Terraform state and Reports; More holds Bookmarks, Tags, Diffusion, Trust, Pushes and Conflicts. Projects leaves the bar (it is on the site band and every profile). The menus work without JavaScript.
  • The hub keeps Terraform and OpenTofu state (#252, #255): an HTTP backend per repository at /api/v1/repos/OWNER/NAME/terraform/state/STATE, with locking. A job uses it with an identity token for {base}/terraform or its job token, for its own repository only; people use a token of theirs and their role. State is sealed at rest and its last 20 versions are kept; backups carry it. The Terraform state page (under Artifacts) lists each state's versions; writers download the current state or any version as a .tfstate file, and maintainers restore one. A restore writes the version again with a serial above the current one, since OpenTofu and Terraform only take a state forward, and keeps the state it replaces as a version, so nothing is lost. Refused while the state is locked, never by a job, and audited.
  • Insights (#257): OxygenFlow's charts for the site (/admin/insights, filtered by repository, owner or runner), an owner (/insights/{owner}) and a repository (/{owner}/{name}/insights): pickup time (median and p95), jobs finished and pass rate, duration, queued and running, runners online and slots busy, a runners table and the busiest repositories, over 1 hour, 24 hours or 7 days. The page refreshes itself every 30 seconds, and ?wall=1 is a view without menus for a second monitor. [server] grafana_url (OXYHUB_SERVER_GRAFANA_URL) adds an Open in Grafana link. log, metrics and insights are now reserved names.
  • Prometheus metrics (#212): /metrics serves Prometheus text to a bearer of [server] metrics_token (OXYHUB_SERVER_METRICS_TOKEN); without one it is 404. It reports the build, uptime, memory, the state cache, bucket requests by kind, HTTP requests by route class with latency histograms, protocol bytes by endpoint, the database pool, pushes accepted and refused, Flow jobs and runs, runners' last heartbeats, backups, mail sent and failed, and the transparency log's size.
  • A transparency log (#238): the hub logs every signed object it accepts (reference and tag attestations, seals, the policy, its acceptances, short-lived keys' certificates, kept receipts and sealed approvals) in one append-only Merkle tree, and signs a checkpoint every 5 seconds while entries arrive. The log is served as C2SP tlog-tiles under /log/, with a /log page. Every refs answer carries the latest checkpoint and each bookmark's entry with its inclusion proof, so oxy checks them on fetch. Hubs can witness each other's logs: [tlog] witnesses and witness_for in hub.toml (none by default, so nothing changes until set).
  • Trusted time (#240): the hub keeps each cosigned checkpoint, lists its witnesses at GET /log/witnesses and in every refs answer, and serves for each logged bookmark the first cosigned checkpoints that hold its entry, from which oxy takes the integrated time: when a witness first saw it, by the witness's clock.
  • Agents act for their owner with no key (#241, #253): an owner makes a grant with oh agent grant NAME [--scope] [--for 30d], signed with their own oxy key; the hub keeps it and answers with the agent's token, shown once. The agent exchanges it at POST /api/v1/keyless/certify for ten-minute certificates, so its fresh keys' commits count in the owner's name, and the same token works for fetch, push and the API as an agent's token of the owner (writing when the grant allows). What the agent does is marked as its own and its owner is told. oh agent list and oh agent revoke manage grants, and a revoked grant gets no more certificates. When a push moves a bookmark with a short-lived key, the hub countersigns it with its own clock. Runners' receipts signed with keys this hub certified are kept, and a release's trust report counts them as known signers.
  • Approval gates (oxygenflow#57, #235): a job whose pipeline asks for approval waits until someone with the role it names approves it on the run page, through the API, or with oh run approve N --job NAME (oh run reject to refuse). Everyone who may decide is notified once the job is ready. A rejected or expired job ends cancelled and the jobs after it are skipped. A gate that asks for a sealed approval (the default for a job with an environment) is released only by a signature of the approver's own key (oxy seal --flow), kept in the repository and named in the job's identity tokens.
  • Environments and deployments (oxygenflow#59): a job's identity tokens name its environment only once the job is approved. /{owner}/{name}/deployments and GET /api/v1/repos/{o}/{n}/deployments show what each environment runs, who approved it, and the history.
  • Artifacts between jobs, and allow-listed egress (oxygenflow#58, oxygenflow#55): a job's inputs come from the artifacts of a job it depends on in the same run, served to the runner holding it. A sandbox with network "egress" goes only to runners offering egress.
  • Flow priorities (#244): runners take queued jobs by level first (urgent, high, normal, low), then by turn, so repositories take turns instead of one repository's burst holding everyone. The level comes from the run when expedited, else the repository (Settings › OxygenFlow), else the organisation, else the site. A maintainer can expedite a queued or running run to urgent with a reason; at most 2 expedited runs per owner go at once unless Admin › Runners › Settings says otherwise. A pipeline may lower its own jobs with priority "low", never raise them.
  • Runners show Connectivity and Status apart (#243): Admin › Runners shows whether a runner is online (when the hub last heard from it) apart from what it is doing (disabled, draining, idle, or running N of M). A busy runner sends a heartbeat, so a quiet job no longer shows it offline. The queue filters by repository, status and runner, and shows when each job started.
  • Why a job waits (#259): a job some runner could take now says why it still waits: which of those runners are busy (earth-1 busy (3 of 3)) or lack the memory or disk for it now, and how many ready jobs come first.
  • Pages update themselves (#203–#207): the proposal page's timeline and Landing section, the notification bell, the inbox, the dashboard, the issue page, the repository overview and Diffusion refresh in place when something changes, over one /live stream per page. Checks keep their width and refresh in place instead of reloading the whole page (#232).
  • Signing in from an invitation with GitHub, GitLab or Google (#214): the registration page shows the providers' buttons for a usable invitation, and the account is made only with an unused code.
  • git clone on the Code panel (#215) when the repository's Git mirror is on, below oxy clone.
  • oh issue comment -F FILE (#260): takes the comment from a file, or from standard input with -F -, as other oh commands with a body do, and refuses an empty comment.
  • PostgreSQL (#211, #242): the hub can run on PostgreSQL 16 (or 15) when built with --features postgres, set by [database] url (OXYHUB_DATABASE_URL). oxygenhub admin copy-db --from <old>/hub.db moves a stopped SQLite hub's records into an empty Postgres database (--replace empties a fresh one first). Releases include oxygenhub-server-VERSION-linux-x86_64.tar.gz, built for PostgreSQL. See docs/postgres.md.
  • SLSA provenance for build artifacts (#239): an artifact of a job whose signed receipt the hub kept has …/provenance, an in-toto SLSA provenance v1 statement in a DSSE envelope signed with the hub's key; release assets link it (built_by.provenance_url in the API). cosign verify-blob-attestation --key oxyhub-signing.pem --type slsaprovenance1 --insecure-ignore-tlog --signature PROVENANCE ASSET checks a download, with the key from /.well-known/oxyhub-signing.pem.
  • Assistants stream their answers (#248): the time limit is now for silence, not the whole answer, so a large proposal no longer times out with nothing. Drafts fill the editor as they are written. The assistant is told every changed file before the diff, and which files it could not see; a provider that refuses a prompt as too long is asked once more with less.
  • Agent tokens decide approval gates only where the repository allows it (#258, #229): a gate exists so a person decides. A decision through an agent's token (an agent-marked token or an agent's grant) is refused, saying why, unless Settings › OxygenFlow lets agents decide the repository's gates (off by default). Where allowed, the decision names the agent ("approved by carol via claude-code"). A sealed approval is a person's own key and is not affected.
  • A landed proposal's runs that have not started are cancelled (#263): they can no longer change anything and held the queue ahead of the target's own run. Each such job says "moot: proposal #N landed"; runs already going finish.
  • Newest run wins (#267): a new push run cancels the older push runs of its bookmark still going, as before, and now a proposal's new run cancels that proposal's older runs too, so proposal/N runs no longer pile up three or four deep. Each cancelled job says which run superseded it. Runs started by hand, by a schedule, by a tag or as a re-run are never cancelled this way. Settings › OxygenFlow › Newest run wins lets maintainers keep superseded runs that are already running, cancelling only the queued ones.
  • Flow on your own runners (#225): a repository's maintainers can turn OxygenFlow on for it themselves once it (or its owner) has a runner of its own: Settings › OxygenFlow › "on, own runners only". Its jobs then go only to those runners, never to the site's; letting the site's runners take them stays a site administrator's choice.
  • Readers never wait for a push or gc (#249): pages, the API and fetches stalled for up to 43 s while a repository's gc or a push held its lock. With oxy's store now safe to read alongside a writer, reads take only a shared lock that deleting or moving a repository holds alone; pushes, gc, landings, web edits and the rest still take turns as writers.
  • Repositories kept in a bucket answer quickly (#228, #234): the hub shares one client per bucket and credentials, keeps ranges of large packs on disk, looks in packs first, remembers a missing key for 30 s and lists packs once a while. The nightly gc remembers the day it ran across restarts, so a deploy during gc_hour no longer runs it again, and a store that is already one pack is left alone. /health shows bucket requests by kind.
  • Restarts drain (#233): on SIGTERM the hub stops taking connections, ends live streams (browsers reconnect) and gives requests in flight up to 30 s, then stops within a second. It can take its socket from systemd (socket activation), so requests that arrive during a restart wait instead of failing: install oxygenhub.socket and the service changes in docs/deploy.md.
  • Diffusion mends itself (#210): every 10 minutes the pin index is compared with each followed bookmark and re-indexed where it differs, and a consumer left behind with no open bump is caught up. A failed index write says which step failed and is tried again.
  • The state cache counts what states really cost (#246): a loaded state was weighed at a quarter of its real size, so the hub held far more memory than its 3 GiB budget. It now weighs states by what they take, so fewer stay cached; raise OXYHUB_STATE_CACHE_MB if large repositories miss it too often.
  • Sessions are kept on the server (#223): the cookie holds a random id, and a session ends after 14 days unused or 30 days in all. Settings › Security lists your signed-in browsers with Sign out on each. Old cookies no longer work, so everyone signs in once more.
  • A new hub gives administrator only to whoever brings its setup code (#245): on a hub with no accounts the first registration made an administrator with no invitation, whatever the registration mode. The hub now writes a one-time setup code to <data>/setup-code (owner-only) and logs where it is, or takes [registration] setup_code (OXYHUB_REGISTRATION_SETUP_CODE); the first registration must bring it. A sign-in provider never makes the first account. oxygenhub admin user create --admin needs no code.
  • Terraform state needs the write role to read (#256): every account has the Read role on public and internal repositories, so any account's repo:read token could read their Terraform state, which often holds secrets. Reading a state now needs a repo:write token and the write role; the repository's jobs keep their access.
  • HTTPS only (#218): on a site whose base_url is https, a request that a proxy reports came over plain http is redirected (GET, HEAD) or refused (anything else), so a form or password is never taken over http. Every cookie is Secure on an https site.
  • Requests out only to public addresses (#219): webhooks and assistants refuse a host that resolves to a loopback, private, link-local (the cloud metadata address among them) or other local address, pin the connection to the checked addresses and do not follow redirects. A site administrator who wants a local service, such as a local Ollama, must allow it with [outbound] allow in hub.toml.
  • Webhooks read at most 64 KiB of an answer (#220), and a hook with 1000 deliveries waiting takes no more until it catches up.
  • Guessing is slowed (#221): failed sign-ins, two-factor codes, device codes and reset requests count per account (5 free) and per address (20 free), then each doubles the wait, from 2 seconds to a 15-minute lockout; the page says when to try again.
  • Security headers (#222, #227, #231): every response is sent with nosniff, a referrer policy and a permissions policy, and pages with X-Frame-Options: DENY and a report-only Content-Security-Policy, with no inline scripts left in the pages. Strict-Transport-Security is sent on https sites with the max-age from [server] hsts_max_age (OXYHUB_SERVER_HSTS_MAX_AGE); none at 0, the default.
  • security.txt and a security contact page (#224, #230): once an administrator sets a contact under Administration › Security contact, /.well-known/security.txt and /security-contact show it.
  • Fetches no longer fail on a bookmark moved by a key that signed nothing (#266, #271): a delegate or short-lived key that moved a bookmark without signing any of its changes left every client, runners included, failing the whole fetch with "unknown key". Fetches now carry the identities of the keys that signed every bookmark's and tag's latest attestation, so any client can verify the move. The fix first made every fetch of a repository with many bookmarks in a bucket slow (14 s for one with 145); the signers are now gathered once per change to the repository.
  • A renewed key approves as the account its predecessor is linked to (#236): a sealed approval signed by a renewed or delegated key with the same key pair was refused because the new identity was not linked. The hub now follows the key's earlier identities to a linked one, as pushes already did.
  • Closing keywords close every issue they list (#226): Closes #1, #2 and #3, Fixes: #4 #5 and resolves #6 & #7 close every number, not only the first.
  • Long hashes in change titles show their two ends (#216), the whole in a tip, and change rows wrap rather than grow wider than their card.
  • A minified file is shown plain at once: a file with a line over 4,000 bytes was highlighted for seconds and is now shown as plain text, as other forges do.

0.5.2 (2026-10-15)

  • Your year on your profile (#217): above the repositories, a waveform of the changes you pushed each day over the last 3, 6, 9 or 12 months (as long as you have been pushing), coloured by the repository the work went to, with the share pushed through agent tokens as a violet trace and release tags on the peaks. It draws itself in, a heartbeat runs along it while idle, and the pointer lights the days around it with a card for each one; the legend shows one repository's year. Pushes now record the agent token they came through.
  • Sign in with Google is set up on oxygenhub.io, beside GitHub and GitLab.

0.5.1 (2026-10-15)

  • Sign in with GitLab (#213): gitlab.com, or a self-managed GitLab at an address the administrator gives, beside GitHub and Google. The providers show as branded buttons beside the sign-in box (GitLab red-orange, GitHub dark, Google white, each with its logo), and on the registration page when registration is open.
  • A landing runs the pipeline (#197): landing a proposal (on the web, through the API or by Diffusion's auto-land) starts the target's run, as a push does, so consumers following an upstream when green are no longer held back by a landed state that never ran.
  • Long hashes show their two ends (#198): a state address or hash on the overview reads 0826ac0c…f8d97241, the whole of it in a wrapped tip.
  • Multi-line watch lists (#200): .oxymodules lists over several lines, as TOML allows, are read in full.
  • Assistants say why there is no answer (#201): answers may be 16,000 tokens long and background reviews and summaries five minutes; when no text comes, the hub says whether the assistant ran out of room while thinking or the provider declined.

0.5.0 (2026-10-14)

  • Mail that works for any site (#181, #182): Administration › Mail picks a service (Resend, Amazon SES, Mailgun or plain SMTP) and takes its key, STARTTLS by default, with SEND A TEST. Every email is an HTML card with the OxygenHub mark, a button for its link and the site's links, beside a plain-text copy.
  • Notifications by email (#183, #186): owners hear about their repositories by default; Flow says when a build breaks and when it is fixed; backups say when they fail, when they recover, and once a day what ran; admins hear of sign-ups; proposals reach watchers and reviewers. Subjects say what happened, e.g. [OxygenHub] 🎉 Build Success, 💥 Build Broken, ⚠️ Backup Failed, 🔀 New Proposal.
  • Agent tokens (#183, #185): a token marked as an agent's (Settings › Tokens, Mark as agent) tells its owner what is done with it, as anyone else's actions would. Settings › Tokens and Keys are cards with their actions beside each row (#188).
  • Two-factor sign-in asks only for the code (#184): the password is not typed twice.
  • Backups, part two: restore brings the records back and release assets travel (#175); Admin › Backups (#176); a repository can be left out or given its own schedule (#177); Amazon S3 with no key, through STS web identity (#178); an API for settings, TEST, back up now and runs, for users and organisations (#179); empty files upload to GCS (#180).
  • Assistants summarise proposals (#190): Summarise with posts its summary of the proposal as a comment via the assistant, under the prompts the site, organisation and repository set.
  • Choose a bookmark or tag on the overview (#191): a picker beside the code search; the side bar stays.
  • Wiki pages (#192, #194): a title row with who edited the page and its revisions, Pages and Contents in the side bar, and a clone box with a copy button.
  • The Markdown editor (#193) is laid out like GitLab's: a Preview tab, icon buttons with strikethrough, task lists and tables, fullscreen and a Markdown supported footer.
  • Backdrops on more pages (#196): a repository's own sky on its overview, wiki, proposals and Conflicts; the mark on Settings › Keys, Tokens and Backups. Live arrivals on the sky (#172).
  • Diffusion auto-land (#174), opt-in per repository; image architectures detected on push and learnt from runs (#173).
  • Fixes: the proposals list API on a bucket-backed repository (28 s, #171); a long list of heads on a proposal (#170); Conflicts is the usual width (#195); + new is the primary blue (#187); Backups' settings are as wide as its runs (#189).
  • Backups (#169, docs/backups.md): a user (Settings › Backups) or an organisation's owners copy every repository they own, with its wiki, issues, proposals, releases and settings (never a secret value), hourly or daily, to a directory the hub allows or a Google Cloud Storage bucket with no key at all (the hub signs in as backup:OWNER through workload identity federation; TEST checks it). Only what changed travels, runs are kept 7, 30 or 90 days, a failure lands in the owner's inbox, and oxygenhub admin backup restore brings repositories back byte for byte.
  • Diffusion (#148, docs/diffusion.md). When a repository that others pin as a subrepo moves on, each consumer that follows it gets a proposal moving the pin, signed by the hub, tested by the consumer's own pipeline, with the upstream's changes listed. One proposal per consumer and subrepo, kept up to date; never landed by itself. Turned on per repository (Settings › Diffusion, PUT /repos/{owner}/{name}/diffusion, or follow in .oxymodules), off by default; green waits for the upstream's run to pass. A repository's page shows who uses it and how far behind each is (Diffusion › Diffuses to, GET /repos/{owner}/{name}/consumers). Consumers are bumped on the bookmark their work lands on, learned from where their proposals land.
  • Jobs go where their build cache is (oxygenflow#51): a runner's claim lists the caches it holds, and the hub holds a job for up to 15 s for the runner holding its cache, ahead of one holding its image; the run page says "holds its build cache".
  • Diffusion has a face (#154): the diffusion account's picture is the Diffusion mark (assets/icons/diffusion.svg), ripples from a source out to its consumers, until someone uploads another.
  • Diffusion in the API (#153): GET /repos/{owner}/{name}/diffusion gives what a repository takes from and what it diffuses to, each with its pill, for the VS Code extension.
  • The card layout everywhere (#159–#164): the About card's counts in its heading and its buttons centred; wider repository cards with each Diffusion row on one line; the Code button and + New project in the accent blue; proposal and issue sidebars, profile and organisation pages, search results and the wiki's sidebar as cards; every panel's and form card's heading a shaded bar.
  • Every backdrop scene (#167, docs/backgrounds.md): the Oxygen mark's two rings turning behind sign in, register and device sign-in; ripples on a repository's Diffusion tab, its upstreams flowing in and changes flowing out to its consumers in their pills' tones; the swarm on Admin › Runners › Images, chunks streaming to a fetching runner from every holder; and one mark adrift on a page not found or a server error.
  • The hub's sky (#165): Explore and the signed-out home draw the same sky from the public repositories listed (a filter narrows it), never a private one, whoever looks.
  • Your sky (#157, docs/backgrounds.md): a quiet, moving backdrop behind the dashboard, drawn in Rust compiled to WebAssembly: your repositories as clusters and their last 30 days (every change pushed, proposal and issue events, releases, Flow runs) as marks around them; the cursor lights and links them, and names the cluster it rests by. Settings › Appearance: Moving, Still or Off; still under reduced motion.
  • Signed-in pages are private (#158): HTML for a signed-in viewer says Cache-Control: private, no-store, so no proxy or CDN serves one viewer's page to another.
  • The dashboard in cards (#155): each section a rounded card with its heading and count in a shaded bar, your repositories a sidebar card with New in its heading.
  • Tidier pages (#152). The repository sidebar is a column of cards with shaded headings: Diffusion in one card (Takes from and Diffuses to, each with its pill), the ten most recently moved bookmarks with a link to the rest, and the hub's delegate key shown with the OxygenHub mark. The code tree has a shaded head with the latest change and the history, and folder and file icons; the site bar has icons; the Diffusion tables share one column grid; Help › Coming from GitHub covers submodules and Renovate.
  • Admin › Runners › Images (#146): every runner image, where it is and what uses it.
  • Faster pages after a restart (#145): bookmark summaries are kept on disk; slow requests are logged.

0.4.2 (2026-10-11)

  • Coverage from a workstation (#136). oh coverage upload FILE [--bookmark B | --change HASH] keeps an LCOV or Cobertura report measured off the hub as the coverage of that state, for suites a runner cannot run in full. The Coverage page, its history and the Reports card say "uploaded by NAME" where a run number would be, so a runner's figure and an upload's are never mistaken for each other, and an upload sets no check.

  • Images by hash (oxygenflow#39). Rootfs images live on the hub: flow rootfs push sends only the chunks the hub lacks (a rebuilt image costs its changes, runs of zeros cost nothing), the hub checks the whole image against its hash, and a runner that lacks a job's image fetches it instead of anyone copying it between machines. The hub offers such a job to runners that fetch, and says when neither the runner nor the hub holds the image.

  • Reports (#127). A Flow job's report "NAME" { path … } output is kept file by file with every run, and a repository with reports gets one Reports tab in place of Coverage and Tests: each report's latest run with its kind, summary and metrics, and how each moved against the run before. A report's page shows its metrics with sparklines across runs, its thresholds, its [context], its runs, and its own page (index.html) in a sandboxed frame that carries no session. Each metric with fail_above or fail_below sets a check named report/NAME/METRIC on the change.

  • Flow's artifacts survive the nightly gc (#126). They are named only in the hub's database, so garbage collection found them unreachable and would have deleted each one 14 days after its run: coverage files, test reports and dist files alike. The nightly gc now keeps every artifact a repository's runs name.

  • Transfer a project (#125). A board's Settings move it to another owner you may make boards under, such as one of your organisations; so do the API (owner on PATCH /projects/{owner}/{n}) and oh project transfer N --to OWNER. It takes the next number there, with its columns, cards and privacy, and its old address redirects to it.

0.4.1 (2026-10-10)

  • The store as its provider's icon (#123). A repository kept in its owner's bucket shows the icon of Google Cloud Storage, Amazon S3 or Azure Blob Storage in its header; hovered, focused or tapped, a card gives the provider and, for its admins, the bucket, prefix, region and URL, with a link to Settings › Storage.

  • A tidier Projects page (#124). A filter and the New project button in one bar, Open and Closed tabs with their counts, a row per project with its board icon, privacy, number, last update and description; the new-project form opens above the list.

  • Making projects, and their references (#118, #119). The Projects page has a New project button for anyone who may make one, links to the organisations' boards from your own, and a Projects tab on every repository; a card or issue reference may name another repository (owner/repo#N). oh project lists, creates and edits boards (#115).

  • Blame of a conflicted file no longer fails (#120). Its conflict markers are lines no change wrote; the page gave an error, it now shows them with an empty blame cell.

  • Test reports (#114). A Flow job's JUnit XML artifact named junit is read like a coverage report: a Tests tab with each run's passes, failures and skips, every case's history, and the flaky ones. A failing job's artifacts kept with when "always" are shown too.

  • Pushes in parts. A long push from oxy 0.4.3 arrives as several requests, each of objects alone but the last; the hub stores each part and records the push once, when the last names the bookmark.

  • Memory under control (#116, #117). The cache of loaded states keeps to a size budget (OXYHUB_STATE_CACHE_MB, 3 GiB) as well as a count, the summary of a bookmark's heads is cached once per state, and the allocator returns freed memory; /health reports the cache.

  • A coverage check (#113). Each coverage report sets a coverage check, such as 81.9% (-0.3 against main), measured against the default bookmark. Settings › Coverage can set a minimum and a largest drop. The check then fails and says why, and as a required check it holds a proposal back. A proposal's report shows the files whose coverage moved against the default bookmark.

  • Coverage reports, built in (#113). A Flow job's artifact named coverage in LCOV format is read when uploaded, and the job's log says what it found. A Coverage tab shows the latest percentage and its change over a chart of every run, a strip of facts (lines covered, hits per line, functions, branches, files, badge with its Markdown), what this run changed and the files whose coverage moved, how files spread over coverage bands, a chart of covered and missed lines, the recent runs, every file with a bar, and each file's lines marked run or missed, as Coveralls does. A badge is at /OWNER/NAME/coverage/badge.svg, and the run page shows each job's coverage. Totals match cargo llvm-cov's own.

  • A wiki can start with a push. Someone who may write to a repository can push the first pages of its wiki to OWNER/NAME.wiki, as well as writing them on the web: the hub makes the empty wiki when a writer first reaches that address, and the push gives it its id. Readers still find nothing there until it has pages.

  • Mermaid diagrams in Markdown. A ```mermaid block in a README, wiki page, issue or proposal is drawn as a diagram, in the page's light or dark theme. The hub serves its own copy of Mermaid 12.1.0, loads it only on pages that have a diagram, and runs it in strict mode. A diagram that does not parse stays as its source, with the reason under it.

  • An organisation's storage policy (#96). An organisation's Settings › Storage can require every new repository to live in a bucket and list the buckets allowed, so its data stays where its own buckets are. The New repository form refuses anything else and says what is allowed.

  • A repository can live in its owner's bucket (#96). The New repository form has a Storage section: this server's disk, or your own Amazon S3, Google Cloud Storage or Azure bucket, with credentials for it. The bucket is checked before anything is made. The credentials are sealed on the server and used only for that repository. The hub keeps a cache in front. The header shows a bucket tag, and Settings › Storage checks the bucket again and replaces the credentials. Tested with a push into a real S3 bucket.

  • Runners decline, and the queue says why a job waits (oxygenflow#36, OxygenFlow spec §11.4–11.5). A runner can hand back a job it cannot run with a reason. The job is queued again and not offered to that runner while what it offers stays the same, for ten minutes at most. After three different runners decline it, the job ends as unplaceable with their reasons. Every 15 seconds the hub works out why each ready job no runner can take is waiting, from each runner's last claim: no runner online, a missing tier, the wrong architecture, an image it does not hold, a label, memory or CPUs, a scope, or a decline. The run page and the API show the reason, and a runner's page shows its tiers and what it declined.

  • Assistants through the API and oh (§13.4). List, add or change, and remove a repository's, an organisation's or the site's assistants, and read or set their prompts: /repos/{o}/{n}/assistants, /orgs/{org}/assistants, /admin/assistants; oh assistant list|add|remove|prompt with --org or --site. Tokens are write-only and read from an environment variable by oh.

  • Adding an assistant picks from what the provider offers (#109). A provider list fills the name, endpoint and API (with Other for anything else), List models fetches the endpoint's own models into the Model field, and a model the endpoint does not list is refused on save with ones it does. Administration is a sectioned page with a left-hand menu, its audit log paged (#110).

  • Agents get their marks, contributors one row each. A repository's contributors show one row per account with each agent's share beneath (↳ via Claude Code · 1), as oxy log does; agents' names on change pages, reviews and the ask buttons carry their brand mark (Claude, ChatGPT, Perplexity, DeepSeek, Mistral; Antigravity with the Gemini mark), served at /avatar/agent/{name}.

  • Jobs go to runners of the right architecture (oxygenflow#36). Runners report each image's architecture; the hub matches a job's arch or its image's against the runner's, and the queue says what a waiting job asks for.

  • Sign in with GitHub or Google (#106). Set up per provider under Administration (client id, sealed secret, on or off); the login page offers them. A first sign-in links the account with the same verified email, or makes an account when registration is open; connect or disconnect under Settings › Security.

  • Wikis (#104). Each repository can have a wiki: Markdown pages kept as an Oxygen repository of their own, read and edited on the web (with history, a sidebar, a page list and search) or cloned and pushed as OWNER/NAME.wiki. Web edits are signed changes, as code edits are. Settings › Advanced deletes the wiki data.

  • Advanced settings: transfer, archive, delete (#108). A repository's Delete section becomes Advanced: transfer the repository to a user or organisation (the old address redirects and oxy remotes keep working), archive it read-only with a reason and bring it back, delete it, and a placeholder for deleting wiki data ahead of wikis.

  • Accounts elsewhere on a profile (#105). Bluesky, Mastodon and GitHub under Settings › Profile, checked and normalised (a handle, @user@instance or a profile URL all work), shown on the profile with the service's icon as rel="me" links.

  • A job's identity: the hub is an OIDC issuer (#103). A job that declares identity "AUDIENCE" { mount-path … } gets a signed token naming the repository, bookmark, run and job, delivered as a file like a secret; /.well-known/openid-configuration and /.well-known/jwks.json publish what AWS, Google Cloud or Vault need to trust it. Tokens are masked in logs.

  • Secrets for pipelines (#107). Administration, organisation and repository settings hold secrets, typed once and sealed; a job gets only those its pipeline declares with secret, as files Flow mounts before it starts, never in the environment; their values are masked in the job's log as it arrives.

  • Variables for pipelines (#102). Administration, organisation and repository settings hold variables for OxygenFlow jobs, inner levels overriding outer, each page showing what it inherits; a claim carries the repository's effective set and Flow adds them to the job's environment under FLOW_* and the pipeline's own. Not for secrets, which Flow mounts.

  • AI assistants, step three: drafted descriptions (#97). The new-proposal form and an open proposal's editor have a draft-with button per assistant; the answer, written from the changes and diff, lands in the editor for the person to change before saving.

  • AI assistants, step two: review requests (#97). Under Reviewers, the author or a writer asks an assistant the repository may use; the hub sends it the proposal's title, description, changes and diff with the site, organisation and repository prompts, and records the answer as a comment review via the assistant, shown as "bob ↳ agent Claude". Approvals count only people, and an assistant's answer never replaces the asker's own verdict. Also POST …/proposals/{n}/review-requests in the API, with via on reviews.

  • AI assistants, step one: settings (#97). Administration, an organisation's settings and a repository's settings each hold a list of assistants (name, endpoint, API shape, model, extra headers, token, context budget, on or off) and a prompt. A repository sees its organisation's and the site's beside its own; the innermost of a name wins and one turned off above is off below. Tokens are sealed with ChaCha20-Poly1305 under keys/assistants.key and never shown again; TEST sends a one-line exchange. Review requests and drafted descriptions follow.

  • A runner's own labels and time limit (#98). A runner's page under Admin → Runners has a Settings form: labels an administrator gives it beside those it reports (jobs that ask for a label match either), and a job time limit in minutes below the site's, applied when it claims.

  • Mail settings on the admin page (#101). The SMTP URL and From address are set and saved from Administration, overriding hub.toml's [email], with a button that sends a test message to the administrator's own address; the password is kept on the server and shown masked. An empty URL turns mail off.

  • Spacing (#99, #100). The Add a runner button stands off the runners table and the webhook and collaborator forms fill their panel; a proposal's project entries keep a space before remove.

0.4.0 (2026-10-04)

  • Who did the work. A change an AI agent signed for its author says so wherever the author is shown: via NAME with an agent badge in history rows, a via row under the author on the change page (the agent's identity, the delegation it acted under and its expiry, its key), a count under the author in Contributors, and via in the API's change summaries. The author stays the human; a revoked delegation turns the badge red.

  • Rotated identities link themselves. A push that brings changes authored by a successor of a linked identity, where every step kept the key pair (a renewal, a delegation added or removed), links the successor to the same account: delegating to an agent no longer leaves your changes unattributed until you re-upload a key.

  • A Flow badge. /{owner}/{name}/flow/badge.svg is the latest run on the default bookmark as a small SVG for a README (passing, failing, running, no runs).

  • History by revset. GET /repos/{o}/{n}/history/{ref}?r=REVSET answers the changes a revset selects (Oxygen's docs/revset_query_language.md), under a two-second budget.

  • The hub garbage-collects every repository nightly (#94). oxy gc runs for each repository in turn at retention.gc_hour UTC (default 3; 24 or more turns it off), under its write lock, logging what was pruned and packed; oxygenhub gc runs the same pass by hand. The oxygen repository had 1.45 GB of loose objects when this was found.

  • Milestones over the API, and oh milestone (#91). POST /milestones creates one (title, description, due date) or changes the one with that title, PATCH /milestones/{title} changes title, description, due date or state; oh milestone list|create|edit|close|reopen.

  • oh issue edit (#95). Title, body (-F for a file), labels and assignees as whole sets (--no-labels, --no-assignees), milestone ("" clears); oh issue label keeps adding and removing.

  • oh dies quietly on a closed pipe (#92), as oxy does: oh issue list | head -1 no longer panics.

  • The trust report prints the tagged state once (#93): "The tagged state has N heads" with the short heads, and each asset's line names the run that built it.

  • A runner busy with its only job stays online (#78). Its log batches and finish call mark it seen, where only claims did.

  • The hub signs for you only with changes,refs. Delegating to the hub key now takes --scope changes,refs; the recipes and the trust page say so.

  • Web workspaces, designed (#69). Spec §13.3 has the design to review before anything is built: data, the runner's workspace job kind with flow-casfs, the owner-only proxy, delegate-signed pushes to ws/{account}/{id}, security, costs, open questions.

  • What starts a job: on (oxygenflow#24). A pipeline's or a job's on { push "main" "feature/*"; tag "v*"; manual } decides which events run it: pushing a tag now runs the jobs that name it, on the tagged state; a run has only the jobs its event starts (and what they need), and an event that starts none starts no run. A rerun repeats its run's event. A bookmark sent again unchanged starts no run. schedule "0 3 * * *" lines run their jobs on the default bookmark when due (UTC), checked each minute.

0.3.1 (2026-10-02)

  • oh for RISC-V and from apt. oh 0.3.1 comes for 64-bit RISC-V Linux too, its .deb adds the apt repository on its first install, and every file is built by OxygenFlow (attested on the release's trust page).

  • Runner settings for the site, and a job's own time limit (#89, oxygenflow#23). Admin → Runners → Settings sets the default job time limit, the most a pipeline may ask for, and how long past its limit a silent job is given up; saved in the database, over hub.toml's job_timeout_minutes. A job runs under its pipeline's timeout (on the job or its sandbox), capped at the maximum, else the default; the limit is recorded on the job when it is claimed, and the lost-job reaper uses each job's own.

  • A job's scope is honoured (#88). A run a push starts leaves out each job whose scope globs none of the files the push's new changes touch (since the bookmark's previous run), unless a job that runs depends on it; a bookmark's first run, "Run pipeline" and reruns keep every job. A push that touches no job's business starts no run.

  • Modern form controls everywhere (#87). Every text field, textarea and dropdown on the site is rounded and padded, rings in the accent colour on focus, and dropdowns draw their own chevron; checkboxes and radios take the accent colour. On issues and proposals, Projects is a gear menu like Labels and Reviewers instead of a dropdown and Add button.

  • A tidier proposal conversation (#86). A pencil by the title and a ··· menu on the description (Copy link, Copy Markdown, Edit) edit the title, target and description in place, replacing the box at the foot of the page. The comment box has one action row (Back to draft, Close proposal, Comment); the review is a compact card with Comment / Approve / Request changes as one segmented control beside Submit. A landed proposal ends with a "Proposal landed" card (who, how many changes, onto which bookmark), offering to delete the source bookmark while it is still there; a closed one with a "Proposal closed" card.

  • An apt repository at /apt (#85). The hub serves the .deb assets of the releases of the repositories named in [apt] repos (public ones only) as a signed apt repository: dists/stable/InRelease, Release and Release.gpg, main/binary-{amd64,arm64,riscv64}/Packages(.gz), and the pool. The index is rebuilt when a release's packages change, and signed with the key in the data directory's apt-gnupg (public key at /apt/oxygenhub.gpg). /apt says how to add it.

  • The proposal page's sidebar matches the issue page's (#84). Sections with headings; Reviewers and Labels open filterable menus from a gear (people with access to the repository, with avatars; labels with colour and description) that apply in one go; requested reviewers show with avatars; Landing, Checks, Approvals, Projects, Code owners and Depends on as sections. The reviewers form takes the whole list (action=set).

  • Device sign-in says it is done (#83). After Approve, /device shows "Signed in: oxy on saturn is now signed in as you; return to it, there is nothing to copy or enter" with a link to its token, instead of the code form again, which read as if the code were needed a second time. Denying shows a matching "Denied" card.

  • The issue page's sidebar, laid out as GitHub's (#82). Assignees, Labels and Milestone each show what is set under their heading (avatars, coloured label pills) with a gear opening a menu: a filterable checklist (people who can be assigned, with avatars; labels with their colour and description; a milestone) that applies in one go. "Assign yourself" when nobody is assigned. Close, Not planned, Lock and Pin are quiet rows with icons at the foot, evenly spaced, and the comment box is as wide as the conversation.

  • Disable and delete accounts (#81). Administration offers Disable and Enable beside Ban: a disabled account is refused like a banned one (sign-in, sessions, tokens, pushes), and signing in says "Your account has been disabled. Contact the hub's administrator." Delete removes an account for good after its username is typed again: its tokens, keys and memberships go, what it wrote stays under its name, and the name stays retired so nobody can take it over. It is refused, naming them, while the account owns repositories or is the only owner of an organisation; nobody disables or deletes themselves or the last admin who can sign in. A deleted account's proposal reviews now stay, as its comments do.

  • Labels from oh (#80). oh proposal label N --add NAME --remove NAME and oh issue label N … put labels on or take them off (triage), and oh label list|create|delete manage the repository's labels. Through the API, PATCH /proposals/{n} takes labels (the whole set, by name, every name checked before anything changes), and a proposal's JSON lists its labels.

  • A clearer release trust page (#79). A row of badges at the top says whether the tag is signed and by whom, how many changes are signed or sealed, and how many assets an attested OxygenFlow build produced. The graph is drawn at its own size (it filled the screen), drops the builds column when nothing was built, and marks every box: a lock where the claim is signed and checked, ! where it is not (unsigned, uploaded by hand), ✘ where it fails, with the reason underneath and the details on hover. Each asset's heading carries its badge too. The GitHub look no longer strokes the graph's text.

0.3.0 (2026-10-02)

  • Edit a proposal and comment on one from oh (#76). PATCH /api/v1/repos/{owner}/{name}/proposals/{n} changes the title or description, for the author or a maintainer as on the page; oh proposal edit N [-t] [-b | -F FILE], oh proposal comment N -b/-F, and oh proposal create -F FILE ("-" reads stdin).

  • A restarted runner's lost jobs end at once (#71). A runner lists the jobs it holds at each claim (OxygenFlow does), and the hub ends any other job it has as running on that runner, with the reason, rather than an hour later at the time limit.

  • Edit a release (#72). PATCH /api/v1/repos/{owner}/{name}/releases/{tag} changes a release's title, notes or prerelease flag, keeping what it is not given, and oh release edit TAG [--title] [--notes TEXT|-] [--prerelease true|false] sends it. oh api -X METHOD without a body now sends the method asked for (#75).

  • Runners get only jobs they can run (#73). A runner that lists the rootfs images it holds (OxygenFlow does, at every claim) is offered only jobs whose image is among them, rather than jobs it would fail for want of one.

  • Lost jobs end by themselves (#71). A job still running five minutes past the job time limit is one its runner no longer reports (it restarted, or lost the hub as the job ended). The hub now looks for those once a minute and ends them as errors with the reason, skipping the jobs that depended on them and settling the run, so a runner is not shown busy with jobs it is not running. Earth's reset stranded five such jobs, closed by hand.

  • A GitHub-like look, now the default (#74). Primer colours in light and dark, system fonts, rounded boxes, underline navigation, grey and green buttons, sentence case and pill labels, for readability and familiarity, light by default (the theme toggle and Settings → Appearance still offer dark and matching the system). The RetroPlay look stays available: ?look=classic on any page keeps it for that browser, and ?look=github returns to the default.

0.2.0 (2026-10-01)

  • One sign-in for oh and oxy (oxygen#47). oh auth login keeps its token in oxy's credential store (~/.config/oxy/credentials.toml, or the keyring through auth.helper), and oh finds tokens there, so oxy auth login signs oh in too and the reverse. ~/.config/oh/config.toml now holds only the default hub. Pinned oxygen brings remote bookmarks by name, and clones check out the repository's default bookmark.

  • Device tokens expire, and the approval page says where a request came from (#70). A token from device sign-in lasts 90 days past its last use; Settings → Tokens shows its expiry, renewed on use. The /device page shows when the code was asked for and from which address (the client's, through the proxy when behind_proxy is set), and warns to approve only a code you started yourself. DELETE /api/v1/user/token revokes the token it is called with, which oxy auth logout uses.

  • Device sign-in (#67). An app gets a token without a pasted secret. It asks /api/v1/device/code for a code and shows it; you approve the code at /device, signed in with your second factor, choosing what the app may do; its next poll gets the token. oh auth login signs in this way; --token still takes a token.

  • Review requests (#68). The author or a writer asks someone to review under Reviewers on the proposal page, or with POST .../proposals/{number}/reviewers. The person asked is notified, and their review answers the request. Every review now tells the author.

  • Runner scopes (#41, part 2). A runner serves the whole site, one owner's repositories, or one repository, set when its registration token is made or later on Admin → Runners. It never takes another scope's jobs.

  • Flow says which subrepos a job lacks (#56). A job's log starts with each pinned subrepo it was not given (the run's actor cannot read it, or it is not on this hub) instead of failing later on missing files.

0.1.0 (2026-09-30)

  • oh for Linux, macOS and Windows. The first downloadable builds of oh, OxygenHub from the command line, beside Oxygen's own 0.1.0. A prerelease.
  • The hub moved to oxygenhub.io, serving on localhost behind the tunnel.
  • Proposals take feature/… bookmarks, record who landed them and how, and say how to resolve a conflict. Organisation repositories with two or more members require one sealed approval by default.
  • A Code button with the clone command and a zip download.
  • Release assets up to the configured limit through the API.