Reference

Junto docs

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

Last updated September 24, 2026. Plain text for machines: /docs.md

Start here

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.

  • 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.

The document

How the canvas works

A Junto canvas is a JSON Canvas 1.0 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
{ "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
{ "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` fieldCarries
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)
flagsOperator flags: blocker, parked, attention
regionRegion behavior on group nodes: hold, instruction, defaults, contract
terminalThe managed-terminal binding: bindingId, harness, sessionId, label, onDelete, launch
gitRepo path for a git node
hostPlacement 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:

GestureAction
scroll / middle-dragpan the field
drag on empty spacerubber-band multi-select, including inside regions
double-click emptyadd a note at the cursor
right-click empty / regionadd-item menu, placed at the cursor (or inside the region)
drag a cardmove a node
drag an edge handleconnect nodes — drop on a card
click an edgeopen the wire's settings in the bottom bar
double-click a region namerename the region
double-click an agentopen its managed terminal
⇧ clickmulti-select
select + right-click a targetconnect all selected nodes to it; ⇧ keeps the selection for fan-out
⌘K or /command bar — jump to a node
⌘Z / ⌘⇧Zundo / redo
⌫ or Deldelete the selection
Escapeclose overlays / clear selection
select + cornersresize a node
1–9focus a hotbar slot; re-tap cycles the region's members
⌘1–9assign 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.

Seats and furniture

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.

KindRoleDrawn asWhat it isPorts it can offer
agentactortextA Junto-managed agent seat: a harness running in the managed terminal bound to the nodemsg.list, msg.send, msg.prompt, seat.wait, seat.read
terminalsinktextA plain managed shell; no work ports—
group, label, git, or unknown kindgeographyanyRegions, 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:

FlagMeaning
blockerMarks that agent seat blocked; mirrors to a crimson native color. Only agents can be blocked.
parkedIntentionally held — out of the active flow without deleting it.
attentionNeeds the operator's eyes.

What the palette offers

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

Connections with meaning

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.

The grammar

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 → targetVerb(s)Plain connectWhat the edge compiles to
agent → agentmessagesmessagesmsg.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, never stored

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.

Seats and processes

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.

Bring your own agents

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.

HarnessCommandInstructionsSessionSettings
Claude CodeclaudeA — --append-system-promptpin --session-id; resume --resume <id>--model, --effort, --permission-mode
Codex CLIcodexB — first typed messagecaptured thread id; resume codex resume <id>-m, -c model_reasoning_effort=…, -a
Grok CLIgrokA — --rules, --agentpin --session-id; resume -r <id>-m, --reasoning-effort, --permission-mode
PipiA — repeatable --append-system-promptpin --session-id; resume --session <path|id>--model, --thinking
Devin CLIdevinB — first typed messagecaptured session lock; resume -r <id>--model, --permission-mode
Cursor agentagentB — first typed messagepin --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
AntigravityagyA — app-owned rules dir via --add-dircaptured; resume --conversation <id>--model, --effort, permission flag
fxfxB — first typed messagecaptured from the session index; resume --resume <id>env FX_MODEL, FX_PERMISSION_MODE
Prime Agentprime-agentA — repeatable --append-system-promptcaptured via its built-in reporter; resume -r <id>--model, --thinking
HermeshermesB — first typed messagecaptured from its state database; resume -r <id>-m, --reasoning, --yolo
Kimi CodekimiA — agent definition file via --agent-filecaptured; resume -S <id>-m, --yolo
MusemuseB — first typed messagecaptured; resume muse resume <id>--model, --reasoning-effort, --yolo
AmpampB — first typed messageprovisioned thread; resume amp threads continue <id>-m mode; no model or effort flag emitted
Oh My PiompA — system-prompt flagcaptured; 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.

Agents talking

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
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.

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).

The agent surface

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>.

CommandWhat it does
ping, doctorLiveness and readiness of the control surface (socket present, token perms, protocol match)
onboardAlways-first orientation: seat, region briefing, connected targets with grants, co-members
capabilitiesThe live edge contract for the calling seat
rulingsOperator-pinned precedent over the seat's region stack
preambleA short-lived thought bubble above the agent node
msg send (--prompt) / list / read / reply / react / sentMail over a messages edge: a notice, or with --prompt the full text; own list marks mail read
seat wait / seat readWait on a connected seat's state; read its terminal
schema show / examples showInput shape and executable examples for a command
overseerCanvas and node commands for a seat you switched to overseer
docsdocs, 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.

Build profile

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.

SurfaceShip profile
Canvas, nodes, edges, verbs, agent seats, terminals, agent mail (messages edges, msg.*), git, note, label, region, the overseer toggle, CLIOn
Harnesses: claude, codex, grok, pi, devin, cursor, agy, fx, prime-agent, hermes, kimi, muse, amp, ompOn
Tasks and task-scoped content (tasks.*, content.*)Off
Reviews edges and verdicts (verdict.post)Off
Requests, artifacts, board, pad, sheetOff
Pages and browser controlOff
Schedulers — cron / timer / relay / watcher kinds and their verbs (wakes, enqueues, flags, announces, chains, fires)Off
Fleet UI and Remote hostsOff
Hermes integration — profile enumeration, snapshots, and the attached agent chat channelOff
Live voice overseer, usage surfaces, help map, audio, developer diagnostics, per-harness settings UIOff
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.

Found an error? Open an issue — the docs describe the current public build.