The wsp skill

What wsp mcp install --agent <id> puts into an agent's skills folder, word for word, so the agent reads it before it opens a thread, sets a person up or forks a machine.

A project is a folder on one computer, a git repo or not, recorded with wsp add, and coding agents work on it as threads. On the computer the app runs on a thread runs in the project's folder itself, beside any other thread there, on the agents already on its PATH; a thread on another branch runs in a worktree of the project's repo, the one git already has that branch checked out in or one wsp makes under its own folder with the project's .env files and installed dependencies carried in. On a box the person joined a thread runs in the project's folder there the same way. The host on the person's computer (the service the desktop app installs, or the first command line that needs one, which starts it) owns the threads and the keys; the wsp command line and the wsp MCP server are thin clients of that same host, so whatever you do here shows in the person's sidebar and they can read and answer any thread. Start with wsp projects to see the projects and wsp threads to see the threads in them, each with its folder and branch. Open a thread with run, giving the project, the message and the agent to run, and --branch for a worktree on another branch; from a thread, run with no project runs beside you in your folder. It returns the reply as soon as the agent gives it, and the thread reads running until the agent process exits. Continue a thread with send, which is also how one thread talks to another, by that thread's id off threads: a send is never refused for meeting a turn, it joins a running turn where the agent takes a message mid-turn and otherwise runs as that thread's next turn, and the answer comes back as its own message when the other thread's start named you. A thread reaches every thread of its own tree, the lead that started it, the threads beside it under that lead and the threads it started, wherever each runs, and nothing else: the person's own thread and another lead's tree in the same folder are left out of threads, a send into either reads as no such thread, and work that has to cross to them goes through the person. Stopping a thread stops every thread under it, so a child that stops its lead stops its siblings and itself with it. Both take detach, which answers with the thread id the moment the turn is started and leaves the reply to the thread's finished line. Which road you take for that line is decided by whether you are a wsp thread yourself, which the launch environment says: a thread starts every child with --notify me and ends its turn, and the child's finished line, carrying its whole report, wakes it as a message; a caller that is not a thread takes the reply of one turn as run or send returns it, and for more than one turn at a time starts one coordinator thread in the project's folder on this computer with the whole job and hands off, telling the person where to read it. stop ends a thread's running turn; wsp worktree <project> <branch> answers a worktree's path to work in by path; run a command in the folder a thread works in with exec; delete takes a thread away with its turns. An agent that wants a pull request runs git push and gh pr create itself. Setting someone up from nothing is its own sequence, and the next section is that sequence: a health check, then recipe_scan, which writes nothing and gives every row a recommended value with one line of why, so you apply those and put only the rows whose reason says worth a question, then recipe with their answers, then wsp init --recipe <path> --non-interactive --json from a shell, which builds their image and prints one JSON line per sign-in for you to hand to the person, since the sign-ins finish in their browser.

Setting a person up from nothing

Asked to set a person up, read this section before running a single verb: a verb's refusal is a branch in this list, not an answer to hand back. The road is a health check, then wsp recipe scan, which prints every option and writes nothing, then two questions to them, the heavy rows with their sizes and the sign-ins with their default choice, then wsp recipe --tick used with their answers, which writes the recipe, then wsp init --recipe ~/.wsp/recipe.json --non-interactive --json, which you run detached from a shell: it builds their image and prints one JSON line per sign-in, the page and the code the person finishes in their own browser, and waits for them, then wsp add for the project and wsp run <project> for the first thread. Two things are theirs and never yours: the sign-ins, which no tool can finish for them, and the keys, which live in their .env and are never typed into your terminal or read back to them. Starting the host is neither: any command that needs one starts it, and it reads those keys off the file itself, so nothing about them passes through you. Every step below ends in an Expect: line; run the command, read that line, and stop at the first one that does not match rather than carrying on. Read where they already are before running anything, and skip the steps that state has passed.

  1. The health check comes before any verb: wsp --version. With no wsp on the path and a shell of your own, npm i -g @wsp-labs/wsp puts the wsp command there; prefer that over npx @wsp-labs/wsp, whose cache write fails under a sandboxed agent and leaves every command after it running with the sandbox off. Then wsp threads --json. A host is what answers it, and when none serves that state file this command starts one and waits for it, so there is nothing to start by hand. (wsp doctor is not this check: it forks a live machine to prove the whole reach path, it bills while it runs, and it does not speak JSON.)

    Expect: wsp <version>, then one JSON line on stdout, {"threads":[]} when nothing is running yet: go to step 6. A starting the host for <path>; its log is <path>, and wsp down stops it line on stderr before it means none was serving and this command started one, which is the ordinary road and not a failure to report. no host answered for <path> within 20.0s, with the log's last lines under it, means the host it started could not serve: read that log. A Threads on <name> run in <folder> on this computer, under your own sign-ins. line in it means the state held no image, so the host recorded that folder as a project and served that: go to step 3 to seal an image, or to step 7 when this computer is enough to start on. A command not found with no shell to install from is the one thing to say and stop on, because nothing below can be run for them.

  2. The keys are the person's. Ask them to write ANTHROPIC_API_KEY=<key> into ~/.wsp/.env when they pay Anthropic by the token; on a Claude subscription they skip the Anthropic one and sign in on the machine during init instead. Do not ask them to paste a key into this conversation, do not print that file, and do not commit it. Then take the host that started without a key down with wsp down and run step 1 again, so the new host reads the file.

    Expect: wsp threads --json answers with one JSON line.

  3. Read everything before writing anything: wsp recipe scan prints every option and writes no file. Five tables: the agents, with what each one's history on this computer says; the tools, each with why it is there (used 412 times in 37 sessions, installed here, never used, catalog default), its download size and, in the last column, what to do about it; what else a package manager on this computer has that the image could take, by manager, with the line that installs each and its size; the commands their agents ran that the catalog does not carry, with counts; and the sign-ins, each with the choice that would be taken by default. --json gives all of it as one object with a recommended value and reason on every row, which is the form to read when you are deciding rather than showing. --project <folder> weighs the histories by a folder, so a setup for one project counts that project's sessions first. Read the why and the size on every row instead of taking the defaults as given: they come from what their agents actually ran, not from what is installed, and an agent with no thread adapter stays off, which is right, since an image carries the agents that can run threads unless the person asks for another.

    Expect: the five tables on stdout, the agents and tools tables each ending in an On: line with the count and the total size, then Nothing was written. as the last line; wsp recipe scan --json answers with one object holding the same rows.

  4. Put the two decisions to the person, each as one message, then write the recipe with their answers. First the heavy rows, over 300 MB, as one multiple-choice question with the size beside each and what the answer does to the total. Then the sign-ins, naming the default choice on each row and what the choices mean: copy it from this computer, sign in during the build, sign in when you first need it, hand it an API key, mint a token here that every turn gets, or skip it. Then write: wsp recipe --tick used, plus --set <id>=on|off for every row they flipped and for anything from the scan's own-computer table they want on the image, --add <id>="<install line>" only for a tool neither the catalog nor this computer has, --signin <id>=copy|machine|later|key|skip|token for every sign-in they chose, and --project <folder> when the setup is for one project, so that project's history weighs first. All of them repeat, all of them go in one call, and the file is never edited by hand.

    Three heavy ones are ticked because they are installed on your computer, and your agents never used them:
    Java 21 (620 MB), Gradle (410 MB), Android SDK (1.2 GB). Which do you want on the machines?
    (a) none of them, 3.4 GB down to 1.2 GB   (b) Java only   (c) all three, and the build takes longer
    

    Expect: the tables again with every flip in them and the total moved by their sizes, then Recipe written to <path>. opening the last line, ~/.wsp/recipe.json when the state file is the default one and a path beside their state file when it is not. That path is the one step 5 runs with.

  5. Build their image. Run init yourself, detached, with the recipe step 4 wrote:

    nohup wsp init --recipe ~/.wsp/recipe.json --non-interactive --json > /tmp/wsp-init.jsonl 2> /tmp/wsp-init.log &
    

    The image is built on a computer of theirs that wsp add user@host joined, which --on <computer> names; with none joined this run builds nothing and records the folder it was run in as a project on this computer, the line step 1 quotes. It boots one builder machine, installs what is ticked, and at each sign-in the recipe answered machine it prints one JSON object on stdout and waits: {"event":"sign-in","tool":"gh","label":"GitHub CLI login","browserUrl":"https://github.com/login/device","code":"8F4A-C21B","nextCommand":"open 'https://github.com/login/device'","waitSeconds":960}. Hand that line to the person as it comes: the page to open on their computer, the code when there is one, and the command that opens the page. No thread and no tool can sign in for them; the run asks the tool's own status on the machine and moves on when it says signed in, or when waitSeconds pass. Read /tmp/wsp-init.jsonl as it grows rather than waiting on the process, which keeps serving the app after the seal. When the person would rather drive the wizard themselves, wsp init --recipe ~/.wsp/recipe.json in their own terminal draws six screens, Agents, Tools, Also on this computer, Sign-ins, wsp for your agents on this computer, and Build, of which the recipe has answered the first three, so their run opens on Sign-ins and ends on Build. A host already serving that state file stops none of this: the screens are the same and the build runs in that host, which records the image in the state it serves, so a box whose host is a service needs nothing taken down. That road refuses --json, since the build's objects then ride the host's own setup, which wsp setup --json reads; run init without --json beside a serving host, or take it down with wsp down first.

    Expect: one {"event":"sign-in-result","tool":"gh","label":"GitHub CLI login","state":"signed-in"} line per hand-off ("state":"not-signed-in" with a note saying why when the person did not finish in time; the run seals either way), then Image v<n> sealed. in the log, which is the seal, and Open http://127.0.0.1:4400/. A JSON line from wsp threads is not that proof: init serves a host for the whole wizard, before the sign-ins and the seal, so step 6 is what tells a sealed image from an init still running.

    When the person set up from the app instead, from its Settings, the same run is a job on their host: wsp setup --json answers with its phase, its rows and each sign-in with the page waiting for them, so hand an open one over as you would the line above, and never start a second init beside it.

  6. Where threads run. On this computer nothing is made: a thread runs in the project's folder, and on a box you added in the project's folder there.

    Expect: wsp computers lists this computer and each box you added.

  7. Record the project: wsp add <folder> for a folder on this computer, a git repo or not, a subfolder of a repo being a project of that repo; wsp add <url> --into <folder> for a repo cloned here into an empty folder, which is then that folder's project; wsp add <url> --on <computer> for a repo the computer clones, or wsp add <owner/repo> --on <computer> for one its signed-in gh or glab clones. wsp add <folder> --on <computer> is the fourth road: that computer clones the folder's own remote and the folder seeds what git ignores on top, so the line prints a menu of those paths with their sizes and sends nothing until --yes; the person picks with --keep <path>, --cut <path>, --no-memory and --no-commits, and --remember keeps their ticks for the next add of that folder.

    Expect: wsp projects lists it, with its folder and, for a folder here inside a git repo, the branches --branch takes.

  8. First thread: wsp run <project> --notify me "<the first message>" runs it in the project's folder; --branch <branch> runs it in a worktree on that branch instead.

    Expect: thread <id> on <project> in <folder> as the first line on stdout, the reply on stdout when it is complete, and the same thread in the person's sidebar for them to answer. --notify me puts each later turn's reply in front of them, so nobody polls.

