Felix Itzenplitz
de

Schwitzer: a task room for coding agents and the people who run them

Issued
Extent
13 min read · 2675 words
By
Felix Itzenplitz
Schwitzer's agent harness flow: one task's stages from Idea to Done with the human decisions above and the agent actions below, the path of one tool call from omp through the plugin and the app's MCP server to the room server, and the path of one live session from the claim marker to every member's sessions panel.
One task through the room, one tool call, one live session. From the repository’s docs/harness.svg.
Contents

Schwitzer is a shared task room for coding agents and the people who run them. Someone opens a room, without signing up, and shares its key. Humans or agents note ideas and propose tasks. Humans accept them. An agent writes a plan, a human approves it, and agents build the approved tasks one at a time, each claiming the next free one.

We built it because the two of us each run omp agents on our own machine and our own subscription, and that work had no shared place. We wanted one light queue with explicit human decisions and a live view of each other’s agent sessions. No Kanban ceremony, and nobody steering another person’s agent. Schwitzer is a narrow desktop window that sits beside the editor all day. A glance at it shows who is working on what, which plans are finished and which agent sessions are running.

The name comes from schwitzen, to sweat. The line it started from is “Agents fischen sich Tasks raus und schwitzen”: agents fish tasks out of the room and sweat. The website says it shorter: “Your agents sweat. You decide.” The first commit is from 18 October 2026. Versions 0.1.0 to 0.1.6 were published that same day, 0.1.7 followed on 19 October, and 0.1.8 came out today. It is licensed under MIT or Apache-2.0. Source is on GitHub (opens in a new window) , and the website is here (opens in a new window) . There is also a public server you can open rooms on right away. More on that below.

How a task moves

Schwitzer's narrow task list for the room harbor: under In progress one task building and one being planned, a plan ready to pick, a proposal and an idea, and below them mara's two live agent sessions.
A room called harbor, with synthetic demo tasks: two claimed tasks under In progress, a plan ready to approve, a proposal, an idea, and two live agent sessions below.

A room holds projects, and every task belongs to one. A task goes through a small, fixed set of stages, and for every step it’s fixed who may take it:

From → toWho
→ Idea, → Proposed, Idea → Proposedhuman or agent
Proposed → Acceptedhuman (Accept)
Accepted → Plannedagent, with its own plan claim (submit_plan)
Planned → Readyhuman (Approve), plan not empty
Planned/Ready → Acceptedhuman (Replan)
Ready → Doneagent, with its own build claim (finish)
→ Rejectedhuman (Reject)
Done/Rejected → Proposedhuman (Reopen)

“In progress” isn’t a stage of its own. It is an Accepted or Ready task that someone has claimed, and the app labels it “Being planned” or “Building”. There are three priorities, High, Normal and Low, and no columns to drag cards between. The list is the whole interface: every task is a raised slab with its next step as a button on it, so Accept sits on a proposal, Approve on a finished plan, Pick on an approved task and Unclaim on your own claim. Reject, Replan and Reopen stay inside the opened task.

Humans decide, agents work. Accept, Reject, Approve, Replan and Reopen exist only as buttons in the app. None of the tools the agents get can send a decision. To be precise about where that rule lives: it lives in the app, not in the server. The server has no roles. Any member may send any request, and store::require_member only checks that the caller belongs to the room. A modified client could send a Decide.

Claiming is the part that lets several agents work from the same queue. A Claim with a task number takes that task or fails. Without a number, the server takes the first unclaimed task in the right stage:

ORDER BY priority, number LIMIT 1

That means high priority first, then the oldest. Selection and update run in one SQLite transaction, so two agents asking at the same moment can’t get the same task. A claim never expires. Reject, SubmitPlan, Finish and Unclaim clear it, and any member can unclaim any claim, so a task left behind by a crashed session can be freed by whoever notices.

Agents work through omp

Linking a repository to a project in Settings makes the app write four files into that repository:

PathContent
.omp/plugins/node_modules/schwitzer/index.tsthe plugin source, compiled into the app
.omp/plugins/node_modules/schwitzer/package.jsonname, the app’s version, the omp extension entry
.omp/plugins/node_modules/schwitzer/schwitzer.jsondata dir, MCP URL, projects, the other linked repos (no token)
.omp/plugins/omp-plugins.lock.jsonthe lock entry with the app’s version

omp loads it as a project plugin the next time it starts in that repo. Every 5 seconds the app compares each installed plugin with what it would write now and rewrites an outdated one, so the plugin follows app updates on its own. In the repo, the plugin gives you one command:

/schwitzer plan            pick an accepted, unclaimed task, claim it for planning, start plan mode
/schwitzer build           pick an approved, unclaimed task, claim it for building
/schwitzer propose [text]  the agent drafts a proposal (or writes up an open idea), asks, then sends it
/schwitzer idea <text>     note an idea right away, without an agent turn

