Origin is version control for repositories that many people, agents, CI runners and review bots read and write at the same time. This document describes the model the v2 prototype implements. Ideas planned for later are listed at the end.
- Zero infrastructure for small projects. A solo project needs the
originbinary and a directory, nothing more: no server, no account, no database. The repository lives in.origin/. The user's identity and node key live in the user's config directory. - The same model at every scale. A monorepo with a thousand agents adds machinery underneath, not different concepts.
- Performance and reliability come first. Every hot path has a benchmark, and every write survives a crash.
- A small core of generic primitives. Plain
originis the CLI and the server. Everything else (CI, review UIs, semantic diffs, integrations) is a plugin built on the same public API. The first plugin isorigin-ci. - Order only what needs ordering. The only things with an order are the landings on each target. Everything else is an unordered set of immutable records.
| Term | Meaning |
|---|---|
| Snapshot | An immutable tree of files, content-addressed with BLAKE3. |
| Change | One piece of work, such as "add retries to fetch". It keeps a stable id while it evolves. |
| Revision | One immutable version of a change: a snapshot, the base it was made on and a description. |
| Statement | An attributable claim about an immutable record: a CI result, a review, a conflict, an abandonment. Annotations are statements. |
| Attachment | The bytes of a revision's description or a statement's payload. It has its own hash domain, so it can never be confused with a file. |
| Target | A long-running, ordered line of history: main, staging, release/v3, a months-long overhaul, or policy. |
| Acceptance | One landing on a target: what landed, the resulting snapshot and the evidence relied on, signed by the target's authority. |
| Authority | The single node that orders a target's acceptances. |
| Node | One running copy of a repository with its own key: a laptop, an agent's machine, a CI runner or a hosted server. |
| Principal | Who acts: user:<name> or node:<name>. Policies also name group:<name>. |
| Workspace | A working copy. Several workspaces can share one repository's store. |
Everyday work happens in changes, not branches. A stack is a chain of changes, each based on the one below. Targets replace branches only where a long-lived, ordered history is wanted.
The shared state of a repository is a set of immutable, content-addressed objects. Each encoding starts with a kind byte, and an object's id is the BLAKE3 hash of the whole encoding, so two kinds can never share an id. Encodings are canonical: little-endian integers, length-prefixed strings and lists, and no trailing bytes.
| Kind | Holds |
|---|---|
| Blob | A file's content. |
| Tree | A directory: names, entry kinds and ids. |
| Revision | One version of a change. |
| Statement | A claim about another record. |
| Acceptance | One landing on a target. |
| Attachment | A description or a payload. |
/// One immutable version of a change.
struct Revision {
change: ChangeId, // random 128-bit id, stable for the change's life
predecessors: Vec<RevisionId>, // the revisions this one evolves from
base: Base, // an acceptance on a target, or another revision
deps: Vec<RevisionId>, // reserved; stacks use `base` today
snapshot: TreeId,
description: AttachmentId, // the intent, written for humans and agents
author: Principal,
}
/// An attributable claim about another record.
struct Statement {
subject: RecordId, // a revision, an acceptance, a snapshot or a statement
kind: Kind, // dot-separated: "ci.test", "review", "core.conflict", ...
verdict: Option<Verdict>, // Pass | Fail | Neutral: the only field core interprets
issuer: Principal,
inputs: Option<Inputs>, // what the claim depended on (see promotion)
payload: Option<AttachmentId>, // free-form, defined by whoever owns the kind
supersedes: Option<StatementId>,// only a statement from the same issuer
promoted_from: Option<StatementId>, // set on a promotion: the original result
}
/// One landing on a target. The authority's ed25519 signature over the
/// encoding follows the body.
struct AcceptanceBody {
target: TargetId,
seq: u64, // 0 for a target's first acceptance
prev: Option<AcceptanceId>, // None exactly when seq is 0
state: TreeId, // the target's snapshot after this landing
landed: Vec<Source>, // a revision, or another target's acceptance
evidence: Vec<StatementId>, // the statements the policy relied on
policy: Option<AcceptanceId>, // the policy in force; None only for policy's first
}Why no global log. A single ordered log of every operation needs one sequencer, which offline nodes can't reach. It also breaks partial replication, because a filtered log no longer verifies. Logs per actor fork whenever one key is used on two devices or a crashed agent restarts. Unordered records avoid both problems. Writers never contend, offline work is free, and a node can hold just the part of the repository it needs.
What we accept in exchange. No node can prove it holds every statement about a revision. This doesn't matter in practice. A check that matters eventually runs, and its result then reaches the nodes that care. An acceptance always lists the evidence it relied on, so the reason for every landing can be verified.
The working copy is always on a revision of the current change. Every command
snapshots it first, so there is no commit and no staging area. .origin and
.git are reserved names, never stored.
A snapshot alone is a draft: the files as they are, which commands that
only read the working copy (status, diff, show, log) show without
recording anything. A command that shares or reshapes the change (describe,
new, edit, push, land, pull, converge, resolve, abandon,
annotate, undo, workspace new, a plugin) first records the draft as a
new revision of the change, if it changed. A change's history then holds the
points that mattered, not every command run while editing.
- Starting and editing.
origin newstarts a change, on the working copy or on a target with--on.origin describeupdates the intent, andorigin edit <change>moves the working copy to another change. - Revising. Editing, rebasing or handing a change to another agent creates a new revision of the same change. Reviews, check results and history stay attached to the change across all of them.
- Divergence. Two revisions made from the same revision give a change two
heads. This is a normal state, not an error. The node that notices it
(locally after a save, the server on a push, a client on a fetch) issues a
core.divergentstatement, which fires an event.origin converge <change>merges every head into one revision. The landing queue never lands a divergent change. - Pushing. When a node is online, a push first compares the change's heads with the remote's and refuses a stale change before uploading anything. The upload itself carries the heads it expects, and the server checks them atomically as it publishes, so two pushes can't fork a change.
- Failed approaches are kept, not deleted.
origin abandon --reason "..."issues acore.abandonstatement. The revision stays available, so the next agent can see what was tried and why it failed. Origin doesn't store agent transcripts. The descriptions of revisions and abandonments hold the useful part.
When a merge (converge, pull, or a rebase in the landing queue) leaves
both sides' changes to the same file, the file gets conflict markers. The new
revision gets a core.conflict statement listing each conflicted path and
the exact content written there.
- A path is resolved once a later revision has different content there, or
when the user runs
origin resolve <path>...to accept it as it is. - Every operation that makes a revision carries unresolved paths forward in
its own
core.conflict: recording a draft,describe,new,converge,pull,undoandworkspace new. - The statement is written before the revision, so nothing can find the revision without its conflicts, even after a crash.
- The landing queue refuses a revision with unresolved paths.
- Markers are only an editing aid. Origin never scans files for them.
A change can move on without the working copy. Its head may land from another workspace, or the queue may rebase it. The working copy is then behind:
statusandlogsay so, and suggestorigin pull.- Commands that build on the working copy refuse until then.
- Edits made meanwhile aren't recorded, so the change doesn't fork. The files are never touched.
origin pull is the explicit catch-up. It fetches first when there is a
remote, then moves the working copy:
- onto the landed tip, if the change landed;
- onto the queue's rebase, if the queue rebased it;
- onto the tip of the target below it, if what it was stacked on landed.
In every case it merges the edits made meanwhile, with conflict markers where they collide. No other command checks files out on its own.
Statements are the one mechanism behind every annotation. Core knows nothing
about kinds beyond the reserved core.* ones, which each have authors:
| Kind | Who may issue it | Meaning |
|---|---|---|
core.abandon |
the change's creator, or an admin | The change was given up; the payload says why. |
core.conflict |
the revision's author, or a node | Paths left conflicted, with the content written there. From a node, it blocks only that landing. |
core.divergent |
a node | The change has more than one head; the payload lists them. |
A change's creator is the author of its first revisions. Reserved kinds can't
be promoted, and origin annotate refuses them.
Every other kind (ci.test, review, semantic.diff, flaky) belongs to
whoever issues it. Statements compose: a human can mark a failed ci.test
statement as flaky, because a statement's subject can be another
statement.
Rules:
- Supersession. An issuer may only supersede their own statements. A
claim to supersede someone else's is ignored. Statements from different
issuers never overwrite each other, so a review from Alice and one from Bob
both stand.
origin annotatesupersedes the user's latest statement of the same kind about that revision. A statement is taken back by superseding it with one that has no verdict (--withdraw), which claims nothing: it never counts as evidence, and nothing promotes it. - Statements are permanent. Reviews, check results and conflicts are
history, and history is never deleted. Only bulky payloads, such as CI
logs, can have a retention period, set per kind in the policy
(
[retention] "ci.*" = "90d").- Age counts from when this node stored the statement.
origin gcdrops expired payloads and keeps the statements: subject, kind, verdict, issuer and inputs.- Payloads of
core.*statements are never dropped.
- Every statement fires an event with its subject, kind, verdict and issuer (see Events).
A statement is always pinned to an exact record. Carrying a check result over
to a new revision never mutates the result. Instead, a node issues a new
statement with promoted_from pointing at the original.
A statement can declare what it depended on:
struct Inputs {
paths: Vec<RepoPath>, // e.g. ["src", "Cargo.toml", "Cargo.lock"]; "" is the whole snapshot
definition: Digest, // the check's command and configuration
toolchain: Digest, // e.g. the hash of `rustc -V` output
external: bool, // also depended on things outside the snapshot
}Each path names a file or a directory and covers everything under it. The
input digest hashes what the snapshot stores at each path (absent, or the
entry's kind and id), plus definition and toolchain. Trees are already
content-addressed, so the digest costs one lookup per path, however large the
tree.
A statement is promotable when it declares inputs, isn't external,
isn't itself a promotion, isn't about another statement, and its kind isn't
reserved. Promotion happens on demand, never on every snapshot:
origin promote;- the landing queue, before it checks evidence;
POST /promote;origin-ci, before it runs a check.
It copies the latest matching result from each issuer, and only results the caller can see.
A promotion counts as its original: the original's kind, verdict and issuer are what policy checks. A result that ran there wins: once the record has a statement of that kind from that issuer, a result or a withdrawal, the promotion no longer counts, whichever came first. It is only valid if:
- its kind, verdict, inputs and payload equal the original's;
- its subject is on the original subject's target: a revision is on the target of the acceptance it's based on, directly or through the revisions below it, an acceptance on its own, and a snapshot on none;
- its subject's input digest equals the original subject's.
Results carry over within a target, never from one target to another, whose checks may differ.
Nodes check this whenever a promotion is used or put.
So a docs-only change keeps its lint and build results, because neither
reads docs. docs-test runs again, because it does read docs. Nobody
writes rules about which changes are "docs-only".
Promotion is only as safe as the declared inputs. A check that also reads the
network or the environment still declares its paths, which remain useful
information, but sets external. It then runs again on every revision. Human
reviews usually declare no inputs, so they never promote automatically.
A target is a hash-chained list of acceptances with one authority. Its tip is the target's current snapshot. Only the authority writes to the chain, and every acceptance names the one before it. Two acceptances with the same predecessor are an error, never something to merge: appends are compare-and-swap.
Policy is a target too. policy holds policy.toml, and changing
permissions or land rules is a landing on policy:
- Policy edits are therefore ordered, reviewed and audited like code, with no extra mechanism.
- Landing on
policyneeds theadmincapability, so an agent can't grant itself rights. - A landing on
policyis judged by the policy before it, never by the one it brings. That way a revoked key can't restore itself. - Every other acceptance names the policy acceptance in force when it landed.
# policy.toml, in the snapshot of the `policy` target
[nodes]
hosted = "<hex ed25519 public key>"
[groups]
maintainers = ["user:alice"]
runners = ["node:ci-1", "node:ci-2"]
[targets.policy]
authority = "node:hosted"
[targets.main]
authority = "node:hosted"
require = [
{ kind = "ci.test", verdict = "pass", issuer = "group:runners" },
{ kind = "review", verdict = "pass", issuer = "group:maintainers" },
]
[targets."release/v3"]
authority = "node:hosted"
require = [{ kind = "ci.test", verdict = "pass", issuer = "group:runners" }]
[retention]
"ci.*" = "90d"
[grants]
all = ["land *", "annotate *"]
"group:maintainers" = ["admin"]
"group:runners" = ["annotate ci.*"]
[[paths]]
path = "secrets"
grants = { all = "none", "group:maintainers" = "write" }
[[paths]]
path = "checks.toml"
grants = { all = "read", "group:maintainers" = "write" }The rules for this file:
- Every table is optional, and unknown keys are errors.
- An authority must be a node listed in
[nodes]with its public key. - A target without its own table uses
policy's authority and has no requirements. - Without
[grants], everyone may land on every target exceptpolicyand issue any kind. Only the local owner is an admin.
origin init creates policy and main. Your node is the authority, and
neither target has requirements, so a solo project lands instantly.
origin repo create --admin <principal> does the same on a server and names
a remote admin.
Common workflows:
- Long-running work.
origin target new overhaul --from maincreates a target, and changes land onoverhaul. Catching up withmainisorigin land --from main --onto overhaul. The overhaul finishes withorigin land --from overhaul --onto main. - Patching old releases. A fix is one change.
origin land <change> --onto release/v3rebases it into a new revision of the same change, so its history shows every target it reached.
Writers never move a target. They submit changes, and the target's authority lands them in order. A stack lands bottom first, one acceptance per change, and stops at the first one that waits.
- Refuse what can never land as it is: divergent changes, abandoned changes, and revisions with unresolved conflicts.
- Rebase the revision onto the target's tip with a three-way merge at
path level, recursing into directories. A revision already based on the
tip lands as it is. Any other becomes a new revision of the same change,
authored by the queue's node, written only if the change's head hasn't
moved meanwhile.
- When one target lands on another, the merge base is found from both histories: the best common ancestors, merged recursively when there are several, as git's recursive strategy does. The merge is checked as a revision on the tip, authored by the queue's node, so results about the same merge on another target never count.
- A landing that changes nothing is skipped.
- On conflict, issue a
core.conflictstatement and keep the landing blocked. Nothing fails, and landing again retries it. - Check the policy in force now:
- this node must be the target's authority;
- the submitter must be allowed to land there, and must be able to write every path the landing changes;
- the required statements must be present, promoted from the same target where the input digest matches.
- If evidence is missing, keep the landing waiting, durably, and request
the checks (
check.requestedevents). It resumes by itself when statements about its subject arrive, and when the server starts. A landing whose submitter's token has expired waits until it's landed again. - Otherwise, write the signed acceptance, listing what landed and the evidence used, and advance the target.
After a landing, the working copy that submitted it follows: a new change on the tip if its change landed, or the queue's rebase if it was rebased.
For a small project the queue runs inside your own origin process. Later,
batching (one acceptance landing several changes) and speculative merge trains
raise a busy target's throughput without changing the model.
Every node keeps a local operation log of what it applied and in what order: records written, landings left waiting, working-copy moves, target advances. It is never synced, and it serves three purposes:
origin undoandorigin op log. Undo only moves this workspace's working copy back; records and landings stay.- Crash recovery, together with the
movingmarker (see On disk). - The event stream.
Events are derived from the log when they're read:
statement.issuedfires for every statement this node stores, fetched and promoted ones included.revision.createdfires for every revision.target.advancedfires when a target's head moves.check.requestedfires once per missing requirement of a waiting landing.
Events are delivered at least once, and consumers deduplicate by record id.
- Locally,
origin watch [--kind ...] [--subject ...] [--since ...]polls the op log for new entries. With a remote it follows the remote instead, unless given--local. - In server mode, the server dispatches each event to the users
subscribed to it, filtered by what each user may read. Subscribers
long-poll:
GET /api/v1/repos/<repo>/events?cursor=...&wait=30sanswers as soon as there are events after the cursor, with a JSON batch and the next cursor.- Without a cursor, only new events come.
waitis capped at 60 s. - A cursor the log no longer holds gets 410, and the client starts over.
- So does a cursor from another op log (it carries a random generation), for example after the remote was replaced.
- Without a cursor, only new events come.
A cursor belongs to one node. Two nodes may deliver the same events in a different order.
Plugins only use the public OriginService API, over HTTP or in-process. A
plugin can do nothing the API can't, which keeps the API complete.
- Workers are long-running processes with a scoped credential. They subscribe to events and issue statements. Examples: a CI runner, a review bot, a semantic differ.
- Tools are executables named
origin-<name>onPATH.origin <name>replaces itself with them, as cargo and git do with their subcommands. Run in a working copy, it records the draft first and setsORIGIN_REPOSITORY,ORIGIN_REVISIONandORIGIN_REMOTE.
Official plugins live in apps/origin-<name>. The first is origin-ci:
- Tool mode:
origin ci run [<rev>]checks a revision as the user. - Worker mode:
origin ci worker --remote <url>answerscheck.requestedwith a token that can read the repository and annotateci.*. It reconciles the waiting landings at start, every minute and after an outage, and outlives server restarts.
Each check runs in a fresh checkout and its own process group. Before running a check, the plugin asks for a promotion and skips the check if a result already covers the revision. Results carry the last 64 KiB of output as their payload.
origin-ci reads the checks from checks.toml. A worker takes them from the
tip of the target being landed on, never from the candidate. The checks that
guard main are the ones already on main, so a submitter can't redefine a
required check by editing it in their change. An edit to checks.toml takes
effect once it has landed, after passing the checks in force. origin ci run
checks a revision with that revision's own checks.toml, since it issues
results as the user.
[checks.lint]
command = "cargo clippy --workspace -- -D warnings"
inputs = ["src", "Cargo.toml", "Cargo.lock"]
toolchain = "rustc -V"
[checks.docs-test]
command = "cargo test --doc"
inputs = ["src", "docs"]
toolchain = "rustc -V"
[checks.links]
command = "lychee docs"
inputs = ["docs"]
external = true # reads the network: never promoted- Each check's kind is
ci.<name>. checks.tomlitself is always one of a check's inputs, so editing a check runs it again.- Protecting
checks.tomlwith a[[paths]]rule, as in the example policy, keeps submitters from changing the checks at all. - A change in a toolchain's output only reaches the queue's promotions through
the inputs. Pin toolchains in an input file, such as
rust-toolchain.toml.
Path rules map a path to none, read or write for everyone, groups and
users. The most specific rule wins. Paths no rule mentions are writable by
signed-in callers and readable by anonymous ones.
Access is split into capabilities:
| Capability | Scope | Allows |
|---|---|---|
read |
path | reading content |
write |
path | proposing revisions that change the path |
annotate |
statement kind | issuing statements of that kind |
land |
target | submitting to the target's queue (land * excludes policy) |
admin |
— | landing on policy |
Reading:
- A caller who can read no path doesn't see the repository at all.
- A revision is visible when it changes a path the caller reads.
- Unreadable files keep their place in trees, but their content is never sent. A clone records the user's rights and leaves withheld paths out of the working copy.
Writing:
-
Writes are attributed: a revision's author and a statement's issuer must be the caller. Anonymous callers never write.
-
A revision's effect on a path is the pair (what its base holds there, what its snapshot holds there). On every path its writer can't write:
- a first revision may have no effect;
- a later revision must have exactly the effect of each of its predecessors;
- a stacked revision must sit on the same change below as its predecessors;
- unresolved conflicts must be carried forward exactly, none dropped and none added; a first revision lists only those of the revision it is on.
So nobody can empty, revert or redirect another person's work without write access to it. Rebasing onto a newer tip still works, and so does handing a change to an agent with other rights, as long as they leave alone what they can't write. Heads that disagree on such paths are converged by someone who can write them.
-
A change's history is fixed: a new first revision for a change that already exists is refused, and so is a predecessor from another change. Only the creator, who wrote the first revision, or an admin can abandon the change. An agent who takes a change over can't abandon it.
-
A statement's subject must be visible to its issuer, and so must a promotion's original.
Tokens:
- Static tokens in the server configuration keep working.
origin token issue --scope ... --ttl ...issues short-lived tokens with narrower scopes for agents and CI runners.- They are signed by the server's node key and verified without storage.
- A scope only narrows its holder's current rights.
- A token lives at most 7 days, and never longer than the token that issued it.
- A typical agent token carries
read /andwrite srcand nothing else.
Origin has no way to erase content from history. A secret that leaks into a revision is rotated. Once anyone has fetched it, deleting it would change nothing.
Each node has an ed25519 key, kept outside the repository in
~/.config/origin/node.key (or $ORIGIN_CONFIG_DIR). Its public key is
published in the policy's [nodes].
- Authorities sign the acceptances they write. A fetch verifies every
acceptance against the key named by the policy that governs it, and only a
policy on the verified
policychain counts. - The node key also signs scoped tokens.
- Other records are attributed by the node that receives them, which has already authenticated the sender.
Signed statements from outside issuers, and identities separate from their keys, come later.
clonefetches the chains ofpolicyand one target (mainby default), then that tip's snapshot. Other snapshots are fetched lazily, when a command needs them.pushshares revisions and statements. It never lands anything: sharing work and accepting it are separate steps.- It refuses a stale change, and compares the expected heads atomically.
- It sends a statement together with the statements it depends on.
- It splits large transfers into parts of at most 64 MiB.
fetchfetches each held target's chain after the last acceptance this node has, and verifies it. It then fetches the changes the remote's events say moved since the last fetch, and falls back to comparing every known change when it has no valid cursor.pullfetches, then catches the working copy up (see Behind).landagainst a remote pushes the stack, submits it to that remote authority's queue, then fetches the result.watchagainst a remote subscribes to its event stream.
Blobs and trees only ever leave a server through a snapshot walk. That walk filters by read access, and is marked durably on the receiving side so an interrupted transfer is detected and repaired. Records are fetched by id.
Syncing never overwrites files in a running workspace. A fetch only adds files the user may newly read.
origin workspace new [--change X] [path] creates a workspace that shares
the repository's store:
- Each workspace has its own current change and stat cache.
- Without
--change, it starts a new change on the current change's base. - Files that another working copy holds unmodified are cloned copy-on-write from it (on Btrfs, XFS or APFS), or copied otherwise. The rest are written from the store.
Checkouts are crash-safe:
- Before any working-copy move that changes files, Origin durably writes a
movingmarker. It records the trees the move goes from and to, and the target revision. - The checked-out files are made durable before the workspace pointer moves.
- A checkout never overwrites a file edited after the command's snapshot. It checks each file again just before replacing it, and if one changed, the move is refused and undone.
- After a crash, the next command finishes the move or takes it back. It only touches files that still match one side, so unrecorded edits survive.
Code that differs between platforms lives behind traits in
crates/origin-platform:
| Trait | Linux | macOS |
|---|---|---|
Durability |
fdatasync for single files, one syncfs per batch |
fsync per file, then one F_FULLFSYNC |
FileLock |
flock |
flock |
CloneFile |
FICLONE |
clonefile |
FileSystem |
statfs magic numbers |
statfs type names |
ReadAt |
pread |
pread |
Processes |
kill a process group once /proc shows it holds its run's lock |
kill nothing: it can't tell which files a group holds |
Each platform has its own module, and a single cfg_select! picks one by
build target. That is the only conditional compilation by OS in the codebase.
One conformance test suite runs against whichever implementation is built.
The standard library, rustix and reflink-copy provide the system calls,
so unsafe stays forbidden everywhere. Windows is not supported.
FileSystem lets writers refuse network file systems such as NFS, SMB, 9p
and AFP. It can't detect FUSE file systems or synced folders such as Dropbox
or iCloud, and accepts them.
A repository is a directory. Its store:
.origin/
objects/loose/<2 hex>/<62 hex> # zstd objects, recently written
objects/packs/<id>.{pack,idx} # small objects, compacted
objects/packs/manifest # the packs in use; replacing it publishes packs
objects/packs/lock # held by the single compactor
objects/collection # writers hold it shared, gc exclusive
targets/<target id>/{name,log,head} # the chain, and its latest acceptance
queue/<target id>/ # landings waiting, and the queue's lock
index/{log,lock} # derived: heads, statements by subject, digests
oplog/{generation,lock,writers/,<segments>}
push # serializes revision puts and queue rebases
fetching/<tree> # snapshot fetches not yet complete
A working copy adds:
workspace # the working copy's revision
state # the stat cache
moving # a move in progress
lock # held by the command working in the working copy
workspaces/<hash> # the other workspaces of this repository
remote # the remote's URL
access/ # each user's rights and event cursor on the remote
Another workspace's .origin/ holds only workspace, state, moving,
lock and repository, which names the main .origin.
- Durability order. Each batch writes its files beside their destinations, syncs them in one batch sync, renames them into place and syncs the directories. Only then is anything published that refers to them. Statements are published before the revision they describe.
- Locking. Records are immutable, so writing one needs no lock. Target
chains, queues, pushes, the index, the op log, the compactor and each
working copy's commands take
flocks, which the kernel releases if the process dies. - Packs. Statements are the bulk of the data, so loose objects of up to
16 KiB are packed. The server compacts in the background once about 8,000
have piled up, and
origin gccompacts on demand. - Names. User-chosen names (targets, changes) never become file names. They are hashed, which avoids case-folding and Unicode normalization problems on macOS.
- The index is derived. It catches up from the op log on open.
origin index rebuildreconstructs it from the store, so a corrupt index never loses data. - Op log segments are checksummed frames. A half-written last frame is cut off. Damage in the middle is read past, reported, and makes the index rebuild.
- Temporary files are
.tmp-<pid>-<n>beside their destination.origin gcremoves the ones a crash left behind. - Local filesystems only. Writers refuse network file systems. A shared folder is a remote, never a live repository.
origin init # a repository with `policy` and `main`
origin new [desc] [--on <target>] # start a change
origin describe "..." # refine its intent
origin edit <change> # move the working copy
origin status | diff | show [<change>] | log [<target>] | evolog [<change>]
origin converge <change> # merge divergent heads
origin resolve <path>... # accept conflicted files as they are
origin abandon [<change>] --reason "..."
origin annotate <rev> <kind> --pass|--fail|--neutral|--withdraw [--inputs ...] [--external] [-m <text> | --payload <file|->]
origin annotate --on <statement> <kind> --pass|--fail|--neutral|--withdraw [-m <text> | --payload <file|->]
origin statements [<rev>] | promote [<rev>]
origin target new <name> --from <target> | targets
origin land [<change>] [--onto <target>] | land --from <target> --onto <target> | land --status
origin clone <url> [path] [--target <t>] | remote [<url>] | fetch | pull | push [<change>...]
origin watch [--kind ...] [--subject ...] [--since <cursor>] [--local]
origin workspace new [--change <c>] [path] | workspace list | workspace rm <path>
origin undo | op log
origin whoami | permissions | token issue --scope ... [--ttl ...]
origin gc | index rebuild
origin serve | repo create <path> [--admin <principal>]
origin <plugin> ... # runs origin-<plugin>, e.g. `origin ci run`
- Unit and integration tests cover each module.
- End-to-end tests run the
originbinary in temporary directories, over a realorigin serveand over local paths. - HTTP tests drive every SDK method against a real server.
- OpenAPI:
docs/openapi.jsonmust match the server;just openapirewrites it.
Benchmarks (just bench) cover:
- store writes and pack lookups;
- snapshots, checkouts and workspace creation on a 20,000-file tree;
- input digests and promotion;
- landing, including concurrent proposers and target-onto-target landings;
- permission checks;
- reading events;
- server request latency and long-poll wakeups;
- clone, push and fetch over HTTP, including a fetch that brings nothing from a long history;
- the CI worker's overhead per check.
just bench-git times status, snapshots and sync against git with
hyperfine.
The swarm scenario is a skill run from T3 Code:
- Three agents work in parallel workspaces and land on a feature target. One changes a shared signature that another depends on, so a landing conflicts.
- Stale results are rerun and unchanged ones are promoted.
- Processes are killed with
kill -9along the way: a checkout, the CI worker and the server. - The work reaches
mainin a single acceptance. - The skill checks that target chains stay consistent and that no revision is lost, and reports timings.
scripted.sh is its dry run, with scripted edits in place of the agents.
- A virtual filesystem for workspaces: FUSE on Linux first, then FSKit on
macOS. It goes behind a
Vfstrait inorigin-platform. - A
Watchtrait (inotify, FSEvents) instead of polling, if polling ever shows up in profiles. - Webhooks, as a plugin.
- Line-level and structure-aware merging. Merges are path-level today.
- Batching several changes per acceptance, and speculative merge trains.
- Set reconciliation (Negentropy-style) for sync.
- Remotes on plain storage, such as S3 with conditional writes.
origin-review(a UI) andorigin-semantic(tree-sitter diffs).- Signed statements from outside issuers, and identities separate from keys.
- A git bridge.
- A local log of drafts,
origin draft listandorigin draft restore, to take back a group of edits.