Wire ljos-mcp

One entry for any runner. The seat names itself after the client that connects, and the shell finds the same name; nothing is set per runner. ljos and ljos-mcp on PATH is the whole install. Shipped shapes cover Claude Code and Codex, plus opencode and agy.

One entry, any runner

$ ljos onboard
{
  "mcpServers": {
    "ljos": { "type": "stdio", "command": "/home/you/.cargo/bin/ljos-mcp", "args": [], "env": {} }
  }
}
$ ljos protocol > instructions.md

Paste the entry where the runner keeps its MCP servers. Every runner that speaks MCP over stdio takes this shape, and there is nothing to fill in: the env is empty on purpose. Put the protocol where the agent reads its instructions; the server serves the same text at ljos://protocol and names it in its instructions, so a runner that loads neither skills nor resources still reads it first.

Who is sitting

The seat does not know the runners. It knows one thing every runner does: it connects, and says its name. ljos-mcp takes that name at initialize as the seat and leaves a record under the runtime directory keyed by the runner's process. ljos in a shell the runner opens walks its own process tree to that record, or, when the runner has no MCP, to the first ancestor that is not a shell, and names the same seat. A runner's tools and its verbs are one seat with nothing set; a new runner needs no table anywhere.

$ ljos seat
seat	acme-cli
holder	acme-cli-39u
source	the server the runner opened, process 4242

Two names come from that. The seat is the program: memory, ballots and trust rows accrue to it across every conversation of that runner, which is what calibration needs. The holder is the seat tagged with the conversation's process, and it is what a claim is held under; occupancy is holder:issue, so two conversations of one runner hold two tickets and a second sitting does not release the first. With several agents open in one terminal workspace, every pane is one process, so every conversation is one holder and the same program stays one seat. A server and its shells share the runner's process and not its environment, which is why the name is read from the process and not from a session variable: a line editor's session id in the shell had made one conversation two holders, and every sitting met held by another seat. ljos doctor carries the same row; LJOS_SEAT overrides the seat when a person wants a name.

A hook, when the runner has one

If the runner has a command hook on prompts, pipe the prompt to ljos hook. The memories it activates come back as context, preferences first, each once per session, with once a session how many claims and habits are due. Do not add it on every tool call to search the pack: a turn issues many tool calls and one prompt. On a tool call the hook applies the TCB (ljos-policyd) and then the pack's rules, and a deny blocks.

The seat guards its own law

Before any rule, the hook refuses a command or a file edit that writes the seat's binaries (ljos, ljos-mcp, ljos-policyd), its config, the approval store, or a runner's hook registration (~/.codex/hooks.json, ~/.gemini/config/hooks.json, ~/.claude/settings.json, ~/.grok/hooks/ljos.json); reading them is fine. An agent the gate refused had replaced ~/.local/bin/ljos with a script that allowed everything, which also broke every other runner's prompt hook. ljos doctor fails its seat binary row when the ljos the hooks run is not the binary running the doctor.

An ask on a runner that cannot ask prints a request id. The person grants that one command, once, in that directory and conversation, with ljos approve ID in a terminal of their own; approve refuses under an agent runner or without a terminal, so an agent cannot grant itself consent.

A runner with a register command

A runner that registers servers by a command of its own, or reads them from a config file, can be described once in ~/.config/ljos/harnesses.toml and onboarded in one verb. ljos onboard --example prints the shape; --dry-run reports and writes nothing.

[[harness]]
name = "runner-a"
register = ["runner-a", "mcp", "add", "-s", "user", "ljos", "--", "{server}"]
registered = ["runner-a", "mcp", "get", "ljos"]
skills = "~/.runner-a/skills"
hooks = "~/.runner-a/settings.json"

[[harness]]
name = "runner-b"
config = "~/.runner-b/config.toml"
marker = "[mcp_servers.ljos]"
snippet = "\n[mcp_servers.ljos]\ncommand = \"{server}\"\nargs = []\n"
skills = "~/.runner-b/skills"
$ ljos onboard --harness runner-a
ok	host key	/home/you/.config/deedar/host.key exists
ok	runner-a mcp	ran runner-a mcp add -s user ljos -- /home/you/.cargo/bin/ljos-mcp
ok	hook	memory hook: add it on UserPromptSubmit in /home/you/.runner-a/settings.json
ok	skill	wrote /home/you/.runner-a/skills/ljos/SKILL.md