Behind the command is an MCP server inside the app, at 127.0.0.1:<mcp_port>/mcp with port 13339 by default. It wants a bearer token, which the app keeps in <data dir>/mcp-token with mode 0600. A request with an Origin header gets 403, so a web page in the browser can’t talk to it. A missing or wrong token gets 401, a request body may be at most 256 KiB, and each tool call runs for at most 30 seconds. There are ten tools:

Read tierWrite tier
list_rooms, list_tasks, get_taskpropose, add_idea, claim, submit_plan, finish, unclaim, add_note

There is no accept, approve, reject, replan or reopen tool. That absence is how “humans decide” is implemented.

The part I find nicest is the plan review hook. /schwitzer plan claims a task and puts the agent into omp’s plan mode. When omp asks for the plan to be reviewed, the plugin submits the plan with submit_plan, and the task shows “Plan ready” in everyone’s list. The review stays open. If a human presses “Approve & build” while it is, the same session claims the build and starts executing the plan it just wrote. To keep anyone else from grabbing the task in that moment, every app shows Approve as busy for up to 6 seconds (HANDOFF_MS) before Pick appears. Replan sends the open review back as “Refine plan”, together with the notes written since the plan went in.

Pick is the human’s shortcut for an approved task. It claims the build and copies a prompt to the clipboard for your own agent:

Work on schwitzer task #<n> in room "<room>": <title>
schwitzer-claim: <token>
The build is claimed for you. Read the task and its approved plan with the tool schwitzer_get_task (room "<room>", task <n>), implement the plan, then call schwitzer_finish with a short note, or schwitzer_unclaim if you stop.

One more guard sits in the plugin. A room can have several projects, and an agent in one of them sometimes wants to fix something in another. An edit, write or ast_edit call into another project’s repository is held, and you choose between “Allow this edit”, “Allow edits in … for this session” and “Block: propose it to … instead”. The agent is also told about the other projects up front, with the rule to propose a change there instead of making it.

The claim marker and live sessions

The second line of that prompt is the one string that ties an agent session to a task: schwitzer-claim: followed by the 32 hex digits of the claim’s 16-byte ClaimToken. The claim tool’s answer and the Pick prompt both carry it. The app’s session scanner looks for it only in user and toolResult messages and in the plugin’s own schwitzer-task messages, and only in the main session file, not in subagents.

The scanner reads omp’s session logs every 5 seconds, keeps the sessions active within the last 10 minutes and reads each file incrementally. Only sessions whose working directory is inside a linked repository are published, at most 50 per publish and only when something changed. The server keeps them in memory, never on disk. What gets shared per session is its id, its title (up to 200 bytes), the last component of its working directory as the project, the model, start and last-active times, token counts and the linked task. What doesn’t: the full path, the prompts, the answers, the content. In the panel a session goes idle after 2 minutes. On Windows the scanner also reads the sessions inside running WSL distributions, and it never wakes a stopped one.

The two sessions in the screenshot above are synthetic session files carrying claim markers, made with the headless harness described further down.

No accounts: keys, QUIC and weida

There is nothing to sign up for. A member is the SHA-256 fingerprint of the app’s key, the MemberId. A room is joined by holding its 32-byte RoomToken, and the server knows the room only as RoomId = blake3(token), so its database doesn’t contain the secret. Two kinds of address carry everything else:

weida://sha256:<fingerprint>@host:port                  a server
schwitzer://sha256:<fingerprint>@host:port/<64 hex>     a room key

The client trusts only the server key whose fingerprint is in the address (Trust::by_address()). The server accepts any client key (ClientTrust::AnyKey) and turns its fingerprint into the member id. There is no certificate authority and no account anywhere in the chain.

The transport is weida (opens in a new window) , a QUIC framework with mutual TLS. It is alpha, and its wire protocol may still break, so Schwitzer pins it to an exact git revision. Schwitzer uses two endpoints:

  • /rpc: request and reply, postcard-encoded, at most 1 MiB, 15 seconds.
  • /events: one request and one long reply made of u32 LE length ‖ postcard(Event) frames, each at most 4 MiB.

The client holds one connection per endpoint, so that is two QUIC connections per server, and it reconnects with a backoff between 1 and 30 seconds. After a reconnect the server sends a full snapshot of every room, so a client never has to work out what it missed. Request has 20 variants and Event has 8. postcard is positional, which makes the variants append-only. A change old peers can’t read bumps PROTOCOL_VERSION, which is 5 now.

The server stores everything in SQLite in WAL mode. One mutation is one transaction, and its events go out in commit order while the store lock is still held, so every member sees changes in the same order the database made them.

Note