Last, say what to run next inside their own agent, which is where the work happens from here. wsp mcp install --agent <id> puts the tools and this skill into that agent (repeatable, and --json for a machine to read; the agents the catalog knows a config for are listed under the verbs below). The tools show up only after that agent restarts, and the wsp command line does the same job until then, so nothing waits on the restart. In Claude Code the skill is then /wsp; in any agent the line to paste is Use the wsp skill and open a thread on <project> that <the first message>.

Expect: that agent lists the wsp tools once it has restarted, run answers with a thread id, and that thread shows in the person's sidebar.

Verbs and tools

The command line and the MCP server call the same functions. Every verb takes --json (one JSON object per line, frames first, the last line the result; see the contract below) and --state <path> (the state file the host serves; default ~/.wsp/state.json, or ./.wsp/state.json in a checkout of wsp itself). A project is named by its name or its id, and a thread by its id or by a prefix that picks exactly one. Each tool's inputs are in the parentheses after its name; a flag on the command line is the input of the same name on the tool, --add-check being add_check, and a repeatable flag is an array.

command lineMCP toolwhat it does
wsp computerscomputersevery computer this host holds, which is the whole of where work can run: the computer the app runs on, each box joined to it. A box's row carries what it last reported (cores, memory, free disk, the engine it has), whether it is connected right now, MACHINES, how many it holds of how many it has room for, THREADS on a computer, how many run there now against its cap (threads at once), and STATE, one word for the row: Setup failed, Needs you while a sign-in waits on the person or a folder or the GitHub sign-in did not land, Setting up while its setup runs, Ready, Full once that count meets the cap or a box has no room, or why it takes nothing. A project lives on a computer: on the computer the app runs on and on a box its threads run in its folder, on a box as the login the box was added with. pending holds every add that has not reached Set up, its STATE Pending with how far it got, or Setup failed with why
wsp computers set <computer> [--name <new>] [--ssh <login>] [--threads <n>] [--nap <minutes>|off] [--turn-limit <hours>|off] [--spawn on|off] [--max-machines <n>] [--max-depth <n>] [--recipe <name>|none] [--reset <setting>]...computers_set (computer, name, ssh, threads, nap, turn_limit, spawn, max_machines, max_depth, recipe, reset)what the person sets on one computer, answered as its row in wsp computers: --name, what a computer the person added is called from now, in every listing and wherever a computer is named, its id, its projects and its machines staying as they are, refused for the computer the app runs on and for a name another computer answers to; --ssh <login>, the login user@host the host reaches a computer you added by over ssh from now, for an update or a remove while its link is down and for the forward it dials back through, saved only once the computer that login reaches reads as this same one (its place file names this computer and this host), so a login whose alias moved to another machine is refused and nothing is written; every flag is checked before any is written; --threads, how many threads may run there at once, a new one waiting past it, except a turn a thread there follows to its end, not detached, which runs in that thread's slot, which is one per 2 GB of that computer's memory up to twice its cores until it is set; --nap, the minutes a machine there with no window of its own runs with no turn and no work before it naps and stops costing anything, waking on the next message (20 until it is set, up to 180, off never naps it, 0 on the tool), counted again from now on every such machine there, which the computer the app runs on does not take since its threads run in folders; --turn-limit, the whole hours one turn there may run before it is stopped, its thread reading failed with a line that names the limit, and a send carries it on from where it stopped (off on a computer the person owns, since the cut for a turn that does nothing already ends a stuck one and nothing there bills by the hour, up to 24, off never stops one, 0 on the tool), read as each turn starts; --spawn, --max-machines and --max-depth, what the agents there may ask of the host where the folder or machine they run in holds no switch of its own (on, 3 machines, 2 levels deep, until it is set; 2 lets a thread a thread opened open its own), read at every ask so one made before the change follows it. A setting left out keeps what stands, and --reset <setting> takes one back to its default by its flag's word. The row says each setting at what it runs at, with the default beside one that was set. A setting the computer does not take, and a line that sets nothing, are refused in one line. --recipe <name> makes it follow that saved recipe: what the recipe holds and the computer lacks goes on, what it took out comes off, and every later change to the recipe, or to a skill, config or server it holds on this computer, reaches it with no step; the row reads Behind while a change is on its way. --recipe none keeps what it has and follows nothing
wsp add <user@host>|<ssh alias> [--recipe <name>] [--later] [--name <name>] [--ssh-port <port>] [--ssh-key <path>] [--host-key <key>], wsp add <computer> --resume [--recipe <name>] [--later]; the same line's other roads take [--update] [--sign-in <agent>] [--on <computer>] [--into <folder>] [--base <branch>] [--yes] [--keep <path>] [--cut <path>] [--no-memory] [--no-commits] [--remember]add (address, recipe, later, resume, name, ssh_port, ssh_key, host_key)adds a computer of the person's over ssh and sets it up from a saved recipe: checks it can run wsp (root, or a login whose sudo runs it as root, systemd with cgroup v2, room on its disk), installs wsp, then the base tools and everything the recipe picks. A sudo that asks for a password is asked once at a terminal, without echo, or in the app's Add a computer, and the tool refuses it with that line; --json prints each step as a setup frame and each sign-in waiting on the person as a waiting frame with its page and code, the last line the computer. At a terminal a sign-in waits there, --later leaves it waiting and goes on. Without --recipe the computer joins and waits on its picks; --resume sets it up, from --recipe or from what it holds, running only what is missing. The tool never waits on the person: it answers at the first sign-in waiting, the end or ten minutes, and is called again with resume. The line also takes a folder, a repo, a provider and the join code, below
wsp recipesrecipesevery saved recipe: a named pick of what goes on a computer of the person's (agents and how each signs in, MCP servers per agent, CLIs, skills, plugins, folders and the git, shell and GitHub configs), one line of what each holds, the computers that follow it and when it was saved; a recipe holds names, never a secret
wsp recipes show <name>recipes_show (name)one recipe whole, every row by kind, the computers that follow it and the hash it resolves to on this computer now
wsp recipes save <name> --from <computer>recipes_save (name, from)saves what one computer was set up with as a recipe, which that computer then follows; refused for a computer set up before picks were kept
wsp recipes remove <name> [--yes]recipes_remove (name)takes a recipe away; its computers keep what it put there and follow none
wsp usage [--range day|week|month] [--by agent|account|computer|project|model|source]usage (range, by)two answers that are never added together. ACCOUNTS: every agent account signed in on any computer, the same account on three computers once, with how much of its plan's session and week is used and when each starts again, as the agent itself printed them in the last turn wsp ran on it; an agent that prints none reads reports no plan limit, a sign-in by API key reads pays per token, no plan limit, and an account no turn has run on yet reads not read yet. USED: the tokens the turns used today, over the last seven days or the last thirty, one row per agent, account, computer, project, model or source (a model once for each agent that ran it; by source, wsp's threads in one row and what the agents on this computer and on each joined one logged outside wsp in another), fresh in, written, cached and out, with the cost the agent reported or a list price off LiteLLM's table (not priced where the table has no entry, beside what the rest came to). Work done outside wsp, which each computer's own daemon reads off its agent logs (a computer away keeps what its last read found), shows on the Usage page, and here only by source, as that other row. A thread's own token reads neither: an agent does not read the person's accounts
wsp usage reset <account> [--credit <id>] [--on <computer>] [--yes]none, the command line alonespends one of the resets a Codex account has banked, the account named by the key wsp usage --json prints, its label or the address it signed in as. It reads the account first on a computer of the person's that holds its login, the first connected one or the one --on names, and refuses a sign-in by API key, a login that lives only on a provider's machines and one that is now another account's, with nothing spent; then it spends one, the one --credit names or else whichever Codex picks, and reads the account again. It asks first on a terminal and is refused off one without --yes. There is no tool for it and a thread's own token is refused it: spending is the person's act
wsp agents [<thread>] [--on <computer>]agents (thread, on)the coding agents the catalog knows as they stand on this computer, a box you added or where a thread runs: whether each is on that login's PATH and where, its version, the newest its vendor publishes (read by the host, kept a day, none with Newest agent versions off in Settings > Privacy or WSP_UPDATE_CHECK=0) and the version wsp's install pins, its sign-in there (signed in, your key from this host's vault, not signed in, unknown), how a person signs it in, and whether it carries the wsp tools. Read as the login the computer was added with, off config and whether files are there: no MCP server is started and no login file is opened. A napping machine answers what stood there when it last ran and is not woken. A word that names a project rather than a thread is refused saying so
wsp skills [<thread>] [--on <computer>]skills (thread, on)every skill there, one row per folder name with its description and every folder it lives in, the agent whose own folder each is (none for the shared ~/.agents/skills) and where a folder links to; a thread adds its project's own skills, and a plugin's skills are marked as the plugin's
wsp servers [<thread>] [--on <computer>]servers (thread, on)every MCP server each agent's config file defines there, and a thread's project files, and on any computer but the one the app runs on the wsp server each thread's launch hands over, marked launch with no file: the agent, the file, how it is reached with every value hidden, the names of its variables and never their values, whether the file switches it off, whether wsp's recipe put it there on a box, and its sign-in as the config says it (open, or unknown for a remote server until something connects)
wsp skills search <query> [--limit <n>]skills_search (query, limit)skills on skills.sh whose words match, as skills.sh ranks them, each with its repo, how many times it was installed and the <owner>/<repo>/<skill> that wsp skills add takes; the host asks skills.sh, and an empty query is refused
wsp skills show <skill> [<thread>] [--on <computer>] [--project [<name>]]skills_show (skill, thread, on, project)a skill's SKILL.md, its first 64 KB and the whole file's size: one on skills.sh by its <owner>/<repo>/<skill> with nothing installed, or one already there by its name, the project's of that name with --project alongside a thread, the thread's own project, or --project <name> with --on for that computer's project; nothing in it runs
wsp skills add <skill> [<thread>] [--on <computer>] [--agent <id>]... [--project [<name>]]skills_add (skill, thread, on, agent, project)installs a skill off skills.sh as the login the computer was added with: its files land once in ~/.agents/skills/<name>, the project's .agents/skills with --project alongside a thread, the thread's own project, or --project <name> with --on for that computer's project, and each agent named that does not read that folder gets a link or a copy in its own; with no --agent, every agent whose own folder's home is there gets it. The download is checked whole first (every path plain and inside the skill, at most 200 files, 1 MB each and 5 MB in all, a SKILL.md at its root), every file lands 0644 and nothing in the skill runs; a skill already there is refused
wsp skills remove <name> [<thread>] [--on <computer>] [--project [<name>]] [--yes]skills_remove (name, thread, on, project)every folder of that skill and every link to it, gone, the project's with --project alongside a thread, the thread's own project, or with --project <name> and --on for that computer's project, where the word after --project is always the project's name (--project deploy --on spoo names the project deploy); a folder a link points to outside the skills folders stays; the skill wsp writes and a plugin's are refused
wsp skills disable <name> [<thread>] [--on <computer>]skills_disable (name, thread, on)turns a skill off by renaming its SKILL.md to SKILL.md.off where it lives, so no agent loads it, with no agent config edited; the skill wsp writes and a plugin's are always on, and a project's lives in the repo and is refused
wsp skills enable <name> [<thread>] [--on <computer>]skills_enable (name, thread, on)turns a skill that was turned off on again, its SKILL.md.off renamed back
wsp servers tools <name> --agent <id> [<thread>] [--on <computer>] [--project <name>] [--refresh]servers_tools (name, agent, thread, on, project, refresh)starts that one server once where it is set up, as the login the computer was added with and with the command and variables its agent's config gives it, or asks its address once from there, and lists its tools with their descriptions and its sign-in as that connect found it; stopped within 20 seconds, the answer kept three minutes as the server's state unless --refresh or a sign-in there ends, an edited entry asked again; a server behind a sign-in its agent holds brings no list, since no login file is read: Claude Code is asked for its word on it, and for any other agent it answers unknown, naming that agent as the one holding the sign-in; a napping machine is not woken
wsp servers add <name> [<thread>] [--on <computer>] --agent <id> (--command "<line>" | --url <address> [--header <name>=<VARIABLE>]...) [--env <NAME>]... [--project [<name>]]servers_add (name, agent, thread, on, command, env, url, header, project)writes one MCP server into that agent's own config as the login the computer was added with, the project's file with --project alongside a thread, the thread's own project, or --project <name> with --on for that computer's project: a command line (split into the program and its arguments as a shell splits it, nothing expanded) with its variables, or an address with its headers. Each value is read by name off the environment the command or the wsp tools run with (--env NAME reads $NAME, --header Authorization=TOKEN reads $TOKEN) and never goes into an answer: on this computer it goes into that file and a variable's value into the vault as well, and on any other computer the file names a variable (${NAME}, or WSP_MCP_<SERVER>_<HEADER> for a header, and for a Codex copy of an Authorization: Bearer header WSP_MCP_<SERVER>_AUTHORIZATION_BEARER, read whole, which the vault fills from the token it keeps alone under WSP_MCP_<SERVER>_AUTHORIZATION) and the value goes to the vault, which hands it to each turn there (on a computer the person added only to Claude Code and Codex, in that turn's launch and never its environment, and not to a Codex server whose own config reads its token through bearer_token_env_var; elsewhere in the turn's environment); an argument or the address may name a variable given with --env as ${NAME} (single-quoted so the shell leaves it; an address's variables are only the ones it names): on this computer the value is put in place, on another computer the name stays in the agent's own syntax for Claude Code, Gemini CLI and OpenCode, and Codex, which reads no variable in either, is refused; a name already in the file is refused rather than written over; the file keeps its mode, a new one is the login's alone, and a file that is a link out of the home is not written through
wsp servers remove <name> [<thread>] [--on <computer>] --agent <id> [--scope <user|home|project>] [--project [<name>]] [--yes]servers_remove (name, agent, thread, on, scope, project)takes that one server's entry out of the agent's config, in the scope wsp servers lists it under, every other server and line of the file as it was, and, once no agent's config on this computer lists that server, frees the vault's values kept for it that no other server holds
wsp servers disable <name> [<thread>] [--on <computer>] --agent <id> [--scope <user|home|project>] [--project [<name>]]servers_disable (name, agent, thread, on, scope, project)turns that server off by the switch its agent reads (Codex's enabled = false, OpenCode's enabled, Gemini CLI's mcp.excluded), so the agent leaves it out; Claude Code keeps no such switch per server and is refused
wsp servers enable <name> [<thread>] [--on <computer>] --agent <id> [--scope <user|home|project>] [--project [<name>]]servers_enable (name, agent, thread, on, scope, project)turns a server that was turned off on again
wsp agents addtools <agent>agents_addtools (agent)writes the wsp server into that agent's own MCP config on the computer the app runs on, the entry wsp mcp install writes, with this skill beside it, and answers the file; a thread on any other computer is handed the wsp tools with every turn, so nothing is written there
wsp agents default <agent>agents_default (agent)the agent a new thread runs when neither its start nor its project names one, on every computer and in the app; the catalog's first without it. A project's own agent wins over it, and an agent turned off on a computer is passed over there
wsp agents set <agent> [--model <slug>] [--effort <word>] [--access <word>] [--hide <model>]... [--show <model>]... [--order <model,model>] [--add-model <id>]... [--drop-model <id>]... [--reset <field>]...agents_set (agent, model, effort, access, hide, show, order, add_model, drop_model, reset)one agent's defaults on every computer: the model, effort and access (ask, auto-edit, full or plan) a new thread on it starts on, and its model picker: models hidden (a start still takes one by name), the order it lists them in, and ids the binary does not list that a start may then name. A project's own wins over these and what a start names over both; a word the agent has no mode for is refused naming the ones it takes, and --reset model|effort|access|models puts one back on the agent's own
wsp agents setup <agent> [--on <computer>] [--enable | --disable] [--program <path>] [--config <folder>] [--arg <word>]... [--env <NAME>]... [--unset-env <NAME>]... [--reset <field>]...agents_setup (agent, on, enabled, program, config, args, unset_env, reset)how one agent runs on one computer, this computer without --on: off takes it out of the app's lists there and refuses a start naming it with the computer named; the program run in its place; the folder it keeps its config, sessions and sign-in in, under that computer's home once its links are followed and outside wsp's own folder (sign it in again there, since a login under the old folder does not follow); words added to every turn's launch; and variables every launch carries, never one that decides how the process starts or what it loads (PATH, HOME, LD_ and DYLD_ ones, NODE_OPTIONS and the like) nor one of wsp's own, each value typed at the line where nothing echoes it and never on the tool, which only takes them off. Answers the agent's row with its variables by name alone
wsp projectsprojects, projects_add (base, into, name, on, source)every project this host holds: name, id, the computer it lives on, where its code comes from (a folder on this computer, or a repo a computer clones), where its folder sits, the branch a machine of it starts on, and how many threads it has. The name is what wsp run takes. projects_add records one: source is a folder on the computer the app runs on, a git repo or not, or a repo's url, into an empty folder there to clone a repo into, on names the computer that clones a repo, name what to call it here and base the branch a machine of it starts on. The command line's word for the same thing is wsp add, which also joins a computer and takes a provider's key
wsp projects remove <project> [--yes] [--force]projects_remove (project, force)takes a project's record out of this wsp with the folder wsp made for it on the computer holding it; a folder of yours on this computer stays. Refused while a machine of it stands, naming them, and stopped while its checkout on a computer of yours holds work no remote has (commits on any branch or HEAD no remote has, uncommitted files, stashes), naming it by its path there, unless --force
wsp projects set <project> [--agent <id>] [--model <slug>] [--effort <word>] [--access <word>] [--after-worktree <command>] [--reset <field>]...projects_set (project, agent, model, effort, access, after_worktree, reset)what a new thread on one project starts on, over each agent's own defaults and under what a start names: its agent, model, effort and access. Nothing about a computer is a project's to set. --after-worktree is a shell line a new worktree of the project runs once at its top, after each ecosystem's own install, for what none of them knows (--reset after-worktree takes it away). A model or effort kept for the project's agent drops to the agent's own on a thread on another agent; a word the project's agent has no mode for is refused. Answers what a new thread there now starts on, each value with where it came from; wsp projects --json carries the same for every project under defaults
wsp threads [<project>] [--tree] [--watch]threads (project)the sidebar's rows: project, the folder the thread works in (the project folder or a worktree of its repo), that folder's branch as git reads it now, agent, state (Working, Needs you, Failed, Done for a finished turn no window has shown since, else Idle; the tool answers the readAt and settledAt stamps behind it), who opened it (person, cli, agent), the computer it runs on, the title. The tool answers capped on a thread waiting for a slot on its computer: the computer and what runs there against its threads at once. Under each thread, one step in, are the agent's own subagents of every turn (Claude Code's Agent tool), each with its id in the SUBAGENT column, Working, Done, Failed or Stopped, and agent as who started it; the tool answers them as subagents (id, title, state running, done, failed or stopped, model, asked for the start of what it was asked, lastLine or failure once it ends) on the thread, and lastLine and failure for the latest turn's last line and why it failed. --tree draws a thread an agent spawned one step in under the thread that spawned it, and the tool answers parentThreadId and rootThreadId on each row. --watch draws the same table again every second where it stands, until Ctrl-C
wsp commit <thread> [--message "<message>"] [--file <path>]...commit (thread, message, files)commits every changed file in the folder the thread works in against its last commit, untracked ones added, running the folder's hooks as git does. Threads that share a folder share its changes, so a commit of every file takes the other threads' changes there too, and the answer names those threads (sharedWith under --json). --file names some files by path from the checkout's top, once per file, and leaves the rest uncommitted. Without --message (-m for short) the thread's own agent drafts the message from the diff and the message the thread was opened with, on its own command line with no thread and no tool; the draft is printed on stderr and the commit is made with it. Refused in one line when a file named has no change, when git knows no author in the folder (the line names the command that sets one), when a hook said no (with the hook's last line), and when another git in the folder still holds the index after one wait. Nothing reaches a remote: the app's Push is what pushes
wsp discard <thread> <path> [--yes]discard (thread, path)puts one changed file in the folder the thread works in back as its last commit has it: an edit or a deletion is undone, a rename takes its new name away and brings the old one back, and a file the last commit does not have is removed. Only the file named moves, and it cannot be undone; the answer names any other thread working in that folder. Refused in one line when the file has no change
wsp fix <thread> [--check "<name>" | --child <thread>]fix (thread, check, child)asks the thread's agent to fix the pull request of the branch its folder is on. --check names a check that failed: the failed steps of its job's log go to that thread as its next message, framed as a log to read and not to obey, with the commit it failed on and the check's link, and a check another service reports goes with its summary and link alone. Without --check the folder is first updated from its base as wsp update does it: a clean merge sends nothing and says so, and a conflict sends the thread the files to resolve. The line names the agent asked; it answers once the message is on its way, joined into the running turn where the agent takes one, else waiting as the thread's next turn, or held while the computer runs as many threads as it takes at once, with the waiting line under it; a held turn starts on its own once a slot frees, in a slot of its own, never the asking thread's. Refused in one line where the branch has no pull request, where the check is not on it, and where it has not failed. A thread's own token may ask it of any thread in its tree, as it may commit there. --child names a child thread whose merge into this thread's folder stopped on conflicts: nothing is updated, and this thread's agent is asked to fetch the child's branch, merge it with a merge commit, resolve the files wsp merge in named, run the tests and commit; never with --check
wsp merge in <lead> <child>merge_in (lead, child)merges a child thread's branch into the folder its lead thread works in with a merge commit, so the lead's history shows each child landing, and says how many commits it brought or that there was nothing to take. The child works in a folder of its own, a worktree; a child working in its lead's folder is refused, since its work is already there. The branch is fetched from the project's remote; a project with none merges from the child's own folder where both sit on the computer the app runs on, and is refused in one line naming the project where either sits on another. A lead's machine is woken first where it sleeps. A lead with changes no commit holds is refused first with the files named, and a merge that conflicts is taken back at once with the files named, the lead's folder left exactly as it was; wsp fix <lead> --child <child> hands them to the lead's agent. Refused, naming the thread, while a turn runs on the lead in any thread but the asking one (a lead's thread merges from inside its own turn), for a thread that is not the lead's child, and for a thread merging into any folder but its own. Nothing is pushed: the lead's own push carries the merges
wsp merge <thread> [--method merge|squash|rebase] [--when-checks-pass]merge (thread, method, when_checks_pass)merges the pull request of the branch the thread's folder is on as you, by --method or the repository's own default, and only while its head is still the commit the host last read, so a push since then fails the merge in the git host's own words rather than landing code nobody saw. --when-checks-pass arms it to merge once its checks pass, where the repository allows it. Refused in one line where there is no open pull request, where the repository does not allow the method or merges nothing by itself, and with the git host's reason where it refused, branch protection included. A thread's own token is refused: merging is the person's act
wsp start <link> [--project <name>] [--agent <id>] [--model <slug>] [--effort <word>] [--access <word>]start (link, project, agent, model, effort, access)opens a thread off a GitHub issue or pull request link. The link names the repository, and the project here whose remote is that repository is the one used; --project names one where two computers hold it, and a link no project matches is refused naming the repository and the wsp add line. The thread's first message is the title, the description, the comments and the link; an issue's thread is asked to have its pull request say Closes #<n>. On this computer an issue's thread runs in the project folder, and a pull request's head is fetched into the project's repo as a branch and its thread runs in a worktree on it, so the project folder's checkout never moves. Prints the thread line as wsp run --detach does. A thread's own token is refused
wsp review <link|thread> [--agent <id>] [--model <slug>] [--effort <word>]review (target, agent, model, effort)starts a reviewer thread on a pull request, off its link or off the pull request of the branch a thread's folder is on: on this computer a worktree on the head, the agent at its read-only access (Codex unless --agent names Claude; an agent with no read-only access is refused naming the two that have one), and the description, the diff and the repository's own review rules in its first message. The answer leads with the reviewer thread's id, which wsp review post takes. Its reply ends in a review the host keeps as a draft, which the app's Pull request pane shows for ticking; nothing reaches GitHub until wsp review post. A thread's own token is refused
wsp review post <thread> [--verdict comment|approve|request-changes] [--summary "<text>"]review_post (thread, verdict, summary)posts a reviewer thread's draft on its pull request as you, in one call: the verdict, the summary and every ticked comment on its line, pinned to the head the review was written against. --verdict and --summary edit the draft first. A comment on a line outside the diff goes into the summary, since GitHub takes none there. Prints thread <id>: review posted on #<n> (<n> comments, <m> in the summary). Refused in one line where there is no review yet, and with GitHub's own reason where it refused, approving your own pull request included. A thread's own token is refused: posting under your name is your act
wsp update <thread>update (thread)merges the latest commits of the base from the remote into the branch the thread's folder is on with a merge commit, so a branch already pushed is never rewritten, and says how many it brought; the answer names any other thread working in that folder. A folder with changes no commit holds is refused first with the files named. A merge that conflicts is taken back at once and the line names the files, the folder left exactly as it was; wsp fix sends them to the agent. Nothing is pushed. A thread's own token may ask it of any thread in its tree, so a lead keeps its children's branches current
wsp imageimagethe image this host owns and the copy each place has built of it: the record is the version, a hash over the recipe it was sealed from and the sign-ins it holds, and the size the builder's disk came to; a copy built at that hash is current and any other is stale, whatever version the place's own manifest gave it. A record that says no sign-ins held was read back off its own copy rather than written at a seal, so it judges none of them, every copy of it asks for the sign-ins again, and cutting the next version holds them.
wsp delete <thread> [--yes]delete (thread, confirm)a thread goes with its turns and checkpoints, and the folder it worked in stays; a thread in a worktree wsp made takes that worktree and every thread in it, refused over files no commit holds, the branch and the project folder left as they are. The tool deletes only when called with confirm true
wsp run [<project>] [--beside <thread>] [--branch <branch>] [--cwd <path>] [--agent <id>] [--model <slug>] [--effort <word>] [--access <word>] [--fast] [--notify <thread|me>] [--title <name>] [--replaces <thread>] [--file <path>] [--detach] "<message>"run (project, beside, branch, cwd, message, agent, model, effort, access, fast, notify, title, replaces, files, detach)an agent works on the project and the reply comes back: a thread in the project's folder, following its first turn to the reply, on a box in its folder there as the login the box was added with. --beside <thread> runs it in the folder that thread works in, on its computer, which is how a fresh thread picks up where a long one stands, and takes no project and no --branch. --branch runs it in a worktree of the project's repo on that branch: the one git already has it checked out in, the project folder or a worktree the person made included, else one wsp makes under its own folder from the project folder's current commit for a new branch, with .env files and installed dependencies carried in. --cwd runs it in a folder inside the project or one of its worktrees, and nowhere else. With no project, from a thread it runs beside you in your folder, and from a terminal inside one of your project folders it goes to that project; the first line says where. With no agent named it runs the one the last thread on that project used; --fast runs the turn in the agent's fast mode on a model that offers one; --file sends a file along, an image as an image and anything else landed in the thread's folder with its path in the message; with --detach, prints the thread id the moment the turn is started and returns. --replaces <thread> makes the new thread the restart of a stopped or failed one: the new row names it, the old row names its restart, and the old one settles once the new one starts, and a restart you start of a child stands under that child's lead; refused while that thread or one under it is working, for a thread that already has a restart (name that restart instead), and from a thread for one that is not under it, each before anything is forked or woken. On a computer already running as many threads as its threads at once the start waits for a slot, says waiting while <computer> is running <n> of <m> threads, and starts on its own when one frees; --detach returns at once with outcome held and capped beside it. A thread whose follow gives up before the turn it follows ends, or that is stopped while that turn runs, works again beside it, and each then holds a slot of its own: the computer runs one past its number until either ends, and nothing new starts there meanwhile. A thread that follows a run or a send to its end, without --detach, lends that turn its own slot, one turn at a time; every other start, a child started with --detach included, waits for a slot of its own, so on a computer set to 1 the children a lead starts detached run one at a time once its turn ends
wsp worktree <project> <branch>worktree (project, branch)a worktree of the project's repo on the branch, answered with its path: the one git already has the branch checked out in, else one wsp makes under its own folder, a new branch starting from the project folder's current commit, with .env files carried in and, for each folder of the branch holding an ecosystem's lockfile where the project folder holds that ecosystem's folder there, or at the top for one that carries nothing (uv, Poetry, Cargo, Go), its relocatable dependency folders carried in, the folders tied to their path (a virtualenv, target) never copied, and its install run once there unless what was carried was built from the same lockfile, then the project's after-worktree command (wsp projects set <project> --after-worktree <command>). made says whether wsp made it, the only kind it ever removes. A running session cannot move into it: work there by path, or start a thread in it with wsp run <project> --cwd <path>. A plain git worktree add works too, without the carried files and the cleanup. One wsp made is read every ten minutes until its work settles: its pull request through the git host's command line, and its pushed branch against the remote; six hours after the pull request merged or closed, or the remote dropped the branch, it is removed if nothing is uncommitted and no thread works there
wsp worktree remove <project> <branch> [--force] [--yes]worktree_remove (project, branch, force)takes away the worktree wsp made for the branch, with git: refused while a thread is working in it, and over files no commit holds unless --force, which loses them. The branch stays, a detached worktree's commit is kept under refs/rescue, and threads that ran there go on in the project folder. A worktree wsp did not make is never removed
wsp thread read <thread> [--last]thread_read (thread, last)the thread's messages as the app lists them, oldest first: who each one is (person, agent, tool for one call folded to a line, turn for the outcome the turn ended with), when the runtime recorded it, and the text; --last gives the final reply alone, the whole message its finished line carries. The transcript is the host's, so nothing on a machine is touched and a paused machine's thread reads the same as a running one's. Reading marks the thread read, as the app showing it does, so a finished thread stops reading Done
wsp thread head <thread>thread_head (thread)the thread's facts and its newest events, what the app draws first on opening it: the title, the agent, model and access it runs on, where it stands and its folder, then as many newest events as fit in 64 KB with each tool result past 2 KB cut and marked. The cheap look; thread read is the whole conversation. Reading a head marks nothing
wsp thread forget <thread> [--yes]thread_forget (thread)drops a thread no turn ever ran on, the row a launch that never got going leaves in wsp threads and in the person's sidebar, a launch the agent refused for want of a sign-in among them; nothing is asked of the machine, and it is refused in one line once a turn of the thread did work
wsp thread settle <thread>... [--finished]thread_settle (threads, finished)folds each thread and every thread under it into the person's Settled in the sidebar, as the app's Settle does; a thread with one working or asking anywhere in its tree is left whole, and one already settled is left too, each with why. --finished keeps each named thread and settles every finished thread under it, a failed one staying for the person and one with work under it left with why. A thread settles itself and the threads it started, never its lead or a thread beside it, which is refused naming the thread. Prints settled <id> <title> and left <id>: <why>, one line each, and the call succeeds whatever it left
wsp thread restore <thread>...thread_restore (threads)brings each thread back out of Settled with the threads under it that the latest settle naming it moved, so after --finished the finished threads that settle moved; a thread folded by the quiet time or settled before stays, and a thread restores itself and the threads under it alone. Prints restored <id> <title> per thread
wsp thread allow <thread>thread_allow (thread)answers the prompt the thread is stopped on and lets the call run, the same pick the app's own button sends; a thread stopped on a prompt reads Needs you in wsp threads and runs nothing until somebody picks. Refused in one line when the thread is waiting on no prompt and when the prompt carries no such answer
wsp thread deny <thread> [--reason "<words>"]thread_deny (thread, reason)answers the same prompt the other way: the call is refused and the turn goes on with that answer. --reason tells the agent what to do instead, in the words it reads after the refusal, as the reason typed in the app's question panel reaches it
wsp slate catalog [<name>]slate_catalog (name)what a slate can hold, read before writing one: the index of pieces, sources, steps, functions and the rules, or with a name (a piece, a source, functions, steps, runs, examples) that entry in full. See slate below
wsp slate write [<thread>] [<file>] [--check] [--set <path>=<json>]... [--press <piece>] [--row <n>] [--action <n>] [--if-version <n>]slate_write (thread, text, document, check, values, press, row, action, if_version)writes the thread's slate from the JSX-like form (a whole <slate> or a patch) or the stored document, and answers the version and a text sketch; --check stores nothing; with --set or --press a check rehearses values and a press on a copy and answers the sketch as it would read then, with what the press would start or send, the file optional; a refusal lists every error with its line and fix
wsp slate state [<thread>] [<path>=<json>...] [--start <run>]... [--if-version <n>]slate_state (thread, values, start, if_version)sets the slate's $values by path, like '$steps[2].done=true' or i=2; reactions on them fire; --start starts a run the person said "Always in this thread" to, so you can check your own slate, and answers any other held without starting it; answers the version and the sketch. To try a slate out, rehearse with slate write --check --set instead of writing the person's values
wsp slate read [<thread>] [--values <path>]... [--no-text] [--no-sketch] [--document]slate_read (thread, values, text, sketch, document)the slate as it stands: the sketch, then the JSX-like form to patch by id, values, derived values, runs, problems, comments, approvals and the paths named (* for every bound one); --no-text leaves out the JSX-like form, --document adds the stored JSON. A lead reads a child's by thread
wsp send <thread> [--model <slug>] [--effort <word>] [--fast] [--file <path>] [--detach] "<message>"send (thread, message, model, effort, fast, files, detach)a message into an existing thread; it runs on that thread's own agent and at the access that thread runs at, which no message changes; --fast and --file mean what they mean on run; follows the turn to the reply, or with --detach returns the moment the turn is started
wsp stop <thread> [--subagent <id>]stop (thread, subagent)ends the thread's running turn, as the app's stop button does; the computer stays up. A thread whose agents spawned threads of their own stops as one, and the line names each of those it ended. --subagent stops one of the agent's own subagents by its id in the SUBAGENT column of threads and nothing else: the turn and its other subagents run on
wsp restartrestartstops the host and brings it back on the road it came up on: the service's manager, the verb that started it, or the app. Running turns go on across it, since each leads a process of its own, and the host that comes back re-opens them; the answer comes once that host serves, with the threads running on it then (running). A send or run following a turn when the restart cut its socket dials the host again and carries on. Refused for a host wsp up holds in a terminal, which only that terminal brings back. This is how a coordinator thread lands a host change without ending itself
wsp exec <thread> [--cwd <dir>] -- <command...>exec (thread, argv, cwd)runs the command on the computer the thread runs on, each word as given, in the folder named or the folder the thread works in; output lines, the exit code and the folder it ran in
wsp recipe scan [--project <folder>] [--json]recipe_scan (project)reads this computer and prints every option, writing nothing: the agents, the tools with why and size, what else a package manager here has that the image could take, the commands the agents ran, and the sign-ins, each with what to do about it and why
wsp recipe [--tick used|installed|default] [--set <id>=on|off] [--signin <id>=copy|machine|later|key|skip|token] [--add <id>=<command>] [--add-check <id>=<command>] [--why <words>] [--engine] [--project <folder>] [--out <path>] [--json]recipe (tick, set, signin, add, add_check, why, engine, project, out)writes the recipe for a machine and prints it as a table: every catalog agent and tool with its tick, why, and its size, and the commands the agents ran that the catalog does not carry; --set takes a catalog id or the id the scan gives a package this computer already has; --why says what the added rows are for; --engine marks the recipe so every machine from its image gets the place's container engine, for a project whose compose file needs one

The app's own roads

These stand behind the app's own screens rather than a page of the command line: they are still parsed, still served as tools, and wsp <verb> --help still answers for each.

command lineMCP toolwhat it does
wsp thread rename <thread> "<title>"thread_rename (thread, title)names the thread in the agent's own store on the machine, the field the agent writes when a person renames the session inside it, so wsp and the agent read the same name; unsupported where the agent keeps no name of a person's
wsp folders [<folder>] [--hidden] [--repos] [--on <computer>]folders (folder, hidden, repos, on)the folders directly inside one folder on this computer, or on a box you added with --on, each with whether git tracks it, for naming one to record; with --repos, every git repo under the home folder with its branch, most recently used first; the roots are that computer's home folder and every project on it, a path outside them is refused
wsp terminal config [--scheme light|dark]terminal_config (scheme)the Ghostty config on this computer as the app's terminal pane applies it, its includes followed and its theme resolved: the font and its fallbacks, the size, the colors, the cursor, the padding, the background opacity, and the blur, which is read but not applied; files empty means no config, and an absent key means the pane keeps its default
wsp setupsetupthe setup on this host as the app's Settings reads it: which keys are held (never their values), the agents here and whether each carries the wsp tools, and the init job's phase, rows and progress when one runs or ran, each sign-in with the page waiting for the person

The command line alone has wsp up, wsp down, wsp status, wsp init, wsp doctor, wsp mcp, wsp add for a project, a provider's key or the join code, wsp remove, wsp join, wsp leave, the four lines under the word host, wsp login, wsp logout, wsp hosts, wsp usage reset and the three sign-in lines, since each starts, stops, installs or hands out access to something on the person's computer, wsp usage reset spends a reset the person owns, the sign-ins run in a terminal a person types into: any command that needs the host starts one when none serves that state file, on a free port, saying so in one line on stderr with the log to read and the wsp down that stops it, so wsp up is only for a host somebody wants to watch or to serve beyond this computer. wsp up [--port <n>] [--listen <addr>] [--state <path>] serves the app and the runtime's WebSocket on one port and the host until it is stopped, so the terminal it runs in has to stay open; wsp up --service hands that same line to the computer's own service manager instead, a launchd agent on a Mac and a systemd user unit on Linux, which serves now and again at every login, and wsp down stops it and takes it away, as it stops a host a command started. wsp status prints whether a host is serving this state file, its port, its token file and what keeps it there, with a non-zero exit code when none does; wsp status --host <alias> reads that host instead, where it answers and whether it did, since what keeps it up is read on the computer it runs on. On a computer joined to somebody's wsp there is no host and never will be, so wsp status there reads the agent that join installed instead: which wsp the computer belongs to, whether that agent is answering and on which port, what the computer is doing right now (cpu, memory, disk), one row per machine it holds under those rows with the name and id that machine has on the host, what it was given, what it holds of that now, its uptime, its process count and the address it answers on, and the ten processes spending the most of the computer, exiting non-zero when nothing answers; wsp status --watch draws those same rows again every second where they stand until Ctrl-C, which needs a terminal. It is the one line that leaves this computer only when a host is named on it or in WSP_HOST: the account's one host and the host a turn's launch carries move every verb and not this one, since a bare wsp status asks whether the host here is serving and an alias answering for a box would hide that. wsp up --listen <addr> binds an address other than this computer's own, which is how a host on a box someone owns is reached from their laptop: the page is then served with no token in it and every browser redeems a one time code for a token of its own. wsp up --advertise <url> says the address a computer being joined dials the host at, which is the relay's name or what this computer answers on without it; a fork's turn dials nothing, its wsp rides its daemon. wsp up --service carries every one of these into the unit it installs, so the service serves the line that was typed and not a shorter one. wsp add is how a computer the person owns becomes a place in their wsp: with no argument it prints the wsp join line and a code to type on that computer, which is spent by the first join and stands for ten minutes; wsp add user@host [--name <name>] [--ssh-port <port>] [--ssh-key <path>] logs in over ssh as the person's own client would, installs the daemon on that box, starts it as a systemd system unit and waits for it to dial back, printing each step as it happens; the box is named after its hostname when no name is given. A bare alias from the person's ~/.ssh/config works in place of user@host: the add dials through that block, with its user and its port unless --ssh-port names another, and names the box after the alias up to its first dot. wsp remove <place> [--yes] [--force] [--json] takes a computer back out after one question: its forks are deleted and its projects leave this wsp with their threads, then the agent and its files come off it, and on a computer added over ssh so does /wsp, with the forks' copies, the project checkouts and the copy of the image in it (one joined with a code keeps /wsp, and so does one whose own wsp is older than this one, since wsp add <computer> --update moves its daemon alone), and the computer is otherwise left as wsp found it. A fork or a project folder there holding work no remote has (commits on any branch or HEAD no remote has, uncommitted files, stashes) stops it before the question, each named, a project folder by its path there: a push keeps a fork's, and --force removes them anyway. A computer that is not answering while forks or projects stand on it is refused until it answers. The answer is a summary by folder, at most a screen, and --json carries every line; on a box whose login's sudo asks for a password, the remove and wsp add <computer> --update over ssh ask for it once at a terminal, as the add does, and the app's Remove confirm asks in its own field; a place that was not connected keeps its agent and the line says to run wsp leave there. The other two run on the computer being joined, not on the host: wsp join <address>... --code <code> [--name <name>] dials those addresses in turn until one answers, proves a key it makes there, writes the place file and installs the agent as a systemd system unit, which needs root and dials again at every boot (a place is a Linux computer, so a Mac refuses to join), --code-file <path> reading the code off a file and deleting it first; the service runs the daemon binary itself, and wsp leave [--yes] [--force] [--takes <project folder>]... sweeps wsp off that computer for somebody whose host is gone: it asks once, and stops before anything goes where a fork's copy or a project checkout under /wsp holds work no remote has, naming each by its path, unless --force. Where /wsp stood before the add, it takes only wsp's own folders there and each project folder a --takes names, which a remove passes from the host's records. wsp host pair prints that code, when it expires and the addresses to open; it is the person's own terminal on the computer the host runs on, never a thread's, since handing out a code hands out the host, and aimed at a host somewhere else, by --host, by WSP_HOST or by the account's one host, it says in one line that it runs at that host's own terminal and dials nothing. wsp host devices lists the computers that took a code and what each token is read as (the owner's own browser, a paired device, a thread's token), and wsp host devices revoke <id> takes one back out; both take the host pick like every other verb, so wsp host devices --host box reads and cuts the devices of the box from a laptop signed in to the same account. wsp hosts is the one listing of the hosts on the person's account this computer can reach, with a live beat for each and the one every line takes marked. A box nobody can reach inbound takes the relay: wsp host link <url> puts the computer the host runs on onto the person's relay account, printing a code and a page they approve in their own browser, and from then on every wsp up there asks that relay for a tunnel, runs the connector against its own loopback port and prints public https://<hostname> once it has one, so nothing has to be open to the world; wsp up --no-relay serves without the tunnel, and wsp host unlink takes the computer off the account and stops it. The other side of it is on the person's own computer: wsp login signs that computer in to their account, printing one word to approve on a computer already in and a page to approve it on when none is, and wsp hosts then lists the boxes on the account and writes a record for each, so every verb reaches them by name with no code typed; the first dial at one proves the key this computer signs with, which the box was told to trust at its link, and takes a device token of its own. wsp login <word> on a computer already in signs the admission for the one waiting, wsp login <id> does the same for a computer already on the account, wsp login on its own lists the account's computers with who admitted each, and wsp logout signs this computer out while wsp logout <id> signs another out, which every box reads on its next beat. wsp host link on a computer that is already signed in puts the box on that account with no code and no page at all. Keys and tokens never pass through the relay: an admission is bytes one computer signed and the relay carries, and every box verifies it with keys of its own. Every verb then takes --host <alias> for one line, as wsp threads --host box does, and the WSP_HOST environment variable does the same for a whole shell; --state names a file on this computer, so it is not read for a host somewhere else and a line that gives both says so. With no host named, a line goes to the host serving the state file on this computer, and only when none does to the one host on the account. wsp init [--recipe <path>] [--non-interactive] [--json] [--yes] builds the image, --json printing each sign-in and its outcome as one object on stdout with everything else on stderr, --non-interactive doing the same in prose, and --yes taking every default and skipping the sign-ins, which is why --yes is refused beside --json; wsp doctor forks a live machine to prove the whole road end to end; wsp mcp [--host <alias>] [--scoped] serves the tools over stdio, against this computer's host or the one that alias names, and --scoped, which the host puts on a thread's own tools, makes a server whose launch pair is missing refuse rather than act as the person; wsp mcp install --agent <id> [--host <alias>] writes the server into that agent's own MCP config, with the alias in the command it registers when the tools are for a host on another computer, this skill into its skills folder, and wsp's own marked section into the instructions the folder it runs in keeps, AGENTS.md for every agent and CLAUDE.md beside it for Claude Code. A second run replaces that section where it stands, so there is only ever one and everything around it is untouched, a file whose end marker somebody deleted is repaired to that one section rather than given a second begin marker, and wsp mcp install --agent <id> --remove takes it back out, leaving the file's other lines byte for byte and the config and the skill where they are. --agent repeats to do several in one call, one failing id costing the others nothing, and off a terminal a run that names none takes every agent whose own command is on the PATH. --json answers with one line holding the server command every config now runs, what each agent took, its docs naming the files the section went into, and a failures array, a provider failure when that array is not empty. The prose ends on the one thing to do next, which is inside the agent that was just set up. The three sign-in lines are the person's: wsp agents signin opencode runs an agent's own sign-in on this computer in their terminal, wsp agents signin gemini <thread> where that thread runs, and a box they added takes wsp add spoo --sign-in codex, which runs a shared login at that box's logins folder and any other as the owner of its home; wsp servers signin notion --agent claude --on spoo runs the harness's own sign-in for one MCP server where it is set up; wsp agents key claude puts the token claude setup-token prints, or an agent's API key, into the host's vault, typed where nothing echoes it and only at the host's own terminal. Hand the person the line; never run one for them. An entry under installed with no path took the skill and not the server, which is the by-hand line the prose prints. Only agents the catalog knows an MCP config for get the server: claude, codex, gemini, opencode. The rest get the skill and a by-hand line.

For a shell script

One verb is a shell script's and not an agent's, because it blocks: a script that starts threads and has to stand at the end of them has nothing else to do while they run.

command lineMCP toolwhat it does
wsp threads wait <thread>... [--timeout <s>] [--tail]threads_wait (threads, timeout)blocks until one of the named threads leaves running and prints that thread's finished line with the reply whole under it, --tail printing the reply's last line alone, the line a notify sends to the person; finished carries its id, status, duration, cost and the reply's last line either way; one thread per call, a thread already over comes back at once, and --timeout gives up after so many seconds with nothing on stdout, one stderr line and timedOut on the tool

In your own conversation this is the wrong road: a blocked wait is minutes of your turn spent on nothing, and nothing can reach you while it runs. Start children with --notify me and end your turn instead when you are a thread, and hand a job of more than one turn to a coordinator thread when you are not; the two rules are the next section's.

Running work well

These decide whether work goes fast or stalls, and they hold wherever a thread runs.

  • A wsp thread starts every child with --notify me and ends its turn, and each child's finished line wakes it with that child's whole report, so nothing is polled and no turn is spent waiting. The launch environment says whether you are a thread, and --notify me resolves to whichever thread the request came out of. Settle each child with wsp thread settle once its report is acted on; a finished child whose report you took reads as read, and settles by itself after the person's quiet time. Restart a stuck child with wsp stop and then wsp run --replaces <child>, so the person sees one row for the job, not two under one title.
  • A caller that is not a wsp thread cannot be woken at all, so it takes the reply of one turn as the call returns, and hands work of more than one turn to a single coordinator thread in the project's folder on this computer, with the whole job in its brief and the person told where to read it.
  • When the person asks for another agent, on another model or another harness, start it as a wsp thread with run, so it shows in their sidebar; your own subagent tool is for your own sub-steps.
  • A thread may own one slate, a live panel on the right panel's Slate tab; slate_catalog lists what it can hold, slate_write writes one, and only a button the person presses reaches you.
  • A thread on this computer runs in the project's folder, beside any other thread there, as panes in one terminal share a folder: put a quick subtask or a second harness there, since it starts in a second, costs nothing and runs the agents already on this computer's PATH over the person's own files and sessions. Work that needs a branch of its own goes in a worktree, wsp run <project> --branch <branch>, so two builders never edit one checkout.
  • One heavy thread per machine: on 2 vCPU and 4 GB one thread runs tests or a build at a time and a second thread is a light send. Three test runs at once starve the machine and every turn in flight fails.
  • A thread that builds gets its own git worktree, which is how two threads share one machine, and that worktree's setup is made cheap rather than skipped: pnpm's side-effects cache so a native module is compiled once per machine, and the checkout's node_modules hardlinked into the new worktree before pnpm install --offline, which then only verifies. A worktree set up from scratch relinks thousands of files and rebuilds native modules, minutes on 2 vCPU.
  • Start long work with your harness's own background road, which wakes you when it ends, and never with a shell &, which nothing tracks and which wakes nobody when it ends.
  • wsp send <thread> continues a thread that has already replied; a send into a thread whose turn is still running opens no second turn and is a mistake. Wait for the turn to end, or wsp stop <thread> first.
  • Restarting the host cuts every turn running anywhere. Finish or stop the running turns before wsp down and wsp up.
  • The person's app and the command line read the same host, so every thread opened here shows in their sidebar and they read its reply there. Name each one with --title and keep the reply short and complete.
  • A command you mean the person to run goes in a fenced block labelled sh, which the app shows with Run: the person runs it with one click, in this thread's folder, and sees its output under the block.

The contract

The command line, the MCP tools and this skill are one contract: every capability exists in all three or in none, and the parity test in the repo holds them to it. With --json, stdout carries JSON alone: one JSON object per line, frames first, the last line is the result; a verb with no stream prints the result alone. The result is the object the MCP tool of the same name answers with, less what the frames already carried, so no line prints twice: wsp exec --json prints one exec.output frame per output line and then a result with the exit code and the folder; wsp add root@203.0.113.7 --recipe mine --json prints a {"setup": ...} frame per step line and a {"waiting": ...} frame per sign-in waiting on the person, then {"computer": ...}. Without --json, stdout is for a person. Progress, the waking line and every refusal go to stderr. A refusal or a failure is one line on stderr, the failure object {"error": "<the line>", "class": "usage", "exit": 3} under --json, and the exit code is the class's:

exitclasswhen
0okit did what its line says; with --json stdout holds the answer
1providerthe host, the runtime, a provider or the machine refused or failed
2authno key, no sign-in, or the host refused the token
3usagethe line was refused before anything ran: a missing argument, an unknown flag or a value nothing takes

wsp exec is the one exception: its exit code is the command's own, and only a machine that could not run it is a provider failure. A person saying no to a confirmation is <name> kept on stderr with exit code 1. Every line that takes something away (wsp remove, wsp leave, wsp delete, wsp discard, wsp projects remove, wsp recipes remove, wsp skills remove, wsp servers remove, wsp worktree remove, wsp thread forget) asks that one question only once the host's own refusals pass, and off a terminal is refused without --yes. An MCP tool answers a failure as a tool error whose structured content is the same object, so class reads the same on both doors; a call whose inputs do not fit the tool's schema is refused by the server before the tool runs.

run

wsp projects                                       # every project, each on its computer; the name is what wsp run takes
wsp run spoo --notify me "Read ticket 308 ... and build it."
wsp run spoo --branch ticket/308 --notify me "Build ticket 308 on its own branch."
wsp run spoo --model claude-sonnet-5 --effort low --access ask "Review the branch ticket/308."
wsp run --agent codex "Say in one line which folder you are in and what is in it."

wsp run <project> opens a thread in the project's folder on this computer: it runs the agent already on this computer's PATH, keeps its session in the person's own agent folder, and nothing is copied, forked or woken for it; several threads share the folder as panes in one terminal do. --branch <branch> runs it in a worktree of the project's repo on that branch instead: the one git already has the branch checked out in, wherever it is, else one wsp makes under its own folder, a new branch starting from the project folder's current commit, with .env files carried in. What else it gets is decided per ecosystem by the lockfiles the branch tracks, each in the folder that holds it and only where the project folder holds that ecosystem's dependency or build folder beside it, or at the top for an ecosystem that carries nothing (uv, Poetry, Cargo, Go), whose environment may sit outside the project, so a lockfile a test fixture tracks costs nothing: relocatable dependency folders (node_modules for npm, pnpm, Yarn and Bun, Yarn's .yarn/cache and .yarn/unplugged, Composer's vendor) are carried in, by a clone where the disk shares blocks and on Linux an overlay of a frozen copy of the folder's own where it does not, made once per install and kept for the next worktree, so a reinstall in the project folder never reaches a worktree; folders that hold the path they were built at (a virtualenv, target, .next) never are; caches outside the project (Cargo's registry, Go's module cache, uv's) are shared as they stand. Each ecosystem's locked install then runs once in the folder holding its lockfile (uv sync --locked, poetry install, cargo fetch --locked, go list -mod=readonly -deps -test ./... at the top, npm install --no-save, pnpm install --frozen-lockfile, yarn install --frozen-lockfile, bun install --frozen-lockfile, composer install), none of which rewrites a tracked lockfile, skipped for pnpm where the node_modules carried in were installed from the branch's own lockfile, and the project's after-worktree command after them at the top, before the thread starts. --cwd starts it in a folder inside the project or one of its worktrees and nowhere else. With no project named, from a thread it runs beside you in your own folder, and from a terminal inside one of your project folders it goes to that project; the first line printed says where: thread 1a2b3c4d on spoo in ~/spoo. Outside every project it is refused in one line and nothing starts. A project on a box runs in its folder there the same way, as the login the box was added with, and a thread there with no project named runs beside you on that box; a thread on a box naming a project on another computer is refused for now, and told to send the lead of its tree a message where that lead can start it, and that the person can start it there otherwise. From a thread, that project is your own or, from a thread on this computer, a box project of your own repository, run in its folder there, and the child joins your tree wherever it runs: wsp threads lists it, send reaches it and --notify me wakes you at its end. A child on a box sends to you in turn and lists your tree, its words alone with --detach, and reaches nothing else of yours: no stop, steer, rename or read of a thread on another computer, and any verb it names on your folder or on a thread of your tree there, an exec or a commit, is refused with a message to you as the road. A project of another repository and another folder the person keeps of yours on this computer are each refused by that rule. Exec takes a thread of your tree, so it reaches none of these. --agent names the agent to run in the thread; absent, the project's agent runs (wsp projects set), else the default agent (wsp agents default), else claude, and a thread runs only on an agent the host has an adapter for; a refusal names the ones it has. --model and --effort pick the model and the reasoning effort by the agent's own words (claude-sonnet-5; low, medium, high, xhigh, max), the same lists the app's composer offers. --access takes wsp's own word, the same on every agent: ask (asks about each action that needs permission), auto-edit (edits files without asking), full (every action without asking) or plan (reads and proposes, changes nothing); an agent with no mode for the word refuses it naming the ones it takes, and an agent's own spelling is refused. Absent, the project's, then the agent's defaults set with wsp agents set, then the catalog's run (high for claude), with full access, and a value the catalog does not list is refused before anything starts, naming the list. A cheaper model for a review is the usual pick. A thread's folder decides which project state (sessions, memory, CLAUDE.md) the agent loads, so put a thread where its project is. --title names the thread, as a person naming it does: the name shows in the sidebar and in the agent's own session list at once, and the title the host asks the agent for as the turn starts never replaces it. Without one the thread is titled by its opening words for the few seconds the agent takes to name it, and by that name after. --notify me names the caller: the thread you are when the host launched you as one, and the person when it did not, which is how a thread starts a child without knowing its own id. --notify <thread> names another thread outright, one of your own tree: the lead that started you, a thread beside you under it, or one you started. From a box it names one of those on that box alone, and your lead hears your end through the --notify me it started you with; any other thread is refused as no thread to notify, and that work goes through the person. Either way the line goes into that thread as a message (steered into its running turn or queued), and a line the person gets shows in their app. The notify line goes once, at the reply; a reply given with background tasks still running says so, and a second line goes at the turn's end. The person's line reads thread 1a2b3c4d finished (completed, 12m 4s, $0.41): <last line of the reply>; a thread's carries the same facts and then the child's final message whole, so you act on the report without reading anything else. A --notify me from a turn that has ended, or with a token this host never launched, is refused rather than sent to the person.

--notify repeats, and each target gets the line once: --notify me --notify 5e6f7a8b wires a builder's end to you and to a reviewer thread together, and the reviewer's own end back to the builder and to you. That removes the relay you would do by hand; it decides nothing, so you still read every report and make the calls. A target whose thread is gone by the time the child ends falls back to the person, and the new thread's own id as a target is refused.

The command line streams the reply to stderr as it arrives and prints it once: into a pipe, which is what you have, stdout carries the whole reply at the end, and on a person's terminal the streamed copy is the reply and stdout adds no second one; a failed turn is a provider failure, its reason the one line on stderr. With --detach (detach true on the tool) nothing streams: the thread id is the whole answer, the turn runs on, and its end goes to whoever --notify named (see the notify paragraph above). A turn ends when the agent process exits, not at its reply, and the thread reads running until then, which can be minutes when the agent left a command running; what a send before then does is under send. When the host restarts under the turn (wsp restart, a service restart, the app relaunching), the command line says so on stderr, dials it again for as long as a host is given to start and prints the reply whole on stdout once it lands, since the stream on the screen has a gap; the tools wait the same way. A host that does not come back in that time is the command's failure in the dial's own words, and the turn goes on. The first stdout line is thread <id>, the id send takes. The MCP tool returns the reply text with the thread id, its record's id, agent and outcome. When the thread's previous turn did not finish (a deadline, a host restart, a nap), the reply text from send and run opens with the line previous turn was cut; resuming and the structured output carries afterCut: true, and the command line prints that line on stderr before the reply; the agent then resumes a transcript that may be missing its last steps, so restate what matters.

send

wsp send 1a2b3c4d "Also cover the codex case in the test."
wsp send 1a2b3c4d --model claude-opus-5-5 --effort high "Now the hard part."

A send is never refused for meeting a turn. Into a thread whose turn is not running the message starts a new turn (outcome started), on the model, effort and access named or the thread's own. Into a thread whose turn is running: when the agent can take input mid-turn (Claude Code does) the message joins the running turn (outcome steered) and arrives at that turn's next tool round, the way a person's message does, and the reply is that turn's, on that turn's picks, with its images where the agent's steer takes them (Claude Code's does); otherwise the message waits for the running turn to end and then runs (outcome queued), and so does one with a --file that is not an image, or with an image to an agent whose steer takes none. A reply the agent gave while a command it started was still running is not a reply yet: that turn is still running and the message steers it. Into a thread whose turn has replied and is waiting only on its agent process to exit, the message waits for that process and runs as the thread's next turn (outcome queued), which the host says as This thread replied, still working; the message runs as its next turn once that process exits; the app's composer shows the same words, and threads reads the thread running until the agent process exits. Two sends keep the order they arrived in. A start on a computer already running as many threads as its threads at once waits for a slot and starts on its own when one frees (outcome held), with capped beside it, the computer and what runs there against its number; the command line says waiting while <computer> is running <n> of <m> threads. A thread that follows a run or a send to its end, without --detach, lends that thread's next turn its own slot, one turn at a time, a turn already held ahead of the message included, so a send into your lead or a thread beside you never waits on your own slot; every other start waits for a slot of its own. A thread whose follow gives up before the turn it follows ends, or that is stopped while that turn runs, works again beside it, and each then holds a slot of its own: the computer runs one past its number until either ends, and nothing new starts there meanwhile. The command line says which outcome on stderr. A person's message on the same thread lands in order with yours. Never start a second thread to hurry a running one.

Threads talk to each other, and this is the road: wsp send <thread> "<message>" (the send tool) puts your words into another thread by its id, one of your own tree, the lead that started you, a thread beside you under it or one you started, wherever it runs, and wsp threads (the threads tool) is where you find that id, with the project, the folder, its branch, the agent and the title beside it. A thread outside your tree, the person's own beside you included, is not yours to reach: threads leaves it out, a send, a stop or a rename naming it reads as no such thread, and its transcript is not in what thread read answers, so a job that needs another tree's thread goes to the person. A stop cascades: stopping your lead stops every thread under it, your siblings and you among them. The other thread's answer does not come back to this call: it comes back as its own next message when that thread was started with --notify me naming you, or with --notify <your thread>. So a thread asking another thread something starts it with a notify that names the asker, sends, and ends its turn.

thread read

wsp thread read 1a2b3c4d
wsp thread read 1a2b3c4d --last

The thread's messages as the app lists them, oldest first, one block each: who it is and the clock on the first line, the text under it. person is the message that opened or steered a turn, agent is the agent's own words, tool is one call of its folded to the line the app's row reads, and turn is the outcome, the duration and the cost the turn ended with, and the reason where it did not complete. --last prints the final reply alone, the whole message the thread's finished line carries, which is what to read when the line that reached you is shorter than the report behind it. When the thread has started another turn since, a second row says so, so the report you are reading is never taken for the one being written. Under --json and on the tool the answer is threadId and messages, each one {who, at, text} with at the ms epoch the runtime recorded. The transcript is the host's own, so a read touches no machine and a paused machine's thread reads the same as a running one's. A call's output and the agent's reasoning are no rows of it, and a thread whose rows the transcript's cap has dropped answers with none, which is an answer and not an error.

thread head

wsp thread head 1a2b3c4d

The thread's title, the agent, model and access it runs on, where it stands and its folder, then its newest events as thread read lists them, as many as fit in 64 KB. Under --json and on the tool the answer is facts (the thread as threads lists it, with model, effort, contextWindow and, while a turn runs, its turnId), events (the newest transcript events, oldest first, each tool result past 2 KB cut with cut set to its whole length), pos (the newest position the transcript has issued; every event carries its own pos) and total (how many events of the thread the transcript holds). Use it to see where a thread stands without reading all of it. A head marks nothing read.

thread allow, thread deny

wsp thread allow 1a2b3c4d
wsp thread deny 1a2b3c4d
wsp thread deny 1a2b3c4d --reason "edit the test file, not the fixture"

A thread whose agent asked to run a command or write a file is stopped on that question: it reads Needs you in wsp threads, runs nothing and ends nothing until somebody answers. A terminal watching the turn prints the question and the keys that answer it; from anywhere else these two lines answer it by thread id, the same pick the app's buttons send. wsp thread read <thread> shows what is being asked. A deny takes --reason, what the agent should do instead: Claude Code reads it with the refusal, and Codex, whose refusal carries no words, gets it as the person's next message in the same turn. Both print what they closed, and both are refused in one line when the thread is waiting on no prompt, so answering twice is never mistaken for answering once.

threads wait

wsp run dev --detach --notify me --title "413 build" "Read ticket 413 ... and build it."
wsp threads wait 1a2b3c4d 5e6f7a8b --timeout 600

--detach on run and send (detach true on both tools) prints thread <id> the moment the turn is started and returns; the turn runs on. threads wait then blocks until one of the named threads leaves running and prints that thread's finished line, thread 1a2b3c4d finished (completed, 12m 4s, $0.41), with the agent's reply whole under it, since a reply's last line is as often a code fence as an answer; --tail prints the last line alone instead, thread 1a2b3c4d finished (completed, 12m 4s, $0.41): <last line of the reply>, which is the line a --notify sends to the person. Under --json and on the tool it is the id, status, duration, cost and the reply's last line under finished either way. One thread per call: a script that started three builders calls it three times, dropping each returned id from the list, since a thread already over comes back at once and would come back again. --timeout <s> (timeout, seconds) gives up after that long: nothing on stdout, thread 1a2b3c4d still running after 10m on stderr (timedOut true on the tool) and the ok exit code, since nothing failed, so other work fits between calls. A host that restarts under the wait is dialled again for as long as a host is given to start and the wait carries on, --timeout still counting from the start. This is a shell script's verb: a script has nothing else to do while its builders run, and a person is reading its output. Do not call it in your own conversation, where it spends minutes of a turn on nothing and nothing can reach you meanwhile. Never poll threads for a state change either: a thread starts its children with --notify me, and a caller that is not a thread hands more than one turn's work to a coordinator thread.

stop

wsp stop 1a2b3c4d

Ends the thread's running turn through the runtime, the way the app's stop button does; the turn ends with status interrupted and whoever followed it gets turn interrupted. The line says thread <id> stopped, or thread <id> not running when the turn had already ended; both succeed, since neither is an error. Nothing is paused or killed and the thread takes the next send. Use it when a send started a turn you did not mean to.

wsp stop 1a2b3c4d --subagent a1b2c3d4e5f6a7b8

Stops one subagent the thread's agent started (Claude Code's Agent tool), by the id in the SUBAGENT column of wsp threads, and nothing else: the turn, the agent and its other subagents run on, and the agent reads that subagent as stopped. The line says thread <id> subagent <id> stopped once the agent took the stop, thread <id> subagent <id> not running when it or the turn had already ended, or the reason after a colon when the agent would not stop it or offers no stop for one subagent, which is said as Stop is not available for <agent> subagents; stop the thread to stop them all. Every one of these succeeds. Under --json and on the tool it carries the thread's id, the subagent's id and the outcome, with error beside a refused or unsupported outcome.

exec

wsp exec 1a2b3c4d -- git status --short
wsp exec 1a2b3c4d --cwd /root -- sh -c 'ls | wc -l'

Each word after -- reaches the computer the thread runs on as one argument; a shell line goes through sh -c. The command runs in the folder --cwd names (absolute, on that computer), else in the folder the thread works in, the project's folder or its worktree, so git status there needs no cd. The command's exit code is the verb's, and a non-zero exit is followed by one stderr line naming the folder it ran in, which the host answers with rather than the caller assuming it; the tool carries that folder as cwd. A non-zero exit is a result; the computer going away is an error. The command runs with no login shell, so bash -c, never bash -lc.

add

wsp folders /Users/zingzy                         # what is inside, for naming a folder to record
wsp add /Users/zingzy/wsp                         # a project on this computer, worked where it sits
wsp add https://github.com/spoo-me/spoo.me --into /Users/zingzy/code/spoo.me   # cloned here into an empty folder, then worked there
wsp add https://github.com/spoo-me/frontend --on spoo --name spoo-landing   # a project a computer clones
wsp add spoo-me/frontend --on spoo                # the same repo through the signed-in gh on that computer
wsp add /Users/zingzy/spoo/spoo-landing --on spoo # a folder here seeding a project there: prints the menu, sends nothing
wsp add /Users/zingzy/spoo/spoo-landing --on spoo --yes   # sends the ticked rows; --keep, --cut, --no-memory, --no-commits move them
wsp add root@203.0.113.7 --recipe mine           # a computer of yours over ssh, set up from the recipe named mine
wsp add spoo --resume                             # finish a setup that stopped, or a sign-in left waiting
wsp projects                                      # every project, each on its computer
wsp add /Users/zingzy/monorepo/apps/web          # a subfolder of a repo: a project of that repo
wsp add /Users/zingzy/notes                       # a folder that is no git repo: a project with no branches

A folder here is a project as it stands, a git repo or not: a subfolder of a repo is a project of that repo and takes its branches, and a folder git holds no repo in takes no branch; a repo's url needs --into <folder>, an empty folder here it is cloned into, or --on <computer> for a computer that clones it. The clone here runs with no prompt: a private repo needs gh auth login or an ssh url, and a failure says git's own last line. One source on one computer is one project, so recording it twice is refused naming the one there is. A computer's add checks it, installs wsp and then sets it up from the recipe in steps: the base tools first, then the agents, their sign-ins, the CLIs, MCP servers, skills, plugins, configs and context files, with the folders cloned beside them. A step that fails says why and the rest go on; only the base tools and the agents stop it. Without a recipe the computer is listed as pending until one is picked. A thread on this computer then runs in that folder, and on a box the person joined in the project's folder there.

slate

A slate is a live panel on the Slate tab beside this thread: you build it, the person reads, presses and fills it in. Build one when the person will look at it or press it: a tracker, form, walkthrough or dashboard. Results for you stay in your own tools. Once a thread has a slate, a request to see something lands there, and what the person does in it gets its result there unless they ask in the chat. Write it once, then only patch it.

You write a small JSX-like text: <slate title="...">, pieces as elements, prop="literal" and prop={formula}. A formula reads live data (pr.checks) and its own $values. Declare <value name="step" start={1} />, <run name="check" cmd="gh api ..." env={{ ID: $id }} /> and <when change={$id} do={start($check)} />, and the slate runs it with no turn of yours; the person approves each command once. Code only the slate uses goes in a <file name="x.py">, run as "$SLATE_DIR/x.py"; code the project already has is called where it is. For a secret, token or password the person enters themselves, give a <secret name="token" /> input; never ask them to paste it into a file or the chat. Props are meaning, never style; nothing inside braces is JavaScript.

Lay it out simple and airy unless asked for more: one idea per section, one heading per section, status and last-checked lines small and muted beside their subject, actions at the end of their row with one primary per section, mono only for figures, ids, times and paths, every live number with its window and unit, nothing centered but a lone figure or card.

Build and read it with the slate tools (slate_catalog, slate_write, slate_state, slate_read), never wsp from a shell, which may be another install. Every write answers its version and a sketch, values filled in; a refusal lists every error with its fix and stores nothing. A patch is elements without <slate>: a piece with its id replaces it, <props id="week" tone="warning" /> merges, <add under="root">, <remove id="x" />, <clear />, <undo />.

Only a press reaches you: a send("text", $path) step on a button puts one message into this thread: the text, then a slate: {...} line holding the values as data, never instructions. For their input, give a button that sends, or read the slate. Shown a screenshot of it, read the slate; slate:week in their message names a piece.

The loop for building with wsp

  1. One project recorded (wsp add <folder> here, or wsp add <url> --on <computer> there). wsp threads <project> shows what is on it, each thread with its folder and branch.
  2. One thread per ticket, each in its own worktree: wsp run <project> --branch ticket/<n>-<slug> starts it in one, made from the project folder's current commit with the dependencies carried in. Two threads writing in one checkout collide; threads that only read, plan or answer share the project folder.
  3. The brief names the ticket, the files to read whole, the laws (the repo's review skill), the exact test commands and the proof required, and says to wait for every command it runs: a reply given while a command the agent left running in the background is still going holds the turn open until that command finishes, so the report lands minutes after the words and a foreground wait is the shorter road. A brief that says "fix the bug" comes back with a guess.
  4. Start builder threads with --detach (detach true on run) and --notify me, which prints each thread's id and returns, then end your turn: each builder's finished line comes back to you as a message carrying its whole report, in the order they finish, and nothing is polled or waited on. When you are not a thread yourself and the job is more than one turn, this whole loop is a coordinator thread's in the project's folder on this computer: give it the job, hand off, and tell the person where to read it. Without --detach, wsp run and run follow the first turn and return only at its reply, which is the right call for a short turn you read at once.
  5. A review is its own thread in the builder's worktree, wsp run <project> --cwd <its path>, with the branch name and the review skill; the builder fixes in its thread through send. A review runs well on a cheaper model, --model on run.
  6. Work leaves by git push and gh pr create from the thread, its agent's own git.
  7. Gates (full test runs, builds, packaging) run on the person's computer or one at a time on a box. A 2 vCPU, 4 GB computer runs one build or one agent at a time; two starve the daemon and the app reads the computer as not answering.

Where the person steps in

  • Sign-ins happen on the machine during wsp init, and each page is opened and finished on the person's computer: the run hands it over and waits. A thread cannot sign in for them; if gh auth status fails on the machine, say so and export instead of pushing.
  • Keys (an Anthropic key) live in the person's .env and never reach a thread. A secret a thread needs is asked for through wsp, not typed into a terminal.
  • An import cuts every secret-shaped file unless --keep names it; the plan's rows are the person's to answer before --yes.
  • The heavy rows and the sign-in choices in the recipe are the person's answers, put to them before anything is built.

Costs and limits

  • A machine naps by itself after 20 minutes without a person, a thread or a command touching it; the next thread, send or exec wakes it, with waking <name> on stderr from the command line.
  • A turn is cut after 10 minutes with no output from the agent, and at 6 hours in all. A long silent step (a full install, a build) needs output flowing or it ends the turn.
  • One thread runs one agent process; a 4 GB machine runs one build or one agent at a time. Put a second builder on a second machine, not a second thread on the same one.
  • A new thread runs on the model, effort and access named with --model, --effort and --access (the tool inputs of the same names), else the project's (wsp projects set), else the ones set for that agent (wsp agents set), else the agent's catalog defaults. A send into a thread runs on the model and effort it names, else the ones the thread's latest turns ran on. --access takes wsp's four words, ask, auto-edit, full and plan, and refuses an agent's own spelling. Choose the role by the brief and the picks, and keep review threads short and cheap.

Tools the catalog does not carry

wsp recipe ticks catalog rows. A tool the person's projects use that the catalog has no row for and no package manager here installed goes on the image as its own row:

wsp recipe --add just="brew install just" --add-check just="just --version"
wsp recipe --add ruff="uv tool install ruff"

--add <id>=<install command> is repeatable and --add-check <id>=<command> says what proves the tool landed (without one, command -v <id>). The line runs on the machine as given, as root, after every catalog install, with Homebrew and apt already there. Rules for adding one:

  • --add is for a tool this computer does not have. A package one of their own package managers already has is a row of its own: tick it with --set, not --add. An --add for one is refused in a line naming the --set word to use instead.
  • Add only what the person's own history or their repository files show in use: a tool their agents ran that no manager here has, a runner their project's config names, a package the image needs and this computer never installed. Never add on a guess.
  • Prefer a Homebrew, npm, uv or apt form (brew install x, npm install -g x, uv tool install x, apt-get install -y x) over a downloader. A line that pipes a download into a shell is refused in review.
  • One tool per row, so a row that fails names the tool that failed. A failed row does not fail the build; it is listed as failed on the machine's lineage.
  • There is no sign-in for these rows. A tool that needs a login needs a catalog row; say so instead of adding it.

Packages this computer already has

Everything wsp recipe scan lists under Also on this computer (alsoHere in the JSON) is a package one of their own managers installed, and each row's id there is the id to tick:

wsp recipe --set brew/zingzy/tap/diskbloom=on --set npm/turbo=off

Such a package is a row of its own, so the build installs it by the road the plan resolves for it: a formula with a Linux bottle by Homebrew, a tap formula with none from its GitHub release, pinned to the checksum its first install recorded, an npm or uv global by its manager. That is the same row and the same road the Also screen ticks when the person drives the wizard, so an agent-written recipe and a hand-driven one build the same image. --set <id>=off unticks one again.

Rules learned the hard way

  • A send into a running thread steers it or waits behind it, never a second concurrent turn (#274).
  • A thread's end reaches its parent thread or the person only when its start said --notify; nothing polls (#286).
  • A thread starts its children with --notify me, which the host resolves to the thread the request came out of, and ends its turn; a caller that is not a thread cannot be reached by a line at all, so it takes one turn's reply or hands the job to a coordinator thread. threads wait blocks and belongs in a shell script (#413, #479).
  • A thread works in --cwd, its worktree, or the project's folder; a relative --cwd is refused before anything starts (#280).
  • A turn ends on 10 minutes of silence, not a wall clock; long steps must print, and a command the agent left running in the background counts as work, so a quiet watch on one is not cut (#295, #679).
  • A reply that lands with a command the agent left running in the background holds the turn open until that command finishes, up to the turn limit of the computer it runs on (none on one the person owns until --turn-limit sets another): the thread reads Working until then and a send into it steers the agent. The reply's finished line goes at once and ends 1 background task still running; another line comes when this turn ends, so a lead waits for the second line rather than taking the child for finished. That second line carries the agent's own next words where the harness woke it with the result, and one line naming what finished where it stayed silent. Where the turn ends with the command still running, a server that never exits among them, the turn reads done and the second line names what is left running, ended with 1 background task running (#313, #679, #1539, #1887).
  • run and send return when the reply is complete, not when the process exits minutes later; the thread reads running until the agent process exits, and a send that meets that gap waits for the process and runs as the thread's next turn rather than being refused (#293, #364, #479).
  • A turn's process group dies with the turn; a server that must outlive it starts with setsid nohup ... & (#275).
  • Export refuses an existing folder; --replace overwrites it on purpose (#224).
  • A resumed thread runs in the folder its session started in, whatever folder is followed in the app (#236).
  • A send carries its own request id, so two clients sending the same text do not adopt each other's turn (#239).
  • The command line and both tool servers refuse a host of another release in one line naming both releases and what to run on the older end: wsp restart for an older host here, which the restart line and tool reach past the check.
  • Not here yet: a GitHub credential on the machine outside a sign-in during init (#279).
Last updated