sluice
sluice admits finished work onto a branch only after the branch, extended by that work and by everything ahead of it, has passed the branch’s gate (22). A session builds in its place and submits its head; it never merges and never moves a branch (1). sluice orders what was submitted, forms each candidate exactly as it would land, and tests candidates ahead of need. When a candidate fails it names the one submission at fault, and it moves the branch only to a revision that passed.
Why it exists
A merge queue enforces the rule that a repository’s branch always passes its tests, by testing the integration revision rather than the change in isolation, and advancing the branch only when that revision passes. Every queue that exists orders pull requests on a forge and waits for a hosted service to report a status. An agent fleet does not work that way. A hundred sessions build in a hundred worktrees on a few hosts, their units merge into a bolt’s branch, and bolts land on the shared branch. The proof of a check lives in a cache keyed by what the check read. Today each session merges for itself: it tests its change against the branch it sees, fast-forwards if nobody moved the branch first, and loses the race otherwise. What lands is verified afterwards, and a red branch is fixed forward.
sluice puts the queue where the work is. A session submits (1), the queue tests the combination that will land (10, 11), and the branch moves only to what passed (22). Because the cache already holds every pass a session proved, testing a candidate reruns only what the combination changed (15).
What it promises
A candidate is the branch plus every entry ahead of it, in order, and nothing else (10). A conflict is found when the candidate is formed, before any check runs (12). Every run is recorded, a fail as faithfully as a pass (16). A failure has exactly one culprit, the newest entry of the first failed candidate, named only once everything ahead of it is known (17, 18), and only that entry’s session hears of it (20). A branch moves only in order, only to what passed, and only from the revision the candidate was formed on (22, 24). Every entry of a branch is in its one chain, tested with the entries ahead of it and landed in chain order (8).
Trust is the branch’s. A bolt’s branch may take a session’s own open run at its word, and the shared branch may require a confined one. The policy says which, and sluice gives no gate a meaning of its own (27, 28, 29).
Why it is open source
A queue for agents in worktrees is a thing every team running a fleet against a trunk needs, and none of the queues that exist is shaped for it. The core knows git and five seams (Part B): a place, a check, a branch, a driver and the records. Each is bound by a profile (Part C). A team with worktrees, a task runner that declares inputs, and a convention for naming branches has a whole queue, and binds its own tools where ours differ.
Where it stops
sluice orders, tests, attributes and lands, and stops there. It runs no check of its own, starts no service and chooses no port (30, 31). It describes no repository and decides no policy (section 8). A verdict is evidence for a person or a platform, never an approval.
What is built above it reaches it through the driver seam like any other (B.5). That includes a planner that supplies what an entry lands after, an operator’s word that admits a submission, and a platform that holds the confined executor.
How to read this book
The chapters are the design: the queue, the gate, places and runs, the
records, the core and its seams, and many hosts, with a glossary and
the open questions. The appendix is the statement of invariants,
included from design/sluice/requirements.md: Part A is what must
hold of sluice, Part B its contracts, and Part C the profiles that
bind them. A number in parentheses points into it.
The queue
A branch has one queue. A session submits a head to it and is done (1). sluice orders the entries, forms each one’s candidate and tests the candidates ahead of need. When a candidate fails it names the one entry at fault, and it moves the branch only to a candidate that passed (22). This chapter is the queue from submission to landing. The gate that judges a candidate is the next chapter.
A submission
A submission is one head offered to one branch, with the entries it must land after, its priority and who submitted it (3). It never names a position: where it lands is the queue’s to decide. The head comes from a place whose tree is exactly its commits; the driver refuses a place holding anything more, and names what it holds (2).
Once submitted, the head is an entry. An entry leaves the queue in three ways only (5): it lands; its submitter, its planner or the operator withdraws it; or a bound the branch’s policy stated in advance removes it. sluice drops nothing on its own. A second submission from the same place to the same branch withdraws the first, which stays readable as withdrawn (67).
Order
Priority orders the entries, then the order of submission (6). Priority moves only entries that are not under test: an entry whose candidate is being tested is never displaced by one submitted after it. An entry never lands before an entry it names as after, and while its after-entries are neither landed nor ahead of it, it waits without holding anyone else (7).
Every entry of a branch is in the branch’s one chain, tested with the entries ahead of it and landed in chain order (8).
Candidates
A candidate is the branch’s current revision extended, in order, by every entry ahead and the entry itself: the revision that entry would land as (10). It contains nothing behind it and nothing removed from the chain. An entry is judged as a candidate and in no other way: never alone against a revision the branch has left, and never in a combination it would not land in (11).
Three entries A, B and C submitted to one branch give a chain of three candidates:
branch + A
branch + A + B
branch + A + B + C
A conflict is found when a candidate is formed, before any check runs, and removes the entry from the chain and nothing else; the chain continues without it (12). The session hears what content is contested and with which entries ahead (65). It can ask the same question before submitting: a forecast forms the candidate the head would get now and reports the conflicts it would meet (64). A forecast creates no entry and moves nothing.
The window
The chain is tested up to the branch’s window: how many candidates are under test at once (13). With a window of four, all three candidates above are tested together, on the bet that they pass. The window never falls below one, grows while candidates pass and shrinks when one fails, within the bounds the branch’s policy sets. An instance of sluice starts no more checks at once than its own bound, whatever the windows of the branches it serves (71).
The policy also says whether every candidate of a chain is tested or only the deepest first (14). Testing the deepest first costs one run for the whole chain when it passes; when it fails, the skipped candidates are tested before anyone is blamed.
Blame
A failed candidate is attributed to exactly one entry: the newest entry
of the first failed candidate in its chain (17). No entry is attributed
or landed before every candidate ahead of it has a verdict (18). So
when branch + A + B fails and branch + A passed, B is the culprit,
A lands, and C is formed again as branch + A + C (21).
Only the culprit’s session is told, with the failing checks and the candidate they failed on; no other session hears of that failure (20). sluice never limits how often a session submits again after its entry was removed (54). The entry’s state shows how many times its place’s work was submitted, and acting on that count is the driver’s or the planner’s.
Landing
A branch moves only to a candidate whose verdict is satisfied, only in chain order, never past an entry without a verdict, and never to a candidate holding a culprit or a conflict (22). Each landing writes one commit per entry, with the revision before it as first parent and the entry’s head as second, naming the entry’s work and the verdict it landed on (23). The branch’s history is never rewritten, and a session’s commits stay reachable from the branch they landed on.
A landing moves the branch from exactly the revision its candidate was formed on. A landing that finds the branch elsewhere is refused, and the chain is formed again on the revision found (24). The candidate’s place is released once its verdict is read; a session’s own place is never released by sluice (25).
The session learns its entry landed through its driver (50). From then on the entry, its state and every verdict read for it stay readable from every host that submits to the branch, whether or not sluice is running (26).
Across branches
Entries of one branch are ordered with each other whichever host submitted them (57). At most one instance of sluice advances a branch at any moment (58). An instance that stopped loses nothing: entries submitted meanwhile are held, and whatever next advances the branch resumes from the records as last written (59). How instances on several hosts share a branch is the many hosts chapter.
The gate
A gate is what a branch requires of a candidate before the branch moves to it. Every branch has one, and it is the branch’s policy’s (27). The policy names the checks the gate requires, the executors it trusts and how a flaky key reads. It says whether every candidate or the deepest first is tested, bounds the window, and sets how long a candidate’s checks may run. sluice gives no gate a meaning of its own and admits every submission a driver makes; whether work may be submitted at all is decided before it reaches sluice (27).
Where the gate is read from
A branch’s gate is read from the branch’s current revision, never from a candidate or a place (56). A submission that changes the policy is judged by the policy it would replace, so no submission loosens the gate that judges it.
The policy is a TOML file checked into the repository, one table per pattern of branch names (C.7). A shared branch and a bolt’s branch carry different tables because they land different things. A bolt’s branch lands units many times a day and wants a fast gate. The shared branch lands whole bolts and demands more.
[branches."bolt/*"]
required = ["#check-fast"]
[branches.main]
required = ["#check-fast", "#check-tools", "#check-e2e"]
batch = true
trust = ["confined"]
systems = ["aarch64-darwin", "x86_64-linux"]
required names the checks. batch tests the deepest candidate first
(14). trust names whose runs count (28). systems names where each
required check must pass (86).
Keys
A key is the identity of one check over the inputs it reads. The same check over different inputs is a different key, and the same inputs anywhere give the same key. A check’s result belongs to its key, not to a branch: a pass recorded under a key satisfies every branch whose gate trusts the executor that recorded it (69). A pass a session proved on its own branch serves the shared branch the moment the shared branch’s candidate reads the same inputs, if the shared branch trusts that executor.
The system a run executes on is part of its key, so one check on two
systems is two keys (85). A gate names, for each check it requires, the
systems a pass must come from, and a pass on one system never stands
for another (86). The system dimension is proposed in
design/sluice/proposals/many-hosts-2.md and awaits the operator’s
ruling; until then a gate names the system of the instance advancing
the branch.
Which checks a candidate runs
A candidate’s verdict is the gate read over the checks the candidate affects (15). The check seam answers which checks a candidate can change the result of, given the branch’s revision and the candidate (36), and only those run. A required check whose key already holds a pass from a trusted executor is read from that pass and not run again (15). When no verdict covers the branch’s revision, the next candidate affects every check the gate requires (70).
A check’s cost never changes how it gates: every check the gate requires has a verdict before the branch moves, and none is deferred past a landing (55).
What is recorded
Every check run on a candidate is recorded with its key, its outcome, its executor and what the executor enforced, before the verdict is read (16, 84). A fail is recorded exactly as a pass. A result a tool answered from an earlier run is a replay, not a run, and is never recorded as one (16); a replay says it is a replay (38). An outcome is what the executor observed, never inferred (38).
Flaky keys
A key holding both a pass and a fail from executors the gate trusts is flaky. It reads as the branch’s policy says, passed or not passed, and never differently for two candidates of one branch (19). A bolt’s branch reads a flaky key as passed so that work keeps moving; the shared branch reads it as not passed until a confined run settles it.
Executors and trust
An executor is what ran a check, with its identity and its host. It is confined when two things hold. It ran in a place holding exactly the candidate and what the repository declares, and nothing of any session could reach that place. And nothing but the executor could have written its records. It is open otherwise.
A result from an executor the gate does not trust never satisfies that gate, whatever its outcome (28). No open run satisfies a gate that trusts only confined executors (29). A run is read as confined, or as held to a limit, only when its executor enforced it (84).
A branch that names no trust accepts every run sluice starts, open,
sandboxed or in a container, and no replay from a cache. A run sluice
starts is in a place sluice made from the candidate, so its result is
the candidate’s. A cache’s replay may hold whatever any shell ran, so
it stays out until the author lists cache.
sluice never runs a check of its own and never changes what a gate requires (30). It reads the gate, runs what the gate names, records what happened, and moves the branch or names the culprit.
The verdict
A verdict is the gate’s reading of a candidate: for each required check, exactly one of passed, failed, missing or flaky. A verdict is satisfied when every required check reads as passed on every system the gate names for it. The landing names the verdict it landed on (23), so what the branch moved on outlives the records.
Places and runs
A place is a working copy sluice makes for one candidate. It holds exactly the candidate’s revision, with the environment the repository declares, and nothing of any other place (33, 35). A candidate’s checks run in it and nowhere else, under an executor that holds them to the limits the branch states.
The place
A place is a detached git worktree at the candidate’s revision, with nothing run in it: no git hook, no worktree tool’s hook, no setup of any kind (C.1). What a check needs before it runs is the repository’s own toolchain step or a task the check depends on, run as a CI job runs its setup.
A place belongs to its candidate. Nothing started in it outlives it, and two places on one host never collide (32). Once the candidate’s verdict is read, sluice releases the place with everything started in it ended (34, 25). sluice releases only places it made; a session’s own place is its driver’s to release.
Caches
A place starts with the caches its branch’s policy declares (35). The content of the files a cache is built from keys it, and only a run that passed saves it. A place takes the cache of its candidate’s key, or else the one saved last. A cache changes how fast a check runs, never what it reports: a test build that takes minutes cold takes seconds warm, and the result is the same either way.
What sluice owes a run
sluice starts no service and chooses no port (31). What a candidate’s checks need, a database, a dev server, an emulator, the checks bring up themselves in the candidate’s place. Every CI job and every test harness owns its own services the same way. The repository’s task graph or its test framework starts them and stops them.
A run gets from sluice exactly four things:
- a clean place holding exactly the candidate;
SLUICE_CHECKOUTandSLUICE_TMPin its environment: the place’s path and a scratch directory of its own. A harness names its databases, sockets and state directories from them and never collides;- a time limit, the branch’s policy’s;
- one process group, which sluice ends when the command exits, times out, or the place is released.
A run that exceeds its time limit is ended with everything it started, recorded as timed out, and read as failed (66). A process that escapes the group, a container or a detached daemon, is the repository’s to stop.
The executor
An executor runs a command in a place within the limits a run is held to (83). The cores are what its instance gives each run. The memory, the network and the paths a run may write are what the branch’s policy states. It reports what it enforced: whether the run was confined, and each limit it held.
An executor that cannot enforce a limit the policy states refuses the run rather than run it without the limit (84). Every run is recorded with what its executor enforced, so a run is never read as confined, or as held to a limit, unless its executor enforced it (84, 16).
The policy names one executor profile (C.2):
| executor | what it enforces |
|---|---|
| open | a process group on the instance’s host; the cores given, by the runner’s own threads; nothing else; never confined |
| sandbox | the host’s kernel sandbox: Seatbelt on macOS, bubblewrap and Landlock on Linux. Writes only to the place and its scratch. The network as the policy says. Processors and memory on Linux, by a cgroup. Confined when no session on the host can write the place |
| container | a container engine on the executing host; the place mounted into a new container from the repository’s image; cores, memory, paths and network; confined as the deployment says |
A run is confined only when its executor established that nothing of any session could reach the place and nothing but the executor could have written its records. Confinement is never inferred from the executor’s kind.
How many runs at once
An instance of sluice starts no more checks at once than its own bound, whatever the windows of the branches it serves (71). A host whose checks are all started by one instance runs no more than that instance’s bound. The bound is the host’s own setting, not the repository’s: a quarter of the cores by default, shared equally between runs, or the number the host’s configuration names.
Where each session runs its own rounds, the branches’ windows are the
only limit, and that is the cost of running that way. One sluice run
on a host, reached by every submission, is what caps the host.
The check seam
The check seam answers two questions. Given the branch’s revision and a candidate, which checks can the candidate change the result of (36)? Given a place and checks, what is each check’s key, its outcome, its executor, and whether the outcome was observed in this run or read from an earlier one (37)? An outcome is what the executor observed, never inferred, and a replay says it is a replay (38).
Two profiles bind it (C.2):
| profile | affected | run |
|---|---|---|
| moon | moon’s affected projects between the branch’s revision and the candidate, downstream dependents included | moon’s run of the affected check tasks; the run report gives each task’s outcome and whether it was replayed; the run record gives its hash as the key and its exit code |
| script | every check, always | one command’s exit code, never a replay |
With moon, a candidate runs only the tasks it affects, and a task whose inputs match a run that passed replays its result. With a script, every check runs every time. Either way the gate reads the same records (53).
Previews
The design below is under review in design/sluice/proposals/ previews-1.md. It holds on one machine.
A person who checks a bolt’s work wants it running. A preview gives them that without anyone keeping a worktree of the branch. sluice makes a place at a revision the branch has landed, and the repository’s own command brings its stack up there on the fakes its checks already use. The caller hands the person the addresses to open (98). The caller also ends it (101).
Three places, three owners
sluice knows two kinds of place already. It makes a candidate’s place and releases it once the verdict is read (25). A session’s place is the session’s own; sluice equips it for the session’s runs and releases it only when the session or its driver asks (95). A preview is the third: sluice makes it, as it makes a candidate’s, and releases it only when its caller asks, because nothing in sluice knows when a person has finished looking.
| place | made by | released by |
|---|---|---|
| a candidate’s | sluice (33) | sluice, once the verdict is read (25) |
| a session’s | its driver | sluice release, at the session’s or driver’s asking (95) |
| a preview | sluice, at a landed revision (98) | sluice release <preview>, at the caller’s asking (101) |
A preview’s revision is one the branch has landed, so a person only ever looks at a combination the gate passed (22, 98). A candidate’s revision, or a session’s head that has not landed, has no preview (S35). The caller names the revision, so a preview asked for as the branch moves again still shows the revision the caller meant. A revision has one preview on a host, whichever branch it was asked through (100).
Each preview keeps its record in .previews/, apart from the other
places’ records, so a sluice that predates previews never reads one.
What runs in it, and under what
The preview’s command is the repository’s, named in [preview] of the
previewed revision’s policy. It runs once, under the host’s executor,
and its process group ends when it exits (66). What it hands to another
process, a container engine’s containers or a process manager’s
servers, stays until the preview is released (98). sluice starts no
service by it (31): the repository’s own tools keep the stack.
What the stack is and how it comes down are the previewed revision’s:
the command, the ports, the caches and the release command are read
from that revision, since they describe its stack. What the command may
use and reach is the branch’s now: the executor, the limits and the
time limit, or [preview] timeout, are read from the branch’s current
revision (27, 56). A gate reads nothing of either; a preview gates
nothing (103).
Names and addresses
Several previews, candidates and session runs share one host, each with servers a person opens by name through a local proxy. sluice gives a preview a name for each port, as it gives each port (31, 99), so no two places alive on the host share either.
A name is one DNS label, <port>-<rev7>--<branch>--<repository>, at
most 63 characters. A longer one is cut and ends in a short hash of the
whole, with the port and the revision kept; a name another preview
holds takes the revision’s first twelve characters instead. One label
holds under a wildcard certificate and a *.localhost redirect, each
of which covers one.
The command receives SLUICE_NAME_<NAME> beside SLUICE_PORT_<NAME>,
with SLUICE_BRANCH and SLUICE_REVISION. The policy writes each
address as a template over the names and ports, and sluice fills it in
as soon as the preview is asked for, before its stack is up (99, S30).
[preview]
command = "moon run app:preview"
addresses = { web = "https://{name.web}.localhost" }
Under portless, the command routes the name to the port with portless alias "$SLUICE_NAME_WEB" "$SLUICE_PORT_WEB", and the release command
removes the alias. sluice names no proxy and never contacts an address.
The life of a preview
A preview is in one of four states, read from its record and its run every time it is asked about, never kept by a process.
stateDiagram-v2
[*] --> waiting: sluice preview, the host at its bound
[*] --> starting: room in the bound
waiting --> starting: a run ends
starting --> up: the command exits 0
starting --> failed: it exits otherwise, or runs out of time
up --> [*]: sluice release
failed --> [*]: sluice release
sluice preview <branch> <revision> makes the place and then waits for
room in the host’s bound, as a session run does, never taking the
share the host reserves for candidates (102). Ctrl-C while it waits
leaves the preview waiting; the next asking takes it up with the same
names and ports. Once the command starts, sluice preview returns with
the addresses, or with --wait once the command has ended. The command
counts against the bound while it runs; a held preview counts against
nothing (102).
A preview whose command fails, or runs out of time, is released at
once: its release command runs, its checkout goes and its ports and
names are freed. It stays listed as failed, holding nothing, with the
end of what its command printed, until its caller releases it (101,
S33). sluice also releases a preview whose checkout no longer exists.
No round, landing or submission ends a preview that is up (101).
Nothing of a preview is a result: its command is never recorded as a run, read by a gate, or waited on by an entry (103).
The caller’s side
sluice knows no bolt and no card. A caller such as crew or flywheel holds both, and owns each preview’s life.
sequenceDiagram
participant C as the caller
participant S as sluice
participant P as the person
Note over C: a unit lands on the bolt at R1
C->>S: sluice preview bolt R1
S-->>C: name, state, addresses
C->>P: R1's addresses, on R1's landing card
Note over C: the next unit lands at R2
C->>S: sluice preview bolt R2
C->>S: sluice release R1's preview
C->>P: R2's addresses, on R2's card
Note over C: the bolt lands on main
C->>S: sluice release R2's preview
One preview per landing: the card’s link always shows exactly the
revision the card names, and a new landing gets a new preview with new
names (S31). A caller that crashed asks again and gets what it asked
for the first time (100, S32). After a crash the caller lists the
host’s previews with sluice previews and releases each whose revision
is on no live card. A caller that forgets one leaks a stack until that
reconciliation finds it, since sluice ends nothing that is up.
A preview and a deployment
A preview is cheap: the repository’s own stack on its fakes, up in seconds to a minute, many at once, on the host it runs on. It answers whether the work behaves, on the stack the gate’s checks started, so what the person sees is what was tested.
It does not answer whether the work deploys and runs in the real environment. Infrastructure code, permissions, the identity provider’s real settings, outside integrations and performance are outside what the fakes model. That takes a deployment by a delivery tool holding the environment’s credentials, which sluice never holds. sluice decides what lands and shows what landed running; putting a revision into a real environment is another tool’s.
| the work touches | the caller asks for |
|---|---|
| behavior, pages, flows, data the fakes model | a preview, on every landing |
| infrastructure code, permissions, identity settings, outside integrations, performance | a deployment from the delivery tool, once the bolt is ready for main |
A caller with both holds one idea for each: an environment for one revision, linked to its card, replaced on the next landing and ended when the bolt lands. Only the provider differs.
The records
Everything sluice knows is in the records: one JSON document per entry and one JSON Lines log of runs and verdicts per branch. They have one form whatever profile wrote them, so a reader of one profile’s records reads another’s (53). A running instance may hold a copy in memory, and is never the only holder. Every state an entry has been in and every verdict read for it is readable by its session, its planner and the operator (26). They read it from every host that submits to the branch, whether or not sluice is running. A state read with sluice stopped is the state sluice last wrote (51).
Read and write
A read returns a branch’s records as last written, and the version they were read at (61). A write names the version its records were read at and the records to write; it succeeds whole or is refused because the records have been written since (62). Two writes from one version never both succeed, and every host reads what the last write left (63). That one rule serves the one-machine case and the fleet alike: at most one instance advances a branch at any moment because at most one write from a version succeeds (58).
Every decision an instance makes is written to the records before it is performed. A landing is written as intended before the branch moves and as done after. An instance that stops at any point loses nothing: every entry submitted meanwhile is held, and whatever next advances the branch resumes from the records as last written (59). The next round finishes, from the records, a landing whose round stopped between its two writes.
What outlives the records
The records are never tracked and never pushed under the local files profile, and a branch’s state ref under the remote refs profile is sluice’s own. What outlives both is the landing commit (23). Each landing commit names the entry it landed, its work and the verdict it landed on, as trailers on the commit. A reader with nothing but the branch’s history knows what was tested and what passed.
chore(land): #12 unit/touches
Sluice-Submission: 12
Sluice-From: crw-1-unit-1 (unit/touches)
Sluice-Tested: 9ab3d07c11
Sluice-Result: passed (2 run, 31 replayed)
The results log
A check’s result belongs to its key, not to a branch: a pass recorded under a key satisfies every branch whose gate trusts the executor that recorded it (69). So beside the per-branch records sits one log of check results per repository, with each result’s key, outcome and executor. A bolt’s branch and the shared branch read the same log and apply their own trust to it.
Profiles
The records seam has three profiles (C.6). A deployment names one.
Local files, one host. The documents live under the repository’s common git directory, one directory per branch and one results log per repository, readable by every place on the host with no process running and never tracked. A version file beside them is bumped on every write; a write takes the branch’s lock and is refused when the version moved.
.git/sluice/bolt/concurrent-units/
version 57: bumped on every write
submissions/12.json
submissions/13.json
log.jsonl every submission, run, result, blame and landing, in order
.git/sluice/.logs/bolt/concurrent-units/<candidate>/<task>.log
.git/sluice/caches/ the caches the policy declares, the last two keys of each
.git/sluice/results.jsonl
every check result of the repository, with its executor
Remote refs, peers. A branch’s records are one commit under
refs/sluice/state/<branch> on the shared remote, its tree the branch’s
documents and its id the version. A write is a push with
--force-with-lease from the commit read: the push succeeds only if the
ref still holds that commit, which is the conditional write of (62) as
git provides it. Beside it the remote holds refs/sluice/heads/<oid>
for every submitted head, refs/sluice/combinations/<branch>/<id> for
every candidate open to claims, refs/sluice/logs/<branch>/<run> for a
run’s output, and refs/sluice/hosts/<host>, rewritten by each host
alone on a fixed cadence as its sign of life. The queue’s history is
git log of the state ref.
Server, a central server. The records are the server’s local files,
and other hosts reach them only over SSH. After every write the server
pushes the same tree to refs/sluice/state/<branch> on the remote as a
projection, so a reader that cannot reach the server reads the state
last written and says that it did (80). A server that is lost is
rebuilt from the projection.
Telemetry
With telemetry turned on, each sluice process pushes traces and metrics over OTLP to a collector the host runs. Each round is one trace, with counters and histograms for queue depth, verdicts, replays, culprits, landings and time to land. Nothing reads them back. The records are the only truth, and an exporter that cannot reach its collector loses what it held and says nothing.
The core and its seams
sluice is a small core that knows git and reaches everything else through seams. Each seam is a contract of Part B, and each is bound by naming one profile of Part C; Parts A and B never change to admit a profile (52). A team with worktrees, a task runner that declares its inputs and a convention for naming branches has a whole queue, and binds its own tools where they differ.
The seams
The place (B.1) materializes a revision into a working copy holding exactly that revision and the environment the repository declares, and releases it with everything started in it ended (33, 34). Its one profile is a detached git worktree with nothing run in it (C.1); a container is the executor’s, not a second checkout.
The check (B.2) answers which checks a candidate affects (36). It runs them and reports each check’s key, outcome, executor and whether the outcome was observed or replayed (37, 38). It executes a command within the limits a run is held to and reports what it enforced (83, 84). The profiles are moon and a script (C.2). moon runs only the tasks a change affects and reads each task’s hash as its key; a script is one command with an exit code, and never a replay. The executor profiles are open, sandbox and container.
The branch (B.4) reads a branch’s current revision, forms a candidate from a revision and a head or reports the conflict, commits a landing, and moves the branch (42 to 45). A move names the revision the branch is expected at; it is all or nothing, and two moves of one branch from one expected revision never both succeed (46). The profiles are local refs, one host’s repository, and pushed refs, a branch on a remote every host reaches (C.4).
The driver (B.5) is the front door. It submits a head, forecasts the conflicts a head would meet, amends or withdraws an entry, reads an entry’s state, and delivers an outcome to its session (47 to 50). A notification reaches the submitter at least once and names its entry and outcome (51).
The records (B.7) read a branch’s records with their version and write them conditionally on it (61 to 63). The profiles are local files, remote refs and a server (C.6).
Policy is not a seam. It is a TOML file checked into the repository, one table per pattern of branch names, read from the branch’s current revision (56) (C.7). The core reads the gate from it and binds nothing to it.
Branch protection
A session submits and does nothing more: it never merges and never
moves a branch, and an attempt to is refused and reported (1). Under
local refs, a reference-transaction hook refuses any update to a
branch the policy names that sluice did not make, and logs the attempt.
Under pushed refs, the forge’s rule protects the branch and only
sluice’s identity bypasses it, an identity no session on any host can
use (C.4). The hook calls the installed sluice, never one built from a
submission, so no submission changes the guard that judges it.
Drivers
Whatever submits work and carries the outcome back is a driver, and a driver decides nothing about the queue (C.5).
- worktrunk:
wt submit, the executablewt-submiton the path, waits for the entry’s outcome and reports it as its exit;--no-waitreturns the entry at once;--checkforecasts; on landing, worktrunk removes the session’s place. - crew: the unit’s merge stage submits, a bolt is submitted from its own place by its conductor, and the conductor reads the entry’s state as the stage.
- flywheel: a session’s approved done exit is the submission, and the entry’s state is evidence on the item.
- remote submit: a session on any host pushes its head to the remote and writes the entry through the records, or sends both to the server over SSH. A waiting submit reads its entry’s state until the outcome.
- pull request, proposed: a forge’s request to merge a head is the submission, and closing it withdraws. Its checks panel shows the entry’s state. The landing is sluice’s push of the branch, after which the forge marks the request merged. The forge’s own merge button, merge queue and squash or rebase merges are not used on a branch sluice leads.
Rounds
A round is one pass of sluice’s work for one branch: read the queue,
start runs where there is room, read finished runs, blame a failure,
land what passed, write the records, exit. Nothing is held between
rounds outside the records, and no two rounds act on one branch at once
(58). sluice step <branch> runs one round and exits, for a tool with
a loop of its own; sluice run runs rounds for every branch with a
queue until stopped. Both do the same work.
sluice starts no service, binds no port and starts no daemon (31). On
one machine a waiting wt submit runs the rounds itself while it waits,
under the branch’s lock, checking each round whether its own entry is
decided. Any number of waiting sessions do this at once, and the lock
lets one round run at a time. A round is sluice’s code whichever
process runs it: the queue, the runs and the gate decide what it lands,
never who called it.
On a fleet, a sluice run the operator started runs every round, and
a waiting submit only submits and waits. The repository’s policy
says a branch is on a fleet with [submit] remote = true, or one
submission says so with --remote; the flag only turns remote on. A
submit on a fleet never takes a branch: it cannot tell an empty fleet
from a quiet one, and only the sluice user holds the branch’s
credential. A fleet with no sluice run accepts submissions, holds
them, and lands nothing until one starts (59).
A branch on a forge, and pull requests
The design below is under review in design/sluice/proposals/ pull-requests-1.md and is not yet built. It holds on one machine and
on many, with any executor: the checks run where sluice runs them.
A repository’s shared branch often lives on a forge such as GitHub, and its work arrives as pull requests that wait for review, for a QA team, or for other systems’ checks. sluice keeps such a branch in step with the forge, and treats a pull request as a submission’s page. People review it there, other systems post their checks there, and sluice shows there what the queue is doing with it.
Two stages: waiting, then the queue
A pull request waits while it is a draft, or while a condition the
branch’s policy names does not yet hold. A condition is an approving
review on the current head, or a check another system posts on the
head by name, such as qa/approved from a QA team that tests a
deployment of the request. Work is submitted only once nothing holds it
back, which is where the statement leaves approval (27).
When the request is out of draft and every condition holds, the driver submits its head, and the entry joins the branch’s one chain (8). From then on it is an entry like any other: tested with everything ahead of it (10), landed in order (22), or removed as culprit (17) or conflict (12).
flowchart LR
open(["opened by submit, or by hand"]) --> wait
subgraph wait ["waits"]
direction TB
draft["draft"] --> ready["out of draft:<br/>reviews, QA, other checks"]
end
wait -- "every condition holds" --> queue
subgraph queue ["the queue"]
direction TB
cand["tested with every entry ahead"] --> pass{"verdict"}
end
pass -- "passed" --> landed(["landed: sluice pushes the branch"])
pass -- "culprit or conflict" --> wait
queue -- "a new commit" --> wait
A request never waits inside the queue. The chain lands in order, so an entry held for a person’s approval would hold every entry behind it, and a refusal would form every candidate behind it again. Holding the request until its conditions hold keeps everything in the queue ready to land the moment it passes.
What a waiting request learns early is its forecast (64). Each round forms the candidate the request would get if it were submitted now, behind everything in the queue, and posts the result on it. The result reads “merges without conflict behind two others”, or names the request it conflicts with. A forecast runs no check and moves nothing.
What lives where
The branch and the pull requests live on the forge. The queue, its candidates, their runs and their verdicts live where sluice runs. On one machine that is the machine’s own files (C.6), and only the branch is pushed. The forge never holds the queue’s state, so it never holds a second copy that could disagree with the records.
flowchart LR
subgraph forge ["the forge"]
main[("main")]
pr["pull requests:<br/>review, other systems' checks,<br/>sluice's posted state"]
end
subgraph host ["where sluice runs"]
records[("the records:<br/>queue, runs, verdicts")]
places["places: one per candidate"]
end
host -- "fetch main each round" --> main
host -- "push main, leased, to land" --> main
host -- "read conditions, post state" --> pr
places -- "checks run here" --> records
A request from submit to landed
An agent’s wt submit pushes its branch and opens the pull request.
A person can open one by hand instead, from any branch of the
repository; the next round finds it the same way. A request from a
fork is never submitted.
sequenceDiagram
participant A as agent (wt submit)
participant S as sluice (a round)
participant G as the forge
participant Q as QA and reviewers
A->>G: push the branch, open the pull request
S->>G: fetch main, list open requests
S->>G: post "waiting: 1 review, qa/approved" and the forecast
Q->>G: deploy, test, post qa/approved, approve the review
S->>G: fetch main, list open requests
Note over S: every condition holds: submit the head
Note over S: form main + entries ahead + this head, run the gate
S->>G: post "testing behind 2 entries"
Note over S: the candidate passed
S->>G: push main --force-with-lease=main:(formed on)
Note over G: head is in main: request merged
S->>G: post "landed"
S-->>A: landed
A waiting wt submit reports the same states the request shows, and
returns when its entry lands or is removed. An agent that should not
wait through a day of review submits with --no-wait and reads the
state later.
When the branch moves outside the queue
Only sluice’s identity may move the branch on the forge, by the forge’s own rules (1). While that identity is a person’s own account, that person’s direct pushes and merge-button clicks still get through, and sluice treats each one as the branch moving under its queue. The landing’s push names the revision the candidate was formed on, and the forge refuses it when the branch has moved. sluice reports the refusal and never retries it blind (24); the next round forms the chain again on the new revision (S8).
sequenceDiagram
participant S as sluice
participant G as the forge
participant P as a person
S->>G: fetch: main is at m1
Note over S: form m1+A, test it: passed
P->>G: push to main: m2
S->>G: push main --force-with-lease=main:m1
G-->>S: refused: main is at m2
Note over S: report the refusal, land nothing
S->>G: next round: fetch, main is at m2
Note over S: form m2+A, test it again
When sluice lands entries exactly as they were tested, the entries behind them keep their verdicts; only a move from outside sends them back to be tested.
The policy
The branch’s policy, read from the branch’s own revision (56), names the remote the branch lives on and the conditions a request meets before it is submitted:
[branches.main]
remote = "origin" # fetched every round; landed by a push
[branches.main.pull_requests]
reviews = 1 # approving reviews on the current head
checks = ["qa/approved"] # checks other systems post on the head
The conditions decide when work is submitted. They never change how it
is tested: the gate is still the checks sluice runs (27). A branch with
no remote stays a local branch, as a bolt’s branch does.
Beside a forge’s job service
A forge’s job service, GitHub Actions for one, can also be the place sluice’s checks and rounds run: the forge as the pool (Many hosts). That deployment uses the driver and the branch above as they stand and changes only where the work happens.
flowchart TB
subgraph general ["pull requests, on any deployment"]
direction LR
g1["the forge holds main and the requests"]
g2["sluice runs on machines you run:<br/>rounds, checks, records"]
g1 <--> g2
end
subgraph pool ["the forge as the pool"]
direction LR
p1["the forge holds main, the requests,<br/>and the records under refs/sluice/"]
p2["the forge's jobs run<br/>each round and each check"]
p1 <--> p2
end
general -.-|same driver, same branch rows| pool
Many hosts
The design below is under review in design/sluice/proposals/ many-hosts-2.md as proposed 72 to 88, and is not yet built.
A deployment is one machine or a fleet, with no hybrid. On one machine
a waiting submit runs the rounds itself. On a fleet every round is run
by a sluice run and a waiting submit only waits. What follows says
which instance advances a branch, which runs a candidate’s checks, and
how a result proves who made it.
What every deployment shares
Entries of one branch are ordered with each other whichever host submitted them (57). At most one instance advances a branch at any moment, and one that has stopped writes nothing more for it (58). An instance that stops loses nothing: entries submitted meanwhile are held, and whatever next advances the branch resumes from the records (59). A candidate’s checks may run on any host, and a run whose executor stopped before reporting has no outcome and is run again (60).
The lead
An instance keeps a branch only by writing its records. Once the deployment’s lease has passed with no write of that instance, any instance may take the branch. Taking it is a write, after which the former lead writes nothing more for the branch (72). The former lead’s next write is refused because the version moved under it (62), and a landing is itself a move from an expected revision (46), so two leads never both land.
Taking a branch needs no knowledge of the former lead. One that stopped and one that was quiet for a lease are treated alike, and the lease is measured by the taker’s own observation of the records, never by the hosts’ clocks agreeing (73). A lead that is alive and makes no progress is taken over, which is the behaviour wanted.
The lease on a branch and the bound on an executor’s sign of life are the deployment’s settings, one value each for every branch it serves, never a branch’s policy’s. A branch’s policy bounds only how long a candidate’s checks may run, and a claim never lapses for the length of its run alone (74).
Claims
A candidate’s checks are taken only by a claim written to the branch’s records, by any instance with room under its own bound (71). The lead opens candidates within the window and assigns them to no one (75). An instance claims the earliest open run first. It never claims a later open candidate of a chain while an earlier open candidate of that chain is unclaimed (76), since the front of the chain is what blocks landing (22).
A claim lapses when its holder has shown no sign of life for the deployment’s bound. A lapsed claim’s run has no outcome, and a report under a claim its reporter no longer holds is never written (77). An instance takes no claim while a place it made on its host belongs to a candidate that has a verdict, is obsolete, or is under a claim it no longer holds (81). A place on a host that never returns is the operator’s to remove, and no verdict is ever read from it.
From the moment an entry is written until it leaves sluice, its head is fetchable by every host that may run its checks or advance its branch (82). A candidate is open to claims only while its revision is fetchable by every host that may claim it. When more than one host submits to a branch, every state an entry has been in is readable from each of those hosts while the host that last wrote the records is unreachable (80).
Vouching
When every host can write the records, a record’s executor field proves nothing. A run counts as an executor’s only when an identity the branch’s policy names vouches for it, in a way every host verifies from the records alone (78). A run vouched for by nobody the policy names is from an executor the gate does not trust (28). A policy that counts runs nobody vouches for says so for that branch, by name. Confinement is never inferred: a run is read as confined only when the identity vouching for it is one the policy names as confined (79). That the identity is reachable by no session is the policy author’s to assert. The shared branch’s default names only executors on hosts that run no sessions; a bolt’s branch by default counts runs nobody vouches for (C.7).
Under the remote refs profile, each host’s sluice user holds an SSH
signing key, a run’s report is signed with it, and the policy names the
key beside the executor it trusts.
The system a run needs
A check’s key names the system its run executes on, so one check on two systems is two keys (85). A branch’s gate names, for each check it requires, the systems a pass must come from (86). A candidate’s verdict is read over every required check on every system named, and a pass on one system never stands for another. A gate that names no system names the system of the instance advancing the branch. A run is taken only by an executor that is the system the run requires (87). A run whose system no live executor is stays open until the candidate’s time limit reads the candidate as not passed (66).
The lead expands the gate into runs, one per required check per required system per candidate. That expansion is the matrix, and it is sluice’s under every deployment. An executor is standing, taking runs by its own claims, or launched by the instance that opened the run (88). A launched executor’s start is its claim and its end its report, and an executor its pool loses is a lapsed claim. Which machine in a pool takes a launched run is the pool’s.
policy (in the repository) lead (one per branch)
main requires forms candidate 7, then opens
#check-fast on its runs:
aarch64-darwin 7 · fast · aarch64-darwin
x86_64-linux 7 · fast · x86_64-linux
aarch64-linux 7 · fast · aarch64-linux
#check-e2e on 7 · e2e · aarch64-darwin
aarch64-darwin and waits for every one
STANDING: runs are pulled LAUNCHED: runs are pushed
lead writes "7·fast·x86_64-linux lead starts one job per run
is open" to the records on a pool, with the system
│ as the job's label
▼ │
┌───────────────┐ ┌───────────────┐ ┌───────┴───────────────┐
│ host mac-1 │ │ host build-7 │ │ pool │
│ aarch64-darwin│ │ x86_64-linux │ │ job: 7·fast·x86_64… │
│ sluice run │ │ sluice run │ │ job: 7·fast·aarch64… │
│ claims only │ │ claims only │ │ job: 7·e2e·aarch64-d… │
│ darwin runs │ │ linux runs │ │ each: checkout, run, │
└───────┬───────┘ └───────┬───────┘ │ write result, exit │
│ │ └───────────┬───────────┘
└── result written to the records ◄───────────┘
│
candidate 7 has every run's outcome → verdict → land
A waiting submit returns when its candidate has a verdict, which needs
every run on every system. Standing costs idle hosts; launched costs a
job’s start and a pool to drive. A fleet may mix them, with the Darwin
runs claimed by a standing Mac and the Linux runs launched on a pool.
Containers do not change the CPU: a Linux container on a Mac is
aarch64-linux.
The deployments
Each deployment is a choice of profiles and no change to Parts A and B (C.8).
| peers | a server with executor hosts | a central server | a forge as the pool | |
|---|---|---|---|---|
| who advances a branch | whichever instance holds it (72); another takes it after a lease (73) | the server, always | the server, always | a launched stepper, one round per job, one at a time |
| where candidates run | any instance with room, by a claim (75, 76) | the server and the executor hosts, by a claim through the projection | the server’s executors | the forge’s runners, one job per run, labelled by system (87, 88) |
| a host stops | its lead and claims pass to others (72, 77) | an executor host: its claims pass; the server: every branch stalls and loses nothing (59) | a session’s host: nothing changes; the server: every branch stalls and loses nothing (59) | a lost job is a lapsed claim (77) |
| confinement | by vouching (78, 79) | by vouching; the executor hosts run no sessions | by construction: one writer, no sessions | a fresh machine per job, vouched for by the forge’s record of it |
| who holds the branch’s credential | every host’s sluice user | the server | the server | an app installed on the repository, holding no state |
| what is new to run | sluice run on each host | sluice run on the server and each executor host | sluice run on the server; nothing elsewhere | nothing: two workflows the repository carries |
Peers. The records are refs on the shared remote, and every host
runs sluice run as a sluice user holding a deploy key that bypasses
branch protection and a signing key that vouches for its runs. A submit
from any host pushes the head and adds the entry to the state ref with a
conditional push, retrying from the new state when refused. On a hosted
forge a branch sustains about 45 submissions an hour before retries
hurt, and several hundred on a local git server. The remote unreachable
stalls everything, submission included.
The central server. One host runs sluice run and leads every
branch. A session’s host runs nothing of sluice but wt submit --remote. It pushes the head to the server’s repository over SSH, runs
the submission there over SSH, and reads the entry’s state the same way
until the outcome. The records are the server’s files, projected to
the remote after every write, so state stays readable with the server
down and a lost server is rebuilt from the remote. Executor hosts that
run no sessions stand behind the server and claim through the
projection; the server’s own runs need no signature, since it is the
only writer.
A forge as the pool. The launched shape, with a forge’s job service
as the pool. The stepper is a launched job too: one sluice step per
job, started by a submit, by a check’s completion and by a schedule,
under a concurrency group so one round runs at a time. The pull request
is the driver. An app installed on the repository holds the bypass and
posts the status on each head. The queue’s whole state lives under
refs/sluice/ in the repository, with nothing behind a vendor’s server.
A round costs a job’s start, so the queue moves in half-minute steps.
That suits a shared branch on a large project and not a bolt’s branch,
which stays on the agents’ machines.
The order of building
The server comes first, in the peers’ form: its records projected in the peers’ layout, so a lost server is rebuilt from the remote and the server is already a peer. Executor hosts come next, claiming through the projection behind the lead. A lead anywhere, which is peers in full, comes only if a stalled server hurts more than the second credential on every host costs.
Glossary
The words of the design, each defined once. The statement’s vocabulary
section is the binding copy. This page adds the words the many hosts
design brings. Beside each is the word a user sees in sluice’s own
output and pages, where the two differ.
| design word | meaning | what a user sees |
|---|---|---|
| branch | a git branch that lands work: a repository’s shared branch, or a bolt’s branch off it | branch |
| place | a working copy prepared for one session off one branch, or made by sluice for one candidate | checkout |
| head | the revision a place holds when its session submits | head |
| submission | one head offered to one branch, with what it must land after, its priority and who submitted it | submission |
| entry | a submission as sluice holds it, with its state | submission |
| after | the entries a submission names as having to land before it | after |
| work | an entry’s commits, from where its head left the branch to the head | commits |
| candidate | the branch’s current revision extended, in order, by an entry and every entry ahead of it: the revision that entry would land as | combination |
| chain | the candidates of one branch, each extending the one before | queue |
| window | how many candidates of a branch’s chain are under test at once | parallel |
| deepest first | testing only the longest candidate of a chain, and the shorter ones only when it fails | batch |
| conflict | a candidate that cannot be formed because its entry and the candidate before it change the same content incompatibly | conflict |
| check | a task run to judge a revision, ending in a pass or a fail | test, task |
| gate | for a branch, the checks it requires, the executors it trusts, and how a flaky key reads | the branch’s rules |
| key | the identity of one check over the inputs it reads, the system it ran on among them | hash |
| system | the processor architecture and operating system a run executes on, named as the toolchain names it | system |
| verdict | a gate’s reading of a candidate: for each required check exactly one of passed, failed, missing or flaky | result |
| culprit | the one entry a failed candidate is attributed to | the blamed submission |
| landing | the branch moving to a candidate that passed | landing |
| executor | what ran a check, with its identity and its host; confined when nothing of any session could reach the run or its records, open otherwise | executor |
| host | a machine on which sessions, sluice or executors run | host |
| preview | a place sluice makes at a revision a branch has landed, in which the policy’s preview command has brought up what a person opens, held until its caller asks for its release | preview |
| instance | one running sluice on one host | sluice |
| lead | the instance advancing a branch | the host running the queue |
| lease | how long an instance keeps a branch without writing its records | lease |
| claim | an instance’s recorded hold on a candidate’s checks, from taking them to reporting them | a test run taken by a host |
| records | everything sluice holds about a branch: its entries, its log of runs and verdicts, and a version | state |
| round | one pass of sluice’s work on a branch: read the records, start runs with room, read finished runs, blame, land, write | round |
| driver | what submits work to sluice and carries the outcome back: worktrunk, crew, flywheel, a remote submit, a pull request | wt submit |
| profile | one way a contract of Part B is satisfied, as Part C describes | profile |
| seam | one of the five contracts of Part B through which the core reaches everything that is not git | seam |
Open questions
What the design does not settle, and where each question is held. A question leaves this page when the operator rules; the ruling lands in the statement and the chapter that rests on it is rewritten whole.
Held by the statement
Section 10 of the statement lists the questions that are the statement’s own, with the scenarios each would have to satisfy.
Held by the many hosts model
design/sluice/models/many-hosts-1.md, section 11, holds the questions
the fleet design leaves open, with the model’s lean on each:
- The lease and the sign-of-life grace as deployment settings, and their defaults.
- Whether a bolt’s branch is ever served by a host its sessions do not run on.
- Where the per-repository results log lives under peers: its own ref with its own lease, or each branch’s log under its own ref, read across refs before a candidate is formed.
- Plain words for what a user sees of the lead, a claim and an executor.
- Who starts
sluice runon each host, and what documents that it must be up everywhere under peers.
Awaiting a ruling
design/sluice/proposals/many-hosts-2.md drafts 72 to 82 and
85 to 88 with the glossary entries and Part C rows they need. Until
they are ratified, the many hosts chapter describes a proposal, and
the statement stands as written.
Preamble, purpose, actors and vocabulary
A statement of what sluice must always do and never do, written to admit any model that satisfies it.
The statement is in three parts. Part A is what must hold of sluice on its own: how work is submitted to a branch, ordered, tested in the combination it will land in, attributed when it fails, and landed. Part B is its contracts: the operations it needs across its five seams, a place, a check, a branch, a driver and the records, and the guarantee each must give. Part C is the profiles: the ways each contract is satisfied.
Parts A and B name no mechanism: no command, file, format, field, service or tool. One thing of the world they name freely, because section 9 fixes it: git. Every other real tool is named only in section 9 and in Part C. A reader who has never seen a merge queue should be able to design from Parts A and B and git alone.
1. Purpose
sluice admits finished work onto a branch only after the branch, extended by that work and by everything ahead of it, has passed the branch’s gate. It orders what is submitted, forms each candidate revision exactly as it would land, tests candidates ahead of need, names the one submission at fault when a candidate fails, and moves the branch only to a revision that passed. It never runs a check of its own invention, never starts a service, never chooses a port, and never decides what a gate requires. A session that built the work submits it and does nothing more.
2. Actors
| actor | may decide | may not |
|---|---|---|
| a session (an agent or a person building in a place) | when its work is ready, and to submit it; how to resolve a conflict or a failure in its own place | merge, land, or move a branch; change another submission’s order; withdraw another’s submission |
| a planner (a plan, or a person) | what a submission lands after, and its priority; to ask sluice to make or remove a branch it lands on (104, 105) | override a gate; move a branch |
| a policy’s author (a repository’s owners, a platform) | which checks gate each branch, which executors each branch trusts, how a flaky result reads, how deep a chain is tested, how long a candidate’s checks may run, what a run may use and reach | make a branch land untested; widen trust a layer above set |
| the operator | everything a policy does not settle | make a landing that skipped the gate count as one |
| sluice | the order among submissions within what the planner and the policy fixed; which candidates to test and how many at once; when a branch moves | run a check of its own, start a service, choose a port, resolve a conflict, decide trust, release a session’s place, move a branch to a revision that has not passed its gate, or remove a branch whose work has not landed into another (105) |
| an executor (a session’s shell, a confined run) | nothing; it runs checks and reports what it observed | report an outcome it did not observe |
3. Vocabulary
Terms are defined by meaning. A model may add terms; it may not redefine these.
- branch: a git branch that lands work: a repository’s shared branch, or a bolt’s branch off it.
- place: a working copy prepared for one session off one branch.
- head: the revision a place holds when its session submits.
- submission: one head offered to one branch, with what it must land after, its priority and who submitted it.
- entry: a submission as sluice holds it, with its state.
- after: the entries a submission names as having to land before it.
- work: an entry’s commits, from where its head left the branch to the head.
- candidate: a revision equal to the branch’s current revision extended, in order, by an entry and every entry ahead of it: the revision that entry would land as.
- chain: the candidates of one branch, each extending the one before.
- window: how many candidates of a branch’s chain are under test at once.
- conflict: a candidate that cannot be formed because its entry and the candidate before it change the same content incompatibly.
- check: a task run to judge a revision, ending in a pass or a fail.
- gate: for a branch, the checks it requires, the executors it trusts, and how a flaky key reads.
- key: the identity of one check over the inputs it reads.
- verdict: a gate’s reading of a candidate: for each required check exactly one of passed, failed, missing or flaky.
- culprit: the one entry a failed candidate is attributed to.
- landing: the branch moving to a candidate that passed.
- executor: what ran a check, with its identity and its host; confined when it ran in a place holding exactly the candidate and what the repository declares, which nothing of any session could reach, and when nothing but it could have written its records; open otherwise.
- host: a machine on which sessions, sluice or executors run.
- profile: one way a contract of Part B is satisfied, as Part C describes.
Part A
Part A. What must hold of sluice
A.1 Submission
- A session submits a head to a branch and does nothing more: it never merges, never lands and never moves a branch, and an attempt to is refused and reported.
- A head is submitted only from a place whose tree is exactly its commits. The driver refuses a place holding anything more, and names what it holds; sluice is only ever given a head.
- A submission names what it lands after, its priority and who submitted it. It never names a position.
- An entry leaves sluice only by landing, by withdrawal by its submitter, its planner or the operator, or by a bound the branch’s policy states in advance. sluice never drops an entry on its own.
- A submission from a place that has an entry on the same branch withdraws that entry, which stays readable as withdrawn.
- sluice never bounds how often a session submits again after its entry was removed as culprit or conflict. An entry’s state shows how many times its place’s work was submitted before it, and acting on that count is the driver’s or the planner’s.
A.2 Order
- Entries of one branch are ordered by priority, then by the order of submission. Priority orders only entries in no candidate under test: an entry under test is never displaced by one submitted after it. An entry never lands before an entry it names as after.
- An entry whose after-entries are neither landed nor ahead of it in its branch’s chain waits, and its waiting holds no other entry.
- Every entry of a branch is in its one chain. No entry is tested apart from the entries ahead of it, and entries land in chain order.
A.3 Candidates and chains
- Every candidate equals the branch’s current revision extended, in order, by exactly the entries ahead of it and itself. A candidate never contains an entry behind it or an entry removed from the chain.
- An entry is judged only as a candidate: never alone against a revision the branch has left, and never in a combination it would not land in.
- A conflict is found when the candidate is formed, before any check runs. It removes the entry from the chain and nothing else: the chain continues without it.
- sluice forms, for a head not yet submitted, the candidate it would form were the head submitted now, against the branch and the entries ahead of it, and reports a conflict so found to the head’s session. A forecast creates no entry, changes no order and moves no branch, and its result is never read as a verdict.
- A conflict reported to a session, forecast or found at forming, names the content contested and every entry it is contested with, with what each of those entries submitted: each entry ahead the head conflicts with alone, or, when none does, every entry ahead up to the one at which the conflict appeared.
- A branch is tested up to its window. The window never falls below one, grows while candidates pass and shrinks when one fails, within bounds the branch’s policy states.
- An instance of sluice starts no more checks at once than its own bound, whatever the windows of the branches it serves. A host whose checks are all started by one instance runs no more than that instance’s bound.
- A branch’s policy says whether every candidate of a chain is tested or the deepest first. Under deepest first, a failure is attributed only after the candidates that were skipped have verdicts.
A.4 Verdicts and attribution
- A candidate’s verdict is its branch’s gate read over the checks the candidate affects. A required check whose key already holds a pass from an executor the gate trusts is read from that pass and not run again.
- Every check run on a candidate is recorded with its key, its outcome, pass and fail alike, and its executor, before the verdict is read. A result a tool answered from an earlier run is not a run and is never recorded as one.
- A failed candidate is attributed to exactly one entry: the newest entry of the first failed candidate in its chain.
- No entry is attributed or landed before every candidate ahead of it in its chain has a verdict. A conflict removes its entry when it is found (12).
- A check’s result belongs to its key, not to a branch: a pass recorded under a key satisfies every branch whose gate trusts the executor that recorded it.
- When the branch’s revision is covered by no verdict, every check its gate requires is affected by the next candidate.
- A key holding both a pass and a fail from executors the gate trusts, each held to the same limits, reads as the branch’s policy says: passed or not passed. It never reads differently for two candidates of one branch. A result under other limits says nothing of a run under these (83).
- The culprit’s session is told the failing checks and the candidate they failed on. No other session is told of that failure.
- An entry removed as culprit or as conflict leaves the chain, and every candidate behind it is formed again without it.
A.5 Landing
- A branch moves only to a candidate whose verdict is satisfied, only in chain order, never past an entry without a verdict, and never to a candidate containing a culprit or a conflict.
- A landing writes one commit per entry, with the revision before it as its first parent and the entry’s head as its second, naming the entry’s work and the verdict it landed on; entries landed together on the deepest candidate’s verdict each name that verdict. The history of a branch is never rewritten, and a session’s commits stay reachable from the branch they landed on.
- A landing moves the branch from exactly the revision its candidate was formed on. A landing that finds the branch elsewhere is refused, and the chain is formed again on the revision found.
- sluice releases every place it made for a candidate once that candidate’s verdict is read, and never releases a session’s place. A session learns its entry landed (50), and releasing its place is its driver’s.
- Every state an entry is in, and every verdict read for it, is readable by its session, its planner and the operator, from every host that submits to its branch, whether or not sluice is running.
A.6 Gates and trust
- A branch’s gate is its policy’s: the checks it requires, the executors it trusts, how a flaky key reads, whether every candidate or the deepest first is tested, the window’s bounds, and how long a candidate’s checks may run. sluice gives no gate a meaning of its own, and admits every submission a driver makes: whether work may be submitted at all is decided before it reaches sluice.
- A result from an executor the branch’s gate does not trust never satisfies that gate, whatever its outcome.
- A gate that trusts only confined executors is satisfied by no open run.
- sluice never runs a check of its own and never changes what a gate requires.
- A check’s cost never changes how it gates: every check a branch’s gate requires has a verdict before the branch moves, and none is deferred past a landing.
- A branch’s gate is read from the branch’s current revision, never from a candidate or a place. No submission changes the gate it is judged by.
A.7 Places
- sluice starts no service. What a candidate’s checks need, the checks bring up themselves, in the candidate’s place. sluice gives the place a port for each name its branch’s policy lists. Each stays the same for the place’s whole life, belongs to no other place alive on the host, and is free when the checks receive it.
- A candidate’s place belongs to the candidate: nothing started in it outlives it, what its checks hand to another process to keep included, and two places on one host never collide.
- A candidate’s checks run under the time limit its branch’s policy states. A run that exceeds it is ended with everything it started, recorded as timed out, and read as failed.
A.8 Hosts
- Every entry of one branch is ordered with every other entry of that branch, whichever host submitted it.
- At most one instance of sluice advances a branch at any moment, whichever host it runs on, and one that has stopped advancing a branch writes nothing more for it.
- An instance that stops advancing a branch loses nothing: every entry submitted meanwhile is held, and whatever next advances the branch resumes from the records as last written.
- A candidate’s checks may run on any host. A run whose executor stopped before reporting has no outcome: it is never read as a pass or a fail, and the check is run again.
A.9 Branches
- The operator or a planner may ask sluice to make a branch it will land on, from another branch it lands on. sluice makes it at the other branch’s current revision, by a move from absent (45), when the policy at that revision gives the new branch a gate (56) and the new branch’s records hold no entry still waiting, under test or landing. A branch already present is refused, and so is one that policy does not name.
- The operator or a planner may ask sluice to remove a branch it lands on, naming the branch the removed branch’s work landed into. sluice removes it by a move to absent (45) from the revision it read, only when no entry on it is waiting, under test or landing, no landing on it is left unrecorded (S9), and that revision is contained in the named branch’s current revision. Its records stay.
- Making and removing a branch is not landing: neither writes a commit, records a verdict or reads a gate’s checks, and 22 to 24 bind neither. A refused one is reported, never retried blind. A branch sluice lands on is made and removed only as 104 and 105 say, and the branch’s protection (1) refuses anyone else’s.
Part B
Part B. Contracts across the seams
B.1 The place
- materialize: given a revision, a place holding exactly that revision, with the environment the repository declares and the ports its policy names (31).
- release: given a place sluice made, its removal, with everything started in it ended, what its checks handed to another process to keep included (25, 32).
- The guarantee: a place holds what it was given and nothing of any other place, except the caches its branch’s policy declares, each keyed by the content of the files it is built from and saved only from a run that passed. A cache changes how fast a check runs, never what it reports.
B.2 The check
- affected: given the branch’s revision and a candidate, the checks the candidate can change the result of (15, 70).
- run: given a place and checks, for each check its key, its outcome, its executor and whether the outcome was observed in this run or read from an earlier one (16).
- The guarantee: an outcome is what the executor observed, never inferred; a replay says it is a replay.
- execute: given a place, a command, and the limits a run is held to (the cores its instance gives each run; the memory, the network and the paths it may write that the branch’s policy states), the command run within them, and what the executor enforced: whether the run was confined, and each limit it held.
- The guarantee: an executor that cannot enforce a limit the branch’s policy states refuses the run rather than run it without the limit, and every run is recorded with what its executor enforced (16), so a run is never read as confined, or as held to a limit, unless its executor enforced it (28, 29).
B.4 The branch
- read: given a branch, its current revision.
- form: given a revision and a head, the candidate revision, or the conflict with the content it is on (12).
- commit: given a candidate, its parent, the entry’s head, the entry’s work and the verdict read for it, one commit (23).
- move: given a branch, the revision it is expected at or that it is absent, and the revision to move to or its removal, the move or a refusal because the branch was elsewhere (24, 104, 105).
- The guarantee: a move is all or nothing, and two moves of one branch from one expected revision never both succeed.
B.5 The driver
- submit: given a head, a branch, the after-entries, a priority, a submitter, an entry (1, 3). 47a. forecast: given a head, a branch and its after-entries, the conflicts the head would meet if submitted now, placed after every entry already submitted, each with the entries it meets (64, 65). 47b. amend: given a queued entry and who asks, its after-entries or priority changed (3, 6).
- withdraw: given an entry and who asks, its removal where 5 allows it.
- state: given an entry, its state and the verdicts read for it (26).
- notify: given an entry’s session and an outcome, the outcome delivered to that session and no other (20).
- The guarantee: a notification reaches the submitter at least once and names its entry and outcome, so a repeat is known as one, and a state read with sluice stopped is the state sluice last wrote.
B.6 Binding the contracts
- Each seam is bound by naming one profile of Part C, and Parts A and B do not change to admit a profile.
- An entry’s records and a candidate’s verdicts have one form whatever profile wrote them, so a reader of one profile’s records reads another’s.
B.7 The records
- read: given a branch, its records as last written, and the version they were read at.
- write: given a branch, the version its records were read at and the records to write, the write, or a refusal because they have been written since (58).
- The guarantee: a write is all or nothing, two writes from one version never both succeed, and every host reads what the last write left (26, 59).
Part C
Part C. Profiles
Parts A and B do not change to admit a profile.
C.1 The place
| profile | materialize | release |
|---|---|---|
| git worktree: a detached worktree, nothing run in it | git worktree add --detach at the candidate; a block of ports held for the place on the host, each checked free | the checks’ process group ended; the policy’s release command run in the place under the checks’ executor, removing what the checks handed to another process, a container engine’s containers above all; git worktree remove; the ports let go |
| container: a clean checkout of the candidate in a container on the executing host | the candidate fetched and checked out into a new container’s filesystem, with the environment the repository declares | the container and everything started in it removed |
C.2 The check
| profile | affected | run |
|---|---|---|
| moon: the repository’s task runner with declared inputs and a content-keyed cache | moon’s affected projects between the branch’s revision and the candidate, downstream dependents included | moon’s run of the affected check tasks in the place; the run report gives each task’s outcome and whether it was replayed, the run record gives its hash as the key and its exit code |
| script: one command with an exit code | every check, always | the command’s exit code, never a replay |
| executor profile | execute | enforces |
|---|---|---|
| open: a process group on the instance’s host | the command in the place, as one process group ended with its run | the cores given, by the runner’s own threads; nothing else; never confined |
| sandbox: the host’s kernel sandbox, Seatbelt on macOS, bubblewrap and Landlock on Linux | the command in the place under a profile that allows writes only to the place and its scratch, the network only as the policy says, and a container engine’s socket only when the policy allows it | paths and network everywhere; cores and memory only on Linux, by a cgroup the run is started in; confined when no session on the host can write the place and the run cannot reach a container engine, whose containers may mount any path |
| container: a container engine on the executing host | the place mounted into a new container from the repository’s image | cores, memory, paths and network; confined as C.8 says |
C.4 The branch
| profile | read / form / commit / move |
|---|---|
| local refs: one host’s repository | git rev-parse; git merge-tree --write-tree; git commit-tree with the branch and the head as parents and the verdict as Sluice-* trailers; git update-ref with the expected old value; making a branch git update-ref <branch> <rev> "", removing one git update-ref -d <branch> <expected> (104, 105); branch protection by a reference-transaction hook that refuses any update to a branch in the policy not made by sluice, and logs it (1) |
| pushed refs: a branch on a remote every host reaches | git fetch then git rev-parse of the remote’s branch; forming and committing as local refs, on the host advancing the branch; the landing a push with --force-with-lease=<branch>:<expected>; making a branch a push leased on its absence, --force-with-lease=<branch>:, removing one a --delete leased on its revision (104, 105); branch protection by the forge’s rules, restricting creating, updating and deleting the branch, which only sluice’s identity bypasses (1) |
C.5 The driver
| profile | submit / withdraw / state / notify / forecast |
|---|---|
| worktrunk: a custom subcommand on the path | wt submit, the executable wt-submit on the path, waiting for the entry’s outcome, which it reports as its exit, or with --no-wait returning the entry at once; wt submit --withdraw; wt submit --status; the outcome of a waiting submit is its exit, of any other the session’s pane prompted through the multiplexer; on landing, worktrunk removes the session’s place; wt submit --check, the same forming as a submission with no entry written |
| crew: a bolt team’s stages | the unit’s merge stage submits, and a bolt is submitted from its own place by its conductor; sluice amend sets an After the conductor adds; the conductor reads the entry’s state as the stage; a red or a conflict re-charges the unit’s slot; the conductor’s wait runs a forecast for each unit in code and tells the unit’s agent of a conflict when it is idle |
| flywheel: the machinery’s effects | a session’s done exit, once approved, is the submission; the entry’s state is evidence; a forecast is evidence on the item, read each tick |
branch: sluice branch | not a session’s: sluice branch create <branch> --from <branch> makes a branch at the other’s current revision and prints it (104); sluice branch delete <branch> --into <branch> removes it, main when --into is omitted (105); each exits 0 when done, 1 when a branch it names does not exist, and 2 on a refusal, naming it |
| remote submit: a session on any host | the head pushed to the remote and the submission written through the records (peers) or sent to the server (central server); withdrawal the same; state read through the records or from the server; a waiting wt submit polls it until the outcome; a forecast through the records or from the server |
C.6 The records
Whatever binds them, the records are one JSON document per entry and one JSON Lines log of check runs and verdicts per branch (53), and what outlives them is the trailers each landing commit carries (23).
| profile | read / write |
|---|---|
| local files: one host | the documents under the repository’s common git directory, one directory per branch and one log of check results per repository (69), readable by every place on the host with no process running, never tracked and never pushed, a version kept beside them; a write under the branch’s lock, refused when the version moved |
| remote refs: peers | the records as one tree committed under refs/sluice/<branch>/records on the shared remote; a write is a push with --force-with-lease from the commit read |
| server: a central server | the server’s local files; other hosts read and write only by asking the server |
C.7 Policy
A branch’s policy is a TOML file checked into the repository, one table per pattern of branch names, read from the branch’s revision (56).
C.8 Deployments across hosts
Two ways of running sluice on many hosts, each a choice of profiles and neither a change to Parts A and B.
| peers | a central server | |
|---|---|---|
| profiles | container places on any host; pushed refs; remote submit; remote refs records | container places on the server; pushed refs; remote submit; server records |
| who advances a branch | whichever host’s sluice wins the write (58, 62); another takes over when it stops | the server, always |
| where candidates run | any host with room; an executor claims a candidate by a write to the records | the server’s executors |
| a host stops | its claims and its lead pass to others (59, 60) | a session’s host: nothing changes; the server: every branch stalls and loses nothing (59) |
| confinement | a container on a host that runs sessions is confined only if no session there can reach it | the server runs no sessions, so its executors are confined |
| what is new to run | nothing but sluice on each host | a server process the operator starts, whose listener belongs to its driver profile, never to the core |
The task runner’s cache makes either cheaper and neither depends on it.
Invariants, non-goals and givens
7. Global invariants
- I1. Nothing lands untested in the combination it lands in (10, 11, 22).
- I2. A session never moves a branch (1).
- I3. Every failure has exactly one culprit, named only once everything ahead of it is known (17, 18).
- I4. A branch never moves past what passed, and never from a revision other than the one its candidate was formed on (22, 24).
- I5. sluice runs no check, starts no service and chooses no port (30, 31).
8. Non-goals
- Running checks, caching their outputs, or hosting an executor.
- Describing a repository: its parts, what a change affects, what a check reads. That is the repository’s description, read through B.2.
- Deciding policy: which checks gate a branch, whom to trust, how flaky reads.
- Approving or reviewing work. A verdict is evidence for a person or a platform, never an approval.
- Submissions that span repositories.
- The forge’s own pull-request flow for contributors outside the fleet.
9. Environment givens
Constraints already fixed:
- Repositories are git repositories, and places are git worktrees.
- Branches are named by convention, so a target is known from its name: the shared branch, a bolt’s branch, a unit’s branch, a fix’s branch.
- sluice is free and open source.
- sluice is written in Rust.
The rest of the stack, which a model builds on unless it says why not:
- The task runner that declares inputs and caches by content is moon, with its cache shared by every place on a host.
- Places are managed by worktrunk, which runs a repository’s hooks and tethers processes to a place.
- The drivers are crew and flywheel; the forge is GitHub.
- Records are plain JSON, kept on one host under the repository’s common git directory until several hosts submit to one branch.
- A branch’s policy is a file checked into the repository.
Questions, scenarios and what to deliver
10. Questions the model must answer
These are not requirements.
- When a branch’s stepper stops, how long before another may advance it, and what tells a peer the stepper has stopped rather than gone quiet?
- How does a peer decide which candidates to run, and how many?
- Does the shared branch’s gate trust a container on a host that also runs sessions, or only hosts that run none?
- How does a branch’s policy name the executors it trusts, and how does a run prove which executor made it, when under peers every host writes the records?
- Under a central server, is a state readable only on the server readable “whether or not sluice is running”?
- Who releases the candidate places of a host that has stopped for good?
- Who sets how long a host may hold a branch, or a claim on a candidate’s checks: the policy, the deployment or the operator?
11. Scenarios the model must satisfy
- S1. Two sessions submit heads to one bolt branch. Both candidates pass. Both land, in order, and neither session ran a merge.
- S2. The second head conflicts with the first. Its session is told the paths before any check runs; the first lands.
- S3. The first candidate passes and the second fails. The second’s session is told the failing checks; the first has landed; no other session hears of it.
- S4. The first candidate fails. Its session is told; the second head is formed again on the branch without it, passes, and lands first.
- S6. Two bolts whose work touches unrelated parts are submitted to the shared branch. The second is tested on top of the first and lands after it, and is not tested again when the first lands.
- S7. One key holds a pass and a fail from trusted executors. The bolt branch’s policy reads it passed; the shared branch’s reads it missing until a confined run settles it.
- S8. The branch is moved by hand between a candidate’s verdict and its landing. The landing is refused and the chain is formed again on the new revision.
- S9. The shared branch tests deepest first. Three bolts are tested as one candidate, which fails; the two skipped candidates are tested, and the culprit is the one entry the rule names.
- S10. sluice is stopped. Every entry’s state is still readable. Started again, it continues without running a check for any candidate that already has a verdict.
- S11. An entry lands. The branch holds a commit whose second parent is the session’s head and which names the verdict; sluice leaves the session’s place alone, and the session’s driver releases it.
- S12. A submission changes the branch’s policy to drop a check its candidate fails. The candidate is judged by the policy on the branch, fails, and the change to the policy lands only with a candidate that passes the gate as it stood.
- S13. Sessions on two hosts submit to one bolt’s branch. Their entries are tested in one order, and each session reads its entry’s state and is told its outcome on its own host.
- S14. The host advancing a branch stops mid-chain. Under peers another host continues from the records; under a central server nothing lands until it is back. In both, no entry is lost and no check that has a verdict is run again.
- S15. An executor stops while running a candidate’s checks. The run has no outcome; the checks run again on another executor, and the candidate’s verdict is read from that run.
- S16. Two sessions build heads that edit one file. The second asks for a forecast before it has finished; it is told the file and the first entry’s work, settles it, and both candidates form without conflict.
- S36. A planner asks for
bolt/xfrommainat M. sluice makesbolt/xat M. A second ask is refused becausebolt/xis present. On another host, an ask made at the same time finds the remote’s branch present and is refused (46). - S37. A planner asks to remove
bolt/xintomainwhile an entry is under test on it. The removal is refused, naming the entry. Once the bolt’s revision is landed onmain, the removal succeeds and the branch’s records still read. - S38. A removal reads
bolt/xat R. Another host lands onbolt/xbefore the removal reaches the remote. The removal is refused because the branch is elsewhere, and the landing stands. - S39. A planner asks to remove
bolt/xintomain, butbolt/x’s revision was never landed onmain. The removal is refused, saying so.
12. What to deliver
A model of sluice that satisfies Parts A and B: its components, the record of an entry and of a verdict, the chain and the window, the attribution rule, and the landing. A conformance suite whose scenarios cite the clauses they exercise. The profiles of Part C, each passing the suite.