Onset: version control where nobody waits and nothing gets lost. The on command, its core and onset-server.
| 1 | # Onset spec (draft 2, October 28, 2026) |
| 2 | |
| 3 | Onset (its command is `on`) was called vcs before it had its name. Machines set up then keep working: `on` reads `vcs.toml`, `VCS_*` settings, `.vcs` workspace folders (renamed `.onset` the first time they're used) and its data folder from then, and the host answers at vcs.coline.dev as well as onset.coline.dev. |
| 4 |
| 5 | Onset is open-source version control where nobody waits and nothing gets lost. It has its own model and its own CLI. Git is a bridge in and out, not the foundation. It runs on its own for one person offline, and it gets a server (Upstream, or one you host) when work is shared. |
| 6 |
| 7 | Three promises: |
| 8 | - **Nobody waits.** Landing is the server's job, so nobody's work is refused because someone else landed first. Making a workspace takes seconds, not a clone and a reinstall. |
| 9 | - **Nothing is lost.** Your work saves itself, and `on undo` takes back what you did, including a land. A change follows you to another machine, and deleting a workspace folder loses nothing. |
| 10 | - **Private where you need it.** A folder can be visible to only some people. Your `.env` files are yours alone, and secrets are checked before anything lands. |
| 11 |
| 12 | The plan behind this spec, with the research and the order of work, is `reports/Upstream version control plan Oct 2026.md`. |
| 13 |
| 14 | ## 1. The words |
| 15 |
| 16 | | Word | Meaning | |
| 17 | |---|---| |
| 18 | | **change** | One piece of work. It has an ID made of letters (`rtnbkcvx`; the first 4 or more letters are enough) and keeps that ID from start to finish. It holds every save. It replaces git's commit, branch, stash and reflog. | |
| 19 | | **main** | The shared line of a project. It only moves when a change lands, and each landing makes a new numbered **version**: main 1, 2, 3 and so on. Numbers are always versions and letters are always changes. | |
| 20 | | **land** | Putting a change on main. The server merges it with whatever landed since the change started, runs the project's checks on the exact result, and adds it as the next version. If something conflicts, main doesn't move and you're told exactly where. | |
| 21 | | **workspace** | A folder working on one change. You can have as many as you like, and each holds one change at a time. Your work lives in the change, not the folder. | |
| 22 | | **save** | A snapshot of a change. Saves happen by themselves, and every save is kept. | |
| 23 |
| 24 | There's no staging, no commit command, no branch names and no pull or rebase before landing. |
| 25 |
| 26 | ## 2. A day with Onset |
| 27 |
| 28 | ``` |
| 29 | $ on get upstream.codes/acme/shop |
| 30 | workspace ~/shop (upstream.codes/acme/shop) |
| 31 | change rtnbkcvx new, empty, on main 48190 |
| 32 | next: edit files, then on land -m "what you did" |
| 33 |
| 34 | $ # ...edit src/auth/login.ts... |
| 35 |
| 36 | $ on land -m "Fix login redirect loop" -m "Why: users with expired sessions bounced between /login and /home. Checked: auth tests pass." |
| 37 | landed rtnbkcvx as main 48214 "Fix login redirect loop" |
| 38 | merged with 23 versions that landed since main 48190 |
| 39 | change qwhmdzst new, empty, on top of what you just landed |
| 40 | next: keep working, or on sync to bring in others' work |
| 41 | ``` |
| 42 |
| 43 | Two agents work in parallel by each getting their own workspace: |
| 44 |
| 45 | ``` |
| 46 | $ on get upstream.codes/acme/shop ~/shop-billing |
| 47 | $ on get upstream.codes/acme/shop ~/shop-search |
| 48 | ``` |
| 49 |
| 50 | Both land whenever they're ready, in any order. Neither waits for the other or pulls first. |
| 51 |
| 52 | ## 3. Commands |
| 53 |
| 54 | Four commands cover nearly all work: **get**, **status**, **land** and **sync**. **undo** covers mistakes. |
| 55 |
| 56 | | Group | Command | What it does | |
| 57 | |---|---|---| |
| 58 | | Start | `on get <project> [folder]` | Make a workspace for a project (`--lazy`: each file comes when it's first opened). | |
| 59 | | | `on mount [folder] [--stop]` | Start a lazy workspace's mount again, or stop it. | |
| 60 | | | `on workspaces [add <folder> [--on <change>] \| clean]` | This project's workspaces on this machine (one per agent): list them, add one next to this one (on main, or on a change that hasn't landed), or remove the ones whose work is done. | |
| 61 | | | `on init [folder]` | Start a new project that lives only on this machine. | |
| 62 | | | `on import <git-repo> [folder]` | Make a project from a git repository, with its history. | |
| 63 | | | `on publish <project>` | Put a local project on a host. | |
| 64 | | | `on git [resume]` | Where git clones the project, and whether that git view is current. | |
| 65 | | | `on login [host]` | Sign in (default host: upstream.codes). | |
| 66 | | See | `on status [change]` | Where you are and what to do next. | |
| 67 | | | `on diff [path...]` | What a change changes. | |
| 68 | | | `on log [version\|change\|path]` | History of main, open changes, saves or operations, or the versions that changed a path. | |
| 69 | | | `on blame <file>` | The version that last changed each line. | |
| 70 | | | `on show <file> [--from <version\|change>]` | Print a file as it is in main, or in a version, change or save. | |
| 71 | | | `on tag [name] [version]` | Name a version of main (`v1.0`); a name works wherever a version number does. `--remove` takes it off. | |
| 72 | | | `on bisect <good> <bad> --run "<command>"` | Find the first version of main where a command fails. | |
| 73 | | | `on watch --run "<check>"` | Keep main working: check each new version, and take back the one that breaks the check. | |
| 74 | | | `on verify [version]` | Who signed the land that made a version, checked here. | |
| 75 | | Work | `on new ["title"]` | Start another change. The current one is kept. | |
| 76 | | | `on line [name] [--from <version>]` | The project's release lines, or a new one (`1.x`) from a version of main. | |
| 77 | | | `on describe "text"` | Set the current change's description. | |
| 78 | | | `on switch <change>` | Put this workspace on another change. | |
| 79 | | | `on drop [change]` | Abandon a change. | |
| 80 | | Finish | `on sync` | Bring the change up to date with main. | |
| 81 | | | `on land [change]` | Put a change on main (or on its release line; `--to <line>` brings a change over to one). | |
| 82 | | Fix | `on undo [version]` | Undo the last `on` command in this workspace, or take a landed version back out. | |
| 83 | | | `on restore <path...>` | Bring files back from an earlier point. | |
| 84 | | | `on purge <path>... [--yes]` | Take files out of the project's whole history and delete their content (owners on a host). | |
| 85 | | | `on gc [--older 30d] [--dry-run]` | Clean up what this machine keeps: old saves, and objects nothing needs. | |
| 86 | | | `on sessions [on\|off\|show <change>\|delete <change>\|delete --all]` | Agent sessions kept with the changes they land (only you see them): list, show, turn off or on, delete. | |
| 87 | | | `on submodule [path [commit]] [--url <url>] [--remove] [--checkout]` | Git submodules: list them, point one at a commit, add, remove, or check them out. | |
| 88 | | Share | `on access <change\|path> [who...]` | See or set who can see a change or a folder. | |
| 89 | | | `on group <name> [@person...]` | See or set one of the project's groups. | |
| 90 | | | `on approve <change>` | Approve someone else's change; the last approval a project needs lands it. | |
| 91 | | | `on key` | Make this machine's signing key and add it to your account; your lands are signed from then on. | |
| 92 | | | `on settings [approvals=N] [git=on\|off] [signed=required\|optional]` | See or set the project's host settings: approvals needed to land, whether main is copied to git, and whether lands must be signed. | |
| 93 | | | `on check` | Check this machine's objects and the host's copy of the project for damage. | |
| 94 | | | `on rollback --to <time>` / `--undo <bookmark>` | Put a hosted project back as it was (owners, the last 30 days), or reverse that. | |
| 95 | | Help | `on guide` | Print the guide for coding agents ([GUIDE.md](GUIDE.md)). | |
| 96 | | | `on help [command]` | Help for a command. | |
| 97 |
| 98 | Every command saves the workspace before it does anything else. Every command takes `--json`. Every command that leaves something to do ends with a `next:` line. |
| 99 |
| 100 | ### References |
| 101 |
| 102 | - `48213` is version 48213 of main, and `main` is the latest version. |
| 103 | - `rtnbkcvx` (or a unique prefix of 4 or more letters, like `rtnb`) is a change at its latest save. |
| 104 | - `rtnb:14` is save 14 of change `rtnb`. |
| 105 | - `1.x` is release line 1.x at its latest version, `1.x/3` its version 3, and `1.x/0` the version of main it started from. |
| 106 | - `--at "10 min ago"`, `--at 15:30` or `--at 2026-10-27T15:30` is a point in time in this workspace's saves. |
| 107 |
| 108 | ### on get |
| 109 |
| 110 | ``` |
| 111 | on get <project> [folder] [--change <id>] [--only <path>...] [--line <line>] [--lazy] |
| 112 | ``` |
| 113 |
| 114 | Makes a workspace. `<project>` is `host/owner/name` (`upstream.codes/acme/shop`) or the path of a local project. The folder defaults to the project's name. |
| 115 | - The workspace starts on a new, empty change on the latest main. |
| 116 | - `--change <id>` starts on an existing change instead. If another workspace holds that change, this one takes it over (see section 7). |
| 117 | - `--only src/web docs` gets just those folders, for big projects. |
| 118 | - `--line 1.x` starts on a new change on that release line; `on land` lands there. |
| 119 | - `--lazy` (Linux) makes the workspace a mount that shows every file at once and fetches each one from the host the first time it's opened. Only the folders and the files' sizes come first. What you write goes to a real folder of the workspace's (in Onset's own folder), so saves, lands and syncs work as in any workspace. After a restart, `on mount` (or any `on` command in the folder) starts the mount again. macOS needs FSKit and comes later. |
| 120 | - History is fetched when `log`, `diff` or `restore` need it. |
| 121 | - Files come from a cache shared by all your workspaces. Folders listed under `copy` in `onset.toml` (like `node_modules`) are copied from your most recent workspace of the same project, copy-on-write where the disk supports it. |
| 122 | - Your private files (section 6) come along. |
| 123 |
| 124 | ### on init, import, publish, login |
| 125 |
| 126 | - `on init [folder]` makes a local project with no server. Landing works locally, and main is numbered locally. |
| 127 | - `on import <git-repo> [folder] [--to <project>]` reads a git repository (a folder, a URL, or `upstream.codes/owner/name`) with the git on your machine. Each commit on the default branch's first-parent line becomes a version, in order, in a local project in a folder named after the repository. `--to upstream.codes/acme/shop` then publishes it. `on import <git-repo> --change <branch>`, run in a workspace, brings one git branch in as a change instead. Section 9 covers the rest. |
| 128 | - `on publish <project>` puts a local project on a host. History and version numbers are kept, and from then on the host orders main. |
| 129 | - `on login [host]` opens the host's sign-in page in the browser and waits while you approve it (upstream.codes uses Upstream's sign-in). Hosts without one take a token: `on login <host> --token <token>`. Agents and CI set `ONSET_TOKEN` instead. |
| 130 |
| 131 | ### on status |
| 132 |
| 133 | ``` |
| 134 | $ on status |
| 135 | project upstream.codes/acme/shop |
| 136 | workspace ~/shop |
| 137 | change rtnbkcvx "Fix login redirect loop" open, yours |
| 138 | based on main 48190 (main is at 48213; land merges the difference) |
| 139 | saved 3s ago (save 14) |
| 140 | files M src/auth/login.ts |
| 141 | A src/auth/redirect.test.ts |
| 142 | next: on land |
| 143 | ``` |
| 144 |
| 145 | `on status <change>` shows any change you can see, including its landing state. |
| 146 |
| 147 | ### on diff |
| 148 |
| 149 | ``` |
| 150 | on diff [path...] [--change <id>] [--from <ref>] [--to <ref>] [--stat] |
| 151 | ``` |
| 152 |
| 153 | By default, it shows what the current change changes compared with its base. `--from`/`--to` compare any two references (`on diff --from 48100 --to main`). The output is unified diff, like git's. |
| 154 |
| 155 | ### on log |
| 156 |
| 157 | ``` |
| 158 | on log [-n 20] recent versions of main |
| 159 | on log 48213 one version: description, author, files |
| 160 | on log rtnb one change: state, saves, description |
| 161 | on log --changes open changes you can see: who, state, which workspace |
| 162 | on log --saves saves of the current change |
| 163 | on log --ops operations in this workspace (what undo walks back through) |
| 164 | on log src/auth the versions that changed a file or folder (added, modified, removed) |
| 165 | on log --line 1.x a release line's versions |
| 166 | on blame src/auth/login.ts each line with the version that last changed it |
| 167 | ``` |
| 168 |
| 169 | History follows files that moved: `on log` shows "(from old/path)" at the version that moved one, and goes on back through its old name; `on blame` does the same. A move is a file that went from one path to another whole or with at least half its lines kept (`on status` shows it as `R old -> new`). |
| 170 |
| 171 | `on bisect <good> <bad> --run "<command>"` finds the first version of main where the command fails, by binary search over the numbered versions. Each version it tries is written to a folder of its own (the workspace's files don't change; `--in <folder>` keeps it), and the command runs there: exit 0 means good, 125 means it can't tell (that version is skipped), anything else means bad. |
| 172 |
| 173 | A path is told apart from a change by having a `/` or `.`, by not looking like a change ID, or by being a file or folder in the workspace. Hosts work out a path's history where the versions are, so a machine with only recent history still sees all of it. Each version keeps the paths it changed, so a path's history reads only the versions that touched it: on 20,000 versions, `on log <file>` takes about 10 ms and `on blame` 40 ms (git: 20–90 ms and 210 ms). Private paths have no history or blame for people who can't see them. |
| 174 |
| 175 | ### on new, describe, switch, drop |
| 176 |
| 177 | - `on new ["title"]` saves and keeps the current change, then starts a new change on the latest main. The workspace's files become latest main. |
| 178 | - `on new ["title"] --on <change>` starts the new change on top of another change that hasn't landed yet (stacking). Landing the top change lands the whole stack (section 4). |
| 179 | - `on describe "Title" ["Body"...]` sets the current change's description. The first line is the title. |
| 180 | - `on switch <change>` saves and keeps the current change, then puts the workspace's files on another change. If another workspace holds that change, this one takes it over. |
| 181 | - `on drop [change]` abandons a change (the current one by default). A dropped current change is replaced by a new empty change on the latest main. `on undo` brings it back. |
| 182 | - `on new ["title"] --line 1.x` starts the new change on release line 1.x's latest version instead of main. |
| 183 |
| 184 | ### Release lines |
| 185 |
| 186 | A release line is a named line of versions next to main, for a maintained `1.x` while main goes on. `on line 1.x --from v1.0` starts one from a version of main (or a tag; main's latest by default), and `on line` lists them. |
| 187 | - A change started on a line (`on get --line 1.x`, `on new --line 1.x`) lands there as the line's next version (`1.x/1`, `1.x/2`...); the workspace stays on the line, and `on sync` brings in the line's newest version. |
| 188 | - `on land <change> --to 1.x` brings a change over: for one that landed on main, what it did there (its version against the one before); for an open one, its own edits. The change itself stays as it was. If the edits conflict with the line, the workspace goes on a new change on the line with the conflicts marked, to fix and land. |
| 189 | - A line takes what main does: the repository's rules (a land on 1.x under them is a change request into branch `1.x`, whose merge comes back as the line's next version), `--at`, `--with`, and `on undo` (right after a line's landing, or `on undo 1.x/3` for an older version: a new version of the line takes it back out). |
| 190 | - The git view has each line as a branch of its name, each version a commit on it (with a `Onset-Line: 1.x/3` trailer). |
| 191 |
| 192 | ### on sync |
| 193 |
| 194 | Brings the current change up to date with the latest main, along with any unlanded changes under it. The change's base becomes the latest main. Conflicts are written into the files with git-style markers, and you can keep working while they're there. |
| 195 |
| 196 | ``` |
| 197 | $ on sync |
| 198 | synced rtnbkcvx onto main 48213 (was main 48190) |
| 199 | conflict src/auth/login.ts lines 40-58 |
| 200 | next: fix the marked lines in src/auth/login.ts, then on land |
| 201 | ``` |
| 202 |
| 203 | Markers look like this: |
| 204 |
| 205 | ``` |
| 206 | <<<<<<< main 48213 |
| 207 | return redirect(session.next ?? "/home") |
| 208 | ======= |
| 209 | return redirect(safeNext(session.next)) |
| 210 | >>>>>>> your change rtnbkcvx |
| 211 | ``` |
| 212 |
| 213 | A conflict counts as fixed once the markers are gone from that spot. For binary files, your version stays in place, and `on restore <file> --from main` takes main's instead. Sync exits with 3 when it leaves conflicts, and `on land` refuses (exit 3) while any are left. |
| 214 |
| 215 | You never need to sync before landing. Sync when you want others' work in your files, or after a land reports a conflict. |
| 216 |
| 217 | ### on land |
| 218 |
| 219 | ``` |
| 220 | on land [change] [-m "Title"] [-m "Body"]... [--allow-secrets] |
| 221 | ``` |
| 222 |
| 223 | Saves, then asks the server to land the change (the current one by default) and waits for the result. It needs a description, from `-m`, `on describe`, or a title given to `on new`. `-m` sets the change's description, so after a failed land a plain `on land` reuses it. Section 4 has the details. Outcomes: |
| 224 |
| 225 | | Outcome | Exit | What happened | Next | |
| 226 | |---|---|---|---| |
| 227 | | landed | 0 | The change is main N. The workspace moves to a new, empty change on top of it, and your files don't change. | Keep working. | |
| 228 | | conflict | 3 | Main didn't move, and your change is unchanged and saved. The conflicting files are listed. | `on sync`, fix, `on land` | |
| 229 | | checks failed | 4 | Main didn't move. A `[land] run` command failed (its output is printed). Required checks on Upstream run on the change request instead (section 4). | Fix, then `on land` | |
| 230 | | waiting | 5 | You can't land this on your own: your token can't land, or you're a reader or an outside contributor. The people who can land it are asked. The workspace moves to a new change on top of it, so later edits don't change what's waiting. Or the repository's rules send it through review: the output has its change request's link, the workspace stays on the change, and it lands when the change request's approvals and checks pass. | `on status <change>` later | |
| 231 | | secret | 8 | Something in the change looks like a secret. The file and line are printed. | Remove it, or move it to a private file | |
| 232 | | offline | 6 | The host can't be reached. The change is saved. | `on land` again later | |
| 233 |
| 234 | Landing again is always safe. If the host landed a change but its answer never arrived (a dropped connection), `on land` again answers as the first time did and never lands it twice. A change whose edits main already has (someone landed the same thing) is refused with "main already has everything in this change" (exit 1): `on drop` it. |
| 235 |
| 236 | Landing someone else's change (`on land rtnb`) is how a person with landing rights accepts an agent's change, a teammate's change or an outside contributor's change. |
| 237 |
| 238 | ### on undo |
| 239 |
| 240 | Undoes the last `on` command in this workspace. Run it again to go further back, and `on log --ops` lists what it will walk through. |
| 241 |
| 242 | | Undoing | Does | |
| 243 | |---|---| |
| 244 | | land | Adds a new version to main that takes the landed change back out, then reopens the change in this workspace. Edits made since the land stay in the files and become part of the reopened change. If later versions changed the same lines, undo stops and says so. | |
| 245 | | sync | Puts the change and the files back as they were before the sync. | |
| 246 | | new, switch | Goes back to the previous change and its files. | |
| 247 | | drop | Brings the change back. | |
| 248 | | describe, access | Puts the old description or rule back. | |
| 249 | | restore | Puts the files back as they were before the restore. | |
| 250 |
| 251 | `get`, `init`, `import`, `publish` and `login` aren't undone. Delete the folder, or use the host. |
| 252 |
| 253 | `on undo <version>` takes an earlier landed version back out without undoing anything else. It adds a new version to main that reverses it, and the change it came from becomes open again (`on switch <change>` to work on it). It stops and says so if later versions changed the same lines. |
| 254 |
| 255 | ### on restore |
| 256 |
| 257 | ``` |
| 258 | on restore <path>... back to how it is in the change's base |
| 259 | on restore <path>... --at "10 min ago" from your own earlier save |
| 260 | on restore <path>... --from <ref> from a version, a change or a save |
| 261 | ``` |
| 262 |
| 263 | `on restore .` restores everything. Restoring is itself saved, so it can be undone. |
| 264 |
| 265 | ### on access |
| 266 |
| 267 | ``` |
| 268 | on access <change|path> show who can see it |
| 269 | on access <change|path> <who>... set who can see it |
| 270 | on access <change> <who>... --edit also let them take the change over and work on it |
| 271 | on access <path> <who>... --sealed encrypt it on their machines; the host can't read it |
| 272 | ``` |
| 273 |
| 274 | `<who>` is `me`, `@person`, `@group` (one of the project's groups, set with `on group`), `team` (everyone with access to the project) or `everyone` (public projects only). Section 6 has the rules. |
| 275 |
| 276 | ## 4. Landing |
| 277 |
| 278 | 1. **Saved first.** `on land` saves, then sends the change's final save. Edits made after that point go into the next change. |
| 279 | 2. **Ordered by the server.** Each project's main has one queue, and landings are decided one at a time in the order they arrive. A land is never refused because someone else landed first. |
| 280 | 3. **Merged on the server.** The server does a 3-way merge of each file: the change's base, the latest main and the change. |
| 281 | - Text files merge line by line, as in git. |
| 282 | - Code merges where git stops: two changes that each add something at the same spot both go in, one after the other, when each is whole declarations or tests (two new functions, two new test cases) with nothing in common; entries added to a sorted list (a Makefile's, `#include`s) go in sorted; and a block of imports takes both sides' new imports (JavaScript and TypeScript, Python, Rust, Go, Java and Kotlin, C and C++, C# and Swift), unless both bring in the same name. Statements added at one spot stay a conflict, as in git: their order matters. |
| 283 | - A merge never declares one thing twice. Two `def helper` in one module, `timeout:` twice in one YAML map, two `"test"` scripts in one `package.json`, two methods with one signature: that's a conflict, even where the lines didn't touch and git would call the merge clean. Sync marks the spot where your change added the second one. |
| 284 | - JSON files merge by key, and so do TOML files (`Cargo.toml`, `pyproject.toml`), so two changes that each add keys to the same file both land. |
| 285 | - Where git's own line merge is clean, Onset's result is byte for byte the same. Checked on real history: 4,000 merges of git's own repository (6,416 files both sides changed that git merged cleanly: all identical; of 576 conflicts its maintainer resolved, Onset settled 37 exactly as he did, left 530 to a person, and settled 9 in a way that differs only in order, blank lines or a line one side added that he dropped). `cargo run -p onset-core --example merge_corpus -- <repo>` runs the same check on any repository. |
| 286 | - A file moved on one side and edited on the other lands as one file at its new path, edited. |
| 287 | - Anything that can't be merged is a conflict (two new files at the same path are one too), and the land stops with the list of files and lines. |
| 288 | 4. **It lands at once,** unless the repository's rules say otherwise. On Upstream, the repository's Rules page (required checks, approvals, "changes only through review") applies to Onset as it does to git: |
| 289 | - **No rules:** the land goes on main at once. |
| 290 | - **Rules:** `on land` turns the change into a change request, merged onto main: branch `onset/<change>`, a change request with auto-merge on, and its link (exit 5). CI runs as for any change request, and Upstream merges it once its approvals and checks pass; the merge comes into Onset's main as that change. Running `on land` again after more edits updates the same change request, and `on status` shows where it stands. The change request page is the review screen: diff, comments, approvals, checks. |
| 291 | - **A change into a private folder** can't be shown in git, so it can't be a change request: someone else who can see the folder approves it with `on approve`. Where checks are required, it can't also touch other files (CI can't see private folders): land the private part as a change of its own. |
| 292 | 5. **One change, one version.** The version's description is the change's description, and its author is the change's author (a person, or an agent acting for one). |
| 293 | 6. **Stacks land in order.** Landing a change that sits on unlanded changes lands all of them, bottom first, as consecutive versions. If one conflicts, the ones below it stay landed, and it and the ones above it don't. The error says which. |
| 294 | 7. **After landing,** the workspace continues on a new, empty change on top of what landed. Your files don't change. Onset never changes files in a workspace unless you run a command that says it does: sync, new, switch, restore or undo. |
| 295 | 8. **Who can land comes from the host.** On Upstream, a repository's admins and writers land; anyone else's land waits, and the people who can land are asked. A token only does what its scopes allow, so a read-only token's land waits too. |
| 296 | - **Approvals:** with `on settings approvals=1` (or more), a land waits until that many people other than the change's owner run `on approve <change>`. An approval counts for exactly what was approved, so editing the change needs a new one. The last approval lands the change. |
| 297 | 9. **Outside contributors** don't need a fork. They get the project, make a change, and `on land` asks the maintainers, who land it or don't. |
| 298 | 10. **At a set time:** `on land --at "fri 9:00"` (or `"in 2h"`, `"tomorrow 9:00"`, `"2026-11-02 09:00"`). The host keeps the change as it is now and lands it then; the workspace goes on with a new change on top. Under rules, its change request opens now, so it can be reviewed, and merges from that time once it's ready. The host keeps the lander's token (sealed) only until then. `on land <change>` lands a kept one now; `on drop <change>` cancels it. |
| 299 | 11. **Together:** `on land --with ../api [--with ../web]` lands this workspace's change and each other workspace's change (one per project, on the same host) together: all of them, or none. Each project's main is held at its landing's last step until every part is ready. Under rules in any of them, or with `--at`, each part waits (in review, or held as it is), and the host lands them all once every change request can merge and the time has come. |
| 300 | 12. **Main stays working at landing speed:** lands don't wait for checks, so `on watch --run "pnpm test"` (on any machine with landing rights: a CI box, a dev machine, a cloud computer) follows main and runs the check on each new version. When it fails, the watcher finds the first failing version by binary search over the versions since the last one that passed, takes it back out (a new version, as `on undo <version>`, whose description says what failed and the end of its output) and its change is open again for its owner, who fixes it and lands it again. A failing check is run again before anything is blamed (`--tries`, 2 by default), and exit 125 means "can't tell". `--once` checks what's new and stops (for a scheduled job); `--from <version>` names one known to pass the first time. If later versions changed the same lines, it stops and says so. |
| 301 | 13. **Signed:** after `on key`, every land from this machine is signed with its Ed25519 key, over the change, its base and content, and its description and author. The host checks the key is on the lander's account and the signature matches, keeps it with the version, and adds `Onset-Signed-By: @handle ed25519:<key>` to its git commit. `on verify [version]` checks it again here. With `on settings signed=required`, unsigned lands are refused. |
| 302 |
| 303 | ## 5. Saving, undo and restore |
| 304 |
| 305 | - **Every `on` command saves first.** Between commands, a background **saver** saves any workspace within a few seconds of a file changing. |
| 306 | - The saver is one process per machine. It starts with the first `on` command and stops after 30 minutes with no activity. |
| 307 | - `ONSET_SAVER=0` turns it off. |
| 308 | - **Saves stay local first,** then upload to the host in the background. On the host they're visible only to you and your agents, unless you share the change. |
| 309 | - **Saves include every file that isn't ignored.** Ignored files (from `.gitignore`, same syntax) are never saved and never land. |
| 310 | - **Kept:** every landed version, and every save of a change that's still open. Saves of a change that landed or was dropped go after 30 days, on hosts and (with `on gc`, which the saver runs once a day) on each machine, along with the objects nothing kept needs. |
| 311 | - **Agent sessions:** when an agent lands, what it was asked, said and ran since its last land is kept with the change, with anything that looks like a secret removed. Only you see it, on any of your machines, plus people you share the change with (`on access <change> @sam`). The project's owners don't, and it never reaches git. On a host that keeps machine keys (Upstream's), it's sealed before it leaves your machine: encrypted with a key of its own, wrapped for each of your machines (each puts its key on your account the first time it gets a project there) and, when you share the change, for theirs. The host keeps it and can't read it. A machine of yours that's new since gets the key the next time you show the session on one that has it. Nothing is recorded between lands, and a land you make by hand keeps nothing. `on sessions show <change>` prints it; `on sessions off` stops it on this machine (`ONSET_SESSIONS=0` for one command); `on sessions delete <change>` or `--all` deletes them here and on the host. Agents found: Claude Code, Codex, and any that names its session file in `VCS_SESSION_FILE` (and itself in `VCS_SESSION_AGENT`). |
| 312 | - **`on purge <path>... --yes`** takes files or folders out of the project's whole history, for a secret or a big file that should never have landed: every version of main and of each release line, every change and save, loses them, and their content is deleted (on a host, once nothing else there has the same bytes). Versions keep their numbers; their IDs change. Without `--yes` it only says what it would do. On a host it's for owners: the git view's main is written again from the first version that had them (a force push), other machines take them out when they next sync, and the project can't be rolled back to before the purge. The files go from this machine's workspaces too, so copy anything you still need first. A secret that leaked should still be changed. |
| 313 | - **`on undo`** walks back through operations. **`on restore`** walks back through file contents. Between them, the only way to lose work is to delete a folder before its last edits were saved, which takes under a few seconds with the saver running. Running any `on` command (`on status`) before deleting a folder makes sure. |
| 314 | - **`on check`** reads every object this machine keeps and checks it against its ID, and asks the host to check its copy: every version's ID against its contents and the version before it, and that everything main needs is stored. In a hosted project, damaged objects here are fetched again from the host (`repaired`); it exits 1 for damage it can't repair. (21,178 versions and main's files: 0.6 s on Upstream's host.) |
| 315 | - **Written to disk before anything names it.** Objects are flushed to disk (one `syncfs` on Linux) before the machine's database records a save, version or change that names them, so a power cut can lose the last few seconds of edits but never leave a save pointing at a broken file. |
| 316 | - **`on rollback --to <time>`** (owners) puts the whole hosted project back as it was at a time in the last 30 days: versions, changes, saves and settings. It prints a bookmark, and `on rollback --undo <bookmark>` puts it back as it was before. Workspaces follow on their next `on sync`, keeping their own unlanded edits. A project with a git view pauses it, since git main still has what came after; `on git resume` keeps those commits in git's history (as a second parent) while main's files are the rolled-back ones. |
| 317 | - **Nothing can write into `.git` or `.onset` (or `.vcs`).** A path part can't be either in any spelling a disk treats as the same (`.GIT`, `.git.`, `git~1`), nor hold a backslash: hosts refuse to land one, imports leave them out, and Onset never writes one into a folder or picks up a nested `.git` or `.vcs` folder. On Windows, vcs refuses to write names Windows can't hold (`aux.c`, `a:b`) rather than write somewhere else. |
| 318 |
| 319 | ## 6. Access |
| 320 |
| 321 | Who can see what: |
| 322 |
| 323 | | Thing | Default | Changed with | |
| 324 | |---|---|---| |
| 325 | | An open change | You, and agents acting for you. The maintainers too, once `land` has asked them. | `on access <change> <who>` | |
| 326 | | A path in the project | Everyone with access to the project | `on access <path> <who>` (project owners only) | |
| 327 | | `.env` and `.env.*`, except `.env.example`, `.env.sample` and `.env.template` | **Mine**: only you | `onset.toml` `[mine]` or `on access <path> me` | |
| 328 |
| 329 | - **A private path** (for example `on access billing/ @sam`) is left out of the workspaces, diffs, logs and API answers of anyone it isn't shared with, and out of the git view for everyone (section 9). Project owners see every path. |
| 330 | - It lands normally, and main's version numbers are the same for everyone. |
| 331 | - A version that changed only paths you can't see shows as "a private change", without its description. |
| 332 | - Writing into a path you can't see is refused when you land. |
| 333 | - Agents see what the person they act for sees. |
| 334 | - **A sealed folder** (`on access vault/ @sam @billing --sealed`, on a host that keeps them, like Upstream's) is a private path whose files are encrypted on its readers' machines, so the host keeps them and can't read them. Project owners see it only if it names them; whoever seals it is added. |
| 335 | - Each machine has a key of its own (`on key` shows its fingerprint). The folder has a key, wrapped for every machine of everyone it names; a file is sealed with XChaCha20-Poly1305 under a nonce made from its content, so an unchanged file keeps its ID. |
| 336 | - A machine without the key gets the folder's files when it has one: its first `on sync` (or `on get`) in the project asks for it, and the next `on sync` on a machine that has it brings it in. `on access vault/` lists the machines that have it and those waiting, by fingerprint. |
| 337 | - The host can't merge sealed files, so two changes to one come back as a conflict when landing, and `on sync` merges it on your machine. The host won't show, blame or edit them through its API, and they stay out of git views (a push from git keeps them as they are and can't write into them). |
| 338 | - Naming fewer people makes a new key for what's written from then on; whoever was taken off keeps what they already had. `on access vault/ @sam` (without `--sealed`) unseals it: its files are kept plain from the next land. |
| 339 | - Main's versions from before it was sealed keep its files as they were; `on purge` takes those out. |
| 340 | - The host hands out machine keys, so one that lied about whose they are could get a folder key wrapped for itself. Comparing fingerprints with `on key` on each machine catches that. |
| 341 | - **Mine files** are saved and follow you to every workspace of the project you make, so a new workspace has your `.env` already. They never land, never show to anyone else and never appear in git views. On a host, only you can read them. |
| 342 | - **Secret check:** before landing, added lines are checked for things that look like real keys and tokens. A match stops the land (exit 8) with the file and line. `--allow-secrets` overrides it, and the override is recorded. Saves are private to you, but once you remove a flagged secret, it's also scrubbed from that change's earlier saves. This is the one case where saves are rewritten. |
| 343 | - **Path rules** are project settings, not files in the project, so a private folder's name isn't exposed to people who can't see it. |
| 344 |
| 345 | ## 7. Workspaces |
| 346 |
| 347 | - A workspace is a normal folder with a small `.onset/` folder inside, and a `.git` for editors and tools that read git (gutter markers, GitLens, `git status`, `git diff`, `gh`, build tools that ask git what changed). It's a view: git is checked out at the version the change started from, so it shows the change's own edits, and it follows when that moves (get, new, switch, sync, land, undo), in the background. Every workspace of a project shares one git repository on the machine, so a second workspace's `.git` is instant. `git commit`, `git push`, merges and rebases are refused with the `on` command that does it (`on land -m "…"`, `on sync`). Without git installed, or with `ONSET_GIT=0` (or `git = false` under `[workspace]` in `onset.toml`), there's none. Section 9 covers git itself. |
| 348 | - Making a second workspace of a project takes seconds. Files come from a shared cache, copy-on-write where the disk supports it (APFS clones on macOS, reflinks on Btrfs and XFS). |
| 349 | - `onset.toml` can list build folders to copy from your most recent workspace of the same project: |
| 350 |
| 351 | ```toml |
| 352 | [workspace] |
| 353 | copy = ["node_modules", "target"] |
| 354 | ``` |
| 355 |
| 356 | - **One change per workspace, one workspace per change.** If another workspace (on this machine or another) holds a change and you `switch` to it or `get --change` it, you take it over. The other workspace finds out on its next command. If it had edits that weren't saved yet, they're saved into a new change labelled "rescued from rtnbkcvx", so nothing is lost. |
| 357 | - **Deleting a workspace** is just deleting the folder. Its change stays, and `on log --changes` lists it, ready for `on get --change`. |
| 358 | - **One workspace per agent.** Two processes editing one folder at once are the classic way to lose work silently. Onset keeps every save, but it can't tell two writers apart. |
| 359 |
| 360 | ## 8. Offline and local projects |
| 361 |
| 362 | - **Offline,** everything works except land, get, access and the parts of log and status that need the host's latest main. Sync uses the last main this machine saw. Saves queue and upload once you're back online. |
| 363 | - **A local project** (`on init`) has no host at all, and landing is local. `on publish` moves it to a host whenever you want, with history and version numbers kept. |
| 364 | - **Full copies:** anyone who can read a project can read every version of it, through Onset or its git view, so a public project can always be copied or mirrored. |
| 365 |
| 366 | ## 9. Git and GitHub |
| 367 |
| 368 | Onset is native, but git still works around it, so nobody has to switch at once. |
| 369 |
| 370 | - **Import:** `on import <git-repo>` reads a repository with the git on your machine. |
| 371 | - Each commit on the default branch's first-parent line becomes a version, in order, with its message, author and time. |
| 372 | - Each git file and folder is read once, however many commits share it. SHA-256 repositories (Git 3.0's format) import too; Upstream's git views are SHA-1, so publishing one gives its git view new commits, and `on import --change` matches a branch to the version with the same files when it can't by commit. |
| 373 | - Submodules come along as pointers to their commits: a workspace shows each as an empty folder (as git does before `git submodule update`), and the git view keeps them. `on submodule` lists them; `on submodule <path> <commit>` points one at another commit (with no commit, the one checked out in its folder); `--url <url>` adds one (and its `.gitmodules` entry), `--remove` removes one, and `--checkout` clones each into its folder at its commit with git. Nothing inside a submodule's folder is ever saved. Paths whose names aren't UTF-8 are left out (the import says how many). |
| 374 | - **"Use Onset" on Upstream:** a repository's admins turn Onset on or off in its settings (or when they create it). On links a project to the repository; when it has git history, the first `on get` brings main's history in (on that machine, as `on import --to` does). Off, Onset takes no more lands and the repository is git only, with everything kept for turning it on again. |
| 375 | - **Publishing an import** (`on import <repo> --to <project>`, or a later `on publish`) also copies the original git history, every branch and tag byte for byte, to the project's git view, and main there is the last imported commit. A repository that already has that history (importing a project from its own Upstream repository) is left as it is. |
| 376 | - **The git view:** every hosted project on Upstream is also its Upstream repository, so git clones it as usual (`git clone https://git.coline.app/acme/shop.git`), and Upstream's GitHub mirror keeps a GitHub copy current. |
| 377 | - Main is a git branch, and each version lands there as a commit within seconds. Its message ends with `Onset-Change: rtnbkcvx` and `Onset-Version: 48214` (and `Onset-Signed-By` for a signed land). A busy project's versions go in at most one push a second, many versions to a push. |
| 378 | - Each release line is a git branch of its name, from its version of main's commit. |
| 379 | - After `on purge`, main (and each line) is written again from the first version that had the purged paths, with a force push; commits before it keep their IDs. A repository that refuses force pushes to main pauses the git view and says so. |
| 380 | - The git view is one view for everyone who can read the repository, so it leaves out every private path. |
| 381 | - **Your own git URL:** someone who can see private folders the git view leaves out gets `https://onset.coline.dev/<owner>/<name>.git` (`on git` shows it; their token is the password). It's main as they see it. Everyone with the same view shares one chain of commits, which starts at the version that was main when that view was first used. Each release line is a branch of it too, starting at its version of main's commit when that view has it. A push to main or a line (on top of its latest commit) lands there like `on land`. |
| 382 | - `on git` shows where it clones from and which commit is the latest version. `on settings git=off` stops copying main to git (for a team that's left git behind); `git=on` catches it up. |
| 383 | - **Git branches come in as changes:** `on import <git-repo> --change <branch>`, run in a workspace, makes the branch a change on top of the version it started from, ready for `on land`. It works on any branch of the git view, and on git commits pushed to main. |
| 384 | - **Pushing to git main works too.** On Upstream, commits pushed to git main come into Onset within seconds of the next request (Upstream says when a repository gets a push): each commit on git main's first-parent line becomes a version with its message and author, already in git. If on landed meanwhile, git main's commits merge into one version and the git view's next push makes it a merge commit of both lines, so neither side loses anything. Private paths aren't in git, so they stay as they were. |
| 385 | - **When they can't come in by themselves** (git and Onset changed the same lines, a commit touches a private path, a force push, or over 32 MB at once), the git view pauses instead of overwriting anything, and `on git` says why. Bring those commits in with `on import <repo> --change main` and land them, then `on git resume`: the next version's commit keeps the outside commits as a second parent, so git history loses nothing. |
| 386 |
| 387 | | git | Onset | |
| 388 | |---|---| |
| 389 | | `git clone` | `on get` | |
| 390 | | `git status` | `on status` | |
| 391 | | `git add` + `git commit` | nothing; it saves itself | |
| 392 | | `git commit -m` + `git push` | `on land -m` | |
| 393 | | `git pull`, `git rebase main` | `on sync` | |
| 394 | | `git checkout -b`, `git switch -c` | `on new` | |
| 395 | | `git switch <branch>` | `on switch <change>` | |
| 396 | | `git stash` | `on new` (the current change stays saved) | |
| 397 | | `git checkout -- <file>`, `git restore <file>` | `on restore <file>` | |
| 398 | | `git reflog`, `git reset --hard HEAD@{1}` | `on undo` | |
| 399 | | `git log` | `on log` | |
| 400 | | `git log -- <file>`, `git blame <file>` | `on log <file>`, `on blame <file>` | |
| 401 | | `git show <rev>:<file>` | `on show <file> --from <version>` | |
| 402 | | pre-commit and pre-push hooks | `[land] run` in `onset.toml` | |
| 403 | | `git tag v1.0`, `git push --tags` | `on tag v1.0` (the git view gets it as a git tag) | |
| 404 | | `git worktree add` | `on get <project> <folder>` | |
| 405 | | `git revert <commit>` | `on undo <version>` (or plain `on undo` right after the land) | |
| 406 | | `git bisect` | `on bisect <good> <bad> --run "<command>"` | |
| 407 | | a `release/1.x` branch, `git cherry-pick` onto it | `on line 1.x`, `on land <change> --to 1.x` | |
| 408 | | `git filter-repo --path <file> --invert-paths` | `on purge <file> --yes` | |
| 409 | | `git commit -S`, `git verify-commit` | `on key` (then every land is signed), `on verify` | |
| 410 | | partial clone, VFS for Git | `on get --lazy` | |
| 411 | | a pull request | `on land` without landing rights, or `on access <change> team` | |
| 412 | | a branch someone pushed with git | `on import <repo> --change <branch>` | |
| 413 |
| 414 | ## 10. Agents |
| 415 |
| 416 | - `on guide` prints [GUIDE.md](GUIDE.md), a short instruction file. Agent harnesses can load it as a skill or paste it into `AGENTS.md`. |
| 417 | - **Tokens:** agents use a token in `ONSET_TOKEN`, made by a person on the host. The token acts for that person and can be limited further, to some projects and to reading or writing. The host enforces the limits, so instructions don't have to. |
| 418 | - **Parallel agents** each get their own workspace with `on get`. Second and later workspaces take seconds, and a project's `copy` folders come along. |
| 419 | - **Output:** short text with a `next:` line, or `--json` with the same fields plus `next`. Exit codes are stable (section 11). |
| 420 | - **Without a checkout:** `POST /projects/{owner}/{name}/edits` lands file edits in one call: contents (text, or base64 for binary files), deletions, which files are executable, a title and `base`, the version they were made on. It becomes a change of the caller's and lands like any other: merged with what landed since `base`, waiting for approvals or landing rights, refused for secrets unless asked. A file is sent whole, so edits to a file that's already on main need `base` (new files alone can leave it out); without it, they'd drop what others landed to the file since. Sending the same edits again (a retry), or the same `key`, answers as the first time. Upstream's host also speaks MCP at `https://onset.coline.dev/mcp` (the same tokens), with tools to read a file, list a folder, see recent versions and land edits, for agents that have no shell or disk. Upstream's own API and MCP server read files, search and diff the git view. |
| 421 |
| 422 | ## 11. Output and exit codes |
| 423 |
| 424 | Text output is short and stable: one fact per line, a label in the first column, and `next:` last. Errors start with `error:` and always say whether anything changed. |
| 425 |
| 426 | ``` |
| 427 | error: can't land rtnb: 2 files conflict with main 48213 |
| 428 | src/auth/login.ts |
| 429 | assets/logo.png (binary) |
| 430 | main didn't change; your change is saved |
| 431 | next: on sync, fix the conflicts in those files, then on land |
| 432 | ``` |
| 433 |
| 434 | `--json` prints one JSON object: |
| 435 |
| 436 | ```json |
| 437 | { |
| 438 | "ok": false, |
| 439 | "error": { "code": "conflict", "message": "can't land rtnb: 2 files conflict with main 48213" }, |
| 440 | "main": 48213, |
| 441 | "conflicts": [ |
| 442 | { "path": "src/auth/login.ts", "kind": "text" }, |
| 443 | { "path": "assets/logo.png", "kind": "binary" } |
| 444 | ], |
| 445 | "next": ["on sync, fix the conflicts in those files, then on land"] |
| 446 | } |
| 447 | ``` |
| 448 |
| 449 | | Exit | `error.code` | Meaning | |
| 450 | |---|---|---| |
| 451 | | 0 | | OK | |
| 452 | | 1 | `failed`, `not_found`, `empty_change`, `nothing_to_undo` | Didn't work. The message says why. | |
| 453 | | 2 | `bad_usage`, `no_description` | The command was wrong. `next` shows the right form. | |
| 454 | | 3 | `conflict` | Conflicts. Sync and fix. | |
| 455 | | 4 | `checks_failed` | A `[land] run` command in `onset.toml` failed, or a required check failed on the merged result. | |
| 456 | | 5 | `waiting` | Someone else has to act (land or approve). Don't retry in a loop. | |
| 457 | | 6 | `offline` | The host can't be reached. The work is saved. | |
| 458 | | 7 | `not_allowed`, `not_logged_in` | Permissions. | |
| 459 | | 8 | `secret` | Possible secret in the change. | |
| 460 |
| 461 | ## 12. onset.toml |
| 462 |
| 463 | Optional, at the project's root, and it lands like any file. |
| 464 |
| 465 | ```toml |
| 466 | [workspace] |
| 467 | copy = ["node_modules", "target"] # build folders new workspaces copy from your latest workspace |
| 468 |
| 469 | [mine] |
| 470 | paths = [".env", ".env.*", "!.env.example", "!.env.sample", "!.env.template"] # the default |
| 471 |
| 472 | [land] |
| 473 | run = ["pnpm lint", "pnpm test"] # on land runs these in the workspace first; if one fails, nothing lands (on land --no-run skips them) |
| 474 | ``` |
| 475 |
| 476 | `[land] run` is this machine's, as git hooks are. Required checks, approvals and who can land are host settings, not file settings, so a change can't loosen its own rules. |
| 477 |
| 478 | ## 13. Limits and targets |
| 479 |
| 480 | | | Target | Measured (Oct 27, `tests/perf.sh`, self-hosted server, one Linux machine) | |
| 481 | |---|---|---| |
| 482 | | `on status` on 20,000 files | under 100 ms | 66 ms | |
| 483 | | Second workspace of a 15,000-file project | under 2 s | 0.6 s | |
| 484 | | Land with no required checks, server time | under 50 ms | 0.5 ms median | |
| 485 | | Landings on one main | 400 or more per second | about 2,000 per second; 63 agents landing at once all landed in 1.2 s | |
| 486 | | A save showing up on another of your machines | under 5 s | not measured yet | |
| 487 | | One file | up to 5 GB, stored in chunks, so a small edit to a big file uploads only what changed | | |
| 488 | | One project | up to 100 GiB, the same as Upstream Rapid today | enforced by Upstream's host | |
| 489 |
| 490 | Against stock git (`tests/bench.sh` on one machine, git with a bare repository on the same disk; `tests/bench-live.sh` against git.coline.app and onset.coline.dev; Oct 28): |
| 491 |
| 492 | | | git | Onset | |
| 493 | |---|---|---| |
| 494 | | `status`, 20,000 files | 16 ms | 54 ms | |
| 495 | | One change, commit + push vs land (same machine / live) | 0.07 s / 0.81 s | 0.12 s / 0.30 s | |
| 496 | | First copy, 20,000 files (same machine) / 275 files and 300 versions (live) | 1.26 s / 1.47 s | 0.98 s / 0.49 s | |
| 497 | | 8 / 32 / 64 agents landing on one branch (same machine) | 0.32 / 3.2 / 13.3 s, 1,960 pushes refused at 64 | 0.04 / 0.14 / 0.28 s, none refused | |
| 498 | | 16 agents landing on one branch (live) | 27.1 s, 107 pushes refused | 1.45 s, none refused | |
| 499 |
| 500 | Git's first copy carries all history; Onset fetches history when it's needed. |
| 501 |
| 502 | Upstream's host (`onset.coline.dev`), measured Oct 28 from a machine in us-west-2: |
| 503 |
| 504 | | | Measured | |
| 505 | |---|---| |
| 506 | | `on land`, whole command | about 0.3 s (two requests) | |
| 507 | | 64 agents landing at once on one main | all 64 landed in 0.8 s | |
| 508 | | A fresh workspace of a small project | 0.23 s | |
| 509 | | A 15,000-file project: land, status, first workspace on a new machine | 0.28 s, 0.05 s, 4.6 s | |
| 510 | | `POST /edits` (landing without a workspace) | 0.15 s | |
| 511 | | The git view catching up 173 versions | 6 s, in batches of 64 per push | |
| 512 |
| 513 | Under load (`tests/load.mjs`, agents landing edits over HTTP as fast as they can, each to its own file; Oct 28): |
| 514 |
| 515 | | | Measured | |
| 516 | |---|---| |
| 517 | | 20 projects × 25 agents, 60 s, plus 40 readers | 184,268 landed, 3,052 a second; none failed; landing p50 141 ms, p99 579 ms; reads p50 138 ms | |
| 518 | | 20 projects × 75 agents (1,500 agents), 45 s | 4,094 landed a second, about 200 a second per project; none failed | |
| 519 | | One project, 256 agents | about 100 landed a second; none failed; each waits its turn (p50 2.2 s) | |
| 520 | | One project with its git view on, 64 agents | about 70 landed a second; the git view caught up 17 s after a 20 s burst (256 versions a push, about 2 s each to git.coline.app) | |
| 521 | | A version reaching the git view when the project is quiet | about 0.35 s | |
| 522 | | Host CPU a landing (Durable Object) | about 3 ms (it was 17 ms before edits stopped rebuilding the whole tree) | |
| 523 |
| 524 | A project is one Durable Object, so projects scale out; one project's landings are one queue. |
| 525 |
| 526 | At rest on Upstream's host, a project's objects in R2 are encrypted (AES-256 in counter mode, a key for each project derived from a host secret, so a byte range of a pack reads on its own; every object's ID is the SHA-256 of its content, so a changed byte can't pass as the object). That covers private folders and mine files with everything else; the host still reads them, since it merges and checks what lands. Sealed folders (section 6) are the exception: the host never has their keys. Small objects kept in the project's database are encrypted at rest by the platform. A project's packs from before encryption are written again encrypted as it's used. |
| 527 |
| 528 | Platforms: macOS, Linux and Windows (x64 and arm64 each). On Windows a file keeps the kind Onset knows it as (executable, link) when edited, as git does there, since the disk can't always say. |
| 529 |
| 530 | ## 14. Not in this version |
| 531 |
| 532 | - Git views in SHA-256: Upstream's git repositories are SHA-1. Onset reads SHA-256 repositories (imports, branches, submodules), and publishing one gives its git view new SHA-1 commits. |
| 533 | - Lazy workspaces on macOS (FSKit) and Windows (ProjFS). On those, `--only` covers big projects for now. |
| 534 | - Editor extensions of Onset's own (editors read the workspace's `.git` view). |
| 535 | - Package managers (Homebrew, winget, apt, npm): installs go through the install scripts. macOS builds are signed ad hoc, not notarized. |
| 536 |
| 537 | ## 15. For implementers |
| 538 |
| 539 | - **Core** (`onset/core`, Rust, no I/O): |
| 540 | - Objects are blobs, chunk lists and trees, each named by SHA-256 of a kind byte and its data. Files over 1 MiB are split into content-defined chunks (FastCDC, 16 KiB to 256 KiB, about 64 KiB on average). |
| 541 | - A tree is a sorted list of entries (kind, name, ID), and kinds are file, executable, link and folder. |
| 542 | - A version records its number, tree, change, author, description, time and the previous version's hash, so main is a hash chain. |
| 543 | - Merges are 3-way per file (histogram diff, git-style markers), with JSON merged by key. Private paths are filtered out of trees and grafted back when someone who can't see them lands. |
| 544 | - Bundles carry objects between machines: `VCSB1` frames, zstd when it helps, each checked against its ID on arrival. |
| 545 | - **CLI** (`onset/cli`, the `on` command): one Rust binary on the core. SQLite and one file per object under `ONSET_HOME` (by default `~/.local/share/onset`), shared by every workspace. The saver watches with FSEvents on macOS and inotify on Linux, and looks at active workspaces every 30 seconds for anything the watches missed. `VCS_TRACE=1` prints each call to the host with its status and time. |
| 546 | - **Protocol:** HTTPS with JSON control calls and bundle uploads and downloads (`onset/cli/src/wire.rs` lists every call). A host names its API in `/.well-known/onset.json` (and `vcs.json`, for machines from before Onset had its name). |
| 547 | - **Self-hosted server** (`onset/server`, `onset-server`): the same protocol on SQLite and a folder of objects, with its own accounts and tokens. It has no git view. |
| 548 | - **Upstream's host** (`apps/onset`, at `onset.coline.dev`): a Cloudflare Worker with one Durable Object per project (SQLite for versions, changes, saves, rules and the object index) and objects in R2, where each upload is stored as it came and indexed by offset. A long history leaves a tree's files spread across many uploads, so the host keeps a snapshot of a recent tree's objects in one R2 object, as git hosts keep packs ready for clones: a new machine gets that snapshot plus what changed since (on a 20,000-version project, 1.4–2 s against 18 s). The core runs in it as Wasm (`onset/wasm`) for merges, private paths and the git view. Accounts, roles and tokens are Upstream's, from its public API. The git view is written by pushing to the repository through `git.coline.app` with the lander's token, like any git client, so Upstream Rapid is unchanged. |
| 549 | - **Licenses:** the core, CLI and protocol are Apache-2.0, and `onset-server` is AGPL. |
| 550 |