Skip to content

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.

Dispatch works off two metadata fields you set on a page:

  • metadata.fs.path: a local directory. Required for code.task.create, code.claude.prefill, and code.claude.run.
  • metadata.git.repo: a remote repository. Required for code.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 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.

  1. code.task.create: file the work

    code.task.create({page, title, description?}) runs bd create (fixed argv, silent) in the page’s linked directory and adds a linked [ ] <id> - <title> bullet on the page with props.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.

  2. code.claude.prefill: type it, you press Enter

    code.claude.prefill({page, task, interactive?}) opens a terminal node on the linked directory and types claude -p '<task>' (or plain claude '<task>' with interactive: 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.

  3. code.claude.run: headless, always gated

    code.claude.run({page, task}) spawns claude -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 requires SUBSPACE_OS_EXEC=1 (or PROVIDERS=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_BIN overrides the CLI binary for testing.

  4. code.devin.run: a hosted session against a repo

    code.devin.run({page, task}) targets metadata.git.repo instead 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.

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 /pty relay. 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.

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.

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.

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 the code/ namespace, versioned and linkable like anything else.
  • A live plugin: when the coding agent produced a plugin directory, the plugins.install tool is the handoff from raw output to something Subspace actually runs. See below.

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.

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.create writes into.

SUBSPACE_BD_BIN overrides the bd binary for tests and fixtures, the same pattern as SUBSPACE_CLAUDE_BIN for Claude.