Coding-agent dispatch
Coding-agent dispatch lets a bullet or an agent hand real coding work to a coding agent: Claude Code running in a terminal on a directory you’ve linked to a page, or a Devin session running against a repository you’ve linked. You stay on the same rising scale of trust Subspace uses everywhere else, from “type the command for me but let me press Enter” up to “run it headless and file the result,” and every step is a normal tool call with the same gates, claims, and run-tree visibility as anything else an agent does.
Linking a page to code
Section titled “Linking a page to code”Dispatch works off two metadata fields you set on a page:
metadata.fs.path: a local directory. Required forcode.task.create,code.claude.prefill, andcode.claude.run.metadata.git.repo: a remote repository. Required forcode.devin.run.
A page with either field becomes a dispatch target: any bullet on it, or any agent working on it, can reach for the code.* tools below. All code.* tools require an admin run owner, since dispatch reaches the host shell.
The dispatch ladder
Section titled “The dispatch ladder”The four code.* tools trade autonomy for approval weight, so you can pick exactly how much you trust a given task before it touches your machine.
-
code.task.create: file the work
code.task.create({page, title, description?})runsbd create(fixed argv, silent) in the page’s linked directory and adds a linked[ ] <id> - <title>bullet on the page withprops.code = {bd: <id>}. This just files a tracked issue, it doesn’t touch any code. Needs approval by default (policy-relaxable), and is claim-guarded per call so a workflow replay can’t file the same issue twice. -
code.claude.prefill: type it, you press Enter
code.claude.prefill({page, task, interactive?})opens a terminal node on the linked directory and typesclaude -p '<task>'(or plainclaude '<task>'withinteractive: true) into the PTY without a trailing newline. Nothing executes until you read the prefilled command and press Enter yourself. This is the tool to use when you want a coding agent’s help but want your own eyes on the exact invocation first. Needs approval by default. -
code.claude.run: headless, always gated
code.claude.run({page, task})spawnsclaude -p <task>directly as the terminal’s PTY process, argv only, no shell, so the task text never crosses a shell-quoting surface. This is always gated, the same floor as raw OS execution, and additionally requiresSUBSPACE_OS_EXEC=1(orPROVIDERS=fake) to be enabled at all. The process’s exit is completion: the session records its exit code, an outbox event fires, and a task card lands (“Coding run finished” or “FAILED”, with a scrollback tail) plus a workflow hook resume for any run waiting on it.SUBSPACE_CLAUDE_BINoverrides the CLI binary for testing. -
code.devin.run: a hosted session against a repo
code.devin.run({page, task})targetsmetadata.git.repoinstead of a local directory. It acquires network access and the Devin API key from the capability broker (ops.secrets['devin.api_key']), starts a session against Devin’s HTTPS sessions API, and immediately files the session id and URL as an output node on the page. Needs approval, and the credential itself never enters the page, the prompt, session state, or logs, only the broker sees it.
How a terminal gets spawned
Section titled “How a terminal gets spawned”code.claude.prefill and code.claude.run both create an ordinary ops.pty_sessions row, the same kind used by any terminal element: it can be attached, scrolled back through, and respawned like any other session. If the invoking bullet is on the same page as the run, the new terminal node parents under it, so the terminal shows up right next to the work that spawned it.
The actual spawn goes through a server-initiated seam, PtyHost.spawn(nodeId, cwd, {argv?, input?, notifyExit?, host?}), that can target two hosts:
- Server host: the linked directory lives on the machine running the Subspace server, so it spawns there directly.
- Shell host: the linked directory only exists on your desktop machine. The authenticated Electron-main host control socket accepts the same argv/input spawn request and binds the session back over the
/ptyrelay. If the desktop app isn’t open, dispatch tells you to open it rather than silently failing or spawning in the wrong place. A shell-linked terminal node can reach the UI before its source connects, without accidentally spawning in the server’s own home directory. If the relay drops, any headless job that was claimed on it dies too, on purpose, since a headless run with no completion channel back to Subspace can’t be trusted to still be tracked.
Devin’s durable poll loop
Section titled “Devin’s durable poll loop”A Devin session doesn’t complete synchronously, so completion is a durable, restart-safe poll: a leased sweep (every 2 minutes by default, SUBSPACE_DEVIN_SWEEP_MS) checks status and persists it in ops.devin_sessions after every attempt. Finished, blocked, expired, and error states are each handled exactly once. On completion, the resulting PR URL (or the blocked detail, if Devin got stuck) files as an output node and a task card, and a devin-session-completed event resumes any workflow hook waiting on that session. Because state is persisted after every poll rather than held in memory, a server restart mid-session picks the poll back up rather than losing track of it.
The coding-task workflow
Section titled “The coding-task workflow”coding-task is a registered durable workflow, also seeded as the agents/coding-task agent definition, that composes the whole ladder into one run: code.task.create → prefill, run, or Devin → a single reusable code-run:<id> hook that races the workflow against persisted status polling (so a fast exit can’t be missed between the spawn and the hook being registered) → an optional plugin review and install → code.result.file. Every call inside it is a normal run-tree tool row with the same gates and claims as calling the tools directly, so a coding-task run reads in the run tree exactly like a manual dispatch would.
Outputs: plugins, code pages, or PRs
Section titled “Outputs: plugins, code pages, or PRs”A coding-agent run’s output lands in one of three places, depending on what the task was:
- A pull request: for Devin sessions and any workflow ending in a PR, the URL files as an output node and a task card, ready to review outside Subspace.
- A
code/knowledge base page: smaller results (a script, a snippet, a generated config) can file directly as a page in thecode/namespace, versioned and linkable like anything else. - A live plugin: when the coding agent produced a plugin directory, the
plugins.installtool is the handoff from raw output to something Subspace actually runs. See below.
From coding-agent output to a live plugin
Section titled “From coding-agent output to a live plugin”plugins.inspect({path}) parses a local plugin directory and returns the same review a human would want: name, version, requested capabilities and dependencies, and every artifact sorted into a trust lane, sandboxed, client-code, or full-trust.
plugins.install({path}) is always gated, no policy can relax it. Its approval card renders that same manifest review directly: plugin name and version, each trust lane’s contents, requested capabilities, and dependencies, so you’re approving exactly what the coding agent built, not a black box. The card’s preview is bound to a hash of the plugin’s source; if a single byte changes between approval and install (for instance, the coding agent kept iterating after you clicked review), the install fails and asks for a fresh review rather than silently installing a different plugin than the one you approved. This is the full handoff path: a coding agent produces a plugin, plugins.install shows you exactly what it does before it’s live, and once approved it runs through the same immutable install pipeline as any other plugin. See plugins overview for what a plugin can contain and how the trust lanes are enforced at runtime.
The beads issue ledger sync
Section titled “The beads issue ledger sync”If a linked directory contains a .beads workspace, Subspace keeps it and the page’s bullet list in sync automatically, on a five-minute interval plus once at boot:
- New open issues in the ledger gain a
[ ]bullet row on the page (capped at 50 new rows per sweep, so a large pre-existing backlog doesn’t flood a page the first time it’s linked). - Issues closed in the ledger get their corresponding row flipped to
[x]. - Rows you check off yourself in the page get closed in the ledger (
bd close). - A conflict, the row and the ledger disagreeing, resolves toward the ledger, since that’s the durable source of truth
code.task.createwrites into.
SUBSPACE_BD_BIN overrides the bd binary for tests and fixtures, the same pattern as SUBSPACE_CLAUDE_BIN for Claude.