{server} is the path to ljos-mcp. Nothing about the seat's name goes in the table.

A runner whose hooks file maps a hook name to its events, and whose payload does not name the event, takes hooks_named: the seat writes its hooks under that name, each command told its event with ljos hook --event. Antigravity's agy is one: ljos onboard --harness antigravity registers the server in ~/.gemini/config/mcp_config.json, writes the skill under ~/.gemini/config/skills, and puts the gate on run_command, the notes on each model call and the stop audit in ~/.gemini/config/hooks.json. That runner asks the person on an ask itself.

[[harness]]
name = "antigravity"
config_json = "~/.gemini/config/mcp_config.json"
json_pointer = "/mcpServers/ljos"
json_entry = '{"command": "{server}", "args": [], "env": {"LJOS_SEAT": "{name}"}}'
skills = "~/.gemini/config/skills"
hooks = "~/.gemini/config/hooks.json"
hooks_named = "ljos"

The two verbs an agent runs

ljos sitting <issue>                      # doctor, cards, due, island, recall, timeline, claim
ljos finish <issue> --lesson "..." [--outcome <option>]   # remember, fire, complete, learn

One verb opens a sitting in the protocol's order and stops at the first store that does not answer. One closes it. The loop that makes the seat a memory runs every time. It does not wait for someone to remember the steps.

Cite a deed on a ticket

ljos evidence deed-…
ljos deed <id> --add deed-…
ljos recall <id>

Citing a deed names it; the bytes stay in deedar. The ticket stays open until vissue closes it.

Share the seat across machines

ljos sync --key                     # this machine's age public key
cat .ljos/sync.toml                 # in the tracker repository
name = "work"
recipients = ["age1…", "age1…"]     # the machines allowed to read it
ljos sync                           # take the others' logs, write this one's

Memory travels with the tracker repository. Each machine writes one sealed log under .ljos/atoms/, and only the named recipients can open it. The sitting takes the other machines' logs before it walks the island, and finish writes this machine's. A lesson takes the scope of the repository its issue lives in; a [projects] table in sync.toml sends one project's lessons to another scope.

Merge the tracker by heading

vissue merge-driver --install       # in each clone of the tracker
git add .gitattributes && git commit -m "chore: merge issues.org by heading"

Two hosts that edit one issues.org merge by issue, not by line: notes and ballots from both sides are kept, and a state both sides moved takes the later logged move. A field both sides changed some other way keeps yours and is written on the issue as a merge-conflict note. ljos doctor fails the tracker row in a clone that lacks the driver.

Let Jev judge, where a machine allows it

~/.config/ljos/jev.toml
enabled = true
key_cmd = ["pass", "show", "api/typesafe/jev"]

Jev is TypeSafe's decision model: it answers yes-or-no, choice and score questions with probabilities, and writes no text. With the file above, three judgments go to it. The prompt hook asks which of up to ten claims bear on the prompt and whether the prompt corrects the agent or puts a choice; the local cross-encoder is then never loaded. ljos vote --as NAME --jev and ljos panel --jev ask for persona ballots, cast one when Jev is sure (confidence 0.8 or above) and write a brief for a subagent when it is not; a panel casts only when every seat is sure and all agree, because personas answered by one model are correlated voters. On Stop and SubagentStop it reads the turn once and holds an agent that claims done beside a red test run, or that puts asked work off. A prompt costs about $0.00004 and a stop $0.00003; monthly_usd caps the month, identical requests are answered from a local cache, and ljos doctor prints the month's calls and spend. With no file, no key or no answer inside two seconds, the local path answers.

Fast judges first, personas when it is close

Jev answers in milliseconds with a calibrated probability and no reasoning, and so does a chat model such as openai/gpt-oss-120b on an OpenAI-compatible endpoint. jev.toml names them as [judges.NAME], [route] says which judges answer each decision, and judges on one decision are pooled: probabilities by the weighted mean of their log-odds, choices by the geometric mean of their distributions.

