# Junto — Documentation

Junto reference: the canvas document, node kinds, connecting seats, terminals and supported harnesses, agent mail, the CLI, and what the official build ships.

Canonical page: https://juntoagents.com/docs — last updated September 24, 2026.
This document describes the feature set of the current public build only; flagged-off surfaces are listed under Feature flags.

## What Junto is



Junto is a desktop app where coding agents work together. It renders a canvas: agent seats, terminals, notes, git cards, and named regions are nodes, and the lines between them are edges. You author the canvas; agents read it and act through the tools its lines grant them.

An edge is the only way an agent gains access to another seat. Drawing a line grants access, the edge's verb limits it to the ports it compiles to, and deleting the line revokes it.

Junto is free, open-source software (Apache-2.0) for macOS 13 or later on Apple silicon. This document describes the official build: agent seats, terminals, agent-to-agent mail, notes, labels, git cards, and regions. More is in the source behind compile-time flags; see [Feature flags](#feature-flags).

- The canvas is authored by the human operator in the running app. Agents never write the canvas document.
- All product state lives in one local SQLite database at `~/.junto/state/junto.db`. JSON Canvas exports and `.digest.txt` / `.svg` sidecars are outputs, never inputs.
- The canvas has a play state. Every launch comes back paused: agents keep working in their own terminals, but they cannot message each other or act through Junto until you press play. Pausing never kills a running process.
- Everything an agent can do through Junto flows through a connected edge and a compiled port — there are no ambient grants.

## How the canvas works



A Junto canvas is a [JSON Canvas 1.0](https://jsoncanvas.org) document — `nodes` and `edges` — extended with one namespaced key, `ether`, on nodes and edges. Nodes have a JSON Canvas `type` (`text`, `file`, `link`, `group`), a position and size (`x`, `y`, `width`, `height`), and an optional `ether` object that gives the node a product identity.

_A node_

```jsonc
{ "id": "n1", "type": "text", "x": 0, "y": 0, "width": 220, "height": 84,
  "text": "planner",
  "ether": {
    "entity": { "kind": "agent", "name": "local:planner" },
    "flags": ["blocker"]
  } }
```

_An edge_

```jsonc
{ "id": "e1", "fromNode": "planner", "toNode": "reviewer",
  "ether": { "verb": "messages" } }
```

Two invariants hold on every write, from the app or the CLI:

1. Graceful degradation — strip every `ether` key and the file is still a valid, readable JSON Canvas 1.0 document.
2. Mirror law — extension semantics mirror into native fields: a `blocker` flag paints the node's `color` crimson. Nothing on an edge mirrors — its live phase is recomputed at read time.

On a node, the `ether` object carries:

| `ether` field | Carries |
| --- | --- |
| `entity` | `{ kind, name? }` — the node's declared identity; `kind` is an open string, `name` is the join key for live state (an agent's seat key) |
| `flags` | Operator flags: `blocker`, `parked`, `attention` |
| `region` | Region behavior on group nodes: `hold`, `instruction`, `defaults`, `contract` |
| `terminal` | The managed-terminal binding: `bindingId`, `harness`, `sessionId`, `label`, `onDelete`, `launch` |
| `git` | Repo path for a `git` node |
| `host` | Placement stamp for executable nodes; absent means `local` |

Decode is strict: a document with unknown or excess fields fails to load rather than silently carrying them, which keeps hand-edited files honest.

One authored default matters for safety: `ether.terminal.onDelete` defaults to `detach`: deleting the card leaves the process alive.

### Reading older files



When the app decodes an older document, retired authored fields on edges (historical `ports`, `wake`, `when`, and similar keys) are read exactly once to infer the verb the edge always meant, then re-stored as `{ verb }` in the verb's semantic order. An edge whose endpoints cannot hold any verb — a pairing the grammar never admitted, a missing node — is dropped, not defaulted. The same fate meets a hand-edited `verb` the pair cannot hold.

### Driving the canvas



The canvas is a spatial surface, not a form. The core pointer grammar:

| Gesture | Action |
| --- | --- |
| scroll / middle-drag | pan the field |
| drag on empty space | rubber-band multi-select, including inside regions |
| double-click empty | add a note at the cursor |
| right-click empty / region | add-item menu, placed at the cursor (or inside the region) |
| drag a card | move a node |
| drag an edge handle | connect nodes — drop on a card |
| click an edge | open the wire's settings in the bottom bar |
| double-click a region name | rename the region |
| double-click an agent | open its managed terminal |
| ⇧ click | multi-select |
| select + right-click a target | connect all selected nodes to it; ⇧ keeps the selection for fan-out |
| ⌘K or / | command bar — jump to a node |
| ⌘Z / ⌘⇧Z | undo / redo |
| ⌫ or Del | delete the selection |
| Escape | close overlays / clear selection |
| select + corners | resize a node |
| 1–9 | focus a hotbar slot; re-tap cycles the region's members |
| ⌘1–9 | assign the selected node to a hotbar slot |
| Space or ` | cycle notifications → ready → working across seats |

A minimap jumps the camera; multi-select opens bulk actions (region, flags, delete, or one prompt to all selected agents). Selecting a node never changes what it can do — only wires do that.

### Where the document lives



The durable authority is not the file — it is a relational schema in one SQLite database (`~/.junto/state/junto.db`): a portfolio head row plus typed node and edge records, each carrying its `ether` body and z-order. A canonical serialization (stable key order, document order as z-order, 2-space indent) produces the JSON Canvas body, and its SHA-256 is the revision identity checked on every write. Every canvas mutation runs as one transaction that re-verifies every revision hash before committing — a mismatch is a hard error, not a repair attempt. Canvas names canonicalize to lowercase `[a-z0-9][a-z0-9_-]{0,63}` — input may be mixed case.

Agents and scripts read the canvas through a separate, projection-only control socket (`~/.junto/canvas/control.sock`) with two operations — `list` and `read`. It cannot change the canvas, and it never opens the database; only the app does.

### Regions



A `group` node is a region — named geography that scopes meaning without being a wiring target. Membership is derived from geometry alone: a node fully inside a region's rectangle is a member; a node inside nested regions belongs to every containing region (the stack is all containers, not a tree path); regions may overlap. Membership is never stored, so the document cannot go incoherent.

Regions carry authored behavior in `ether.region`: `hold` makes nodes travel with the region, `instruction` is operator briefing returned on `onboard`, and `defaults` stamps create-time properties. A region's rollup severity — `blocked` over `attention` over `working` over `ready` over `parked` over `idle` — is the worst of its members', computed live and never persisted.

### Play and pause



Every time Junto opens, the canvas comes back paused. Agents keep working in their own terminals, but they cannot message each other or act through Junto. To play, press the paused button at the top right of the window, or choose Play canvas from the command bar; mail then flows between agents joined by a line. Press it again to pause. Pause can also be scoped to one node or a region inside a playing canvas, and an unreadable pause state counts as paused.

Pausing stops what agents do through Junto, not the agents. While paused, a seat's reads stay open, but every call that acts fails with a typed `Paused` error, including sending, listing, reading, replying to, and reacting to mail. A paused seat can still see its state: `capabilities` and `onboard` return `paused` with a `next_step`. Pause holds no mail: a message already sent, a prompt from you, and the notice a seat gets when its lines change are typed into the seat at once, paused or not. Open agent sessions and terminals keep running; pause never kills a process.

## Nodes



Every node is a JSON Canvas node (`text`, `file`, `link`, or `group`). The `ether.entity.kind` field assigns a product identity from an open vocabulary. Well-known kinds compile to a role (actor, sink, or geography), which decides what the node can do. Unknown kinds are inert geography: they display on the canvas but hold no seat, no ports, no inbox, and no work claim.

| Kind | Role | Drawn as | What it is | Ports it can offer |
| --- | --- | --- | --- | --- |
| `agent` | actor | `text` | A Junto-managed agent seat: a harness running in the managed terminal bound to the node | `msg.list`, `msg.send`, `msg.prompt`, `seat.wait`, `seat.read` |
| `terminal` | sink | `text` | A plain managed shell; no work ports | — |
| group, `label`, `git`, or unknown kind | geography | any | Regions, notes, labels, repo cards, files, links: inert furniture | — |

Roles are derived from the kind — never authored, and a group node is geography whatever kind it carries. `agent` is the only actor kind: the only node type that holds a live process seat and carries an inbox. Geography may still display agent state; display grants no capability.

`entity.name` follows a `<host>:<profile>` convention for managed entities (for example `local:planner`). An `agent` node becomes a deliverable seat only with all three of `entity.name`, `ether.terminal.bindingId`, and `ether.terminal.harness` — without them the node exists on the canvas but no process occupies it.

### Flags



`ether.flags` marks operator intent on a node:

| Flag | Meaning |
| --- | --- |
| `blocker` | Marks that agent seat blocked; mirrors to a crimson native `color`. Only agents can be blocked. |
| `parked` | Intentionally held — out of the active flow without deleting it. |
| `attention` | Needs the operator's eyes. |

### What the palette offers



The add-item menu in the official build offers: agent, terminal, git, note, label, and region.

## Edges and verbs



An edge is `{ id, fromNode, toNode, ether: { verb } }` plus optional presentation fields (`fromSide`/`toSide`, `fromEnd`/`toEnd`, `color`, `label`) that never carry meaning. The `verb` is the one authored fact on an edge — what the relationship is. Everything else an edge does, such as which ports it grants, is compiled from the verb plus the two endpoint kinds. Compiled facets are never stored on the edge.

Direction is semantic, not spatial. `fromNode` is always the verb's source end, whichever way the edge was drawn; an edge drawn backwards is stored in the verb's own order.

A pair of node kinds with no verb refuses to connect, as do self-loops and duplicate pairs. Each edge reads as a plain sentence in the bottom command bar, painted in a fixed per-verb hue. No dash pattern, width, or arrowhead carries meaning. Clicking a drawn line selects it.

Deleting an edge revokes the capabilities it granted. There is no separate permission store: the drawing is the policy. A call outside the grant fails closed with a `ScopeError` that names the missing edge and the fix.

### Grants and attenuation



Internally, a compiled edge yields a port grant — `full` (everything the target offers), `subset` (a named list), or `empty`. Grants can only shrink: attenuation intersects a grant with a mask and never expands it, so no path downstream of an edge can gain a port the edge did not carry.

### Live phase: blocks and relates



An edge's live phase is derived, never authored or stored. Two phases exist: `relates` (the relationship is in force) and `blocks` (a stoppage is flowing through it). The phase is painted at render — on the canvas, in the wire card, in the SVG export — and nothing writes it back to the document.

In the official build, a seat enters the blocked set only through a manual `blocker` flag, and it blocks that seat alone. There is no automatic multi-hop fan-out.

## Every admitted pairing



Every pair the official build lets you connect, and what the edge compiles to. Pairs not listed here are refused at draw time.

| Source → target | Verb(s) | Plain connect | What the edge compiles to |
| --- | --- | --- | --- |
| `agent` → `agent` | `messages` | `messages` | `msg.list`, `msg.send`, `msg.prompt`, `seat.wait`, `seat.read`: mail between the two agents, plus waiting on and reading the other seat |
| `terminal`, either side | — | — | No verb reaches a terminal; it cannot be connected |

## Derived rules



The running app derives these rules from the document; none of them is stored. Roles, capability grants, blocked seats, and live edge phase are recomputed from the canvas plus live state; absent data invents nothing.

- **Roles are laws, not labels** — `actor` (only `agent`), `sink`, `scheduler`, `geography`. A kind's literal group is its role; capability code matches on the role tag, never on a kind string.
- **Blockable means actor** — only agent seats can be in the blocked set.
- **Admission is ordered and fail-closed** — a call passes these checks in order: the nodes exist, an edge connects caller to target (region co-membership grants visibility, never ports), the role pair is legal (a non-actor caller or a geography target is denied), and the requested port is inside the edge's grant and the target's offers.
- **No multi-hop** — nothing propagates past the direct edge.

The canvas's visible glance state is itself derived: occupancy per node projects to `empty`, `idle`, `working`, `attention`, `activity_blocked`, `stalled`, `parked`, or `gone` from occupant presence, harness activity, last-seen time, and operator flags — missing inputs stay honest rather than inventing a state.

## The PTY system



Terminals in Junto are managed PTY sessions: the app spawns the process, owns the pseudoterminal, and renders it in the canvas or a focus surface. An `agent` node is a managed terminal carrying a harness; a `terminal` node is a plain managed shell — seat-plane geography, no actor powers, and by construction it cannot hold a harness.

A seat is the durable binding between a canvas node and a process generation. There are two creation paths: a raw terminal (a plain shell — geography) and an agent seat (an actor: declared harness, agent key, canvas and node identity). Creating an agent seat on an occupied seat is never a silent replace — the incumbent must match the exact requested actor identity or the call fails as a seat-identity conflict.

Occupancy is independent of process liveness:

- A seat is vacant or occupied. Creating a session occupies; a stopped but still-indexed generation still occupies — a stopping incumbent is not a vacancy.
- Exited, missing, or absent means vacant. Resumable, crashed, stalled, and paused describe the occupant process, never the seat.
- Occupying a vacant seat and activating an occupied seat are different operations. Activation is validate-only: harness, agent key, node, binding, and generation must all match — an occupied generation is never repurposed for a different actor.
- Pin, unpin, and remount activate a view; they never occupy.

### The observer plane and seat state



Every live local session feeds a headless terminal observer — one virtual grid per session, batched at least 200 ms apart, with 50,000 lines of scrollback retained while a surface is attached. The observer supplies the serialized screen state a newly attached surface paints, and the signals that drive the seat state machine.

Seat state is derived from the screen, not the process: `unknown`, `idle`, `working`, `attention`, `gone`. Low-confidence `working → idle` transitions hold for three confirmations or a 700 ms cap so a repaint flicker cannot fake a state change; a replacement process generation starts at `unknown` and must earn fresh activity evidence.

### The typed-write gate



Junto's own writes into a managed terminal (its instructions and orientation notices) never type on spec. Such a write requires the seat to be `idle` and the composer proven empty — an operator's draft in progress refuses or queues it. Queued writes wait for a proven-clear composer (30 s default), one drained per idle transition. `Ctrl+C` is always allowed mid-turn. A write is acknowledged only once its text has physically left the composer, so a false “working” repaint cannot ack an unsubmitted one.

Mail is not gated. A message is typed into the recipient's input and submitted at once, whatever the seat is doing: idle, mid-turn, showing a dialog, or holding a draft. The harness queues or steers it by its own rules. The only waits are physical: another write already on the same terminal finishes first, and Grok gets its short post-launch window. Hermes cannot submit a multiline paste, so its mail arrives on one line.

An event-driven injection supervisor applies the same discipline to instruction and re-orientation writes: it reacts to seat-state transitions, screen snapshots, operator input, and work-plane proof — never wall-clock polling. After three turns without proof that the seat knows it runs inside Junto, the supervisor escalates as a canvas event rather than typing blind. Escalation is always a canvas event, never a PTY write; while the operator is present, typing, or the seat shows a modal or attention state, these writes hold.

### Named sessions and resume



A seat resumes one explicit harness session id, or it starts fresh — there is no “continue whatever was last”. Harness `--continue`, bare `--resume`, and latest-session pickers are stripped from managed launches by construction.

Harnesses split into two families. Pin harnesses (claude, grok, pi, cursor) mint the session id at spawn via a flag. Capture harnesses (codex, prime-agent, devin, agy, fx, hermes, kimi, muse, omp) print no id — Junto watches from spawn and stores the session marker it proves, then emits that harness's resume form (`codex resume <id>`, `prime-agent -r`, `devin -r`, `agy --conversation`, `fx --resume`). When a locally proven named session survives a failed generation, the pin flag is rewritten into the resume flag so the orphan is reclaimed rather than duplicated.

### Environment and doctrine injection



Every managed spawn scrubs ambient environment first: nested-session markers and harness-internal namespaces (`CLAUDE_CODE_*`, `CURSOR_*`, `PI_*`, `FX_*`, `PRIME_AGENT_INTERNAL_*`, and friends) are stripped so a Junto-spawned process is never misclassified as a nested child, and appearance or preference flags stay in the harness's own config — a managed seat behaves like the same harness started by hand.

The seat then receives its work-plane environment: `PATH` prefixed with the directory containing `junto`, `JUNTO_WORK_HOME`, `JUNTO_SOCKET` and `JUNTO_WORK_SOCKET`, `JUNTO_CLI` (the one canonical CLI path), `JUNTO_SEAT` (the agent key), and `JUNTO_NODE_REF` (`canvas:nodeId`). Secrets stay file-backed — the control token is never injected as an env value. Instructions reach the harness in one of two tiers, decided per template: Tier A in argv at spawn (a system-prompt or rules flag), or Tier B as the first typed message once the seat proves idle.

### Where terminals render



A terminal renders inline on the canvas, docked or split for multi-surface work, or in a focus surface — a centered, measure-constrained overlay for single-subject work (one agent or one terminal). Attaching a surface takes a lease: `control` is exclusive (a takeover releases the prior holder), `observe` is read-only. Terminal surfaces share one theme.

Junto signals only processes it spawned, through handles minted at spawn; there is no ambient `kill(pid)` path. An agent's own shell is not sandboxed.

## Supported harnesses



An `agent` node runs an agent CLI — a harness — inside a managed terminal. Each harness has a template declaring the spawn binary and arguments, how Junto delivers its instructions (Tier A: a system-prompt or rules flag in argv at launch; Tier B: the first typed message once the seat proves idle), the named-session contract, which settings it takes (model, effort, permission mode), and how the seat reads status.

| Harness | Command | Instructions | Session | Settings |
| --- | --- | --- | --- | --- |
| Claude Code | `claude` | A — `--append-system-prompt` | pin `--session-id`; resume `--resume <id>` | `--model`, `--effort`, `--permission-mode` |
| Codex CLI | `codex` | B — first typed message | captured thread id; resume `codex resume <id>` | `-m`, `-c model_reasoning_effort=…`, `-a` |
| Grok CLI | `grok` | A — `--rules`, `--agent` | pin `--session-id`; resume `-r <id>` | `-m`, `--reasoning-effort`, `--permission-mode` |
| Pi | `pi` | A — repeatable `--append-system-prompt` | pin `--session-id`; resume `--session <path\|id>` | `--model`, `--thinking` |
| Devin CLI | `devin` | B — first typed message | captured session lock; resume `-r <id>` | `--model`, `--permission-mode` |
| Cursor agent | `agent` | B — first typed message | pin `--new-session-id`; resume `--resume <id>` | `--model`, effort inside the model value `model[effort=high]`, `--yolo` permission mode; `--trust` prefix skips the workspace-trust modal |
| Antigravity | `agy` | A — app-owned rules dir via `--add-dir` | captured; resume `--conversation <id>` | `--model`, `--effort`, permission flag |
| fx | `fx` | B — first typed message | captured from the session index; resume `--resume <id>` | env `FX_MODEL`, `FX_PERMISSION_MODE` |
| Prime Agent | `prime-agent` | A — repeatable `--append-system-prompt` | captured via its built-in reporter; resume `-r <id>` | `--model`, `--thinking` |
| Hermes | `hermes` | B — first typed message | captured from its state database; resume `-r <id>` | `-m`, `--reasoning`, `--yolo` |
| Kimi Code | `kimi` | A — agent definition file via `--agent-file` | captured; resume `-S <id>` | `-m`, `--yolo` |
| Muse | `muse` | B — first typed message | captured; resume `muse resume <id>` | `--model`, `--reasoning-effort`, `--yolo` |
| Amp | `amp` | B — first typed message | provisioned thread; resume `amp threads continue <id>` | `-m` mode; no model or effort flag emitted |
| Oh My Pi | `omp` | A — system-prompt flag | captured; resume `--resume <id>` | `--model`, `--thinking`, `--approval-mode` |

- Three session families: **pin** harnesses take a Junto-minted id at spawn; **capture** harnesses print none, so the app watches from spawn and stores the id it proves before any resume form is ever emitted; Amp **provisions** a thread and resumes it by subcommand.
- Resume launches re-apply model, effort, and permission flags — several harnesses do not inherit them across a resume, and a seat that silently reverted its model would be lying about what is running.
- A harness whose instructions freeze at thread creation (codex) cannot receive re-injected instructions on resume; later generations get orientation notices instead.
- Settings cover what a harness is: model, effort, permission mode. Look-and-feel flags are never emitted; they stay in the harness's own config, so a managed seat behaves like the same harness started by hand.

Harnesses run with their own accounts, tools, and provider costs. The seat contract — process-bind identity, edge-granted ports, the typed-write gate — is identical across harnesses; what varies is fidelity: how completely each harness's screen states, resume shape, and settings have been probed.

Every harness above is authorable in the official build. Each remains behind its own feature flag so a single harness can be switched off without rebuilding the surface — see [Feature flags](#feature-flags). Mail reaches every harness the same way, typed into its input; what a harness does with text that arrives mid-turn is its own behavior.

## Agent mail



A `messages` edge between two agent seats lets each one mail the other through the `junto` CLI. The sender picks the shape. A notice types one short line into the recipient's input, naming the sender and the command that reads the message (`junto msg read <id>`). A prompt types the full text instead.

_From inside a seat_

```sh
junto msg send '{"target":"reviewer","text":"auth refactor is up, please review src/auth"}'
junto msg send --prompt '{"target":"reviewer","text":"Review src/auth and reply by mail with any blocking issue."}'
junto msg list
```

Either way, the text is typed into the recipient's input and submitted at once, whatever the agent is doing; its harness queues or steers it. Nothing is refused and nothing needs a retry. A recipient whose seat is not running, or has no seat yet, gets the mail when its terminal comes up. The send answers `delivered` (typed into the seat) or `waiting` (the seat is not up yet).

Mail is durable: it stays in the recipient's mailbox until read. A seat's own `msg list` marks its mail read, `msg reply` answers, `msg react` acknowledges without a reply, and `msg sent` shows whether each message was delivered and read. While the canvas or seat is paused, an agent cannot send or read mail, but mail already sent still reaches it; see [Play and pause](#canvas-play).

The same edge grants `seat wait` (block until the other seat is `idle`, `working`, or needs attention) and `seat read` (read the other seat's terminal).

## CLI and the work socket



Agents and scripts reach the running app through the `junto` CLI, which talks to the app's local control socket (`~/.junto/work/control.sock`, bearer token in `~/.junto/work/token`). The CLI never opens the SQLite database; only the app does.

Identity is process-bind: the CLI must run as a descendant of a live, Junto-registered agent process. The app admits callers by their Unix peer pid and walks ancestors — a CLI may be a grandchild — and start-key epochs reject pid reuse. The seat's principal (agent key, node, canvas) is resolved server-side; the wire carries no identity claim at all, and the token is a file-backed (0600) transport credential, never an env var or an argument.

The capability contract an admitted agent sees comes from its edges. `junto capabilities` and `junto onboard` report the live edge contract for the calling principal; calls outside it fail closed with a scope error.

Every work command takes one JSON argument (inline, `@file`, or stdin) and returns JSON. The CLI is self-describing: `schema show <command>` prints the input shape, `examples show <command>` prints executable examples, and `junto docs` prints the generated reference — the node catalog, per-kind depth, concepts, and this contract — straight from the physics registry, so it cannot drift from the program. Batch-capable commands accept a JSON array plus `--concurrency <n>`.

| Command | What it does |
| --- | --- |
| `ping`, `doctor` | Liveness and readiness of the control surface (socket present, token perms, protocol match) |
| `onboard` | Always-first orientation: seat, region briefing, connected targets with grants, co-members |
| `capabilities` | The live edge contract for the calling seat |
| `rulings` | Operator-pinned precedent over the seat's region stack |
| `preamble` | A short-lived thought bubble above the agent node |
| `msg send` (`--prompt`) / `list` / `read` / `reply` / `react` / `sent` | Mail over a `messages` edge: a notice, or with `--prompt` the full text; own `list` marks mail read |
| `seat wait` / `seat read` | Wait on a connected seat's state; read its terminal |
| `schema show` / `examples show` | Input shape and executable examples for a command |
| `overseer` | Canvas and node commands for a seat you switched to overseer |
| `docs` | `docs`, `docs list`, `docs doctrine`, `docs nodes`, `docs node <kind>`, `docs concepts`, `docs contract`, `docs show <topic>` |

Errors are a closed family carrying `type`, `retryable`, and `next_step`. The agent-facing members: `ScopeError` (not connected; the fix is an edge), `InputError` (payload failed decode; `schema show` prints the shape), `RuntimeDown` / `Paused` (wait and re-onboard), `Blocked` (this seat is blocked; stop and wait), plus `UnknownTarget`, `StaleNodeRef`, `AuthError`, `ProtocolError`, `InternalError` for the plumbing. Retry only when `retryable` is true, at most twice. While a seat is blocked, `ping`, `doctor`, `capabilities`, `onboard`, and `rulings` stay open; mailbox and every mutating op refuse.

In a source checkout, `bun run canvas:ls` lists canvases, `bun run digest <canvas>` prints a text projection of one, and `bun run render` writes an SVG image of the canvas. They read the running app and never write the canvas.

## Feature flags: what the official build ships



Product surfaces are compile-time gated. Official builds use the `ship` profile; source builds can turn on the rest for development. This document covers what `ship` enables.

| Surface | Ship profile |
| --- | --- |
| Canvas, nodes, edges, verbs, agent seats, terminals, agent mail (`messages` edges, `msg.*`), git, note, label, region, the overseer toggle, CLI | On |
| Harnesses: claude, codex, grok, pi, devin, cursor, agy, fx, prime-agent, hermes, kimi, muse, amp, omp | On |
| Tasks and task-scoped content (`tasks.*`, `content.*`) | Off |
| Reviews edges and verdicts (`verdict.post`) | Off |
| Requests, artifacts, board, pad, sheet | Off |
| Pages and browser control | Off |
| Schedulers — `cron` / `timer` / `relay` / `watcher` kinds and their verbs (`wakes`, `enqueues`, `flags`, `announces`, `chains`, `fires`) | Off |
| Fleet UI and Remote hosts | Off |
| Hermes integration — profile enumeration, snapshots, and the attached agent chat channel | Off |
| Live voice overseer, usage surfaces, help map, audio, developer diagnostics, per-harness settings UI | Off |
| Residual wire ops from flagged-off features (e.g. `relay.trigger`) | Refuse at call time — “disabled in this Junto build” |

A node or edge kind that is compiled out still decodes if it exists in a document — durability outlives features — but it has no authoring surface and cannot be created in this build.
