Requirements
Product
- R1 Agent sessions of different people can find each other, send messages and claim work.
- R2 The core does not block any agent tool that supports MCP. Other tools can join later.
- R3 A feature of one agent tool is an optional adapter, never the core. Infrastructure is always Google.
- R4 A person starts a riff on one Linux machine with at most three commands, and with no sign-in.
- 01M3MN2R92DA7QPP80G1AENX4M The book page “Start a Riff” starts
with one question: did a person give you a riff address? Yes leads to
“Join a Riff”. No leads to one of two paths: “Just this machine”,
with no sign-in, or “Start a Team Riff”. The page names no
--insecure, no systemd and noriff-server install. - R203 A person adds a second machine to the riff of a first machine
on a network that they trust. The riff of the first machine takes
connections from the network. The second machine names that riff with
RIFF_SERVER. The riff of the first machine has sign-in with the OAuth client of the person, and the person signs in on each machine (01M3JZN229S3YA3BR6GN5H3MTY). - R166 A session picks an open work item of the current wave (R214) that no session holds. It takes work only from the current wave (R216). It never picks an item whose needs are open (R215). When the current wave has no free item, the session verifies, runs its checks after the release, or waits. It picks the item that it thinks is best. It does not wait for a plan or for permission. The order of the items in a wave does not matter. A scope from the user of the session wins. A scope message from another session is advice (R10).
- R172 Each issue has acceptance criteria: a
Done when:line. Each criterion names what to run or look at, and what the result must be. The criteria follow ASD-STE100. - 01M3PRHM97CJ16FZD9ZK48ERAD A session writes all prose in ASD-STE100: each message, status, issue, pull request, commit, release note, doc and answer to its user. The level line of a release uses plain words that a person understands.
- 01M58BCCKMNG6V495YF8AM9P5C A session uses natural tenses for
things in time: the future with “will” for a future action, and the
past for a past event. It never writes a future action in the
present tense.
CLAUDE.mdand the instructions of the lead say so. - R173 After a session claims an issue, it reads the issue. When the
Done when:line is missing or cannot be tested, the session does not start work. It writes the criteria, adds them to the issue, posts to the repository thread, releases the claim and picks a different item. - R174 A session that writes the criteria for an issue does not implement that issue in the same claim. The next session that claims the issue reviews the criteria.
- R188 A session never verifies its own work. Before the merge,
another session checks the work against the
Done when:line of the issue. - R189 When the author finishes, the checks of the repository
pass. The author pushes its branch, opens a pull request with
auto-merge on (01M3JFEXMPNFEV4HBZJQ15JD25), and posts a verify
request to the repository thread. The request names the issue, the
pull request and the commit. Its
tolist wakes the sessions of the repository. - R190 A verify request is free work (R166). The verifier claims
verify-ITEM, for exampleverify-issue-12, so that only one session verifies. It skips a request whose issue is closed, or whose commit has a result. - R191 The verifier checks out the commit in a worktree of its own and tests each criterion. It does not change the code.
- 01M5174ASF376JDNBQ4423TTAR A control shows that a test finds a fault: a test of the work with no fix, or a guard test with a fault in the code. The verifier never edits the code for a control, also not for a moment. It runs a control at the base commit of the pull request, in its verify worktree, and then checks out the commit of the pull request again. When the base commit cannot show the fault, it reads and runs a test of the guard on a sample, uses the control of the author on the issue, or checks by a read. The result says which way it used.
- R192 The verifier posts the result to the author with the
selector
claim=ITEM. A pass names each criterion and how the verifier checked it. A fail names each criterion that failed and the steps to see the failure. The verifier also puts the result on the pull request and sets the verify status of the commit (01M3JFEXPXRTXYHCV0WSKEK07M). Then the verifier releasesverify-ITEMand removes its verify worktree. - R193 The author never merges and never pushes to the default branch (01M3JFEXJG2D651PWA30DNRGWF). The forge merges the pull request after the checks and a pass on its head commit. An author that is not a worker keeps its claim and waits (01M3Z9N6NFNMW62JZ796RV4643), also when no session takes the request. It does not verify while it waits (01M3K0FZ5M08Z4YPSVKFADCAKC). On a fail or a conflict, the author pushes a fix or a rebase and sends a new request with the new commit. On a pass, the author waits for the merge, posts that it is done and releases the item.
- R194 A criterion that only the shared riff can test is a check
after the release. It does not stop a pass. The verifier names it in
the result. The pull
request links the issue so that the merge leaves it open, and the
issue stays open until that check passes. After the merge, the
session that does the steps after the merge adds a comment to the
issue:
Merged in #PR (COMMIT), and the check that is left. - 01M3Z9N6GQK0NYCMGQ66FW406V On a pass, when no session holds the
item, the verifier does the steps after the merge: it waits for the
merge, adds the
Merged incomment when a check after the release is left, posts the done note, and removes the worktree and the branch of the item on its machine. It releasesverify-ITEMafter the note. On a fail, it releases at once. The item is then free with its earlier work (01M3WFYEP1H3VPW8G90KQDE6FW): the next session that claims it reads the result, goes on from the branch, and sends a new verify request for the same pull request. “Pick up dropped work” in the instructions of the riff tools says so. - 01M3Z9N6NFNMW62JZ796RV4643 A session that is not a worker keeps its claim after its verify request, and does the steps after the merge itself (R193).
- 01M3Z9MY0CDBB1G749XBVMVV8X riff makes the state of the pull
request of an item from the status
riff/verifyof its head commit. The pull request is open, is not a draft, and names the issue (01M51BF5S2YKB8JG10DF6YVT42). With no status, it waits for a verify. Withsuccess, it waits for the merge. Withfailureorerror, the verify failed. - 01M3Z9N6SPWPSSCBEVDCKBESSV The answer to a granted claim of
issue-Nnames the open pull request of the item and the state of its verify (01M3Z9MY0CDBB1G749XBVMVV8X), fromriff claimand from theclaimtool. For a failed verify, the line names the commit and the URL of the result, and tells the session to go on from the branch. For a pull request that waits for a verify or for the merge, the line tells the session to release the item. riff asksghonly when the clone knows a pushed branch of the item, and waits at most 5 seconds. With nogh, the claim has no such line. A verify claim gets no such line. - 01M3Z9N70J4H79VJN4ZKKH3G6S
riff verifyposts the result to the holder of the issue (01M3NB6FYXXKX80VHEVA5CV6RY). When no session holds the issue in the repository, the post also wakes the lead of the user of the verifier in that repository. - 01M3W62QG36F9RD4SZ1X508T3A A verify result holds only the check
of the item against its
Done when:line. No public text on the forge holds a live security fault: a security fault in the code of the default branch, or in a server that runs. The public text is a verify result, the body of a pull request, a comment on a pull request, an issue, a comment on an issue and a commit message. A post to a thread holds no live security fault too. The session tells the fault to the lead withtell. The lead decides on a private advisory. - 01M3K0FZ5M08Z4YPSVKFADCAKC Only a session that holds no claim takes a verify request. A session that holds an item, also one that waits for its own verify, does not verify. The lead gives a verify request to a session with no claim. When none is free, the lead starts a worker for it.
- R71 A session talks to other sessions only through riff. It never uses the session tools of the agent tool to reach another session.
- R67 The context tells the session to read and then start the watch again each time the task ends.
- R68 The context tells the session to keep the watch that runs for it. It tells the session to start a watch only when none runs (R169).
- R171 A session starts the watch again as its next action after the task ends, also in the middle of a turn.
- 01M3MYQ299XKJE9X9FHWZ7JFM4 At a new start in a linked worktree where another live session works, the start context names that session and the main worktree. It tells the session not to follow the start routine, to claim nothing, to change no file there, and to ask its user, through the lead, to start it again in the main worktree.
- 01M3MYQ2BFKS3KJ8DWNWDJKWB9 At a new start in a linked worktree where no other live session works, the start context names the worktree and the main worktree, and points to “Pick up dropped work”. In the main worktree, the context has no worktree line.
- 01M3JN21YJSP7HM1JPS1TCFM9W The update of each machine pulls each
clone of the project with
git pull --ff-only. - 01M3XY2T2YEV7GT7DKJHSMMHYR In a linked worktree, riff also reads the local settings and the project settings of the main clone. The local settings come before the project settings.
- 01M3XY2SWEK0N8MC3MY4TMYTD3
RIFF_ON=1turns riff on for the processes that have it, also outside a repository.just devand the helper crateisolatedset it. A worker that such a process starts getsRIFF_ON=1. - 01M510NWEFSN0NWQH8KQ6FA7DC
RIFF_DYNAMIC=1letsriff initand the riff daemon take a riff that is not static. Only the helper crateisolatedsets it: in a test, a fakepodmanrunsriff relayon the host. - 01M3NJDSQ23FFRMH8ZD4GC57WY
riff --helplists the commands that people use under the headings Get started, Work in the riff, Pull requests, Lead and Members, in that order. Each command has one short line: a phrase of at most 60 characters with no period.riff help CMDshows the long text. The plumbing that the plugin runs (hook,mcp,statusline,watch,workers run) is hidden, andriff help CMDstill shows it. The help ofriffandriff-serverwraps at 80 columns and shows no value of an environment variable. - 01M3NT228WA11PGNWDJ0WP7PQD The help of
--serveron each command is one short line of at most 60 characters.riff help servershows the forms of a value and the default.riffwith no argument shows the help ofriff --help.riffwith options and no command says to name a command. Neither names a hidden command.
Waves
- R213 A wave is a numbered group of work items: Wave 1, Wave 2, and so on. The waves run in number order. The items of one wave run at the same time.
- R214 The current wave is the open wave with the lowest number. The next wave is the open wave after it. When a repository has no waves, each open item is in the current wave. An item that the lead keeps out of the waves is not free work.
- R215 An item names the items that it needs in a
Needs:line. An item is merged when it is closed, or when it has the commentMerged in #PR (COMMIT)(R194). An item is closed when it is merged and each check after the release passed. - R216 A wave is done when each of its items is closed. The order in a wave is in 01M58Q03ZBVVXAQ2WZ8EJ5FWS4. A riff with no shared server of its own code skips the release and the deploy. Then the lead ends the wave. No session starts an item of the next wave before the current wave is done.
- R217 The lead plans the waves. When a repository has the leads of more than one person, the people agree on one lead to plan them.
- R218 A person or a session can add a work item at any time, with no wave. A session that adds an item tells its lead. The lead looks for items with no wave when it starts and each time a riff line wakes it.
- R219 The lead puts each new item in a wave and writes its
Needs:line. Each item is in a later wave than each of its needs. No item blocks or breaks the other work of its wave. When a new item fits in no open wave, the lead makes a new wave. Its number is the last number plus one. - R220 The lead posts each change of the waves to the repository thread: the item, its wave, what it needs and what needs it. When a wave starts, the lead posts the current wave with its items, the next wave, and the conflicts between items.
- R221 When each item of the current wave is merged, the lead stops the workers, asks an admin to make a release and deploy it to the shared server, tells its user to update each machine, and tells the sessions to run their checks after the release. When each item is closed, the lead ends the wave.
- 01M3ZWRC6H0P7EF6CMECCYZ2RC
riff audit --wave WAVEchecks that a wave followed the rules, from the log of the riff and from the forge. It changes nothing. It checks seven rules: (1) each item of the wave has a claim, a pull request, a verify by another session, a merge and a release; (2) each verifier held no other claim and is not the author; (3) each worker had a clear between its last release and its next claim; (4) no claim while the riff or the repository was paused; (5) no claim of an item of a later wave or with an open need; (6) the lead held no claim of a work item; (7) each request came from the lead of the user of its sender. It prints each rule with pass, fail or not checked, and names the records by their position. A rule with nothing to check, and a check that the facts cannot prove, is not checked, with the reason. It exits with 1 when a rule fails. - 01M3ZWRCC46EHYKQ4Q2NZS9TEB Rule 1 of the audit takes a release of an item by its holder as early when it comes before the merge and before a verify result of the item. A release of a worker after its verify request is not early (01M3Z9N6AK6W9KCA1MN72X78B6). The span of a wave starts when it is the current wave: at its start, or at the end of the last wave before it. It ends at the end of the wave, or now. Rule 1 looks at the whole log; the rules 2 to 7 look at the records in the span. A wave whose earlier wave is open has no span, and the audit refuses it.
- 01M3ZWRCF5H95R4SYX09CWAJS8
riff auditchecks one wave. It has no span of time of its own. - 01M588N3R2VP12A8X4EASX0BJ4 After the checkpoint rule removed the
oldest chunks of the log,
POST /v1/loggives the records from the oldest kept chunk, with the position and the time of the oldest kept record.riff auditprints them. A check that needs a gone record is not checked, with the reason: the rules 2 to 7 when the span starts before the oldest kept record; rule 1 for the facts of the log of an item with no claim in the kept records; rule 1 for a release after a verify request by a session whose start is gone; rule 7 for a request from a user with no lead in the kept records; rule 2 and rule 3 for a claim of a session with no fresh start in the kept records; rule 4 when the kept records have no pause of the riff or of the repository; rule 6 for a claim of a user with no lead in the kept records. - 01M43GSGB9ZFHSG0Q83Y50FEGW A lead holds an item of its repository
with a reason, and frees it again:
riff plan hold ITEM REASON,riff plan free ITEM, and the toolsholdandfree. The server keeps each hold as the recorditem_held, with who held it and when, and each end asitem_freed. A hold of a held item replaces its reason. A hold names one item by its exact name. A hold is not a claim: it does not end a claim, and only a free ends it. A held item is no free work. The lead holds an item withhold, not with a claim. - 01M43GSGGY0QMB5D5EH92M6ZFP Only a lead of the repository thread,
the owner or an admin can hold and free an item. A worker gets
not_allowed, also a worker of the owner. A hold needs a reason of 1 to 200 characters. A free of an item with no hold makes no record: the trace is ano_changeline. - 01M43GSGPJ69TPWPA4935WR8RW A claim of a held item by a worker is
refused with the code
on_holdand the status 409. The reason names the lead, the time and the reason of the hold. Each other session gets the claim, and the reply has the same text as a warning. The checks of a claim run in this order:must_clear, the name of the item,paused, the caller holds the item already,on_hold,held. - 01M4A4YTNSJR0R1T9JNXPBSKHC The server keeps the plan of each
repository thread: the current wave, its items with their needs, and
the done needs. The command
planwrites the full plan as the recordplan_set, and the record replaces the plan of its thread. The commandplan_offwrites the recordplan_ended: the server forgets the plan, and the holds stay. Aplan_offwith no plan makes no record. - 01M4A4YTR2NKVBPE6BT9EC3X75 A
plannames itsbase: the position of theplan_setrecord of the plan that the client compared with, or none when the repository has no plan. Whenbaseis not that position on the server, the server refuses the command with the codestale_baseand the status 409. The reason names the position of the plan of the server, and the client reads that plan with the queryplan. So no old plan replaces a new one. Aplanequal to the plan of the server makes no record, and counts as aplan_seen. - 01M4A4YTTB24XNB4G49675QMHT
planchecks the form: each item, each need and each done need isissue-N, no item is twice in the items, and the thread is a repository thread. Else the server refuses it withbad_request. - 01M4A4YTWK68JDA0DKXX2HV4FA Each session in the repository thread
that is not a worker, the owner and each admin can send
planandplan_off. A worker getsnot_allowed, also a worker of the owner. - 01M4A4YTYVHFGK0CJVACJQ8DQ3 The signal
plan_seennames the position of the plan that a look saw. When it is the position of the plan of the server, and the caller is a session in the repository thread that is not a worker, the server keeps the time as the last look of the plan. Else the signal changes nothing. The reply is the plan of the server. - 01M4A4Z1NKPDBXV2PRZCG86G6A The query
plangives each member the plan of a repository thread: the wave, the items with their needs, the done needs, the holder of each item of the plan with a claim, the position and the time of theplan_setrecord, the time of the last look, and whether the plan is stale. It also gives each hold of the thread. The reply ofplanand ofplan_seenis the same. - 01M4A4Z1QTHYXZDMCP9DZ39WVT A plan is stale when no
planand noplan_seencame forPLAN_TTL(10 minutes). The time of the last look is in memory: after a new start of the server, the count starts at the start. - R222 The requirements and the book name the concept of waves first. Each keeps the form of a forge in one part of its own. A new forge needs no change to the concept. Outside these parts, no text names the objects of a forge that hold a wave.
- 01M5NBY42N0ACGG1WAY56ED8CT A pull request on a branch that names
no issue and has no trailer
Issue: #Nstill counts for the item of the lineCloses #Nof its body. The wave starts no build of that item.
Waves on GitHub
- R223 On GitHub, a wave is a milestone named
Wave N. A name can follow, for exampleWave 5: Cloud. A work item is an issue in the milestone. A milestone with another name is out of the waves. - 01M3JD8WWMK2ZQTFER4TJFV37V On GitHub, the backlog is the
milestone
Backlog. It is out of the waves. An item in the backlog is not free work: no session starts it. Only the lead moves an item from the backlog into a wave, when its user schedules the item. - R224 On GitHub, an open wave is an open milestone. The lead ends a wave: it closes the milestone. It closes a milestone only when the milestone has no open issue.
- 01M509TJRBE2J7HYKKHRJ1ZFR0
riff wave listshows the open milestones of the repository of the current folder: the waves in number order, then each other milestone by its title. Each line has the number of the open and the closed issues of the milestone. riff reads them with theghof the machine. - 01M58ATZ408RBCX0PM0D0ZCY4B
riff wave listmarks the current wave, the open wave with the lowest number, withcurrent. The line of the current wave names each host whose riff daemon runs its loop:runs on HOST. Under the line of each wave come its open issues in the groups of the board ofriff top:free,held,claimedandverify. riff reads riff-server with the sign-in of the caller, asriff topdoes. - 01M58ATZ6FA5PJMD3D848CEWBJ When
riff wave listcannot read riff-server, it shows the milestones with no groups and no hosts, and one line on stderr says why. - 01M509TJTV7TC32EKXW0TEQ5NZ
riff wave make TITLEmakes a milestone. The title isWave N, orWave N: NAME. riff refuses each other title, and a number that an open wave has. - 01M518A0BEQBX9RH9H0CEFYF9Q
riff wave rename N NAMEgives the open milestone of the wave with the number N the titleWave N: NAME. The number stays. riff refuses a blank name, and a number that no open wave has. Then it changes nothing. The instructions of the lead and the page “Waves” give it as the step to rename a wave. - 01M509TJXE04CTG5D1K751WXJY
riff wave end Ncloses the open milestone of the wave with the number N. riff refuses when no open wave has the number, and when the milestone has an open issue or an open pull request. Then it closes nothing. - 01M5924014WN63RCHJWGZS4AFZ
riff wave move ITEM... --to WAVEputs each issue ITEM in the open wave with the number WAVE, or in the open milestone Backlog when WAVE isBacklog. riff makes theghcall itself. It refuses an item that a session claims on riff-server, and a target that no open milestone has. Then it moves nothing. - 01M592403Q4TNZEDNR76C7NK9M
riff wave end N --move-open-to-backlogmoves each open issue and open pull request of the wave N to the Backlog, then ends the wave. It refuses an item that a session claims, and then it moves and ends nothing. Without the flag,riff wave endrefuses a wave with an open item. - 01M592406KTHSXHTWQWA3RN6Q2
riff initwrites the allow rules of the lead forriff wave moveandriff wave end, in a terminal and in a container.text::MCP_LEADnames these commands for the steps of the waves, and notgh issue edit --milestone. - 01M588K2M7G0CV3RNJ4N074FTG
--wave WAVEofriff usageandriff audittakes the number of the wave,NorWave N, or a full title. riff finds the milestone in the open and the closed milestones of the repository: the milestone with the same title, else the one milestoneWave NorWave N: NAME. riff refuses a wave that matches no milestone, and names the open waves. It refuses a number of more than one milestone, and names them. Then it shows no tokens and checks no rule. - 01M3Q5QE2EWGKVD57Y6YCWA4BR The rollout of workers
(01M3Q5QE01DB0FJQJWFKR450KQ) reads its free work with
gh. The current wave is the open milestoneWave Nwith the lowest N. A free item is an open issue of it that no session claims, with no commentMerged in #, and with each issue of itsNeeds:line closed. An open need blocks the item, also a need outside the wave. A pull request that waits for a verify names its issue (01M51BF5S2YKB8JG10DF6YVT42), is open, not a draft, has no statusriff/verifyon its head, and no session claimsverify-issue-Nfor it. Both count as free work. - 01M3Z9N5HHHS1E17NFGMVBKZ0K An item counts one time as free work. An open issue with no claim whose pull request waits for a verify or for the merge (01M3Z9MY0CDBB1G749XBVMVV8X) is no free item: it is work for a verify, not for a build. An open issue with no claim whose verify failed is a free item.
- 01M3ZWRC9F7TGSHB966TPVVS9Q On GitHub,
riff auditreads withgh: each milestoneWave Nwith its start and its close, each issue with its milestone, itsNeeds:line and the time when it was merged (its close, or its first commentMerged in #, the earlier one), each pull request of the milestone with itsIssue:trailer and its merge, and the last statusriff/verifyof the head of each. - 01M5QDWWSYK0J3N24GF8J123JQ The read of the open pull requests with
gh pr list --limit 100 --jsonnames no field thatghexpands to a nested connection (commits,comments,reviews,files,labels). GitHub refuses a query of more than 500,000 possible nodes, andcommitswith the authors of each commit counts 1,000,000. The WIP check reads the head commit of a pull request that waits for its verify, and no other commit, withgh api repos/OWNER/REPO/commits/SHA. A fault of that read is no WIP. A test fails when the fields name such a connection. - 01M5QVF58KEC9WNHAJ8WTZ18EP
riff wave makealso makes the open milestoneBacklogwhen the repository has none, and says so. When it cannot make it, it says why on stderr, and the wave stays made. - 01M5QWA0H5KJ2SRRP50478X3J2 The check of the Backlog in
riff initpasses when the repository has the open milestoneBacklog. Its step makes that milestone, asriff wave makedoes. - 01M5R0HERC566W14SEJ3ZCG6V5
riff wave move ITEM... --to WAVEwarns when an issue has no line that starts withDone when:, and moves it. The warning says that the wave takes only an issue with that line.--to Backloggives no warning. - 01M5SMMP561HR1ZQY3ZE4361FD The names that riff uses on the forge
are rules of riff, the same for each repository. They are the
milestones
Wave NandBacklog, the labelstracker,broken-mainandno-verify, the lineDone when:and the branchworktree-issue-N. The book lists them in one place: the part “The names riff uses on the forge” of the page about waves. - 01M6GX4HYR3755G6EJW1XHEFVE A read of
ghthat fails is not a read that gave no issues.riff wave listnames the error ofghin its line on stderr:riff: the list has no items and no hosts: gh issue list failed: ERROR. It saysgh gave no open issues of REPOonly whenghread the issues and gave none.riff topreads the issues in the same way. When the read fails, it keeps the last issues, and its first line names the error. This adds to 01M58ATZ6FA5PJMD3D848CEWBJ and 01M3ZC09FA9DZPTHK31XECZ566.
Issue hygiene on GitHub
- 01M3JDRSW3E9ENAKC3S0KHPV8M
just hygiene pr Nchecks the form of pull request N withgh. The body has exactly one link line:Closes #NorRefs #N(link). No other closing keyword of GitHub stands before an issue number (keyword). The title does not end with(#N), because GitHub adds the number of the pull request (title). - 01M3JDRSYEGKYVNGF49SJMACEB The body of a pull request has the
trailers
Issue: #NandMilestone: M, each one time, in its last block of trailers (01M5RP9PSKWXCD1F2SKCQN5379). N is the issue of the link line (issue-trailer). M names the milestone of the pull request as 01M518A04JWYJE5FA1VR309SAA says (milestone-trailer). - 01M518A04JWYJE5FA1VR309SAA The trailer
Milestone: Mnames the milestone of the pull request when both are waves with the same number:Wave N, with a name or with none. A milestone that is not a wave needs its whole title. So a rename of a wave breaks no pull request (milestone-trailer). - 01M3JDRT0SHF2J3NXNGZFWYH6W A pull request and its issue have the
same milestone (
milestone). The issue is open (issue-open). - 01M3JDRT31M12MJ1K592RAJQ78
just hygiene commit [REV]checks the message of a commit onmain: the title ends with(#PR), PR is not the issue (commit-title), and the message has the trailersIssue: #NandMilestone: M. - 01M3JDRT5B52GDZK91VETC6VV2 The workflow
hygiene.ymlruns the jobHygiene(hygiene pr) on each pull request event: opened, edited, synchronize, reopened, milestoned and demilestoned. It does not run theGateagain. On a push tomain, it runshygiene commiton the new commit (01M3JFEXS6M5549TC6MH0G2MS0). - 01M3JDRT7HMZD4FHWHDH3S1A1D Each error of
hygienenames its rule. It exits with status 1 on a broken rule, and with status 2 whenghorgitfails. - 01M5RP9PSKWXCD1F2SKCQN5379 The trailers
Issue: #NandMilestone: Mof a message stand in its last block of git trailers: the last paragraphs of the message that hold only linesKey: value. Other trailers (Co-authored-by:,Signed-off-by:,Generated-by:) may stand in the block, before or after them. A message that ends with a paragraph of text, or with a line of text in the paragraph of its trailers, has no trailer. GitHub adds theCo-authored-by:lines of a squash commit in a paragraph of their own, sohygiene prchecks the message that the squash will make: the body, and aCo-authored-by:line for each co-author of its commits, except the author of the pull request. Supersedes 01M3JDRSYEGKYVNGF49SJMACEB and 01M3JFEXS6M5549TC6MH0G2MS0 in part. - 01M6FYR8XH4WPAH2AFW6B0NARJ The merge wraps each line of the body
of a pull request that is longer than 72 characters, with no indent on
the wrapped part, and that part is no trailer line. So a line of the
last block of trailers that is longer than 72 characters breaks the
block in the commit on main.
hygiene prpredicts the wrap in the squash message (merge_wrap), and fails each such line with the ruletrailer-wrap. The error names the line, its length and the limit. The rule is one of the rules of riff (rules = "riff"), and a repository that is not riff does not get it. The book says how long a trailer line may be.
Pull requests on GitHub
- 01M3JFEXG85AJK8ZE8N807EQVB
just githubsets up the repository withgh api, and is safe to run again. Auto-merge is on. Squash is the only merge. The squash commit takes the title and the body of the pull request. GitHub deletes the branch after the merge. The rulesetmainon the default branch needs a pull request with 0 approvals and the checksGate,Hygieneandriff/verify. A branch need not be up to date withmain. No force push and no deletion ofmain. - 01M3JN4QQCM0GXK9BCGXVS2YC7 The ruleset
mainhas no bypass actor. GitHub refuses a push tomainand a merge that skips a check, from each session and from each person. For an urgent fix, our user turns the ruleset off, pushes, and turns it on again withjust github. - 01M56H9Q0757ESXMJEDM16Z7AT Our user runs
just githubfrom a checkout ofmainonly. Thedeploy/github.shofmainsets each ruleset. - 01M4YTW8788K4XYTEMRSXDH7B9 The GitHub release of the line that the shared riff runs is marked “Latest”. A release of another line is not, also when its version is higher.
- 01M3JFEXJG2D651PWA30DNRGWF No session pushes to
mainor runsgh pr merge --admin. The project settings of Claude Code deny both. - 01M3JN4QVR3JCRWC8TFJJTCPHF Each session uses the GitHub account
of our user, so the author of a pull request can set
riff/verifyon its own commit. We accept this gap until the sessions act with their own GitHub identity (#97). - 01M3JFEXMPNFEV4HBZJQ15JD25 The author opens a pull request with
riff pr open, with the milestone of the issue and the body form of the rules that apply (01M5SMSHNQ9E4P9W6RCB8E1WTN, 01M5SMSMWE89SC3Q7XFBQCFY32), and auto-merge with a squash on at once, before any other push. No session runsgh pr mergeafter a push. The last pull request of an issue hasCloses #N. Each other one, and one with a check after the release left, hasRefs #N. - 01M3JFEXPXRTXYHCV0WSKEK07M The verifier puts its result on the
pull request as a comment that names the commit. It sets the commit
status
riff/verifyon that commit:successon a pass,failureon a fail. A new commit has no status, so it needs a new verify. - 01M3JFEXS6M5549TC6MH0G2MS0 The workflow
Hygienealso runs on each push tomain. It checks the commit message with the commit rule of the hygiene check. The trailers stand in the last block, as 01M5RP9PSKWXCD1F2SKCQN5379 says. - 01M3NB6FTGPD0S5JTXXXNGNNDT
riff pr open --title TITLEopens the pull request of the current branch withgh. The issue is the one claimissue-Nof the session, or--issue N. The body is the link lineCloses #N(Refs #Nwith--refs), the summary of--file, and the trailerIssue: #N. With the rules of riff (01M5SMSMWE89SC3Q7XFBQCFY32), it also has the trailerMilestone: Mof the issue (01M518A07QF1YACBYT91M0DD1R). It refuses a pull request that breaks the rules that apply (01M5SMSHNQ9E4P9W6RCB8E1WTN), and opens nothing then. After the create, it runsgh pr merge N --auto --squashat once. - 01M518A07QF1YACBYT91M0DD1R With the rules of riff
(01M5SMSMWE89SC3Q7XFBQCFY32),
riff pr openwrites the trailerMilestone: Wave Nfor a wave: its number, with no name. For a milestone that is not a wave, it writes the whole title. A trailerMilestone:of--filewith the same wave number is the one of the pull request. - 01M3W2627GYXR8CFW76KB6CB9W
riff pr openadds the link line and each of the trailersIssue:and, with the rules of riff (01M5SMSMWE89SC3Q7XFBQCFY32),Milestone:only when the text of--filedoes not have it. When that text has a link line, or a trailerIssue:orMilestone:, that is not the one of the pull request, it names the line, opens nothing, and exits with status 1. - 01M3NB6FWMGBQ9VTY6RCBPKBHK
riff pr wait Nlooks at pull request N withgheach--everyseconds (default 30), until it is merged. Then it prints the merge commit and exits with status 0. It exits with status 1 and the reason when the pull request is closed and not merged, or when a required check fails. - 01M3Z8GG5EGEYAVEXG0HS46ACT After one good look,
riff pr waitgoes on when a look ofghfails. It prints one line with the error on stderr, one time until the next good look, and looks again at its interval. When the first look fails, it exits with status 1 and the error. - 01M3NB6FYXXKX80VHEVA5CV6RY
riff verify pass|fail N --file RESULTreports a verify withgh: one comment on pull request N that names its head commit and holds the result, one statusriff/verifyon that commit (successorfailure) with the URL of the comment, and one riff post of the result to[{"claim": "issue-M"}], where M is theIssue:trailer of the pull request. The tested commit isHEADof the directory, or--commit SHA. When it is not the head of the pull request, it makes no comment, no status and no post, and exits with status 1. - 01M49HAZ7P3JMWNCG1SWCMAXQP
riff verify pass Nreads the check runs of the full check (01M5QYDE41FVFX26EC8K5QGYH4) of the head commit of pull request N withgh. When the commit has no run of the full check, or a run is not completed, or a run has a conclusion that is notsuccess, it makes no comment, no status and no post. It exits with status 1 and one line that names the commit, the state of the Gate andgh pr checks N.riff verify fail Nneeds no Gate. - 01M5QYDE41FVFX26EC8K5QGYH4 The full check of a commit is each
check that the ruleset of the default branch requires, but
riff/verify: riff reads it withgh api repos/OWNER/REPO/rules/branches/BRANCH.riff verify pass Nand the verify of the wave read the check runs of the full check by name. A pass, and the start of a verify, need each check to be done: the pass needs asuccessof each. The line of a refused pass names the checks. Supersedes 01M49HAZ7P3JMWNCG1SWCMAXQP in part. - 01M5R13DE5XRWB13E0BQ17C80F A check of the full check that ends
skippedorneutralcounts as a success, as on GitHub:riff verify pass,riff release verify, the verify of the wave and the look atmainuse one rule. A conclusion offailure,timed_out,cancelled,staleoraction_requiredis no success. Supersedes 01M5QYDE41FVFX26EC8K5QGYH4 in part. - 01M5RGM8HBVQMY25H5PYESK17J Of the check runs of one name on a
commit, riff reads only the newest: the latest started_at, then the
latest completed_at, as gh pr checks and the merge rules of GitHub do.
An older run of the name that failed does not count when a newer run
passed, and an older run that passed does not count when a newer run
failed. Runs that tie all count.
riff verify pass,riff release verify, the verify of the wave and the look atmainuse one rule. Supersedes 01M5QYDE41FVFX26EC8K5QGYH4 in part. - 01M5RG6BZQC8T6SZ8NX9W8TT5M A release goes on
mainonly (01M5RG6BTXAJY8DV6MT315VZWF). CI runs onmainand on each pull request tomain. There is no branchrelease/**, no rulesetrelease-branches, and no how-to for a patch of an older release line.riff pr openhas no flag--base. This supersedes 01M4YJ6KGBPTCSVMFQCDKACZPV and 01M5QWJJ94T4M3KJB0R3WTJCQF, and 01M56H9Q0757ESXMJEDM16Z7AT for the rulesetrelease-branches. - 01M5SMSHNQ9E4P9W6RCB8E1WTN In a repository whose riff config
(
riff.tomlat the top of the work tree) has norules = "riff"in the table[hygiene],riff pr openkeeps only the rules for what riff reads: the link lineCloses #NorRefs #N(link), and the trailerIssue: #N(issue-trailer). It needs no milestone on the issue, writes no trailerMilestone:, and opens a pull request whose title ends with(#N). When the issue has a milestone, the pull request gets it. Supersedes 01M3NB6FTGPD0S5JTXXXNGNNDT, 01M518A07QF1YACBYT91M0DD1R and 01M3W2627GYXR8CFW76KB6CB9W in part. - 01M5SMSMWE89SC3Q7XFBQCFY32 With
rules = "riff"in the table[hygiene]of its riff config,riff pr openapplies each rule of the hygiene check (01M3JDRSW3E9ENAKC3S0KHPV8M and the requirements after it): the milestone of the issue, the trailerMilestone:and the rest. Theriff.tomlof riff has it. Another value ofrulesis an error. The jobHygieneof the CI of riff runs each rule on each pull request of riff, whatever its riff config says. - 01M5SQ45MQVHFX8ZSFSF02SFKH The help of
riff pr openand of its flag--titlesays what holds in each repository. It says that the trailerMilestone:and the rule for a title that ends with(#N)hold only withrules = "riff"in the table[hygiene](01M5SMSMWE89SC3Q7XFBQCFY32). It links nothing: the book has the table of the rules. - 01M5SMRRAG1WPBJMQKKTPSRHRP
riff verify pass Nchecks a pull request that changes the session image inriff.toml: the registry of the new pin must give the manifest of the image with no login. riff asks the registry API of the OCI distribution spec, with no podman: the anonymous token that theWWW-Authenticateheader of a 401 names, then the manifest by its digest. When the registry gives no manifest, or does not answer, it makes no comment, no status and no post. It exits with status 1 and one line with the fix: make the package public, or runpodman login REGISTRYon each host of the wave.riff verify fail Nneeds no such check. The verify of the wave uses the same rule. - 01M5SMNKDWBD1J6Y18ENQG87CK A repository with no CI has no full
check. When the ruleset of the default branch requires no check but
riff/verify, the full check is empty, andriff/verifyis the only check that gates a merge. riff names no checkGatefor it. The verify of the wave starts when the pull request has a head commit, andriff verify passneeds no check run.riff initmakes the rulesetriffwithriff/verifyalone when no pull request ran a check, and it says in a note that the repository has no CI.riff.tomlhas no setting for the full check. Supersedes 01M5QYDE41FVFX26EC8K5QGYH4, 01M5QWA0E9A4ZJ8X49EM2MJM5Q, 01M5QZ01Y14Z76RC220RT7YTX7 and 01M5R1675HZ6TJPMQ6536X37T5 in part. - 01M5TKPHDNHY75BHPD4XVBJRN9 The ruleset
mainof como-technologies/riff has a merge queue with the squash method. A pull request merges through the queue: GitHub builds a merge group of the default branch and the pull request, runs the required checks on it, and merges it when they pass.deploy/merge-queue.sh on REPO RULESETadds the rule to a ruleset, andoffremoves it. The ruleset thatriff initmakes has no merge queue. - 01M5TKPHH2627K9NDS4P1Y1N3T CI runs the Gate and Hygiene on the
event
merge_group. A job of CI sets the statusriff/verifyon the merge group commit: success when the head commit of the pull request of the group has a passedriff/verify, else failure. A verified pull request merges through the queue with no new verify. - 01M6FYW8NG0E9HYPWTG2RV5EAM
riff verifytakes the repository of the pull request from its thread, the thread of the post, not from the URI of the verifier. A person in a terminal has no repository in the URI, in any folder. So the repository of the thread decides which session holds the issue (01M3Z9N70J4H79VJN4ZKKH3G6S) and in which repository the lead of the user of the verifier wakes. The selector of that lead isuser=U,repo=OWNER/REPO,lead=true.riff verifyruns from any folder of a clone of the repository, also outside a session. A folder that is not the worktree of the test needs--commit SHA. - 01M6G0BFAJJV9YNQZT502P1KGJ The look of the wave at the open pull requests reads the check runs of a head by the rule of 01M5RGM8HBVQMY25H5PYESK17J: of the runs of one check, only the newest. An old failed run does not count when a newer run of the same check passed, for the fix of checks, for the message to the lead, for the group stuck of riff top, and for settled (01M6FZ144M0BTEDWNB9V4MGWDD): the look reads the runs that wait for approval of a settled pull request only. The end of a session that fixed checks reads the checks the same way. The wave starts no fix session for a pull request that is merged or closed, and sends no message that a check still fails for one.
- 01M6H3EK1J3DEEV09FAKKKFPEB A body of
riff pr openthat ends with a lineKey: valueof more than 72 characters, for example a lineTests: ..., breaks the ruletrailer-wrap(01M6FYR8XH4WPAH2AFW6B0NARJ).riff pr openrefuses it before the pull request opens, names the rule, and opens nothing. The same text as a plain sentence passes. The start text of a worker says in one line: write tests and results in plain sentences, not as aKey: valueline, and keep each trailer line to 72 characters. The how-to “Keep a trailer line short” says thatriff pr openchecks the body first.
Tokens on GitHub
- 01M3Y1YP0QY11VR28RF9MKPN0G riff sums the tokens of each claim from the transcripts of its session, on the machine of the session: the input, output, cache write and cache read tokens of each model. A reply counts when its time is from the claim to before its end. A reply counts one time, also when the transcript has it on more than one line. The transcripts of the helper agents of the session count.
- 01M3Y1YP1JAQMJ66K2QXC7766C Tokens in a time with two claims of one session count for the claim that started last.
- 01M3Y1YP1ZA5TBRA01MKWM3VC6 At the end of a claim, riff puts its
sum on the issue of the claim as one comment, with
gh.issue-Nandverify-issue-Nare claims of the issue N. A claim ends at its release, at a leave, at a new start of its session and at the end of its session. The log ofriff-serverholds no tokens. - 01M3Y1YP2CSNHCWV7T4CE9HZ4Y The comment has one line for a
person, then the same numbers as JSON in a
detailsblock with the summaryriff:usage. It holds only the item, the kind of the claim (work or verify), the first 8 characters of the session ID, the two times, and the four kinds of tokens for each model. It holds no text of a transcript, no path and no email. - 01M3Y1YP2TVYQC7GCCAMN6111K A second report of the same claim replaces the comment of that claim that the same person wrote. It adds no comment.
- 01M3Y1YP39VFX6GH33H7B8A8KR A report never fails a release, a
leave or a hook. With no
gh, aghthat fails, an item that names no issue, or a thread that is no repository, the sum stays in the marks of the machine, and the result of the release says why. A claim with no tokens gets no comment. - 01M588N85Z94PX1ZP0BDXTPX83 At the end of a container session, the riff daemon puts the sum of its claim on the issue of its item, as a release does: for a work claim and for a verify claim. The claim counts from the start of the container to its end. The report never fails or delays the end of the session. The daemon log has its line.
- 01M588N8AYSGVWFW05A5RV976H The daemon reads the transcripts of a
container session in the Claude folder of the session:
claude/projects/*/SESSION.jsonl, with the riff session ID. - 01M3Y1YP3QMKS6B35PJ42KNYXX
riff usage ISSUEsums the comments of the issue with the mark. It counts a claim one time: of two comments of one person for one claim, the later one counts. It shows the total with the four kinds, then the work claims and the verify claims, each with its session, its models and who wrote its comment. - 01M3Y9TD41FZBDQBK42FVG89B8
riff usagetrusts a comment of another person only for its numbers. It takes a report only when its item isissue-Norverify-issue-Nof that issue, its kind agrees with the item, and its session and each of its models have only ASCII letters, digits,.,_,-and:. Each other report counts for nothing. A comment of another person never replaces a report: it counts as a report of that person. A sum past the largest number stays at the largest number, and fails nothing. - 01M3Y1YP45VQS5HMJCXKRN3CCR
riff usage --wave WAVElists each issue of the wave, open and closed, with its total, and shows the sum. - 01M3Y1YP4KVHK1DTZ85YNGDG0T
riff usagewith no issue and no wave shows each session with marks on this machine: its total, the tokens of each item, and the lineno issuewith the tokens outside each claim. - 01M3Y1YP514MPX8DTKMTWDHE8Q After the merge,
riff pr waitputs the open claim of the issue on the issue as it is then, and adds one comment with the total of the issue and its models. Each later report of a claim of the issue writes that comment again. A failure of this step does not fail the wait, and a total that riff cannot write does not fail a report. - 01M3ZRQY9F9P7DF187Q0PJDS30
riff usage ISSUEsays how many comments with the mark it did not count, and why: an item of another issue, a text that is no name, or no report that it can read. When riff cannot write the total comment of an issue, the text of the release and ofriff pr waitsays that the comment of the claim is on the issue, that the total is not updated, and why. A total comment of another account is not updated: GitHub lets only that account edit it. - 01M3Y1YP5GC5W9KVJQP6PXPM3G The book has the how-to “See the
tokens of an issue” with
riff usage. It says that the numbers are public on the issue. - 01M49HF057R08DGW6X5A8EHR42 riff gives
gh apithe body of a write in a file, never on stdin. When the write fails, the error names the HTTP status of the reply, whenghgot one. - 01M49HF07WQC3M5HGAQNHR8WA0 When riff cannot write the total comment of an issue, it reads the comments again and tries one more time.
- 01M49HF0ADEQ83XTJCQT0PK3RS
riff usage ISSUE --totalwrites the total comment of the issue again, then shows the tokens of the issue. The book has the how-to “Write the total of an issue again”.
Pause
- 01M3JCFTWCR72HQB8CBTQKXJNF The whole riff is paused or running.
The state is one for each
riff-server. It is a record in the log, so it stays when the sessions and the server restart. A new riff starts paused. - 01M3XAHZBGSSJB3YX23K88W01K Each repository has a pause of its
own. A session is paused when the whole riff is paused, or when the
repository of its place is paused. A thread is paused when the whole
riff is paused, or when it is the thread of a repository that is
paused. A pause of one repository does not stop a session or a claim
of another repository.
riff pauseandriff resume, and the MCP tools, name the repository of the call.--riff(the tools:rifftrue) names the whole riff.--repo OWNER/REPOnames a repository. A resume of a repository while the whole riff is paused changes only the pause of the repository. - 01M3XAHZDSQR263QZVB41CK0MX A person (a call with no session ID,
for example
riff pausein a shell) or the lead of a repository can pause and resume the repository of the call. Only the owner or an admin can pause and resume the whole riff, or a repository that the call names: as a person, or as a session that is a lead. The server refuses each other caller with the codenot_allowedand a reason. In a riff with no sign-in, each caller has the role of an admin. - 01M3JCG3WBHDF0ZWM06XV94ZDC While a thread is paused, a claim in it fails. The refusal says which pause stops it (the riff or the repository), who set it, and who can end it. The sessions keep the claims that they hold. A release still works.
- 01M3JCG3YD7C2Y3V0QJPF082YH A pause or a resume that changes the state wakes each session that it stops or starts, and that is not gone: the client posts to the thread of the repository, to that repository. For the whole riff, it posts to the thread of each repository of such a session.
- 01M3XAHZSJ5914BRQBZ2G4ZBSA A session that another pause still stops does not wake: a resume of a repository in a paused riff wakes nobody, and a pause or a resume of the whole riff does not wake the workers of a repository that has a pause of its own (01M6FYGVTCBKC9C8AXMFD7WMW2 tells its lead). The answer to a resume names each pause that still stops work.
- 01M3XAHZG26ECNARX35JD73YXJ The record
pause_setholds a pause that is set or ended:scope("riff", or{"repository":"OWNER/REPO"}) andstate(pausedorrunning). A build reads a scope that it does not know asother, and such a record changes no pause.make_riffmakes apause_setrecord. - 01M3XAHZQ92GGFHBC50FQ7FQ0K The state keeps who set each pause
(the
byof its record) and when (the time of its record). The checkpoint holds each pause with the two. A start from a checkpoint gives the pauses of a full replay. A checkpoint from before the pause of a repository reads. - 01M3XAHZJAF6YVDJ7WX74X8RBX
riff whoami,riff who,riff top, the MCP toolswhoamiandwho, and the start hook show which pause stops a session and who set it. The reply to/v1/riffhas the state for the place of the caller, the pause of the whole riff, and each repository that is paused. - 01M3JCG40FN0DP135EHHF403TY While the riff is paused, a new session says hello to the lead, waits, and claims nothing.
- 01M3JCG42FYS8FJ0V6WK89KXAP While the riff is paused, a session with work stops at its next step. A command that runs finishes first. The session commits each change as a WIP commit on the branch of its worktree, pushes that branch, and waits. Nothing goes to the default branch: a session between a verify pass and its merge stops before the push, and a verify stops with no result.
- 01M3JCG44JKRVY8T4TZMB5PXNF While the riff is paused, messages still flow. A session answers a status request and a question from the lead. The watch runs, so the session stays live.
- 01M3JCG46KKK97VD00KQ4DW6HK When the riff resumes, each session goes on from where it stopped. A new session on a host follows “Start routine” in the instructions of the riff tools.
- 01M3JCG4AV80MHFP73CWDY5E3M
riff who,riff whoamiand thewhoamitool show the state of the riff. - 01M5P1XQ6WTGFHW0P5M114P535 The riff daemon sends no verify request for a pull request whose head commit is a WIP commit. The item goes back to a build.
- 01M6FYGVTCBKC9C8AXMFD7WMW2 A pause or a resume of the whole riff
that changes the state also posts one message to the lead of each
repository that has a pause of its own, in the thread of that
repository, to
{repo, lead: true}. The text says that the riff runs again (or is paused), that the pause of the repository still stops its workers, and who set that pause. The workers of that repository do not wake. The answer to the call names the leads that it told. - 01M6GXACQVG2EZBAGYXG3D5ED7 The instructions of the riff tools hold “Pause” for each kind of session: the session on a host, the session in a container, and the lead. “Pause” says what a session does while the riff or its repository is paused, and what the resume does. Each text of riff that names “Pause” names these instructions. A doc test of each instruction text checks the part.
Service
- R118
riff-serverwith no command runs in the foreground, in a terminal. - 01M3K0QM5HY852J4E5M2YQDYEM
riff-serverhas one command,log(01M3TJWHNYRCA7RTPFNYM5ZNQS), and installs no service. A person runs it in a terminal. The cloud runs it on Cloud Run. It keeps no settings: it reads them only from its options and the environment at each start. - 01M3TJWJ3VK671T9NM95F3ES82 Each log line of
riff-serveris one JSON object on stdout, with the fieldsseverity,time,messageandtarget, and each field of the event. TheseverityisDEBUG,INFO,WARNINGorERROR. An error of the options comes before the log starts: it is text on stderr. - 01M3TJWJ12WEDCXW3W0529KRP2
GET /v1/servergives the facts of the instance: if it serves, or why it replies 503; its last error; the log position, and the time and the duration of the last chunk write; the numbers of write errors and skipped records since the start; the position, the time and the version of the newest checkpoint, and why this build writes none; the numbers of chunks, sessions, read cursors, threads and live token chains; the memory in use; the start time, and how long the load and the replay took. It answers also while the instance replies 503 to each other call, and to ariffof each version. With sign-in, it needs a token.riff servershows the facts under the riff thatriffuses. - 01M3MRDEVR5VPPV6B1BDDVYSBG A worker never runs
riff update,cargo installof riff,just installorriff connect. The tests run the binaries of the worktree with a home of their own, so the installed binaries stay the same. - 01M3MY2KWKBJCQ0BCNC6533RBW A test run and
just devnever touch the riff of the machine: its sign-in, keyring entries, device key, settings, local files, the riff of the machine on 127.0.0.1:7878, or the shared server. Each integration test runsriffandriff-serveronly through the helper crateisolated. It removes eachRIFF_andCLAUDE_variable,TMUX,TMUX_PANE,GH_TOKENandGITHUB_TOKEN. It setsRIFF_SERVERtohttp://127.0.0.1:9, where nothing listens, unless the test names its own server. It setsRIFF_HOME,HOME, the XDG dirs andTMPDIRto a temp dir of the test, a D-Bus address that does not exist, and git with no config of the machine. A test fails each test file that names a binary of riff without the helper.just testandjust cirun with the sameRIFF_SERVERand a D-Bus that fails each call.riff serverandriff updatestill ask the riff of the machine for its build, by design.just devsetsRIFF_HOMEtotarget/dev-homeof its tree. - 01M3WG82ZMQYG1TGHE4ET0BDDW In a test, git reaches no remote on
the network: the helper
isolatedsetsGIT_ALLOW_PROTOCOLtofile. - 01M43B48Z8R0SXAWBQP75CPR55 A test that needs a place outside each
git repository gets its temp dir from
isolated::outside_git. The dir is in the first temp root with no repository at it or above it:TMPDIR, then/var/tmp,/tmpand/dev/shm. So the test passes, and writes nothing, also whenTMPDIRis in a repository, for example the home of the person. - 01M49NP2907J4SH4S6MAY09VXE Each cargo run in the riff repository,
also a plain
cargo test, hasTMPDIRset to/var/tmp:.cargo/config.tomlsets it withforce. So no test of a worker finds the repository of the home, or writes to it. - 01M49NP2JW8JFWYY56K7AK3H05 The temp dir of the test helper
isolatedis outside each git repository, also when cargo runs from a dir outside the riff repository. So no command of a test writes settings in a repository above the dirs of the test. - 01M3W98PMDPZW1CR3KJYMPHVQZ A test server starts with no old sign-in at its URL. The tests of one test binary share one mock keyring, and the OS can give the port of an earlier test to a later test. A test in process that puts a new riff at its URL keeps its listener. A test that needs a URL where nothing listens holds the port with a socket that does not listen.
- 01M41A0M2XWCWTWGF7T9DR03W0 A test waits for the fact that it checks, for example a message, a line in a log or a look of a host. It does not wait for a fixed time. A time limit of a wait only ends a test that hangs, so it is generous. A test passes on a busy machine. Only a check that a thing does not come waits for a time.
- 01M49HCWEHTKSVQ6176G7C6H2S A timed check in a test fails only
when the logic is slow. It uses a
Spanof the crateisolated. A low CPU time does not pass a check. When a check is over its limit and the CPU pressure of the span (/proc/pressure/cpu, thesomeline) is 10 % or more, the test printsslow under loadand the check passes. - 01M56QW660NSKW0HC5W518EZZQ A
Spanalso reads the IO pressure and the memory pressure of the span (/proc/pressure/ioand/proc/pressure/memory, thesomeline). When a check is over its limit and the highest of the three pressures is 10 % or more, the test printsslow under loadwith the name of the resource, and the check passes until 60 s. - 01M49HCWH5HR0GPKHXKYZTXPW3 A wall-clock limit in a test only finds
a hang: each check of a
Spanfails after 60 s. Each test that checks a time with a limit under 60 s uses aSpanorisolated::in_time. - 01M513N34NJ2G3TK3EH91M10FJ A unit test of
riffthat runs a fake program has a child process write the file. A doc test that drops a lock, and then checks that the lock is free, is astandalone_crate. A unit test of that kind waits for the end of the lock. - 01M517P9J77TZX28QYBR9SH6WW Each integration test (a file under
crates/*/tests) writes a program, and each new text of a program, withisolated::write_program. It copies a program withisolated::copy_program. A child process writes the file, so the test process never holds a write handle of it. A test of the crateisolatedfails on each line of such a file that writes a program in the test process: a mode 755, a text that starts with#!, or a write to the file of a program that the test file made with the helper. - 01M517P9MTBWPN0FQRMXEJ9JG5 The unit tests of
riffset the mock keyring store in one place:secrets::mock_store, one time for the process. A unit test fails on each other line of code ofriffthat sets the store, apart from the one line that sets the store of the OS (01M556QPPRSQ91HDED128G2X7G). - 01M517P9QE2SQ45SPV3K2SM71B
just stress ARGSrunscargo test ARGSmany times while busy loops keep the CPUs busy: 50 runs and 8 loops, or the numbers inSTRESS_RUNSandSTRESS_LOAD. It builds the test first, keeps the output of each failed run intarget/stress, stops the loops at its end, and fails when a run failed. - 01M3MY2KSV73WS8D902YCH2PRX With
RIFF_HOME=DIR,riffkeeps its settings inDIR/config.toml, its local files inDIR/state, and each secret in a file ofDIR/secretsthat only the owner can read. It never opens the OS keyring. Only the tests andjust devset it. - 01M51KE2HCRCM69EG1KHZAHH7A With
RIFF_HOME, the file of a secret is the name of the secret in hex. When that is longer than 255 bytes, the file issha256-and the SHA-256 of the name in hex. - 01M3K0QM89E2XM1NWSPT4KXSTC
just devloads.envat the root of its tree, when the file exists, into the environment ofriffandriff-server, for exampleRIFF_OIDC_CLIENT_IDandRIFF_OIDC_CLIENT_SECRET. Onlyjust devloads it. Git ignores.env. - R33
riff-serverrejects a token that it does not know. A lost token record means the person signs in again. - 01M3QA6TDF6FB5PH8E5V7HCYDQ Each event stream of
riff-server(/v1/watch,/v1/tail) sends the comment: readywhen it opens. So a front end that holds a reply until its first body byte lets the connect finish at once.riff chatshows its history less than 1 second after the connect. - 01M5GVK79HV5KSDE626Z5AS1EG A test that sets
RIFF_SOCKETgives it a path in a temp folder of the test, never the socket of a session/run/riff/riff.sock. So a test gives the same result in a container session and on the Gate. The helper of the release tests refuses another path. - 01M6FYHS06W0Y98GQTQY6Q37F0 A test that runs code of riff which
calls git starts with a helper of
isolatedthat removesGIT_CONFIG_COUNT,GIT_CONFIG_KEY_*andGIT_CONFIG_VALUE_*from the environment of the test process. A test in a shared test binary reruns itself withisolated::rerun_without_git_config. A doc test callsisolated::clear_git_config_envfirst. So the test gives the same result in a container of riff, in CI and on a host.
Saved state
- R30
riff-serverkeeps its state in memory. With--bucket NAME(RIFF_BUCKET), it keeps its log and its token store in that Cloud Storage bucket. With--dir DIR(RIFF_DIR), it keeps them as files in that directory. At start, it loads the newest checkpoint and replays the log after it (01M3TBZBMMSMNWP126ZQED13YG). - R34 Storage is behind one interface. Tests use an in-memory store
or a temporary directory, never the real bucket. Without
--bucketand--dir, the log is in memory, and a restart loses it. - R124 The bucket holds the chunks of the log, the checkpoints, the
token store and the lease. The token store is the object
signins.json. - R31 A restart loses the open streams, the proof IDs, the sessions with their places and statuses, each change of a read cursor since the last checkpoint, and each record whose chunk was not written. The call of such a record got no success.
- R125 After a replay, each session counts as stopped at the time of the load. Its claims and its lead end after the grace period (R9), unless it comes back. A record that came since the load (01M3THEE08ZKV8WGHDSVWV69ZE) does not start the time again.
- R126 After a replay, each session is gone until it calls again.
who --allshows it in the place of the last record that names it. - R127
riff-serversaves the changed token store at most once each second. - 01M53T8X5KH59R3P8H2D22WE32 The second of R127 counts from the end of one save of the token store to the start of the next. So the store gets at most one save of the object each second, also when one save is slow and the next one is fast.
- 01M53T8XH45WSVKS9FV3VXF2X8
riff-serverreplies to a refresh and to a swap for a session token before it saves the token store. While the last save of the token store failed, a refresh first saves it again, and gets 503 when that save fails too. A swap never waits for a save, and a failed or busy save does not refuse it. - R150 When that save fails,
riff-serverreplies 503. The next save tries the change again. - 01M3ZZQ9TRG9385GRQGM79RCXX When the store refuses a save of the
token store for its rate limit (HTTP 429),
riff-serverdoubles the least time to the next save, to at most 32 seconds. A good save sets it back to one second. - 01M3ZZQCEKEYK9CE8MGPM80Z2P While the last save of the token store
failed for the rate limit, a refresh does not save the token store.
riff-serverreplies to the refresh while the last good save is younger than the least time to the next save, and replies 503 after that. - R129 On SIGTERM,
riff-serverreplies 503 to each new call, writes each record in the queue, saves the token store, then exits. Ctrl-C does the same. - R46 A lifecycle rule of the bucket deletes each thread object 30 days after its last change.
- R147 The name of each chunk of the log starts with
log/. The name of each checkpoint starts withcheckpoint/. The token store and the lease have names outsidelog/andcheckpoint/. - 01M3MMXYS1V8CA89D2XHKPR6C4 When
riff-servercannot read a saved object at start, for example state of an old format, it does not migrate it. It logs one error at the ERROR level and stops. The error names the object, for examplegs://BUCKET/signins.json, and the fix: stop each server of the store and remove the old state, with the command. - 01M588N3JF6MXF3ZKB5PBWP06G The fix that removes the old state
comes only in the error of a start of
riff-server. The reply to a request of a client never has it. An error of the store in such a reply is 503 only when the store did not do the call. - 01M588N3CKQMSDQPVFYGTJQQ2T A read of the log from a position before the oldest chunk fails with an error that names the oldest position and the position that the read needs, and says that the checkpoint rule removed the chunks before it.
- 01M4ZHW1Y1X21ZPV053D0CXWMC The crate
riff-stateis the state machine: the state and its commands, the log and its replay, the checkpoints and the stores. It does no HTTP and knows no sign-in. Its log and its checkpoints have the format of riff-server: the format tests of riff-server check the fixtures of each release against it. Only riff-server uses it.
The log
-
01M3T410XDD9W4EC0Y68FAA7XN Each change that must not be lost is a record in one log: a message with the sessions that it woke, a join or a leave of a thread, a claim or a release, a lead, the state of the riff, a setting, and a forgotten session. Each record has a position: 1, 2, 3, and so on. A record is one line of JSON. The name of its change says what happened, in the past tense, for example
claimed. -
01M3T4111PFM0C6KPREWFS9EQQ A new field of a record has a default, and the default means “as before”. A build skips a field that it does not know. A change of a record never changes the type or the meaning of a field, and never uses the name of a removed field again. A new kind of change gets a new name. A build that does not know a kind skips the record, and logs a warning with its position.
-
01M3T411QW1SQV12RJVATEJ8YD A record names a session by its URI with no lead mark and no claims: the user, the session ID and the place at the time of the change.
-
01M3T4115BF1F0JFHYMK0WRKCX
riff-serverchecks each call against the pending state: the written state and each record in the queue. Each read, wake, view and reply uses the written state: only the records whose chunk is written. A call that makes a record replies after its chunk is written. Its wakes and itstailevents go out after the write too. A command that makes no record waits too (01M3WRD933ESXF33WDEDFCRFB8). A signal and a query do not wait. -
01M3T4118SDERYGJ25TAT1RGR2 The writer takes each record in the queue into one chunk: a new object with the name
log/and the first position in 20 digits, for examplelog/00000000000000001234.jsonl. The first line of a chunk is a header with the format and the first position. The writer writes at most one chunk at a time, outside the lock of the state. -
01M3T411BZQB8N4D2S0JFVESMS Each write of a chunk is “only when new” (
ifGenerationMatch=0). On the first try, an object with the same name stops the instance for good (R141). Each other error, for example a 429, a 5xx, a timeout or a failed token, is tried again with a backoff for 10 seconds. A later try that finds an object with the same bytes is done. After 10 seconds, the instance logs one error that names the chunk, and stops for good (R140). The next instance replays without the chunk. -
01M3T411F3K6FD28R3Q3ZE4VCN The replay reads each chunk in name order, and checks the positions: each record is the last position plus 1. A gap, a repeat, a line that does not read, or a chunk of a later format stops the start. The error names the chunk (01M3MMXYS1V8CA89D2XHKPR6C4).
-
01M3WNQRCBP0PHSA0H3THDH5NJ The state of
riff-serverhas two types: the riff (the state that the log gives) and the presence (memory). Onlyapplychanges the riff. A record changes the presence only inPresence::applied. Each command is a type with itshandlerule, in the file of its group: sessions, threads, work, or the riff. -
01M3WNQQWA7XGK4Y9ET8HJZ8NN
applyreads only the record and the riff, and no clock. It makes no decision. The places where it reads the riff are in a list instate/riff.rs, and a new place needs a line there. Three places stay for good: asession_forgottenrecord removes each thing of the session, aleft_threadrecord ends the lead of the session in that thread, and a thread keeps its last 200 messages. Areleasedrecord of a session that does not hold the item changes nothing. Aclaimedrecord replaces the old holder. Alead_setrecord replaces the old lead of the user in the thread. Thepostedand thesession_forgottenrecords keep the index of the signed messages. -
01M3WNQR41K41TV832GRQZ2CQS A log and a checkpoint that a build that was released wrote read with no change, from 1.0.0 on. A test reads the log and the checkpoint of each release, and compares the state and the bytes.
-
01M3XM2C18TT8VSKGD77YPZG53 A record with a value that a build read as
other, in its change or in itsby, counts as a skipped record: the build writes no checkpoint past it, andriff servercounts it. The fields arereason,scopeand the class inby. Each of them reads a text and each other form of JSON that the build does not know asother. Thestateof apause_setrecord has noother. -
01M3XSF90E9JYYTC13D9THY4WE In a
postedrecord, the kind of the message and each selector follow the rule ofothertoo. A build reads a kind that it does not know asother, and a reader shows the message as a message, with its text. A selector that the build does not know is a selectorother: an object with a field or a value that the build does not know, and each other form of JSON. It keeps its JSON as it came, and it matches no session. Such a record counts as a skipped record. Apostcall with such a kind or such a selector is refused, with the codebad_request: only the read of the log takes a value of a later build. The rustdoc of the records lists each type with named values in a record, with its rule. -
01M3XYYSY536AEJVERBPTQFQYX A session URI in JSON keeps each query part that the build does not know, as it came. The session is the same session: its user, its session ID and its place. The part gives no mark: no lead and no claim. A record with such a URI counts as a skipped record.
riff-serverrefuses a call of a caller with such a URI, with the status 400. The rustdoc of the records lists each text with a grammar in a record, with its rule or with the reason why it never grows. -
01M3XM2C3MND6YB24SGZ565353 A kind of record and a kind of command are never renamed, and the name of a removed kind is never used again. A file in the fixtures lists the kinds of each release. A test fails when a name of the list is gone from the code. The enum of the kinds and its list come from one place.
-
01M3XM2C60TKF05NETHY8EYP3P The format of 1.0.0 has only the kinds of a build that was released: it has no
riff_state_set. A fixture holds one record of each kind, and CI replays it. A test makes a checkpoint at each position of the fixture: a load of it gives the state of a replay up to that position, and a start from it gives the state of a full replay. The import of go-live writes only kinds of 1.0.0. -
01M43GSRSDJMGAH8SR1GD4Z3XF The fixtures of each release after 1.0.0 are in a directory of their own, for example
crates/riff-server/tests/fixtures/1.1.0/. Itskinds.jsonlists only the new kinds of the release, and the test of the kinds takes the union of the lists. Its log has each new kind, and gets the same checks of a checkpoint at each position. -
01M43GSGVYJW7C09SVRWRAQZDZ The checkpoint has the part
plans: each hold of each repository thread, with its reason, its caller and its time. An empty part is not written, so the log of 1.0.0 gives the checkpoint of 1.0.0. -
01M4A4Z3QVRC57RE7M43ZRF4T2 The part
plansof the checkpoint also has the plan of each repository thread, with the position and the time of itsplan_setrecord. The time of the last look is not in the checkpoint. -
01M3WRD8WJ2JF9077PRDX04T9A Each command of
riff-servergoes throughEngine::dispatch, and each signal goes throughEngine::signal. The engine module owns the state, its lock and the queue of the writer: no other code locks the state. A query reads the written state. -
01M3WRD8YQFSKR2PENZC6CX24B The stages of a command are types:
Authenticated,Checked,QueuedandApplied. Each one is made from the stage before it, and only the engine module makes one. OnlyAppliedgives the reply. -
01M3WRD90WBBCWTDGVQCBR6MNT The writer finishes each command. It writes the chunk, applies its records to the written state in the order of their positions, sends the wakes and the
tailevents, and then tells each call of the chunk. A call that the client drops loses only its reply. -
01M3WRD933ESXF33WDEDFCRFB8 Each command waits until the writer is done with its entry in the queue. This is also the rule for a command that makes no record, and for a command that is refused. So no reply and no refusal tells of a change that is not in the log.
-
01M3WRD8TBDPA4JNEZY6J4N2EX Each call is one wire type that gives its path and the type of its reply (
Call, inriff-core). The client sends each call with one function. The wire type of a command that a client sends is its command type, and one handler serves each such command (Routed). -
01M3WRD959DYNZHDKP5ZT9Q1C7 One function,
permits, says who can send each command. It reads only the caller (its class, its worker mark and its role) and the role that the command needs. A kind of command with no row does not compile. The token layer gives the class. The engine adds the worker mark and the role under the lock of the state. A refusal ofpermitshas the codenot_allowed. A worker cannot sendlead. -
01M51JBTEM10W68K6F267BB79N A rule fails closed. A new kind of command, class of caller, sign-in error or failure of a call does not compile until a person gives it its rules. Each of these rules is a
matchthat names each kind, with no wildcard arm and nomatches!: the classes and the worker rule ofpermits, the commands of the people, the commands that register an unknown caller first, the log text of a caller and of a sign-in error, and the reply to a refused sign-in. -
01M3WRD97EZJK3AABXECXEY133 A status, the place of a session, a keep-alive, the start and the end of a watch stream, and a read cursor are signals. A signal changes only the presence: it makes no record and no entry in the queue, and it does not wait for the writer. A command can give a signal: a
registergives the place, and anendgives the end of the session. A refused command sets no signal. A signal or a query of a session that the state does not know first sendsregisterthrough the dispatch, and waits for its write. -
01M3WRD99M99PNGP8ME50KC6WS The first start of a riff sends the command
make_riffof the server: the first record of the log pauses the riff. A later start adds no record. -
01M3WRD9BSBKS9TN66H29TGTBV
pause,resumeandset_idleare commands, each with a path of its own:/v1/pause,/v1/resumeand/v1/idle/set. A read of the pause (/v1/riff) and of the settings of idle workers (/v1/idle) is a query. -
01M3Z8MRDZEKTXSKZTDTDSCZ3W Go live keeps the work of each repository of the riff. A start of
riff-serveron a store that has the objects of ariff-serverfrom before the log (sessions,tokensorthreads/) and no log is the import: the commandimportof the server writes the state of the old objects to the log as one chunk. The riff keeps its ID, its people, its settings, each session with its threads, its claims and its lead, and the last 200 messages of each thread with their seq. A session with no sign of life for 30 days is not in the import. A session that ended, or that stopped more than 5 minutes before the save, keeps no claim and no lead. The riff is paused after the import. The server writes a checkpoint with the read cursors at once, and opens its port only after the import. An old object that does not read stops the start, and the server takes no lease. -
01M3Z8MRKTAN8CBAQB721JNZAK The import runs one time. The command
importis refused in a log that has a record. A start on a store with a log does not read the old objects. The import changes no old object and deletes none. -
01M3Z8MRGWWA0CNZ003D67H6R4 The import keeps each live sign-in of the old
tokensobject, at the position of the end of the import. A person refresh token of the old server that was not used works one time: it gives the first pair of a new chain. A used token, a session token and an access token of the old server give nothing. No person runsriff loginat go-live. -
01M3ZCDNQY2G9ET537B6SBYCBB The import reads the old objects two times: before the server takes the lease, as the check, and again after its wait for the old instance. It imports from the second read. When the store has a log after the wait, the server does not import and does not change the sign-ins: it replays the log.
-
01M3ZCDNR0DT9J5XXXS89APTQ2 The import makes the payload of a signed message of release 0.8.0 from the fields of the message. A message that was verified before the import is verified after it. A message whose signature does not sign that payload stays as it is.
-
01M3WRD9G5GAF65EX8P6D5DMQM A riff with no sign-in takes a call with no token: the caller is then the
meof the body, with the role of an admin. A call whose body names nomeneeds a token. -
01M3WRD9JBQMNN96TXJH8EAJ3W A refusal of a command has a code and a reason. The reply has the status of the code: 403 for
not_allowed; 409 forheld,paused,not_holderandother_user; 400 forbad_request. It has the code in the headerriff-refused, and the reason as its text. A claim of an item that another session holds is refused with the codeheld, and the reason names the holder.riff claimand theclaimtool show that reason. -
01M3WRD9MGSC3FTBAANT4ZSMKY
release_foris a command of its own, with the path/v1/release/for: the lead of a user frees the claim of another session of that user. Only a session can send it. The note of the server for it is apostedrecord in the chunk of the command, after thereleasedrecord. -
01M3X4Z60G1FXQTDC5XDJ05BAX The envelope of each record names its cause:
by, the caller of the command, andcommand, the kind of the command.byis an object that names the class of the caller (person,session,sign_in), or the textserver. A record with nobyreads: its cause is not known. A build reads a class that it does not know asother. The records of one command are in one chunk, one after another.riff-server logprints the cause of each record. -
01M3X4Z62RJREQ5H8F18Y85T6V Each command leaves one trace: its records, or one log line. A command that is refused, or that makes no record, gives one line with
caller,key,commandandresult(refusedorno_change). A refusal also hascodeandreason.keyis the thumbprint of the device key of the token. A chunk whose write fails gives one line with the resultfailedand the severityERRORfor each of its commands. A command that waits in the queue when the server stops gives afailedline with the severityWARNINGand the reason of the stop. The writer makes these lines. A command with records, a signal and a query give no line. -
01M3X4Z64ZNRD0G0F4JV1M64FN The token layer writes one line with the result
deniedfor each call that it refuses, up to the limit of rate (01M3Z67DZX9BC3TYF3PWGFGZJ7). The line haspath,code(no_token,bad_token,bad_proof,not_youorold_build) andnamed: the caller that the call named, withprovedfalse. The server reads the body of the call for the name only after the refusal, and at most 64 KiB of it. A name of more than 200 characters is cut, and the line says so (named_cut). -
01M3Z67B9RMVKY7TCXCG8HEZT4 The read of the body of a refused call takes at most 2 seconds (
trace::BODY_TIME). After it, the server writes thedeniedline with nonamed, and the reply closes the call. -
01M3Z67DZX9BC3TYF3PWGFGZJ7 The server writes at most 100
deniedlines (trace::DENIED_MAX) in each window of 10 seconds (trace::DENIED_INTERVAL). Over the limit, it writes no line and does not read the body of the call. At the end of a window with such calls, it writes one line with the resultdropped, the severityWARNINGand theircount. A stop of the server ends the window. The reply to a refused call is the same with a line and with no line. -
01M419Z1RM0TDJ50F6SEJC40GB The
droppedline also hascounts: a JSON object with the count of each code that lost a line, for example{"no_token":900,"not_you":3}. -
01M419Z1V3YT48PR2NTFYWAXG9 The server keeps 20 of the
deniedlines of each window (trace::DENIED_KEPT) for a call with a valid token: the codesnot_youandbad_proof. A call with another code takes a line only while the window has more than 20 lines left. -
01M3X4Z675D0ZQX93E93F3M8FA No line of a trace holds the body of a post, a token or a key.
-
01M3X4Z69CFV23V4QZBE8RP1GJ The codes of a refusal are a fixed set. The status of the reply is 403 for
not_allowed,no_sign_inandnot_member; 409 forheld,paused,must_clear,not_holderandother_user; 400 forbad_request. Each test of a refusal compares the code. -
01M3X4Z6BKM251H7CS2CEGR205 A claim that takes the item of a holder that is gone gives a
releasedrecord for the old holder, then theclaimedrecord, in one chunk. -
01M3X4Z6DSWKMJ2R549R4TSYP0
Engine::finishtakes the proof of the write of a chunk (Written). Only the write of the log makes the proof. So only a written chunk reaches the written state. -
01M3X4Z6G0TG0B4FT2N1FSPDHS The role of a caller with no token comes from the trust of the riff: an admin in a riff with no sign-in, else a member.
-
01M3X9X9M079WGFPJZHNXH9VEP The
startcall carriesreason(process,resumeorclear) andworker. A start makes onereleasedrecord for each claim of the session, then asession_startedrecord with the session, the reason and the worker mark. Aregistermakes asession_startedrecord with the reasonjoinwhen the log does not know the session, or when the worker mark of the call is not the mark of the state. Theregisterthat the engine runs first keeps the mark of the state. So the worker mark is in the log, and not in the presence. A build reads a reason that it does not know asother, and such a record changes nothing. The start hook sendsprocessfor the sourcestartup,resumeforresume,clearforclear, and no start forcompact. A person has nosession_startedrecord. -
01M3X9XCSBR11ACD86FNXKF8JH
forgetgives onereleasedrecord for each claim of a session, then itssession_forgottenrecord. -
01M4263ZXH4K23CSY6C5GJPVQH After a start of
riff-server, a session counts as seen at the later of its last call in the checkpoint and the last record of its own call. A record of the server, of a person or of another session only names the session, and does not make it newer: for example a record of the import of go-live, or a release by the lead. Only when neither is known does the last record that names the session count. -
01M4263ZZVY8QJ2METTEVR1W26 The checkpoint holds the status of each session, with the time of its set. A start of
riff-servergives each session its status again. -
01M49NP8F3A9CTJWZ74MCNZG0M The checkpoint holds the long step of each session in the same entry as its status: the name, the time of its start or its failure, and the reason of a failed step. A start of
riff-servergives each session its step again, with its age from its first start. A checkpoint with no step loads. -
01M4264028A3KVDK10PPERHM0C On a shutdown (R129),
riff-serverwrites a last checkpoint after it writes the log, also when no record came after the last checkpoint. -
01M3XA875QZ584JBGA37853PWX The people are state of the log: the riff ID, the email of each USER, the members, the admins that the owner made, the owner, a riff whose owner is gone, and the request for the owner role with its time. Only
applyof the kindsriff_made,person_joined,member_invited,member_removed,admin_set,owner_set,owner_asked,owner_deniedandsignins_endedchanges them. The checkpoint holds each of them, so they stay after a restart with a bucket.who,membersandGET /v1/sign-inread them from the written copy. -
01M3XA87NXAP6TEMB34QJ28HVP Each change of the people is a command through
Engine::dispatch:admit,invite,remove,set_admin,pass_owner,take_owner,deny_owner,grant_owner,end_owner,name_ownerandrevoke. Only a person sendsinvite,remove,set_admin,pass_owner,take_owner,deny_owner,revokeandset_idle. Only the sign-in sendsadmit. Only the server sendsgrant_owner,end_ownerandname_owner. A riff with no sign-in refuses each command of the people with the codeno_sign_in. A command that names a person who is not a member is refused with the codenot_member. -
01M3XA87F70CD3WH4STADSCW6S The role of a caller with a token comes from the people of the pending copy, with the admins of the settings (R210). So a command that comes after a change of a role in the queue gets the new role. A query reads the people of the written copy. The admins of the settings, the public address and the times of the owner role are settings of the state: each view holds them, and they are not in the log.
-
01M3XA877YZQ649SWB5TN60V5P The first step of a sign-in is the command
admit. Its caller is the sign-in: the verified email of the provider. Then the token store starts the chain.admitmakes no record for a person that the people know, so a second try after a stop makes no second record. A sign-in gets its reply after the write of the records ofadmitand the save of the token store. A revoke and a change of the people get their reply after the write of their records. -
01M3XA87A9GGFA89RQXWSKY0V6
signins.jsonholds only the sign-ins and their chains. Each sign-in keeps the position of the pending copy at the check of itsadmit. After the write of amember_removedor asignins_endedrecord, the writer ends each sign-in of the person from before the position of the record. In the same step, the token store keeps that position for the person, and it starts no sign-in of the person below it. A load drops each sign-in below the last such position of its USER. So a stop between the write and the end of the sign-ins lets no removed person in. The reply toremoveand torevokehas the number of sign-ins that ended. -
01M3XA87HE06Z6M32ZJPSYSYRZ Each riff has a riff ID. It is a field of the command
make_riffand of its recordriff_made: the first record of a new log. A new riff gets a random ID.make_riffmakesriff_madeonly for a riff with no ID, andapplykeeps the first ID.GET /v1/sign-ingives the ID of the written copy. A restart on the same bucket keeps it. A server with no bucket gets a new riff ID at each start. -
01M3XA87KQ6ESMQ4W1W1PTW766
applyofowner_setstores only the owner, and ends the request that waits. A record with no email says that the owner is gone.handlekeeps the old owner an admin and a member: it givesmember_invitedandadmin_setfor the old owner beforeowner_set, in the same chunk.set_adminfor a person who is not a member givesmember_invited, thenadmin_set. The notes oftake_owner,deny_owner,grant_ownerandend_ownerarepostedrecords in the chunk of the command. -
01M3XA87CJHCGZX283ZQAFKARZ No log line holds an email. An email is in a record, and in a reply to a member. The line of a refused command of the people has its code and no reason. The line of a sign-in names its USER.
-
01M3XGNZYD1E35DXYTHHJT1CR7 At a load,
riff-serverdrops each sign-in that the log does not hold: a sign-in whose position is after the position of the log, and a sign-in of a USER that the people of the log do not know. So after a log that went back to an earlier position, for example afterriff-server log cut, each sign-in that started after the new end is gone, and the person signs in again. -
01M3XGP011KGXDP9D1FNMT374F
admitchecks first if the person may join, and then the USER of the email (R209). A person who may not join gets the refusalnot_member, also when another email holds the USER. -
01M3XGP03RDF6S15JYS718WWFC The token layer gives the engine the position of the sign-in of each token. The engine refuses each command of a caller whose sign-in started before the last end of the sign-ins of its user in the pending copy, before
permits: with the codenot_memberwhen the user is no member, else withnot_allowed. So between the entry of a removal in the queue and the end of the sign-ins after its write, the removed person changes nothing, also not through a session. -
01M3TBZBMMSMNWP126ZQED13YG A checkpoint is one object of JSON. Its name is
checkpoint/, the position in 20 digits,-, and the time of the write in milliseconds, for examplecheckpoint/00000000000000001234-1790000000000.json. It holds the format, the version of the build that wrote it, the state that the log gives up to the position, the last messages of each thread, the read cursors, and the last call of each session. A start loads the newest checkpoint that reads, and replays only the records after its position. When the newest checkpoint does not read, the start uses the one before it. -
01M3TBZBQDF0ES4KM54FJQF6Z8
riff-serverwrites a checkpoint each 1,000 records, or each 60 minutes when records came. It encodes the checkpoint outside the lock of the state. A build writes no checkpoint past the first record that it skipped. A build writes no checkpoint while the newest checkpoint comes from a later version, or does not read. The server keeps the last 3 checkpoints and the newest checkpoint of each day for 30 days. It deletes the others. It deletes a chunk only when each kept checkpoint is past it. No rule deletes chunks by age. -
01M4YNCECQDQR3Z8QTGQ1PW3WV The 1.x instance drops the forge state of 2.0. A replay drops each record of the kind
forge_allowed. A dropped record is not a skipped record: the build writes checkpoints past it, andriff serverdoes not count it. -
01M4YNCEMNRCAXD37V03T4JQYR Each major release line has its own instance. A start passes over each checkpoint whose
buildhas a later major version: it does not use it, and it does not block the checkpoints of the build (01M3TBZBQDF0ES4KM54FJQF6Z8). A checkpoint of a later minor or patch of the same line still blocks. -
01M3TBZBT7MME9BG1RWX5SZAZ6 Memory and the checkpoint keep the last 200 messages of each thread. Nobody reads an older message. The copy check (01M3JEJVXXEPPNGT3FY4ZSFCWZ) knows only the kept messages.
-
01M3TBZBX140GJWCV5GZ73Q5Z5
readgives one page: at most 50 messages. When more messages follow, the reply has the number of the last message of the page. An unread read again gives the next page. A read of all withafterset to that number gives the next page.riff readandriff read --allread each page. Thereadtool ofriff mcpgives one page, and says how to read the next. -
01M51TKZSYR7PH629S29JX00ZD A read marks as read only the messages that it shows. A read of all with
afterpast the read cursor skips the unread messages of other sessions up toafter, so it keeps the cursor. Those messages stay unread: the next unread read shows them. For example, thereadtool with no thread givesafterto each thread, so a direct message that came after the first page of a long repository thread stays unread. -
01M58AG7Z2PETHBZG7X9QD8PK5 A read with no thread shows each direct thread first, then the other threads in the order of the list of the server.
riff readand thereadtool ofriff mcpdo the same. Only the part of 01M588J4XNV1TXEPMGTKRVY9VK comes before them. -
01M3TBZBZVH907QD359AB8TBSX Each hour, a timer of
riff-serverwrites asession_forgottenrecord for each session with no sign of life for 30 days. It does not run in the first 3 minutes after a start. The record drops the session, its read cursors, its memberships, its claims, its lead, and each direct thread whose two sessions are gone. -
01M3TJWHNYRCA7RTPFNYM5ZNQS
riff-server logprints each record of the log as one line of text: the position, the time, the kind of the change and its facts.--from POSITIONnames the first record.--bucketor--dirnames the store. The command runs no server. -
01M3TJWHRP49M66NYNHWSYD3XP
riff-server log verifyreads each checkpoint, and each chunk from the oldest kept checkpoint. It names each object and each line that does not read, and each gap or repeat of a position. It does not stop at the first problem. For a problem in a chunk, it names the last good position and the cut that removes each record after it. It exits with 1 when it finds a problem. -
01M3TJWHVN730ZWCWHT9ER186R
riff-server log cut --after POSITION --yesdeletes each record and each checkpoint after the position. It prints each record and each checkpoint that it removes, and the threads of these records. It keeps the bytes of each line that stays. It removes a chunk whose header does not read. It deletes the chunks from the end of the log to its start, so a cut that stops leaves no gap. It refuses a position before the oldest kept checkpoint, when the log does not start at position 1. -
01M3X342G8KF2W06PABGXTERMZ
riff-server log cutwith no--yesremoves nothing. It prints each record and each checkpoint that the cut removes, and the command with--yes. -
01M3X342K007K3Z9G0CYWFKVMA
riff-server log cut --yesrefuses while the lease is live (01M3X342DH98YEZ3X5CND43DGD). The refusal names the instance that holds the lease. A cut with no--yesruns, and names that instance. -
01M3X5TP9CD4NXGPJEGP2Q4RS0
riff-server log cut --yestakes the lease before it reads the log: it writes an ID of its own to the lease, only when the lease is the version that it read. When an instance wrote the lease in between, the cut refuses and changes nothing. After a lease that ended by its age, the cut then waits 10 seconds and reads the lease again. The cut ends its lease after its work, also after a refusal. When an instance took the lease during the cut, the command names the instance and exits with 1. A cut with no--yesdoes not write the lease. -
01M3X342NQWZBJPS0GXV98BQME
log verifyandlog cutread each line of a chunk by its bytes, so a line that is not UTF-8 is one bad line. The good part of the log is its first records that read and have the right positions.log cutkeeps only the good part, up to the position. It removes each line after the first problem, also a line with a lower position, and each later chunk. It prints only the lines that it removes. So the cut thatlog verifynames repairs the log, and removes no record before the first problem. When the first problem is before the oldest kept checkpoint,log verifynames no cut, andlog cutrefuses. -
01M3ZWRC11R5M9V1KTF05P240W
POST /v1/loggives the records of one repository thread, for an audit. Only the owner and the admins can read them; each other person gets 403. The server reads the log from the store. A record of the repository is a post, a membership, a lead, a claim or a release in its thread; a direct message from a session in the repository; the start or the end of a session in the repository; a pause of the riff or of the repository; and a hold or a free of an item of the repository. -
01M3ZWRC3XBFN8FJDGE8XWZ5EA A post in the reply of
POST /v1/loghas no text of its body, no signature and no payload. Its body is only its mark:request,verify requestorverify resultwhen the body starts with that word and a colon, else empty. The kind, the sender, thetoand the time stay. -
01M43GSMZKCMET3DG07K538EDD A reader of the reply of
POST /v1/logskips a record of a kind that its build does not know. Soriff auditof one build reads the log of a later server. -
01M3TJWHYB9FTZ3G8G227V0N05 With a bucket, a tool of the log takes its access token from the metadata server of Cloud Run. When that server does not answer in 2 seconds, the tool takes the token of the Google sign-in of the person: the output of
gcloud auth print-access-token. -
01M48VFX22S4811DYBBD7QDW24
riff-serverruns each command with a call ID one time only. The call ID is in the headerriff-call. The key of a call is its caller and its call ID. The writer keeps the result of each accepted call: its records and its note. A second try of a kept call runs nohandle. Its reply comes from the kept result, on the written state of now, and has the headerriff-repeat: 1. A query and a signal take no call ID. -
01M48VFX8K93F8XRWDB2BDP240 A second try of a call whose first try waits for the writer waits for the same entry. Then it gets the reply of the kept call.
-
01M48VFXBGBW3PTC2JNHYNSE0W A refused command keeps no key. A second try of it runs
handleagain. -
01M48VFXEE2GGT7JE10DWBNZEV The server keeps a call for
CALL_KEEP(24 hours), and at mostCALL_KEEP_MOST(1024) calls of each caller. The oldest call goes first. -
01M48VFFY5CK9MRXJESV2NHY5F The envelope of each record of a command with a call ID has the field
call, next tobyandcommand. A record with nocallis of a command with no call ID. A build that does not know the field skips it. -
01M48VFXHHND8SX4DBXZTFMJGQ A checkpoint keeps the records of each kept call, in the field
calls. A load makes the kept calls from the checkpoint and from each record after it, with the default note. So a start from a checkpoint and a start from the full log give the same reply to a repeated call. A command that made no record is not kept after a start. -
01M48VFXMHBT491BA5XYSRB3BA A rollback to a build with no call ID loses the kept calls. For
CALL_KEEPafter it, a repeated command can run two times. The steps of a rollback say so.
Cloud
- R5
riff-serverruns on Google Cloud Run. It runs with--require-sign-in. For now, its public URL is its Cloud Run URL. The book never names that URL. - R181 For now, no shared server runs, and
CLOUD_DEPLOYis not set. Each person runsriff-serveron their own machine. The shared server runs again when the open issues are done. - R6 Later, its public URL is
https://riff.comotechnologies.io. Cloud Run then maps the domain to the service. Google manages the certificate. - R29 Only one instance of
riff-serverserves at a time. An instance is one runningriff-serverprocess. Cloud Run keeps one instance, with its CPU on also between calls. During a deploy, a second instance runs for a short time. The lease (R137) stops one of them. - R130 An instance takes up to 1000 calls at a time. Each open stream is one call.
- R131 Cloud Run ends each call after 60 minutes.
riff watchandriff tailthen connect again. - R132
rifftries a call again after a fault, while the budget of the call lasts (01M4A803Z4Q0KX6NT1KC6QR43H, 01M4A8041F8EK1VYDE4C9QG8N8). A 503 of the server is a fault. - 01M3THEE5V3RFHF9QTA8MA8QDF When a call waits for more than 1
second while the server replies 503,
riffshows one dim line(waits for riff-server…)on stderr. It shows the line one time for each gap, also with more than one call.riff chatshows the line above its prompt.riff topkeeps its table. - 01M3TJWJ9914B7Z5EQJF310REK
rifftries a refused connect to a loopback address again only when its process got a reply from that server before (01M4A804683G1EXM53893VHW7S). It then waits as for a 503 (R132), and shows the same line. A process that got no reply from the server fails at once. - R148
riff watch,riff tail,riff chat,riff topandriff workers hostconnect a stream again at once when it ends. The open of a stream is one try. When it fails, they try again afterSTREAM_RETRY, 5 seconds. They stop only when the person stops them. - R133
riffuseshttp://127.0.0.1:7878, the server on the same machine, when no server is set.--serverorRIFF_SERVERnames another server, for example the shared server (R5). - 01M3K0Q7X3FEKWZK0B3854C4RV
rifffinds its server only in--serverorRIFF_SERVER. It keeps no chosen server in a file. A person setsRIFF_SERVERin the profile of the shell, so each new session uses it. - 01M3K0Q80BCZQD7DNQQ333ZN09
--serverandRIFF_SERVERtake a URL,HOSTorHOST:PORT. With no scheme,riffuseshttp://, and port 7878 when there is no port. A bare IPv6 address gets brackets in the URL. - 01M3Q5VE74608N5H2M73RB6Y2Z
riff servershows the server thatriffuses, and where that choice comes from:--server,RIFF_SERVERor the default. For that server, it shows if the server answers, its build and the sign-in. 01M3NTEMQAY1Z10H1GX2K6PEAH says when it shows the server of the same machine. - 01M3NTEMQAY1Z10H1GX2K6PEAH
riff servershows a short aligned table, one fact on a line: the release ofriffwith its commit and date short, the server with where it comes from, its release and the sign-in. The server of the same machine shows only when it answers or is the server ofriff. When the person must act, the last line says what to run: yellow when the versions can talk, red when they cannot or when the person must sign in.--colorworks as inriff tail. - 01M56Q55SHHJSKJWNHMW79X7HA The last line of
riff serverand the note of eachriffprocess sayriff updateonly when the server runs a newer build thanriff. Whenriffis newer, also on the same line, they say “Update riff-server when you can.”, orriff serversays “Update riff-server.” when the versions cannot talk. - 01M3Q5V313XQN86BA2PBTXHEZC Each
riffcommand that shows facts or a list has one look. Facts arekey valuelines, aligned. A list is a table with a header row. A row shows a short session ID (8 characters).--longshows the full URI or session ID in its place; a row never shows both. A status is on the row of its session, in its own column. A setting shows askey value (file), and one dim line at the end says how to change it. An action that the person must take comes last, in yellow or red. - 01M3Q63NQQT3G30GMS55FY4499
riff whoamishows the factssession,uri,riffandbuild. When riff cannot read the state of the riff, the factriffsaysunknownand the reason, in red. - 01M3Q5VE4VVXT9FH4J4MAWX68V
riffreadsRIFF_SERVERitself. A usage error of ariffcommand does not name--serveras a required argument, also whenRIFF_SERVERis set. - 01M3MRMB2M0RM1JFVJ0W695SHB
riff servershows the release ofriff, and the release of each server that answers with a build. - 01M3MX598VTWZ02R7J6AYJB2E5 Only the network counts in the wait
of
riff serverfor an answer. A slow keyring does not make a server that answers show “no answer”. - 01M3MNT26K77E4RDHH42SB5AEG Two server URLs name one riff when
they have the same port, and the same host or two loopback names,
for example
localhostand127.0.0.1.riff servershows one riff once. - R32 The OIDC client secret is in Secret Manager. Cloud Run gives
it to
riff-serverasRIFF_OIDC_CLIENT_SECRET. - R134
riff-serverruns as its own service account. The account can read and write only its bucket, and read only its secret. The bucket is private. - R135 The image holds only the
riff-serverbinary and CA certificates. It runs as a user that is not root. - R151 Cloud Build builds the image as its own service account. That account can only build and store images.
- 01M3NJAZ6BYH7TWKDYTVEK78PG CI deploys
riff-serverwhen the repository variableCLOUD_DEPLOYistrue, in two cases: a push of a release tagvX.Y.Z, after the jobRelease checkof the tag passes; or a person runs the CI workflow onmainwith the inputtag, after the gate passes. CI builds the image of that release, pushes it to the image repository of the project, and deploys it. One deploy runs at a time. - 01M3NJAZ8HSE87H0GZ8SNGWN31 A push to
mainnever deploys the shared server. A tag that is notvX.Y.Znever deploys. The release tag at the end of a wave deploys the release (R216). A run by hand is for a redeploy or a rollback to an older release. - 01M3N73AW2J3TVSZWFJ88A91PG A release is a git tag
vX.Y.Z. X.Y.Z is the version of the crates inCargo.tomlandCargo.lock. The rules 01M3N73E5JWFTYZ90JX5AVFGEP to 01M53N7KN4QKZ70W2GNQ3VQWQ9 pick the level of the release. CI fails a pushed tagv*whose version is not the version of the crates. - 01M3MRMAVVKJ5WS8GWCJHWH0R4
riff updateinstalls a release, not the head ofmain. With--tag vX.Y.Z, it installs that release. With no--tag, it installs the release that the server ofriffruns, from the version of its build, also when a newer tag exists. Whenriffuses the server of the same machine, it installs the release that GitHub marks “Latest” (01M510MPR1S0EQNRK2195XTXG5). - 01M3N73Y9DMVMCV0PJE1R8YCFH When the server of
riffanswers with no build thatriff updatecan read,riff updateinstalls the release that GitHub marks “Latest” (01M510MPR1S0EQNRK2195XTXG5), and says so in one line. When the server does not answer,riff updateinstalls nothing and names--tag. - 01M510MPR1S0EQNRK2195XTXG5 Where 01M3MRMAVVKJ5WS8GWCJHWH0R4 and
01M3N73Y9DMVMCV0PJE1R8YCFH say the newest release tag,
riff updateinstalls the release that GitHub marks “Latest” (01M4YTW8788K4XYTEMRSXDH7B9), not the highest tag. It asks the API of GitHub, with no sign-in. WithRIFF_RELEASES, it reads the filelatestthere, in the form of the reply of the API. When it finds no Latest release, it installs nothing and names--tag. - 01M3MRMAY3P1K151RGAP9K6GSH The deploy takes a release tag as its
input. It checks out that tag and deploys only it. It refuses an input
that is not a tag
vX.Y.Zof the version of the crates. - 01M3MRMB0AJVPD952AQYD7X1RN Only an admin of the repository makes
a release: the tag ruleset
releasesonv*lets only the repository admin role create, move or delete a tag.just githubmakes it. - 01M3NB3EWE2V9PCTMNTZAKEXMA Each release has a GitHub release on
its tag. Its notes start with the level line of the release pull
request and the command that each person runs. GitHub generates the
rest: each pull request merged since the last release, except the
pull requests with the label
release. The GitHub releases are the changelog. The repository has no changelog file. - 01M40AB1D5KQ4SERC6M709Z3JF The generated notes of a release put
the pull requests in three groups, in this order:
Changes(each pull request with neither the labeldesignnorinternal),Design(the labeldesign) andInternal(the labelinternal)..github/release.ymlsets the groups. - 01M3NJAZAQ3AKMAM0EGM7R3S89 CI signs in to Google Cloud with the
OIDC token of GitHub. No key exists. Only the
mainbranch and the tagsv*of the repository can sign in. The deploy account can push images, deploy the service, and run it asriff-server. - 01M49M8W30M2084QN4HX1FJFKS Only a CI job in the GitHub environment
of an instance (
CLOUD_GITHUB_ENVIRONMENT) signs in as its deploy account:productionfor the shared riff,stagefor the stage. No deploy account takes each job of the repository. The binding names the attributeenvironmentof the job, not its subject.riff cloud createsets it. - R152 Cloud Run lets each caller in.
riff-serverchecks each token itself (R5). - R143 The Google Cloud project
como-riffholds each cloud resource of riff. It holds nothing else. - R144
deploy/cloud/shared.envholds the cloud settings and the OAuth client ID of the shared riff. The repository is public. No file in it holds the account data of a real person: an email address, a billing account ID or an organization ID. - R136 A person makes the project and links its billing account with
gcloud, by the how-to in the book.
riff cloud createmakes the resources of riff in the project. It checks each resource first, so it can run again.riff cloud deploywith no tag builds the image and deploys it to Cloud Run. - 01M3TJWJEPTSF1S3S5PJD25Z7Y The bucket is a standard bucket with
object versioning. A lifecycle rule deletes each older version of an
object after 7 days. The service has 1 GiB of memory.
riff cloud createsets each of them, and each deploy sets the memory. - 01M3TJWJ6J3M6JRXJTAETZ5M6F
riff cloud createmakes an alert in the cloud project: a log line of the service with the severityERRORor more sends an email to the owner, at most one each 5 minutes. The email comes fromRIFF_OWNER. With noRIFF_OWNER, the setup makes no alert, and says how to make it.riff cloud log NAME --errorsshows these log lines. - R145 A person makes the OAuth client by hand in the console, with
the how-to in the book.
riff cloud signinputs the client secret in Secret Manager and the client ID in the settings file. The secret is never in the repository or in a downloaded file. - 01M496JTN962N0AX378MA1MBPM The stage is a second riff-server in
the project, for the rehearsal of a release and the check of each
merge.
deploy/cloud/stage.envholds its settings. It has its own service, bucket, accounts, sign-in client and secret, and no alert or domain. No setting of one riff names the bucket, the service, the secret or another resource of a different riff. A test proves it. - 01M3ZE3Z580RB5AYAJX6321DFW
riff cloudtakes the settings of a riff by name:stagereadsdeploy/cloud/stage.env, andsharedreadsdeploy/cloud/shared.env, the shared riff.riff cloud deploy NAME TAGdeploys the image that CI built for the release tag. Noriff cloudcommand changes the GitHub variableCLOUD_DEPLOY. - 01M3ZE3Z80274JFTN53DNJ2F2G A release that moves the state of the shared riff is rehearsed on the stage before its tag, by the how-to in the book. The results go on its issue.
- 01M3ZGRZ0F3G93Q3ET9GGWKT7E
riff cloud loggives its filter togcloudwhole, with its spaces and quotes. With no--limit, it shows the last 50 lines. - 01M4262DQ9RNFNJ07CRTSGEAM1
riff cloudmakes and runs riff-server instances on Cloud Run with thegcloudof the machine, asriff prrunsgh:create,signin,deploy,list,status,loganddelete. The shared riff and the stage take the same code path. The repository has no cloud scripts and nojust cloud. - 01M4262DSKVWP4064FKSMAQACZ Each instance has one settings file
NAME.env: indeploy/cloudof the repository of the directory when it has that folder, else incloudbeside the riff settings of the machine.riff cloud create NAME --project P --region Rwrites the file of a new instance. Its service, bucket, accounts and secret take the name of the instance, so its bucket has its own riff ID. - 01M4262DVY8QCSZS61VDQ61SB3
riff cloud deleteasks for the name of the instance first.riff cloud deployasks for it when the settings haveCLOUD_CONFIRM=true, as the shared riff has. With no terminal, only--confirm NAMEgives it.deletekeeps the bucket unless--with-state. - 01M558C4VZ2PSMEYPAZBRYW1VV These riff commands write outside the
session that runs them, to the riff, the forge or the cloud: the
writes of
riff cloud:create,signin,deploy,smokeanddelete. Each one refuses, and changes nothing, in a worker (RIFF_WORKER=1) and in an agent session that is not the lead of its user. They use one check in code, and this check is the guard. A person in a plain terminal and the lead can run them. The flag settings of a worker deny the plain shape of each write ofriff cloud(riff cloud WRITE ...) as a second layer. A write with a global flag before it passes these rules, and the check in code refuses it. Each session can run the reads ofriff cloud:list,statusandlog. - 01M4262E0QJHCZXNH7EFG7FXN2 The CI deploy job deploys with
riff cloud deploy shared TAG --confirm shared, from the riff of the tag. - 01M496K1KN392S11YEH33JGK9N The CI deploy job of a release runs in
the GitHub environment
production. It waits for the approval of the reviewer of that environment, the owner, and deploys only after it. - 01M496JT94NQ686GVSY5CCGZK7 Each push to
maindeploys to the stage in CI, when the repository variableSTAGE_DEPLOYistrue, after the gate of the push passes. The job runs in the GitHub environmentstage, with no approval, and signs in to Google Cloud as the deploy account of the stage. One stage deploy runs at a time. - 01M496JTDB16G52G22CJZRA8J0 The stage job tags the image of a push
with the full ID of its commit, and deploys it with
riff cloud deploy stage COMMIT. Only an instance withCLOUD_CONFIRM=falsetakes the image of a commit. The shared riff takes only a release tag. - 01M496JT648QS9WTE1QVHAJEE5 The setting
CLOUD_MIN_INSTANCESis the least number of instances of a service: 1, or 0. With no value it is 1. The shared riff has 1. The stage has 0, so it scales to zero between calls. - 01M496JTHN19BZ7YN94993R35X After each stage deploy, CI runs
riff cloud smoke stage. It signs in as a test account with the refresh token of the provider inRIFF_SMOKE_TOKEN, a secret of the environmentstage. It registers a session, posts a message to the threadriff/smoke, reads it back, checks that the server runs the build of theriffthat runs the test, and ends the session. Each step prints one line. A failed step stops the test with its name and a code that is not 0. Only the stage admits the test account. - 01M4382RKERWAPKBRY9W8F2GSA When a
gcloudcall ofriff cloudfails, riff prints the error ofgcloudin one line and exits with a code that is not 0. An ended sign-in givesgcloud: the sign-in ended: run gcloud auth login. riff says that a resource is missing only whengcloudreplies that it does not exist. A reply that the account has no permission is an error, also when it says that the resource may not exist.
One instance
- R137 The lease is an object in the bucket. It holds the ID of the instance that may serve.
- R138 At start, an instance loads its state (01M3THEE08ZKV8WGHDSVWV69ZE). Then it makes a random ID and writes it to the lease. It then waits 15 seconds, applies the chunks that came since the load, and starts to serve.
- 01M52JYGHHK5X0XS39VFT3ZGGM The start order of an instance with a store is: load, then the port, then the lease. The load reads the sign-ins, the newest checkpoint and the log after it, with no lease. When the load fails, the instance exits. It opens no port and writes no lease, so the old instance serves on. After the wait for the lease, the instance applies the chunks that came since the load, and loads the sign-ins again.
- 01M3TJWJC08ZR5TWA1Y9CDE0QM After the wait for the lease, the instance also lists the checkpoints again. When a checkpoint came since its load, it takes the position of the newest one. It writes no checkpoint when that one comes from a later version, or does not read (01M3TBZBQDF0ES4KM54FJQF6Z8).
- 01M51X8MG0YQT5KBZC9H7917J3 When an instance reads the ID of another instance in the lease, it stops as in R140. Then it writes a last checkpoint at its written position: the hand-over. It does not prune. It writes none when the lease holds the ID of a tool, when the holder ended the lease, when its own shutdown began, or when it writes no checkpoint (01M3TBZBQDF0ES4KM54FJQF6Z8). A shutdown waits for the end of that write.
- 01M53T8XCTVEXGJVF9GSYVAT7Y At the hand-over, before the checkpoint, the instance also saves the token store when it changed since its last save. The save names the version that the instance knows (R141). It does not wait for the second of R127. It has a time limit of half the wait for the lease. A save that waits for its second when the instance stops ends at once. So a token from the last second of the old instance works on the new instance: the new one loads the sign-ins again after its wait.
- 01M51X8P4M8PHSSJMWF1J1KQS1 After the wait for the lease, when a checkpoint came since the load, the instance loads its state again from the newest checkpoint that reads, and replays the log after it. So the read cursors, the statuses and the last calls of the old instance stay after a deploy.
- 01M52JYGM7WH565B7PCVBEJC17
riff-serveropens its port after the load, before it takes the lease. Until it serves, it replies 503 to each call, with the build of riff-server. So at an instance change, Cloud Run always has an open port for the calls. Before the end of the load, each connect is refused. - R139 An instance reads the lease every 2 seconds. It serves only for 5 seconds after the last read that showed its own ID. Else it replies 503.
- 01M52JYGPX5ETP3HKR0S0Y0YCR An instance that reads another ID in the lease stops for good. It closes each stream, replies 503 to each call in flight, and saves nothing more, except the checkpoint and the token store of the hand-over (01M51X8MG0YQT5KBZC9H7917J3, 01M53T8XCTVEXGJVF9GSYVAT7Y). It closes its port at once, so it takes no new call, and Cloud Run sends each call to the new instance. It exits when the hand-over is written, at most 10 seconds after the stop.
- R141 Each save of the token store names the version that the instance knows. Each write of a chunk is only when new (01M3T411BZQB8N4D2S0JFVESMS). When the bucket holds another version, or a chunk with the same name, the save fails. The instance then stops as in R140.
- R155 An instance saves and writes only in the time that R139 gives it to serve. The writer checks it before each try of a chunk.
- R156 A new instance starts to serve at a whole second. When the lease shows another ID after its wait, it exits and does not serve.
- 01M53NHJXGK1PN2A9F7NSX0B4G The lease also holds the revision of
its instance: the number of its Cloud Run revision, from
K_REVISION. The number gives the order of the deploys. An instance of an older revision does not write over a live lease of a newer revision. It replies 503, reads the lease every 2 seconds, and takes the lease only when that lease ended. When the instance or the holder has no revision, the newest instance takes the lease (R138). - 01M56T4SZ4Y6PSY1VTVA3758FT A SIGTERM before the end of the wait for the lease ends the start of an instance at once. The instance ends the lease that it took, closes its port and exits with success. It saves nothing: it has no state to save yet. So an instance of an older revision takes the lease at its next read. When the end of the lease fails, the lease ends by its age. After a crash, the lease also ends by its age.
- 01M3X34282SG0DJ6X34F90HS26 The lease also holds a time. An instance writes the time when it takes the lease, and again every 30 seconds. While a write of the time that is due fails, the instance does not serve (R139).
- 01M3X5TPBMF81TDVZ7Q4NVXBQX An instance whose last good write of
the time to the lease is 90 seconds old stops for good, as in R140. It
does not read or write the lease again, and it does not serve again
from its state in memory. It counts the 90 seconds on the monotonic
clock and on the wall clock.
riff-serverthen exits at once with an error that says why, so that a new instance loads the state from the store. A shorter fault gives 503, and the instance goes on. - 01M3X342ARX5Y7R9ZJDT12R9A1 On a shutdown (R129), an instance that holds the lease marks the lease as ended, after it saved.
- 01M3X342DH98YEZ3X5CND43DGD A lease is live until its instance ends it, and for 90 seconds after its time. A tool reads the time with its own clock, which can be at most 50 seconds ahead of the clock of the instance. A lease with no time is from an older build. It is live until a person deletes the object. The refusal of a tool names the object.
- 01M3X342RCXX2GGK879VYK06TS The shared server runs with CPU always on and with exactly one instance. So an instance writes the time to the lease also when it gets no call.
Sessions
- R8 A new message can wake an idle session.
- R9 A claim ends 5 minutes after the last sign of life of its session (R204), unless the same process comes back first, for example after a short network fault. A session that waits for its user keeps its claims. A claim ends at once when its session ends (R205) or starts again (01M3JEE1QQCFS5TMZW5N2DAD2D).
- 01M3JEE1SWR05DWQA5WQ8AXFTF The context of a new start names each claim that the start freed. It tells the session to claim an item again before it goes on with it, and to pick up the earlier work (01M3JEE1W32CMQP8CP2HJ829E7).
- 01M3JEE1W32CMQP8CP2HJ829E7 When a session takes an item, it looks for the work of an earlier session on the item before it starts: a pushed branch, or a worktree on its machine with no live session. It goes on from that work, or starts again. Its start post says which, and why.
- 01M3JY13Y75S9S0SMK5XQ529AD When a session starts an item again, it
deletes the pushed branch of the earlier work first
(
git push origin --delete worktree-ITEM). Its start post says so. - 01M3WFYEKTWVVZ1FWVNQMGBNN0 A session commits its work as a WIP
commit and pushes its branch before each long run (
just ci, a test loop, a build) and at each change of step. A WIP commit hasWIPin its subject. The pull request merges with a squash, so no WIP commit shows on the default branch. “Start routine” in the instructions of the riff tools says so. A WIP push needs no rebase (01M3MNP39172Y463WGQAW125KW). - 01M3WFYEP1H3VPW8G90KQDE6FW A session that claims an item with a pushed branch, or with a worktree of a session that is gone, goes on from that work. It first commits the files of that worktree that are not committed, and pushes them. It starts again only when the earlier work is wrong. Its start post says what it found.
- 01M3WFYER9QWA698KY2E1HNTCW The answer to a granted claim names
the earlier work on the item, from
riff claimand from theclaimtool: each branch oforiginwith its last commit, and each worktree on the machine with the count of its files that are not committed and of its commits that are not pushed. riff runsgit fetch --prune originfirst, for at most 5 seconds. A branch or a worktree belongs to the item when its name holds the item as a whole word. A verify claim gets no such line. riff does not show the subject of a commit. - 01M3ZT825YAA5FW85YXH7JR1K0 riff counts no commit of a worktree as
not pushed when the default branch of
originholds its work: a merge ofHEADinto that branch changes nothing. This is so after a squash merge deleted the branch of the worktree. - R35 A session has a URI:
riff://USER@HOST/OWNER/REPO?session=ID&lead=true&claim=ITEM#WORKTREE. It shows who the session is, where it works and what it works on. - R36 Who: USER comes from the sign-in. ID is the session ID of the
agent tool. For Claude Code, this is
CLAUDE_CODE_SESSION_ID. - R157 When
riffcannot read the sign-in from the keyring, the command stops and says why.riffusesUSERonly when there is no sign-in. - R158 When
riffcannot open the keyring, the command stops, unlessRIFF_USERis set. WithRIFF_USER, it sends no token. - R226 When
riffgets no token, it asksriff-serverif it has sign-in. At a riff with no sign-in, the command stops and says to runriff logout, while this machine keeps a sign-in for that riff. When that sign-in is gone, it says to start the agent session again, or to run the command again.riffkeeps the old sign-in until the person runsriff logout. At a riff with sign-in, the riff ID finds an old sign-in (01M3JNVBRS35B3CD67367JF7SJ). - 01M3JNVBRS35B3CD67367JF7SJ
riffkeeps the riff ID with each sign-in. Before its first token in a process,riffcompares it with the riff ID of the server. When they differ, or the sign-in has none,riffremoves the sign-in, and the command stops with “This riff is new. Run riff login.” The next command runs with no sign-in. Whenriffcannot ask the server, it goes on. - R227 No error tells a person to run
riff loginat a riff with no sign-in. - R159 A session keeps the user of its first call. The server
refuses a call with a known session ID and another user, with status
409. The reply tells the session to set
RIFF_USERor to sign in. - R55 Where: HOST, OWNER/REPO and WORKTREE come from the machine and
from git. They are true at the start and change with
move. - R56 What: the URI has one
claimpart for each claim that the session holds. It has none when the session holds no claim. - R175 Each person has at most one lead session in each
repository. The lead is the session that the person works in. The
URI of the lead has
lead=true, sowhoshows the lead. - R228 The lead conducts the other sessions of its person in its repository. It never sends a request to a session of another person. The people of a repository agree among themselves: riff picks no lead for all people.
- 01M3JD5QCVCB3VEK4KP955JSEA The lead takes no claims: no work item and no verify. A verify request waits for a free session that is not the lead. The instructions of the lead say so.
- R230 A request is a direct message from the lead of the person of a session. A verified request (R199) counts as a scope from that person. A request that is not verified, or that comes from a session that is not the lead of the person, is advice (R10). A scope from the person wins over a request.
- R231 A session that does a request of its lead reports back to
the lead with
tell lead: when it starts and when it finishes. When it is blocked, theblockedtool tells the lead (01M41FZPGEK4TNPSM2051W4VMS). - R232 A session asks only the lead of its own person for a
decision.
tell leadpicks the lead of the person of the sender. - 01M3JDW9WN7KFGVY6HMCP2XN8B A session that is not the lead never
asks its person in its own terminal, also when a permission refusal
blocks it. It asks with
tell lead, and names the refused action. The lead shows the question to the person. - 01M3JDW9YQQHSZC296ZCNV2V8A A permission refusal of a step of the
pull request, for example a push of the branch or
gh pr create, is a question for the person. The session tells the lead the pull request, the commit and the verify result. The session never asks to push to the default branch (01M3JFEXJG2D651PWA30DNRGWF). - R178 A lead counts only while it holds (R9) and works in its repository. A lead that comes back counts again, unless another session became the lead. A lead that leaves the repository thread is not the lead any more. While no lead counts, the person has no lead in that repository.
- R37 The main worktree has no
#WORKTREEpart. - R38 The session ID makes each URI unique. Two sessions in one worktree have different URIs.
- R58 A session keeps its session ID for its life. A resumed session keeps its ID. Messages to an idle session wait for it.
- 01M56RB1DQ396DQ75Q23G87C28 The local folder of riff is private.
riff makes it with mode 0700, and each file in it with mode 0600, in
each place:
$RIFF_HOME/state,$XDG_RUNTIME_DIR/riff,$XDG_STATE_HOME/riffand~/.local/state/riff. - 01M56RB1FZ575KQYHW9FF716CZ At its start, each riff process gives each local folder of riff that exists mode 0700, and each file in it mode 0600. When it cannot, it says why on stderr, and goes on.
- R59 A session ID is not a secret. It never gives access.
- R64 The
movetool gives a session a new place. Its session ID and its claims stay. - 01M517N7ANC06S4GTEEZHAB255 The reply of
moveandriff whoamishow the URI of the session fromriff-server, with its claims andlead=true. - 01M517N7FRG389BETVWA1SX5YD Only these steps end a claim: its
release, a release of the lead for the session, the end of the
session (R205), a new start of the session
(01M3JEE1QQCFS5TMZW5N2DAD2D), and a stop with no end (R9).
move, a keep-alive,statusand each other call keep each claim. - 01M51JBEQRWWZKKP3GWW8QX96C When a new start of a live session
(01M3JEE1QQCFS5TMZW5N2DAD2D) or a release of the lead for it
(01M3WG243BW7P6E1ME0DFNQF8C) ends its claims,
riff-serversends that session one direct message fromriff@server, of the kindmessage, so it wakes. The message names each claim, the reason and the step: after a new start, claim the item again to go on, or stop the work on it; after a release of the lead, stop the work on it. It is in the chunk of the command, after thereleasedrecords. A session that is gone gets no message. The release of the holder itself sends none. - R65 A person who posts from the command line has no session ID.
The sender is
riff://USER@HOST. A selector withuserreaches the person throughriff tail. - 01M3MWW8KYJ3ZV91X22RBSAF33 A post of a person names the host where its command ran, also when the user has sessions or commands on other hosts.
- R39 People see the short form
USER@HOST:REPO#WORKTREE. Riff does not route on the short form. - 01M3JDCA6R894JG6SDJ2R7AFMN
riff tailshows each message as a block. The header has the local time, the sender in bold, the address, the markverifiedornot verified, and the number. Each session has its own color, from a hash of its session ID. A person has another style. A date line comes before the first message of a day. The body is under the header, with an indent, wrapped to the width of the terminal. The status lines go to stderr: a warning is yellow, an error is red. The MCPreadtool andriff watchstay plain. - 01M3JDCA9070MY30AYHK3Y67EF
riff tail --color <auto|always|never>controls the color. The defaultautouses color only when stdout is a terminal, and obeysNO_COLORandCLICOLOR_FORCE. - 01M3JDCAB7K6QA58HDTN9BR1AH Before
riff tailprints a message, it removes each escape sequence and each control character from the body and the names. It keeps newlines and tabs. A body cannot change the terminal. - 01M3ZRQY6YQ8QAKZPGWH1XD6WW Each text that riff reads from the
forge goes through one filter before riff prints it, stores it, or
puts it in a message, a status or a comment: the title of an issue,
a pull request or a wave, a login, a branch name, a check, a
commit, a URL and an error of
gh. The filter removes each escape sequence and each control character, makes each line break and tab a space, and keeps at most 256 characters. A body of the forge is only read for numbers and trailers, and is never printed.riff pr openrefuses a wave name that the filter changes. - 01M3Q63MVZ74WPNBA3QJYQGHFG
riff whoshows the facts of the riff, then a table with a row for each session. The facts areriff(runninggreen,pausedyellow, with who set the pause),pausedfor each repository that is paused,ownerandbuild. The columns are SESSION, STATE, ROLE and DETAIL. SESSION is the name with the short session ID, bold, in the same color as inriff tail. STATE and DETAIL are the state of the session and its detail (01M3QB6CJ1XCQG5B1BVR8AF3B4). ROLE hasyouin bold and the tags. With--long, the column URI takes the place of SESSION. The styles come from one shared module. The MCPwhotool stays plain. - 01M3Q5VE2D244XDZRYXM8DNSRS
--color <auto|always|never>is an option of eachriffcommand.autouses color only when stdout is a terminal, and obeysNO_COLORandCLICOLOR_FORCE. A pipe gets plain text. - 01M3Q63NK0AHM25MB258B0K8XP
riff whonames the owner of the riff in the factowner:USER (EMAIL), ornone. A riff with no owner ends with the line that namesriff owner --take, in yellow. A riff with no sign-in shows no owner. The owner is a person: only a row with no session of the owner gets the tagowner. The MCPwhotool shows the owner lineThe owner is USER (EMAIL).orThe riff has no owner., plain. - 01M3NT4M159EHN5W8JRTQ417N4 A session has a role:
lead,worker, or none. A worker (RIFF_WORKER=1) says so in each register call, and the server keeps it and gives it inwho.riff who, the MCPwhotool andriff toptag a session with its role,leadorworker, and with its kind (01M510MG2YSETB2CCAS8XDHPZ0), the same on each machine. No session gets the tagowner. - 01M510MG2YSETB2CCAS8XDHPZ0
riff who, the MCPwhotool andriff toptag a session in a containercontainer, in place ofworker. The lead in a container haslead container. The session ofriff workers hosthas the taghost: its status is the status of a host. - 01M589HH6HCWYJP1ZC7MRAXZ6E The session of the riff daemon has the
kind and the tag
daemoninriff who, the MCPwhotool andriff top: its status starts withdaemon host:. It comes after the host in the order of the kinds. The line of a host inriff topcounts it indaemons. - 01M589HH8Z1TCYDCZHQQHTYWMK The session of
riff workers hostand the session of the riff daemon take no claims. So anidleone does not showready for work for. The session ofriff workers hostshowshosts workers forand the time. The session of the riff daemon showshosts containers forand the time. - 01M3NT4M3A4E3K5S2NM7MS6PQD The reply to
wholists each member of the riff with a USER, also when away: the USER, the role (owner,adminormember), whether a session of the person is live, and the seconds since the last call of a session of the person. A riff with no sign-in lists none. - 01M3NT4M5D36KTZ5XZMDP6QFQT
riff topshows a tree with├─and└─(01M42KHN33M4K13GKTX2WM6CMM). The person line has the USER in a bold color, the tagowneroradmin, and the state of the person (01M3QB6CJ1XCQG5B1BVR8AF3B4). Each member ofwhogets a line, also when away; with no sign-in, each user of a session. People come by USER, hosts by name, and repositories byOWNER/REPO. In a repository, blocked sessions come first, then by session ID. A person on the command line gets no session line. - 01M3NB54P1RBHTA5TKXP8BMY3K
riff topshows a live table of the sessions ofriff who, and draws it again in place every 3 seconds and after each message of the repository thread, until Ctrl-C.riff top --onceprints one table and exits. The header has the facts ofriff who: the state, the owner and the build. A board of the current wave of each repository follows (01M42KHN80V49HDDZF953HXDT0): the wave, then one line for each group of its open items,free,held,claimedandverify(01M3Z9N6X92KT051P10CKKV7EK). The first line of a session has the short session ID, the tag of its role, and its state. Under it comes one line for each fact of the detail of the state (01M3QB6CJ1XCQG5B1BVR8AF3B4). The titles and the wave come fromgh issue list, kept for one minute. With nogh, the rows still print.--colorworks as inriff who. - 01M3Z9N6X92KT051P10CKKV7EK On the board of
riff top, an item with no claim whose pull request waits for a verify or for the merge is inverify, not infree. An item with a failed verify is infree.riff topreads the open pull requests withgh pr list, and keeps them as long as the issues. - 01M58ATZ1F42DWNNB24C2HJPEY The board of
riff tophas the groupheld: each item of the wave that the lead holds, with no claim and no pull request in verify. The reason of each hold follows its items, for exampleheld: #1003 #1008 (VmLck reading at 19:40 UTC). A held item is never infree.riff topreads the holds from riff-server at each look. - 01M588G5K0B58QXD35AA5MTZ6E When the whole riff or a repository
is paused, the wave line of its board in
riff topends withpaused, and the board has no groupfree. - 01M3WNHCD659FH3Z5VYYH69WWR The first line of a session in
riff topnames its worktree after the short session ID:#WORKTREE, or nothing in the main worktree. The session is under the line of its repository. A repository line has the short name of the repository when the repositories of the sessions have one owner, elseOWNER/REPO. The wave line names the repository of its board:Wave N (OWNER/REPO). Only a claim in that repository is on its board, and a session shows the title of an issue of its own repository. - 01M42KHN33M4K13GKTX2WM6CMM The tree of
riff tophas four levels: person, host, repository, session. A person, host or repository line ends with its counts: the sessions, then the busy, idle and blocked sessions and the claims, each that is more than 0. A line with only one line under it, other than a session, takes that line after a›, so a small riff stays short. - 01M510MG5J92PYDWZSDQS8HMWK The line of a host in
riff topalso counts each kind of session, after the sessions and before the states: leads, containers, workers, hosts and daemons (01M589HH6HCWYJP1ZC7MRAXZ6E), each that is more than 0, for example6 sessions: 3 workers, 2 containers, 1 host; 2 busy, 2 idle, 4 claims. A session counts once, as the first of these that matches. The person and repository lines count no kinds. - 01M514C142K5BM2746F1D9Y448
riff topputs the mark↑, in yellow, after the short ID of each session that is out of date: the file of its riff was replaced, or its build is older than the build of the server (01M514C19H3WQP1FSX9RBVSC69). A session with no riff inwhogets no mark, and no error. - 01M514C16VG9PD70A8424J173B The line of a host in
riff topcounts the sessions that are out of date, after the kinds and before the states:2 to update. A count of 0 is not shown. - 01M42KHN80V49HDDZF953HXDT0
riff topshows a board for each repository with a live session, and for the repository of the working directory when no--userand no--hostis set and--repomatches it, in the order ofOWNER/REPO. It reads the issues and the pull requests of each one withgh, each minute. A read that fails keeps the last issues of its repository. A repository with noghread shows its sessions, no board and no error line. - 01M42KHNCBMBCT3TFBYWE339H5
riff top --user USER,--host HOSTand--repo OWNER/REPOshow only the sessions that match each flag that is set, and only the boards and the blocked lines of these sessions. A person with no session shows only with no--hostand no--repo.riff top --by repoputs the repository at the top of the tree: repository, person, host, session.--by personis the default. - 01M3QA8EZHX5B8C9CKF8Q3154X
riff topgrows down, not across. No line is wider than the terminal: the real width when riff knows it, else 80 columns. riff cuts a wider line with…. A session with no detail takes one line. No line has a column heading. - 01M3NB589WMPRSAR43BSG9SP41
riff topmakes only read calls: theriffandwhocalls ofriff who, and the stream ofriff tail. It posts nothing and wakes no session. - 01M3Z8FXE2DY34ZP75WJE1S8HR After one good look,
riff topstays open when a look fails and a new try can repair the fault: riff cannot reach the server, a connection fails in the middle of a call, the front end replies by itself, or no reply comes in the budget of a call (01M4A803Z4Q0KX6NT1KC6QR43H). It keeps the last table. Its first line is then one red line with the time of the last good look and the fault. It looks again at its interval. The line goes at the next good look.riff top --once, a first look that fails, and each other fault endriff topwith the error and a status that is not 0. - 01M53NDCJ0EF0YKDV8AARXKYCQ When a call of a look of
riff topgets no reply in its budget, the line names that budget:riff-server gave no reply in 60 secondswithSHORT_BUDGET(01M4A803Z4Q0KX6NT1KC6QR43H). The line never names the limit of one try or the interval of the look. A test that sets the budget withRIFF_LINK_BUDGET_MSsees its own budget in the line. - 01M3ZC09FA9DZPTHK31XECZ566 When a later read of
ghfails,riff topkeeps the titles and the board of its last good read ofgh. - 01M3Q53RNDJBDHVDFHJ9HCX9S1
riff setupadds the Claude Code permission rules of riff work to the project settings,.claude/settings.jsonat the top of the repository. Allow: each riff tool (mcp__plugin_riff_riff,mcp__riff),Bash(riff),Bash(riff *), and the pull request steps (gh pr create,gh pr merge --auto --squash,gh pr commentandgh pr view). Deny: a push to the default branch (origin/HEAD, elsemain) andgh pr merge --admin. It adds only the missing rules. A rule in the user, project or local settings counts as there. It keeps each other rule and key in its order, and writes the file only when it changes.riff setup --checkchanges nothing, names each missing rule, and exits with status 1 when a rule is missing. - R41 A session joins the thread
OWNER/REPOby default. - R42 A cloud session uses the host
cloud. - R100 A session is a cloud session when
CLAUDE_CODE_REMOTEistrue.RIFF_HOSTstill wins. - R43 Outside git, the URI is
riff://USER@HOST/-?session=ID#DIRECTORY. - R49 When a watch starts, it wakes the session once if an addressed message is unread.
- 01M588J4XNV1TXEPMGTKRVY9VK An unread read of one page also gives
each unread message after the page that woke the session, and that
no read gave so yet. The
readtool ofriff mcpshows them first, in their own part. For R49, such a message is no longer unread: the next watch does not wake for it. The read cursor does not move over it, so a later page shows it again in its place. - R163
riff-serverrecords the time of each call of a session.whois a call too. A keep-alive (R204) is not a call.whoshows each session aslive, or with the time since its last call, for exampleidle 2m. - R164 A gone session (R206) is not in
who.who --alllists it. The server keeps its record until it forgets the session (01M3TBZBZVH907QD359AB8TBSX), so a resumed session keeps its ID. - R206 A session is gone when it ended (R205), or when the server got no call and no keep-alive from it for 3 minutes (01M3WG240PNMQYZ7TX6Z7ZF6M9). A gone session matches no selector. A direct message to it fails and says that the session is gone. An end frees the claims of the session at once. Its lead does not count while it is gone. After a stop with no end, the claims and the lead end together, 5 minutes after the last sign of life (R9).
- R207 A call or a keep-alive from a gone session makes it live again, with the same ID, threads and read cursors. After a stop with no end, it gets back each claim that no other session took. After an end, it has no claims. A lead is the lead again, unless another session became the lead (R178).
- 01M3WG243BW7P6E1ME0DFNQF8C The lead of a user frees the claim of
another session of that user in its repository, live or gone:
riff release ITEM --session ID, or thereleasetool withsession. ID is the session ID of the holder, or a start of it of 4 or more characters. The server refuses each other caller. It posts a note in the thread of the claim that names the lead, the item and the holder. - R182 A session sets its status with
riff statusor thestatustool. A status is the current step of the session, in its own words. It makes no state. A new status replaces the old one. - R183 A status is one line. The step is not empty. The step has
at most 400 characters. The reason of a block has the same rules.
riff-serverrefuses a status or a block that breaks a rule, with status 400. - R184
riff-serverkeeps the last status of each session, with the time that the session set it. It saves the status with the session.whoshows each status and its age, for example4m ago: write the tests. - 01M3Q551WCMPQRCNJ8FXQEBFY4
whogives for each session the seconds since its claims last changed: a claim, a release, a new start of the session, or a start ofriff-server. A worker with no claim is idle for this time. - 01M3Q551YHYZBFV2NDS1QCYXCD A status is stale when the session set
it before the last change of its state: a claim or a release of the
session, or a pause or a resume of the riff or of the repository
of the session.
whomarks a stale status. A claim that the session holds already, and a set of the riff to its state, are no change. A status from before a start ofriff-serveris stale. - 01M3Q555KC1RKNEC4ZA9HQYJG2 A stale step is dim and says
stale. - 01M3QB6CJ1XCQG5B1BVR8AF3B4
riff-serverderives the state of each session and gives it inwho. No session reports its state. When awhoreply has no state, riff derives it the same way from the other facts of the reply. The first state that matches wins (01M41FZQVEF8S2W9RCM4V87C3D).riff top,riff who, the MCPwhotool andriff workersshow the word of the state, then its detail: foroffline,seenand the time, then the claims, then the last step with its age (01M522MRW21RHGF2B6F0VCP0BD); forpaused, the claims andstopped at:the step; forblocked, the reason with its age,the lead gave no answerwhen the lead gave none, then the claims; forwaiting, the claims, then what they wait for; forbusy,working on #Norreviewing #N(a verify claim) for each claim, then the work (01M41FZNTPXQNCZ1S99HE42PYQ), then the step; foridle,ready for work forthe time since the last release, then a current step. A session that takes no claims shows noready for work for(01M589HH8Z1TCYDCZHQQHTYWMK). Anidlelead showsmonitoring work forand the time, notready for work for.riff topadds the title of each issue. The colors:blockedred,waitingcyan,busygreen,idledim,offlinegrey,pausedyellow. Ablockedsession comes first inriff top(01M41FZR4XRP55M409YBCPTHPH). A person isonlinewhen a session of the person is live, elseofflinewithseenand the time since the last call. - 01M522MRW21RHGF2B6F0VCP0BD The detail of an
offlinesession isseenwith the time, then its claims, then its last step with its age:seen 5m ago,working on #15,10m ago: docs. So a session of a daemon that is offline shows its item and its status (01M519Z554SC3EAT69Q3M6JDAP). - 01M3WKCYM623M66ATHCH3QGMKP The automatic step of the lead shows
no text of a direct message: the step of a
tellistold SESSION. In TEXT, each run of white space or control characters is one space. An automatic step does not end a block of the lead (01M41FZPGEK4TNPSM2051W4VMS). A call thatriff-serverrefuses sets no step. - 01M3Q555NV8ZCQ8PVPBXQ7J82C The instructions of the riff tools,
the start hook and the
statustool do not tell a session to set its status for a fact that riff derives: a claim, a release, a pause, an idle worker, its work, or a wait for a verify, a merge or a need. - 01M41FZQVEF8S2W9RCM4V87C3D The first state that matches wins:
offline(no open watch stream, and not a lead or a session in a container that is not gone, 01M510MG0CKDXAKF6YJ768K7E8),paused(the riff or the repository of the session is paused),blocked(a block that holds, not a lead),must_clear(01M3X9XC99KY4RQY36A7CYWY11),waiting(each claim of the session waits, or a lead with a block),busy(a claim, or a lead in a turn),idle(each other session). - 01M48VDGQ5KETKPM4G6TKTC2MB A lead is live while it is not gone,
also with no open watch stream. So a lead that calls only the
command line shows in
riff whoandriff topwith its state and its status, not asoffline. Ariff statuscall and ariff stepcall are signs of life. - 01M510MG0CKDXAKF6YJ768K7E8 The riff daemon and
riff runsend the keep-alive of a session in a container with the markcontainer. The server keeps the mark until the session ends. A session with the mark is live while it is not gone, also with no open watch stream: it shows its state from its facts, notoffline. When its container stops, the daemon ends the session, and it is gone at once. With no keep-alive for 3 minutes, it is gone too. - 01M514C11BPE93PJT4221PYVHS The process that lives as long as a
session names its riff in each keep-alive: the build, and
replacedwhen Linux names the file of the processPATH (deleted). These processes areriff mcp, the riff daemon andriff runfor a session in a container, andriff workers host. Each other keep-alive, for example ofriff watch, names no riff. The server keeps the last riff that a keep-alive named, and gives it inwho. A session of an older riff names none, andwhogives none for it. - 01M48VDGTD40P8RBZMS0XB5M9N A session shows a long step with
riff step start NAME,riff step doneandriff step fail REASON. A new start replaces the old step.riff whoandriff topshow the step after the detail of each state butoffline:NAME for 12m, or in redNAME failed 3m ago: REASON. A failed step shows until the next change. Done removes it. The step is a signal, not a record of the log. - 01M48VDS663X064YS5ZGCCZSTB
riff step failsendsstep failed: NAME: REASONto the lead of the user, which wakes it. A lead sends no message to itself. - 01M48VDSB4CHQS9P6XVDJ6FMKS A lead that waits for its person
calls
blocked. It sends no message, and showswaiting for USER: REASONwith its age. A message that wakes the lead is no answer to its block. The lead is never red and never in the blocked lines ofriff top. - 01M41FZRF5HEZCDS515CP7DYCV The work, the block and the facts of
the items are signals in the memory of
riff-server, not records of the log. A start ofriff-serverloses them, and the clients send them again. - 01M57XY3Q6HEYQG27FE0CEQ7HB The plugin has a
PermissionDeniedhook for each tool:riff hook denied. The settings of each container session, the lead and each worker of the riff daemon, run/opt/riff/riff relay deniedfor it. The hook reads its input, keeps one record, starts the send as a detached process, and returns at once. It never waits for riff-server. It does nothing where riff is off, and in a session that left the riff. In a container session, the riff daemon posts the record as the session. - 01M57XY3SEG8HCXPZFBVATQ1W2 Each refusal of auto mode is one note
in the thread
denials: one line, the tags first, then a colon and the text:denied reason=R tool=T head="H" role=O user=U host=N repo=P session=S claim=C: TEXT. A note wakes no session. - 01M57XY3VP35QDP2KK88ERF82M The tags of a record:
reasonis the rule in the square brackets of the reason as a slug,no-verdict, orchecker-unavailablefor “Classifier unavailable”.toolis the tool name, with@SERVERfor an MCP tool.headis, for Bash, the first two words of the command afternice -n N,env, the variables andcd X &&; for another tool, the path, else the first field of the input; at most 60 characters.roleislead,worker(in tmux),containerorsession, with+subagentin a subagent.sessionis the first 8 characters of the session ID.claimis the claim of the session, or-. The text is the command or the input, on one line, at most 300 characters. - 01M57XY3XXCKBXMVPFBY0C2KDM No secret goes to the thread
denials. The hook replaces each secret of the session with[secret]before it keeps the record: the secret variables, the values of the MCP servers of the session and the tokens of the sign-in file ofclaude. - 01M57XY405BSXEH3K3Z977RWAW After it posts the records, the
session leaves the thread
denials. So no session sees the records of other sessions at itsread. - 01M57XY42CNR16M5G024PWKANV When riff-server or the riff daemon does not take a record, the record stays in a local file: in the local folder of riff on the host, and in the Claude folder of a container session. The next send posts the kept records first, in order.
- 01M57XY44K2N3PS9F8NQ6SZQP9
riff denialsreads the threaddenials. It counts the records byreason, bytoolandhead, byroleand byrepo, and then shows the last 20 records.--since TIME(for example7d,12h) keeps only the records of that time.--format jsonprints the records. - 01M41FZP2C4Z4J6WKRXZ5B31EH
riff-serverhas no credential of the forge. The clients send the facts of the items of their repository:riff pr open(a verify is asked),riff verify(passed or failed),riff pr wait(merged), and theriff mcpof the lead each minute, with each open pull request and each open item of eachNeeds:line. A list of the lead replaces each fact of the repository. A fact of one command replaces the fact of its item. - 01M41FZP9A50CH4A2VX344DW49 A session is
waitingwhen each of its claims waits: the author of an item with an open pull request with no verify result waits for a verify; the author, and the session that verifies, wait for the merge when the verify passed; the author of an item with an open item in itsNeeds:line waits for that item. A wait wakes nobody. It ends when its fact ends. - 01M49HAW3NXNXNX02ETDZD3YCN For a wait, an item of a
Needs:line is open only when its issue is open and has no comment that starts withMerged in #. - 01M49Q30XMVRFX42YTM1PHX0RZ When the look of the lead sees an open
pull request of an item with auto-merge on and the state
CONFLICTING, and the pull request is not in the merge queue (01M5TKPNDET56VR8WZ74FSR71F), it sends a message to the session that holds the item. When no session holds the item, the message goes to the lead. - 01M49Q316RXNATJP587DWGDNCD When the look of the lead sees a pull
request that waits for a verify, and no session claims its
verify-item, for 30 minutes, it sends a message to the lead. Averify-claim starts the 30 minutes again. - 01M49Q31FASDM7CG3JEGPYCZB9 Each message of a pull request that stops comes one time for each pull request, head commit and state. The riff daemon keeps what it told (01M5QVH3MAEPPJN9PPVDX6N2EY).
- 01M51QEYKTD7ZEB0YA0C3MQTPN The look of the lead reads the check
run
Gateof the head commit ofmaineach minute. When the Gate failed (failureortimed_out) at a commit that the lead did not get, andmainpassed before, the lead gets one message: the commit, the run, the failed jobs and the fix item. Whenmainwas broken at an older commit, the lead gets a note. A Gate that runs, a cancelled Gate and a head with no Gate give nothing. The riff daemon keeps the last broken commit that it told (01M5QVH3MAEPPJN9PPVDX6N2EY). - 01M51QEYPGD6W345MZ4F85C38F When the Gate of
mainfails, riff files one fix item: an issue with the labelbroken-mainin the current wave, with the commit, the run, the failed jobs, aNeeds:line and aDone when:line. riff makes the label when it is missing. When an issue with the label is open, riff files no new one: it puts a comment with the new commit and run on the open one. When riff cannot file the fix item, the message to the lead says why. - 01M51QEYS538XA58KDRY844T52 An open issue with the label
broken-mainis free work before each other item: its verify first, then the item. This is true for the rollout of the lead and for the wave of the daemon. It is free work also outside the current wave and outside each wave, when no session claims it, it has no commentMerged in #, no pull request of it waits for a verify or the merge, each issue of itsNeeds:line is closed, and no lead holds it. “Start routine” in the instructions of the riff tools tells a session that picks its own item to take it first. - 01M51QEYVQN74Q1VDTVHB0445Q When the Gate of
mainpasses, riff closes each open issue with the labelbroken-mainthat no session claims and that no open pull request names, with a comment that names the commit. The lead gets a note whenmainwas broken before, or when riff closed an item. The note names the items that stay open. - 01M41FZPGEK4TNPSM2051W4VMS A session that cannot go on with no
decision of a person calls the
blockedtool, or runsriff blocked REASON. One command sets the block and sendsblocked: REASONto the lead of the user, which wakes it. No command does one with no other. With no lead, the block holds, and the reply tells the session to ask its own user. Thestatustool andriff statushave no reason of a block. - 01M41FZPT31ATXP75QW965P3JB A message, not a note and not a status request, that wakes a blocked session is its answer. The block ends at the next sign of work after the answer: a fact of the hooks with a turn that runs, at or after the answer. A claim, a release and a new start of the session also end its block.
- 01M41FZQ545HQ9Q75CSKX8HF8H The
riff mcpof the lead looks at the blocks of the sessions of its user in its repository each minute, withlead.wakeminutes of the machine of the lead (default 15). A block with no answer for that time gets a second wake of the lead: a message of the server,blocked: SESSION (ITEM) has no answer after N minutes: REASON. Only the lead can look. - 01M41FZQCHWY1YVGAZ60ZHJK21 A block with no answer for
lead.wakeminutes after the second wake is unanswered.whogives it, andriff topshowsthe lead gave no answerunder the reason. An answer ends each step. - 01M41FZQKZKW131Z8822G31T5G When a block becomes unanswered, the
riff mcpof the lead shows one desktop notification withnotify-send: the short session ID, its claims and the reason, and no text of a message. A machine with no display or nonotify-sendgets none, and nothing fails.lead.notifyturns it off. It is on by default. - 01M41FZR4XRP55M409YBCPTHPH
riff tophas one red line for each blocked session before the board:blocked, the short session ID, its claims, the time that it waits,the lead gave no answerwhen the lead gave none, then the reason. Awaitingsession is cyan, not red. - 01M3MEEFH79XXNZW6DWSPTEW2A The leave holds for the life of the
session, also over
/clearand a resume:riffrecords it in a fileleft-ID, the mark of the leave. A new session joins as usual. - 01M3MEEFSD4TEQESRDJENCFW7N The tools that join and leave a thread
are
join_threadandleave_thread. - 01M3XQVK05FAT3PR43W8RNEYHY The
leavetool writes the mark before the end call. The end call is the one request that goes out with the mark. When the end call fails, the tool removes the mark, refuses, and the session stays. With no directory for the mark, the tool refuses. Thejointool removes the mark before the register. - 01M5GVQQ8NK3RD9CM3VTGXEHZQ The settings of each container session
have a
UserPromptSubmithook,/opt/riff/riff relay prompt. It tells the riff daemon of the prompt through the socket of the session, and always exits with status 0. For the lead, the daemon sends a keep-alive with a prompt of age 0 to riff-server: the next prompt of the person ends the block of the lead. A prompt in a worker ends no block. - 01M5MTWEN9F58X3B0H02DSTQG6 A riff daemon that runs the wave of a repository also serves –host for it with no riff daemon host. At its start it writes served.json from waves.json for each repository that is missing there, with the server of the wave.
- 01M5NJAXTQ0YKN7Q7XN0A7ZMPF Only a person names the lead: the
command
leadof a person names the session ID of the new lead, or its start. riff-server refusesleadfrom a session, also for itself, soriff leadin a session fails, and the riff tools have noleadtool. A worker cannot be the lead. Supersedes R177. - 01M5NJAY3FTR005TDJJY1TMGNX No session becomes the lead by itself:
a
registeror astartnever makes a lead, also for the first session of a person in a repository. Supersedes R176 and 01M3X9XA3H6YF0QCYSNB2P0CT2. - 01M5NJAYC7MXS13TA3VJB4N6RP
riff lead startand the daemon forlead movemake the new session the lead as the person, with no session ID. A person in a terminal with no session ID names a lead withriff lead --session ID, for example a session on the host when the lead container ended. - 01M5NJAYMYF50B9WXH5RF9W737 When the lead changes, riff-server sends the old lead a direct message: it is not the lead now, and the session that is. A gone old lead gets none.
- 01M5P1XN4YA3S1CWTN939DQCWW A session in a container starts
riff relay watch --followagain when its Monitor ends, and only then. The instructionstext::MCP_LEADandtext::lead_startsay so. - 01M5P1XN9EX680KQNKRHWGRWJT A new watch does not wake a session for
a message that a
readreturned, also areadwithallandafter. Such a read keeps the cursor. - 01M5R9S69W7QEMNKV3JRNV0RNB riff never guesses where a command
works. It asks git for the repository, the remote origin and the
worktree. When git says that the directory is outside git, the place
has
-for the repository. When git says that the repository has no origin, the repository islocal/NAME. When a git call fails for another reason, riff tries it 3 times, then the command fails and names the call and the reason. No session registers at a place that a failed git call made up. - 01M5TKG2P26WXHJZ10Y9J69VFJ The look of a daemon posts each message of a pull request that stops with a key: the pull request, its head commit and the state. So a wave on more hosts sends each of these messages one time to the lead of the repository. A new start of the daemon sends none of them again while the message is one of the last 200 of the repository thread.
- 01M6FYH7BFFSZQ2TAJSX8A5QQV The look of a daemon posts each message
about
mainwith a key (01M6GX8CN4ZTV0X40GD5AWPXH9). So a wave on more hosts sends each of these messages one time to the lead of the repository. A new start of the daemon sends none of them again while the message is one of the last 200 of the repository thread. - 01M6FZY6NABXGXVRYF6W1509XE The wave of a daemon posts the notices
that come from the facts of the repository with a key: an issue with
no
Done when:line (no-done-when-N), a pull request that is stuck (stuck-and the pull request, its head commit and the reason) and “Wave N is complete” (wave-complete-N). So a wave on more hosts sends each of these notices one time to the lead of the repository. A new start of the daemon sends none of them again while the message is one of the last 200 of the repository thread. The notices about one host have no key and come from each host: the host waits for a fact, the pull of the image failed, a job failed or an item is spent. - 01M6GXAFYJYG9JC76W651KF2KT The instructions of the riff tools of a
session on a host hold “Start routine” and “Pick up dropped work”.
Each text of riff that names one of these steps names these
instructions: the result of
claim, the notice of a freed claim, the context of a new start and the result ofjoin. A doc test checks each part. - 01M6GXAG10W6EJ89AQA5B4HHPJ No text of riff for a session, no requirement and no page of the book sends a reader to a Claude Code skill: riff installs none. A test checks the code of each crate, the requirements and the pages of the book.
- 01M6GX8CN4ZTV0X40GD5AWPXH9 The key of a post about
mainholds the commit, the run of the check and the state, broken or passes. The run is the number of the run and the number of the job of the check that the message names. So a rerun of the same commit gives a new key:mainthat breaks, passes on a rerun and breaks again gives the lead three messages. A second look at the same run gives the same key. Supersedes 01M6FYH7BFFSZQ2TAJSX8A5QQV in part.
Threads
-
R23 Sessions talk in named threads. A direct message is a thread with two members.
-
R24 Threads are flat. There are no nested replies.
-
R25 A thread keeps its last messages (01M3TBZBT7MME9BG1RWX5SZAZ6). A session that joins can read them.
-
R26 Only an address wakes a session. Text in a message body never wakes a session.
-
R27 A person can read and post in each thread from the command line.
-
R28 A claim belongs to a thread.
-
R48 A person can claim and release work from the command line.
riff claimexits with status 1 when another session holds the item. -
R50 A session lists and reads by default only the threads that it joined. It can read any other thread by name, except a direct thread of two other sessions (01M3T411J3TN00FER230V3YX17).
-
01M3T411J3TN00FER230V3YX17
read,tailandwatchuse one rule. The caller acts as its token (R104), and gets a direct thread only when it is one of its two sessions. Elsereadandtailreply 404, andwatchgives no wake of it.riff tailsends the URI of the caller. -
R51 A post has a
tolist of selectors. A selector names one or more of these fields:user,session,host,repo,worktree,claim,lead. A session matches a selector when each named field matches. A post wakes each session that matches one or more selectors. -
R60 Riff matches the selectors when the message is posted. A session that matches later gets no wake.
-
R61 The post result names each session that woke, and each selector that matched no session.
-
R62 A direct message is a post with one
sessionselector, or one selector withleadset to true (R179). -
R63 A person addresses a post with
riff post --to FIELD=VALUE. -
R78 A person sends a direct message with
riff tell SESSION. SESSION is a session ID, a full session URI, orlead. -
R179 The selector field
leadistruefor the lead andfalsefor each other session.tellwithleadsends a direct message to the lead of the user in the repository of the sender. The sender does not need the session ID of the lead. When the user has no other session as the lead there, thetellfails and tells the sender to ask its own user. -
01M56P1T2AZCP48RJBM01RRRZ4 A direct message with
lead=truegoes to the lead while its lead holds (R9, R125), also while the lead is gone (R206). So after a start ofriff-server, the lead gets it before it calls again. -
01M57D4FR8TPY47WPT9G66H4PE A session can send a direct message to the person of its own user: a post with one selector that names only that user. The direct thread is the thread of the session and the person. Only those two read it, and no other session joins it.
-
R79
riff readshows the unread messages of the threads of the person. It joins the person to the thread of the directory first.--threadreads one thread.--allshows each kept message. It reads each page (01M3TBZBX140GJWCV5GZ73Q5Z5). -
01M58AJE14M9TPTQE4E8R9PA9M A session names one of its direct threads
dm:PEER. PEER is the session ID of the other side or the start of it, or the user of a person.threadsandreadshow each direct thread asdm:PEER (direct with SHORT), with the first 8 characters of the session ID. Thethreadof thereadtool andriff read --threadtake this name or this label. riff looks for PEER only in the threads of the caller, so a session reads only its own direct threads. The line of a next page names the thread. -
R185 A post has a kind:
message(the default),statusornote. A post of kindstatusis a status request. It wakes the sessions that its selectors match, as each post does. A person sends one withriff post --kind status --to FIELD=VALUE. A status request needs no body. -
01M3JPMQE6S7YM4HPEVGXWK7ET A post of kind
notewakes no session. Each session that its selectors match joins the thread, so a direct note reaches its receiver.readshows a note with the wordnote. A person sends one withriff post --kind note. -
01M3JY1TBPQHH6WPPBTF42T64H In a thread, a selector with
lead=truethat matches no live session matches each live session with no claim that its other fields match. So a verify request reaches a free session of the author’s user when that user has no live lead. With a live lead, it wakes only the lead. The book says so. -
01M58AQX46RH9KFP91QAFS43N7 The sender of a post counts as a live session for the fallback of 01M3JY1TBPQHH6WPPBTF42T64H. A lead that posts to its own
lead=trueselector wakes no other session for that selector, and the post result names no unmatched selector for it. The book says so. -
R186 The wake line and
readshow that a message is a status request. A session that a status request wakes answers withstatus. It does not post a reply. -
01M3JPK82PN4F706MCHDH771MW
readdoes not give a session its own posts, and the unread counts ofthreadsleave them out.readwithallgives them. -
01M3JPK85FT5CCQPF3WDCXSMDF Each message in
readshows a short sender:USER@HOST:REPO#WORKTREE, the first 8 characters of its session ID, andlead=truefor a verified lead. A post to each session of the repository of its thread showsto all.whogives the full URI. -
01M3JPK885GPD16FPK7D05R2RC
telltakes a session ID, the start of a session ID asreadshows it, a full URI, orlead. A start that fits more than one session inwhois an error. -
01M3NB5MY93KV9RKZGGSMZW00D The people of a riff chat in the thread
chaton the riff server.riff chatis a line client in the style of IRC. It shows the history and each new line with the time andUSER@HOST, and posts each line that the person types.--colorworks as inriff tail./quitor Ctrl-C exits. The chat has no private channels, no nick changes and no files. -
01M3NB5N0D99JB5CE6RB4VEYPF A chat line wakes the lead of the sender, unless it names a lead:
@leadwakes the lead of the sender, and@USERwakes the lead of USER. The instructions of the lead tell a lead to answer a chat line with a post to the threadchat. -
01M3NJD39JVJHY5G71CD79JBY3 In a terminal,
riff chathas a prompt line at the bottom. A new line prints above the prompt, and the typed text stays. After Enter, the typed line goes away, so the line shows once: as the chat line from the server. The start line comes before the history. With a pipe,riff chathas no prompt and no line editor. -
01M3NJD3BR0XAYNNFTEY0CG761 A chat line shows a person as
<USER@HOST>, a verified lead as[USER's lead], and each other session as[USER ID], with the first 8 characters of its session ID. -
01M3NJD37CNQX580YC24S7K6ES
/me TEXTinriff chatposts an action line: a plain message with the body/me TEXT, so the riff of the release before reads it.riff chatandriff tailshow it as* USER@HOST TEXT, andreadas* USER@HOST TEXTafter the head.@leadand@USERin an action wake as in a chat line.riff chatsends no line that starts with an unknown command, and names its commands. A line that starts with//sends the line from its second/. -
01M3NK7VHXB0PAR8VH8GQQA06K A stream that ends is normal for a long poll.
riff chat,riff tailandriff workers hostconnect again at once, and show nothing for it. Only a connect that fails shows one short dim line(reconnecting…), and the next item(back). A server that riff cannot talk to shows its error. -
01M49Z4E8QB1QDXVCPE6MX5JX2 At each connect,
riff tailandriff chatopen the stream first, then read the thread after the lastseqthat they showed, then show the live messages. They show each message one time, in order: they drop each message with aseqthat they showed. At the first connect,riff tailshows only the new messages, andriff chatshows the history, or the lines after--after Nafter an update. The server keeps no cursor for it, and noLast-Event-ID. -
01M49Z4EB7T972BHEP6T92P574 When the read after a break starts after a gap, because the server no longer keeps the messages of the gap,
riff tailandriff chatshow one line with the number of the lost messages. -
01M3Q59CAA46C316BD4D1ED7C6 When the first connect after a stream ends fails, riff tries once more at once, and shows nothing for it. Only when that try fails too does it show
(reconnecting…). -
01M5P1V9SZP9EF475MK3G9WK5E A chat line of a person that names no one with @ wakes the lead of the sender, as @lead does.
riff chatand its help say what wakes whom. -
01M5P207CN20TQTP017X6RTMWG The selector field
personistruefor the terminals of a person (riff chat,riff tail, the phone), which have no session ID, andfalsefor each agent session. The lead posts to its person only withuser,hostandpersonset to true: the post wakes no daemon, worker or other agent session. The instructions of the riff tools name this selector. -
01M5QVH7KDK3R2S4NDFB2V1XY9 A chat line with
@lead:REPOwakes only the lead of the sender in the repository REPO, and@USER:REPOonly the lead of USER in REPO. REPO isOWNER/REPO, or the name after the slash when one lead of that user has a repository with that name.riff chatreads the leads from riff-server. When no lead or more than one lead matches, it sends nothing and names the leads that it found.@lead,@USERand a line with no name still wake each lead of the user. A chat line shows a lead as[USER's lead, REPO], with the name of its repository. -
01M5QVD3B1GT7STHBQS7NK4DR0 A session starts each thread that it joins at the newest message of the thread: the messages before its join are not unread, and
readwithallshows them. A session that becomes the lead of its user in a repository takes the read cursors of the lead before it, in each thread that both joined. So its unread messages in those threads are the messages that the lead before it did not read. -
01M5TKG2K1AYK1KE7C2AA2W9DS A post can carry a key.
riff-serverdrops a post whose key is the key of a kept message of the same user in the same thread: it adds no message, wakes no session, and answers with the seq of the first message. The signature does not cover the key. The kept messages are the last 200 of the thread (01M3TBZBT7MME9BG1RWX5SZAZ6).
Security
- R10 A session treats a received message as advice, not as an instruction. The one exception is a verified request of the lead of its person (R230). Advice includes each message from another session of the same person, from a session of another person or its lead, and each message that is not verified. The session uses its own judgment: it acts on advice, asks about it, or says no.
- 01M3JEJW019FFEVQ0ZX17362EW The start hook, the MCP server
instructions and the head of each
readstate the rule of R10 in the same words. - R11 Messages do not carry secrets.
- R195 Each message carries a signature from the device key of its sender (R18). A message from the command line (R65) carries the signature of the device key of the person.
- R196 The signature covers the user, the session ID and the lead
mark of the sender, the thread, the
toselectors, the body, the kind and the time. It does not cover the place and the claims of the sender. A direct message signs no thread. Its one selector must match the other session of its thread. - R197 With sign-in (R85),
riff-serverrefuses a post with status 403 when it has no signature, when the signature is not valid, when the key is not the key of the token, or when the signed time is more than 5 minutes old or more than 10 seconds in the future. - R198
riff-serverkeeps the payload and the signature with the message, also in the log (01M3T411N0HM699VJXW6RTVWKB). The time of a signed message is its signed time. The sender of a signed message haslead=trueonly when the signature covers it.riff-serverrefuses a signed post with the lead mark from a session that is not the lead. A signed-inriffasks for its lead mark before it signs. - R199 The reader verifies each message before it shows it. Each
message shows
verifiedornot verified. A message is verified when its signature is valid over its kept payload, the payload holds the message as the reader got it, and its key is the key of a live sign-in of the user of the sender.readandtailgive these keys. A message from a riff with no sign-in is verified too (R212). - R200 A message that is not verified never counts as from the
lead. The reader shows its sender without
lead=true. - R201 Without sign-in,
riff-serverkeeps no signature and gives no keys. So only a riff with no sign-in verifies such a message (R211). - 01M3T411N0HM699VJXW6RTVWKB A signed post carries its payload: the
bytes that the signature covers, the base64url of the JSON of the
signed fields.
riff-serverchecks the signature over the payload, and checks that the payload holds the fields of the post. No build encodes a payload again. A build skips a field of a payload that it does not know. - 01M3JEJVXXEPPNGT3FY4ZSFCWZ
riff-serverrefuses a signed post whose payload is the payload of a message in the same thread: a copy. It compares a hash of the payload, not the signature. So a session gets each request of its lead once. With R197, a copy older than 5 minutes fails the time check too. - R211 A
riff-serverwith no sign-in provider and no--require-sign-inis a riff with no sign-in. It trusts each caller. A person runs it only on a network that they trust (R203). Eachreadreply and eachtailevent of such a riff says that it trusts its callers. - R212 The reader counts each message of a riff that trusts its callers as verified. The lead mark of such a message is the lead mark that the server gives.
- 01M3JCE4ZD4DZCQ21FA69RT52D A riff with no sign-in (R211) listens
only on a loopback address.
riff-serverrefuses to start on another address, unless it gets--insecure(RIFF_INSECURE). With--insecureon such an address, it warns at start: each machine that can reach it can read, post and answer as any person. A riff with sign-in listens on any address once it has an owner (01M3JN3AQMHZHT6JP3P6GM9PWZ). - 01M3JZN1VEF73EPFE2FJY36EY4 When
riff-serverrefuses an address that is not loopback because it has no sign-in, its error namesRIFF_OIDC_CLIENT_ID,RIFF_OIDC_CLIENT_SECRETand--insecure. - 01M3JCE51T84JZKJ0NR89TPDNY
riff-servernever terminates TLS. A proxy or the platform in front of it does, for example Cloud Run.
Sign-in and tokens
- R14 The first sign-in provider is Google.
- 01M3JZN229S3YA3BR6GN5H3MTY riff has no built-in sign-in provider.
No client ID and no client secret is in the source or in a binary.
Each person who runs a riff with sign-in makes their own OAuth client
and gives it to
riff-serverin the environment:RIFF_OIDC_CLIENT_IDandRIFF_OIDC_CLIENT_SECRET. - 01M3JZN1XQVVNVD0MJVM8J91HC A
riff-serverwith an OAuth client (RIFF_OIDC_CLIENT_ID) requires sign-in, as with--require-sign-in. - 01M3JZN1ZZED3FXQEFNJ4KVCN5
riff connect claudeinstalls the plugin, then runsriff loginwhen the riff has sign-in and this machine has no sign-in for it. An old sign-in (01M3JNVBRS35B3CD67367JF7SJ) does not count. When riff cannot check or the sign-in fails, the command warns and says what to do. - R15
riff-serveraccepts the accounts of its allowed domains, and the people of 01M3JN3AFA2SAX0CEC1Y6E4NM5. The allowed domains are a setting. The default iscomotechnologies.io. - R94 The domain of an account is the
hdclaim of its ID token. An account withouthdis in no allowed domain. - 01M3JN3AD44CC98AGMVP43F56G The first person who signs in to a riff with sign-in is its owner. On a riff with admins (R210), only an admin becomes the owner. The owner is an admin. A riff that has an owner keeps it.
- 01M3JN3AFA2SAX0CEC1Y6E4NM5 These people can sign in to a riff with
sign-in: the owner, the admins, the members, and the accounts of the
allowed domains (R15). A member is named by verified email. A member
needs no allowed domain and no
hd. On a riff with no owner and no admin, the first person signs in. The refusal of a person tells them to ask the owner forriff invite EMAIL. - 01M3MEF4B33Z6WVJMDP29C7SS2
riff invite EMAILprints the public address of the riff (the--public-urlofriff-server) and the lines that the person runs to join: installriff, setRIFF_SERVERto the address, andriff connect claude. The lines hold no secret. - 01M3MEFG6F102T1H8DFJ38EJ4A The book page “Start a Team Riff”
starts a riff with sign-in for a team, on a Linux host that the team
reaches, behind a TLS proxy: the OIDC app of the team in the
environment of
riff-server,--public-url,--owner, a sign-in of the owner, andriff invite EMAILfor each person. No secret is on the page. - 01M3JY7T109BR860EQBSKEFDHY
riff admin add EMAILmakes a person an admin and a member.riff admin remove EMAILmakes an admin a member again. Only the owner can do either. The owner stays an admin.riff removerefuses an admin that the owner made. - 01M3JY7T3645CMQ8CS4T4ABZTP
riff-serverkeeps the admins that the owner made with the owner and the members. They stay after a restart with a bucket. The admins of R210 add to them.riff membersshows both. - 01M3JYX8NPZASQY6031R35H39P
riff owner EMAILpasses the owner role to a member or an admin. Only the owner can. A riff has one owner at a time. The old owner stays an admin. - 01M3JYX8QSEZDB5RZJ3Y57DR4Y
riff-serverkeeps a passed owner role in its state. It stays after a restart with a bucket, also when--ownernames the old owner. - 01M3MN14ZCTRVD3T455P6TFK1B After
riff-servermakes a change of the members,riff invite,riff remove,riff admin add,riff admin removeandriff ownerpost a note of the change to the thread of each repository of the riff. The note names the user that made the change, the email and the change. It wakes no session. The client signs it. A repository of the riff is the repository of a session inriff who --all. - 01M3MN1537Z0K3BRK6H2BZKZT0 A note of a change of the members holds no secret and no token. When the post of the note fails, the command still prints the change, and prints the error of the post.
- 01M3N7K3ZAZFGABN7032AYJWEM
riff owner --take: an admin asks for the owner role. Only an admin can.riff-servertells the owner at once: a direct message to each live lead of the owner, and a note. One request waits at a time. A second request is refused, and the refusal names the admin that asked first. - 01M3WRJAFS6W3J2ZRJ6XSW3SB5
riff owner --takeby the owner is not an error. It says that the user is the owner already, and exits with 0. It changes nothing: no request, no note and no message. - 01M3N7K41N03P26BEFFNX5617K The owner answers a request in N
minutes.
riff owner EMAILpasses the role and ends the request.riff owner --denykeeps the role: the riff tells the admin with a direct message to each live lead of the admin, and a note. With no answer in N minutes, the admin that asked is the owner. The old owner stays an admin. - 01M3Q5460YESBSQHTV3M15PE53
riff-serverhas three settings of the owner role, each 1 or more: N is--owner-take-minutes(RIFF_OWNER_TAKE_MINUTES, default 10). M is--owner-ping-minutes(RIFF_OWNER_PING_MINUTES, default 10). P is--owner-pings(RIFF_OWNER_PINGS, default 3). - 01M3Q546335NBTKG5BHQ27QC93
riff-serverchecks the owner each M minutes, while the riff has an owner and an admin who is not the owner. A check misses when no session of the owner is live, and the owner made no call since the last check, also as a person. A check wakes no session. After P misses in a row, the server warns the owner: a note to the sessions of the owner in the thread of each repository, and one line in the chat. When the next check misses too, the owner is gone, and stays an admin. The admin of a request that waits is the owner at once. With no request, the riff has no owner, and asks for a volunteer: a direct message to each live lead of each admin, and a note. - 01M3Q63NNC6SC03BFCG80M7B4D On a riff with no owner,
riff owner --takeof an admin makes that admin the owner at once.riff membersshows the ownernoneand ends with the line that says that the riff has no owner, in yellow.riff admin,riff owner EMAILandriff owner --denyare refused, with a text that namesriff owner --take. A sign-in makes no owner. - 01M3N7K4BC1RPZKQ1XNDTBRPGF
riff-serverposts its own notes and messages asriff://riff@server. The USERriffbelongs to the server: no person signs in with it. A post of the server has no signature. The server is not a session:whodoes not show it. - 01M3N7K4DVHSF7AQ402F14J26Z Each change of the owner role posts one note to the thread of each repository of the riff: a request, a pass, a deny, a grant after N minutes, a gone owner, and a take on a riff with no owner. The note wakes no session and holds no token. The server posts each of these notes, except the note of a pass (01M3MN14ZCTRVD3T455P6TFK1B).
- 01M3N7K4GAKJ621V5AWJRQVF3M
riff-serverkeeps a request that waits, and a riff with no owner, with the tokens in its state. They stay after a restart with a bucket. A request whose time ended while the server was down is granted at the first look after the load.--ownernames no owner on a riff with no owner. A riff with no owner counts as a riff with an owner for the listen address (01M3JN3AQMHZHT6JP3P6GM9PWZ). - 01M3JN3AQMHZHT6JP3P6GM9PWZ A riff with sign-in listens only on a
loopback address until it has an owner, so the owner signs in from the
machine of the server.
riff-serverrefuses another address at start, also with--insecure. It checks again after it loads its bucket. Before the load, it countsRIFF_OWNERor a bucket as an owner. - 01M3JN3ASSV9SA0QZKXXJ0RTEV
--owner EMAIL(RIFF_OWNER) names the owner of a riff that has none. The cloud deploy passes it from the GitHub Actions variableRIFF_OWNER. The deploy stops with an error when the variable is missing. - R16
riff-serverissues its own tokens. It accepts sign-in from each OpenID Connect provider in its settings. - R17 An access token expires in 10 minutes or less. A refresh token changes at each use. A reused refresh token revokes all tokens from that sign-in, except after a lost reply (01M3MX4TG7PNNETZ986DQS10JJ).
- R110 Only the device key of a sign-in can revoke it by reuse. A reused refresh token with another key is refused and changes nothing.
- 01M3TFG4SJ5C96NH8W7XRXJG6Z A refresh token names its chain and its
generation:
chain.generation.secret. A refresh with the current generation gives the next generation.riff-serverkeeps the hash of the refresh token of the current generation of each chain, and of the generation before it. A refresh token of an older generation is reused (R17), at any time. A refresh token with a wrong secret is not known, and changes nothing. - 01M52JYGWD3ZJKNYE3HG5BJYTP A sign-in has one chain: the chain of
the person. A session token has no refresh token and no chain: the
reply to the swap has an access token and an empty
refresh_token. A new session token ends no other token, so each process of a session keeps its own. The token store keeps the hash of each session token (01M52JYGSNTFPQ0ZP50AD2BG89). - 01M3TFG4WE7CZQ4TCJE2NTC52E After a start, the saved token store
can be one generation behind. So the first refresh of each chain after
a start takes the current generation of the saved store, or the next
one, as good.
riff-serverchecks the device key first (R110). - 01M52JYGSNTFPQ0ZP50AD2BG89 The token store keeps the hash of each live access token, of a person or of a session, with its sign-in, its session and the time when it expires. So an access token works after a restart and after a deploy, until it expires, and a new start makes no client refresh. A new token from the last second before a crash can be lost: its client refreshes one time.
- 01M3MX4TG7PNNETZ986DQS10JJ A used refresh token counts as reused
only after the refresh token of the pair that its last use gave was
used. Before that, the reply of the last use was lost: when the token
comes back with the device key of its sign-in,
riff-serverends that unused pair and gives a new pair. So a stolen refresh token that comes back after the next refresh still revokes the sign-in. - 01M3MX4TSEH18FSNQ28GEH2GFJ
riffsays that the sign-in ended only whenriff-serverrefuses the grant withinvalid_grant. Each other error of a refresh keeps its own text. - 01M3W947QF6PFBWR28ZVXCVQHG
riffsends a refresh token thatriff-serverrefused one time only. It keeps the sign-in of the machine as ended, with its user and with no token. Then eachriffprocess says that the sign-in ended with no call to/v1/token, untilriff login.riff connect claudesigns in again. - 01M3MX4V43SF2XFCZWANHD19WV
riff-serverchecks no version on/v1/tokenand/v1/sign-in, andriffchecks none on their replies. Soriff loginand a refresh work when the versions do not match. - 01M3MX4VCEBTY0DN4JMF624WYE When
riff-serverreplies 401 to a call with a token,riffdrops that token, gets a new one, and sends the call once more. When the server refuses the person token in a swap for a session token,riffrefreshes the person token once and asks again. - 01M3ND6R8YXN1KTRTRAV5A7F14
riffgets each access token in a task of its own. One refresh runs at a time, and a call that waits for a token never waits on a call that its own task does not poll. - 01M3MX4VM8CK1GAGJAM2P29NWH When the sign-in of the machine ended,
riff watchandriff tailsay so once and try again (R148). Each tool ofriff mcpsays so in its result. Afterriff loginon the machine, each of them goes on with no restart. - R80 A sign-in ends when none of its refresh tokens is used for 30 days. The person then signs in again.
- R81
riff-serverkeeps only a hash of each token, never the token itself. - R90
riff loginsigns in with the provider thatriff-servernames. It opens the browser. The code comes back to a loopback port. The client uses PKCE with S256. - R112 Only a request with the right
stateends the wait on the loopback port. It reads at most 8 KiB of the request line. - R91
riff-serverswaps a valid ID token of its provider for the first riff tokens. The email in the ID token must be verified. - R117 Each fetch from the sign-in provider stops after 10 seconds.
- R146 At start,
riff-serverchecks its OAuth client with the provider. When the provider refuses the client,riff-serverstops. - R153 When
riff-servercannot reach the provider at start, it logs a warning and serves. - R208 USER is the part of the verified email before the
@, in lower case. Each character that a URI part cannot hold becomes-. - R209 A USER belongs to one verified email. The first email that
signs in with a USER holds it.
riff-serverrefuses each other email that gives the same USER. The refusal names the USER. - R93
riff logoutremoves the sign-in at one server from the device. - R18 Each token is bound to a key that stays on the device.
- R86 Tokens use DPoP (RFC 9449) with ES256. Each request with a token carries a new proof from the device key. A proof is valid for 5 minutes, and up to 10 seconds in the future.
- R87
riff-serverrefuses a bearer token, a proof that it saw before, and a proof for another method or URL. The URL is the public URL of the server and the path. - R113
riff-serverrefuses a proof whosejwkholds a private key. - R114
riff-serverkeeps at most 100,000 proof IDs. When it must forget an ID early, it refuses each proof as old as that one. - R115
riff-serverkeeps a proof ID only after the request shows a valid token or ID token. A caller without one adds nothing. - R142 At start,
riff-serverknows no proof ID. It refuses each proof issued before it started to serve. - R88
riffkeeps one device key for each server, in the OS keyring. - R19 Each session gets its own token. The token works only for that session.
- 01M3WFVADCDZM8XX590KAEMEYG A session gets its token by token
exchange (RFC 8693): it sends a person access token and its session
ID. Before the session token expires,
riffswaps the person token for a new one. - R104 A token acts only as its caller. The user and the session ID
in
memust match the token, orriff-serverreplies 403. A person token acts only as the person, with no session ID. - R105 Only a person access token gives a session token.
- R106
riffkeeps a session token only in the memory of the process. Only the person tokens go to the keyring. - R107 Only one
riffprocess at a time refreshes the person tokens of one server. A lock file makes the others wait. - 01M52JMEKD4P2ZT659CBNPN9CC The lock file of R107 is in the local
directory of riff (
riff::local::dir), not inTMPDIR. So eachriffprocess of one user on one machine finds the same lock file, also a worker with aTMPDIRof its own. WithRIFF_HOME, it is in$RIFF_HOME/state. A process that sets the store of its secrets itself, a test, keeps its sign-in in that store, and its lock file inTMPDIR(01M3MY2KWKBJCQ0BCNC6533RBW). - R20 A person or an admin can revoke all tokens of a person at once.
- R101
riff logout --allends each sign-in of the caller, on each device. An admin adds--user USERto end the sign-ins of another person. - R210 The admins are a setting of
riff-server:--admin EMAILorRIFF_ADMINS. Each admin is named by verified email. A name that is not an email names nobody, and the server logs a warning at start. There are no admins by default. - R111 Admin emails, user names and the named person compare trimmed and in lower case.
- R21 A client keeps tokens and keys only in the OS keyring. The
one exception is
RIFF_HOME(01M3MY2KSV73WS8D902YCH2PRX). - R82
riffkeeps each secret under the keyring serviceriff. On Linux, it needs a Secret Service, for example GNOME Keyring or KWallet. - 01M3NBTZDT67WD9ZX0RHDVCW9T
riffwaits at most 10 seconds for one call to the OS keyring. A keyring that does not answer in that time is an error that says so. It never blocks the process or its Ctrl-C. - 01M4385CCATXC0B8HV1XD6EFWG When the OS keyring is locked or does
not answer, the error of
riffnames the host, says to unlock the keyring at the desktop, and says thatghstops too. - 01M54510PS9FDKQKJB0RAH3XG3 A
riffprocess opens the OS keyring at most one time, and reads each secret at most one time. It keeps the value for its later calls, and a write or a delete of the process changes the kept value. Only the refresh of the sign-in reads it again, under the refresh lock (R107). - 01M556QPPRSQ91HDED128G2X7G On Linux and BSD,
riffopens each session of the Secret Service as a plain session, asghdoes, not as a DH session. It keeps each secret as the store of thekeyringcrate did: an item in the default collection with the attributesserviceandusername. So it reads and writes the secrets of an olderriff. - 01M54510SFK1Z9VPFYBYYSV10F The
ghcalls of one clean of the worktrees (01M3ZV0TKSHNW5QC2NG1XTJEJB), ofriff top, and of the rollout and the look of the lead share one token. riff reads it withgh auth tokenat the first call, and again at the first call after 10 minutes. It gives the token to each call inGH_TOKEN: in the environment ofghonly, never in a command line or a file. With noghcall, riff reads no token. When the environment hasGH_TOKENorGITHUB_TOKEN, riff reads no token. - 01M54510W507A3T5JRTZ55H0ZQ When
RIFF_KEYRING_LOGnames a file,riffadds one line to it for each call to the keyring store: the process ID andread NAME,write NAMEordelete NAME. The first call of a process adds the lineopenbefore it. The lines hold no value of a secret. It is for tests and checks. - R22
riff-serverfollows the MCP authorization spec, revision 2026-07-28. - R83
riff-serverhas no authorization endpoint for now. Its metadata lists no response type. - R84 The public URL of
riff-serveris a setting. It is the OAuth resource and the issuer. Each token is only for it. - R85 With the setting
--require-sign-in, each route except the token endpoint, the sign-in route and the metadata needs a live access token in theAuthorizationheader. - R47 Each session connects through
riff. Direct connections from an agent tool are not supported for now. - 01M6FYH5XZAZYJFPFQVAFQSSVT Each change of the owner role that
changes who the owner is also posts one line in the chat, as
riff://riff@server, with the same text as its note: a pass, a grant after N minutes, a gone owner with or without a new owner, and a take on a riff with no owner. A request and a deny change no owner: they post no line. The line has noto: it wakes no session. A person sees it inriff chat. The server posts the line in the chunk of the command that makes the change, so a replay of the log has it. Supersedes 01M3N7K4DVHSF7AQ402F14J26Z in part.
Terminal
- 01M3JPQT13ANVA7DNJDVNJ0S8P The settings of riff on a machine are
in
$XDG_CONFIG_HOME/riff/config.toml, or~/.config/riff/config.toml. A change keeps each other key. - 01M41FMA3FGF3TKTXPBND9NDW1 A change of the settings writes a new file and renames it over the old one. A process that reads the settings at the same time reads the old or the new settings, never a part. A settings file that is a symbolic link stays a link.
- 01M49JW9Y8SNT3J242SF646DF4 The fast-forward of the main clone and
the WIP push of the
leavetool act only on a dir that is the top of a git worktree. For a dir in a repository above it, the fast-forward changes nothing and says nothing, and theleavetool refuses. - 01M3WN72ECF0WKR4M7M6ZYAF9J Each stream of the client (
watch,tail) has a connection of its own. That connection is never in the pool of the client, so no call uses the connection of an open stream. - 01M48RW9E8NS2FPHFHG2S10R7A Each call of the client has time
limits: 5 seconds for a connect (
CONNECT_WAIT), and 20 seconds for each try (TRY_WAIT). The client of the calls sends an HTTP/2 ping each 10 seconds, also with no open call, and drops a connection with no answer in 5 seconds. On Linux, a TCP connection with data that gets no answer for 20 seconds closes. So a call never waits for ever on a dead connection. A try with no reply in its limit is a fault (01M4A8041F8EK1VYDE4C9QG8N8). When the budget of the call ends with no reply, the error says that the server gave no reply in the budget. - 01M48RW9HNKPNZ75H9R01BG6V5 A stream of the client has the same
limit for a connect, and no total limit. A stream that gives no byte
for 45 seconds (
STREAM_IDLE, three keep-alive comments of the server) ends, andfollowconnects again. So a stream that died with no sign, for example after a sleep of the machine, comes back. - 01M4A803WN0KTDGGAX2E771XDF Only the link talks to
riff-server. A process has one link for each server: a map by the URL of the server. EachApiof one server shares the link: one HTTP client of the calls, one client of the streams, and what the process knows of the server. - 01M4A803Z4Q0KX6NT1KC6QR43H Each call has a budget: the time from
its first try to its end. A short command, a tool call of
riff mcp, a post ofriff chat, a look ofriff topand a call ofriff workers hosthaveSHORT_BUDGET, 60 seconds. The start hook hasSTATE_WAIT, the end of a sessionEND_WAIT, and the status line one try inSTATUSLINE_WAIT. The budget is a deadline: the link cuts the open try at it. Each try has the limitmin(TRY_WAIT, the budget that is left). The open of a stream is one try, and a stream has no budget. - 01M4A8041F8EK1VYDE4C9QG8N8 Each try ends in a reply, a fault or a
refusal. A fault is no connect, a cut before the end of the reply, no
reply in the limit of the try, a 5xx or 429 with no build header, or
a 503 of
riff-server. A fault gets a new try after a wait, while the budget lasts. The wait grows from 250 ms, double each time, toMOST_WAIT, 5 seconds, less a random part of up to half of it. A refusal is each other reply ofriff-server, and a build that riff cannot talk to. It gets no new try. A 401 to a token gets one new try with a new token. - 01M4A8043S2ZCKRH19Z3Q8AJ1F A fault with no HTTP reply (no
connect, a cut, no reply in time) makes a new HTTP client of the
calls of the link, which each
Apiof the server shares. A 503 or a 429 keeps the client. - 01M4A804683G1EXM53893VHW7S A refused connect to a loopback address of a server that never replied to the process ends the call at once. It is no fault. Each other connect error is a fault.
- 01M4A8048J60YSVNVYF2432KE8 Each call of
riffhas a call ID: 16 random bytes in base 64 with no padding, in the headerriff-call. Each try of the call sends the same ID. Soriff-serverruns a command one time only (01M48VFX22S4811DYBBD7QDW24), also when riff sent it again after a cut or after no reply in time. - 01M4A804AWYEHPYZ966A6PXRPF
riff chatposts the typed lines from a task of their own, in their order. Its screen takes input while a post waits. At its end, it waits for the posts of the typed lines. - 01M4A804D7JAPRAT1YKD09ATK3 A test sets the budget of the short
calls of a
riffprocess in milliseconds with the environment variableRIFF_LINK_BUDGET_MS. - 01M4ZQVVK4P72XMM37RXG0C8ZH A test sets the limit of each try of a
riffprocess in milliseconds with the environment variableRIFF_LINK_TRY_MS. - 01M48RW9MA30A12E7XWX047CJ0 The server ends a
watchortailstream when the stream lags behind its buffer of events. It does not drop the events with no sign. The client connects again, and awatchgets the newest wake that its session did not read. - 01M51BF5S2YKB8JG10DF6YVT42 The trailer
Issue: #Nof a pull request names its issue, as forriff verify. With no trailer, its branch names the issue (worktree-issue-12). The free work of the rollout and ofriff wave run, the line of a claim and the look of the lead find the pull request of an item so. A pull request on a branch that names no issue, for example a release branch, counts for the issue of its trailer: while it waits for a verify or for the merge, the item is no free item (01M3Z9N5HHHS1E17NFGMVBKZ0K). After a failed verify, the item is free.
Builds
-
01M3MX1DYY6AVDW946NR0B9T2C A build names the crate version, the last commit that changed
crates,Cargo.tomlorCargo.lock, and the UTC time of that commit. The crate version is a semantic version. Its line is the major, and the minor while the major is 0. Ariffand ariff-serverof the same line talk. -
01M3MX1E1EY1M7JGNCN6FCEVQK
riff-serveralso talks with ariffof the line before its own:0.3.xon a0.4server,1.xon a2server. It does not talk with ariffof a later line. -
01M514C19H3WQP1FSX9RBVSC69 A build is older than another build when its version is lower, or when the version is the same and the time of its commit is older. A build of a higher version is not older, also from an older commit.
-
01M3N73E5JWFTYZ90JX5AVFGEP While the major is 0, the minor has the role of the major.
-
01M3N73E7YTHFX2J2KXT7017QX A patch release (
0.3.0to0.3.1) changes nothing that another machine or session can notice: a fix that restores the intended behavior, the docs, an output format, the tests, or words of the instructions of the riff tools or of a requirement that make a rule clearer with no change in behavior. -
01M3N73EAAD1G88TG7SNFWE4P1 A minor release (
0.3.xto0.4.0) has a change that another machine or session can notice:- the wire: a route or a field that a client needs, a removed one, or a new meaning;
- the saved state of
riff-server; - the plugin contract: the hook input, and the names and arguments of the riff tools;
- the behavior: a change of the instructions of the riff tools or of a requirement that changes what a session does, for example the pull request flow, the verify, the claims, the waves or the pause.
A wire change stays additive for one line, so that
riff-serverserves the line before its own (01M3MX1E1EY1M7JGNCN6FCEVQK). Riff does not hide a difference in behavior: the version note tells the older session to update. -
01M3N73ECKSDSH6820AKW9J8F7 The major release
1.0.0promises that the wire and the behavior stay stable. After it, a major breaks, a minor adds and a patch fixes. A change of behavior that can break a pipeline or a policy is a major. -
01M3N73EEQ4HPCPPAGCAHH3S6B The test for each release: can a session on the old version and a session on the new version, in the same riff, act differently or fail to understand each other? Yes: a minor, and a major after 1.0. No: a patch. A wave release is a patch unless the wave has a minor change.
-
01M3N73EGY4NQDQQP8Y185E9VZ The pull request of a release states the level and the reason in one line. The release notes list the commands that each person runs, for example
riff update --tag vX.Y.Zwhen the oldriffcannot read the new server. -
01M3JEE7P46GWXR1BD4Q1TTSGN Each call of
riffnames its build in the headerriff-build. Each reply ofriff-servernames the build of the server in the same header. -
01M3MX1E65XGWDZ062PQ9YXQ5T
riff-serverrefuses each call of ariffthat it cannot talk to, or that names no build, with status 409.riffrefuses each reply of ariff-serverthat it cannot talk to, or that names no build. The error names both builds, the lines that the server talks with, the older side, the step to update it, and the link to the book. The OAuth metadata stays open to each client. -
01M3QCMJ9F1GRTRRSB4AW9TC3D
riffchecks the build only of a reply that comes fromriff-server. A reply with status 5xx or 429 and no build header comes from the front end: a short outage, not another build.rifftries the call again, as after a 503 (R132). After the last try, the error is a plain error, not a version error. Soriff chat,riff tail,riff watchandriff mcpgo on quietly. A reply with another status and no build header is still a version error. Its text names the status and the URL of the reply. -
01M56WNP3TEGH98B9EAM4H62P4 At a deploy, Cloud Run can send calls to the old instance for a few seconds after it exits. Cloud Run then replies 500 or 429 with the text “The request was aborted because there was no available instance”, and no build header. Each client of
riff-servertries the call again, as after a 503 (01M3QCMJ9F1GRTRRSB4AW9TC3D): each command,riff mcp,riff watch,riff tail,riff chatand the daemon. The status line shows(not in the riff)for one refresh, as after a 503.riff serverandriff updatetake that reply as a server that does not answer, not as a server with no build. -
01M56WQBZ629ENP5E12RN3WYDH The check after a deploy counts the 500s of
riff-serverand the replies of Cloud Run with “no available instance” apart. A 500 ofriff-serveris a bug. A reply of Cloud Run in the hand-over is not. -
01M3MX1E8M9TKBN90P4DYKH3H8 With another build that it can talk to,
riffgoes on. Eachriffprocess prints one note to stderr:riff-server runs build X; this riff runs build Y. Run riff update when you can.Whenriffis on the line before the server, the note names the next line of the server that refuses it, and endsRun riff update soon.Whenriffis newer, the note names the older side (01M56Q55SHHJSKJWNHMW79X7HA).riff servershows both builds and that the versions can talk. -
01M3MNVTC248YYJJQKFD9H1WY9 On a version that they cannot talk to,
riff watchandriff taildo not stop. They print the error once, try again every 5 seconds, and go on when the versions can talk. When a newriffbinary is on disk, they run it in their place with the same arguments. -
01M3NJGD45GF7Y4CZWQ7GRDHZN After an update, the new binary of
riff watchandriff tailkeeps the host, repository and worktree of the old one. It does not read them from its directory. When the working directory is gone, the new binary runs in the nearest parent directory that exists, and the process says so on stderr. -
01M3NJGD6H8DNVHHHG80F9YFCE When
riffcannot read its working directory, the error names the directory fromPWD, and tells the person to change to a directory that exists. -
01M3NT6WXGCNKW3EQ7MBJDQTR4 When a new
riffbinary is on disk,riff topandriff chatrun it in their place, asriff taildoes. The chat does it between two lines. The new chat shows no line of the old chat again, and draws its prompt again. -
01M3Q55KMQSSJVQEN86XFB8PSG Each process that
riffstarts, for example a worker pane, runs theriffbinary on disk. It never runs the path of a binary that a new one replaced. -
01M3JEE7WT04BKX377VW5GDSPY
riff --version,riff-server --version,riff whoami,riff whoand the whoami tool show the build. -
01M3JEE7YXQPWS65FBVTASAEBX The image build of
riff-serverhas no git. It gets the commit and its time asRIFF_COMMITandRIFF_COMMIT_TIMEfromdeploy/build-id.sh, which uses the same git command as the build ofriff. -
01M3N7JJC5WQBJ7SJZSZNBAVVR
riff update --auto onandriff update --auto offsetupdate.autoin the settings of the machine, and install nothing. The default is off.riff update --autowith no value shows the setting. -
01M3NT6WV8Q8EFZBK8DHYKW5CC On a machine with no
update.autokey,riff loginandriff connect claudeask the person once in a terminal:Update riff by itself when the riff gets a new release? [Y/n]. The answer sets the key:nornois off, each other answer is on. With no terminal, they do not ask. -
01M3N7JJEKZMN1E5NJQRK2QYVB With
update.auto = true, when ariffprocess gets a reply from ariff-serverof a newer version than its own, it startsriff update --tag vX.Y.Zof the release of that server in the background, and prints one note. It never installs an older release. Withupdate.auto = false, it starts nothing, and the note to update stays. -
01M3N7JJH0SXXQYYBAHWPCNQGX One update in the background runs at a time on a machine. It tries each release once: many processes that see the same release start one update, and a failed release waits for the next release.
-
01M3N7JJKBME6VSNTHD8VPN3K9 After an update in the background, the lead of the user gets one direct message from the person: the host, the old release and the new release. After a failed update, the message holds the error, and the old binaries stay.
-
01M56P1T4KK03MF7EK5CC0YZNB When the direct message to the lead after an update in the background fails, the update tries it again every 5 seconds, for at most 2 minutes. The lead gets it once.
-
01M58AW5SBG3ZP7149ZY66VTYW The update in the background tells the lead in the repository of the process that started it: a session, or
riff workers host. The daemon never takes a place from its working folder, and starts the update with no place. An update with no place, or with a place outside each repository, tells the lead in each repository whose wave the daemon of the machine runs on the same riff-server. With no such repository, it tells no lead, and its log says why. -
01M58AVFM0D62R48HKK8CFY4VW The update in the background tells the lead before its step for the daemon (01M517BTDRKDXD3X84P6AEAF7P, 01M517BTJ9EV4ZGPYRB6HXJJC9). A new start of the unit stops the update that the daemon started, so the message goes first.
-
01M58AW9YR84SKSYBYBJ6V7TNJ The daemon does not ask systemd to start it again (01M517BTP6BAGTRBYWPX9GPVE0) while an update of riff holds the update lock of the machine.
-
01M53NEE7W0ZC8B7EV9ET6QHBB The update in the background prints no note that
riff-serverruns another build (01M3MX1E8M9TKBN90P4DYKH3H8): it installs that build. So no line ofupdate.lognames the old build as the build that runs. -
01M3NT2Q0RNM9PVHT42V459624 An update in the background runs in the local files of riff, not in the working directory of the process that started it. It gets the place of that process, so the message to the lead finds the repository also when that directory is gone.
-
01M3NT2Q30P9GCQGENWMP1NKN2
riff updaterunscargo,git,riff connectandriff-server --versionin the home directory, else in/. So it works in a removed directory. -
01M3NT2PYFHPB0C19Q2QB2AE6W When the working directory of an update in the background is missing, the update stops before
cargoruns, and does not mark the release as tried. The nextriffprocess tries again, and the message to the lead says so. Each other failure marks the release as tried, so one release gets at most one install and one message to the lead. -
01M3N7JJNPPJNTXFDTEY4MSDVJ An update in the background stops no session, and changes no sign-in and no device key. The sessions take the new
riffas 01M3MNVTC248YYJJQKFD9H1WY9, 01M3NT6WXGCNKW3EQ7MBJDQTR4 and 01M3NT6WZTKAFKGDWGCFKC8TB5 say. -
01M5P1VAX6WNM9DX16NS4Z0SWC The actions of the pipeline stay on a version that GitHub supports. Dependabot opens a pull request each week for an action with a newer version.
-
01M5RG6BTXAJY8DV6MT315VZWF riff supports the latest release only. Each machine runs the latest release. riff has no code, test, check, text or how-to that keeps an older release working, or that removes what an older release left on a machine. A history page and this page may name an older release. This supersedes 01M56N6KSMFJ46PW99Z80GF9DM, 01M56N6KVZRGT3W7CEK05DHR57 and 01M53N7KN4QKZ70W2GNQ3VQWQ9.
-
01M5RG6BXBQVFABA1N4NRZBMQF
riff-servertalks with ariffof its own line and of the line before its own, and with noriffof another line (01M5RG6BTXAJY8DV6MT315VZWF, 01M3MX1DYY6AVDW946NR0B9T2C, 01M3MX1E1EY1M7JGNCN6FCEVQK). The rule reads the version of each side at run time. No release number of riff is a constant incrates/*/src. A test of the rule takes the version of the build at run time, and checks that the line before talks and that two lines before do not. This supersedes 01M55FCZNNZGKM4EKAXZQFTDWR and 01M53NGVXEMBTJ8E4K3MHPQ7W9. -
01M5RMZEM2C1SN71V1XCRPRP0C No code, test or text of riff keeps a rule for a pair of lines that is two lines apart. The rule of 01M5RG6BXBQVFABA1N4NRZBMQF is the only rule of lines that talk. This supersedes 01M5A72C25A1FDZPW3FK2R1F92 in full.
Code
- R12 All code is Rust.
- R13 The repository stands alone. It is not part of a larger suite.
- R123 The crate audit ignores no advisory. When a dependency has an advisory, we update it, change a feature, or replace it.
- 01M3J3FGCENZTA3JGZ6S3NX7YM A new requirement gets a ULID from
just ridas its ID. The ID has no meaning. Nobody writes it by hand. The old IDs R1 to R232 stay as they are. No new requirement gets an old ID. A requirement is never renumbered. - 01M3J3FGEND8977RZTRS9C0V5Z
just cifails when two requirements have the same ID, and when code, tests, the book or the notes for agents cite an ID that no requirement has. It warns when a requirement ID is not an old ID (R1 to R232) and not a ULID. - 01M3MNT28NXA9VRST119QR8AQK Each prose line of the book and the requirements is at most 72 characters. A line with one link or one code span and nothing to break is longer when it must be.
- 01M3WNN836EFG7GJQKZRTSK5FT
just wraprunshygiene wrap docs/src. It fails on each line that breaks 01M3MNT28NXA9VRST119QR8AQK (wrap). No test of a crate checks the wrap. - 01M51K1VMWTNZGD6PT1BYDGWM3 No Markdown file of the book or of
the plugin has two blank lines in a row outside a code block.
just wrapalso runshygiene blank docs/src crates/riff/claude-plugin. It fails on each blank line after a blank line (blank). - 01M3T5MG7VVYG4A8YBM75FGSMB
just bookrunshygiene book docs. It builds the book with mdbook, and fails on anERRORline of mdbook (mdbook), on an include line that mdbook left in the text of a page outside a code block (include), and on an empty code block (empty-code). It skips an escaped include example in a code block. - 01M3W5YW0172EVF2JA8T7WW392
just cichanges no tracked file.hygiene bookinstalls the theme of the book when it is missing, and keepsbook.tomlas it is. It fails when the install or the build of the book changed a tracked file (tracked). - 01M3WNMKB6PAP6J0QXX4A684HH
just ciruns only the checks that the diff can break. The diff is each file that differs from the merge base ofHEADandorigin/main: committed, not committed, or not tracked. When each file of the diff is text,just ciruns onlybook,reqsandwrap. In each other case it runs each check, also when no file differs and when git cannot compare. A file is text when it is indocs/ordesign/, or when it is a.mdfile outsidecrates/.just ciprints one line that names the set and the reason. - 01M3WNN7VQJKN5MJH7JN50VF4D
just ci-fullruns each check for each diff:fmt-check,lint,test,doc,book,reqsandwrap. The Gate on GitHub runs the same checks (01M513W2K3HNRWPQTQNBQPT8MZ). - 01M43DKYVAX0TJ2F5YYGYFSZ4G One
just ciorjust ci-fullruns at a time in a worktree. A run holds the lock filetarget/.riff-ci.lockwith its pid and its start time, and removes it at its end. A second run while the holder lives stops at once with the exit code 1 and one line:a just ci runs in this worktree already (pid N, started M min ago); wait for it, or stop it. A lock of a process that does not run does not count. A run that the holder starts takes no lock. The check uses no cargo. - 01M49HAZ5BZYGAR9PGC089RM3F Each pushed commit gets one full test
run: the Gate on GitHub. The forge merges only on a Gate pass. An
author runs
just checkbefore a push, notjust ci. A verifier does not runjust ci. It reads the Gate of the commit, and does the work that a test run cannot do: a review of the code and of its fit with the design, a check of eachDone when:criterion by its test, the book, the rustdoc and the requirements. - 01M49HAZA5K08XW2JQ11TG87JP
just checkrunsfmt-check,lint,doc,book,reqsandwrap, then the tests of each crate of the diff and of each crate that depends on one of them. The diff is the diff of 01M3WNMKB6PAP6J0QXX4A684HH. A file in no crate needs the tests of each crate, also a text file: a test can read it. A tree that git cannot compare needs them too.hygiene cratesprints the arguments ofcargo test, and one line that names the tests and the reason.just checkholds the lock of 01M43DKYVAX0TJ2F5YYGYFSZ4G. - 01M513W2K3HNRWPQTQNBQPT8MZ The Gate on GitHub runs its checks as
jobs at the same time:
Clippyrunsjust lint,Tests I/Nruns part I of N of the tests in the session image, andChecksrunsjust fmt-check doc book reqs wrap. Together they run each check ofjust ci-full. The last job,Gate, needs each of them. Each of them has the conditiongithub.event_name != 'schedule' && github.ref_type != 'tag', andGatehasalways()and the same condition. SoGateruns also when one of them fails or is cancelled, and it passes only when each of them passed. The ruleset needs onlyGate. - 01M513W2NSE303E6XAM18BW7T6
just test-part I/Nlists each test of the workspace, also each doc test, withcargo test -- --list. Of the tests that are not doc tests, it runs each test whose name has the 64-bit FNV-1a hash H with H mod N = I − 1, withcargo test --tests --no-fail-fast -- --exact. Part 1 also runs each doc test, withcargo test --doc --no-fail-fast: the name of a doc test has spaces, and rustdoc splits a filter at each space. It prints one line with the count of its tests and of all tests. A list with no test fails. The N parts together run each test. - 01M513W2RERJES76ZXKKHCV0DP Each job of the CI workflow runs on the
runner that the repository variable
RIFF_RUNNERnames. With noRIFF_RUNNER, the runner isubuntu-latest. - 01M4A4T67MPF0BCNX7AFZRDA8J The dev and test profiles of the workspace keep only the line tables for the code of the workspace, and no debug info for the dependencies. A backtrace still names the file and the line.
- 01M4A4T69XGN1XVJHGHVZN1MRJ The integration tests of a crate are
one test binary,
all:tests/all.rshas one module for each file oftests. A file whose tests change a thing of the whole process is a test binary of its own:back_in.rsandjoin_a_riff.rsofriffset the keyring store, andisolation.rslistens on the port of the riff of the machine. Each other test ofriffthat needs a mock keyring usescommon::mock_keyring. - 01M514XARN6564BBWHWWYVCH9K Each doc test of
riffthat sets the default store ofkeyring_coreis astandalone_crate, so it runs in a process of its own. A unit test ofsecretschecks each file ofcrates/riff/src. - 01M4A4T6C5DH311AXMM6AG54DV A test fails when a file of
testsof a crate is in no test binary: not a module oftests/all.rs, and not the path of a[[test]]in theCargo.tomlof the crate. - 01M49W17GGV1K5FZEKJRPV0GNA A type of the log, the wire or the
checkpoint has one struct. A reader, a saved form or a reply that
carries the data of a type holds that type, not a second struct
with its fields. A tolerant reader holds it with
#[serde(flatten)]. The read of the frozen format of v0.8.0 (import.rs) is the one exception. - 01M49W17M2JVNYSHDQJWHZ8A7X Each conversion between two types of
the log, the wire, the checkpoint or the state names each field of
its source in a destructure with no
... A field that the target does not need is named with_. So a new field fails the build at each conversion. A type of the frozen format of v0.8.0 gets no new field, so the read inimport.rsneeds no destructure. - 01M49W18ETF4KZJN848M91VY35 Each type of the log, the wire and
the checkpoint has a round-trip test: a value of each variant with
each optional field set, written to JSON, read back and compared.
The schema of the type shows each field that a value leaves out,
and each variant that no value shows. A test lists the serde types
of
record.rs,wire.rs,selector.rs,signed.rsanddpop.rs, and fails when one has no round trip. The round trip of the checkpoint starts at its root type, so it reaches each type in it, and a load of the state gives the same checkpoint again. The round trip of the log goes through the store. - 01M49W18QQKF4KYDRYP3ZK9F1Q A new or changed type of the log, the wire, the checkpoint or the state cannot hold an invalid state. For example, a saved session has a status, a step, or both: an enum, not two options.
- 01M5P2CWS1ENC30XNENRX11MY3 An integration test of a command that
calls riff-server uses a riff-server that needs a sign-in
(
common::start_signed), and signs in the command withcommon::sign_in. The server notes each call with no token, and the test fails when it ends. - 01M5R2XVDN99TE9NEJYY1GJE91 A text of riff, a line of rustdoc, a comment, a test and a page of the book name no version of riff as the riff of today: they say “riff runs each session in a container”, not “riff N runs …”. A version name stays in only four places: a requirement on the history page, which is a dated statement; the release notes on GitHub; a rule between versions, with its tests; and the code that removes what a named old release left on a machine, with its texts and how-tos.
- 01M5R2XVGMTB2HC9PRMS85ZWTY
just versionsrunshygiene versions. It fails onriff Norriff N.M, alsoriff-server N, in each file ofcrates/*/src,crates/*/testsanddocs/src, butdocs/src/requirements-history.md(versions). Each use that must stay is a linePATH | TEXT | REASONincrates/hygiene/versions.allow. It fails on a line of the list that allows no name.just check,just ci-textand the job Checks of the Gate run it. - 01M5RGXD7WV4H3VR0YA7JW9GXR docs/src/requirements.md holds only the rules of today. A requirement that a new requirement supersedes in full moves, in the same change, to docs/src/requirements-history.md: the same section, the same ID and the same text. A requirement that a new requirement supersedes in part stays, and its text keeps only the part that holds. The history page is in the part History of the book.
- 01M5RGXDB28TBJJ9X1N9YQATZ6 just reqs (reqs check) reads requirements.md and requirements-history.md. An ID is unique across both pages, and a citation of an ID finds it on either page. just req writes only to requirements.md. The check superseded-in-full fails on a requirement of requirements.md that has the words Superseded by or Replaced by and no words in part.
- 01M5ST2QHBG5S6F2H0FPJ5A24P The sign-ins of a
common::Signedtest server end when the server ends. A later server at the same URL, for example on a port that the OS gave again, gets no old sign-in of an earlier test. A machine at an open riff-server holds no sign-in.
The phone app
The phone app is the design of #951, a child design of #982. The book page riff on a phone has the pictures. The build items are #962, #963, #977, #964 and #965.
- 01M53NTQTHC8WHW9ZZ6S1VZ2W8 The first phone app shows the thread
chat(the history, then each new line), posts a line, sends@lead,@USERand/measriff chatdoes, and shows a notice when a line names its user.riff top, the questions of the lead and the steps of a lead come later. - 01M53NTQXQTM29BT67E6KHZFCK The phone reaches the chat through
riff-server over HTTPS. It acts as a person, as
riff chatdoes, with the calls ofriff chat: join, read, tail and post. - 01M53NTR10TGJ9PHHCEN1P15K7 The phone app is for Android 10 or later. It is a Slint app, all in Rust. The riff project writes no Kotlin or Java source for it. A crate or a library may ship its own Java. No part of the app draws in a web view.
- 01M53WZ08JY80X32PP6P7FGNWP The app uses the royalty-free license of Slint, and shows the AboutSlint widget in its About screen.
- 01M53RWDFBC3Q8R6PEX9R0XAVD The app builds on the core library of #982: the client of riff-server and the models of the views. It calls the parts of Android through JNI: the Keystore, Credential Manager, the camera and the notices.
- 01M53XPZTG203SNM9MBKBERMB5 One command adds, lists and removes
each kind of device of a person:
riff device add [NAME] [--kind KIND],riff device listandriff device remove NAME. The kind sets the scope of the device at add time. The first kind isphone, the default. Each child design of #982 adds its kind and its scope. A machine ofriff loginis a device of the kindmachine, with its host as its name and the scope of today. - 01M53NTR47XBDX6C6QN49RNP29 A person adds a phone from a machine
with a live sign-in:
riff device add [NAME], in a terminal. NAME is the name of the device, as the host name of a machine, and not a Google ID. With no NAME, the name is the kind, for examplephone. The command refuses with no terminal. riff-server makes a pair code that works one time, for 10 minutes, ties it to the Google account of the sign-in of the terminal and to the kind, and keeps only its hash. The command draws a QR code withtui-qrcodethat holds the address of riff-server and the code, and prints the code as text. The code is in no URL. - 01M53WZ032G5W435GJMPVBHD71 The app reads the QR code with the
camera, through the NDK camera API and the decoder
rqrr. A typed code is the fallback. - 01M53WZ05VFPG8SN581VZSWHKJ After the pair code, the app signs the person in with Google through Credential Manager. riff-server takes the code, the name, the Google ID token and a DPoP proof of the key of the phone. The audience of the ID token is a Web OAuth client of the riff. riff-server makes the sign-in of the phone only when the phone signs in with the Google account that the code is tied to.
- 01M53NTR7D6145FSQ3WVZ1H52W The app makes the device key in the
Android Keystore: ECDSA P-256, not exportable, in StrongBox when the
phone has it. The key never leaves the Keystore. riff-server binds
the sign-in of the phone to the thumbprint of that key. Each call of
the app has a DPoP proof of the key, as for
riff login. - 01M53NTRAKPS7QVCTFS2GJPX4G riff-server sets the host of each call
of a phone to the name of its pair. A line of a phone shows as
<USER@NAME>. riff-server refuses a name that is the name of another device of the same person, or the host of one of its sessions or posts. - 01M53NTRDKDBTCWX0QNFJY9D4K The scope of the kind
phoneis the chat only: the token of a phone acts only on the threadchatand on the notices of that phone. riff-server refuses each other call of a phone token, for example a claim, a pause, an invite,who, a session token and a read of another thread. - 01M53NTRGP2MJG6JT85BCVYE18
riff device listshows each device of the person: its kind, its name, the time of its sign-in and the time of its last call.riff device remove NAMEends the sign-in of that device at once, also of a machine. With--user, an admin names a device of another person.riff logout --allandriff remove --emailalso end the sign-in of each device of the person. - 01M53NTRKQ894YHQD9YYRNJBHZ The app takes the parse of
@leadand@USER, the signed payload of a post, the check of a signature and the DPoP proof from riff-core. So the app andriff chatcannot differ. - 01M53NTRPS73Z0TPVDH7DCM2J3 The app sends its own build, as each client of riff does. When riff-server refuses its release line, the app says to install the APK of the release of riff-server.
- 01M53RWDJ4J5TERZMBB3FC2ZKE Gradle builds the APK, with no Java
source of the riff project. Each release of riff attaches
riff-phone.apk, of the same build, signed with the release key of the riff project. The key is a secret of the GitHub environment of the release. A person installs the APK by hand. When Android asks for a verified developer, the riff project uses a free account of limited distribution. Google Play comes later, if at all. - 01M53NTRSTXCK3VP8YGQETEZX5 The app keeps no text of a message in its storage: only the alias of its key, its tokens and the number of the last line that it showed. A key of the Keystore encrypts the tokens.
- 01M53NTRZT0B7539TXA5BV2BR8 A phone gets a notice for a line of
the thread
chatwhosetohas a selector with its user andlead=true, from a sender of another user. It also gets a notice for each line of the verified lead of its user. A line of a person client of its own user gives no notice. riff-server finds the lines from theto, the sender and the lead mark, not from the body. - 01M53NTS32HD38TSK335EY7V9C While the app runs, it follows the chat, and shows a local notice through JNI with the sender and the line. In the background, the phone gets an FCM notification message with a fixed text, and no text of the line. The Firebase library shows it.
- 01M53NTRWVX0Z7R211JF1T6GKJ riff-server sends the notification messages through Firebase Cloud Messaging (FCM), with the HTTP v1 API and its Cloud Run service identity, with no key file. The Firebase project is the Google Cloud project of riff-server. A riff-server with no FCM project sends no push. A phone with no Google Play services gets no background notice.
- 01M53NTS63FNFS2MESV5E9JZH4 The person turns the notices on and off in the app. The app asks Android for the permission to post notices at the first turn on. riff-server deletes the FCM token of a phone when FCM answers that it is not registered. A push never delays a post.
- 01M53NTSFADCRYKW71JSKW2504 The app calls only its riff-server, Google for the sign-in, and FCM through the Firebase Messaging library. It has no analytics, no crash report and no other library of Firebase.
- 01M53NTS97JVY20GS4BRR3NDDP If a later release removes riff-server, the phone talks over iroh to a daemon of its user, as a client of that daemon. It is no peer of the riff, and holds no copy of its records. That daemon pairs the phone. That release chooses how a notice reaches the phone.
- 01M53NTSC8C86B8S0HN0A5XAHB The design of end-to-end encryption (#958) chooses the encryption key of a phone, and whether a new phone reads the old lines. The app holds the encryption of #958, in Rust. The pair of a phone is the step where a machine of the person admits the new keys of that phone.
Sessions in containers
The book part Sessions in containers has the picture and the table of what sessions share.
The re-cut: isolation by default
The requirements of the re-cut (#953) replace the plan of Wave C and the riff of peers. The build items of the re-cut hold the requirements of this part when they merge.
- 01M53NGS8FGRNY12DDJA3P0GH2 riff is isolation by default, with riff-server. The daemon of each machine runs each work item, a build or a verify, as a container session (01M4Z60B7NW4GHKGCYZZDG26R6). riff-server stays the server of the riff: the claims, the messages, the threads, the chat, the sign-in, the pauses, the holds and the update of each machine. The waves stay on the forge (R223). riff has no riff of peers (01M53NGW0DWJ075C4MDP06WAGS).
- 01M53NGVB7SZJW7X8SYS3B0ZHG The parts of riff-server stay as they
are: riff-server and its log,
riff cloud, the sign-in, the tokens, the device key and the signed messages, and the members and the roles of riff-server. The riff of peers is gone (01M53NGW0DWJ075C4MDP06WAGS). A second machine can use the riff-server of a first machine (R203). - 01M53NGWTNC3655ZZJ76M1R5R7 The claim of a container session is a claim on riff-server. The daemon makes it as the worker session of the item (01M4ZV3B6Q0Y6AX0KSKC55S31R). The riff tools of each container session act on riff-server (01M4ZV3B6T2KB09WJ59F07HXET).
- 01M53NGVE839TRH7XB7CDPQNZE The rollout runs the wave of the daemon
(01M4ZV3B6J89KJQ0F14YWTZMJQ). When the rollout of a lead finds free
work, it asks the daemon of its machine to run the wave of the
repository, when no wave of that repository runs there. It starts no
worker in tmux by itself.
riff lead startalso starts the wave of the repository on its daemon, when no wave of it runs. - 01M5515CFHRA6AJG4GKX0ZV77W
riff wave stopstops the wave of the daemon until the next look of the rollout with free work. To keep it stopped, a person pauses the repository. - 01M5515CJ7QDJ639VGR7SJTE08 When
riff lead startcannot start the wave of the repository, it prints the reason and the stepriff wave run, and it starts the lead. - 01M558EY88CACNZ55FWMRT8RP7 Each look of the rollout waits at most 5 seconds for the status of the daemon of its machine. A daemon that gives no answer in that time runs no wave for that look. The look goes on: the idle workers in tmux still get their requests, and the lead gets the note with the reason (01M5515CA4CZ6KV4ZD6Y0744Z7).
- 01M558EYAJFZK7Y822NVWGBS9H Before the ask of the wave, the
rollout and
riff wave runask the daemon to have the session image that the repository pins: podman runs, and the image is on the machine, or the daemon pulls it. When the daemon cannot, it gets no ask of the wave, and the lead gets the note with the reason. So a machine that cannot start a container runs no wave, and sends no message of a failed start. - 01M558EYCXPFW8CJV3XJ7HK3NR When riff-server refuses the claim of a job of a wave, the daemon ends the new worker session of the job at once. A refusal for a pause does not count as one of the 3 starts of the item (01M4ZV3B6PSRH40EYQEVYH2MBA), and the lead gets no message for it. Each other refusal counts, and the lead gets the message of a failed start.
- 01M57A7A1W2K9440WZA1QZ3M9T Before each start of a wave, the
daemon checks the facts of the host that no item changes: the
sign-in of
gh, and the sign-in ofclaudewith 60 minutes or more left (01M4Z81B3A324KKE8M8Z71TM4M). When a fact fails, the wave starts no item, and no start of an item counts. It waits, as it waits for room, and looks again. The lead gets one message when the wave starts to wait, with the step for the person.riff daemon statusshows the wave as waiting, with the reason. - 01M57A7A48A70Y7PTD95A3MBYC
riff wave runon a wave that the daemon runs clears the starts of each item of the wave (01M4ZV3B6PSRH40EYQEVYH2MBA). A spent item then starts again. - 01M57FJ3WWMQSDBFRRHXN3SMFQ The wave of the daemon starts a build
session for the item of an open pull request with auto-merge on and
a conflict with
main, also after its verify passed, when the pull request is not in the merge queue (01M5TKPNDET56VR8WZ74FSR71F). A pull request whose verify failed is free work as a build already. The start text tells the session to rebase the branch onmain, resolve the conflict, run the checks, and push to the same pull request. Such an item comes after the fix items of a brokenmain(01M51QEYS538XA58KDRY844T52), before each other item. The wave starts no verify of a head commit with a conflict. Each head commit with a conflict starts one session in a run of the wave. A start that riff-server refused for a pause, or a fault of the host, does not count.riff wave runclears the starts of the rebases too (01M57A7A48A70Y7PTD95A3MBYC). - 01M57FJ3Z8AFDY9G0KTAYZJESY When the rebase session of a wave ends and its pull request still has the head commit with the conflict, the daemon sends no verify request. The lead gets a message with the pull request, the commit and the steps for the person.
- 01M53NGVH8A19QS3CQ79ENVVXT A second machine of a person takes work
in containers when its daemon runs the wave of the repository:
riff wave runin a clone on that machine. The claims on riff-server keep two machines from one item (01M4ZV3B6M7PZRJ39XTXBZP3ZH). The limit of the containers of each machine is the limit of its daemon (01M4ZV3B6HAFTSC2K63FMC5FYA).riff workers hoststays, for the workers in tmux. - 01M599XR3NMJD9RAJ6ZAMW6KG5 riff starts no worker in tmux. The
rollout of the lead asks the daemon for the wave, and gives no request
to a session in tmux. riff has no
riff workers, no workers host, no pool of build jobs, no monitor of workers, and no clear, reap or stop of a worker pane.riff tophas no row for a worker in tmux. The daemon runs all work in containers. - 01M59BRGZT6J45BWKPM9XGEPEJ riff stops no idle worker. The server
has no idle stop: no ask to stop in the reply to a keep-alive, no note
to the lead for it, and no hold for an unread request. A message that
starts with
request:is a plain message. The settings of idle workers stay in the log so that an old record reads, and they do nothing. See #887. - 01M53NGW0DWJ075C4MDP06WAGS riff has no riff of peers of Wave B (#803): its code, its tests and its book text are gone (#959). No cargo feature and no flag keeps it. Its items wait in the Backlog: the live checks (#846, #920, #921, #922), the fixes (#928, #943, #944, #946, #957) and the facts of n0 (#941). A new design brings them back when Mike decides.
- 01M53NGW3FR555TWP8Z8HHRVDJ The daemon binds no iroh endpoint,
contacts no service of n0, reads and makes no daemon key in the OS
keyring, opens no riff of peers and no folder
state, writes no record, and reads and releases no claim on the forge.crates/riff/Cargo.tomlnames no dependency that only the riff of peers uses, andCargo.lockhas noiroh. - 01M53NGW6F5HP1DZ9QRGP50MGT riff has none of these, also not in
--help:riff join,riff leave, the flag--peersofriff runand ofriff wave run, and the flag--forgeofriff release. The member commands act on riff-server (01M588J2QAF288ZB3M9PBN71ZS).riff daemon status,riff viewandriff topshow no key, no riff of peers and no peer link. With no--host, they show no session of another daemon. - 01M53NGW9FH02EM74GKY829808 The re-cut supersedes each requirement of Wave B (#803), and each requirement of the riff of peers in “Data and peers”: the log of peers, the keys of the members, the claims on the forge and the transport. 01M4ZV3B6T2KB09WJ59F07HXET and 01M501P9XWXWDBSXP4QRTHTZ4P hold, except their words of peer to peer: the riff tools of a container session and of a lead in a container act on riff-server.
- 01M53NGWFBWR1WMJWC2DHVHJ3T riff does not read, change or delete
the state of the riff of peers that an old release made on a machine:
the folders
stateandstate.lost-MSand the filesinvites.jsonandforges.jsonin the folder of the daemon, and the secretdaemon-key DIRin the OS keyring. A claim refrefs/riff/claims/ITEMon the forge stays until a person deletes it. - 01M53NGWN190T2MYB1EZAD36VB The items of Wave C wait in the Backlog: the update with no riff-server (#881), the move (#882), the delete of the cloud parts (#883), the live check of the move (#884), the commands of a person on the daemon (#885), the removal of the sign-in (#886), the removal of the plugin and of the workers in tmux (#887), the removal of riff-server (#888), and the steps of #889 that end riff-server. A new design brings them back when Mike decides that riff-server goes.
Shape
- 01M4Z60B7E6X53NBJ01R95RYGE riff is one binary: the CLI, a daemon, the MCP shim and the relay. It runs on the machine of each person. Cloud workers are future work. riff-server stays a second binary.
- 01M4Z60B7FCREZDCTVR73WWRY1 The daemon of riff is a systemd user unit. It owns the sessions. riff-server stays the server of the riff.
- 01M4Z60B7GTV4HHF9R3TMZRS7J riff runs on Linux only. Each platform
part is a trait, and its Linux implementation is behind
cfg(target_os = "linux"):Runtime(the start, the stop, the attach and the events of a session) andServiceManager. A session is aSessionSpecof plain data: the image, the binds, the environment and the limits. The OCI implementation makes a container from it. A pod backend can use the same spec later. - 01M4ZBGK3A977KZW50AZFR95E9 The steps of a
Runtimeare: find an image, pull an image, start aSessionSpec, stop a session, and attach a terminal to a session. A start gives the input, the events, the errors and the end of the session. A spec holds no secret: the value of each variable that comes from the host goes to the start beside the spec. - 01M4ZBGK5G3EFXWT61DR2NHMDD The code that drives a session takes
its
Runtimeas a parameter. Its tests use the fake runtime of riff, which starts no container. A session of the fake runtime prints the events that the test gives, keeps its input, and ends with the exit 0 when its input closes. A stop ends it with no exit code. - 01M5MRG35BYRGZYD4VQVV8ZTYX Each command of
riff, andriff-server log, ends with the exit 0 and prints nothing more when its stdout or stderr is a pipe that the reader closed, for example inriff daemon status | head -1. A panic hook does it. The process keeps SIGPIPE ignored, so a write to a socket or to the pipe of a child process still fails with an error and does not kill the process.riff daemon,riff relayand the server ofriff-serverkeep the default panic hook.
Ship
- 01M4Z60B7H67YSP22NMGT3DM7X riff ships as one static binary, from a
GitHub release or with
cargo install. riff has no image of its own. - 01M4ZPJ04D9MJHNPAV0WN1KCES The static
riffandriff-serverare built forx86_64-unknown-linux-muslin the imagerust:CHANNEL-alpine, where CHANNEL is the channel ofrust-toolchain.toml.just staticbuilds them with podman, else docker, intotarget/static. The ELF of each has no interpreter. - 01M4ZPJ06QYVA23N9SQ45T8WQC The CI job “Static riff” builds the
static binaries of each pull request and each push. It fails when an
ELF has an interpreter, or when
riff --versiondoes not run in the session image thatriff.tomlpins, withriffbound at/opt/riff/riffwith no write. - 01M51117NDKF11FNZ915TTQED4
just session-testruns the tests ofcrates/riffin a container of the session image thatriff.tomlpins. The container has--init, the user of the host withUSERandLOGNAMEand an entry in its passwd and group files, a home of its own and the limits--cpus 4and--memory 8g(01M56Q4AVRM8F7Y585TSGD53K3). In CI, the test parts of the Gate run there (01M515Z3Y7GSFNTJGXJD727MQK). - 01M56Q4AVRM8F7Y585TSGD53K3
just session-testkeeps--cpus 4and--memory 8g: a runner of the Gate has 4 CPUs. These are not the caps of a session of the host (01M56Q4AQ86V99RC8CA1KGCMSZ). - 01M515Z3Y7GSFNTJGXJD727MQK The jobs
Tests I/Nof the Gate rundeploy/session-test.sh --part I/N: part I of N of the tests of the workspace, in the container of 01M51117NDKF11FNZ915TTQED4, with docker and a cache oftarget/session. The key of the cache is the hash ofriff.toml,rust-toolchain.tomlandCargo.lock. No other job runs the tests in the session image. - 01M511FGMFF17G2G8RV6ST9070 Before the tests,
just cargo-testbuilds each binary of the workspace. So a test of one crate finds the binary of another crate, for exampleriff-serverin a test ofriff, also in a new clone with notarget/. - 01M513W2V4G94R7F4D4Q3M2RA1 For a pull request and a push to a
branch, the CI job “Static riff” builds the profile
dev, with a cache oftarget/staticand of the downloaded crates. The key of the cache is the hash ofrust-toolchain.tomlandCargo.lock. For a tag, it builds the profilereleasewith no cache.build.shkeeps the downloaded crates in the volumeriff-static-cargo, or in the volume or the absolute folder thatRIFF_STATIC_CARGOnames. - 01M4ZPJ091W3NYV1V0FJS9K6MS For a release tag, the CI job “Static
riff release” adds
riff-x86_64-unknown-linux-musl,riff-server-x86_64-unknown-linux-muslandSHA256SUMSto the GitHub release of the tag. It waits up to 60 minutes for the release. The job “Deploy” of the tag runs only after it passes. - 01M4ZPJ0BAQZ6AN06FGXS5D4FP On Linux x86-64,
riff updatedownloadsriffandriff-serverfrom the GitHub release of the tag. It checks the SHA-256 of each file againstSHA256SUMSof the release, and that each ELF is for this CPU and has no interpreter. When a check fails, it installs nothing. Else it renames each file over the old one in the bin folder of cargo, so a process that runs keeps its old file. - 01M4ZPJ0DJ9FTYNPTBBVJCTB37 When the release has no
SHA256SUMS, or no line for the binaries of this machine,riff updatebuilds the release withcargo install, and says so. - 01M510NWBNC20GXFRMHPW12TW9 With
update.auto = true, when theriffof the bin folder of cargo gets a reply from ariff-serverof its own build, and thatriffor theriff-serverbeside it is not static, it startsriff update --tag vX.Y.Zof its own release in the background. Soriff updateputs the static files of the release in place. This fix gets one try for each release, apart from the try of the update to that release. A riff that runs from another folder starts no fix. - 01M4ZPJ0FXTC78Z8GG2QA65167
just real-sessionruns the static riff of the tree, so its daemon can runriff relayin the session image. - 01M4Z60B7JF80TMP6FHQ04MHCZ Each repository names its session
image, pinned by digest, in its riff config. An image has
claude,git,ghat/usr/bin/gh, CA certificates, a POSIX shell and the toolchain of the repository. It holds no riff. - 01M4Z7SP3KSKJYKNRP1MQ70C4Q The riff config of a repository is the
file
riff.tomlat its root. The keyimageof its table[session]names the session image asHOST/PATH@sha256:DIGEST. riff refuses a tag, also beside a digest, and a name with no registry host. - 01M4Z7SP5TGJ3DMWFGE5SN68N1 The session image of this repository is
built from the folder
deploy/session/. - 01M4Z7SP7Y19BQ8QTV1T6AE8SR The workflow “Session image” runs on
each change of
deploy/base/,deploy/rust/,deploy/session/, ofriff.tomlor of the workflow, in a pull request and onmain. It tags each imagetree-ID, where ID is the git tree of its folder, and pushes it to the package of the image on ghcr.io. When the registry has that tag, it builds nothing. The summary of each job names the digest. - 01M4Z7SP9YZ5JCHA8YD8R5A4Y9 The workflow “Session image” fails when
riff.tomldoes not pin the digest of the image ofdeploy/session/of its commit. So a change of the Dockerfile and the change of its pin go in one pull request. - 01M4Z7SPC20RWG218EWPEGYZTF
riff imageprints the session image that the riff config of the repository of the current folder pins. - 01M51STPHXSSZMHF9T7Z85097R The merged pin of a main clone is the
session image that
riff.tomlpins on the default branch oforigin, the branch oforigin/HEAD. riff fetches that branch first. The wave of the daemon,riff wave run,riff run,riff lead startand the image step of an update use only the merged pin. So an edit ofriff.tomlthat is not merged never chooses the image that riff pulls and runs. - 01M51STPMEM62Z8B26YM2CH6QH When
riff.tomlin the working tree of the main clone pins another image, or no good image, riff says so in one line, and uses the merged pin. - 01M51STPPWYFKQ1ESCNA6V3WV2 When the fetch of the default branch
fails, riff says so in one line, and reads the branch that the clone
has. A main clone with no
origin/HEADhas no merged pin: riff stops, and gives the fixgit remote set-head origin --auto. - 01M51SVXQK037E20R7WAY66532
riff image,riff initand the image of this folder inriff update --checkreadriff.tomlof the working tree. So a person can try the pin of a branch before its merge. - 01M4Z7SPE5Q02RQWW5VZBEK2G3 The packages
riff-session,riff-baseandriff-ruston ghcr.io are public, as the repository is. So podman pulls each image with no sign-in. - 01M4Z60B7KEV0H161RBZ1SDRY2 riff binds its own binary into each
session with no write, and runs it as the entrypoint:
riff relay. So the riff code in a session is always the riff code of the host. - 01M4Z60B7K13MSYM8ZY20WN6NV The first run of
riffchecks for podman, installs the user unit of the daemon and pulls the image of the repository. - 01M4Z60B7MW963MBQAPN2CJEQZ The first run of
riffalso checks rootless podman:/etc/subuidand/etc/subgidhave a range for the person, and cgroup v2 givescpuandmemoryto the user. It readsuser@UID.service/cgroup.controllers, notpodman info. It names the fix of each check that fails. - 01M5QY976MTPRX5J4N1MKXAGDK A main clone whose default branch of
originhas noriff.tomlhas no merged pin: riff stops, and the error namesriff.toml, the branch and the fix. The fix addsriff.tomlwith the keyimagein the table[session], and points to the how-to “Add another repository to the riff”.riff lead startchecks the merged pin before it asks the wave and before it prints that the lead starts: with no merged pin, it prints no line that the lead starts. - 01M5SMVVC4GQS310CVK3H9CMQ7 The table
[session.env]ofriff.tomlgives the variables of the environment of a session, and the table[session.volumes]gives its named volumes: the key is the name of the volume, and the value is the path in the container. A value and a path may have$HOME: riff puts the home of the person there. Both come from the mergedriff.toml, as the image does (01M51STPHXSSZMHF9T7Z85097R). Ariff.tomlwith neither table gives a session neither. - 01M5SMW28KT5D5HPRH7KAZ8TVC riff sets in a session only what riff
needs itself: the home, the user, the sign-ins, the git config, the
Claude folder, the clone, the folder of the socket,
--memoryand--cpus. It sets no variable and no volume of a language or a tool: noRUSTC_WRAPPER, noCARGO_HOME, noCARGO_BUILD_JOBS, no sccache and no cache volume. A repository sets them inriff.toml. This supersedes 01M4Z60B7RPSQ1ABGVMJSZWS5P and 01M57818168N3AQGE3G5GDYDDD in full, and 01M56Q4AQ86V99RC8CA1KGCMSZ in part. - 01M5SMW2B4GMJ3V6BCX1KGC1ZY riff refuses a
riff.tomlwhose[session.env]names a variable that riff sets itself (HOME,USER,LOGNAME,GH_TOKEN,CLAUDE_CONFIG_DIR,CLAUDE_CODE_OAUTH_TOKEN, or a name that starts withGIT_CONFIG_orRIFF_), or whose[session.volumes]names a volume that podman could read as a path, or gives a path that is not absolute. The error names the key and the file. - 01M5SMW2DQ8G919EJ4P0GXF9Y3 The
riff.tomlof this repository sets the build cache of its sessions: the volumesriff-cargoat$HOME/.cargoandriff-sccacheat$HOME/.cache/sccache, and the variablesCARGO_HOME,SCCACHE_DIR,SCCACHE_CACHE_SIZE=40G,RUSTC_WRAPPER=sccache,SCCACHE_IDLE_TIMEOUT=0andSCCACHE_IGNORE_SERVER_IO_ERROR=1. The tools lock the volumes themselves. The toolchain is in the image, with no write, so there is no volume for rustup. A test checks that the file sets them. - 01M5SMSB0NFBD1XHEW3BSE4BDP
riff image --checkasks the registry the question of 01M5SMRRAG1WPBJMQKKTPSRHRP for the image of the current folder, and gives the same fix when the registry gives no manifest with no login. So a person can check the package before a pull request pins it, and on each host of the wave. - 01M5SNT57ANM99NQS5SNB8XPWV riff publishes the images
ghcr.io/como-technologies/riff-baseandghcr.io/como-technologies/riff-rust.riff-basehas Debian slim,bash, CA certificates,git,curl,procps,claudeat a fixed version andghat/usr/bin/ghat a fixed version. It has no language, no build tool, no D-Bus, no keyring, no riff and no secret.riff-rustis builtFROMa digest ofriff-base. It adds the C linker and one Rust channel withrustfmtandclippy. A project builds its session imageFROMa digest of one of them, and adds its toolchain. Supersedes 01M4Z60B7JF80TMP6FHQ04MHCZ in part: the first base. - 01M5SNTDW9WVEXZW5D0TKGBA8Z The images of this repository come from
three folders, each
FROMthe one before it by digest:deploy/base/(riff-base),deploy/rust/(riff-rust) anddeploy/session/(riff-session).riff-sessionadds only what riff needs:just,mdbook,sccache,rust-analyzer, D-Bus and the keyring of the tests. The Rust channel ofdeploy/rust/is the channel ofrust-toolchain.toml, and the components ofdeploy/rust/anddeploy/session/together are its components. A test checks that they agree. Supersedes 01M4Z7SP5TGJ3DMWFGE5SN68N1 in part. - 01M5SNTDZ95BRDCPQEKDTYD2PY The workflow “Session image” builds the
three images in the order of their folders. Before it builds an
image, it checks that the
FROMof its Dockerfile pins the digest of the image before it. After the last image, it checks thatriff.tomlpins the digest ofriff-session. It runsdeploy/contract.shonriff-baseandriff-rust, built or found. - 01M5SNTE280ABA4RYEPK445P3A
deploy/contract.sh KIND IMAGEchecks the contract of an image in a container: for KINDbase, each tool ofriff-baseis there at its fixed version, and no language is; for KINDrust,rustc,cargo,rustfmtandclippy-driverare there at the channel ofrust-toolchain.toml. A test readsdeploy/base/Dockerfilefor the same contract: each tool, and no language. - 01M5SNTE54DH3N568GZXRQ86DC For a release tag, the workflow
“Release images” checks that the packages
riff-baseandriff-rustare public, with an anonymous pull. Then it adds the tagsX.Y.Zandlatestto theriff-basethatdeploy/rust/Dockerfilepins and to theriff-rustthatdeploy/session/Dockerfilepins, and adds their digests to the notes of the GitHub release. It waits up to 60 minutes for the release.
Sessions
- 01M4Z60B7NW4GHKGCYZZDG26R6 Each work item has one session. The session starts, does the item and ends. No session waits idle, and no session clears its context between items. A verify is an item too.
- 01M4Z60B7PTJMB37578K8FV22Q Each session has its own clone:
git clone --reference MAINinto a folder of riff, bound into the container with write. The session reads the objects of the main clone through the alternates, and pushes from its own clone. No session writes the git dir of the main clone or of another session. The clone of a session goes when its item ends, after its push. - 01M4Z60B7PQ07HRJBXS1TA73PX The container sees the objects of the
main clone (
MAIN/.git/objects) at the same path, with no write. So git reads the alternates of the clone of the session. - 01M4Z60B7QDWEFQ96H448RE9N2 Each session is a rootless podman
container (
podman run --rm --userns=keep-id) from the image of its repository. riff binds with write only: the clone of the session, its Claude folder, the folder of its socket, and the volumes thatriff.tomlnames (01M5SMVVC4GQS310CVK3H9CMQ7). riff binds~/.gitconfigand~/.config/ghwith no write, or as an overlay (:O) whose changes go when the session ends. Each other write goes to the copy-on-write layer of the container, and goes when the session ends. - 01M4Z60B7SHAMXXFK15BE3KZSP The home of the person is a tmpfs in
the container, at the same path, with
HOMEset:--mount type=tmpfs,destination=$HOME,tmpfs-mode=0700,U=true. The binds of~/.gitconfigand of the Claude folder go on top of it. Each other write in the home goes when the session ends. - 01M4Z60B7S8V9KKZPNBCQ6M99N The
SessionSpecsetsUSERandLOGNAMEfrom the host, as a login shell does. - 01M4Z60B7TQV799Z4J5KAW2XMZ Each container has its own
/tmp, its own processes and its own loopback. So a temp file, apkillor a test port of one session does not hit another. A session reaches the internet, as a plainclaudedoes: no proxy and no filter. - 01M4Z60B7VCTG76S8N1CBWWG6D riff adds no bubblewrap, no Landlock and no seccomp filter to a session. The defaults of podman stay.
- 01M4Z60B7WXBRPYW38XW9NVC17 A session runs its builds and its tests in its own container, with its own rights. riff has no broker, and no separate sandbox for a test run.
- 01M4Z60B7W6F3CC5XJE0VS7Y86 The relay talks to the daemon through one Unix socket, in a folder that only that session sees. The socket tells the daemon which session asks. A local session needs no session grant, no session key and no DPoP token.
- 01M4Z60B7XT7462XMV49GJTVSJ Each container gets
--memoryand--cpus(01M56Q4AQ86V99RC8CA1KGCMSZ).podman stopends a session and each of its processes. - 01M56Q4AQ86V99RC8CA1KGCMSZ The caps of a container are settings
of the host, not constants. Each new container gets
--cpus Nand--memory GBg. The defaults are 8 CPUs and 8 GB. The daemon puts the caps in the spec when it starts the session. A session that runs keeps its caps. - 01M51117K3WXK35CE67KPRFMA8 Each container of a session starts
with
podman run --init. The init of podman (catatonit) is PID 1. It reaps each orphan process of the session, and it passes each signal to the command of the session. The init must be on the host. - 01M4Z60B7Y4XD6TJCYEYJT2R4S The daemon keeps one limit for the host, in addition to the limits of each container: the number of live sessions, and a floor of free memory. It starts no session while the free memory is below the floor. The defaults are at most 3 live sessions and a floor of 8 GB.
- 01M4Z60B7Z24WBD6HV6ER0WN9K A session lives through a stop, a
restart or an update of the daemon. conmon, the monitor of podman, is
the parent of each container, not riff. So the container runs on when
the daemon stops.
riff relaystartsclaudeand holds its stdin and stdout. While the daemon is away, the relay keepsclauderunning and keeps its events. When the daemon comes back, the relay connects again and sends the kept events by sequence number.claudedoes not see the break. - 01M4Z60B800BH5BHX0H9JVDG6A An update of riff reaches a worker and a verify at their next start only: the bind keeps the old binary in a session that runs. So the protocol between the relay and the daemon stays compatible across one version. The recovery of the sessions after a reboot or a crash of the host is future work.
- 01M4Z60B81RTZT71PN5HV0GZHX The lead runs in a container with a
terminal (
podman run -it). conmon holds its PTY. The ratatui view of riff shows the sessions, and attaches to the lead or to a takeover. riff uses no tmux. - 01M59BXNPR3AZ0HAS9GQCZ4PH2 riff has no plugin, no hooks and no
commands for a session on the host:
riff connect,riff enable,riff disable,riff watch,riff hook,riff statuslineandriff mcpdo not exist. A session runs in a container, and gets its tools fromriff relay mcp.
The riff daemon
-
01M4ZDTVH57VE77YT20G65A4BQ
riff daemonruns the daemon in the foreground. The systemd user unit runs the same command. At a SIGTERM or a SIGINT, the daemon stops, and stops no session. -
01M4ZDTVK74596M0ZA46N51NKV
riff daemon installwrites the unitriff.servicein the folder of the user units of systemd (XDG_CONFIG_HOME/systemd/user, else~/.config/systemd/user). The unit runsriff daemonwith the absolute path of this riff. It gets thePATHof the shell that installs it, andXDG_DATA_HOMEwhen it is set. Then riff runssystemctl --user daemon-reload,enableandrestart, and waits up to 10 seconds for the daemon to answer. The unit file and the link of itsenableare the only files of riff in the home outside its own folders. WithRIFF_HOMEset, riff installs nothing. -
01M501M0T0JBWGMMHCT7HT95T9
riff daemon uninstallrunssystemctl --user disable --now riff.service, deletes the unit fileriff.service, and runssystemctl --user daemon-reload. It deletes only a unit file whose first line riff wrote. It keeps the folder of the daemon and each container. With no unit file, it says that no daemon is installed, and exits with 0. WithRIFF_HOMEset, riff removes nothing. -
01M4ZDTVNBKDM64043GGWQEV4S
riff daemon statusshows the process ID, the build and the age of the daemon, its folder, and each session that it owns. When no daemon answers, it shows the state of the unitriff.service, and exits with 1. -
01M4ZDTVQEHD87WD5TSN93XFSS The folder of the daemon is
daemonin the data folder of riff, with mode 0700. It holds the control socketriff.sock, and a foldersessions/NAMEfor the socket of each session, with mode 0700. The daemon starts no session whose spec binds the folder of the daemon, a folder above it or a path in it, or binds a path at/run/riff. -
01M53YP97S9BDMFBN0X5G444MY With
RIFF_DATA=DIR, the data folder of riff isDIR: the folder of the daemon isDIR/daemon, and the session folders are inDIR/sessions.RIFF_DATAwins overRIFF_HOMEfor the data folder. The settings, the local files and the secrets stay where they are. Only the real-session test sets it. -
01M53YP9AHPZ9YPNQFJ1SYZGWF A daemon with
RIFF_DATAorRIFF_HOMEset is never the daemon of the unit: it changes no unit at its start, and it does not start again for a new riff. With either variable set,riff daemon installinstalls nothing,riff daemon uninstallremoves nothing, andriff updatedoes no step for the daemon. -
01M4ZDTVSP0T1YENKAWGKA114D The daemon binds the folder of the socket of a session at
/run/riffin its container, with write, and setsRIFF_SOCKET=/run/riff/riff.sock. Each session has its own listener. So the listener tells the daemon which session asks: a hello on the socket gets the session ID, the item and the name of the container. The folder goes at the end of the session. -
01M4ZDTVVV56X964S28GS6K2Z2
riffand the daemon send JSON, one message on each line. The first line of each connection has the version of the protocol and one ask. The daemon refuses another version, and says to start it again withriff daemon install. An error of the daemon shows no part of a line that it cannot read. -
01M4ZKX0NXN7E33VC3PSFGSPDT
riff run ITEMdoes one work item in one rootless podman container, through the daemon. It asks the daemon for the image (the daemon pulls it when it must), then for the session. The host does each riff step: it checks the facts, starts a worker session, claims the item, makes the clone, sends the verify request and releases the item. In the container,riff relaystartsclaude(01M4ZHSZZDH0C01A5EVD0KFY14). The daemon writesevents.jsonl, and sends each event, with no secret, toriff run. With no daemon,riff runstops before the claim, and says how to start the daemon. Ctrl-C asks the daemon to stop the container. Whenriff runends with no stop, the session goes on in the daemon. -
01M4ZDTW00B7N6T9P3MK066KA7 A stop or a restart of the daemon stops no container. The daemon starts each
podmanwith noINVOCATION_ID, so podman puts conmon in a scope of its own, not in the cgroup of the unit of the daemon.podman runhas--sig-proxy=false, so a signal to it does not go to the container. The daemon stops a container only whenriff runasks, or at the quiet limit (01M4ZSZX77AJDGWNQMGBPV2QXG). -
01M517BTDRKDXD3X84P6AEAF7P
riff updatereads the unitriff.servicethat riff wrote. When itsExecStartruns another riff than theriffonPATH, riff writes the unit again with that riff and the same variables, and runssystemctl --user daemon-reload. With no unit file, or withRIFF_HOMEset, it changes nothing. A failed step fails no update. -
01M517BTJ9EV4ZGPYRB6HXJJC9 The daemon runs an old riff when its binary was replaced, or when its unit names another riff. Then
riff updaterunssystemctl --user --no-block try-restart riff.serviceonly when the daemon owns no session, no start waits for room, and no job of a wave runs. Else it says that the daemon starts again after its last session. A daemon that does not answer stays as it is. -
01M517BTP6BAGTRBYWPX9GPVE0 The daemon that the unit runs looks for an old riff at the end of each session and of each job, and each minute. When it runs an old riff and owns no session, no start waits and no job runs, it asks systemd one time to start the unit again. A failed ask is one line in its log.
-
01M53NEE2AVNHA8RD8N40FPH03 The new start of the unit stops the
systemctlchild of the daemon that asks for it. When that child ends by a signal with no text on stderr, the new start runs: the daemon logs no failure and noriff daemon installstep. An exit code that is not 0, or a signal with text on stderr, is a failed ask, and its line names the error. -
01M4ZHT01HH88KPA8628M7VBEY The daemon keeps a record of each live session in
live/NAME.jsonin its folder, with no secret. At its start, the daemon takes up each session whose container runs, or when podman cannot say: it binds the socket of the session again in the same folder. It removes the record of each other session. -
01M4ZHT03MAH0B5Q4S6GZC5JRP A session ends when the daemon has the exit of its relay, or when its container ends.
-
01M4ZSZX77AJDGWNQMGBPV2QXG After the daemon closed the input of
claude, it stops the container of the session when no event comes for the quiet limit: 30 minutes. Each event starts the quiet limit again. The daemon tells eachriffthat follows the session why it stops the container, and writes it to its own output. The quiet limit is longer than the longest check of a session. -
01M4ZHT05QHQRQ2KGVJFB9F7HD On the control socket,
riffcan follow a live session: the daemon sends each event after a number, then each new event, then the end. -
01M4ZKX0KMS58NR2E9AF8229A8
riff daemon events NAMEshows each event of the live session in the containerNAMEfrom its start, asriff runshows them, and each new event until the end. Ctrl-C ends the view. The session goes on. -
01M579Z9JE6HY9HG3PWR1RR0RQ The daemon of a host has a session of its own on riff-server for each repository that it serves. It serves a repository after
riff daemon hostorriff wave runin its main clone. It keeps each one inserved.jsonin its folder, with its session ID, and serves with the same session after its own restart. Its status isdaemon host: pid P, build B, N sessions. -
01M579Z9MPS86MX7890T6HV18S
riff daemon status,riff daemon limit,riff daemon cache,riff daemon events,riff daemon sweep,riff daemon end,riff daemon restart,riff wave runandriff wave stoptake--host HOST. With it, riff posts the request as a direct message to the session of the daemon of HOST. The daemon does it as the command does on HOST, and posts the reply as a direct note to the sender. riff prints the reply, and exits with 1 when the daemon refused or failed. -
01M57D4FTP2NXAE18XFRXCJ79H Each request to the daemon of another host and each reply go in the direct thread of the sender and the daemon. No other session and no other user reads them. A person in a terminal gets the reply in the direct thread of the person.
-
01M57D4FX5VHQR150QTDS6BA1C
riff daemon events NAME --host HOSTshows the same lines asriff daemon events NAMEon HOST: each event from the start of the session, then each new event, then the end. riff asks for one page after the other: each page has the lines of the events after the last event of the page before, and the lines of the daemon after the last line of the page before, at most 200 lines. With no new event, the daemon holds a request for at most 20 seconds. A session that ends between two pages ends the follow with its end, as on HOST. riff exits with 1 when the session failed. -
01M57GCZVJAHAMPEYY1T6GA48P After the end of a session, the daemon keeps its end for 5 minutes: the events, the lines of the daemon about the session, and how it ended. A follow of the session in that time gets the events after its number, the lines and the end, as a follow that stayed.
-
01M57GCZXZZRPGGWC5HZS49DGM The daemon keeps each line that it says about a session, for example why it stops the container. A new follow gets each line in its place among the events.
riff daemon events --hostshows each line one time. -
01M57D4G1TS8BHJF5A8PW2D79C The events of a session go over the wire only from the events that the daemon keeps: the daemon took each secret out of each event before it kept it. So no secret leaves the host.
-
01M57D4FZGCVE62AP4MVG59HM4
riff view --host HOSTshows the view ofriff viewfor the daemon of HOST, through riff-server. riff asks the daemon of HOST for its status each 3 seconds. Enter andtwork only on HOST: in the view of another host they show a note. -
01M579Z9PZZCVWQKG7V8WZ5KRB The daemon of a host acts only on a verified request from its person, or from the lead of its person in its repository. It replies to each other request with a refusal, and changes nothing: another user, and a session that is not the lead, for example a worker.
-
01M588QXAGD3B8BTK7N17M29WM The refusal of the daemon of a host names its cause: the user of the request when it is another user, else the session of the request, which is not the lead of its user in the repository of the daemon. The help of each
--hostflag that asks the daemon or the workers host of another host says who can use the flag, with the same words as the book. -
01M579Z9SAN24QGN1VV5C7EHM9
riff daemon restartstarts the daemon again on the new riff on disk only when it runs an old riff and no session, no start waits and no job of a wave runs. Else it says why not. It asks systemd 5 seconds after the reply.riff daemon statusshows the new pid and build. -
01M579Z9VRD12FRK6DSYVKDHQB
riff daemon end NAMEends one session of the daemon: the daemon frees its item on riff-server at once, ends its session there, then stops its container. It refuses the lead. -
01M57D3F50347RD8ED79KEA9V7 Each 30 seconds, each session of the daemon of a host sends the facts of the host to riff-server, as the numbers of its machine:
daemon facts:and one line of JSON, at most 4000 characters. The facts are the riff build, the limit and the floor, the live sessions with their item, phase and last step, the starts that wait, the state of each wave, the load, the free memory, the disk, the version of podman, and the sign-ins of riff,ghandclaude. Facts older than 180 seconds count as none. -
01M57D3F7FXTPZB3M72Z1DQ1T7
riff whoends with the partHosts: the facts of the daemon of each host, with their age. -
01M5923PW9YW5S58RQG5Y532RS The
whotool of a session ends with the same partHostsasriff who, in plain text. -
01M57D3F9SWMKP22WNRPRY3X5T
riff topshows the facts of the daemon of a host under the row of that host, with the container sessions of that daemon. A host with facts gets nono numbers. -
01M57D3FC5PB5EQ6Y38Y5R2DBB When the sign-in of
claudeon a host has less than 2 hours left, or is missing, the daemon posts one note to the lead with the step for the person. It posts no new note at each look. When the sign-in has 2 hours or more again, it posts one note. -
01M57D3FEJPHD962GW0H82N8E6 When a sweep of the daemon keeps a folder whose work is over, because its clone holds work that is not on
originand that the sweep cannot push, the daemon posts one note to the lead of its repository. It posts a new note only when the reason changes. -
01M57D3FGV57W96A7SWTEFRCGW The facts give the minutes left of the sign-in of
claude. The sign-ins of riff andghrefresh or have no end: the facts sayonorofffor each, or?when riff cannot read it. -
01M588HSV1APXX4TQ1J1VRZCBF The facts of a daemon show the 1-minute load of the host against its hardware threads, the logical CPUs, with the word
threads:load 1.5/32 threads. The load has the warning style when it is more than the threads. The line of numbers ofriff topshows the same part for a host that tells no numbers of the monitor. The wordcoresnames only the physical cores. -
01M5MS2T8VY8NX4VBMQTCKWDKH A riff command that asks the riff daemon of this machine tries again for 5 s when no daemon answers while systemd says that riff.service is active or activating: the daemon listens a short time after its start. With no unit that runs, it fails at once.
-
01M5MS1Q78AFEY3ZVXNAQXM1FJ A start of a wave that waits for room does not keep the daemon on an old riff: the wave is in
waves.json, and the new daemon waits for room for it again, also with the limit 0. A start ofriff runthat waits for room keeps the daemon on the old riff, as a session and a job of a wave do. -
01M5NVKNGZPK0GG7B7ZWBS0DFE When the sweep of 01M53NJ5V27BCHDNHK2THQQZZQ finds a folder whose work is over, and its clone holds work that is not on origin, it pushes that work to the branch riff-kept/ITEM-ID8 of origin and then removes the folder. It removes a folder with files of the container with podman unshare. The push has the commits of HEAD that origin has not, and the changes of the tracked files (git add -u). It never has a file that git does not track. A clone with only such files pushes nothing. When the push fails, or a commit is on a local branch that is not HEAD, or git cannot read the clone, the folder stays and riff daemon sweep says why. This supersedes 01M53S7WAXD46YPN0DR96XSTMG in part.
-
01M5NVKNKN3PTCWB45BV03M1TW The sweep never pushes or removes the folder of a session that runs, of a job of the daemon, or of an item whose pull request is open or whose issue is open with no pull request. The folder of an open item stays.
-
01M5NVKNPD938ASWY4TD5BGK67 The sweep deletes each branch riff-kept/* of origin whose tip commit is older than 30 days, for each repository of the folders that it looked at.
-
01M5NVKNS35SC8P514GYDJSRQM After a sweep that pushed work, removed a folder with files that git does not track, or deleted a riff-kept branch, the daemon posts one digest note to the lead of the repository. It posts no note at each start for a folder that it already pushed and removed. This supersedes 01M57D3FEJPHD962GW0H82N8E6 for folders whose work the sweep can push.
-
01M5NVKNVTVR722EA148V5127M The book says where the kept work goes, riff-kept/ITEM-ID8 of origin, and how to get it back with git fetch origin riff-kept/ITEM-ID8.
-
01M5P1XBJ6ZX3Q8ZVHR1EJFMFF The lead does not keep the daemon of its host on an old riff. The daemon counts no lead (a session with a terminal) as work: with only the lead,
riff updateand the look of the daemon start the daemon again on the new riff (01M517BTJ9EV4ZGPYRB6HXJJC9). The new daemon takes the lead up with its container, its terminal and its socket (01M4ZDTW00B7N6T9P3MK066KA7), soriff release checkpasses “each host takes the build” while the lead runs. -
01M5P2MG28EE75PPGYAWHTY2S1 The daemon starts the lead, each work item and each verify with claude –effort LEVEL. The effort of the three roles is xhigh by default. The limit of the host holds the three levels in limit.json, and riff daemon limit sets them with –effort for work items, –effort-lead and –effort-verify, also with –host. riff refuses a level that Claude Code does not know: low, medium, high, xhigh and max. A session that runs keeps its effort.
-
01M5QVCAXF5ANTTT46XHR6SF7H When the sweep of 01M5NVKNGZPK0GG7B7ZWBS0DFE finds a folder whose clone has commits on a local branch that is not HEAD, and no branch of origin has them, it pushes each such branch to the branch riff-kept/ITEM-ID8-BRANCH of origin, with the same rule as for HEAD: the commits and the tracked files, never a file that git does not track. Then it removes the folder. A commit on a local branch that is not HEAD no longer keeps the folder. The daemon tells the lead one time, in the digest note of 01M5NVKNS35SC8P514GYDJSRQM, with each branch that it pushed. It posts no note at the next sweep. This supersedes 01M5NVKNGZPK0GG7B7ZWBS0DFE in part.
-
01M5RG2AS2CYJPWGKTV7N2HNYP A stop or a restart of the daemon leaves the network of each container that runs (01M4ZDTW00B7N6T9P3MK066KA7). When the daemon has an
INVOCATION_ID, it runs in a systemd unit, and it starts eachpodman runassystemd-run --user --scope --collect --quiet -- podman run .... So pasta, the network process of podman, runs in a scope of its own, not in the cgroup of the unit, and a stop of the unit does not end it. With noINVOCATION_ID, the daemon startspodman runitself. -
01M5SN561BM9SK24SS0AVJ3P5J
riff wave run --host HOSTandriff daemon host --host HOSTfor a repository that no daemon of the user on HOST serves go to the session of another daemon of the user on HOST. That daemon makes a main clone of the repository withgh repo clone, with theghof the user on HOST, inclones/OWNER/REPOin its folder. Then it serves the repository. Forriff wave runit runs the wave too. A daemon that serves the repository keeps its main clone and clones nothing, so a second request changes nothing. -
01M5SN5644YVEPF3D452ZA2JVD The person, and the lead of the repository of the request, can ask a daemon of the user to serve that repository, also when the daemon serves another repository. The daemon takes a repository only as
OWNER/REPO, with no part that starts with-and no part.or... -
01M5SN566ZH5ZQ52NYT24WYA4P
riff wave runwith no--hostin a lead container starts no wave. It names the hosts of the user with a daemon that does not serve the repository, and exits with 1. -
01M5SMQBTA3HQXPKKK01ZGFB4H When the daemon takes up a session whose container runs (01M4ZHT01HH88KPA8628M7VBEY), it looks if the container reaches the network:
podman exec NAME getent hosts api.anthropic.com. A container that lost its network process runs on with no network, and an answer that does not come in 15 s counts as no network. When podman cannot say, the session counts as connected. A worker or a verify with no network: the daemon frees its item and stops the container, so the wave starts the item again. A lead with no network: the daemon tells the person one time in riff chat, andriff daemon statusshows a line for it as long as the lead runs. The line says that the lead of the repository lost its network at the restart of the daemon, and gives the steps:podman stop NAME, thenriff lead start, then/resume. -
01M5SNV7H3739PXW73H14XM4B8 Before the daemon of a host starts again on a new riff, it posts one note to each lead on that host. The note says that the daemon starts again on the new riff, that riff starts the container of the lead again on the new riff when the lead is idle, and that the lead keeps its state in its notes. The daemon posts it for
riff daemon restart, for the look at the end of each session, and forriff update. -
01M5TKETGZP9NTHBXFBP1RM1HP A commit that riff makes for itself has no signature: the commit of the sweep (01M5NVKNGZPK0GG7B7ZWBS0DFE) and the WIP commit of a session that ended with no push (01M5P26VSA7AA99K1RYG83R8J9) run with
-c commit.gpgsign=false. A host whose git signs each commit keeps no folder for that reason, and riff saves the work. -
01M5TKPG5WZ1T58FNAMZNP6CJ5 The facts of a daemon show the session image for each wave, not for the host. Each wave has the short digest (12 hex digits) of the merged pin of its repository, as the main clone has it, and whether podman has that image on the host:
hereornot here. Each live session has the short digest of the image that its container runs. The daemon asks podman for it withpodman container inspect --format {{.ImageDigest}} NAME.riff who --longand thewhotool of a session show the digests.riff whoandriff topshow onlyhereornot herefor each wave. This supersedes 01M57D3F50347RD8ED79KEA9V7 in part.
riff relay
- 01M4ZHSZZDH0C01A5EVD0KFY14 The daemon binds its own binary at
/opt/riff/riffin each container, with no write. The command of a session is/opt/riff/riff relay -- COMMAND. The daemon starts no session whose spec binds a path at/opt/riff/riff. - 01M510NW8YEVMQKD6RWJPW38N3 Before each start of a session, the
daemon reads the ELF of the binary that it binds at
/opt/riff/riff. When the ELF has an interpreter, the daemon starts no container, and its error namesriff update. It reads the file at each start, so afterriff updatethe next start works with no restart. - 01M4ZHSZMH4Y8CB41FYGE3C3R2 The relay and the daemon count the
events and the inputs. Each event has a sequence number: the event N
is line N of
events.jsonl. At each connection, the relay says how many inputs it gaveclaude, and the daemon says how many events it has. The relay sends each event that the daemon does not have. The daemon sends each input that the relay did not give. So no event and no input goes two times, and none is lost. The daemon acks no event that it cannot keep, and acks the exit ofclaudeonly when it has each event before it. In place of that ack, it closes the connection, and the relay sends the line again at the next connection. - 01M4ZHSZPRYK42C20ETXM3HJZ2 On the socket of a session, the daemon takes a hello of its own version of the protocol, of the version before it, and of the version after it. A change of the protocol adds a line or a field. Each side skips a line that it does not know.
- 01M4ZHSZRY0P9HHNVSFET8HEEN The relay takes each secret of the
session out of each event and out of each line of the stderr of
claude, before it sends or shows the line: the values ofGH_TOKEN,CLAUDE_CODE_OAUTH_TOKENandANTHROPIC_API_KEY, and the tokens of the sign-in file ofclaude. The relay writes no file. - 01M4ZHSZV46RF3XDXDTQXWGXRG The relay gives
claudethe riff tools: an MCP serverriffthat runsriff relay mcp. It passes each line betweenclaudeand the socket of the session. The daemon serves the tools as that session. - 01M4ZV3B6T2KB09WJ59F07HXET The daemon serves the riff tools of
each session that it owns, of
riff runor of a wave, on the riff-server of the session, as the worker session that holds the claim of the item. The daemon uses the sign-in of the person on its machine. A session gets only the tools that act on riff-server, and on no folder of the host and no claim:whoami,who,threads,read,post,tell,statusandblocked. The daemon refuses each other riff tool with a text that says why. - 01M4ZHSZXAY7F7E17B3CMHG562 When the daemon goes away,
riff relay mcpanswers each call in flight with an error, and keeps each new line until the daemon comes back. At the new connection, it gives the daemon the initialize request ofclaude, so the daemon skips the handshake. - 01M4ZKX0R850RPSS63B001NNJZ After the end of
claude, the relay waits up to 10 minutes for the daemon to take the last events. Then it ends with the exit code ofclaude, and the container goes. - 01M4ZKX0F25S284R0N7KZVTHNA When the daemon goes away during a
session,
riff runsays so, and waits up to 10 minutes for a new daemon. Then it follows the session again after the last event that it showed. So it shows each event one time. When no daemon comes back,riff runsays so, finds the pull request, and releases the item.
The waves of the daemon
- 01M4ZV3B6G5VB89SRSWCSBZ630 Each start of a session waits for room
in the limit of the host (01M4Z60B7Y4XD6TJCYEYJT2R4S), in the order of
the asks: a start of
riff runand a start of a wave wait in the same line. A start that has room counts as a live session until the daemon owns its session. The free memory isMemAvailableof/proc/meminfo. When the host does not say, only the sessions count. - 01M4ZV3B6VS0RAR0P7MRHKE8CQ While a start of
riff runwaits for room, the daemon says why toriff run. Whenriff runstops it, or leaves, the daemon starts nothing. - 01M4ZV3B6HAFTSC2K63FMC5FYA
riff daemon limitshows the limit of the host.--sessions Nand--floor GBset it inlimit.jsonin the folder of the daemon. The daemon reads the file at each look for room, so a change counts at once.riff daemon statusshows the limit, each start that waits for room, and each wave with what it did last. - 01M56Q4ASHTDWVA2MS2CBQ2PAM
riff daemon limit --cpus Nsets the CPUs of each new container of the host.--memory GBsets its memory cap. Both go inlimit.json. Alimit.jsonwith no value for one of them gives the default.riff daemon limitandriff daemon statusshow both. riff refuses 0 for each. - 01M5781930TF6XBX7ME7RAMX2X
riff daemon cacheshows the size of the volumeriff-sccachenow.riff daemon statusshows the same line. - 01M5781A4PRTTYRHH5GJ1GHN7G
riff daemon cache --cleardeletes each file and folder in the volumeriff-sccache, and keeps the volume. It works while sessions run. A session withSCCACHE_IGNORE_SERVER_IO_ERROR=1compiles each file that the clear deleted. - 01M588JW2KS6EVX1SE49686FQ7
riff daemon cacheandriff daemon statusshow the size on the disk of each file in the volumeriff-sccache, with 1GB = 1024³ bytes: the unit thatsccachereads for eachG.podman system df -vcounts 1GB as 1000³ bytes, so its number is about 7% larger, for example 8.3GB for 7.7GB. When the person cannot read a folder of the volume (rootless podman), riff reads it withpodman unshare. When riff cannot read it, it says so, and never shows 0.riff daemon cache --clearalso deletes withpodman unsharewhen the person may not, and fails with the reason when it cannot delete. - 01M57840K7SNPS243CYMYNYVFX
riff daemon mcpshows the MCP servers of each new container session of the host. The default isriffonly.riff daemon mcp add NAMEcopies the server NAME from the MCP config of the person intomcp.jsonin the folder of the daemon, readable only by the person.riff daemon mcp remove NAMEtakes it out.riffalways stays.riff daemon statusshows the list. - 01M57840NN08NA4PH0YR81PDGT
riff daemon mcp add NAMErefuses a name that the MCP config of the person does not have. It refuses a stdio server whose program is not in the session image of the repository of the current folder, and names the program. - 01M57840R2HNXGP9B2ENSKTWD3 The daemon gives each new container
session the servers of
mcp.jsonin a variable whose value is in no spec and in no command line. The relay adds them to the MCP config ofclaude, besideriff, and givesclaudeno such variable. Each value of theenvand theheadersof a server is a secret that no event shows. - 01M4ZV3B6J89KJQ0F14YWTZMJQ
riff wave runin a clone asks the daemon to run the current wave of the repository of the clone. The daemon keeps each wave inwaves.jsonin its folder, with the main clone, the riff-server and the host, and starts it again after its own restart.riff wave stopends the wave: the daemon starts no new session for it, and the sessions that run go on. - 01M4ZV3B6KP33YV750DJ89SMFF The loop of a wave first waits for
room. Then it looks at the pause, the claims and the holds on
riff-server, and at the free work of
gh: the pull requests that wait for a verify, then the free items of the current wave, as the rollout of the lead takes them. It starts one session for the first work that it may start. With no such work, it waits for the end of a session, or 60 seconds. - 01M4ZV3B6M7PZRJ39XTXBZP3ZH A wave starts nothing while the riff or the repository is paused. It never starts a held item, an item that a session claims, or an item that a session of the wave runs.
- 01M4ZV3B6NKPEZ5FNE27X95GF0 A wave starts the verify of an item only when no session holds the item, and the full check of the head commit of its pull request is done (01M5QYDHN27RC5SRHAB56PFKK0). The verify is a new session, so an author never verifies its own item.
- 01M4ZV3B6PSRH40EYQEVYH2MBA A wave starts one item at most 3 times in a run of the wave. Then the lead of the person gets one message, and the wave leaves the item.
- 01M4ZV3B6Q0Y6AX0KSKC55S31R For each item, the daemon does the
host steps of
riff runitself: the checks, a new worker session, the claim, the keep-alive, the clone and the start. After the end of a build, it sends a verify request for the open pull request of the branch to the lead (but see 01M51KH0H7J35PEKMHP5KQB84T), then releases the item and ends the session. The record of the session holds the job of the wave, with no secret. So a new daemon goes on with the keep-alive, and does the steps after the end. When the session ended while no daemon ran, the new daemon does them at its start. The daemon runsghwithGH_REPO: it runs in no clone. - 01M4ZV3B6R4WSEJXYT7QPRWT6X A verify session has a clone at the
head commit of the pull request, detached. Its start text says to
check the work against the
Done when:line and to change nothing. Its last reply is the result: the first line isPASSorFAIL. The daemon reads it fromevents.jsonl, and reports it asriff verifydoes: the comment on the pull request, the statusriff/verifyof the commit, and the result to the holder of the item and to the lead. With no verdict, or when the report fails, the lead gets a message, and the pull request waits for a new verify. - 01M4ZV3B6S1A64TYK8MTDNRCQ3 At each look, a wave removes the folder
of each session of its repository that does not run, when the pull
request of its branch merged. For a pull request with
Refs #N, it first adds the commentMerged in #PR (COMMIT)to the issue, when the issue does not have it. The folder of a verify that did not pass goes at its end, and the folder of a verify of a closed pull request goes at a look. - 01M5NCZ6CVCSTN3ZZDWNVGFG5S The wave starts no session for the
release issue of a release: the issue with the label
release, made byriff release cut. It starts no build, no verify and no rebase for it. - 01M5ND16V2SVN8A9K7W90VC6X9 A tracker issue is an issue with the
label
tracker, or an issue whose title starts withTracker:. It has no code of its own: its parts are separate items. A wave starts no build session and no verify session for a tracker issue, and it is no free work of the rollout of the lead.riff wave listandriff topshow it in the grouptracker, never infree.riff wave makemakes the labeltrackerwhen it is missing. - 01M5P20DPK4GAQRDEKC1E6V718 A verify request for a head commit, in the thread after the last verify result for it, answers a failed verify of that head: the wave starts a verify of the head, not a build, when its Gate and Hygiene pass.
- 01M5P26VPJA73WCPVZY6CPG9HD A session of a wave has a grace of 60 seconds after the daemon closed the input of claude. When no event comes in that time, the daemon stops the container, and the item is free. It does not wait for the quiet limit, unless a background task runs (01M5R0QFM265CCKTADK4T5XREQ).
- 01M5P26VSA7AA99K1RYG83R8J9 When a build of a wave ends and its branch has no pushed commit, the daemon commits each change of the clone as a WIP commit on that branch and pushes it. The lead gets one message with the branch and the commit.
- 01M5P26VW5GBJDQZGEYW9A77Y0 The daemon removes the folder of one ended session on purpose with riff daemon sweep –remove NAME. It first checks that the work of the folder is on a merged branch, or that the person gave –force. The lead can run it with –host.
- 01M5QVARWYFHTQV5THNRB09SZA The clone of a verify session has the head commit of a pull request from a fork too. After the clone, the daemon fetches refs/pull/N/head from origin, with N the number of the pull request, then checks out the head commit, detached. It checks out the commit, not the ref, so a push to the pull request after the look does not change what the verify reads.
- 01M5QVH3MAEPPJN9PPVDX6N2EY The riff daemon runs the look of the
repository of each wave that it runs until
riff wave stop: it reads the open pull requests and the Gate ofmainwith “gh”, and posts as the person. The messages of 01M49Q30XMVRFX42YTM1PHX0RZ, 01M49Q316RXNATJP587DWGDNCD and 01M51QEYKTD7ZEB0YA0C3MQTPN come from this look, and the steps of 01M51QEYPGD6W345MZ4F85C38F and 01M51QEYVQN74Q1VDTVHB0445Q too. A message goes to the lead of the person, or to the session that holds the item of a pull request with a conflict. The daemon keeps what it told in memory only. The wave goes on when the look fails: the daemon prints the fault one time until it changes. - 01M5QW2KVAS05Q72630RNW47G0 A pull request whose verify passed can fail a check of its head commit: a check run that ended with failure, timed out or startup failure, or a status in failure or error, except riff/verify. The wave of the daemon reads the rollup of each open pull request at each look. For the item of such a pull request, it starts a build session with the names of the failed runs of GitHub Actions in the start text. The text tells the session to read the failed run, to fix the work and push to the same pull request, and, for a known flake, to run the job again one time. Each head commit starts one such session in a run of the wave. A pull request with a conflict gets the rebase first. A cancelled or skipped check did not fail.
- 01M5QW2KYFM4C287D9NMSPH8TA When a session of a wave for failed checks ends with the same head commit, the daemon sends no verify request. It reads the checks of the pull request. The lead gets a message when a run of GitHub Actions still fails, and a note when none fails because the session ran the job again.
- 01M5QW2M1M5QZJXSP0BY6XG118 A pull request whose verify passed and that has auto-merge off never merges. The wave starts no session for it. It tells the lead one time for each head commit, with the reason and the command gh pr merge –auto –squash N.
- 01M5QW2M4WX53N29SHRV0S32WM No session builds a release: a pull request on a branch release- or of an issue with the label release. A failed check of a release pull request, at any state of its verify, gets one message to the lead for each head commit, with the names of the checks.
- 01M5QW2M8FPBMMF4S0D8PC39AB riff top and riff wave list show an item with no claim whose pull request cannot merge by itself in the group stuck, with the reason: conflict, checks, or auto-merge off. The group comes after verify.
- 01M5QYDHN27RC5SRHAB56PFKK0 A wave starts the verify of an item
when the full check of the head commit of its pull request is done
(01M5QYDE41FVFX26EC8K5QGYH4). It gets its verify when each required
check of the default branch but
riff/verifypassed or ended. A wave that cannot read the required checks fails its look, and says why. Supersedes 01M4ZV3B6NKPEZ5FNE27X95GF0 in part. - 01M5R0QFM265CCKTADK4T5XREQ While the last background_tasks_changed event of a session of a wave lists one task or more, the daemon uses the quiet limit for it, not the grace (01M5P26VPJA73WCPVZY6CPG9HD). When the list is empty, the grace of 60 seconds counts from the last event. It supersedes 01M5P26VPJA73WCPVZY6CPG9HD in part.
- 01M5R13DGMTNWPY88A3EG49WWS The look of the daemon watches the head
commit of
mainwith the full check of the repository (01M5QYDE41FVFX26EC8K5QGYH4), not with the check runGate. It reads the required checks of the default branch (01M6GYN97HH70E4C1FGM6HA2B0), and the check runs of each of them onmainat each look.mainis broken when a check ended withfailureortimed_out, and passed when each check passed. A check that runs, a cancelled check and a head with no run of any check give nothing. The messages, the fix item and the note say “the full check”. Supersedes 01M51QEYKTD7ZEB0YA0C3MQTPN in part. - 01M5R0HEP1MXWYY643F1N1DWEM The free work of a wave has no build of
an issue whose body has no line that starts with
Done when:. The wave tells the lead one time for each such issue, with the fix: add the line. The wave takes the issue at its next look after the line comes. A verify and a rebase do not change. - 01M5QYBWPAJ219BE8QKVKG1TWW A repository can have any default
branch.
riff wave runreads the default branch of the repository on the forge one time, and the wave keeps it. The start text of a build, of a rebase and of a verify names that branch, notmain:git diff origin/BRANCH...HEAD,git rebase origin/BRANCH, and “Never push to BRANCH”. The look reads the full check of the head of that branch (01M5R13DGMTNWPY88A3EG49WWS).riff runnames the branch oforigin/HEADof its clone. - 01M5R0J131464KZX180BW2AK98 A default branch that does not read stops no look at the blocks. The look of the lead reads the default branch of the repository at each look, after the look at the blocks, and a failed read gives no look at the full check, like a forge that does not answer. Refs 01M5QYBWPAJ219BE8QKVKG1TWW.
- 01M5R56GN3DC09GAPAF69GENEZ The wave keeps the default branch of
the repository in no file. The loop of the wave and its look read the
branch from the forge (01M6GYN97HH70E4C1FGM6HA2B0), and a build, a
rebase and a verify read it at their start. So
waves.json,served.jsonand the record of a session have no branch, and the files of an older build stay valid after an update: a machine takes each release with no step of a person. A branch that does not read gives the error of the look: the wave starts no work at that look, and reads again at the next one.riff wave runreads no branch. Supersedes 01M5QYBWPAJ219BE8QKVKG1TWW in part. - 01M5SN0PY23JDWND8DGGAF9XMB A failed check of a pull request whose verify passed starts a build session only when the check is a run of GitHub Actions: a check run with a workflow. For each other failed check, a check run of another app or a status, the wave starts no session. The lead gets one message for each such check and head commit, with the name of the check, its app and its link. The app is the host of the link. A pull request with both kinds gets the session for its runs of Actions, and the message for each other check. When a session of a wave for failed checks ends with the same head commit, only the runs of Actions count as failed. Supersedes 01M5QW2KVAS05Q72630RNW47G0 and 01M5QW2KYFM4C287D9NMSPH8TA in part.
- 01M5SMSB3H1E9NXMBDN8ZDJ6CD When podman cannot pull the session
image of a wave, the daemon starts no item of that wave for one
minute, and no start counts. Then it tries the pull again.
riff daemon statusshows the image and the reason of the failed pull, in the line of the wave, until a pull works. The daemon tells the lead once for each image that fails to pull, with the fix of 01M5SMRRAG1WPBJMQKKTPSRHRP: make the package public, or runpodman login REGISTRYon each host of the wave. - 01M5SNTPHHXP54YBTV09JNRY3F At the first job of a wave of a
repository after the daemon starts, the daemon runs
claude --versionandgh --versionin the merged session image, with podman and no network. When a version is older than the version indeploy/base/Dockerfileof this riff, the daemon tells the lead one message: each old tool with its version and the version that riff needs, and the digest ofriff-baseto buildFROM. The daemon tells the lead of a repository one time for each image and each base, until it starts again. A tool that gives no version gives no message. - 01M5SVE4HJ0EEVECE848DVT1RX The wave of the daemon starts no verify
of an open pull request whose state
mergeableisUNKNOWN: GitHub did not find yet if the head commit has a conflict withmain, as after a push or after a move ofmain. The verify waits for the next look. A look that readsMERGEABLEputs the verify in the free work, and one that readsCONFLICTINGputs the rebase (01M57FJ3WWMQSDBFRRHXN3SMFQ). - 01M5TKPNDET56VR8WZ74FSR71F The wave of the daemon starts no session and sends no message for an open pull request that is in the merge queue: no rebase, no verify, no notice of a conflict or of auto-merge off (01M5TKPHDNHY75BHPD4XVBJRN9). The wave reads the queue with one GraphQL call for all open pull requests. A call that fails reads as no queue. Supersedes 01M49Q30XMVRFX42YTM1PHX0RZ and 01M57FJ3WWMQSDBFRRHXN3SMFQ in part.
- 01M5TKPNGG27Z1SQKTA296D9H7 A pull request that the merge queue
removed, with no push to it since, and whose verify passed: with a
conflict with the default branch, the wave starts one rebase session
for the head commit, also with auto-merge off, and the session turns
auto-merge on again. Else the lead gets one message for the head
commit, with the reason of GitHub.
riff topandriff wave listshowstuckwith the wordsremoved from queue. - 01M6FZ141Y0J8G2AYX0G5HY1CX A pull request whose verify passed, with auto-merge on, can wait for a review of a person. The wave of the daemon reads reviewDecision and mergeStateStatus of each open pull request at each look. It starts no session. It tells the lead one time for each head commit, with the reason and the command gh pr review N –approve, when reviewDecision is REVIEW_REQUIRED, or when the merge state is BLOCKED with no decision, no failed check, no check that goes and no run that waits for approval (the likely cause is an author with no GitHub account). riff top and riff wave list show stuck with the word review (#1433).
- 01M6FZ144M0BTEDWNB9V4MGWDD A run of GitHub Actions of a pull request from a fork waits in action_required until a maintainer approves it. GitHub counts its required check as not reported. For a pull request whose verify passed, with auto-merge on, that GitHub blocks with no check open, the wave of the daemon reads the runs of the head commit with gh api. For the newest run of each workflow that waits, it tells the lead one time, with the run and the command gh api -X POST repos/OWNER/REPO/actions/runs/RUN/approve. A call that fails reads as no run. riff top and riff wave list show stuck with the words approve run (#1433).
- 01M6GYN0CSFRBSHGE5CEBHDVZH The riff daemon counts each call of gh by its caller (the wave, the look, the watches, the sweep and the usage meter), by its kind, and by the GraphQL points it cost. The points come from a table of the queries that gh sends, and from the cost that a reply with rateLimit gives. The daemon keeps the last hour. riff daemon status shows the calls and the points of that hour for each caller and kind, and the GraphQL points that GitHub says are left with the time of their reset.
- 01M6GYN92N3Z99GPXTYHDDWF4R The looks, the watches and the sweeps of the daemon use the GraphQL points of the token of a person within a target: two waves with no work use at most 300 points an hour, and one wave with 8 sessions at most 1,500. One token has 5,000 points an hour for each session, look and lead of its person (#1549).
- 01M6GYN9526HAYVKJC4HS858TH A wave that has no work, no session and no change since its last look looks each 5 minutes, not each minute. A session that ends, a job that ends and a start of the wave look at once. A paused wave looks each minute. The look of the daemon at the pull requests and at main waits 5 minutes after a look that told nothing while no session of the wave runs. The watch of a wave waits 5 minutes after a look while it is the only watch, and a new watch ends the wait. Supersedes 01M5QVH3MAEPPJN9PPVDX6N2EY and 01M5RGD0RTXYA7FF5K1TH72MGX in part.
- 01M6GYN97HH70E4C1FGM6HA2B0 The loop of a wave, the look of the daemon and the sweep read the open issues and the open pull requests with the same call of gh. The reply of one read serves the others when it is not older than the wait of the loop that asks. The loop of a wave takes no old reply: it sees the forge as it is. The default branch and its rules are read one time in 10 minutes, and the message of a commit one time. The sweep makes no call for a folder whose branch has an open pull request in such a reply, and asks the forge about a folder that stays one time in 10 minutes. The sweep of a wave deletes no old riff-kept branch. Supersedes 01M5R13DGMTNWPY88A3EG49WWS and 01M5R56GN3DC09GAPAF69GENEZ in part.
The lead, the view and the takeover
-
01M501P9MXNAP5QXNVBVF5JJFN
riff lead startin a clone starts the lead of the repository of the clone through the daemon, when it does not run, in the containerriff-lead-OWNER-REPO. Then it attaches the terminal of the person to it. When the lead runs already, it only attaches. Ctrl-P Ctrl-Q detaches, and the lead runs on. -
01M51PD7M04HZR2PVVJA4WCBK5 Before each attach to the lead, from
riff lead startand fromriff view, riff sets the terminal of the container to a size that is not the size of the terminal of the person. The attach then sets the size of the person, soclaudedraws its screen at once. When riff cannot set the size, it says why, and attaches. -
01M51PD7Q24N252V66NFEHY49N After an attach to the lead, riff tells a detach from the end of the lead: the lead ended when its container does not run. After a detach,
riff lead startandriff viewsay that the person left the terminal. After the end, they say that the lead ended and how to start it again.riff lead startalso says that theclaude --resumeline ofclaudeworks only in the container. -
01M501P9Q51AYN9GBPWKVHA229 The folder of the lead is
leads/OWNER-REPOin the data folder of riff. At the first start, riff makesclone/withgit clone --reference MAIN, on the default branch oforigin, and the Claude folderclaude/. Both stay between the starts of the lead. Each start is a new session of riff-server and ofclaude. -
01M50RRQ9DT0JPSB358EAZRP8Z Each later start of the lead fetches
origininto the clone of the lead, and fast-forwards its branch to its upstream. A clone with a local change stays as it is, and riff shows a note. A local change is a change that is not committed, a commit that is not on the upstream, or a branch with no upstream. -
01M51JYDHAQDJ3GH2J8DT3AZQS When git cannot fast-forward the clone of the lead, for example because of an untracked file with the path of a new file of
origin, the clone stays as it is. riff shows the note with the error of git, and starts the lead. -
01M501P9SDGVWG3MTVCW5BDRQQ The Claude folder of the lead gets the first-run answers of
claude:hasCompletedOnboarding, thethemeof the.claude.jsonof the person (elseauto), and the trust of the clone. A key that is there stays. riff writes thesettings.jsonof a session (01M4ZD48DHAKJ1EHX6QHYAQ7ZM) only when it is not there. -
01M51JACH6TQP318ZFG6NQ9GRM The Claude folder of each session of
riff runand of the daemon gets the same first-run answers as the Claude folder of the lead (01M501P9SDGVWG3MTVCW5BDRQQ). So a takeover shows the history of the session with no first-run question. One function of riff writes the Claude folder of each session. -
01M501P9VN47QAKZ6B8P39ZSW4 The spec of the lead is the spec of a worker with the name of the lead, and the command
claude --session-id ID --dangerously-skip-permissions START, where START is the first prompt of the lead. The daemon runs it as/opt/riff/riff relay --terminal -- COMMAND: the relay adds the riff tools, and runs the command in its own place, so the command has the terminal. -
01M501P9XWXWDBSXP4QRTHTZ4P The host starts a session of riff-server for the lead, not a worker. While the container of the lead runs, the daemon keeps that session alive, also after its own restart. At the end of the lead, the daemon ends that session. The lead gets the riff tools that talk, and
lead,hold,free,pauseandresume. The daemon refuses each other tool with a text that says why. -
01M501PA04NP79RJQEDP4D7YTM
riff relay watchin a session asks the daemon, on the socket of the session, to wait for the next wake of the session on riff-server. It prints the line of the wake, asriff watch --oncedoes, and ends. While the daemon is away, it tries again. The instructions of the riff tools of the lead tell it to keepriff relay watchrunning as a background task. -
01M501PA2C087SEN4M4QTK5J1E The daemon starts a session with a terminal with
podman run --rm -d -i -t: conmon holds the terminal.podman waitgives its end. Such a session does not wait for room, and does not count in the limit of the host. The daemon saysdonetoriffwhen its container runs. -
01M501PA4MG5NYNK7FBBRRAT20
riff viewshows the daemon of this machine with ratatui, and draws it again each second: each session with its container, its item, its state, its age and its last step; the daemon and the limit of the host; each start that waits for room; each wave. Up and Down select a session. Enter attaches to the lead: the selected session when it is a lead, else the first lead.ttakes over the selected session.q, Esc and Ctrl-C end the view; the sessions go on. Its tests draw it on theTestBackendof ratatui. -
01M51PD7T7451SR11W7JA7QW3X The SESSION column of
riff viewis as wide as the longest name of a session, so each name shows in full. -
01M50RRQBZ8ASPT25DYPX78HP9 After a takeover from
riff view, the note under the rows says that the session goes on, or that it ended before the takeover. The view reads the keys of the person in a blocking task of tokio, not in its async loop. -
01M501PA6W0NCG9JEK017F9304
riff takeover NAMEtakes over the worker in the container NAME. It stops the turn of the session at once (01M51KJ1R9K7A7RW8T8XH8VW43). Then it runspodman exec -it NAME /opt/riff/riff relay --terminal -- claude --resume ID --permission-mode autowith the terminal of the person. When that ends, it tells the daemon to go on. It exits with 0 when the session goes on, and with 1 when the session ended before the takeover. -
01M51KJ1R9K7A7RW8T8XH8VW43
riff takeover NAMEandtinriff viewstop the turn of the session at once, also in the middle of a tool call.riff takeover --at-end NAMEwaits for the end of the turn. A takeover also waits for the end when the input ofclaudeclosed after the result of its turn, and when the relay of the session cannot stop a turn.riff takeoverand the view say which one they do. The result of a turn that a takeover stops is no fail of the session. The new input after the takeover says that the stop can come in the middle of a step. -
01M501PA94CDZDH214GGFMTH5V On the socket of a session,
holdtells the relay to close the stdin ofclaude, and to keep the container at the end ofclaude. The relay then saysheld, and waits.go ontells it to startclaudeagain, with--resume IDin place of--session-id ID. The sequence numbers of the events go on. The hello of the relay saysheldwhile it waits, so a new daemon knows. -
01M501PABB010P4S8YF7R29XXS The daemon keeps a takeover in the record of the session, from the ask until it gives the session back. It gives the session back at the
go onofriff takeover, or at the end of its connection: it saysgo onto the relay, and givesclaudeone new input that says that a person used the session. The quiet limit does not count during a takeover. After a restart of the daemon, a takeover stays, and a newriff takeover NAMEgets the terminal at once. The daemon refuses a takeover of a session with a terminal, of a session with no relay, and of a relay of a protocol before 4. -
01M501PADKZNRHBZD9XPMJGW06
riff daemon statusandriff viewshow the state of each session and its last step. The state isworks,endsafter the result of its turn,holds for a takeoverwhile a takeover waits,taken overwhile a person has it, orleadfor the lead. The last step is the last line thatriff runshows of its events. -
01M5GG2X5F1HXCQJ2JJWV6305K The lead starts claude with –dangerously-skip-permissions, in place of –permission-mode auto: claude –session-id ID –dangerously-skip-permissions START. A worker and a verify keep –permission-mode auto.
-
01M5GVPNAKAPA596K9361HSAAV In a container, a riff command that needs riff-server, with no
--serverand noRIFF_SERVER, asks the riff daemon through the socket of its session (RIFF_SOCKET). The daemon makes the call on the riff-server of the session with the sign-in of the person on the host. Soriff wave listandriff release checkwork in the container of the lead. Through the socket, each session can read the build of riff-server, the sessions and the holds of a thread. Only the lead can resume the whole riff. -
01M5GXYC201EFKMGA3ZNJM2J7Z In a container, a –host request (riff release test –host, riff daemon status –host, riff daemon sweep –host, riff wave run –host, riff build –host) goes through the socket of the session. The daemon posts it to the daemon of the host through riff-server, as the session. Only the lead can send it. riff daemon status with no –host in a container asks the daemon on the socket of the session. It never looks for the socket or the unit of the host.
-
01M5GY61WSDXG5G6E2SQQ82AWC In a container, riff wave move and riff wave end –move-open-to-backlog read the claims of riff-server through the riff daemon, as riff wave list does. No riff wave command calls riff-server straight when RIFF_SOCKET is set and no server is named.
-
01M5NG4Z0XS1B4QYXFJBW9CWGX
riff lead startmakes the new lead session the lead of its user in the repository, and prints which session was the lead before; that session is not the lead any more. In a lead container,whoamisays in its first line when the session is not the lead, and names the lead (#1301). -
01M5NJ87NXF6077Y9W32840BR8 riff lead start refuses to start the lead on a host with no claude token of riff claude-token. It names the step: claude setup-token, then riff claude-token, on that host.
-
01M5NJ87YEHHGJBANM7CRYV56Z When the lead runs with the sign-in file of claude of the host and the host replaced that file, riff tells the person that the sign-in of the lead is stale, with the step to fix it and to start the lead again: at each attach of riff lead start and riff view, and once in riff chat from the daemon.
-
01M5P21M7MSP503125GD2PJGRE In the container of the lead, riff release cut, publish and approve reach riff-server through the socket of the session, as riff release check does: the roster, the chat line, the read of the chat and the pause of the riff go through the riff daemon.
-
01M5QV9STQF1P9GN65P35E0FJH The lead has no Remote Control. A person answers the lead in riff chat (#1363). The book names no flag or command of Remote Control, and the start of the lead is riff lead start.
-
01M5QVX8D3JE51FAYRWXMAYFAY In a container, riff members, riff audit and riff session why read riff-server through the riff daemon on the socket of the session (RIFF_SOCKET), as riff wave list does. They need no RIFF_SERVER and no sign-in in the container. Only the lead can read the log for riff audit, and riff-server still gives it only to the owner and the admins. riff invite, riff remove, riff admin and riff owner stay for a person in a terminal on the host (#1338).
-
01M5QVX8FFG2FPWZEW61A985CF In the container of the lead, riff run ITEM asks the riff daemon on the socket of the session to run the item as a job of the wave of the repository: the claim, the clone, the session in the room of the limit, and the steps after its end, as the wave does them. riff run answers at once and does not follow the session. It takes no –image. Only the lead can ask it. When the job does not start, the daemon tells the lead why (#1338).
-
01M5QW31TFA1W2GWPGRS2Y65GP In a container, riff who and riff whoami read riff-server through the riff daemon on the socket of the session, as riff members does. Each session can read them. The URI that riff whoami shows is the URI that riff-server holds for the session (#1338).
-
01M5QVCVXWBBG1BMGRVBQS25W9 In a session with a terminal, the lead and a takeover, the relay puts the folder of /opt/riff/riff first on the PATH of claude. So a person who types ! riff in the terminal runs the riff of the container. The path /opt/riff/riff stays, and the permission rules that name it still match.
-
01M5RG6C25DB49ZKM03Y93QB1F The hello of the relay has no field
interrupts, andholdalways has the fieldinterrupt(01M5RG6BTXAJY8DV6MT315VZWF). A relay always interrupts the turn ofclaudeat aholdwithinterrupt. This supersedes 01M51KJ1V0STB4QCN4N5SBYGSP. -
01M59BTTH6ZATH7R2PFC6NGHJK riff has no
riff worktrees, noriff lead compact, noriff lead blocked, no tidy of the worktrees of a host, and no compact of the lead. The tidy of the folders of container sessions stays (the sweep of the riff daemon).lead.wakeandlead.notifystay as keys of the settings file. -
01M5TC24PB3YC2CC8BA275WSEN After an update of riff, the daemon starts each lead again on the new riff. A lead runs an old riff when the file that its container binds at
/opt/riff/riffis not the file on disk:riff updatereplaced it. The daemon looks when it takes up the sessions that run, and each minute. It starts only a lead that is idle: the transcript of its conversation got no write for 5 minutes. A lead in a turn waits for the end of the turn. Supersedes in part 01M4Z60B800BH5BHX0H9JVDG6A and 01M5SNV7H3739PXW73H14XM4B8. -
01M5TC2919TB9XPF42YFNJBP79 The daemon stops the container of an old lead and starts it again with the same session, the same clone and the same Claude folder. The new container binds the riff on disk. Its command is
claude --resume IDof the conversation of the lead, and its first prompt tells the lead to start its Monitor again. The session of the lead on riff-server stays: the daemon does not end it. The person gets one line in riff chat for each lead that started again. -
01M5TC29A4Q94NJP7HFC75XCF3 When the daemon cannot start a lead again, it tells the person one time in riff chat. A fault before the stop leaves the lead on the old riff, and the daemon tries again at the next look. A start that fails after the stop ends the lead: the daemon ends its session, and the line says
riff lead start.
The first run
- 01M4ZJN6R68YBD3XMM725ZRJ7W
riff initdoes the first run on a machine (01M4Z60B7K13MSYM8ZY20WN6NV): first the checks, then the daemon, then the session image. It needs no riff-server and no sign-in. On a host that is not Linux, it changes nothing, and says that riff needs Linux. - 01M4ZJN6TEWCYCB39XTH6BBD9H The checks of
riff init, in this order:podman --versionruns, and riff does not run as root, so podman runs rootless;/etc/subuidand/etc/subgideach give the person 65536 IDs or more, by the login name or by the user ID, added over the lines; cgroup v2 is on, and thecgroup.controllersofuser@UID.servicehascpuandmemory(01M4Z60B7MW963MBQAPN2CJEQZ);riff.tomlof the repository of the current folder pins the session image (01M4Z7SP3KSKJYKNRP1MQ70C4Q). riff prints one line for each check:pass, orfailwith what it found and the fix. Each failed check has a fix of its own. - 01M510NW68CTZBHHW7ZYFV7Q3X
riff initalso checks the riff binary of its own process: its ELF has no interpreter. A riff ofcargo installorjust installlinks the libc of the machine, so this check fails, and its fix isriff update. - 01M511JYPZ8H73DCJ7867YSFGH After the check of podman,
riff initchecks the init of podman:catatonitin a helper folder of podman (/usr/local/libexec/podman,/usr/local/lib/podman,/usr/libexec/podmanor/usr/lib/podman). Each session needs it (01M51117K3WXK35CE67KPRFMA8). The fix installscatatonit. - 01M501K21EK8F8N9NPR4565FVP The fix of
/etc/subuidor/etc/subgidadds a free range of 65536 IDs: it starts at the first ID after the end of each range in the file, and at 100000 or more. - 01M4ZJN6WR0JJ891R9JMQNPKGD When a check fails,
riff initinstalls nothing and pulls nothing, and exits with 1.riff init --checkruns the checks and changes nothing. - 01M4ZJN6Z1DGD43CKZ19QPN2NK When a riff daemon answers,
riff initkeeps it, and does not start it again. When the build of that daemon is not the build of this riff,riff initsays so, and namesriff daemon install. When no daemon answers,riff initinstalls the daemon asriff daemon installdoes (01M4ZDTVK74596M0ZA46N51NKV). - 01M4ZJN718EF1PC9YY9GFD0EJ2 Then
riff initasks the daemon for the session image. The daemon pulls it when podman does not have it. The last line says that the machine is ready forriff run. - 01M5QVF5535YRN1EF3DZAB4W8G
riff initalso checks the repository of the current folder on GitHub, with theghof the machine, when the remoteoriginof the folder names one. It prints one line for each check: the repository allows auto-merge and squash merge; a ruleset of its default branch requires the checkriff/verify; the repository has the open Backlog. Each failed check names its step, and each step has its own fix. A folder with no remoteoriginhas no such check. Whenghcannot read the repository, one check fails, and its fix isgh auth login.riff init --checkonly reads. - 01M5QVF5C3CYJPMQ9CXNZJ8E93 The book page “Start a Riff” has the
how-to “Add another repository to the riff”: the checks of
riff init,riff.tomlwith the session image on the default branch, the settings of the repository, the ruleset of the default branch, the Backlog and the first wave, and a session image with another toolchain, built from the session image of riff. Each step has a copyableshblock. - 01M5QWA0E9A4ZJ8X49EM2MJM5Q Each failed check of the repository on
GitHub carries its step in code (
riff::repo_setup::Step): allow auto-merge and squash merge, make the rulesetriffthat requires a pull request and the checkriff/verify, and make the Backlog. A step is safe to do again. The how-to makes the same ruleset: a test compares the two.riff initdoes the steps after a question (01M5R1672NF56P15VD0JEX51K4). - 01M5QZ01Y14Z76RC220RT7YTX7 The how-to “Add another repository to
the riff” has a step “Use the checks that you have” with a copyable
shblock that lists the checks that riff waits for: the required checks of the ruleset of the default branch, butriff/verify(01M5QYDE41FVFX26EC8K5QGYH4). The step says that an empty list means that the repository has no CI, and thatriff/verifyis the only check. The verifier of the wave gets the same rule in its start text. - 01M5R1672NF56P15VD0JEX51K4 In a clone of a repository on GitHub
that is not set up for riff,
riff initdoes the setup of the repository after one question,Question::Setup, with[y/N]: only ayin a terminal does it.riff init --yesdoes it with no question.riff init --checknames each step and changes nothing. A step is one of these, in this order: allow auto-merge and squash merge; makeriff.tomlon the default branch oforigin(the branchriff-setup, the pull request “Set up riff”, a squash merge); make the openBacklog; make the open waveWave 1; make the rulesetriff. Each step usesghand the API of GitHub, never the working tree of the clone. A second run ofriff initin a repository that is set up asks nothing and makes no call that changes GitHub. - 01M5R1675HZ6TJPMQ6536X37T5 The ruleset
riffthatriff initmakes requiresriff/verifyand the checks that the CI runs on a pull request: the check runs of the head of the latest pull request of the default branch that has some. When the setup pull request is the only one, riff waits for its check runs.riff initalso checks the CI check of the default branch. It has no step for it: the check names the fix. A person who is not an admin of the repository gets each step named, and riff changes nothing. A step that fails prints why and its fix, and the next steps run. - 01M5RGG9KJXCKH8C94145WS8SM Before the question of the setup,
riff initprints the line of a check that a step of the setup fixes withstepin place offail, and names the step, when a plainriff initorriff init --yescan do the step (the person is an admin of the repository). A check that no step fixes staysfailwith its fix. The check of the checks of a pull request counts as fixed by a step when no pull request ran a check yet and the pull request ofriff.tomlis to come, and the check of the session image counts as fixed when neither the clone nor the default branch hasriff.toml.riff init --checkand a person who is not an admin getfailand the fix. - 01M5RGGD6YR36EXZEG61XCJVRY After the steps of the setup,
riff initprints the list of the checks again, and the end of a run with a failed check names each check that still fails with its fix. A check whose step failed shows asfailwith its fix in the second list. - 01M5RGGD9AYNC12Z2CG5CDZWNA After the step that merges
riff.tomlon the default branch, when the clone is on the default branch, has no change, and lacks only commits oforigin,riff initrunsgit pull --ff-only origin BRANCHin the clone and says so, and the check of the session image reads theriff.tomlof the clone. Elseriff initsays why it ran nogit pulland names the step to do by hand. - 01M5RN89Z749VYS1CRYJQTKAWY
riff initchecks no file of rules for agents: notCLAUDE.md, notAGENTS.md. It prints no line for such a file, and a repository with none passes. ACLAUDE.mdis the choice of the repository.riff.tomlhas no table for the check or the rules of a repository, and riff reads no file of a build tool: the CI of the repository is the gate of a change. Supersedes 01M5R1675HZ6TJPMQ6536X37T5 in part: the check of theCLAUDE.md. - 01M5SMJMR5KS1Z1Z7CEPH9JVBB In a session container (
RIFF_SOCKETis set),riff initandriff init --checkrun no check of the machine: not podman, not the init of podman, not/etc/subuid, not/etc/subgid, not cgroup v2. A session container runs no podman, and the host runs those checks. In place of them, riff prints one line,skipand the wordsthe checks of the machine: the host runs them, and then runs the checks of the repository. A run ofriff initwith no--checkin a session container does the steps of the repository, installs no daemon and pulls no image: the host has the daemon. On a host, the checks stay as 01M4ZJN6TEWCYCB39XTH6BBD9H gives them. - 01M5SMMP7ZW2PZNGK63WWZAC8P
riff initcounts the open issues of the repository that have no line that starts withDone when:, and names them in a note, asriff init --checkdoes too. A tracker and a release issue are not counted: no session does them. The note changes no exit code, andriff initprints nothing when each open issue has the line. Whenghcannot list the open issues, the note says so. - 01M5SNTPEP41NMSP3SEKR7KVB0
riff initpinsriff-basein a repository that has noriff.toml: the digest in theFROMofdeploy/rust/Dockerfileat the commit of the build (riff::session_image::base). It prints one line that says the project adds its toolchain in an image of its own, builtFROMthat image. With--image REF, it pins REF in place ofriff-base. riff refuses a tag in REF, as it does inriff.toml. Supersedes 01M5R1675HZ6TJPMQ6536X37T5 in part: the image thatriff initpins.
The permission rules
- 01M5084JXTWZQEG7DJHS2E96NQ riff owns the Claude Code permission
rules of each role in one table in its code
(
riff::permissions::TABLE). Each row has the rule, its kind (allow, deny, or the step of a person), the roles that need it (lead, worker, verifier), and its places: the project settings (riff setup) and the settings of each session of the daemon. A test checks that eachriffandghstep of the start text of a session of the daemon, and of the instructions of the lead, matches an allow rule of the session place for the role of the session. - 01M5084K4YQCZF20XA0P2485SG The daemon writes the rules of the
session place into the
settings.jsonof the own Claude folder of each session. The Claude folder of the lead stays between its starts: at each start, riff adds each rule that it lacks. - 01M51K1VJ67KYHRD7DQ86DSX1X The lead in a container plans the
waves with
/opt/riff/riff wave. The session ruleBash(/opt/riff/riff wave *)of the lead allows these steps. The instructions of the riff tools of the lead name them. - 01M5084K79M5XJT3MP8FFVQKDX A check that needs a new build of riff
builds it in the one clone
~/.cache/riff/checkof the machine. One check at a time uses the clone. The check calls the riff of the build by its path,~/.cache/riff/check/target/static/release/riff, with no variable and no prefix. The check runsriff initof the build with--yes. - 01M56P380A5PXGMVJVCSD8KBRG
riff buildmakes the build of a check in~/.cache/riff/check. When the folder is not there, it clones theoriginof the repository of its folder into it. Then it fetchesorigin, puts the clone on the default branch and runsjust static. It prints the path of the riff of the build. A session in a worktree runs no git step in another folder, so no text of riff names a git step in that clone. - 01M509TJNREVNYR9MNN4P9P4RA Each allow rule of the table names its
program and its words. Its only
*is at its end, after a space. No allow rule names a path of the API of the forge: the steps of the waves areriff wavecommands. A test checks each allow rule, and a list of commands that no allow rule of a place may match. - 01M509TK01PW3RN2YPRJD38RSX riff writes a settings file of Claude Code to a new file in the same folder, then renames it. A process that reads the file at the same time sees the old text or the new text, never a part. The file keeps its mode. A settings file that is a symbolic link stays a link: riff writes the file that it names.
- 01M51JCW04FAHZDQ144KDNCCPX
riff initandriff init --checkname each wide allow rule, with its file: in the user settings of Claude Code, and in the project settings and the local settings of the repository of the current folder. A rule is wide when it is a rule of the Bash tool, and it has no pattern, or it has a*that is not at its end after a space, or it matches a command of the listriff::permissions::OUT_OF_BOUNDS. The old end:*of a pattern is*. A rule of another tool is never wide. A wide rule fails no check. - 01M51JCW2EKF8H8QFQYSVRSPET
riff initasks one yes or no question before it removes the wide rules. Only ayin a terminal removes them. A no, no terminal,--yesand--checkkeep each rule. riff removes only the wide rules, and keeps each other rule and key in its order. - 01M51M3ZKWQYBBPD97R0G9QAX0 A binary is a release when its
SHA-256 is the line of
riff-TARGETinSHA256SUMSof the GitHub release of its version tag. With each other binary, for example a check build or a dev build, and when riff cannot readSHA256SUMS, the riff daemon changes no settings of Claude Code. - 01M51QARV45MGEKC6KGWX1N5NS The sync of the rules reads
SHA256SUMSonly from the GitHub releases. WhenRIFF_RELEASESis set for the riff daemon, its sync changes no settings of Claude Code, and its line names the variable. - 01M51M3ZTV61C9BMFT3J1M06NF The unit of the riff daemon gets
CLAUDE_CONFIG_DIRwhen it is set at the install, so the daemon andriff initchange the same user settings of Claude Code. - 01M5084K9J566981EXZ7YJRGRR When the auto mode check refuses a step, the session stops that step, writes the command and the reason on the issue of its item, and tells the lead. It does not try the step again in another form. The fix is a new rule in the table (the step is in bounds) or a change of the text of the session (the step is out of bounds). The fix is never that a person does the refused step by hand.
- 01M51V0Q5E7MNPWP9T7TSJFT5W A session runs each
riffandghstep alone, in its own Bash call, and gives each file of the step by its absolute path. Nocdcomes before the step, and no&&,||or;joins another command to it. The instructions of the lead say so. - 01M516HETNZCBFBJNHT8BKFB1Y riff gives each session that it starts
the auto mode settings of one place in its code
(
riff::permissions::auto_mode):skillOverrideswithauto-mode-setupoff, so no session shows the dialog “Teach auto mode about your environment?” and no session has/auto-mode-setup; andautoMode.environmentwith"$defaults"first, then the places of riff work: the GitHub owner of the repository, the repository of riff with its releases,ghcr.ioand the riff server of the session. - 01M516HEX3GVTN9K2QS12A4PG3
riff workers startgives each worker the auto mode settings in its flag settings (--settings). The user settings file does not change. The daemon writes them into thesettings.jsonof the own Claude folder of each session. The Claude folder of the lead stays between its starts: at each start, riff adds each setting and each entry that it lacks. riff writes noautoModeinto the project settings: Claude Code does not read it there. - 01M5QWJJBY5VW53F0XAWWDQ79K A worker session in a container may read the checks of a pull request, read the log of a failed run, and run a failed job again: the session rules Bash(gh pr checks *), Bash(gh run view *) and Bash(gh run rerun *). The start text of a build session for failed checks uses them.
- 01M59EWWDC7EBQEG02TVNN81HD riff has no user place of the
permission rules.
riff initwrites no rule into the user settings of Claude Code, and the riff daemon syncs no rule into them. The tableriff::permissions::TABLEhas the project place and the session place only.riff initstill names the wide allow rules of the user settings (01M51JCW04FAHZDQ144KDNCCPX). - 01M5SMPA9WN40DMQFNE3V1H4SK The package sources that the auto mode
of a session trusts come from the riff config of the repository: the
key
sourcesof its table[session], a list of hosts. riff adds none by itself, so a config with nosourcestrusts none. riff reads the list fromriff.tomlon the default branch oforigin, as for the merged pin (01M51STPHXSSZMHF9T7Z85097R): an edit that is not merged trusts nothing. Each host is one entry ofautoMode.environment, “Trusted internal domains: HOST.” riff refuses asourcesthat is not a list of hosts.riff.tomlof riff namescrates.io,index.crates.ioandstatic.crates.io. Supersedes 01M516HETNZCBFBJNHT8BKFB1Y in part: the entry ofcrates.io. - 01M5SNV7KZRDBRC3NM2E5W92KS
riff initwrites the permission rule of the lead forriff release hold, in the container of the lead. - 01M6FZXZ9267YKMH97E6A2Y2EP A worker and a verifier do not change
the body or the title of an issue. They add to an issue with
gh issue comment. Only the lead edits an issue. The session rules are by role (riff::permissions::session_rules): the session place givesBash(gh issue edit *)to the lead only,Bash(gh issue comment *)to a worker and a verifier, and to a worker and a verifier it gives deny rules forgh issue editwith--body,--body-file,-b,-F,--titleand-t, also aftergh -R. No rule namesgh api: no allow rule matches agh apicall (riff::permissions::OUT_OF_BOUNDS), a deny rule cannot list each form of an API write, and the auto mode check judges the call. The start text of a worker and of a verifier says the rule in one line. Supersedes 01M5084K4YQCZF20XA0P2485SG in part: a session gets the allow rules and the deny rules of its own role.
The steps of an update
- 01M51QFPJAABDP1V7WFY8NQR60 The opt-in to the update by itself
(
update.auto = true) is the consent of the person for each step of an update. At each start, the riff daemon of a release does each step ofriff initthat needs no person and that is not in step: it writes its unit again (01M51QFPQ68JHS08TWXNNWZ0D7), and pulls the session image of each wave (01M51QFPSMDDMFPZXAEYWZYJPV). It logs one line for the gate and one line for each step. Withupdate.auto = false, or a binary that is not a release, it changes nothing. - 01M51QFPMSG81YRDDGQQ1WJDSQ Each step of an update that changes
the machine trusts
SHA256SUMSonly from the GitHub releases, as 01M51QARV45MGEKC6KGWX1N5NS says for the rules. WithRIFF_RELEASESset, the riff daemon does no step, and its line names the variable. - 01M51QFPQ68JHS08TWXNNWZ0D7 The unit of the riff daemon is in step
when its file is the text that this riff writes for the
ExecStartand the variables of the file. Else the daemon writes that text, runssystemctl --user daemon-reload, and starts again on the new unit when it is idle, as for a new riff (01M517BTP6BAGTRBYWPX9GPVE0). - 01M51QFPSMDDMFPZXAEYWZYJPV The session image of a wave is in step when podman has the merged pin of the main clone of the wave (01M51STPHXSSZMHF9T7Z85097R). Else the daemon pulls it.
- 01M51QFPW37EXVH4AXH70MSXBT The riff daemon never does a step that
needs a person: the checks of the machine of
riff init(podman, the init of podman,/etc/subuid,/etc/subgidand cgroup v2). It keeps the first such step that is not in step, with its fix, in the local files of riff. The status line of each session shows it as a tagneeds you: STEP: FIX, until a later look finds it in step. - 01M51QFPYHK0916C5ZN2B55BR0
riff update --checkshows what the riff daemon of this riff does at its next start, and one line for each step of an update: the riff binary, the unit of the daemon, the session image of each wave and of the repository of the current folder, and the checks of the machine. Each line saysin step,not in step,needs youorfailed, with the fix. It changes nothing, except the step that needs a person for the status line. It exits with 1 when a step is not in step. - 01M5RG6C4H3YPACS41S6EPS87E riff has no step that removes what an
older release left on a machine (01M5RG6BTXAJY8DV6MT315VZWF).
riff init,riff init --check,riff update --check, the update by itself andriff claude-tokenremove no plugin of Claude Code, no key of its settings and no old file of the token. The book has no how-to for the update from an older release. This supersedes 01M5JFF7TAVA87TMJXK25HDJ19 and 01M5GT0QV72BZA4VVWDPR1WG17, and 01M58XZF928T82YAMF4DSJBKBF for the removal of the old file.
riff run
- 01M4Z81ARSTAK8HR96S084SDFD Each session of
riff runhas a folder of its own,sessions/ITEM-ID8in the data folder of riff, whereID8is the start of the session ID. WithRIFF_HOME, the folder isRIFF_HOME/sessions/ITEM-ID8. It holds the clone, the own Claude folder andevents.jsonl, each event with no secret. The clone stays until the work of its branch is over (01M53NJ5V27BCHDNHK2THQQZZQ). - 01M53NJ5V27BCHDNHK2THQQZZQ The riff daemon removes the folder of
each session of
riff runand of a wave that does not run, also when no wave runs: at its start, then each 10 minutes, and before each look of a wave. It removes the folder, when the sweep of 01M53S7WAXD46YPN0DR96XSTMG allows it, when the branch of its item has no open pull request and a merged or a closed one, or when the branch has no pull request and the issue is closed. A session runs while the daemon has its record, while a job of the daemon starts it (01M53S7WDPQ2GWHJWJ6SZXZ5PN), or while theriff runthat made the folder runs: it holds the lockITEM-ID8.lockbeside the folder from before the clone to its end. - 01M53NJ5Y2CMY5MWF9328KW7DE
riff daemon sweepasks the daemon to remove the folders of 01M53NJ5V27BCHDNHK2THQQZZQ at once. It prints each folder that the daemon removed and each folder that stays, with the reason (01M53S7WAXD46YPN0DR96XSTMG), or one line that it removed none. - 01M53S7WAXD46YPN0DR96XSTMG The sweep of 01M53NJ5V27BCHDNHK2THQQZZQ
removes a folder only when its clone has no file that is not committed
(
git status --porcelain) and no commit ofHEADor of a local branch that no branch oforiginhas, or when the sweep pushed that work (01M5NVKNGZPK0GG7B7ZWBS0DFE). Else, and when git cannot read the clone, the folder stays.riff daemon sweepprints each folder that stays for this reason, with the reason. The log of the daemon gets that line one time, and again when the reason changes. - 01M53S7WDPQ2GWHJWJ6SZXZ5PN A job of the daemon keeps each folder
of its item from before the clone of its session to the end of its
last step after the end. The steps after the end read the folder, for
example
events.jsonlof a verify, and the daemon has no record of the session then. This is also true for a job that a new daemon takes up. - 01M53S7WGM9DQV2DXFHBRSMTP8 Each
ghcall of the sweep of 01M53NJ5V27BCHDNHK2THQQZZQ has a time limit of 30 seconds. At the end of the limit, riff stopsghand each process that it started. The sweep then removes nothing for that folder, says why, and goes on to the next folder. The next sweep tries again. - 01M4Z81ATZK3FX9GRFTM236FE6
claudein a session ofriff runtakes the riff session ID as its own session ID (--session-id), and runs in the auto permission mode (--permission-mode auto). - 01M4Z81AX3ND5RX7V1GEY4HMF5 The branch of a session of
riff runisworktree-ITEM. Whenoriginhas the branch, the clone goes on from it. - 01M4Z81AZ88ZS0HPHV5KGQF3HV
riff runtakes only an issue,issue-N. The session opens the pull request withgh, with the frame ofriff pr open(Closes #N, the summary, and the trailers of the issue and its wave), and turns on auto-merge at once. - 01M4Z81B1913NF6HBZ5G38B3Q9 When
claudeends,riff runfinds the open pull request of the branch withgh, and sends a verify request to the lead of its user. Then it releases the item, and ends the session. Ctrl-C or SIGTERM stops the container withpodman stop, and riff then releases the item. The exit code is 0 only when the turn ended with no error and the branch has an open pull request. - 01M5157TYDEHZHDXSC1AHCRDRN
riff runsends no verify request when a lead holdsverify-ITEM. It reads the holds of the repository thread after the session, in the end step of the verify request, and prints the hold: who, when and why. It still releases the item, and the exit code does not change. - 01M5157V0P92EBHF4M7N9K0RGR An issue with the label
no-verifygets no verify. Its pull request is no free work for a verify, for the rollout of the lead and for the wave of the daemon.riff runsends no verify request for it, and prints why. riff reads the labels of the issue withgh. Whenghcannot read them,riff runprints the fault and goes on to the holds (01M5157TYDEHZHDXSC1AHCRDRN). - 01M51KH0H7J35PEKMHP5KQB84T After the end of a build, the wave of
the daemon does the check of
riff run: when a lead holdsverify-ITEM, or the issue has the labelno-verify, the lead gets a note with the reason in place of the verify request (01M5157TYDEHZHDXSC1AHCRDRN, 01M5157V0P92EBHF4M7N9K0RGR). - 01M4ZQVVGQYXTAVRB69C7YPC3Y The verify request and the release of
riff runhave the budgetEND_BUDGET, 10 minutes, notSHORT_BUDGET. A fault gets a new try while the budget lasts (01M4A8041F8EK1VYDE4C9QG8N8). So a new instance of riff-server at the end of a run drops neither. The keep-alive of the session runs until the release. - 01M4ZYFJFTX9ZEX7QT4QH7E9T6 A SIGINT or a SIGTERM during the end
steps of
riff run(the verify request, the release and the end of the session) stops the wait for riff-server at once. Thenriff runsends no more end step, names each step that it did not send, and ends with the exit code 1. riff-server frees the claim when the session stops, as for each other session. - 01M51JG5Y47184401EWT4HVEG0
riff runlistens for SIGINT and SIGTERM with one set of signal streams, from the start of the session to its last end step. A signal after the end of the session and before the first end step, for example whileghfinds the pull request, stopsriff runbefore that step (01M4ZYFJFTX9ZEX7QT4QH7E9T6). A second signal while the container stops does the same. - 01M4ZBGK7SFEXE1AGH6Z0YQHJP On a host that is not Linux,
riff runstarts nothing, and says that it needs Linux. - 01M4Z81B3A324KKE8M8Z71TM4M Before the claim,
riff runchecks podman and the image, the sign-in ofgh, the sign-in ofclaude(01M4Z60B869HWDTZMSYPVYACD8),HOME,USER, and the wave of the issue. A failed check takes no claim. - 01M4Z81B5A456HDKW7G0WFGCY6
riff runtakes the merged pin of the main clone (01M4Z7SP3KSKJYKNRP1MQ70C4Q, 01M51STPHXSSZMHF9T7Z85097R).--imagegives another image, for example a local build. When podman does not have the image,riff runpulls it before the claim. When podman cannot pull it, riff takes no claim. - 01M4ZD48DHAKJ1EHX6QHYAQ7ZM The own Claude folder of a session of
riff runhas asettings.jsonwithincludeCoAuthoredBy: false. Soclaudeadds no attribution line, and the trailers end each commit and each pull request body. It is a setting of riff for its sessions: riff does not read or copy the settings of the person. - 01M4ZSZX9H59T04QSJ01V4F9TE After the
resultof a turn,riff runandriff daemon eventsshow a line with the description of each background task thatclaudewaits for, from the eventsystem/background_tasks_changed. They show a new line at each change of the tasks until the next turn starts. - 01M4ZSZXBXM6T6MGQ00ACQA66X The start text of
riff runtells the session to stop each background task that it started, and to leave none at the end of its work. - 01M5R580VCRCHJM8FY7NT485R1 The start texts of a build, a rebase, a
fix of checks and a verify, and the instructions of the lead, say only
what riff needs from a session: the item, the branch, the frame of the
pull request and the verify report. They name no convention of the
riff repository: not
just,requirements.md, the book or the Gate. The verify text names the checks of the full check as riff read them. TheCLAUDE.mdof riff holds the rules that left the texts. A test of the texts (text::tests) checks the words, and the how-to “Write a CLAUDE.md, when you want one” says what aCLAUDE.mdof a repository may say. - 01M51R8H3TXPKAEWDJQGQ01PBK A repository with no session image of
its own pins the session image of
como-technologies/riff: itsriff.tomlhas the same keyimageas theriff.tomlof riff. That image hasclaude,git,gh, a POSIX shell and Rust. The people of the repository can make an image of its own later (01M4Z60B7JF80TMP6FHQ04MHCZ). - 01M5RN89WH3PHV3BHMJ25T3SX4 The start texts of a build, a rebase, a
fix of checks and a verify, and the first prompt of the lead, send the
session to the rules of the repository for agents when it has them,
for example
CLAUDE.mdorAGENTS.md. They name no file as required and no command. A build, a rebase and a fix of checks tell the session to run the checks that the repository names, when it names some, before a push. The full check of the pull request (its CI) and the verify decide the merge. Supersedes 01M5R580VCRCHJM8FY7NT485R1 in part: the part that sends a session to theCLAUDE.mdof the repository.
How the relay drives claude
- 01M4Z60B82FHN2YG9YJHW8XZX1 A worker and a verifier run
claude -pwith--input-format stream-json,--output-format stream-jsonand--verbose, underriff relay. Oneclaudeprocess holds the whole session. The daemon gives each wake as one input (01M5792DP1KZR2XGSCMRWDHCGZ). The relay writes it as one line on stdin. No session runs with--bare. - 01M5792DP1KZR2XGSCMRWDHCGZ While a worker or a verifier runs, the
daemon watches its wakes on riff-server. Each
riff tellto it and each message that wakes it is a wake. The input ofclaudeis open from the start until theresultof the first turn, and closes there. While it is open, the daemon gives each wake as one input: the line ofriff watch --once. During a takeover, the wake waits for the go on. The daemon gives each wake once. - 01M5792DRA12JD032F3H8JVC0Z For each wake of a worker or a verifier
after its input closed, the daemon tells the sender in a direct
message that the session ended its work and does not read the
message. After the end of the session on riff-server, a
riff tellto it fails. - 01M57AVKBHFVYJBRT0W2ZTAJDK The daemon refuses each sender at most one time for each session.
- 01M4Z60B828HF30H7H017WPXGS The relay sends the events of
claudeto the daemon:system/initat the start of each turn, each tool call and its result, each retry,system/permission_denied,system/vcs_state_changedfor a commit and a push,rate_limit_eventfor the windows of the plan, and theresultof each turn with its usage.total_cost_usdandduration_api_msadd up over the turns of a session: the daemon takes the difference for each turn. - 01M4Z60B83ZH10HYKW7X1C0SSS The permission rules decide each tool
call of a session.
--permission-prompt-toolsends each other call to riff. A takeover stops the turn of a worker (01M51KJ1R9K7A7RW8T8XH8VW43), and opensclaude --resume IDwith a terminal in the same container. - 01M4Z60B84FTGMXDZ1F52DC98C riff keeps the MCP servers of the
claude.ai account of the person in a session, as a plain
claudehas them.
The home of the person
- 01M4Z60B85X62PNYSVJWKXEWH3 riff writes nothing in the home of the
person outside its own folders. It does not write
~/.claude,~/.claude.json, a git config or a shell file. - 01M4Z60B86N999JRN7Z1G52PG4 Each session has its own Claude folder
in a folder of riff. So riff never changes the Claude config of the
person, and that config does not change the sessions.
claudesigns in with the token of the plan (riff claude-token). - 01M4Z60B869HWDTZMSYPVYACD8 The daemon gives each session the
long-lived token of
claude setup-tokeninCLAUDE_CODE_OAUTH_TOKEN. riff gives podman only the name of the variable (-e CLAUDE_CODE_OAUTH_TOKEN), so the value is in no command line. riff keeps the token in its own folder, with mode 0600, and never in a log, a message or a clone. Only on a host with no token, riff binds~/.claude/.credentials.jsoninto the Claude folder of the session with no write, and starts no session while the access token has less than 60 minutes left (01M58B7H50RCG2VXXV9XX5Y7HM). - 01M58B7GZT7WFSBBG1XWJHR4MH
riff claude-tokenreads the token from stdin, with no echo. It refuses when stdin is not a terminal, so an agent session cannot run it. It keeps the token in the fileclaude-tokenin the folder of the daemon, with mode 0600, in the place of the old one. The token goes through no riff-server, no message, no log and no clone. - 01M58B7H2HMYT6VTY0B8T2G0KW When a token is set, the daemon starts
each container with
-e CLAUDE_CODE_OAUTH_TOKENand binds no.credentials.json. The value goes in the environment of podman, and the scrub of the session takes it out of each event. - 01M58B7H50RCG2VXXV9XX5Y7HM When a token is set, the daemon,
riff runandriff lead startdo not read the sign-in file ofclaude, and have no rule of 60 minutes. When no token is set, the bind and the rule of 60 minutes hold. - 01M58B7H7HD0H48A0C79F8M4CR When
claudein a session refuses the token, the daemon writes the markclaude-token-refusedbeside the token, and says it in the lines of the session. While the mark is there, each start fails with the step for the person, so the wave starts no new session and the lead gets one message (01M57A7A1W2K9440WZA1QZ3M9T).riff claude-tokenremoves the mark. - 01M58B7HA8CRCW0MJ17QP3R5QG
riff daemon statusand the partHostsofriff whoshowclaude token:set,refusedornot set, and never the value. With no token,riff daemon statusgives the step: runclaude setup-token, thenriff claude-token, on this machine. - 01M58XZF928T82YAMF4DSJBKBF This supersedes
01M58B7GZT7WFSBBG1XWJHR4MH, 01M4Z60B869HWDTZMSYPVYACD8 and
01M58B7HA8CRCW0MJ17QP3R5QG in part, for the place of the token
(R21).
riff claude-tokenkeeps the token in the OS keyring under the nameclaude setup-token, withriff_client::secrets::set, and writes no file for it. The daemon reads the token from the keyring at each container start, withKEYRING_WAIT. A keyring that is locked, or does not answer, starts no container: the wave tells the lead one time, as for the sign-in of riff (01M4385CEWGCP31DP5PAMPXZ97). The daemon never falls back to the sign-in ofclaudeon the host then. The markclaude-token-refusedstays a file.riff daemon statusandriff whoshowclaude token:set,not set,refusedorkeyring locked. - 01M58B7HCXKZF39DCF6KV84JRV When a token is set, the daemon posts
no note for the sign-in of
claude(01M57D3FC5PB5EQ6Y38Y5R2DBB), and the facts of the host do not warn about its minutes left. - 01M4Z60B874MBWZWDP8YDEM3HR riff trusts the clone of each session
in its own Claude folder, as the person trusts the main clone: the
.claude.jsonof the folder hasprojects[CLONE].hasTrustDialogAccepted: true. - 01M4Z60B88DC67N3GVHY8JGRPQ A session uses the sign-in of the
person for git and
gh(01M4Z60B7QDWEFQ96H448RE9N2, 01M4Z60B89BHXQVJ2MGRRQ2CKJ), as a plainclaudedoes. A commit has the git identity of the person and aRiff-Session:trailer. riff has no GitHub App. - 01M4Z60B89BHXQVJ2MGRRQ2CKJ The daemon reads
gh auth tokenon the host, and gives it to the container inGH_TOKEN. riff gives podman only the name of the variable (-e GH_TOKEN), so the value is in no command line. - 01M4Z60B89G8YNVMTEPGX7FQCV A session commits with no signature.
riff sets
commit.gpgsign=falsein the environment of the container (GIT_CONFIG_COUNT,GIT_CONFIG_KEY_0andGIT_CONFIG_VALUE_0), so it applies to each git call of the session. The squash merge on GitHub makes the commit onmain. No gpg agent goes into a container.
The security of riff’s own code
- 01M4Z60B8JEX15M1Q2435FZSDM A session gets no power that a plain
claudelacks, other than the riff tools. An owner or admin step, for example to admit a member or to change a role, needs a person at the CLI. No MCP tool does it. - 01M4Z60B8KDXD190RWC7D184Q3 No secret goes into a log line, a
message or a file of a clone. riff builds no shell string from input.
CI keeps
clippy -D warningsandcargo audit.
Process
- 01M4Z60B8PW9GSQ79H176P6915 Before code, one short table says what
sessions share and how riff keeps them apart: the files, git, the
processes,
/tmp, the ports, the caches, the home, the CPU, the memory and the network. It is in the part “Sessions in containers” of the book page How It Works (#709). - 01M4Z60B8Q8YH6DRSZWPV42CQ0 Each release passes a real-session
test: the riff binary and the session image, with a real
claude, on a host with a real home (a~/.gitconfig, project settings, first-run state), do one item from the start to a pull request. The test runs in theSessionSpecof a session. CI runs the same image. - 01M5940ES080P07A5NGWWRWF1Z A new requirement goes into
docs/src/requirements.md with
just req SECTION "text", never by a hand edit. The command takes the heading of the section, gets an ID likejust rid, wraps the text at 72 characters, and writes it after the last requirement of the section. The checkblock-endofreqs::checkfails when a requirement block, its first line and the indented lines under it, does not end with a full stop. CLAUDE.md says to use the command. The start text of a container session no longer says it (01M5R580VCRCHJM8FY7NT485R1).
The release commands
- 01M58B0HZYMAK1B2AJMWKHCXG0
riff release cut X.Y.Zmakes the release pull request fromorigin/main. A lead or a worker runs it. It sets X.Y.Z inCargo.toml, in theplugin.jsonof the plugin and inCargo.lock, runsdeploy/release-check.sh, and pushes the branchrelease-vX.Y.Z. The pull request “Release vX.Y.Z” has the labelrelease, the current wave, auto-merge on, and a body with the level line, the list “Waves in this release”, and the items of each wave since the last release tag. It saysRefs #NandPart of #Nfor the release issue, neverCloses #N. riff makes the release issue in the current wave when there is none. It refuses a version that is not newer than the last published release. - 01M593FVJRTCGD1RPWXYJP5BNH The release issue that
riff release cutfiles has two parts. “Done when” is the release: published, deployed, and checked after the deploy. “Verify of the release pull request” is the verify of the pull request: only the change of the version, the format of the body, and the Gate. The issue lists the steps after the merge apart. The verify of a pull request with the labelreleasereads that part, and not the line “Done when”. - 01M58B0J4VQ61ER69Q09JYP7CG
riff release cutrefuses while the version onorigin/mainis newer than the last published release and has no published release. Only--skip VERSION --reason TEXTwith that version lets it go on. The reason goes into the body of the new release pull request, and from there into its GitHub release. - 01M58B0JABCET719T3HFB19NDW
riff release publish vX.Y.Zrefuses, and changes nothing, when one of these is false: the pull request “Release vX.Y.Z” with the labelreleasemerged, and the statusriff/verifyof its head commit is a success; its merge commit is onorigin/main, andCargo.tomlthere has X.Y.Z; the tag does not exist on GitHub; a commit from the merge commit toorigin/mainhas a pass ofjust real-session(01M58KMKZ2EG8NZG4F085S6NQX); the account ofghis an admin of the repository. Then it makes the annotated tag on the newest commit with a pass, pushes only that tag, and makes the GitHub release: the body of the pull request with no link line, no linePart ofand no trailer, then the generated notes from the last release tag, marked as the latest. It prints the URL of the CI run of the tag, and says that the deploy waits for the approval ofproductionby a person. When GitHub refuses the push of the tag, it says so, names the ruleset of the tags, and deletes the local tag. After the release, it posts a chat line to the owner inriff chat: the version, the CI run, andriff release approve vX.Y.Z(01M58KMM1BHX3YY81BPZG04JSX). - 01M58B0JEYNAW67NS226FMX5XH
riff release statusshows, for each session: the version onorigin/main, the last published release tag, a merged release pull request whose version has no published release, and each CI run that waits for the environmentproduction. - 01M58B0JMFQ5E40TWTQ48MPSGK
riff release approve vX.Y.Zfinds the CI run of the tag that waits for the environmentproduction, shows the tag, the commit and the run, asks yes or no, and on yes pauses the whole riff (01M58KMM3NFGJRW4CCV50KJQB2) and approves it with the commentRelease vX.Y.Z. With no waiting run, it says so and changes nothing. - 01M58B0JSQMWNCMSNX19536XT8
riff release publishrefuses, and changes nothing, in a worker and in an agent session that is not the lead of its user, also when riff cannot ask riff-server, and for a member, in a terminal or in its lead: a user that is not the owner and not an admin of the riff. In a terminal it also refuses when riff cannot ask riff-server for the role. A riff with no sign-in has no roles, so there each person and each lead may.riff release approverefuses in each agent session: a worker, a container session, a process with a session ID, and a process under Claude Code (CLAUDECODE), also a!command. The lead of the owner may approve with a message of the owner in the threadchat(01M594XQJMXAXY3JXP66Y8EVQ7). A lead in a container may publish (01M59269HHZ5C82DR3RG3QHAM3). The instructions of the lead have no tag line and nogh releaseline. - 01M59269HHZ5C82DR3RG3QHAM3
riff release publish,check,retryandrollbackallow the lead of the owner or of an admin in a container (riff lead start). The daemon tells the lead from a work session by the socket of the session: only the session with a terminal is the lead, and a work session cannot forge that. A work session in a container is still refused. The container of the lead hasRIFF_USER, so the riff commands find their user without a keyring, andRIFF_CHECKnames a folder that it can write forriff build. - 01M5ND0ZNFFAPT1AWD3DP1Y77N A release pull request needs no model
verify.
riff release verify Nchecks that the pull request has the labelrelease, that its diff is onlyCargo.tomlandCargo.lock, that its body has the format ofriff release cut, and that the Gate of its head commit passed. Then it sets the statusriff/verify. It refuses else, and sets nothing. - 01M5NCYW06FJ8P5X629VFK1BNR One message of the owner or of an admin
in riff chat approves the deploy of a release: “approved”, “approve
4.0.1” or “approve v4.0.1”, from a person and not a session. A message
with no version counts for the version of the last release line of
riff before it. With it, the lead runs
riff release approveandriff release check.riff release approveof the lead accepts this message instead of the publish line, and replaces 01M594XQJMXAXY3JXP66Y8EVQ7 in part. A message withnot,neverordon'tdoes not approve. A message of the owner or of an admin withstoporwaitafter the approval cancels it, and the lead stops before the next step. - 01M5R13DK401K3TZ91E2QK2DYM The refusal of
riff release verifyfor the full check of the head commit names the commit, the checks of the full check and their state, as the line ofriff verify passdoes. The full check is the same as for 01M5QYDE41FVFX26EC8K5QGYH4. Supersedes 01M5ND0ZNFFAPT1AWD3DP1Y77N in part. - 01M5SMPX8NM75P8E38DGQ17W7B Only a repository whose
riff.tomlhas the table[release]has the release cycle of riff. Without it, eachriff releasesubcommand buthold,riff buildand the real-session test refuse with one line, “this repository has no release cycle of riff: riff.toml has no [release]”, and touch nothing on the forge. Thenriff wave end Nhas no release gate, and the verify start text has no part for a release pull request. The probe fordeploy/release-check.shdoes not decide it. Theriff.tomlof riff has[release]. Supersedes 01M58KMMCZCE7HDNVT9PVHVZX2 in part. - 01M5TKEH5E2ZQ22PAN9FTT27VW The daemon of a host decides the
release cycle with the merged
riff.toml, never with the working tree of its main clone. For the requestrelease testand the requestbuild, it fetches the default branch oforiginand refuses with the line of 01M5SMPX8NM75P8E38DGQ17W7B when that file has no table[release]. A[release]that only the working tree has does not count, and a[release]that only the merged file has does. A main clone with noorigin/HEAD, a fetch that fails and ariff.tomlthat is not TOML are errors that name their cause.riff releaseandriff buildin the clone of a person still read the working tree. - 01M5TKDYAFA683KNAJTGYTQD5W The line that
riff release cutwrites in riff chat asks the owner for nothing: it says that the release is cut and that riff tells the owner when the deploy waits for an approval. The only ask is the line of riff while the jobDeploywaits (“riff release vX.Y.Z waits for your approval”, also “still waits” in a reminder). The message of the owner or of an admin that approves a release counts only after the first such line, or after the lineriff release vX.Y.Z is published. A line of riff approves nothing. Supersedes 01M5NCYW06FJ8P5X629VFK1BNR in part. - 01M5TKMVB7CF0QNAWXEVGEQETJ Only a message that a person wrote
counts as the word of the owner or of an admin in riff chat. A line
that riff writes as the person of the riff has the kind
riff: the line “waits for your approval” of the watch of the deploy and its reminder, the other lines of the daemon in riff chat, and the lines ofriff release cutandriff release publish. riff-server keeps the kind, a reader shows the line as a message, and it wakes as a message. The kindriffhas no name inriff post.riff release approveof the lead skips each line of the kindriffwhen it looks for the approval and for a stop. With only lines of riff in riff chat, it refuses and changes nothing. A line of a session never counts, also with no kind. This adds to 01M5NCYW06FJ8P5X629VFK1BNR, 01M594XQJMXAXY3JXP66Y8EVQ7 and 01M5TKDYAFA683KNAJTGYTQD5W. - 01M5TQ0DX4M2EJJGE7QA8DZVYS After a rollback of riff-server to the
release before, the kind
riffis refused, and a line of riff in riff chat must still post. When riff-server refuses a post of the kindriffwith “no such kind”, riff posts the line again with the kindmessageand the mark[by riff]at the start of the body. A line with the kindriff, or with this mark, is a line of riff:riff release approveof the lead skips it when it looks for the approval and for a stop. Any other refusal of the post is an error. This adds to 01M5TKMVB7CF0QNAWXEVGEQETJ.
The release cycle
- 01M58KMKR33P1FSCFDQRZ56A8P Each wave with a change that a person
sees since the last release tag ends with one release. Each step of
the cycle is a riff command:
riff release cut,publish,status,approve,check,retryandrollback,just real-session, andriff wave end. No session skips a release: only a person skips one. - 01M58KMKTE2XC35CNJBFKZWAXF
riff release statusshows the state of the current release: the open release pull request, else the version onorigin/main. The states arecut,merged,tested,published,waiting,deploying,deployed,checked,failedandrolled back. A closed release issue, or a passed check on it, ischecked. A rollback on it isrolled back. - 01M58KMKWRKSV1DR42FRH2K43X Each command of the release cycle
prints the next step in a line
next: …, also in a refusal. - 01M58KMKZ2EG8NZG4F085S6NQX
just real-sessionsets the commit statusriff/real-sessionof the commitHEADof its clone:successafter a pass,failureafter a failed step, none after a signal.riff release publishrefuses while no commit from the merge commit of the release pull request toorigin/mainhas a pass, and tags the newest commit with a pass. - 01M58KMM1BHX3YY81BPZG04JSX
riff release publishdoes not need a paused riff. After the release, it posts a chat line to the owner inriff chat. - 01M58KMM3NFGJRW4CCV50KJQB2 HUMAN GATE 1: on yes,
riff release approvepauses the whole riff, which tells each other lead, and then approves the deploy. While a deploy waits for the approval,riff release cutrefuses, and the riff is not paused. - 01M58KMM5ZPAQ289S2YJVQ5WW6
riff release check vX.Y.Zrefuses while the jobDeployof the newest CI run of the tag did not end. After a pass of the job, it checks:riff servershows the build; the Cloud Run log after the start of the job has no run of 503 longer than 20 s, no 500 of riff-server, no 401 or 429 of the token store, and no line “riff-server serves” of an older revision after the new one; each host of the partHostsofriff whohas the build. It writes the result on the release issue. When each check passes, it closes the release issue and resumes the whole riff. After a failed job, it files an issue with the failed jobs and the run in the current wave, writes it on the release issue, and resumes the riff. - 01M58KMM8CV9731AWGVZQBB80D
riff release retry vX.Y.Zruns the failed jobs of the newest CI run of the tag again. It refuses a run that did not fail. - 01M58KMMAPJ33CQWR8GRX62GMR HUMAN GATE 2:
riff release rollback vPREVstarts the CI workflow onmainwith the inputtag=vPREV, and writesriff release rollback vPREVon the release issue of the newest release. It refuses a tag that is not a published release older than the newest one. The run by hand has the nameDeploy vPREV, soriff release approve vPREVandriff release check vPREVfind it. - 01M58KMMCZCE7HDNVT9PVHVZX2
riff wave end Nrefuses while a merged pull request of the wave, other than a release pull request, is in a release tag with no checked release, and names the next step. It refuses while one is in no release tag, unless--no-release TEXTis given: then riff writes “No release: TEXT” in the description of the wave. - 01M58KMMF9WMH39ZMD0MKGRVZB
riff initwrites the permission rules of the lead forriff release cut,publish,status,check,retryandrollback, and no rule that namesapprove.publish,check,retryandrollbackrefuse a worker, a container session, an agent session that is not the lead, and a member, aspublishdoes (01M58B0JSQMWNCMSNX19536XT8). - 01M58KMMKZ8T11KT3R3Z7D0RE1 HUMAN GATE 3:
riff release cut --skiprefuses in each agent session, and for a member. - 01M58KMMP9FKRFYX58ESVRSZQX When riff cannot read the Cloud Run
log or ask the shared riff,
riff release checknames each check that it could not do, saysgcloud auth loginfor the log, and changes nothing but the comment on the release issue. - 01M58Q03ZBVVXAQ2WZ8EJ5FWS4 The order in a wave: merge each item, make a release, deploy the release to the shared server, stop the workers, update each machine, start the sessions again, run the checks after the release, close each item. The work goes on while the deploy waits for a person. The lead stops the workers after the deploy, before the update of each machine.
- 01M5GSDJ3YWANWEZNZ6VSDC1BE
riff release checkcounts a host as one that has the build when the version of the build of its daemon is the version of the release. The commit and the time of the build do not count. - 01M5GVPND32X3R9MYXYB5SV83W When the container of the lead has no
gcloud, each check ofriff release checkthat reads the Cloud Run log names the step for a person on the host:riff release check vX.Y.Zin a terminal. - 01M5MQHDMBYG187T0Q622XWBMN The check “no 500 of riff-server” of riff release check counts only the 500 replies of the newest revision: the revision of the last line “riff-server serves” after the deploy. An instance of an older revision that starts in the switch to the new one stops before it serves, and its 500 replies do not count. With no line “riff-server serves”, each 500 reply counts.
- 01M5ND5B14MXGGK0RJ7R2BREW6 In the container of the lead, riff release check reads the Cloud Run log through the riff daemon of the host, with the gcloud and the sign-in of the person on the host. The container gets no Google credential. The daemon refuses a worker. When the sign-in of gcloud ended, each check of the log says that a person on the host runs gcloud auth login, and the daemon tells the lead in chat.
- 01M5RGCW5NCKQYA61RM63914TX Each async operation that a riff
command starts has a watch that wakes the lead at its end, with its
result and the next step. The command that starts the operation
registers the watch with the riff daemon of its host in the same call,
so no session has to remember to wait. The operations are the Gate of
the release pull request (
riff release cut), its merge (riff release verify), the real-session test (riff release test), the CI run of the tag up to a deploy that waits for a person or a job that failed (riff release publish), the jobDeploy(riff release approve,retryandrollback), and the end of the current wave. - 01M5RGD0RTXYA7FF5K1TH72MGX The daemon keeps its watches in
watches.jsonof its folder, so a restart keeps them. It looks at each watch each minute, but at the watch of a wave alone (01M6GYN9526HAYVKJC4HS858TH). When an operation ends, with a success or a failure, the daemon posts one message to the lead of the repository: the result, the failed jobs and the next step. Then the watch goes, and no second message comes for the same end. A watch whose state riff cannot read goes on. - 01M5RGD0XTR4X1MEV726HMCEGQ The chat line of
riff release publishsays that the CI run of the tag builds, and that riff tells the owner when the deploy waits. The watch of the tag posts the line “waits for your approval” to the owner inriff chat, and a message to the lead, only when the jobDeploywaits forproduction. While it waits, the watch reminds the owner inriff chateach 4 hours. - 01M5RGD12P0GZDDR9HWHP4N0KP When the last open item of the current
wave closes, the daemon posts one message to the lead: “Wave N is
complete”, with the next step
riff release cut X.Y.Z, orriff wave end N --no-release TEXT. A release issue and a tracker are no item. A repository with no check of a release tag gets the next stepriff wave end N. - 01M5SNV75DRFHR26BT2M8243VV Each step of a release posts one note
to each lead of the riff, other than the lead that runs the step:
riff release cut,publish, the wait of the deploy for the approval,approve, the end of the deploy (a pass or a failure),checkandrollback. The note is a message in the threadrelease. It names the version and the step, and says what comes next for that lead. A command posts the note of its own step. The watch of the daemon posts the note of the wait and of the end of the deploy, in the same look that wakes the lead of the release. - 01M5SNV78EBAYY4SZ891DM2MQG
riff release hold vX.Y.Z REASONis the hold of a lead. It posts the reason in the threadreleaseto each other lead of the riff.riff release hold vX.Y.Z --endends the hold of that lead, and posts that to each other lead. Only a lead holds, and only a lead ends its own hold. A hold counts only from a verified message of a session that was the lead when it posted. A lead holds one version once: a second hold of the same version replaces its reason. - 01M5SNV7BC5J58WRZRJ5HS4NSJ
riff release statusshows each hold of the current release and of each deploy that waits, as “the lead of OWNER/REPO asks to wait: REASON”. The chat line to the owner while the jobDeploywaits for the approval, and each reminder, shows each hold of the version in the same words. - 01M5SNV7E775DRY9QY60E8JPJR
riff release approve vX.Y.Zrefuses while a hold of that version stands. It names each hold with its reason, and saysnext:the end of the hold or--over-holds. A person in a plain terminal passes--over-holdsto approve all the same. The lead of the owner never passes it:--over-holdsrefuses in each agent session. - 01M5SSXJNW7CR0E1EE6G4KNFJ4 The command
riff release holdworks in a repository with no[release]inriff.toml: the lead of another repository holds a release of riff, and that repository has no release cycle. It needs no file of the repository and touches nothing on the forge. Supersedes 01M5SMPX8NM75P8E38DGQ17W7B in part: the refusal of eachriff releasesubcommand. - 01M5TMZE0AAER3F5Q8DT3K0AJJ A result of
riff release checkwith no failed check and one or more checks that could not run, for example when the sign-in of gcloud ended, is an unknown result. It gives each other lead its own note in the threadrelease: the checks could not all run, the lead that runs the check checks again after a sign-in on the host, the riff stays paused, and the lead that reads the note waits for the next note. The note says nothing of a failure or a rollback. Only a failed check gives the note of a failed check. - 01M6H12ET0FQ553QHGVF5XJMQ9 A call of
ghin the release commands runs again after a 502, 503 or 504 of GitHub: up to 3 new tries, after 2 s, 4 s and 8 s. A call that still fails gives the error of GitHub and the number of tries. A write that GitHub did before it answered 504 can show twice. - 01M6H12QRZFBDTT5D5VQ4FB4C1
riff release checkprints the result of each check before it writes the comment on the release issue. When the comment fails, the error says that the checks ran, that riff did not close the issue or resume the riff, and namesriff release check vX.Y.Zas the next step. - 01M6H160E2ZK2AM0PCTQG82V2G The watches of one repository that
cannot read the forge give the lead one message for each fault, with
the name of each watch that failed with it. A watch that told the
fault tells it no more until a look reads the forge, and a watch that
starts in a known fault tells nothing. The post has the key
watch-fault-and the start of the SHA-256 sum of the fault, so a wave on more hosts, and a watch that lost what it saw, send the fault one time to the lead of the repository while the message is one of the last 200 of its thread. The text adds no dot after an error that ends with one. Supersedes 01M5RGD0RTXYA7FF5K1TH72MGX in part.
The real-session test
-
01M4ZCFGCY301G2ARFMRXV7XDR
just real-sessionis the real-session test (01M4Z60B8Q8YH6DRSZWPV42CQ0). It makes a scratch issue in the backlog that adds one line to the book, and holdsverify-issue-N. Theriffof the tree runs the issue withriff runand the image thatriff imageof the tree names. The test passes whenriff runends with 0, the home check passes (01M4ZCFGHCS7WJ6CFYBZSX0AT5), the branch of the item has an open pull request that adds the line, and the check runGateof its head starts in 10 minutes. Then the test closes the pull request, deletes its branch, cancels each CI run of the branch that did not end, closes the issue as not planned, frees the hold and removes the folder of the session. It cleans up also after a failed step, and after SIGINT or SIGTERM. -
01M5157V34H7FWFH6S2DRQXRMP Step 4 of the real-session test keeps the verify of the scratch item from the workers, also when a worker runs the test. It makes the label
no-verifywithgh label create --force, and puts it on the scratch issue (01M5157V0P92EBHF4M7N9K0RGR). The step fails whenghcannot. Then it holdsverify-issue-N. A failed hold is a warning. -
01M4ZCFGF6QN6KZEVMBNW9G4EW The first step of the real-session test that fails stops it. Its message names the step by its number and its name, and gives the reason. The exit code is 1, or 130 after a signal.
-
01M504HBQ3Y0123HWGFP99Z6NV A child of a step of the real-session test that ends by SIGINT or SIGTERM counts as a signal to the test: the step fails with
a signal stopped the test, the test cleans up, and the exit code is 130. Only a second signal to the test itself ends it with no clean-up. -
01M4ZCFGHCS7WJ6CFYBZSX0AT5 The home check of the real-session test lists each path of the home before and after
riff run, on the file system of the home, with no link followed. A new path fails the test. A new folder shows as one path. The check leaves out the folders of riff (RIFF_HOME, andriffin each XDG base folder),containersinXDG_DATA_HOMEand inXDG_CONFIG_HOME, the main clone and the worktree of the test,XDG_CACHE_HOME, and each folder of--skip. In the Claude folder of the person (01M51K053WS0KK3E376V6DH6GR), only a new path that names theID8of the session counts. -
01M51K051FR0WN71NJXNA86XWY The home check of the real-session test also sees a change to a file that is there already, but only in the files that riff must not write (01M4Z60B85X62PNYSVJWKXEWH3):
~/.gitconfig,gitandghinXDG_CONFIG_HOME, the shell files (.profile,.bashrc,.bash_profile,.bash_login,.bash_logout,.zshenv,.zprofile,.zshrc,.zlogin,.zlogout, andfish/config.fishandfish/conf.dinXDG_CONFIG_HOME),.claude.json, andsettings.jsonin the Claude folder. Beforeriff run, it keeps the size, the time of change and the inode of each of these files. A file with a new value fails the test, and the message names it. A change of another file of the home does not count. Each path of--skipis left out, also a file. -
01M51K053WS0KK3E376V6DH6GR In the home check, the Claude folder of the person and each file whose name starts with
.claude.jsonbeside it, for example its lock, count as the Claude folder: only a new path that names theID8of the session counts. A changed.claude.jsoncounts only when its text names theID8. -
01M51MQB46AYY39YSAY18NMD8T The home check of the real-session test also leaves out, with no flag,
snapin the home andclaude/locksinXDG_STATE_HOME. The snap apps and eachclaudeon the host write there while the test runs.--skipstays for the other programs. -
01M51SPS50QQ9Y5C6VE26A18FZ The home check of the real-session test also leaves out, with no flag, each path in a folder
.gitexcepthooks,configandconfig.worktree, and the folder.claude/worktreesof each git clone of the home (a folder with.gitin it). The other sessions of the person build and commit in their worktrees, and make new worktrees, while the test runs. A new path in the rest of each clone still counts, also a new hook and a new config file of git. -
01M51X6JA3W2CMV1S5VZEY7FC3 The home check of the real-session test also reads
configandconfig.worktreein the folder.gitof each git clone of the home, before and afterriff run. This includes the main clone and the worktree of the test: the check leaves out the rest of them. A new, a changed or a removed key fails the test, and the message names the file and each key. A key of a section[branch "NAME"]does not count. A change to a file in.git/hooksfails the test too.config.worktreein.git/worktrees/NAME, and the git folder of each submodule in.git/modules, stay left out. -
01M51X6JCHZB8HJ8C8M3J1G6GZ When the home is itself a git clone, the home check of the real-session test leaves out
.claude/worktreesin the home, as in each other clone. -
01M4ZFPYNGFF49WWHR8EBWSTNG Before
riff run, the real-session test startsriff daemonof the tree in a process group of its own, and waits up to 10 seconds untilriff daemon statusanswers. When a daemon of the test answers before the start, the step fails (01M53YP9DATAD95EZEZ6R75WEF), and the test does not stop that daemon. The clean-up stops the daemon of the test with SIGTERM, and kills it when it does not end in 10 seconds. -
01M53YP9DATAD95EZEZ6R75WEF The real-session test runs its riff daemon beside the daemon of the machine. Each
riffthat the test runs getsRIFF_DATAset toreal-sessionin the data folder of riff (01M53YP97S9BDMFBN0X5G444MY). So the daemon of the test has its own socket and its own session folders, and keeps them for the next run. The daemon of the machine keeps running. The test does not talk to it, and no step of the test or the book stops, starts or changes it. Step 5 fails only when a daemon of the test answers before the start. -
01M4ZCFGKDKH16KGKR5NBEK3Y4 Before each tag, an admin runs
just real-sessionon the merge commit of the release pull request, on a host with podman. It passes before the tag. -
01M59235BBXEKPZBRH23C39574
riff relay watch --followin a session asks the daemon, on the socket of the session, for each wake of the session. It prints one line for each wake, once for each message, and never ends. While the daemon is away, it tries again. The lead starts it once with the Monitor tool of Claude Code: each line wakes the session. The model does not start it again. The instructions of the lead say so. -
01M5924C8BTT6CW05Q0YZJ1GCY
riff release test vX.Y.Z --host HOSTasks the riff daemon of HOST to run the real-session test on the newest commit oforigin/mainfor a merged release that no pass covers. The daemon clonesorigin, detaches at the commit, sets the commit statusriff/real-sessiontopending, replies, and runsjust real-sessionin the clone. At the end it setssuccessfor exit 0, elsefailure, also after a signal. It runs one test at a time. The daemon acts only on the person and the lead of its user: riff refuses a worker, and the daemon refuses each other session. The daemon is not the author of any item.riff release statusshowsreal-session test: running|passed|failed|none on COMMITfor a merged release.just real-sessionstays for a person. The check of a reading that a container cannot do, for example the VmLck reading of #1003, is the same mechanism with a name for the check: not built. -
01M5MR8CM887EFGMKZM5BQA9JJ The riff daemon runs the real-session test of
riff release test --hostwithRIFF_SERVERset to the riff-server of the repository of the request, fromserved.jsonof the daemon. Soriff runandriff plan holdin the test reach the riff-server of the repository, also when it is not on the host. -
01M5MR9FGEBKQWHH7RZ9THAHQB The home check of the real-session test leaves out DATA/gvfs-metadata, for example ~/.local/share/gvfs-metadata: gvfsd-metadata of GNOME writes there when the desktop touches a file.
-
01M5NBZRJ71MREAJCPNVFYVKCM While the real-session test of a merged release runs,
next:ofriff release statusandriff release testsays to wait for the test on its commit. After a fail on the newest commit, it names the fix. After a pass, it namesriff release publish. -
01M5NC1CANKNF7JWVVFPX1X340 When the real-session test of
riff release testfails, the riff daemon writes the failed step and the last 30 lines of the output of the test, with no secret, as a comment on the release issue. It sets the description of the statusriff/real-sessionto the failed step. So the lead in a container reads the reason with no access to the journal of the host.riff release testgives the tag to the daemon for the issue. -
01M5ND3M84D8MHQ66M9P4DFNS1 The real-session test uses a riff-server of its own, never the shared one. At the end of step 1 it starts the riff-server of the tree, from the folder of the riff binary of the tree, in memory, on a free loopback port. It gives the address as RIFF_SERVER to the daemon of the test and to each riff of the test, whatever RIFF_SERVER of the person says. The clean-up stops that server last. So a release of a new major line passes while the shared riff-server runs the line before.
-
01M5NNQ24F2PX74859CV7SMFBZ At the end of step 1, the real-session test resumes the whole riff on the riff-server of the test with
riff resume --riff: a new riff starts paused and refuses each claim. A failed resume fails step 1. -
01M5QVBTXE8YRY44A1C1VP71QS The riff daemon shows the real-session test that it runs, with its tag, its commit and its minutes.
riff daemon statusshows it on a line of its own: “real-session test of TAG on COMMIT, for N min”.riff top,riff whoandriff viewshow it on the line of the host.Status::busycounts the test as one, so the daemon starts again on a new riff only after the test.
The UIs
The UIs are the design of #982: one Rust core library, and a child design for each UI. The book page Design: the UIs of riff has the pictures. A requirement of this part holds when the build item that builds it merges.
- 01M54215XK0QAM58RBRMS8F4ZX Each UI of riff is Rust: the CLI, the TUI, the phone app, the web UI and the desktop GUI. The riff project writes no JavaScript, uses no JavaScript framework, writes no UI in HTML and CSS, and uses no npm tooling. A Rust app built to WebAssembly can run in a browser, when its Rust toolkit makes the small HTML, JavaScript or CSS that starts it.
- 01M542160DDTZMMDCZYZZ0Q4WC The phone app is for Android (#951). riff-server stays on Google Cloud Run while the UIs come. A server part of a UI runs on Cloud Run, in the Google Cloud project of riff-server.
- 01M5421633BKPPMANYRVZ76BVP Each UI shows the sessions in one view:
the sessions on riff-server and the container sessions of the
daemons, in one list. In the TUI, the sessions tab replaces the live
table of
riff topand the view ofriff view, with each of their actions, also the attach to a session. - 01M542165T96VW72HAWFXVD160 The CLI stays complete: it serves
scripts, agents through the MCP server, and hooks. Each command prints
plain lines that
grepcan find. - 01M542168PYP1WH78D3D9DKEK6 Each command that prints data takes
--format text(the default) or--format json. serde makes the JSON from the types of the data. It has the same facts as the text. - 01M54216BCJQTQFKK5SD1PRJK9 When the TUI ships,
riff topprints one look of the sessions and ends, asriff top --oncedoes. The live view is the sessions tab of the TUI. The TUI design picks a plain alias ofriff top, or none. - 01M54216E2GY49GFV5SNPWTBEB
riffwith no command starts the TUI in a terminal. With no terminal, it prints the help of the CLI.riff --helpstays the help of the CLI, and says thatriffwith no command starts the TUI.riff COMMANDstays the CLI. When the TUI design finds that a bareriffmakes the CLI awkward, the TUI isriff ui. - 01M54216GT8SX6NVMKRGNR2H0T The TUI is a ratatui app with a tab for each area: the sessions, the chat, the waves (the items, the claims, the pull requests and the verifies), the machines, the devices, and the reports (the usage, the audit and the log of a session). Each action of the TUI calls the same code as its CLI command.
- 01M54216KMTGS7KD1R3RY5JKQ0 The crate
riff-clientis the core library of each UI. The client of riff-server (the sign-in, the tokens, the API calls and the watch stream) and the models of the views (the sessions, the threads, the waves and the machines) are in it, not in the crateriff.riff-corestays the model that the clients and riff-server share. - 01M54216PBE319RASGNMY9F0MR
riff-clientholds no terminal code. It builds for Linux, for Android (aarch64-linux-android) and for WebAssembly (wasm32-unknown-unknown). The Gate builds it for each of these targets, and fails when it depends on a crate of the terminal, for example ratatui, clap or rustyline. - 01M54216S51X5VBXN3YAERW6J6 Each part of
riff-clientthat is not the same on each platform is a trait that each UI gives, for example the store of the tokens and of the device key. The socket of the riff daemon stays in the crateriff. A terminal UI gives the container sessions of its daemon to the sessions model ofriff-client. - 01M56S4M0BSV0KP825XSDNNXNX A UI gives its platform parts to
riff-clientone time, at its start. The store of the tokens and of the device key is a store ofkeyring-core. A process that gives no platform gets the defaults: no OS keyring, no local folder, and plain lines on stderr. - 01M56S4M2NMATA3XZAMPSRAW8X The browser step of
riff login(the local port of the OAuth redirect) stays in the crateriff.riff-clientswaps the ID token of the provider for a riff sign-in. - 01M56Y8KAPHJMSK89ZW1PA8B0E
riff-clientholds the models of the views as plain data with serde: the sessions, the threads, the waves and the machines. It makes each model from the replies of riff-server, and from what the UI gives: the container sessions of its riff daemon, and the reads of the forge. A UI only draws a model. The plain lines, the colors and the ratatui widgets stay in the crateriff. - 01M56Y8KD05JH2J7AMK636WGT7 The sessions model has one entry for
each session ID: a session of riff-server, a container session of
the riff daemon of this machine, or both. An entry with both has the
state of its container.
riff topgives the model no container session, andriff viewgives it no session of riff-server. Each draws its part of the model. - 01M56Y8KF7EDWHX1JE5RA0XMCW The machines model holds the numbers
that a host tells in its status line: the limit, the floor, the
workers, and the numbers of its machine, its disk and its monitor.
riff-clientwrites and reads that line. The reads of this machine (/proc, the disk, the settings and tmux) stay in the crateriff. - 01M56Y8KHD61NXC57V7H546KWW The threads model has one line for
each message: its number, its time, its sender, its receivers with
the mark of the whole thread, the mark
verified, the lead mark of a verified lead, its kind and its body.riff read,riff tailandriff chatdraw these lines. - 01M54216VVY6072F2X2PM2Z0DQ The CLI, the TUI, the MCP server and
each graphical UI use
riff-clientfor each call to riff-server. - 01M54216YKW1X70KDV4TQCQJTE The graphical UIs (the phone app, the web UI and the desktop GUI) use Slint, and share their screens. Each child design checks that Slint does its platform today.
- 01M5421718PRVCDJP42J8X5J3E The web UI runs on Cloud Run, at one address with the API of riff-server, so the browser needs no CORS. The start is a Cloud Run service of its own for the web app, behind a router (for example the rewrites of Firebase Hosting, or a load balancer with a URL map), so a deploy of the UI does not restart riff-server. The web design compares it with a riff-server that serves the web app itself, and picks one.
- 01M542179DFVKMFQ008TV9DM8C Each UI that signs in is a device of
the person, with the device commands of the phone design
(01M53XPZTG203SNM9MBKBERMB5):
riff device list,riff device addandriff device remove. The devices tab of the TUI shows each device with its kind, its name, its scope and its last use. It adds a device, and draws the QR code withtui-qrcode. It removes a device. Each graphical UI has a devices view that lists and removes devices, and adds a device where it can show a QR code. - 01M54217C256KCGWSVAVPHEQQB The UIs come in this order: the TUI, the phone app, the web UI and the desktop GUI. After the core library, the TUI and the phone app can be built at the same time: the TUI does not block the phone app.
- 01M54217HF6JK1KYK4Q11551KR Each UI has a child design issue that links #982: the TUI (#994), the phone app (#951), the web UI (#995) and the desktop GUI (#996). The build items of a UI come from its child design, after Mike signs it off.
- 01M594XQJMXAXY3JXP66Y8EVQ7
riff release approve vX.Y.Zalso works for the lead of the owner, also in a container, when riff-server holds a verified message of the owner or of an admin in the threadchat, from a person and not a session, that comes after the publish line ofriff release publish vX.Y.Z, names the version, and saysapproveordeploy. Code reads the thread from riff-server. The lead asks no yes or no. The approval comment names the message number. It refuses a message of a session, of another version, before the publish, or of a member, and a worker, a work session in a container, and a session that is not the lead. In a plain terminal it works as before. When the auto mode checker blocks a release command,riff denialshas a record, and the lead files an issue for it.
The notes of the lead
- 01M59258K3TYR09HC4AWHWZPEX riff-server keeps the notes of the lead
for each user and each repository thread: one text, with the record
notes_set(thread,user,text) and the commandnotes(POST /v1/notes/set). A new text replaces the old text. Each change is a record of the log, and the server keeps each old text in the history, also in the checkpoint. The same text makes no record. The text has 64 KiB at most. A longer text getsbad_requestthat says the limit. - 01M59258NTXPMWV18TKRKWNTYN The user of the lead and the owner
of the riff read the notes (
POST /v1/notes, with the history when asked). Another member getsnot_allowed. The person of the user and a session of the user that is not a worker write the notes of the user. A worker getsnot_allowed. - 01M59258RJ3CG498Z8AVQ5TV5Q riff refuses a notes text that looks
like a token (a known prefix such as
sk-ant-orghp_, a private key, or a long run of letters and digits with both). The refusal does not repeat the text. - 01M59258VCNV9AMGMQTDH936SK
riff lead notesshows the notes of the person in the repository thread, with--historythe old texts, with--userthe notes of another user (owner only), and with--threadanother repository thread.riff lead notes editopens$VISUAL,$EDITORorvion the notes, andriff lead notes edit --file FILEtakes the new text from a file (-is the standard input). - 01M59258Y62VSAHDRRY7765RZJ The MCP tool
notesreads the notes of the lead with notext, and replaces them with atext. It is inLEAD_TOOLS, and not inCONTAINER_TOOLS: only the lead has notes. The instructions of the lead name each tool ofLEAD_TOOLS, and a test checks it. - 01M592590XREH99ABQF2D2KT6H The start text of the lead sends it to the notes and to the unread messages at each start, after each resume and after each compaction. It says to keep the running state in the notes and on GitHub, never only in the Claude memory of the host.
- 01M592593NSC1DE7A653Q8PCPS The instructions of the lead
(
text::MCP_LEAD) hold each rule of the lead (text::LEAD_RULES). No rule stays only in the Claude memory of one host, and a test checks each rule. A rule for each agent of the repository goes inCLAUDE.mdor here. A choice of one person or one host, and the running state, go in the notes of the lead. - 01M596DH64SQ5AJ4Y6BETGTPSK
riff lead move HOSTmoves the lead of the repository to HOST with no other step by the person. It asks the old lead to write its handover to the notes, and waits at most 5 minutes. A lead that writes none in time gets a warning, and the move goes on. It asks the daemon of HOST for the new lead, and then sends the old lead a direct message that it is not the lead any more. It prints where the person attaches. A session that is not the lead refuses it, and so does a HOST with no daemon, and the old lead stays the lead then. The lead itself writes its handover first, because it cannot answer its own message. - 01M596DH8G4A5XQFHJFBX1X379 The request
lead moveto the daemon of a host (remote::Request::LeadMove) starts the lead of the repository there, asriff lead startdoes and with no attach, when none runs. Its first prompt sends the lead to the handover in the notes (text::lead_moved). The daemon then makes the new session the lead (R177) and replies with its session ID. Only the person and the lead of the person act on it. - 01M5A5HXKZTDP5M5HY2FHCEBZA A session of a wave that ends with no
open pull request leaves one record as a note in the thread
endson riff-server (ends::post). The first line has the tagsitem,session,code,failedanddenied, then a colon and the reason in words. The lines after it are the last 50 lines of its output, each with no secret and at most 300 characters. The session leaves the thread after the post. A session with an open pull request leaves no record. - 01M5A5HXP9X3AXTS6N3A57DMQM
riff session why ITEMreads the threadendsand shows the last record of ITEM, and how many records it has. It works in a container: the lead reads the cause there, with no look at the host. An item with no record says so. - 01M5A5HXRH4PFQDFA7XPFFRHWS The message “started N times” to the
lead names the reason of the last end of the item, and sends the lead
to
riff session why ITEM. - 01M5SMP2D4MGME90EHVKAQHQ26 A verify session of a wave that ends
with no PASS or FAIL leaves one record in the thread
endstoo (wave::after_verify, #1441). It has the same first line and the same last lines as the record of a build, with the itemverify-issue-N. Its reason says that no PASS or FAIL came, not that no pull request came. The post goes before the folder of the verify goes.riff session why verify-issue-Nshows it. A verify with a PASS or FAIL leaves no record. - 01M5SMP2FFEHJPZBJVGAWBXNNP
riff daemon events NAMEin a container, withRIFF_SOCKETand noRIFF_SERVER, asks the daemon on the socket of the session (ServerCall::Events), one page after the other, asriff daemon events NAME --host HOSTdoes. Only the lead may. With--host HOSTandRIFF_SOCKET, it asks the daemon of HOST through the socket (ServerCall::Host). Neither looks for the socket of the daemon on the host, and neither calls riff-server straight.
Open
None.