johnandersen777.bsky.social/knotless
git that lives in your Atmosphere account
Forked from chadtmiller.com/knotless
0
0
# Command reference
## knotless
`<url>` is a repository's `knotless://<handle or did>/<name>`. A trailing
`/` and a trailing `.git` are dropped, and any other `/` in the name is
refused. A name is 1 to 100 bytes of `[A-Za-z0-9._~-]`, not `.` or `..`.
`<at-uri>` is a pull's, issue's or comment's `at://` URI, as `pull create`,
`issue create`, `pull list`, `issue list` and `pull show` print them.
An error prints `knotless: <error>` and exits 1. One a PDS refused for want
of scope, as a login from before knotless asked for more is, adds a second
line, `knotless: your login is older than what knotless asks for now: run
`knotless login` again to allow this`. One the login asked for, but its PDS
did not grant, adds `knotless: your login asks for this, but your PDS did
not grant it` instead, and for a write in a space goes on to say why: a PDS
grants writes in a space as the lexicon knotless publishes for its type
listed them when you logged in, so once that lists the collection,
`knotless login` again grants it. Until then, any command but `login`
and `logout` ends by printing to stderr `knotless: your login is older than
what knotless asks for now: run `knotless login <handle>` again to allow
all of it`, whether or not it failed.
Once a day, a command asks `KNOTLESS_APPVIEW` for its newest release
(`/releases/latest`) and the oldest version it asks clients to be
(`dev.knotless.describeServer`'s `client`), waiting 3 s at most, and keeps
what it said in `update.json`; an appview that does not answer leaves
what was known. With a newer release out, a command ends by printing to
stderr `knotless: knotless <latest> is out, and this is <version>. Run
`knotless upgrade` to install it.` Older than the appview asks for, any
command but `upgrade`, `login`, `logout` and `help` fails at once, saying
`knotless <version> is older than the <minimum> the appview asks for.
<reason> Run `knotless upgrade`, or set KNOTLESS_IGNORE_MINIMUM=1 to go on
anyway.` `KNOTLESS_NO_UPDATE_CHECK` turns both off. knotless-mcp over
stdio does the same: with a newer release out, its first tool's answer
ends with that line, and older than asked for, every tool fails with that
refusal.
Every command is `knotless <group> <action> [<url>] [arguments]`, and the
groups are singular nouns: `account`, `repo`, `branch`, `tag`, `pull`,
`issue`, `comment`, `label`, `triager`, `queue`, `run`, `workflow`,
`runner`, `agent`, `secret`, `release`, `mod`, `admin`, `org`, `theme` and
`notification`. Only `login`, `logout`, `upgrade` and `status` stand alone.
An action that has an opposite takes `--undo` (`repo star --undo`), never a
command of its own. `knotless-mcp` offers each command as the tool
`<group>_<action>` (`repo_star`, `pull_merge`), with the same arguments:
[MCP tools](mcp.md) lists them by group. A command with no tool acts on
this machine: `login`, `logout`, `upgrade`, `repo mirror`, `workflow pin`
and `release download`.
The names the commands had before groups (`knotless pull`, `knotless
merge`, `knotless checks`, and the rest) still work for one release, but
usage does not list them. [The old names](#the-old-names) say what each
became.
### Session and setup
| Command | |
| --- | --- |
| `login <handle> [--password-stdin]` | log in with OAuth in a browser, or with an app password read from standard input (or `KNOTLESS_PASSWORD`) |
| `logout <handle>` | drop the session, revoking its tokens |
| `upgrade` | install the newest release over this one, running the appview's `/install.sh` into `KNOTLESS_INSTALL_DIR`, or else the directory this binary is in, and printing what it said (`knotless: installing <version> for <target> into <dir>`). Refused where a package manager put it: `~/.cargo/bin` (`cargo installed knotless here: upgrade it with cargo`), a Homebrew `Cellar` or `/usr/bin`. With no appview, fails `an appview hands out releases, and none is set: set KNOTLESS_APPVIEW`; with one that hands out none, `<appview> hands out no releases` |
| `status [<server>] [--repo <handle>/<name>] [--json]` | a `knotless-serve`'s health, from `/_status`: its feeds, index, a mirror, its disk's room and its code search index; the server defaults to `KNOTLESS_APPVIEW` |
`status` fails with `<server> is unhealthy` when `/_status` says it is
not, and with `status takes the server's URL: KNOTLESS_APPVIEW is set empty` when it
has neither.
### account
| Command | |
| --- | --- |
| `account delete-data <handle> [--yes]` | delete every knotless record from your account ([storage.md](../design/storage.md#deleting-your-knotless-data)): prints your repositories, one line each, `repository <name>`, then ` (private)` and `, shared with <n>` where they apply, then each collection's count and name, tab-separated, and `<n> spaces`; asks you to type the handle again unless `--yes`, and refuses anything else (`nothing deleted: …`); then prints `<n> deleted` as it goes, has the appview forget you, and prints `deleted <n>, and logged out`. Run again, one cut short carries on. An OAuth login from before this asks for it with a new scope: log in again |
| `account delegate <handle> <gateway did>` | let a gateway write for you, so collaborators can push through it |
| `account profile [--name <text>] [--bio <text>] [--pronouns <text>] [--link <url> [--title <text>]]... [--avatar <file>] [--bot \| --no-bot]` | how you show on knotless, over your Bluesky profile; `''` shows Bluesky's field again, and `--link ''` its links; the avatar is a PNG, JPEG, WebP or GIF, made smaller when over 1 MB; `--bot` says the account is a bot, which its profile marks and the feed leaves out, printed as `bot: yes`, and `--no-bot` takes that back |
| `account email` | how many addresses you claim: `<n> addresses claimed`. Each is kept only as a hash salted with your DID, so none can be listed |
| `account email add <address>` / `account email remove <address>` | claim, or take back, an address your commits are authored as, printing `<address> added`, `removed` or `unchanged`; one with no `@` fails `"<address>" is no email address` |
| `account follow <handle> [--undo]` | follow an account on knotless (a `dev.knotless.follow`, not a Bluesky follow), printing the follow's `at://` URI; one already followed keeps its follow; yourself fails `you can't follow yourself`. With `--undo`, take your follows of it back, printing `unfollowed`, followed or not |
| `account follows [<handle>]` | the accounts you, or `<handle>`, follow, newest first: handle and when, tab-separated |
| `account followers [<handle>]` | the accounts following you, or `<handle>`, newest first, a handle a line, as the appview counts them; with no appview, fails `an appview counts followers, and none is set: set KNOTLESS_APPVIEW` |
| `account search <text> [--limit <n>]` | the accounts owning public repositories whose handle holds `<text>`, in any case, as the search on the appview's pages finds them (`dev.knotless.searchOwners`): those whose handle starts with it first, at most `<n>` (10 by default, at most 50), each handle and how many public repositories it owns, tab-separated; with no appview, fails `an appview finds accounts, and none is set: set KNOTLESS_APPVIEW` |
### repo
| Command | |
| --- | --- |
| `repo create <url> [--private] [--description <text>] [--link <url> [--title <text>]]... [--default-branch <name>] [--runner <handle>]... [--no-runner]` | create an empty repository; the default branch is `main` unless named; each `--link`, an http or https address, is one its pages show, in order, its website first, up to five, called `--title` if one follows it; `<url> already exists` if it does. Its `runners` are those `--runner` names, none with `--no-runner`, and otherwise those the appview names for new repositories (`dev.knotless.describeServer`); a repository a first push makes names those too. A private one's are made members of it and invited at the appview, so that they find it, each printed as `<handle> reads it, to run its checks: …`; one that could not be invited prints `warning: <did> was not told of it: <why>`. In an organization, says how many of its members may push to it: on a public one, its public members, as a hidden member's merges count only where readers can see them. An admin of an organization makes one in it by naming it in `<url>`, through its gateway; forking, moving or linking one there takes the organization's own login |
| `repo list <handle>` | list an account's repositories |
| `repo fork <url> [<name>]` | copy a repository into your account, under its name unless one is given; one's own fails `you own <url>: a fork is of someone else's repository`, and a name you already have fails `<url> already exists` |
| `repo edit <url> [--description <text>] [--link <url> [--title <text>]]... [--topic <name>]... [--default-branch <name>]` | as the owner, change what a repository says it is (`''` drops it), the links its pages show (the `--link`s given replace those it had, as `account profile`'s do, and `--link ''` drops them), its topics (the `--topic`s given replace those it had, each lowercased and named once, of letters, digits and dashes, at most 50 long and 20 in all; `--topic ''` drops them), and the branch its HEAD names, one of its branches once it has any, printing `description: <text>`, `default branch: <name>`, `topics: <name>, …` when it has any, and a `link: <url> (<title>)` line for each link |
| `repo rename <url> <new name>` | rename; the old name redirects; `<new url> already exists` if another repository has the name |
| `repo archive <url> [--undo]` | as the owner, make a repository read only, printing `archived <url> at <time>`, or with `--undo` writable again, printing `unarchived <url>`. While it is archived, pushes and commits to it are refused, by `knotless` and by the gateway, and so are pulls, issues and comments about it, each with `<name> is archived and read only. Its owner can unarchive it.`; runners leave it. One already as asked fails `<url> is already archived` (or `not archived`) |
| `repo delete <url> --yes` | delete a repository and everything in it: its records, and what you wrote about it (your pulls, issues, comments, labels, star and reactions on it); run again, a delete cut short carries on |
| `repo transfer <url> <handle or url>` / `repo transfer <url> --undo` | offer a public repository to another account, or with `--undo` withdraw the offer |
| `repo accept <url>` | take a repository offered to you, its pulls and issues included |
| `repo share <url> <handle> [--write] [--undo]` | let an account read a private repository, or push too, printing `shared`, and leave it an invite at the appview (`KNOTLESS_APPVIEW`), by which an agent watching your repositories (`knotless-agent --owner`) finds it; with `--write` before you have delegated your writes to a gateway, delegates them to the appview's first, as `account delegate` does, approved in the browser, printing `delegated to <did>` before it. With `--undo`, take access away |
| `repo members <url>` | who may read and push, a line each: the DID, then `owner`, `write` (a collaborator), `member` (of the organization the owner is) or `read`, tab-separated |
| `repo star <url> [--undo]` | star a public repository, printing the star's `at://` URI; one already starred keeps its star; a private one fails `<url>: a private repository can't be starred, as a star is public`. With `--undo`, take your star back, printing `unstarred`, starred or not |
| `repo stars [<handle>]` | the repositories you, or `<handle>`, starred, newest first: `knotless://<handle>/<name>` and when, tab-separated |
| `repo compact <url> [--due]` | as the owner, fold its packs into one, printing `<n> packs, <n> folded, <n> old claims dropped, <n> deleted refs' records dropped`: the last, the records clients before deletes removed them left of deleted refs, once a day old. With `--due`, as the owner's `git push` runs it, only once it holds `KNOTLESS_COMPACT_THRESHOLD` packs; under that, it counts them and folds nothing |
| `repo storage <url> [--json]` | what it takes up on its PDS, from the pack records alone: `<n> packs, <n> objects, <size>, <size> in blobs`, or `{packs, objects, bytes, blobBytes}` |
| `repo mirror <url>` | sync the cache mirror and print its path |
| `repo refs <url>` | print the refs, in one round trip |
| `repo tree <url> [<path>] [--rev <rev>] [--json]` | a directory's entries at the default branch or `<rev>`, from the cache mirror with no clone: a line each of the mode, `blob` or `tree`, the object's SHA, its size (`-` for a tree) and its name, tab-separated |
| `repo read <url> <path> [--rev <rev>]` | a file's bytes at the default branch or `<rev>`, from the cache mirror with no clone, printed as they are |
| `repo log <url> [--rev <rev>] [--path <path>] [--limit <n>] [--json]` | the commits reachable from the default branch or `<rev>`, newest first, optionally only those touching `<path>`, at most `<n>` (20 by default), from the cache mirror with no clone: a line each of the commit's first characters, when it was written, its author's name and its subject, tab-separated |
| `repo search <url> <pattern> [--rev <rev>] [--limit <n>]` | the lines holding a fixed string, in any case, at the default branch or `<rev>`, at most `<n>` (100 by default), as `path:line:text`, read from the cache mirror with no clone; an empty pattern fails `an empty search` |
| `repo blame <url> <path> [--rev <rev>]` | who last changed each line of a file, at the default branch or `<rev>`, read from the cache mirror with no clone: a line for each run of lines one commit last changed, as `<first>-<last>`, the commit, its author's name, when it was written (UTC) and its subject, tab-separated |
| `repo diff <url> <base> <head>` | what `<head>` changed since it forked from `<base>`, as `git diff base...head` shows it, with no clone |
| `repo commit <url> -b <branch> -m <message> [options]` | commit without a checkout (below) |
| `repo site <url> [<dir>] [--fallback <path>] [--not-found <path>] [--commit <sha>] [--check <at-uri>] [--down]` | as owner, deploy `<dir>` as the repository's website: its files, uploaded as blobs but for those the live site holds already, then its `dev.knotless.site` record, replaced (see [pages](../design/pages.md)). Prints `uploaded <n> of <m> files`, then the address it is served at, `https://<owner>.knotless.page/<repo>/`. `--fallback` names the page a single-page app answers every other page with, `--not-found` the one a missing path gets, with 404; each must be one of the files. `--commit` is `KNOTLESS_RUN_SHA` unless given. With no `<dir>`, the live site: its address, files, commit and when it was deployed, tab-separated, or `<url> has no site`. `--down` takes it down, printing `taken down`. A site holds 1 to 2,000 files, each path at most 512 bytes, and no links. A private repository's site is kept in its space and served to its members at `https://<repo key>-<plc id>.private.knotless.page/`; an owner with no `did:plc` fails `a private site's owner is named by a did:plc` |
`repo commit` takes:
| Option | |
| --- | --- |
| `--put <path> <file>` | write `<file>` at `<path>`; `-` reads standard input |
| `--put-exec <path> <file>` | the same, as an executable (mode 100755) |
| `--delete <path>` | remove `<path>` |
| `--edit <path> <old> <new>` | replace `<old>`, which the file must hold exactly once, with `<new>`, in the file as the branch holds it, keeping its mode; edits to one path are made in turn. A change to part of a file need not send all of it |
| `--author <name> <email>` | the author; by default whom git commits as here (`user.name`, `user.email`), or, where git has none, your handle and `<did>@knotless.invalid`. The committer is the same as the author |
| `--base <commit>` | the commit the changes were made against; a path changed since then is a conflict |
| `--batch` | as the owner, go through the repository's gateway, which batches concurrent commits to the branch |
| `--retries <n>` | how many times to replay onto a branch that moved (default 5); only a `fetch first` refusal is replayed |
| `--json` | print `{sha, parent, attempts, changed}` instead of the commit's hash; through a gateway `attempts` is 1 |
A path is relative, with no empty, `.` or `..` segment, no NUL or newline,
and no `.git` segment in any case. A change that leaves the tree as it was
pushes nothing (`changed: false`). With `--base`, a path changed since then
fails `conflict: <paths> changed since <base>`. An edit whose `<old>` is not
in the file fails `edit <n> of <path>: old is not in the file`, and one it
holds more than once says how many times; an edit to a file not there, or
not a regular file, fails `<path>: no file there to edit`. When the branch
moved, edits are made again in the file as it is there.
### branch
| Command | |
| --- | --- |
| `branch create <url> <name> [<from>]` | create a branch at the default branch, or at a branch or commit, and print its commit; `<url> already has a branch <name>` if it exists |
| `branch delete <url> <name>` | delete a branch; refuses the default branch (`<name> is <url>'s default branch`) and one that does not exist (`<url> has no branch <name>`) |
| `branch rename <url> <name> <new name>` | rename a branch, printing `renamed <name> to <new name>`: the new one starts where the old one is, each open pull into it is pointed at the new name, printed `retargeted <at-uri>`, and the old one is deleted. Refuses the default branch. Needs an appview, which lists the pulls |
| `branch protect <url> <branch> [--merges] [--queue] [--reviews <n>] [--code-owners] [--dismiss-stale] [--gate <handle>...]` | hold collaborators to moving the branch forward, never deleting it, and, without `--merges`, adding no merge commits; with `--queue`, pulls land on it only through the merge queue (see [queue-only branches](../design/merge-queue.md#queue-only-branches)); with `--reviews <n>` (0 to 10), a pull merges into it only once `<n>` of the owner and collaborators other than its author approve it, with `--code-owners` only once an owner of each path it changes does, and with `--dismiss-stale` only approvals of its head as it is now count, and with `--gate <handle>`, given once for each, it waits for that account's review of its head, its request for changes blocking it, though it may not write (see [gates](../design/collaboration.md#gates)) (see [required reviews](../design/collaboration.md#required-reviews)). It replaces any rule the branch had |
| `branch protect <url> <branch> --undo` | drop a branch's rule |
| `branch rules <url>` | the protected branches, a line each: the branch and `linear` or `merges`, then `queue` when it takes pulls only through the merge queue, then `reviews <n>`, `code owners`, `dismiss stale` and `gate <did>` for each gate as it asks for reviews, tab-separated; `branch protect` prints the same |
### tag
| Command | |
| --- | --- |
| `tag create <url> <name> [<from>]` | tag the default branch's commit, or that of a branch, tag or commit, and print it; `<url> already has a tag <name>` if it exists. A lightweight tag, naming the commit |
| `tag delete <url> <name>` | delete a tag, printing `deleted <name>`; `<url> has no tag <name>` if there is none |
### pull
A pull's `<at-uri>` is the one `pull create` and `pull list` print.
| Command | |
| --- | --- |
| `pull create <url> <base> <head url> <branch> <title> [-m <body>] [--image <file>]... [--draft] [--reviewer <handle>]...` | open a pull to merge `<branch>` of `<head url>` into `<base>`; `-m -` reads the body from stdin; each `--image` uploads a picture and shows it after the body, by its file's name, at the size its density says ([collaboration.md](../design/collaboration.md#pictures)); `--draft` opens it as a draft, which neither merges nor queues until marked ready; each `--reviewer`, by handle or DID, asks an account to review it, which it hears of once the pull is not a draft ([drafts and requested reviewers](../design/collaboration.md#drafts-and-requested-reviewers)). Asking yourself fails |
| `pull list <url> [-q <query>]` | pulls, newest first; `-q` filters them in GitHub's search words ([collaboration.md](../design/collaboration.md#filtering-pulls-and-issues)) |
| `pull show <url> <at-uri>` | a pull or issue and its comments, each followed by a `reactions: +1 2 heart 1` line when anyone reacted, as the appview counts them, or for a private repository as its space has them |
| `pull diff <url> <at-uri>` | the unified diff a pull would apply: its head, in whichever repository it is, against where it left its base |
| `pull template <url> [<base>]` | what the repository asks a pull's title and body to follow, into the default branch or `<base>` ([templates](../guide/collaborating.md#write-from-the-repositorys-templates)) |
| `pull edit <url> <at-uri> [--title <title>] [-m <body> [--image <file>]...] [--draft \| --ready] [--reviewer <handle>]... [--no-reviewer <handle>]...` | change your pull, issue or comment; `-m ''` drops a pull's or issue's body, `-m -` reads it from stdin; the pictures it keeps are those the new body shows. For a pull, `--draft` makes it a draft and `--ready` marks it ready, asking its reviewers again as of now; `--reviewer` asks another account to review it and `--no-reviewer` stops asking one. Any of these on an issue or comment fails `<at-uri> is not a pull` |
| `pull close <url> <at-uri>` / `pull reopen <url> <at-uri>` | change an item's state, as its author, the owner or a collaborator; refuses an item that is not there, a merged pull and a state it is in already |
| `pull label <url> <at-uri> <label>... [--undo]` | as owner, collaborator or a triager of their scope, put labels on a pull or take them off with `--undo`; prints those it carries now |
| `pull assign <url> <at-uri> <handle>... [--undo]` | as owner or collaborator, assign a pull to accounts, or with `--undo` take them off it; prints those it is assigned to now ([collaboration.md](../design/collaboration.md#assignees)) |
| `pull react <url> <at-uri> <content> [--undo]` | react to a pull, issue or comment with `+1`, `-1`, `laugh`, `hooray`, `confused`, `heart`, `rocket` or `eyes`, printing the reaction's `at://` URI; one already there is kept; `--undo` takes it back, printing `taken back`; on a private repository the reaction is written in its space, as a comment is |
| `pull ask-review <url> <at-uri> <handle>... [--undo]` | ask accounts to review a pull, or with `--undo` stop asking them: as its author, in the pull, as `pull edit --reviewer` does, or as owner or collaborator, in your own record. Each stops asking only those it asked, and is told who asked when it did not. A required reviewer, a gate of the rule protecting the pull's base, is asked of every pull and stays asked until the rule stops naming it, so unasking one fails. Prints those asked now |
| `pull dismiss-review <url> <at-uri> <handle or review at-uri> [<reason> \| -m <reason>]` | as owner or collaborator, dismiss that reviewer's standing review of a pull, named by handle or by the review's `at://` URI, saying why (`-` reads the reason from stdin; `-m <reason>` may stand for it): a comment naming it, so it counts for nothing, for or against, until they review again. Prints the comment's URI; `pull show` then marks the review `, dismissed` |
| `pull retarget <url> <at-uri> [--base <branch>] [--head <branch>]` | point a pull at other branches; the head only by the pull's author |
| `pull merge <url> <at-uri> [--keep-branch] [--wait <secs>] [--force]` | merge a pull; `--wait` waits for its checks, and `--force`, the owner's alone, merges past them (see [the merge gate](#the-merge-gate)). Into a branch protected with `--queue`, it queues the pull instead, unless forced |
| `pull merge <url> <at-uri> --queue` | put a pull in its branch's merge queue, and return, printing `queued into <base>: <n> of <of>`; refused unless the branch it lands on is protected with `--queue`; takes no `--keep-branch`, `--wait` or `--force` |
| `pull merge <url> <at-uri> --auto [--undo]` | set a pull to merge by itself once its checks pass, on a branch with no merge queue, printing `set to merge when its checks pass, at <head>`: the runner that lands the repository's queues merges it then, keeping its branch. A push by anyone who may not merge it, or retargeting it, cancels it; `--undo` cancels it, as the pull's author may too. Takes none of `--queue`, `--check`, `--keep-branch`, `--wait` and `--force` (see [merge when checks pass](../design/collaboration.md#merge-when-checks-pass)) |
| `pull merge <url> <at-uri> --check` | merge nothing: say whether it would merge now, how, and what you may do to it, as a pull's page's merge box shows it |
| `pull revert <url> <at-uri> [-m <reason>]` | as owner or collaborator, undo a merged pull: one commit on a new branch `revert/<rkey>`, cut from its target branch, and a pull of it into that branch, titled `Revert "<title>"`; prints the new pull's URI. See [reverting a pull](../design/collaboration.md#reverting-a-pull) |
| `pull update-branch <url> <at-uri>` | as owner or collaborator, merge an open pull's base into its branch, in the repository itself, and push it; where both sides only added lines at the same place, the base's come first. Prints `merged its base into <branch>: <commit>`, then `both sides only added lines in <path>: the base's come first` for each such file, or `<branch> holds its base already`. Refused on any other conflict, naming its paths. See [updating a pull's branch](../design/collaboration.md#updating-a-pulls-branch) |
### issue
| Command | |
| --- | --- |
| `issue create <url> <title> [-m <body>] [--image <file>]...` | open an issue |
| `issue list <url> [-q <query>]` | issues, newest first; `-q` filters them in GitHub's search words |
| `issue template <url>` | what the repository asks an issue's title and body to follow |
| `issue show`, `issue edit`, `issue close`, `issue reopen`, `issue label`, `issue assign`, `issue react` | as `pull show`, `pull edit`, `pull close`, `pull reopen`, `pull label`, `pull assign` and `pull react` are, for an issue; the arguments are the same, less the pull's `--draft`, `--ready`, `--reviewer` and `--no-reviewer`, which fail `<at-uri> is not a pull` |
### comment
| Command | |
| --- | --- |
| `comment add <url> <at-uri> <text> [--approve \| --changes] [--image <file>]... [--line <path>:<n>[-<m>][:old] <text>]...` | comment, or review: a pull only, and not one you opened. Each `--line` comments on lines of the pull's diff at its head, `:old` for lines it removes, starting a thread; sent with a `<text>` or verdict, as that review's, otherwise on their own (`<text>` may then be `''`); at most 100. Given a comment's URI, answers it in its thread |
| `comment edit <url> <comment-uri> [-m <body> [--image <file>]...]` | change your comment; `-m -` reads the body from stdin; the pictures it keeps are those the new body shows |
| `comment delete <url> <at-uri>` | take back your comment or review |
| `comment react <url> <comment-uri> <content> [--undo]` | react to a comment, as `pull react` does to a pull |
| `comment resolve <url> <comment-uri> [--undo]` | resolve the thread a comment on a pull's lines starts, or with `--undo` open it again: its author, the pull's, the owner or a collaborator |
### label
| Command | |
| --- | --- |
| `label list <url>` | the labels a repository defines: name, color, `exclusive` and description, tab-separated |
| `label define <url> <name> [--color <#hex>] [--description <text>] [--exclusive]` | as owner, define a label (or redefine it) on the repository |
| `label delete <url> <name>` | as owner, take a label away, off everything it is on |
### triager
| Command | |
| --- | --- |
| `triager list <url>` | the accounts, such as a triage bot, whose labels of their scopes count, below the owner's and collaborators', each as its DID and scopes, tab-separated |
| `triager add <url> <handle> <scope>...` | as owner, name an account a triager of those scopes. Named for a private repository, it is made a member of its space and invited, as a runner is, and `warning: <did> was not told of it: <why>` follows when the invite could not be left |
| `triager remove <url> <handle>` | as owner, stop naming one |
| `triager eval <url> <handle>` | how an account's labels compare with everyone else's, one line per scope the repository defines, in the order of its labels: the scope, the pulls and issues the account put a label of it on, how many of those someone else did too, and how many of those it agreed on (everyone else's labels of the scope, worked out without the account's, are its own), tab-separated |
### queue
| Command | |
| --- | --- |
| `queue list <url> [--json]` | the merge queues in order, and what was ejected |
| `queue leave <url> <at-uri>` | take a pull out of the merge queue |
`pull merge --queue` puts a pull in the queue.
### notification
| Command | |
| --- | --- |
| `notification list [--peek] [--kind <kind>]` | what others did that concerns you, the latest 50, from the appview: `new` (or nothing, once seen), when, why (`star`, `follow`, `pull`, `issue`: on a repository you own, collaborate on, or that an organization you joined owns; `comment`: on your pull or issue, or one you commented on; for a review, `approve` or `changes`; `mention`: a pull, issue or comment that names you as `@handle`; `invite`: an organization's invitation you have yet to answer, with the role it offers, as `as admin`; `checks-failed`: checks failing at your pull's latest run, and `ejected`: the merge queue taking your pull out, each from the runner, with the checks that failed, as `failed test, web`), who, and the repository and title, tab-separated; marks them seen unless `--peek`. `--kind` lists only `mentions`, `reviews`, `comments`, `pulls-and-issues`, `checks` or `stars-and-follows`, and marks none seen, as that would mark seen those of other kinds before them. With no appview, fails `notifications come from an appview, and none is set: set KNOTLESS_APPVIEW` |
| `notification settings [--on <reason>]... [--off <reason>]... [--followed-only \| --anyone]` | which notifications you hear of, kept by the appview: each reason turned on or off, and with `--followed-only` only of what accounts you follow did (`--anyone` undoes it). Prints every reason the appview gives, `<reason>`, a tab and `on` or `off`, then `from`, a tab and `anyone` or `accounts you follow`; with no flags, only prints them. A reason the appview does not give fails `"<reason>" is no reason: star, follow, …`. `notifications`, MCP and the pages leave out what you turned off. An OAuth login from before this asks for it with a new scope: log in again |
| `notification watch <url> [all \| releases \| ignore \| participating]` | what you hear of a repository, kept by the appview: `all` it does (pulls and issues opened, comments, releases; the default), its `releases` alone, nothing (`ignore`, but a review asked of you), or, `participating`, only what you take part in. Your own repositories are watched at `all` until you choose. Prints the level, a tab and the repository's record |
| `notification subscribe <at-uri>`, `notification mute <at-uri>`, `notification unsubscribe <at-uri>` | hear of every comment on a pull or issue (`subscribed`), nothing of it but a review asked of you (`muted`), or, with `unsubscribe`, as your part in it says again (`participating`). Prints the level, a tab and the URI |
| `notification subscriptions` | every choice `watch`, `subscribe` and `mute` made, the latest first: level, subject and when, tab-separated |
### theme
| Command | |
| --- | --- |
| `theme list [<handle>] [--find <name>]` | themes made for the pages, from the appview (`dev.knotless.listThemes`), the latest 100: everyone's, or `<handle>`'s, or with `--find` those whose name starts with it, in any case, by name. Each its name, its maker's handle and its `at://` URI, tab-separated; with no appview, fails `themes are listed and chosen by an appview: set KNOTLESS_APPVIEW` |
| `theme check <file>` | whether every text of a palette reads at 4.5 to 1 on what it is drawn on, as the pages hold their own themes: prints `every text reads at 4.5 to 1 or more`, or fails `some text would be hard to read, each needing 4.5 to 1: textMuted on background is 1.2 to 1; …`, each named as the palette names it, or as the pages name a colour they mix from it. `<file>` is JSON, a `dev.knotless.theme#palette` or a record holding one; `-` reads stdin |
| `theme publish <name> <file> [--use]` | publishes the palette, once checked as `theme check` does, as a `dev.knotless.theme` called `<name>` (1 to 64 characters), for anyone to choose, printing its `at://` URI; `--use` draws your pages in it as `theme use` does |
| `theme use <theme>` | draws your pages in a theme, kept by the appview with your preferences, so the pages take it as you next log in to them: `system`, `light`, `dark`, `latte`, `mocha`, `dracula`, `pckt`, `plyr`, `blacksky`, one published, by `at://` URI, whose colours are copied, or a palette of your own, unpublished, from a `.json` file or `-`, checked first. Prints `drawn in <name>` |
| `theme delete <uri>` | deletes a theme you published, printing `deleted`; those who chose it keep its colours. One not yours fails `<uri> is no theme of yours` |
### run
| Command | |
| --- | --- |
| `run list <url> [--ref <name>] [--commit <sha>] [--pull <at-uri>] [--queue <branch>] [--workflow <name>] [--log] [--images <dir>] [--wait <secs>] [--json]` | what those runners report, newest first; `--ref`, of pushes to that ref and runs asked for there, in full or a branch or tag named bare, as `main` (a pull's are read with `--pull`); `--queue`, of the merge queue's batches; `--log` adds the end of each log; `--images` saves each check's pictures; `--wait` waits for runs to finish |
| `run rerun <url> --ref <name> \| --pull <at-uri> [--workflow <name>]` | as owner, ask the runners to run it again, or only one of its workflows |
| `run rerun <url> --ref <name> --workflow <name> [-e <name>=<value>]...` | as owner, ask for one workflow there, with variables its steps see |
| `run cancel <url> --ref <name> \| --pull <at-uri> [--workflow <name>]` | as owner, ask the runners to cancel its runs going or waiting, or only one workflow's |
`run list` prints a line per check: `<status>`, `<workflow>`, the pull's URI
or the ref, the commit's first 12 characters, `startedAt` and any
`skipped` note, tab-separated. `--log` follows it with the log, and
`--images` ends the list with an `image` line per picture saved, then a
tab and its path, named `<workflow> <alt>.<ext>` with `/` as `-`. The
newest run keeps that name; an older run's picture of the same name is
`<workflow> <alt> <commit>.<ext>`, the commit's first 12 characters, and
one older still at that commit is not saved, so none is written over.
`--queue` takes a bare branch name. `--commit` takes a commit's hex ID or
at least its first 4 digits, as `git log --oneline` prints them, in either
case; anything else fails with `a commit is its hex ID, or at least its
first 4 digits, not "<given>"`. `--wait` reads again every
`KNOTLESS_POLL_MS` until it has found a check and none is queued or running, or the
time is up. Up with none found, it fails with `no checks of <what was
asked> after <secs>s`, such as `commit 0838c57, ref refs/heads/main`,
rather than print nothing and exit 0. Reads that fail in a way that may pass (a connection error, a
timeout, a 5xx or a 429) are retried with a backoff of up to 60 s. The MCP `run_list` tool waits at most 300 s,
and returns the pictures with `images: true`. A repository naming no
runners is an error, not an empty list. A private repository's checks,
logs and pictures are read from its runners' repos in its space, with a
space credential.
`run rerun` writes a `dev.knotless.rerun` record in the owner's account and
prints its AT-URI. A bare branch or tag is taken as its full ref, and a ref
the repository lacks is refused (`<url> has no ref <name>`), as is a
repository naming no runners. It asks for one ref or one pull (`name one
ref or one pull to run again`); `-e` needs `--workflow` (`variables go
to one workflow: name it`) and a ref (`variables go to a workflow run at
a ref, not a pull's`). At most 32
variables: a name is 1 to 64 letters, digits or `_`, not starting with a
digit or with `KNOTLESS_RUN`, and a value at most 4096 bytes.
`run cancel` writes a `dev.knotless.cancel` record in the owner's account
and prints its AT-URI. It takes the ref as `run rerun` does, and the same
refusals: one ref or one pull (`name one ref or one pull whose runs to
cancel`), a workflow's name, and a repository naming no runners. A runner
cancels the runs going or waiting when it reads the request, a deploy's
too, and files their checks as `cancelled`, logged `cancelled as the
repository's owner asked`. Runs started after it go as ever. A merge
queue's batches are not a ref's runs: `queue leave` takes a pull out.
### workflow
| Command | |
| --- | --- |
| `workflow list <url> [<ref>]` | the workflows at a branch or tag (the default branch), and what runs each there |
| `workflow pin [-C <dir>] [<uses>...]` | pin the shared steps the checkout's workflows use, and those they use, in `.knotless/uses.lock` (see [shared steps](../design/reusable-workflows.md)): each to the highest release its `@` takes; with `<uses>` named, only those, the rest as the lock had them |
`workflow list` prints the ref in full, then a line per workflow in file name
order: its name, then what runs it there, comma-separated, of `push`,
`pull` (a pull into it) and `asked` (`run rerun --ref --workflow` may ask for
it: it runs on a push there, its `asked` trigger names the ref, or it has
a schedule and the ref is the default branch), then, for one that does
not load, why. At the default branch, each of its schedules follows on a
line of its own, indented, as `schedule <cron>`. A ref it lacks is
refused (`<url> has no ref <name>`).
A shared step is released as the package `@step/<step>` with `release publish`,
its one file the step file and `--commit` the commit it is at. `workflow
pin` reads the workflows under `.knotless/workflows` in `dir` (`.`),
and takes for each `uses` the highest version that its `@` takes, of
those without a pre-release part unless the `@` has one. It prints a
line per `uses` pinned: the `uses`, the version it was pinned at (or
`-`), the version now and the commit's first 12 characters,
tab-separated. It fails, writing nothing, with `<dir> has no
.knotless/workflows`; `<url> has released no @step/<step> that @<spec>
takes`; `@step/<step> <version> names no commit it was made from`;
`@step/<step> <version> holds no <step>.yml`; `@step/<step> <version>'s
<step>.yml is not .knotless/steps/<step>.yml at <commit>`, when the file
released and the file at its commit differ; or when the step file
cannot be read at that commit.
### runner
| Command | |
| --- | --- |
| `runner list <url>` | the accounts whose checks of the repository readers show |
| `runner add <url> <handle>` | as owner, name an account a runner; for a private repository it is made a member and invited as `repo create` does (`added`, then `warning: …` if it could not be). A runner added after secrets were set has none of them, as nothing keeps their values to seal again: `warning: <did> gets the secrets set before only once each is set again: <names>` |
| `runner remove <url> <handle>` | as owner, stop naming one, printing `removed`; a private repository's keeps it a member (`repo share --undo` takes it back). Deletes the secrets sealed to it |
| `runner minutes <url>` | the minutes each runner the repository names counts for its owner's public repositories in the last 30 days, a line each: the runner, then `<n> of <allowance> min in 30 days` (adding `past it: runs go one at a time` once past), `<n> min in 30 days, no allowance`, or `nothing counted yet`, and the soonest minutes to free up. Each runner counts on its own (see [the runner](../design/runner.md)) |
### agent
| Command | |
| --- | --- |
| `agent list <url>` | the accounts, such as a coding agent, whose pulls the runners check as they move, as code no one vouches for (see [the runner](../design/runner.md#whose-code-runs)), each as its DID |
| `agent add <url> <handle>` / `agent remove <url> <handle>` | as owner, name one or stop naming it, printing `added` or `removed` |
### secret
| Command | |
| --- | --- |
| `secret set <url> <name> [--ref <pattern>]... [--runner <handle>]...` | as owner, or logged in as an admin who joined the organization that owns it, give the repository's runners the secret `<name>`, read from stdin less one line ending, sealed to the key each publishes so only it reads it, and left at the appview (`KNOTLESS_APPVIEW`), replacing one of the same name; `--ref`, repeatable, limits it to pushes to those refs, as a trigger's `refs` match; `--runner`, repeatable, by handle or DID, gives it to those of the repository's runners alone, and takes back the copy any other of them had. Prints `sealed <name> to <runners>`. Fails if the repository names no runners, a `--runner` is not one of them, or one it seals to publishes no key (a knotless-run from before). `<name>` is a shell variable's, not starting `KNOTLESS_RUN`; the value at most 16 KiB, the refs at most 16. See [Secrets](../design/runner.md#secrets) |
| `secret list <url>` | as owner or such an admin, each secret given, one line per runner, `<name>\t<runner>\t<refs, or *>\t<updated>`, never a value |
| `secret remove <url> <name>` | as owner or such an admin, take a secret back from every runner, printing `removed <name>` |
### release
| Command | |
| --- | --- |
| `release publish <url> <package> <version> <file>... [--commit <sha>] [--check <at-uri>] [--notes <text>] [--oci <dir>]` | as owner, publish a version of a package: its files, uploaded as blobs, then its `dev.knotless.release` record, never changed after (see [releases](../design/packages.md)). Prints the record's AT-URI. `--notes -` reads the notes from stdin |
| `release list <url> [<package>]` | the versions published, by package and then highest version first |
| `release yank <url> <package> <version> [--undo]` | as owner, withdraw a version: its files can still be read, but it is no longer the latest ([yanking](../design/packages.md#yanking)). Asking for what is so already changes nothing. Prints the release's AT-URI |
| `release read <url> <package> <version> <file>` | one file of a version (`latest` for the latest), checked against its size and SHA-256, printed as it is |
| `release download <url> <package> [<version>] [--file <name>]... [-o <dir>]` | a version's files, or the latest's, each checked against its size and SHA-256, into a directory (`.`) |
`release publish` names each file by its path's last part. A package's name
follows npm's rules: lowercase letters, digits, `-`, `.`, `_` and `~`, not
first a `.` or `_`, after an optional `@scope/`, at most 214 bytes. A
version is Semantic Versioning 2.0, such as `1.4.0` or `2.0.0-rc.1`. A
release holds 1 to 64 files, by distinct names of letters, digits, `.`,
`_`, `+` and `-`, not first a `.`, and none empty. Each is split into
blobs of `KNOTLESS_CHUNK_SIZE` as a large pack is. In a runner's step,
`--commit` is `KNOTLESS_RUN_SHA` unless given. A version published before
is refused before anything is uploaded, and by the record's create if
another publish got there first: `<package> <version> is published
already: <at-uri>`. A private repository's are refused: `a private
repository's releases are not published yet`.
`--oci <dir>` publishes an image, for knotless-serve's registry at `/v2/`
([api.md](api.md#images)): the files of the OCI image layout in `dir`,
as `docker save` or `docker buildx build --output type=oci,tar=false`
leaves one, beside any named. They are its `index.json`, which must name
one manifest (`index.json names more or less than one manifest`), and
each blob under `blobs/sha256` as `sha256-<hex>`, checked against its
name (`blobs/sha256/<hex> is not the blob its name says`); the manifest
the index names must be among them (`index.json names <digest>, which is
not among the blobs`). As a release holds at most 64 files, an image
holds at most 63 blobs, its manifests, configs and layers together.
`release list` prints a line per version: the package, the version,
`createdAt`, the commit's first 12 characters (or `-`) and the files'
names, comma-separated, then `yanked` if it is, tab-separated.
`release download` takes the package's latest, its highest version without a
pre-release part that is not yanked, unless a version is named, and every file unless
`--file` names some. It prints a line per file: the package, the
version and the path written, tab-separated. A file whose bytes do not
match fails with `<name>: what was read is not the file its release
names`; a version or file the release lacks with `<url> has published no
<package> <version>` (`latest` for none) or `<package> <version> has no
file <name>`.
### mod
| Command | |
| --- | --- |
| `mod hide <url> <at-uri> [--undo]` | as owner, hide a pull, issue or comment, or with `--undo` show it again |
| `mod block <handle> [<url>] [--undo]` | leave out what an account writes about your repositories, or about one, printing `blocked <did>` or `unblocked <did>`; yourself fails `that is you`, another's repository `only <url>'s owner can keep accounts out of it` |
| `mod blocks` | the accounts you block, the latest first: handle, `all your repositories` or the repository's `knotless://` URL, and when, tab-separated. Blocks naming a private repository live in its space, and are not listed |
| `mod report <handle, url or at-uri> <reason> [-m <text>]` | report to the moderation service; the reason is `spam`, `violation`, `misleading`, `sexual`, `rude` or `other`; prints `report <id> sent` |
A report's reason is sent as `com.atproto.moderation.defs#reason<Name>`,
as `reasonSpam`. Its subject is a `com.atproto.admin.defs#repoRef` for a
handle or DID, and a `com.atproto.repo.strongRef` with the record's
current CID for an `at://` URI, or for a `knotless://` URL's `repo`
record. It goes to the `#atproto_labeler` service of `KNOTLESS_MODERATOR`'s
DID document, else of the appview's `/.well-known/did.json`, with a service
token from your PDS; with neither set it is an error. The `admin` commands
need an appview (`KNOTLESS_APPVIEW`) and an account it names in
`KNOTLESS_ADMIN_DIDS`: each call carries a service token from your PDS
for its method, made out to the DID in the appview's
`/.well-known/did.json`.
### admin
| Command | |
| --- | --- |
| `admin takedown <handle, url or at-uri> [--undo]` | as the appview's operator, take a subject down, or restore it; prints `taken down` or `restored` |
| `admin status <handle, url or at-uri>` | as the operator, a subject's moderation status: `taken down` or `up` |
| `admin reports [<limit>]` | as the operator, the reports received, newest first (default 50): a line per report of `<id>`, `<reporter did>`, `<reason>`, `<subject>`, `<text>`, tab-separated |
| `admin stats [--days <n>] [--json]` | as the operator, how the appview is used, day by day, from `dev.knotless.getUsage` (14 days by default) |
| `admin logs [--trace <id>] [--message <m>] [--level <l>] [--min-ms <n>] [--limit <n>]` | as the operator, the lines the appview logged, from `dev.knotless.getLogs`, one JSON line each, oldest first: a request's by its trace (up to 500, unless `--limit` says fewer), or the last (100 by default, at most 500) of a message, a level and above, or a time; one of them is needed |
### org
| Command | |
| --- | --- |
| `org add <handle> [--admin] [--org <org>]` | invite an account to your organization, which your account is from then on (the first invitation says so), or change a member's role, which keeps how their membership shows and when they joined; `--admin` makes them an admin, who may also change the organization's members and make repositories as it, and change its repositories' settings from their own login (`repo edit`, `repo archive`, `repo compact`, `branch protect`, `label define`, `label delete`, `triager add`, `triager remove`, `agent add`, `agent remove`, `repo share`, `runner add`, `runner remove`, `secret set`, `secret remove`): not logged in as a repository's owner, these ask the appview (`KNOTLESS_APPVIEW`), which writes them as the organization through its gateway, and refuses anyone not its admin. Renaming, deleting or transferring one takes the organization's own login. With `--org`, to an organization you are an admin of, through its gateway. Says where the invitation is kept: in the organization's space, which only it and its members read, or, on a PDS without spaces, in its public records. An invitation lapses after 7 days; inviting again starts it over. Your organization acts through a gateway, so delegate first (`knotless account delegate`) |
| `org remove <handle> [--org <org>]` | take a member out, or withdraw an invitation: they no longer push to its repositories, and leave the spaces of its private ones, but those shared with them on their own |
| `org join <org> --public \| --hidden` | accept an invitation, which makes you a member: `--public`, anyone sees it, and you merge, close and label on all its repositories; `--hidden`, only its members see it, and on its public repositories you push and open pulls, but merging, closing, labelling and queueing take a public membership. Neither, and it says what each means. Again, to change which. Hidden takes a PDS with spaces |
| `org leave <org>` | stop being a member, at once, or decline an invitation; warns if the organization's gateway did not take you out of its spaces, and run again, asks it again |
| `org members [<org>] [--public]` | an organization's members, yours unless named, a line each: the handle (or DID), the role (`admin` or `member`) and the standing (`invited`, `lapsed`, `public` or `hidden`), tab-separated. The organization and its members see every one; anyone else, public members, and `--public` shows them so to whoever asks. Or that it is not an organization, and, for yours, how to make it one |
| `org list [<handle>]` | the organizations an account is a public member of, a handle a line; yours, with no handle, hidden ones too |
| `org delete --yes` | take every member out and withdraw every invitation: your account is no longer an organization. Without `--yes`, says whom it would take out |
### What the pull, issue and queue commands print
| Command | Output |
| --- | --- |
| `pull create`, `issue create` | the new record's `at://` URI |
| `comment add` | the `at://` URI of each comment it wrote, a line each: the review first, then its comments on lines |
| `comment resolve` | `resolved`, or `unresolved` with `--undo` |
| `pull list`, `issue list` | a line per item: `<kind>`, `<state>`, `<uri>`, `<title>`, tab-separated |
| `pull show`, `issue show` | `<state> <kind> by <author>: <title>`, then `draft` for a draft pull, `review requested: <handles>` for one that asks for reviews, its base's required reviewers among them, then `closed by <pull>` for an issue a pull closed, `mentioned in <pull>` for each pull whose title or body names an issue, oldest first, ending `, to close it` where it names it after a closing keyword, `reverts <pull>` for a revert, `reverted by <pull>` for a merged pull whose revert merged, then for a pull each reviewer's standing review as `review: <reviewer> <verdict> at <commit>` (the first 12 characters of the head it looked at), then for an open pull in or ejected from the merge queue `merge queue: <state> at <createdAt>` and where it stands, and for one ejected as checks failed `logs: <command>`, the `knotless run list` that reads their logs, then for an open pull ever set to merge when its checks pass `merge when checks pass: set by <handle>`, or `cancelled, as <handle> pushed after <handle> set it`, or `cancelled, as the pull moved off <branch>`, then the body and each comment as `<author> [<verdict>]: <body>`, where a comment on lines says ` on <path>:<lines>[:old] (<uri>[, resolved])` before the colon, and an answer in a thread ` answering <uri>` |
| `pull merge` | the commit the branch is now at, then `deleted <branch>` and a `closed <issue>` line per issue it closed; queued instead, `<base> takes pulls through the merge queue: queued, <n> of <of>`, then where to follow it |
| `pull merge --queue` | `queued into <base>: <n> of <of>`; queued again at the head the queue last ejected, a line saying so and why; then where to follow it, and to read an ejection's logs before queueing again |
| `pull merge --auto` | `set to merge when its checks pass, at <head>` (12 characters), then what happens next; with `--undo`, `cancelled: it merges only when someone merges it` |
| `pull merge --check` | `<state>`, `<method>` and `checks <checks>`, and `reviews <passed or blocked>` when its branch asks for reviews, tab-separated, then a `missing <what>` line for each thing its reviews lack (`reviews 1 of 2`, `owners of web/: @alice or @bob`, `changes asked by @carol`, `a review of its head from @bot`; owners who are the pull's author alone add `, its author alone, whose own approval never counts, so web/ needs another owner in CODEOWNERS`), an `uncounted <why>` line for each approval that does not count (`@dan's approval does not count: only the owner's and collaborators' approvals count`), a `warning <what>` line for each CODEOWNERS line skipped, a `conflict <path>` line per file that conflicts, `deletes <branch>` for the branch it would delete, `may <what>`, `queue only` when its branch takes pulls only through the merge queue, and last why it cannot merge, in the words `pull merge` would refuse with, and that the caller may not merge it when it may not |
| `queue list` | a line per pull: `<position>`, `<base>`, `<state>`, `<uri>`, `<title>`, tab-separated, where the state is `queued`, `pre-flight <workflows>` while the [pre-flights](workflows.md#the-merge-queue) it waits for run, comma-separated, `failed <workflows>` once the newest run of one failed and none runs, as the runner has yet to eject it or run it again, or `<checking or landing> at <commit>`; an ejected pull has `-` and `ejected`, and a line after it with the reason; one [waiting for the pull below it](../design/merge-queue.md#stacks) has `-` and `stacked`; `the merge queue is empty` if it is |
| `queue list --json` | each pull waiting, with its `place` and `batch`, or the `preflight` workflows it waits for |
| `queue leave` | `left the merge queue` |
| `pull edit`, `comment delete`, `pull close`, `pull reopen`, `pull retarget` | `edited`, `deleted`, `closed`, `open`, `retargeted` |
`pull merge --queue` refuses `--keep-branch`, `--wait` and `--force` (`--queue
takes none of --keep-branch, --wait and --force`), and `pull merge --check`
those and `--queue`. Its state is `clean`, `conflicting`, `blocked` by
its checks or its reviews, or `draft`, whatever else holds, until its
author marks it ready (`pull merge` and `pull merge --queue` refuse one, in the
words its reason gives); its checks are `passed`, `failed`, `running`, `pending` (not run at
its head merged with its base as it is now) or `none` (the repository
names no runners, or has no workflow that runs on the pull, so nothing is
waited for, or it conflicts); its method
`merge` or `squash` (into a branch protected as linear).
`may` lists, comma-separated, what you may do to it: `merge` and `queue`
(the owner and collaborators, the queue only with runners), `force` (the
owner), `base` and `head` as `pull retarget` changes them (those who may close
it, and its author), and `rerun` (the owner, with runners). `queue list` takes one flag,
`--json`. `pull show` reads an item under a minute old from its
author's repository when the appview has not indexed it yet. Where the
merge queue's `standing` lines come from is in [the merge
queue](../design/merge-queue.md).
### The merge gate
In a repository naming runners, `pull merge` refuses a pull until each
workflow that runs on it has passed at its head merged with its base as
it is now ([design](../design/runner.md#the-merge-gate)). It says which:
- `<pull> did not pass its checks at its head, <h12>, merged with
<branch> at <b12>: <workflow> (<status>), ...`
- `... are still running: ...`, each still waiting for a job on its
runner named `<workflow> (queued)`, then `` Wait for them with
`--wait <secs>` ``
- `<pull> has no checks at ... of <workflows>`, then why: none has run
yet, the newest are of an older head, or the newest are of the branch
at another commit, and `It is not checked yet`, then `` The owner asks
for them with `knotless run rerun <url> --pull <pull>`; wait for them
with `--wait <secs>` ``
Each ends ``read their logs with `knotless run list <url> --pull <pull>
--log`; the owner, its author or not, merges past it with `--force` ``.
`--force` from anyone else is refused: `only the owner of <url> merges a
pull past its checks`. Over MCP, `pull_merge` takes `wait` (at most 300
s) and `force`, and its refusals name tools rather than commands: `the
run_list tool, pull and log`, `the pull_merge tool and force`.
A refusal says what to do last, after a full stop, in the words of
whoever reads it: here as a command to run, over MCP as a tool to call,
and on the pages, or `getMergeStatus` and any write of theirs refused,
as the control there that does it, leaving out what only the command
line can (`--force`, `--wait`, reading logs, beside the failed checks
the pages already link). A runner's reason for ejecting a pull from the
merge queue says only what stands in its way, as every client reads it.
### The old names
Before commands were grouped, each stood alone. Those names still work,
hidden from the usage, for one release. Each became:
| Before | Now |
| --- | --- |
| `delete-data`, `delegate`, `profile`, `email` | `account delete-data`, `account delegate`, `account profile`, `account email` |
| `follow`, `unfollow`, `follows`, `followers`, `owners` | `account follow`, `account follow --undo`, `account follows`, `account followers`, `account search` |
| `create`, `ls`, `fork`, `describe`, `rename`, `archive`, `delete` | `repo create`, `repo list`, `repo fork`, `repo edit`, `repo rename`, `repo archive`, `repo delete` |
| `transfer`, `transfer --cancel`, `accept` | `repo transfer`, `repo transfer --undo`, `repo accept` |
| `share`, `unshare`, `members` | `repo share`, `repo share --undo`, `repo members` |
| `star`, `unstar`, `stars` | `repo star`, `repo star --undo`, `repo stars` |
| `compact`, `storage`, `mirror`, `refs`, `search`, `blame`, `commit`, `site` | `repo compact`, `repo storage`, `repo mirror`, `repo refs`, `repo search`, `repo blame`, `repo commit`, `repo site` |
| `diff <url> <base> <head>`, `diff <url> <at-uri>` | `repo diff`, `pull diff` |
| `branch <url> <name>`, `--delete`, `--rename` | `branch create`, `branch delete`, `branch rename` |
| `protect`, `unprotect`, `rules` | `branch protect`, `branch protect --undo`, `branch rules` |
| `tag`, `tag --delete` | `tag create`, `tag delete` |
| `pull`, `issue` | `pull create`, `issue create` |
| `list --pulls`, `list --issues` | `pull list`, `issue list` (`list` with neither is gone) |
| `show`, `edit`, `close`, `reopen`, `label`, `unlabel`, `assign`, `unassign`, `react` | `pull` or `issue` with the same action, `--undo` for the `un-` ones; `comment edit` and `comment react` for a comment |
| `template <url> pull`, `template <url> issue` | `pull template`, `issue template` |
| `ask-review`, `unask-review`, `retarget`, `revert`, `update-branch`, `dismiss` | `pull ask-review`, `pull ask-review --undo`, `pull retarget`, `pull revert`, `pull update-branch`, `pull dismiss-review` |
| `merge` | `pull merge` |
| `comment`, `delete-comment`, `resolve`, `unresolve` | `comment add`, `comment delete`, `comment resolve`, `comment resolve --undo` |
| `labels`, `labels add`, `labels remove` | `label list`, `label define`, `label delete` |
| `triagers` | `triager list`, `triager add`, `triager remove`, `triager eval` |
| `queue`, `queue --leave` | `queue list`, `queue leave` |
| `checks`, `rerun`, `workflows`, `uses update` | `run list`, `run rerun`, `workflow list`, `workflow pin` |
| `minutes`, `runners`, `agents`, `secret ls`, `secret rm` | `runner minutes`, `runner list`, `agent list`, `secret list`, `secret remove` |
| `publish`, `releases`, `download` | `release publish`, `release list`, `release download` |
| `hide`, `unhide`, `block`, `unblock`, `blocks`, `report` | `mod hide`, `mod hide --undo`, `mod block`, `mod block --undo`, `mod blocks`, `mod report` |
| `themes`, `notifications` | `theme list`, `notification list` |
`login`, `logout`, `upgrade`, `status`, `admin`, `org` and `theme` (but
for `themes`) are as they were.
## knotless-serve
| Flag | Default | |
| --- | --- | --- |
| `--listen <addr>` | `127.0.0.1:2590` | where to listen |
| `--ttl <secs>` | 5 | how old a mirror may be before a read syncs it, when no feed is up; whole seconds |
| `--jetstream <wss://host>` | | a Jetstream v2 host to follow |
| `--firehose <wss://relay or pds>` | | a firehose to follow instead |
| `--index <file>` | | keep an appview index in this SQLite file |
| `--backfill <https://pds or relay>` | | at start, index every account on a PDS, or those holding knotless records on a relay |
| `--relay <https://relay>` | | backfill from this relay when the index was rebuilt (a new file, or another build's tables), and every ten minutes after |
| `--directory <https://host>` | | ask this collection directory (`com.atproto.sync.listReposByCollection`, as lightrail answers it) which accounts hold knotless records before the relay or PDS a backfill is from; repeatable, asked in turn; empty, none |
| `--mirror <https://host>` | | read each account's repository from this mirror (`com.atproto.sync.getRepo`, as hubble answers it, STAR-lite or a CAR) before the account's PDS; empty, none |
| `--public-url <https://host>` | | the URL the server is reached at; makes it a gateway, as `did:web` of the host, and serves MCP at `/mcp` |
| `--labeler <did>` | | honour this labeler's takedowns; repeatable |
| `--default-runner <did>` | | the runner a new public repository names unless its owner names others, as `dev.knotless.describeServer` answers; one only, as each runner a repository names runs all its checks (`--default-runner names one runner`); empty, none; anything else not a DID is refused (`--default-runner takes a DID, not <value>`) |
| `--client-minimum <version>` | | the oldest client version this appview asks for, as `dev.knotless.describeServer` answers: older clients refuse to run until upgraded. For a release found to harm repositories; anything not a version is refused; empty, none |
| `--client-minimum-reason <text>` | | why, which those clients print with their refusal |
| `--ios-minimum-build <build>` | | the oldest build of the iOS app this appview lets run, as `dev.knotless.describeServer` answers: older builds show only a way to update. Anything not a number is refused; empty, none |
| `--ios-minimum-reason <text>` | | why, which those builds show |
| `--web <http://app>` | | the SvelteKit app that draws the pages |
| `--releases <dir>` | | client builds to hand out under `/releases`, with `/install.sh` |
| `--stale <secs>` | 120 | how long a feed may be silent before `/_status` calls it dead; whole seconds |
| `--usage <file>` | | count requests and page views in this SQLite file, for `knotless admin stats` |
| `--logs <file>` | | keep each line logged in this SQLite file too, for `knotless admin logs`: every serve writing it, across deploys, for 30 days |
| `--logs-keep <lines>` | 500000 | how many lines at info and below `--logs` keeps, the last of them; those above stay their 30 days |
| `--check-index <file>` | | only check that the index there is of this build's tables, so starting would keep it |
| `--metrics <addr>` | | answer `/metrics` (Prometheus) and `PUT /log` (the log's level) there, apart from the rest; prints `metrics on <url>` after `listening on` |
| `--sign-up-pds <https://pds>` | `https://selfhosted.social` | the PDS where the pages send someone making an account ([Logging in](../design/serve.md#logging-in)) |
| `--oauth-url <https://host>` | `--public-url` | where the PDS sends the gateway's OAuth logins back to, when that is not `--public-url`: an https front for a server at a loopback address, as the scenarios run it |
| `--bots <dir>` | | let the accounts `KNOTLESS_BOT_DIDS` names log in at `/oauth/bot`, keeping their sessions in this config directory for `knotless-run` and `knotless-triage` ([Bots](../design/gateway.md#bots)) |
| `--pages-domain <domain>` | | answer repositories' sites on this domain, each owner at a label of it, as `knotless.page`: a request whose host is the domain or a name under it is answered as a site and nothing else; needs `--public-url`, where an owner's root redirects ([Pages](../design/pages.md)) |
| `--apns-key <file>` | | send the iOS app's notifications through Apple's push service, signing with this `.p8` key; takes `--apns-key-id` and `--apns-team`, and an index ([Push notifications](../design/serve.md#push-notifications)) |
| `--apns-key-id <id>` | | the id Apple gave that key |
| `--apns-team <id>` | | the Apple team the key is of |
| `--code-index <dir>` | | search the code of public repositories, keeping Zoekt's shards in this directory; `zoekt-webserver` and `zoekt-git-index` must be on `PATH` ([Code search](../design/serve.md#code-search)) |
An empty `--jetstream`, `--firehose`, `--backfill` or `--default-runner` is the same as none,
so a setting such as `--firehose "$KNOTLESS_FIREHOSE"` left unset turns it
off. `--labeler` and `--apns-key` take `--index`. `--private` is gone, and refused
(`--private is gone: knotless-serve shows a private repository to each
member who logs in to its pages (--public-url), read as them, …`): a
server people log in to shows each member their private repositories.
`--bots` takes `--public-url` (`--bots takes --public-url, where bots
log in`), and
a `--public-url` is `http(s)://host[:port]` with no path.
An unknown flag prints `unexpected <flag>` and the usage, and a flag
without its value `<flag> takes a value`; `--ttl` and `--stale` that are
not whole seconds print `--ttl takes seconds` or `--stale takes seconds`.
These exit 2. An error once started prints `knotless-serve: <error>` and
exits 1. Once bound, it prints `listening on http://<addr>`, and it stops
cleanly on SIGTERM or SIGINT.
`--check-index <file>` prints `<file>: this build keeps it`, and passes a
file that does not exist. It fails with `<file>: its tables are version
<v>, this build's <version>: starting would rebuild it` otherwise.
`/install.sh`, served with `--releases`, installs `knotless`,
`git-remote-knotless`, `knotless-mcp`, `knotless-run`, `knotless-triage`,
`knotless-agent` and `knotless-claude-review` for `aarch64` or `x86_64`,
macOS or Linux (musl), with curl or wget. A release from before the last
four shipped installs the first three. It checks each download's SHA-256, and
moves each binary into place, so a running copy is replaced. The routes
are in the [API reference](api.md#releases).
## knotless-run
`knotless-run [flags] <handle>/<name>...` watches the repositories
named, and with `--discover` those whose records name its account as a
runner (then it needs none named), acting as `KNOTLESS_IDENTIFIER` with `KNOTLESS_PASSWORD`, or as
the account logged in with `knotless login`. A private repository takes
an OAuth login, for a space credential, and the runner's account a
member of it: `knotless repo create --private` and `knotless runner list <url>
add` make it one, and invite it, which `--discover` reads.
| Flag | Default | |
| --- | --- | --- |
| `--executor docker \| shell` | `docker` | run steps in a container, or as the runner's user (the library's `Options::new` defaults to `shell`) |
| `--image <image>` | `debian:stable-slim` | the image for a workflow that names none |
| `--jobs <n>` | 4 | runs at once, over every repository |
| `--pull-jobs <n>` | half of `--jobs`, at least 1 | of those, how many runs other than pushes to a default branch and the batch landing next may take; at most `--jobs` |
| `--owner-jobs <n>` | all of `--jobs` | of those, how many the runs of one owner's repositories may take; at most `--jobs` |
| `--allowance <minutes>` | none | the minutes of steps each owner's public repositories' runs may take in 30 days, the runner's own account's aside; past it, that owner's runs go one at a time |
| `--private-allowance <minutes>` | none | the same for private repositories' runs, counted apart |
| `--unmetered <did>` | none | an owner whose runs, like the runner's own account's, count against neither allowance, and may take every job, past `--owner-jobs`; repeatable |
| `--step-timeout <secs>` | 1800 | the longest a step may run, and a container may take to start |
| `--run-timeout <secs>` | none | the longest a run's steps may take together; a step going when it is up is killed and the rest skipped. A run asked for by name is not held to it |
| `--cpus <n>` | none | under `docker`, the CPUs a run's container may use, as docker's `--cpus` takes them (`2`, `1.5`) |
| `--memory <GiB>` | none | under `docker`, a whole number of GiB a run's container may use, with no swap beyond it; past it the kernel kills what is running and the step fails |
| `--pids <n>` | none | under `docker`, the processes and threads a run's container may have at once |
| `--network <name>` | docker's default bridge | under `docker`, the network runs join; made if missing as a bridge whose containers cannot reach each other, used as it is if there already |
| `--runtime <name>` | docker's default (`runc`) | under `docker`, the runtime runs' containers start with, as docker's `--runtime` takes one: `runsc` runs them under gVisor, a kernel of its own between a run and the host's. The daemon must know it (see [the runner host](../../deploy/runner-host/README.md)) |
| `--trusted-runtime <name>` | `--runtime`'s | under `docker`, the runtime of a run whose code an owner `--trust` names vouches for: a push to their repository, or a pull or batch every writer of which they vouch for. `runc` spares it gVisor's cost |
| `--trust <did>` | none | an owner whose vouched-for runs start under `--trusted-runtime`; repeatable |
| `--resolv <file>` | docker's | under `docker`, a file on the docker host that runs' containers see as their `/etc/resolv.conf`. Under `runsc` on a network docker made, a run resolves no names without one: docker's resolver there is reached through NAT rules gVisor does not apply |
| `--paused <file>` | none | a JSON object of owner DIDs, or `*` for all but the runner's own account, each to why their runs are paused; read again on every pass, so editing it takes effect without a restart. A paused owner's runs are filed as `error`: `paused by this runner's operator: <why>` |
| `--keep-checks <days>` | 30 | how long the default branch's checks are kept, each workflow's newest whatever its age; the runner [prunes](../design/runner.md#pruning) its checks of no more use each hour. 0 keeps every check |
| `--sweep <secs>` | 600 | how often every repository is read again, in case an event was missed, and leftover containers removed |
| `--stop-wait <secs>` | 900 | how long a stop waits for runs going before it cancels them; 0 waits not at all |
| `--poll <secs>` | 10 | how often a private repository's space is asked whether anything was written there, which Jetstream never carries; while `KNOTLESS_APPVIEW`'s appview passes on its writes, no more often than once a minute (see [Private repositories](../design/runner.md#private-repositories)) |
| `--cache-max <GiB>` | 16 | a whole number of GiB: the size at which a cache's new base is not used, to start afresh, and the most kept layers may take in all |
| `--disk-floor <GiB>` | 20 | a whole number of GiB kept free on the disk `--caches` is on: below it, a run about to start first sheds every cache's base no run reads and every kept run's layers, then waits for room, looking every 30s; 0 keeps no floor (see [Room on the disk](../design/runner.md#room-on-the-disk)) |
| `--caches <dir>` | `<cache dir>/run/caches` | where caches' bases and runs' layers are kept; a path the docker daemon sees under the same name |
| `--secrets <file>` | | the [secrets file](workflows.md#secrets-file) |
| `--secrets-key <file>` | `secrets-key.json` beside the state file | where the runner keeps the key owners seal secrets to, made if missing, and published as its `dev.knotless.runnerKey` record (see [The runner's key](../design/runner.md#the-runners-key)) |
| `--state <file>` | `<cache dir>/run/state.json` | the state file |
| `--work <dir>` | the system's temporary directory | where checkouts go while their steps run; made if missing |
| `--jetstream <wss://host>` | `wss://jetstream2.us-east.bsky.network` | where events come from |
| `--discover` | off | also watch every public repository whose record names this runner's account in `runners`, as the appview's `listRepos?runner=` lists them and Jetstream brings them; one no longer naming it is left. Needs `KNOTLESS_APPVIEW` (see [Repositories that name it](../design/runner.md#repositories-that-name-it)) |
| `--pulls` | off | also run open pulls, at their head merged with their base, without secrets; takes `docker` |
| `--pulls-only <handle>/<name>` | none | watch that repository for its open pulls alone, never its pushes or its merge queue, which another runner runs; as there, a queued pull does not run on its own, and the rest run again as their head moves, not their base; repeatable; takes `--pulls` |
| `--queue` | off | land the repositories' [merge queues](../design/merge-queue.md) where its account may push, or where they name it and no runner that may ([Who lands a queue](../design/merge-queue.md#who-lands-a-queue)); takes `docker` |
| `--batch <n>` | 8 | with `--queue`, the most pulls in a batch, 64 at most (a check record's `batch` holds no more) |
| `--depth <n>` | 2 | with `--queue`, 2 checks the next batch on top of the one being checked; 1 turns that off; nothing else |
| `--notice <secs>` | 30 | with `--queue`, how often the owner's and collaborators' queue records are read |
| `--review-wait <secs>` | 1800 | with `--queue`, how long a queued pull waits for a review of its head from its branch's [gates](../design/collaboration.md#gates) before it is ejected |
`<cache dir>` is `KNOTLESS_CACHE_DIR`'s. `--jobs`, `--pull-jobs`, `--owner-jobs`, `--allowance`, `--private-allowance`,
`--sweep`, `--batch`, `--notice`, `--review-wait`, `--poll`, `--run-timeout`, `--memory`
and `--pids` take at least 1, as in `--batch takes
a number of pulls, at least 1` and `--notice takes a number of seconds, at
least 1`. `--depth 0` is refused with `--depth takes a number of batches,
at least 1`, and more than 2 with `--depth is 1 or 2: one batch at most
runs ahead`. `--batch` above 64 is refused with `--batch is 64 pulls at
most`. `--pulls` and
`--queue` refuse `--executor shell` (`--pulls and --queue run code anyone
can open a pull with: they take --executor docker`), and so do `--cpus`,
`--memory`, `--pids`, `--network`, `--runtime`, `--trusted-runtime` and
`--resolv` (`--cpus, --memory, --pids, --network, --runtime,
--trusted-runtime and --resolv hold a run's container: they take
--executor docker`).
`--cpus` takes a number above 0 (`--cpus takes a number of CPUs, more
than 0, not <value>`). `--pulls` refuses
to start with `KNOTLESS_APPVIEW` set empty; `--queue` starts so, but
its reads of pulls then fail. A repository that cannot be found stops the
start: `<label>: no repository by that name this runner can read`.
A usage error exits 2 and prints the usage; an error at run time exits 1,
as `knotless-run: <error>`. Once started, the runner logs `knotless-run:
watching <repositories> as <did>` to standard error, and an error in one
repository as `knotless-run: <label>: <error>`, and goes on: the same
error again only once a minute, with how many times it was seen. Each pass
logs how long it took, and one going 5 minutes says where it is, every 5
minutes ([What a pass logs](runner-internals.md#what-a-pass-logs)). A pass that
finds a repository's key changed fails with `<label> no longer names the
repository it did`.
It follows Jetstream's `subscribeEvents` for the `dev.knotless.*`
collections, with `commit`, `identity`, `account` and `sync` events, over
the `xrpc.v1.json` websocket subprotocol, without a cursor. After a
session ends it connects again in 500 ms, doubling the wait after each
failed attempt up to 60 s, and back to 500 ms once connected. A
connection silent for 60 s gets a ping, and silent another 60 s is given
up.
What the runner keeps on disk, its containers and its volumes are in
[knotless-run internals](runner-internals.md).
## knotless-triage
`knotless-triage (--decide <url> --model <name> | --classify <command>)
--scopes <scope>,... [flags] [<handle>/<name>...]` triages the repositories
named, and with `--discover` those naming it, acting as
`KNOTLESS_IDENTIFIER` with `KNOTLESS_PASSWORD`, and reads public
repositories' issues through `KNOTLESS_APPVIEW`.
| Flag | Default | |
| --- | --- | --- |
| `--decide <url>` | | a decision model's endpoint: one taking `{model, state, questions}` with a bearer token, as TypeSafe's Jev (`https://api.typesafe.ai/v1/systemone`) or Cloudflare's Clef on Workers AI (`https://api.cloudflare.com/client/v4/accounts/<account>/ai/run/@cf/cloudflare/clef`) do. Its token is `KNOTLESS_DECISION_TOKEN` |
| `--model <name>` | | with `--decide`, the model asked, as `jev-latest`, `clef` or `clef-flash` |
| `--threshold <p>` | 0.5 | with `--decide`, the least probability, from 0 to 1, that sets a label |
| `--classify <command>` | | instead of `--decide`, a command run with `sh -c` once per issue to judge |
| `--scopes <scope>,...` | | the scopes it chooses labels of, as `kind,area` |
| `--jetstream <wss://host>` | `wss://jetstream2.us-east.bsky.network` | where events come from |
| `--sweep <secs>` | 600 | how often every repository is read, whatever events say |
| `--settle <secs>` | 5 | how long after an event the repository is read, for the appview to index the issue |
| `--timeout <secs>` | 120 | how long the classifier has for an issue before it is killed |
| `--once` | off | read each repository once, and stop |
| `--discover` | off | also triage every repository whose record names this account in `triagers`, in the scopes it names it a triager of that `--scopes` holds: those the appview's `listRepos?triager=` lists, private ones that invited it, and those Jetstream brings, at the start and every `--sweep`. One no longer naming it, or that cannot be read, is left, and found again if it still names it. Needs `KNOTLESS_APPVIEW` |
A decision model is asked one question per scope whose labels are all
exclusive, a `choice` among them named by their names after the scope,
with `none_of_these` for none, and, of any other scope, one `noul`
question per label, named by the label with `_` for each character other
than a letter or digit. Each label's description, when it has one, goes
with it. The state is the issue's `title` and `body`. A label is set when
its probability (a choice's `confidence`, a noul's `noul`) is at least
`--threshold`. The record's `judged` gives the `model` the answer names,
or `--model`, and as its `reason` each question's answer and probability,
as `kind/bug 0.91; area/web 0.10`, or `kind: none 0.80`. An answer that is
not a success, or has no `answers` (at the top, or in `result` as Workers
AI gives them), is logged as `judging failed`.
A command reads one issue as JSON on standard input: `subject` (its
AT-URI), `author` (a DID), `title`, `body` when it has one, and `labels`,
the repository's definitions of the labels to choose among. It writes
`{"labels": [...], "model": "...", "reason": "..."}` on standard output;
`model` and `reason` may be left out, and are kept to 200 and 500
characters. A name not offered is passed over, and of a scope's exclusive
labels only the first offered is set. A classifier that exits non-zero,
answers something else, or a classifier of either kind that takes longer
than `--timeout`, is logged as `judging failed`,
and the issue is asked about again at the next read. Each issue judged is
logged as `judged`, with the labels set.
A usage error exits 2 and prints the usage, as `name its scopes and a
repository, or --discover` or `name one classifier, --decide or --classify`; an error at run time exits 1, as
`knotless-triage: <error>`, as `--discover finds repositories through an
appview, and KNOTLESS_APPVIEW is set empty`. With `--discover` it logs each
repository it finds as `found` and each it leaves as `left`; a listing or
the invites it could not read, as `finding repositories failed`. Watching, it stops at the start if a
repository cannot be opened; afterwards a repository that cannot be read
is logged as `reading the repository failed`, and it goes on. With
`--once`, any such error stops it.
It follows Jetstream as knotless-run does, reconnecting in 500 ms, doubling
up to 60 s, and giving a connection up once it stays silent 60 s past a
ping. It reads every repository on connecting; an issue written about
a watched repository, or a watched owner's repository record, has it read
the repositories again after `--settle`; with `--discover`, any repository
record written has it read that repository's, to find one that names it.
## knotless-agent
`knotless-agent review --command <command> [flags] [<handle>/<name>...]`
reviews the open pulls of the repositories named, and of every repository
an `--owner` holds, that ask its account for a review, acting as `KNOTLESS_IDENTIFIER` with `KNOTLESS_PASSWORD`, and
reads public repositories' pulls through `KNOTLESS_APPVIEW`. [Reviews by
Claude](../guide/reviews.md) sets one up, and [review
agents](../design/collaboration.md#review-agents) says why it works as it
does.
| Flag | Default | |
| --- | --- | --- |
| `--command <command>` | | the command run with `sh -c` once per review, as `knotless-claude-review` |
| `--all` | off | review every open pull that is not a draft, asked or not |
| `--rounds <n>` | 3 | the most times it asks for changes on one pull; it stops reviewing the pull after that. Approvals do not count |
| `--timeout <secs>` | 1200 | how long the command has for a pull before it is killed |
| `--jetstream <wss://host>` | `wss://jetstream2.us-east.bsky.network` | where events come from |
| `--sweep <secs>` | 300 | how often every repository is read, whatever events say |
| `--settle <secs>` | 5 | how long after an event the repositories are read, for the appview to index the pull |
| `--once` | off | read each repository once, and stop |
| `--owner <handle\|did>` | | also watch every repository this account holds that the agent reads: the public ones, listed from its PDS at each read, so one it makes later is watched too, and the private ones it shared with the agent's account, from the invites `repo share` leaves it at the appview (an invite it can no longer read by, as once unshared, is passed by; without a gateway's DID at the appview to ask with, it logs `listing the invites left it failed` and watches the public ones). Repeat it for more accounts. An owner whose listing fails is logged as `listing an owner's repositories failed`, and its last listing stands |
It reviews a pull that is open and not a draft, that its owner, a
collaborator or a member of the owner's organization opened, and whose
`reviewers` names its account (or any, with `--all`), as it does of every pull into a branch whose rule names it a gate, once at each head
commit, until it has asked for changes `--rounds` times. It never reviews its own. A head whose
changes against the base are those of the head it last reviewed, as after
a merge of the base into the branch, is not read again: its last verdict
is written at the new head, beginning `Carried forward:`, and counts as no
round. A request for changes is not carried so when the pull's title or
body changed after it: the new head is read.
The command reads one pull as JSON on standard input: `repo`
(`knotless://<owner>/<name>`), `pull` (its AT-URI), `title`, `body`,
`author` (a handle, or a DID where none resolves), `base` (the branch it
merges into), `head` (`repo`, `branch` and the `commit` it is at), `round`
(from 1), `rounds`, `changes` (how many times it has asked for changes
on the pull), `approved` (the head its last review approved, or
`null`), `earlier`, its own earlier reviews of the pull,
each `verdict`, `body` and `commit`, and `instructions`, what
`.knotless/review.md` says on the pull's base branch, when it is there. It writes `{"verdict": "approve" |
"changes", "body": "...", "lines": [...]}` on standard output, each of
`lines` `{"line": {"path", "line", "startLine", "side"}, "body"}` as
`comment add --line` takes them (at most 100); `lines` may be left out. Its
standard error goes to the agent's. A command that exits non-zero,
answers something else, or takes longer than `--timeout`, is logged as
`reviewing failed`, and the pull is read again at the next read. A review
written is logged as `reviewed`, with its verdict. One whose head moves as
the command reads it is read again at the new head, and nothing is
written of the first answer.
`knotless-claude-review` (`crates/knotless-agent/claude-review`, in the
server image at `/usr/local/bin`) is such a command: Claude Code, headless,
reading the pull through `knotless-mcp --read-only`, logged in as the
agent, and following `instructions` and `KNOTLESS_REVIEW_PROMPT` besides
its own rules. It takes `CLAUDE_CODE_OAUTH_TOKEN` (from `claude
setup-token`) or `ANTHROPIC_API_KEY`, `KNOTLESS_REVIEW_MODEL` for
Claude's `--model`,
`KNOTLESS_MCP` for the `knotless-mcp` to run and `CLAUDE` for the `claude`.
`knotless-agent keep [flags] [<handle>/<name>...]` keeps the pulls the
merge queue turned away moving, for the same repositories, with the same
`--jetstream`, `--sweep`, `--settle`, `--once` and `--owner` flags, and
`--attempts <n>` (3), the most times it brings one pull up to date. It
takes no command. Its account must be the owner or a collaborator, as it
pushes to the pull's branch and queues it. See [keeping turned-away
pulls moving](../design/merge-queue.md#keeping-turned-away-pulls-moving).
It reads a pull that is open and not a draft, that the owner or a
collaborator opened from a branch of the repository itself, that the queue turned away at the head its branch
is at now, and whose body has no line `Keeper: off`. When it conflicts
with its base, the keeper merges the base in as `knotless pull update-branch`
does, comments, beginning `Merged` and naming the new head, and queues
it again; one it finds still turned away at the head it gave it, as when
queueing failed, it queues with nothing more said. Where both sides
changed the same lines it pushes nothing, and comments once at that
head, naming the paths. A pull turned away for another reason, pushed
to since, or brought up to date `--attempts` times, it leaves alone.
Each pull it updates, queues or comments on is logged as `kept`, with
`updated`, `queued` or `conflicted`; one it fails on, as `keeping
failed`.
A usage error exits 2 and prints the usage, as `name its command and a
repository, or --owner`; an error at run time exits 1, as `knotless-agent: <error>`.
Watching, it stops at the start if a repository cannot be opened;
afterwards one that cannot be read is logged as `reading the repository
failed`, and it goes on. With `--once`, any such error stops it.
It follows Jetstream as knotless-triage does. It reads every repository
on connecting; a pull or a queue record written about a watched
repository or any of an owner's, a repository record an owner writes, or a ref pushed by an
account whose pull asks it, has it read the repositories again after
`--settle`.
## Environment
### Identity and login
| Variable | Default | |
| --- | --- | --- |
| `KNOTLESS_IDENTIFIER` | the last login | the account to act as |
| `KNOTLESS_PASSWORD` | | an app password, instead of OAuth: `KNOTLESS_IDENTIFIER`'s, used only to log in as that account, and only when it has no stored session that still refreshes: an OAuth one whose refresh fails other than by its server turning it down (`invalid_grant`) is kept and tried again, the failure logged as `login refresh failed`, rather than replaced by the password's, which reads no private repository |
| `KNOTLESS_BROWSER` | the system's | the command an OAuth login opens its page with |
| `KNOTLESS_SERVICE` | | the PDS every identity is served by, skipping DID resolution |
| `KNOTLESS_PLC_URL` | `https://plc.directory` | the PLC directory |
| `KNOTLESS_HANDLE_RESOLVER` | `well-known:https,https://public.api.bsky.app` | where handles are resolved, in order: `well-known:<scheme>` for `/.well-known/atproto-did`, or a service URL for XRPC |
| `KNOTLESS_DID_WEB_SCHEME` | `https` | the scheme `did:web` resolves over |
| `KNOTLESS_IDENTITY_TTL_SECS` | 3600 | how long a resolved identity is cached |
| `KNOTLESS_CA_CERTS` | | a PEM bundle of extra CAs to trust, such as a local network's own |
### Servers
| Variable | Default | |
| --- | --- | --- |
| `KNOTLESS_APPVIEW` | `https://knotless.dev` | the `knotless-serve` index that lists pulls and issues on public repositories; set empty, there is none. `knotless-serve`'s `/mcp` reads its own index before the default |
| `KNOTLESS_MODERATOR` | the appview's | the DID of the moderation service reports go to, by its `#atproto_labeler` service |
| `KNOTLESS_ADMIN_DIDS` | | on `knotless-serve`, the accounts that operate it, comma-separated: the admin methods take their service tokens, for the `admin` commands |
| `KNOTLESS_BOT_DIDS` | | on `knotless-serve` with `--bots`, the accounts that may log in at `/oauth/bot`, comma-separated |
| `RUST_LOG` | `info` | what `knotless-serve` and `knotless-run` log, as JSON lines on stderr: `debug`, or `info,knotless::outgoing=debug` for each call made, `knotless::git=debug` for each git command |
| `KNOTLESS_SLOW_MS` | 1000 | on `knotless-serve` and `knotless-run`, how slow a request served or made, or a git command, is before it is logged at warn, and on `knotless-serve` a tenth of it, an index's SQL statement; on `knotless-run`, a pass taking this long logs `pass ended` at info rather than debug |
### Storage
| Variable | Default | |
| --- | --- | --- |
| `KNOTLESS_INLINE_MAX` | 192 KiB | the largest pack kept inline in its record, at most 256 KiB (262144 bytes), the lexicon's limit |
| `KNOTLESS_CHUNK_SIZE` | 4 MB, or the PDS's blob limit if smaller | the size of a large pack's, or a release file's, blob parts |
| `KNOTLESS_COMPACT` | `background` | how the owner's `git push` compacts: `background` (a detached `knotless repo compact --due`), `inline` (before the push returns) or `off` |
| `KNOTLESS_COMPACT_THRESHOLD` | 32 | the packs, counting the push's own, at which the compaction the owner's `git push` starts folds them |
| `KNOTLESS_COMPACT_GRACE_SECS` | 600 | how old a pack must be to be compacted away |
| `KNOTLESS_CLAIM_GRACE_SECS` | 86400 | how old a claim must be to be dropped |
### Client
| Variable | Default | |
| --- | --- | --- |
| `KNOTLESS_TIMEOUT_SECS` | 60 | a request's timeout; connecting times out after 10 s, and an idle connection is dropped after 4 s. A handle's `/.well-known/atproto-did` and a `did:web` document get 3 s, as atproto's resolvers do |
| `KNOTLESS_POLL_MS` | 10000 | how often `--wait`, on `run list` and `pull merge`, reads checks again |
| `KNOTLESS_CONFIG_DIR` | `$XDG_CONFIG_HOME/knotless`, or `~/.config/knotless` | where sessions are kept |
| `KNOTLESS_CACHE_DIR` | `$XDG_CACHE_HOME/knotless`, or `~/.cache/knotless` | where mirrors and caches are kept |
| `KNOTLESS_NO_UPDATE_CHECK` | | set, the appview is not asked for its newest release or the oldest version it asks for |
| `KNOTLESS_IGNORE_MINIMUM` | | set, run although older than the appview asks for |
| `KNOTLESS_INSTALL_DIR` | the directory the binary is in | where `knotless upgrade` installs |
A number that does not parse is its default. Every process shares one
HTTP client, so requests reuse connections.
The config directory holds `sessions/<did>.json` (mode 0600), `me` (the
last account logged in), `credentials/` (space credentials) and, on a
gateway, `oauth-client-key`. The cache directory holds:
| File | |
| --- | --- |
| `identity.json` | handle or DID to DID and PDS, for `KNOTLESS_IDENTITY_TTL_SECS` |
| `handles.json` | DID to verified handle, as long |
| `servers.json` | each PDS's blob limit, as long |
| `keys.json` | each repository's key, and whether it was found public or in a space; never expires |
| `emails.json` | the accounts whose first-push email claim was already looked up here |
| `update.json` | the appview's newest release and the oldest version it asks for, and when it was asked, for a day |
| `mirrors/` | the bare mirrors and their lock files |
| `tmp/` | scratch repositories and indexes, removed when done |
Each JSON file is written whole and renamed into place. A missing or
unreadable one reads as empty.
### Install script
| Variable | Default | |
| --- | --- | --- |
| `KNOTLESS_INSTALL_DIR` | `$HOME/.local/bin` | where the binaries go |
| `KNOTLESS_VERSION` | the latest | which release |