How to set a person up on wsp from nothing and how to run threads on wsp from the command line or the MCP tools. The numbered walkthrough from a health check to the first thread, the scan that reads their computer, the recipe minted from what their agents actually used, the two questions to put to them about the heavy rows and the sign-ins, the init you run for them and the sign-in lines you hand over as they come, every verb and tool with its flags, its inputs and an example, the loop for bu...
Installs into .claude/skills of the current project.
Are you the author of Wsp?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/wsp-labs-wsp)
---
name: wsp
description: How to set a person up on wsp from nothing and how to run threads on wsp from the command line or the MCP tools. The numbered walkthrough from a health check to the first thread, the scan that reads their computer, the recipe minted from what their agents actually used, the two questions to put to them about the heavy rows and the sign-ins, the init you run for them and the sign-in lines you hand over as they come, every verb and tool with its flags, its inputs and an example, the loop for building a ticket, where the person has to step in, what a machine costs and where its limits are, and the rules learned the hard way. Read it before setting anyone up, opening a thread, sending into one, making a worktree, forking a machine or bringing a project home.
---
# wsp
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<!-- cloud --> or a cloud account<!-- /cloud --> a thread runs on a machine forked in seconds from the image the person sealed with wsp init, with the project inside. 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 machines 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 on a machine with `exec`; <!-- cloud -->`snapshot` a machine with a project loaded as a project image so the next machine starts with the project in place; <!-- /cloud -->`export` brings a folder and its agent sessions home; `delete` takes a thread away with its turns, and a box's machine with its threads. `pause` naps a machine and `wake` wakes it; `run`, `send` and `exec` wake a paused machine themselves before running, so a paused one needs no wake first. 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. <!-- cloud -->`Solari API key: no terminal to ask on; set SOLARI_API_KEY in the environment, ./.env, or ~/.wsp/.env.` in it is no key at all: go to step 2. <!-- /cloud -->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<!-- cloud -->, and `SOLARI_API_KEY=<their key from console.getsolari.com>` beside it, one line each<!-- /cloud -->; 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: <!-- cloud -->the host's log no longer names the Solari key, and <!-- /cloud -->`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 &
```
<!-- no cloud -->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. <!-- /no cloud -->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. <!-- cloud -->Say what it costs before starting it: about $0.11 an hour while the builder runs, and it holds one of the account's two machine slots. <!-- /cloud -->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, `Workspace first (<id>) forked from image v<n>.` on the first 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. First machine. The first seal forks one, named `first`, so on a fresh image `wsp wake first` answers with its state and it is the one a thread on that computer's project runs on. On this computer nothing is made: a thread runs in the project's folder.
Expect: `wsp wake first` prints its state, running once the machine is booted and reachable. `no image yet; run wsp init` instead means the host is serving without a sealed image, because an init is still running or ended without sealing: wait for it, or go back to step 5.<!-- cloud --> A refusal that names the account's machine cap means two are already up; the builder stays up ten minutes after a seal and counts as one, so right after a seal that is `first` and the builder, and the person chooses which machine to pause.<!-- /cloud -->
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.<!-- cloud --> Take `wsp snapshot first` once the project stands on the machine, so the next machine of that project starts from a project image with the checkout in place and no second clone.<!-- /cloud -->
Expect: `wsp projects` lists it, with its folder and, for a folder here inside a git repo, the branches `--branch` takes<!-- cloud -->, and `wsp snapshot first` prints a `project image <id>` line naming the project<!-- /cloud -->.
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 box's machine, the `<workspace>` a box verb takes, by its name, or by its id when two share a name. A thread is named 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 line | MCP tool | what it does |
|---|---|---|
| `wsp computers` | `computers` | every computer this host holds, which is the whole of where work can run: the computer the app runs on, each box joined to it<!-- cloud --> and each cloud account<!-- /cloud -->. 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<!-- cloud --> and MACHINES on a cloud<!-- /cloud -->, how many run there now against its cap (threads at once<!-- cloud -->, machines at once<!-- /cloud -->), <!-- cloud -->SPEND on a cloud, what it spent today against its spend per day ($2.31/$10), <!-- /cloud -->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, <!-- cloud -->At limit once a cloud's spend today reaches its spend per day, which refuses a new machine there until midnight and leaves running ones alone, <!-- /cloud -->or why it takes nothing<!-- cloud -->; a cloud row carries its hourly rate<!-- /cloud -->. A project lives on a computer: on the computer the app runs on its threads run in its folder, and on a box<!-- cloud --> or a cloud<!-- /cloud --> on a machine forked from the image with the project inside. `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>] <!-- cloud -->[--machines <n>] [--spend <usd>] <!-- /cloud -->[--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, <!-- cloud -->machines, spend, <!-- /cloud -->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<!-- cloud -->, for a cloud<!-- /cloud --> 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, which is one per 2.5 GB of that computer's memory up to 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<!-- cloud -->, and 6 on a cloud<!-- /cloud -->, 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, as `wsp workspaces agents` sets it for one (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, while one given a switch of its own keeps it and a cap named on one tightens the switch it inherits<!-- cloud -->; on a cloud `--machines`, how many machines may run there at once (3), and `--spend`, the dollars a day it may spend before it starts no new machine ($10)<!-- /cloud -->. 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 recipes` | `recipes` | every 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 and the computers that follow it; 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>` | `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 this computer's agents 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 the host reads off this computer's own agent logs, 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 alone | spends 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 [<workspace>] [--on <computer>]` | `agents` (workspace, on) | the coding agents the catalog knows as they stand on this computer, a box you added or a workspace: 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 workspace answers what stood there when it last ran and is not woken |
| `wsp skills [<workspace>] [--on <computer>]` | `skills` (workspace, 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 workspace adds its project's own skills, and a plugin's skills are marked as the plugin's |
| `wsp servers [<workspace>] [--on <computer>]` | `servers` (workspace, on) | every MCP server each agent's config file defines there, and a workspace's project files: 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> [<workspace>] [--on <computer>] [--project [<name>]]` | `skills_show` (skill, workspace, 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` from a workspace, or `--project <name>` with `--on` for that computer's project; nothing in it runs |
| `wsp skills add <skill> [<workspace>] [--on <computer>] [--agent <id>]... [--project [<name>]]` | `skills_add` (skill, workspace, 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` from a workspace, 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> [<workspace>] [--on <computer>] [--project [<name>]]` | `skills_remove` (name, workspace, on, project) | every folder of that skill and every link to it, gone, the project's with `--project` from a workspace, 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> [<workspace>] [--on <computer>]` | `skills_disable` (name, workspace, 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> [<workspace>] [--on <computer>]` | `skills_enable` (name, workspace, on) | turns a skill that was turned off on again, its SKILL.md.off renamed back |
| `wsp servers tools <name> --agent <id> [<workspace>] [--on <computer>] [--project <name>] [--refresh]` | `servers_tools` (name, agent, workspace, 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 workspace is not woken |
| `wsp servers add <name> [<workspace>] [--on <computer>] --agent <id> (--command "<line>" \| --url <address> [--header <name>=<VARIABLE>]...) [--env <NAME>]... [--project [<name>]]` | `servers_add` (name, agent, workspace, 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` from a workspace, 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 or workspace the file names a variable (`${NAME}`, or `WSP_MCP_<SERVER>_<HEADER>` for a header) and the value goes to the vault, which hands it to every turn; 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> [<workspace>] [--on <computer>] --agent <id> [--scope <user\|home\|project>] [--project [<name>]]` | `servers_remove` (name, agent, workspace, 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> [<workspace>] [--on <computer>] --agent <id> [--scope <user\|home\|project>] [--project [<name>]]` | `servers_disable` (name, agent, workspace, 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> [<workspace>] [--on <computer>] --agent <id> [--scope <user\|home\|project>] [--project [<name>]]` | `servers_enable` (name, agent, workspace, 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 workspaces agents <workspace> [--spawn on\|off] [--max-machines <n>] [--max-depth <n>]` | `workspaces_agents` (workspace, spawn, max_machines, max_depth) | what the agents inside that workspace may ask of the host. Every turn there is launched with the wsp tools and a token of its own, scoped to its thread, whatever the switch says. On, which every workspace reads as until a person turns it off, that thread may open threads and fork machines under itself, up to `--max-machines` machines standing at once under one root thread (3 by default) and `--max-depth` levels deep (2 by default, so a thread a thread opened may open its own and those open no more), and may touch no other workspace, delete nothing, pause nothing, import or export nothing and pair no computer. Off, each of those acts is refused in one line naming the workspace and the act. A cap named alone tightens the switch as it stands, on or off. On this Mac the token is identity, not confinement: a thread there runs as the person and can read the host's own token file, so its children nest and count, but nothing walls it off. A machine forked under a thread carries the same switch, and `wsp stop <root>` ends the whole tree |
| `wsp projects` | `projects`, `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>` | `projects_remove` (project) | takes a project's record out of this wsp; nothing of the code is touched, and it is refused while a machine of it stands, naming them |
| `wsp projects set <project> [--agent <id>] [--model <slug>] [--effort <word>] [--access <word>] [--reset <field>]...` | `projects_set` (project, agent, model, effort, access, 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. 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. 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 TASK 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) on the thread. `--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 fork <workspace> [--name <n>] [--size <cpu>x<memGb>] [--spawn on\|off] [--max-machines <n>] [--max-depth <n>] [--send "<task>"] [--agent <id>] [--model <slug>] [--effort <word>] [--access <word>] [--cwd <path>] [--notify <thread\|me>]` | `fork` (workspace, name, size, task, agent, model, effort, access, cwd, notify, spawn, max_machines, max_depth) | a sibling from the source's image version (a new machine, not a copy of its live disk); with a task, its first thread, and the flags after `--send` are run's. `--spawn`, `--max-machines` and `--max-depth` set the new workspace's agents switch, as `wsp workspaces agents` takes them; a fork a thread asked for carries the forking workspace's switch | <!-- cloud -->
| `wsp commit <workspace> [--message "<message>"] [--file <path>]...` | `commit` (workspace, message, files) | commits the files the workspace's copy changed against its last commit, untracked ones added, running the copy's hooks as git does. `--file` names some of them by path from the checkout's top, once per file, and leaves the rest uncommitted; without it every changed file goes in. Without `--message` (`-m` for short) the workspace's own agent drafts the message from the diff and the task its newest 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 copy (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 copy still holds the index after one wait. Nothing reaches a remote: the app's Push is what pushes |
| `wsp discard <workspace> <path>` | `discard` (workspace, path) | puts one changed file of the workspace's copy 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. Refused in one line when the file has no change |
| `wsp fix <workspace> [--check "<name>" \| --child <workspace>]` | `fix` (workspace, check, child) | asks the workspace's agent to fix its pull request. `--check` names a check that failed: the failed steps of its job's log go to the workspace's 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 copy 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. Refused in one line where the workspace 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 workspace in the thread's tree, as it may commit there. `--child` names a child of the workspace whose merge into it stopped on conflicts: nothing is updated, and the workspace'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 the child's branch into the lead's copy 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 branch is fetched from the project's remote; a project with none merges from the child's own folder where both copies sit on the computer the app runs on, and is refused in one line naming the project where either sits on another. A lead 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 copy 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 workspace that is not the lead's child, and for a thread merging into any workspace but its own. Nothing is pushed: the lead's own push carries the merges |
| `wsp merge <workspace> [--method merge\|squash\|rebase] [--when-checks-pass]` | `merge` (workspace, method, when_checks_pass) | merges the workspace's pull request 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\|workspace> [--agent <id>] [--model <slug>] [--effort <word>]` | `review` (target, agent, model, effort) | starts a reviewer thread on a pull request, off its link or off a workspace's own pull request: a fresh copy at 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 task. Its reply ends in a review the host keeps as the workspace's 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 <workspace> [--verdict comment\|approve\|request-changes] [--summary "<text>"]` | `review_post` (workspace, verdict, summary) | posts the review workspace'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 `<name>: 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 <workspace>` | `update` (workspace) | merges the latest commits of the workspace's base from the remote into its branch with a merge commit, so a branch already pushed is never rewritten, and says how many it brought. A copy 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 copy 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 workspace in the thread's tree, so a lead keeps its children's branches current |
| `wsp pause <workspace>` | `pause` (workspace) | naps the machine; it wakes on the next thread or command |
| `wsp wake <workspace>` | `wake` (workspace) | wakes the machine ahead of a thread or command and prints its state after; a running one comes back unchanged |
| `wsp rename <workspace> "<name>"` | `rename` (workspace, name) | names the workspace on this computer, the name every listing shows and the one every verb takes; a name another workspace holds and a blank one are refused and nothing is renamed. Threads on the machine run on through it |
| `wsp image` | `image` | the 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. The project images taken off workspaces are listed under it, each with its snapshot id, the workspace it was taken off, its size where the provider lists one and its date |
| `wsp image build <place> [--force]` | `image_build` (place, force) | builds this host's image at a place from the record alone: a builder is forked there with the recipe the image was sealed from and every sign-in set to skip, the sign-ins the seal held are landed on it out of the vault, and the copy is sealed and recorded at that place under the record's hash. Nothing signs in again and no Keychain is read. A place that already holds a copy built from this record is answered with that copy and `built` false, so asking twice costs nothing. Refused for a place this host does not hold, for the place this host forks on, whose copy is what `wsp init` builds, for a place that takes no copy at all, and for a record sealed without the recipe it was built from; a record holding no sign-ins is refused too, since every copy of it would ask for them again, and `--force` builds it anyway | <!-- cloud -->
| `wsp forget <workspace> [--yes]` | `forget` (workspace) | drops a workspace whose machine is gone: its record and threads leave this computer; refused while the machine exists |
| `wsp delete <thread>\|<workspace> [--yes]` | `delete` (thread, workspace, confirm) | a thread on this computer 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. A box's workspace goes as before: the machine is deleted at the provider, then its record and threads leave this computer once the provider reads the machine gone; nothing left on that disk survives. The tool deletes only when called with confirm true |
| `wsp run [<project>] [--branch <branch>] [--cwd <path>] [--agent <id>] [--model <slug>] [--effort <word>] [--access <word>] [--fast] [--notify <thread\|me>] [--title <name>] [--file <path>] [--detach] "<message>"` | `run` (project, branch, cwd, message, agent, model, effort, access, fast, notify, title, 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. A project on another computer gets a new machine forked from the image, named off the message as the app's New thread names one, its stages on stderr. `--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 |
| `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 and installed dependencies carried in and build folders tied to their path left to rebuild. `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]` | `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>` | `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 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> [--task <id>]` | `stop` (thread, task) | ends the thread's running turn, as the app's stop button does; the machine stays up. A thread whose agents spawned threads of their own stops as one, and the line names each of those it ended. `--task` stops one of the agent's own subagents by its TASK id off `threads` and nothing else: the turn and its other subagents run on |
| `wsp restart` | `restart` | stops 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 <workspace> [--cwd <dir>] -- <command...>` | `exec` (workspace, argv, cwd) | runs the command on the machine, each word as given, in the folder named or the one a thread would start in, waking it first when it is paused; output lines, the exit code and the folder it ran in |
| `wsp snapshot <workspace>` | `snapshot` (workspace) | a project image: the image plus the loaded project as its disk stands, synced first so a file written just before is whole on the image; a sync that fails takes nothing | <!-- cloud -->
| `wsp export <workspace> <folder> [--from <path>] [--replace] [--agents <ids>]` | `export` (workspace, folder, from, replace, agents) | the folder and the agent sessions keyed to it come home to this computer |
| `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 line | MCP tool | what it does |
|---|---|---|
| `wsp rebuild <workspace>` | `rebuild` (workspace) | the road out of gone: a fresh machine from the workspace's image, with the vault its last nap left imported, under the same workspace, name and threads; prints the new machine's id and state, and is refused on a machine that still answers. Work that was not pushed is lost with the old disk, and its home folder comes back from the last saved nap | <!-- cloud -->
| `wsp image move <workspace>` | `image_move` (workspace) | moves the workspace onto the newest version of its image: a fresh machine of that image replaces the old one and the home folder comes across, less the files the image itself wrote and nobody changed here, whose newer copies come with the image. `kept` names the files of the image's own this workspace had changed and which travelled instead. An archive carries no deletion, so a file taken out of a folder the image writes into comes back with the new image. Anything installed outside the home folder comes from the new image, and everything running on the old machine stops with it | <!-- cloud -->
| `wsp image remove <snapshot id> [--yes]` | `image_remove` (image, confirm) | deletes a project image's snapshot at the provider and then drops its record, so no later `--from` forks from it; it takes the id `wsp image` lists and never a project's name. The provider's listing is read back until the id leaves it: a snapshot the provider already lost drops its record and says so, a listing that still holds the id after the wait keeps the record, and any other refusal keeps it with the provider's own words. Refused while any workspace stands on the image, whatever its state, naming them, so a gone one blocks it until `wsp forget` takes its record; the tool without `confirm` answers with what would go and removes nothing | <!-- cloud -->
| `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<!-- cloud -->, and a cloud account keeps no computer to browse<!-- /cloud --> |
| `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 setup` | `setup` | the 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<!-- cloud -->, what a machine costs<!-- /cloud -->, 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`<!-- cloud -->, `wsp image export <file>`<!-- /cloud -->, `wsp ssh`, `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<!-- cloud -->, `wsp image export` writes the person's own sign-ins into one file<!-- /cloud --> and `wsp ssh` pipes the bytes of an ssh client on the computer the host runs on: any command that needs the host but `wsp ssh` 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. <!-- cloud -->A service starts without the shell that installed it, so the Solari key has to be in `~/.wsp/.env` and not exported in that shell, and `wsp up --service` refuses with that line when it is only in the shell. <!-- /cloud -->`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<!-- cloud -->, and `--provider <name>` which machine provider that host forks on<!-- /cloud -->; 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; <!-- cloud -->`wsp add <provider>` asks for that provider's key, puts it to the provider before anything is written and saves it in `~/.wsp/.env`; <!-- /cloud -->`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 under that login's own service manager 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>` takes a computer back out: the agent and its files come off it over the link, the machines standing on it go with their threads, and the computer is otherwise left as wsp found it; 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` sweeps wsp off that computer for somebody whose host is gone. `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. <!-- cloud -->`wsp image export <file>` writes the image record and the sign-ins it holds into one encrypted file at that path, sealed to a passphrase: it asks for it twice at a terminal and reads `WSP_IMAGE_PASSPHRASE` where there is none, never a flag, since every process on a computer can read another's command line. There is no tool for it: the vault leaves the host only at a person's hand. <!-- /cloud -->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 <workspace>` inside a box's machine, 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. `wsp ssh <workspace>` is the ProxyCommand the host's own ssh config at `~/.wsp/ssh_config` gives every `wsp-<name>` alias: it starts the machine's own ssh server there, with the one key wsp made on this computer allowed and the server's key pinned, and carries the connection over the host's link, so `ssh wsp-<name>` from the person's terminal and an editor's remote window land inside the machine. The person's own `~/.ssh/config` reads that file through one `Include` line, put there only when they say yes on their first Open of a machine on another computer. 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 line | MCP tool | what 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.
- 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.<!-- cloud --> Fork a cloud machine for builds that run beside each other, for anything that should not touch this computer, and for a disk you can snapshot and hand to the next machine.<!-- /cloud -->
- Snapshot a machine once a project's dependencies are installed on it: `wsp snapshot <workspace>` keeps that disk as a project image, so every later machine of that project starts with the install already there and no thread installs the same dependencies twice. <!-- cloud -->
- 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`.
- Pause a box's machine nobody is using with `wsp pause <workspace>`: a sleeping machine costs nothing beyond its disk<!-- cloud --> and gives back one of the two machines the account runs at once<!-- /cloud -->, and the next thread or command wakes it.
- 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<!-- cloud -->; `wsp fork dev --send "<task>" --json` prints the creating frames, the machine and then `{"turn": ...}`<!-- /cloud -->; `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:
| exit | class | when |
|---|---|---|
| 0 | `ok` | it did what its line says; with --json stdout holds the answer |
| 1 | `provider` | the host, the runtime, a provider or the machine refused or failed |
| 2 | `auth` | no key, no sign-in, or the host refused the token |
| 3 | `usage` | the 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. 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 and installed dependencies carried in and build folders such as `target` and `.next` left to rebuild on first use. `--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 another computer gets a new machine forked from the image for the thread, named off the message's first words as the app's New thread names one, with the fork's stages on stderr; a machine named in place of the project runs the thread there, as before. `--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; 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, and a reply given with background tasks still running goes once they are done. 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; otherwise the message waits for the running turn to end and then runs (outcome `queued`). 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. 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. The machine is not paused or killed and the thread takes the next `send`. Use it when a `send` started a turn you did not mean to, instead of pausing the machine.
```
wsp stop 1a2b3c4d --task a1b2c3d4e5f6a7b8
```
Stops one subagent the thread's agent started (Claude Code's Agent tool), by the id in the TASK 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> task <task> stopped` once the agent took the stop, `thread <id> task <task> 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 is `{threadId, task, outcome}`, with `error` beside a refused or unsupported outcome.
### exec
```
wsp exec dev -- git status --short
wsp exec dev --cwd /root -- sh -c 'ls | wc -l'
```
Each word after `--` reaches the machine as one argument; a shell line goes through `sh -c`. The command runs in the folder `--cwd` names (absolute, on the machine), else in the folder a thread on it would start in, the project's folder, which on a fork is where the project was cloned or imported and else the machine's home folder, 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 machine going away is an error. The machine runs as root with home /root and no login shell, so `bash -c`, never `bash -lc`.
### fork, snapshot <!-- cloud -->
```
wsp snapshot dev # project image of dev: its image plus the imported project, its disk synced first
wsp image remove snap_1a2b3c4d # deletes that project image at the provider and drops its record; refused while a machine stands on it
wsp fork dev --send "Run the gate." # a sibling machine from dev's image version, first thread opened
wsp fork dev --size 2x4 # a machine at a size the provider offers; the refusal lists them
```
`fork` copies the image version, not the live disk: work on the source's disk is not on the fork. Fork from a project image when the fork needs the project. When a fork's first turn fails, the fork still exists and the error names it; continue with `wsp run` on it, do not fork again. `snapshot` takes only a running first-life machine with a project imported; a woken machine or one without a project is refused in one line and nothing is taken.
### 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 every other computer on a machine forked from that computer's image with the repo cloned into it at `/root/<project>`.
### export
```
wsp export dev /Users/zingzy/wsp-from-dev --agents claude
```
The folder must not exist on this computer unless `--replace`. `--from` is the folder's path on the machine, the same path as the destination when absent. The sessions keyed to the folder land in each agent's home here, keyed to the new path.
### 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 (on a machine only when the image signed in to GitHub during wsp init), or by `export` to this computer.
7. Gates (full test runs, builds, packaging) run on the person's machine or one at a time on a box's machine. A 2 vCPU, 4 GB machine runs one build or one agent at a time; two starve the daemon and the app reads the machine 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 (<!-- cloud -->the Solari key, <!-- /cloud -->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.
- The account holds two machines at once. A third `fork` is refused; the person decides which machine to pause. <!-- cloud -->
- A pause or a wake that does not return is stuck on the provider side; tell the person rather than retrying in a loop. <!-- cloud -->
- A machine is the image's size unless `fork` is given `--size`; its row says the size the machine has, and the created line says so when that is not the size asked. A size the provider does not offer is refused in one line that names the ones it does, with the rate of each. A build or a test run wants the largest memory offered: on 4 GB one build starves the machine. <!-- cloud -->
- 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 cloud machine costs about $0.11 an hour while awake (2 vCPU at $0.035 per vCPU-hour plus 4 GB at $0.01 per GB-hour). Snapshots are free up to 10 GB an organization, then $0.05 per GB-month from 2026-10-01. <!-- cloud -->
- 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, and `wsp wake` wakes it ahead of them.
- 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.
- The root disk is 20 GB; wsp keeps 2 GB free and skips tool installs that would go under it. <!-- cloud -->
- 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<!-- cloud --> and six hours on a cloud<!-- /cloud --> until `--turn-limit` sets another): the thread reads Working until then, a send into it steers the agent, and the reply is delivered when the command is done, carrying 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 reply's last line names what is left running, `ended with 1 background task running` (#313, #679, #1539).
- `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).
- Snapshot only a running first-life machine with a project loaded; a woken machine is refused (#223). <!-- cloud -->
- 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 MCP server and the command line refuse a host of another version in one line; restart it with wsp up (#290).
- Not here yet: a GitHub credential on the machine outside a sign-in during init (#279).