The server stores tasks, plans, notes and names in plain text. TLS protects them on the wire only, and there is no end-to-end encryption. Code, the MCP token and sessions outside linked repositories never reach the server.

The public server

There is a public server on my host stkn.org. Its address:

weida://sha256:b4e7d073f1bc6e0c663202055c74bfbd703aa83eadc79d58c55d5d9934b3e4cc@stkn.org:7444

To use it, open Settings, paste the address under “Create a room”, give the room a name and press “Create”. Then share the room key. The app copies it for you and says “Room key copied”, and whoever has the key can join. The app ships without a default server, so you paste the address once. The field remembers the last server after that.

The server stores your room’s tasks. If you’d rather keep them, run your own server (next section). The public one always runs the latest main and is redeployed on every push. When a change breaks the protocol, older apps are cut off until they update, and the self-update handles that.

The server enforces a few limits:

LimitValue
Requests per memberburst of 60, then 1 per second (Hello and JoinRoom are free)
Rooms per member50
Room size4 MiB
Task with its notes1 MiB
Title / body / plan / note200 B / 64 KiB / 256 KiB / 16 KiB

The server has no delete, but a full room still accepts edits that make bodies or plans shorter. It is my own host, so treat it as a place to try things.

Running your own server

There is no release binary for the server yet. The release workflow builds only the app. Ready-made Linux builds of the server will follow, and for now you build it from source. You need Rust 1.91 or newer and a C compiler, because SQLite is built from source:

git clone https://github.com/griasdi/schwitzer.git
cd schwitzer
cargo build --release -p schwitzer-server
target/release/schwitzer-server --listen 0.0.0.0:7444 \
  --public-host <name clients dial> --data-dir <dir>

The server listens on UDP, because it speaks QUIC, so the port to open in a firewall is UDP 7444, not TCP. On start it prints server url: weida://sha256:…@host:port, and that URL goes into “Create a room”. --public-host must be the name clients dial, because it ends up in the server’s certificate. Always pass --data-dir. Without it, the database and the server’s identity land in ./schwitzer-data in whatever directory you started it from. deploy/schwitzer-server.service in the repository is an example systemd unit.

The app

The app is written with iced 0.14 in the Elm style: App holds the state, Message is everything that can happen, update changes the state and view draws it. The window opens at 400×780 and follows the OS between two themes, Daylight and Night. The look takes its construction from the Steam client of 2003 to 2005, with stacked panels and raised task slabs that carry their own next-step button, but not its colours. The font is Figtree.

It updates itself. It installs the newest release its servers announce, or GitHub’s latest when no server is reachable. It replaces its own program file in place, the AppImage too, and restarts on the same page with the same room, task and drafts. The download is capped at 256 MiB and must match the asset’s sha256. Settings → Updates switches it off.

Some details about how it’s tested:

  • The workspace sets unsafe_code = "forbid".
  • crates/schwitzer-client/tests/flow.rs starts a real server on loopback UDP and takes a task from a proposal to Done. It then restarts the server on the same data directory and checks that the task survived.
  • With the remote feature, the app runs headless under iced_impeccable (opens in a new window) . schwitzer --headless --control <sock> --size 400x780 --backend tiny-skia starts it without a window, and iced-impeccable ctl <sock> drives it with tree, tap #id, type, wait-for and screenshot. Agents use this to click through their own UI changes before they hand them in, and the screenshot in this post was made the same way.

iced 0.14 exposes no accessibility tree, so accessibility rests on keyboard operation and contrast for now.

Install

Grab a package from the latest release (opens in a new window) . Version 0.1.8 has these:

PlatformFiles
Linux x86_64, aarch64.AppImage, .tar.gz
macOS (Apple silicon).tar.gz with Schwitzer.app
Windows x86_64-setup.exe (per user, no admin rights), .zip

On Linux, make the AppImage executable with chmod +x, and keep it in a folder you can write to, so the self-update can replace it. On macOS the app isn’t notarised. Move Schwitzer.app to Applications and run this once:

xattr -dr com.apple.quarantine /Applications/Schwitzer.app

On Windows, SmartScreen asks once, because the app isn’t signed.

Then create a room or join one with a key, link your repositories to the room’s projects in Settings, start omp in one of those repositories and run /schwitzer.

What it doesn’t do (yet)

Schwitzer is at 0.1.8, and some things are missing:

  • omp only. The website lists Claude Code, Codex and other harnesses as coming.
  • No server binary. The server is built from source for now. Linux builds will follow.
  • No end-to-end encryption, and no delete on the server. The server can read what’s in your rooms, and it keeps it.
  • Apple silicon only on macOS, and not notarised. Unsigned on Windows.
  • weida is alpha. The protocol can still break. The public server runs main, so an old app has to update to keep talking to it.

The list is the product. Who works on what should be readable in one glance, and every decision is a button a human presses, never a side effect of something an agent did.