A ballot they leave open goes to the persona whose ballot it is, when the persona has a runner: Claude Code, Codex, Grok Build, omp, hermes or Antigravity's agy, on whatever model it runs.

$ ljos persona buildengineer --anchor 0.25 --view "Runs production pipelines." --runner grok
$ ljos vote surf-ab12 --as buildengineer --jev
buildengineer: Jev leaned A at confidence 0.61, under the 0.80 cut; not cast.
  buildengineer reasons in its own session in herdr pane w1:p2
$ ljos ask buildengineer "why A over B?"

The runner works in the persona's home as the seat named after the persona, so its memories, ballots and trust rows are the persona's. It runs in a pane the person can watch and talk in, opened in the first tool that answers. A [[tool]] table in harnesses.toml comes first, then a herdr workspace when herdr's server is up, then a window of the tmux session ljos-personas. ljos doctor names the tool. A runner that exits non-zero is resumed in its pane, at most three times a minute. It writes its reasoning with vissue note and casts ljos vote --as NAME; consensus weighs the ballot by the persona's trust row, and learn moves it. The runner's [[harness]] table says how it resumes the latest session of a directory (resume = ["grok", "--continue"]), so the next hand-off continues the same conversation, and an open pane takes the next task in place.

Persona panes in any tool

[[tool]]
name = "mytool"
detect = ["mytool", "--version"]
open = [["mytool", "new-pane", "--cwd", "{home}", "--name", "{label}", "--", "sh", "{script}"]]
prompt = [["mytool", "send", "--pane", "{label}", "{line}"], ["mytool", "send-key", "--pane", "{label}", "Enter"]]
alive = ["mytool", "has-pane", "{label}"]

Every verb of a tool but pane_pointer is an argv or a list of them, so a tool the seat has not met is one more table. A table named like a shipped shape (herdr, tmux) replaces it. A tool that refuses is named in the line ljos ask prints, with its own words, and the next one is tried. LJOS_PANE_TOOL narrows the choice to the tool it names.

Personas as subagents

$ ljos agents --harness grok
ok	agents grok	wrote 5 and removed 0 agent definitions in /home/you/.grok/agents

Every persona becomes an agent definition, ljos-NAME.md, that Grok Build spawns by name (spawn_subagent with subagent_type ljos-reviewer) and Claude Code reads from ~/.claude/agents. Cursor's copies go to ~/.cursor/agents. The persona runs ljos brief, casts one ballot before reading the others, notes why and stops. It has no edit tool under Grok; elsewhere its body tells it not to edit. onboard writes them, and a persona written later refreshes each agents directory harnesses.toml names, if it exists. A panel the seat opens itself runs its members on the seat's own runner when that runner's shape gives a headless argv.

When a command fails

A failed tool brings back up to three standing claims, on a runner that reports one (Grok Build's PostToolUseFailure). A claim needs two or more content words in common with the command and its error. A compaction takes every memory handed over so far off the session's record, so a later prompt can bring it back. Up to eight of them fire together, if two or more were handed over. The next delivery names the issue the conversation still holds: Claude Code hears it as the session resumes, Grok Build and Cursor with the next tool result.

The status bar

[ui.status_line]                    # ~/.grok/config.toml
type = "command"
command = "ljos statusline"         # ljos grok · surf-ab12 · 3 due

Claude Code takes the same command as its statusLine. A session's line is cached for fifteen seconds.

Push where it is safe, cite where it is not

An asked git push is gated by where it goes, and the forge says whose a remote is: for GitHub, gh api gives the person's push permission, whether the owner is their own account, the collaborators and the releases. The answer is remembered in the pack as a standing claim on repo:OWNER/REPO, so later pushes read the pack, the hook raises it when a command names the repository, and the review clock brings it back. A branch push to the person's own repository with no other collaborator and no release runs. One with releases, other collaborators or an organisation's name runs when the line cites the decision behind it, and the pass is noted on that issue:

LJOS_CITE=surf-ab12 git push origin main     # surf-ab12 settles, or closed as a decision
LJOS_CITE=d-7f3a9c git push origin main      # or a current deed

A remote the person cannot push to, tags, a mirror or a force stay the person's to run. A wrong claim is forgotten with ljos forget ID, and the next push asks the forge again.