Imported from Zingzy/wsp (
skills/wsp/SKILL.md). Install upstream withnpx skills add Zingzy/wsp --skill wsp. Copyright stays with the author.
wsp
wsp runs cloud machines called workspaces, forked in seconds from the image the person sealed with wsp init, each with coding agents working inside it as threads. A project is a repo on one computer, recorded with wsp add, and a workspace is a copy of that computer with that project inside, made for one piece of work and named by it. On the computer the app runs on a workspace is a copy of the project's folder beside it: it forks nothing, costs nothing and runs threads on the agents already on its PATH, which is where a quick subtask or a second harness goes when nothing needs a machine. The host on the person's computer (the desktop app, 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 workspaces to see every workspace and wsp threads to see the threads inside them. Open a thread with run, giving the workspace, the task and the agent to run; 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, on whatever workspace each runs, and nothing else: the person's own thread and another lead's tree on the same workspace 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 on the local workspace with the whole job and hands off, telling the person where to read it. stop ends a thread's running turn; run a command on a machine with exec; snapshot a workspace with a project loaded as a project image and new from it so the next machine starts with the project in place; export brings a folder and its agent sessions home; forget drops a workspace whose machine the provider no longer has, and delete deletes a workspace's machine and drops it here. pause naps a machine and wake wakes it; run, send and exec wake a paused workspace themselves before running, so a paused one needs no wake first. 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 the first workspace and wsp run <name> 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.
-
The health check comes before any verb:
wsp --version. With no wsp on the path and a shell of your own,npm i -g @zingzy/wspputs thewspcommand there; prefer that overnpx @zingzy/wsp, whose cache write fails under a sandboxed agent and leaves every command after it running with the sandbox off. Thenwsp 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 doctoris 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. Astarting the host for <path>; its log is <path>, and wsp down stops itline 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.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. AWorkspace <name> (ws_...) is a copy of <folder> on this computer; its threads run here, under your own sign-ins.line in it means the state held neither an image nor a workspace, so the host recorded this computer and served that: go to step 3 to seal an image, or to step 6 when this computer is enough to start on. Acommand not foundwith no shell to install from is the one thing to say and stop on, because nothing below can be run for them. -
The keys are the person's. Ask them to write
SOLARI_API_KEY=<their key from console.getsolari.com>into~/.wsp/.env, one line, andANTHROPIC_API_KEY=<key>beside it when they pay Anthropic by the token; on a Claude subscription they skip that 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 withwsp downand run step 1 again, so the new host reads the file.Expect: the host's log no longer names the Solari key, and
wsp threads --jsonanswers with one JSON line. -
Read everything before writing anything:
wsp recipe scanprints 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.--jsongives all of it as one object with arecommendedvalue 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, thenNothing was written.as the last line;wsp recipe scan --jsonanswers with one object holding the same rows. -
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|offfor 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|tokenfor 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 longerExpect: 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.jsonwhen 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. -
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 &It boots one builder machine, installs what is ticked, and at each sign-in the recipe answered
machineit 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 whenwaitSecondspass. Read/tmp/wsp-init.jsonlas it grows rather than waiting on the process, which keeps serving the app after the seal. 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. When the person would rather drive the wizard themselves,wsp init --recipe ~/.wsp/recipe.jsonin 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, whichwsp setup --jsonreads; run init without--jsonbeside a serving host, or take it down withwsp downfirst.Expect: one
{"event":"sign-in-result","tool":"gh","label":"GitHub CLI login","state":"signed-in"}line per hand-off ("state":"not-signed-in"with anotesaying why when the person did not finish in time; the run seals either way), thenImage v<n> sealed.in the log, which is the seal,Workspace first (<id>) forked from image v<n>.on the first seal, andOpen http://127.0.0.1:4400/. A JSON line fromwsp threadsis 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 (the
Set up cloud machinesrow in its sidebar), the same run is a job on their host:wsp setup --jsonanswers 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. -
First workspace. The first seal forks one, named
first, so on a fresh imagewsp wake firstanswers with its state (over MCP,workspaceslists it) and it is the one to use;wsp new devmakes another.Expect:
created dev <id>, printed when the machine is booted and reachable, or with--jsonthe creating stages and then the workspace, one JSON line each.wsp new: no image yet; run wsp initinstead 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. 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 isfirstand the builder, and the person chooses which workspace to pause. -
Record the project:
wsp add <url> --on <computer>for a repo the computer clones,wsp add <owner/repo> --on <computer>for one its signed-inghorglabclones, orwsp add <folder>for a folder on this computer, which every workspace of it is a copy of.wsp add <folder> --on <computer>is the third 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-memoryand--no-commits, and--rememberkeeps their ticks for the next add of that folder.wsp new "<what you are working on>"then makes the workspace with the project inside. Takewsp snapshot <workspace>once it stands, so the next workspace of that project starts from a project image with the checkout in place and no second clone.Expect:
wsp exec first -- ls <folder>lists the repo on the machine with the command's own exit code, 0, andwsp snapshot firstprints aproject image <id>line naming the project. -
First thread:
wsp run first --cwd <the project folder on the machine, absolute> --notify me "<the task>".Expect:
thread <id>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 meputs 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 first that <the first task>.
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 workspace is named 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 and each cloud account. A box's row carries what it last reported (cores, memory, free disk, the engine it has), whether it is connected right now, and WORKSPACES, how many it holds of how many it has room for; a cloud row carries its hourly rate. A computer is not a workspace: a project lives on a computer, and a workspace is a copy of that computer with the project inside |
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>" [--env <NAME>]... | --url <address> [--header <name>=<VARIABLE>]...) [--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 goes into that file alone, never into an answer; 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 |
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 workspaces [--watch] |
workspaces |
every workspace this host runs, one row each: its name, its id, the project it holds, the computer it runs on, the shape of its machine and its state as the sidebar shows it (running, paused, waking or unreachable, off the phase with the provider's word for the machine and the daemon reach beside it) where the kind has one. The name is what wsp run and every other verb take. --watch draws the same table again every second where it stands, until Ctrl-C, which needs a terminal and is refused beside --json |
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. Off, which every workspace reads as until this is run, they reach it not at all. On, every turn there is launched with the wsp tools and a token of its own, scoped to its thread: 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 (1 by default, so a thread a thread opened opens no more), and may touch no other workspace, delete nothing, pause nothing, import or export nothing and pair no computer. A machine forked under a thread carries the same switch, and wsp stop <root> ends the whole tree |
wsp projects |
projects, projects_add (base, 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 the checkout sits inside a workspace of it, the branch a workspace starts on, and how many workspaces stand on it. The name is what wsp new takes; a thread opened with no --cwd starts in its workspace's project. projects_add records one: source is a folder on the computer the app runs on or a repo's url, on names the computer that clones a repo, name what to call it here and base the branch a workspace 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 workspace of it stands, naming them |
wsp threads [<workspace>] [--tree] [--watch] |
threads (workspace) |
the sidebar's rows: project, workspace, agent, state (running, completed, interrupted, failed), who opened it (person, cli, agent), the computer it runs on, the title. --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 new [<project>] "<what you are working on>" [--from <project image>] [--size <cpu>x<memGb>] [--engine] [--spawn on|off] [--max-machines <n>] [--max-depth <n>] |
new (project, name, from, size, engine, spawn, max_machines, max_depth) |
a workspace for one piece of work: a copy of the project's computer with the project inside, booted and reachable when it returns, named by the work. With one project the name of it is not needed; with more, a line that names none is refused with the ones there are. The project decides which computer it lands on, so nothing else says where. On the computer the app runs on the workspace is a copy of the project's folder beside it, with a port of its own, so two pieces of work on one project are two checkouts. --size picks a machine size the provider offers, 2x4 for 2 vCPU and 4 GB; absent, the image's size. --from forks a project image of that project instead of the computer's own image head. --spawn on turns the workspace's agents switch on at the create, with --max-machines and --max-depth as wsp workspaces agents takes them. --engine gives the workspace the place's Docker or podman through a socket at the path a Docker client expects, which sees that workspace's own containers alone and refuses what would reach the computer itself; a place with no engine refuses it, and a recipe marked with wsp recipe --engine gives it to every workspace from its image |
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 new takes them; a fork a thread asked for carries the forking workspace's switch |
wsp bring back <workspace> [--title "<title>"] [--body "<body>"] |
bring_back (workspace, title, body) |
the work leaves the workspace the one way work leaves any checkout, as a branch on the project's remote: the branch the agent made is pushed and its pull request against the base is opened, or the one already open is answered with. The base is the branch its parent was on at the fork for a workspace forked out of another, whatever that parent does after, and the project's own base otherwise. On the base branch itself it is refused in one line, since wsp makes no branch and pushes none of the branch the work started from, and so is a branch with no commits the base lacks. --title names the pull request, and without one the host fills the title and the body from the commits. The two halves are reported apart: the branch, the count over the base and the diffstat are printed whenever the push landed, then either the pull request, the note saying why it waits where the machine has no signed-in command line for the git host, or the pull request half's own refusal, which the push still stands under. A push git refused for want of a credential says so and names the command only the person at that computer can run; what is left uncommitted inside the workspace stays there and the line says how much |
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 |
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 <workspace> [--yes] |
delete (workspace, confirm) |
deletes the machine at the provider, then drops the workspace's record and threads from this computer once the provider reads the machine gone; nothing left on that disk survives, and the tool deletes only when called with confirm true. A machine the provider still holds after two asks keeps its record, the delete fails naming it, and the workspace's reason in wsp workspaces --json says so until a delete takes or the host restarts |
wsp run [<workspace>] [--agent <id>] [--model <slug>] [--effort <word>] [--access <word>] [--cwd <path>] [--notify <thread|me>] [--title <name>] [--image <path>] [--detach] "<task>" |
run (workspace, task, agent, model, effort, access, cwd, notify, title, images, detach) |
an agent works in the workspace and the reply comes back: a thread in that workspace's project, following its first turn to the reply; with no workspace, run from inside one of your project folders, it goes to that project's workspace and the first line says so. With no agent named it runs the one the last thread on that project used; with --detach, prints the thread id the moment the turn is started and returns |
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 workspace reads the same as a running one |
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> |
thread_deny (thread) |
answers the same prompt the other way: the call is refused and the turn goes on with that answer |
wsp send <thread> [--model <slug>] [--effort <word>] [--image <path>] [--detach] "<message>" |
send (thread, message, model, effort, images, 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; follows the turn to the reply, or with --detach returns the moment the turn is started |
wsp stop <thread> |
stop (thread) |
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 |
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 |
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>] [--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 on the tool says what the added rows are for; --engine marks the recipe so every workspace 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 written since that nap is not on it |
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 |
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 |
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, and a cloud account keeps no computer to browse |
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 cloud setup on this host as the app's Set up cloud machines modal reads it: which keys are held (never their values), the agents here and whether each carries the wsp tools, what a machine costs, 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, wsp remove, wsp join, wsp leave, the seven lines under the word host, wsp login, wsp logout, wsp hosts, wsp image export <file> and the three sign-in lines, since each starts, stops, installs or hands out access to something on the person's computer, the sign-ins run in a terminal a person types into, and wsp image export writes the person's own sign-ins into one file: any command that needs the host starts one when none serves that state file, on free ports, 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>] [--ws-port <n>] [--listen <addr>] [--state <path>] serves 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. 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. wsp status prints whether a host is serving this state file, its ports, 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 workspace it holds under those rows with the name and id that workspace 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 default alias 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 client redeems a one time code for a token of its own. wsp up --provider <name> says which machine provider that host forks on, and --advertise <url> the address a machine reaches the host back at, which the host works out for itself when it binds an address of its own. wsp up --service carries every one of these into the unit it installs, so the service serves the line that was typed and not a shorter one. wsp add is how a computer the person owns becomes a place in their wsp: with no argument it prints the wsp join line and a code to type on that computer, which is spent by the first join and stands for ten minutes; wsp add <provider> asks for that provider's key, puts it to the provider before anything is written and saves it in ~/.wsp/.env; 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 workspaces standing on it go with their threads, and the computer is otherwise left as wsp found it; 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 launchd agent or a systemd user unit which dials again at every login, --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 default alias, 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 the laptop paired with it. wsp host connect <url> --code <code> --name <alias> is the other side of that code: it redeems one against a host on another computer, keeps the token it buys in a file only this person can read, and gives that host a name. wsp hosts is the one listing of the hosts this computer can reach, the ones on the person's account and the ones it paired with a code, with the road each is held by in a VIA column, a live beat for the account's and the one every line takes marked; wsp host default <alias> moves that mark, and wsp host forget <alias> hands the token back and forgets it. A box nobody can reach inbound takes the third road: 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 default alias. 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 one workspace end to end; wsp mcp [--host <alias>] serves the tools over stdio, against this computer's host or the one that alias names; 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. 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. 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 workspace, and a box they added takes wsp add spoo --sign-in codex, which runs a shared login at that box's logins folder and any other as the owner of its home; wsp servers signin notion --agent claude --on spoo runs the harness's own sign-in for one MCP server where it is set up; wsp agents key claude puts the token claude setup-token prints, or an agent's API key, into the host's vault, typed where nothing echoes it and only at the host's own terminal. Hand the person the line; never run one for them. An entry under installed with no path took the skill and not the server, which is the by-hand line the prose prints. Only agents the catalog knows an MCP config for get the server: claude, codex, gemini, opencode. The rest get the skill and a by-hand line.
For a shell script
One verb is a shell script's and not an agent's, because it blocks: a script that starts threads and has to stand at the end of them has nothing else to do while they run.
| command 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 on a workspace well
These decide whether work on a machine goes fast or stalls, and they hold on every workspace.
- 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 meresolves 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 on the local workspace, with the whole job in its brief and the person told where to read it.
- A project on this computer is a workspace too, the one
wsp add <folder>andwsp new "<what you are working on>"make: put a quick subtask or a second harness on it, since it forks nothing, starts in a second, costs nothing and runs the agents already on this computer's PATH over the person's own files and sessions. Fork a cloud workspace 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 workspace. - Snapshot a workspace once a project's dependencies are installed on it:
wsp snapshot <workspace>keeps that disk as a project image, andwsp new <name> --from <that image>starts every later workspace with the install already there, so no thread installs the same dependencies twice. - 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_moduleshardlinked into the new worktree beforepnpm install --offline, which then only verifies. A worktree set up from scratch relinks thousands of files and rebuilds native modules, minutes on 2 vCPU. 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, orwsp stop <thread>first.- Restarting the host cuts every turn running on every workspace. Finish or stop the running turns before
wsp downandwsp up. - Pause a workspace nobody is using with
wsp pause <workspace>: a sleeping machine costs nothing beyond its disk and gives back one of the two machines the account runs at once, 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
--titleand keep the reply short and complete.
The contract
The command line, the MCP tools and this skill are one contract: every capability exists in all three or in none, and the parity test in the repo holds them to it. With --json, stdout carries JSON alone: one JSON object per line, frames first, the last line is the result; a verb with no stream prints the result alone. The result is the object the MCP tool of the same name answers with, less what the frames already carried, so no line prints twice: wsp exec --json prints one exec.output frame per output line and then a result with the exit code and the folder; wsp fork dev --send "<task>" --json prints the creating frames, the workspace and then {"turn": ...}. 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, Solari 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 c
Truncated - read the full file at https://github.com/Zingzy/wsp/blob/3d97283622eac0d770ca7ef43a64c63687374654/skills/wsp/SKILL.md.
