:description: How to wire ljos into a runner, write to another seat, sit an issue, and check the seat. :cover: https://leidarljos.github.io/ljos/_static/og.png Wire the seat into an agent runner ================================== One entry, for any runner that speaks the Model Context Protocol (MCP) over stdio: .. code:: console $ ljos onboard {"mcpServers": {"ljos": {"type": "stdio", "command": "ljos-mcp", "args": [], "env": {}}}} Paste it where the runner keeps its MCP servers. The ``env`` is empty on purpose. The server names the seat after the client that connects, so a runner that calls itself ``acme-cli`` is the seat ``acme-cli`` and a runner that calls itself ``brio`` is ``brio``, with nothing set per runner. A shell the runner opens finds the same seat through ``LJOS_SEAT``, or through the markers the runner leaves in the environment. A derived name is a seat. Each conversation keeps its claims as the seat tagged with its session, so two conversations of one runner hold two tickets. .. code:: console $ ljos seat seat acme-cli holder acme-cli-39u source the client name the MCP server received The protocol goes where the agent reads its instructions: ``ljos protocol`` prints it, 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. Each tool description opens with when to call it. 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: .. code:: toml [[harness]] name = "runner-with-a-command" register = ["runner", "mcp", "add", "-s", "user", "ljos", "--", "{server}"] registered = ["runner", "mcp", "get", "ljos"] skills = "~/.runner/skills" [[harness]] name = "runner-with-a-config-file" config = "~/.other/config.toml" marker = "[mcp_servers.ljos]" snippet = "\n[mcp_servers.ljos]\ncommand = \"{server}\"\nargs = []\n" skills = "~/.other/skills" .. code:: console $ ljos onboard --harness runner-with-a-command ok runner-with-a-command mcp ran runner mcp add -s user ljos -- ljos-mcp ok skill wrote /home/you/.runner/skills/ljos/SKILL.md ``--dry-run`` reports what would be written. Codex runs a hook only after you trust it. After ``ljos onboard --harness codex``, open ``/hooks`` and trust each ljos hook before it runs. Trust it again after an upgrade rewrites one. Until then the hook does not run, and policyd sees none of Codex's commands. ``ljos doctor`` reports an untrusted hook when the trust file has no ``trusted_hash``, or is older than the hooks file. The example carries the runners this seat has carried through one piece of work, each in the shape it takes the server: a runner with an ``mcp add`` of its own by that command; ``grok`` and a runner with a TOML config by a table appended to it; ``opencode`` and ``omp`` by a JSON pointer set in their MCP file; ``hermes`` by its ``mcp add`` with the tool question answered. The same verb takes ``cursor`` (the IDE and the ``agent`` CLI share ``~/.cursor/mcp.json``), ``windsurf``, ``zed``, ``vscode``, ``claude-desktop``, ``gemini``, ``amazonq`` and ``kiro``. Copy the tables for the runners on the machine into ``harnesses.toml``, then one verb each: .. code:: console $ for h in opencode hermes omp grok; do ljos onboard --harness $h; done $ ljos doctor --all | grep runner ok runner mcp opencode: ljos registered ok runner skill opencode: /home/you/.config/opencode/skills/ljos/SKILL.md ok runner mcp hermes: ljos registered ok runner skill hermes: /home/you/.hermes/skills/ljos/SKILL.md ... A runner with a hooks file (``hooks`` in its table) takes the memory hook too; for the others the skill and the ``ljos://protocol`` resource carry what the seat knows, and the sitting's island brings the memories in. Seat a shell-only agent ======================= Grok Bot runs shell commands on a Linux box. It is not an MCP runner and it has no command hook, so the env file names its seat. .. code:: console $ ljos onboard --harness grokbot --skills ~/.grokbot/skills ok skill wrote /home/you/.grokbot/skills/ljos/SKILL.md ok env wrote /home/you/.config/ljos/grokbot.env; source /home/you/.config/ljos/grokbot.env $ source ~/.config/ljos/grokbot.env $ echo "$prompt" | ljos hook --prompt $ ljos policy --fail-on-deny -- git push --force ``--skills DIR`` writes the protocol there. ``ljos onboard --harness shell --skills DIR`` is the same shape under the name ``shell``. The env file sets ``LJOS_SEAT`` to the runner's name. Nothing is registered as an MCP server or a hook. ``--fail-on-deny`` exits 1 when the answer is a deny. Without the flag, ``ljos hook`` and ``ljos policy`` still exit 0, so a runner that reads the JSON sees no change. An ``ask`` still exits 0; the text says ``ask``. Add a seat, and let it close tickets ==================================== Coming in 0.35.0. The tracker's ``vissue.toml`` lists the seats. ``ljos seat add NAME`` writes the row, so nobody edits the file by hand. .. code:: console $ ljos seat add carol By default a seat cannot close a ticket. ``ljos finish`` leaves the ticket open, and a person closes it. A grant changes that for one seat. Here a person says in their own words that carol may close the tickets she filed or has claimed, and a recorder writes it down: .. code:: console $ ljos seat grant carol close=own --quote "carol may close what she files" --at 2026-10-29T02:00:00Z --via alice The row lives in ``vissue.toml``. It holds the quote, the time, the recorder named by ``--via`` and the tracker commit. A grant edited into the file by hand is ignored. A seat cannot grant itself. Every close that uses the grant cites the record. Write to another seat ===================== Seats on one pack write to each other as pack atoms. ``ljos sync`` carries those atoms in the sealed log, so a seat on another machine sees them after a sync. .. code:: console $ ljos group ops --add alice --add bob $ ljos send bob "the build is red" --issue acme-4 --interrupt $ ljos inbox $ ljos read ID $ ljos reply ID "looking" ``ljos send SEAT TEXT`` writes to one seat. ``ljos send --group NAME TEXT`` writes to the other members. ``--interrupt`` puts the message ahead of the others on the next prompt. ``--issue ID`` threads it on that vissue issue, and a reply keeps the issue. ``ljos inbox`` lists what this seat has not read, and writes no receipt. A pack that does not answer is an error. A writer that refuses kind ``message`` is an error too: the command exits non-zero, names packset 0.13.0, and does not print an empty inbox. ``ljos send`` names the packset version that keeps mail. ``ljos read ID`` writes one receipt. ``ljos reply ID TEXT`` writes back to the sender. ``ljos hook --prompt`` shows unread mail at the start of the next prompt, at most eight messages and interrupts first, and writes one receipt for each message it shows. The rest stay unread. The receipt atom's text is four lines, ``read ID``, ``by SEAT``, ``at STAMP`` and ``msg ID``. The sender's next inbox prints that receipt once, as the message id, the reader and the stamp, and a second prompt writes nothing. Shown there is read. A shell writes the receipt before the hook exits. Cursor holds the note on the prompt and writes the receipt when it is delivered: ``postToolUse`` puts the note in ``additional_context``, and a turn with no tool puts it in ``followup_message`` on ``Stop``. A shell seat has no hook, so it polls ``ljos inbox`` and reads with ``ljos read``. Wake a seat when mail reaches it ================================ Mail waits until the recipient's next prompt, and an idle agent has no next prompt. A seat names how to wake it, and ``ljos send`` starts that wake after the message is written. The sender does not wait on it and needs to know nothing about the recipient's runner. .. code:: console $ LJOS_SEAT=claude-review ljos wake --timeout 1800 --command 'cd ~/src/app && flock -n /tmp/claude-review.lock claude -p --resume SESSION "Read your ljos mail and act on it."' $ LJOS_SEAT=codex-ci ljos wake --timeout 1800 --command 'cd ~/src/app && codex exec resume --last "Read your ljos mail and act on it."' $ LJOS_SEAT=cursor-cli ljos wake --timeout 1800 --command 'cd ~/src/app && agent -p --resume=CHAT_ID "Read your ljos mail and act on it."' $ LJOS_SEAT=grok-bot ljos wake --webhook https://api2.cursor.sh/automations/webhook/ID --key-file ~/.config/ljos/grok-bot.key $ ljos wake claude-review --fire A command runs under ``sh -c`` with ``LJOS_SEAT`` set to the recipient, so ``ljos inbox`` in it reads the right mail. With a prefix wake, ``LJOS_SEAT`` is still the run seat that matched the prefix. ``LJOS_WAKE_FROM``, ``LJOS_WAKE_MSG``, ``LJOS_WAKE_ISSUE`` and ``LJOS_WAKE_INTERRUPT`` name the message, so a wake can ignore plain mail and act only on an interrupt. policyd judges the command when it is set and again on every run. A ``deny`` or an ``ask`` keeps it from running, since nobody is there to answer an ask. A command is stopped after 30 seconds; ``--timeout S`` changes that, and an agent run such as ``claude -p`` needs a longer one. Two sends close together start two wakes, so a wake that resumes a session holds a lock (``flock -n``) and leaves a running session to its ``Stop`` hook. A webhook gets the envelope as JSON: the seat, the sender, the message id, the issue, the interrupt flag, and ``via`` when a prefix matched. The body stays in the pack, and the woken agent reads it with ``ljos inbox``. The key is read from ``--key-file`` or the variable ``--key-env`` names, and never written to ``wake.toml``. A key is a prefix only when it ends in one trailing ``-`` after a non-empty stem; names such as ``-``, ``--`` and ``a--`` are exact names. A Grok Bot wakes this way: make a routine with a webhook trigger, copy its URL and sender key from the routine's panel, and save the key in a file only you can read. A wake that fails or runs past its timeout is a line in ``~/.local/state/ljos/mail/wake.log``, and the message is still in the pack. ``ljos wake SEAT --fire`` runs the wake now and prints what happened. A session that is still working needs no wake. When it is about to stop, its ``Stop`` hook holds the turn once for unread interrupt mail. Claude Code, Codex and Grok Build get ``decision: block`` with the mail as the reason, and Cursor gets a ``followup_message``. Each letter is receipted once it is delivered, so the next stop is not held for it again. Plain mail waits for the next prompt or the wake. Inject memory before the agent acts =================================== Add ``hooks`` to the runner's table and onboard again: .. code:: toml [[harness]] name = "runner-with-a-command" register = ["runner", "mcp", "add", "-s", "user", "ljos", "--", "{server}"] registered = ["runner", "mcp", "get", "ljos"] skills = "~/.runner/skills" hooks = "~/.runner/settings.json" .. code:: console $ ljos onboard --harness runner-with-a-command ok host key /home/you/.config/deedar/host.key exists ok runner-with-a-command mcp ljos registered ok hook memory hook: add it on UserPromptSubmit in /home/you/.runner/settings.json ok skill /home/you/.runner/skills/ljos/SKILL.md is current From then on, before each prompt, the runner pipes it to ``ljos hook`` and the memories it activates return as context, preferences first. Do not add ``PreToolUse`` as a search event. A turn issues many tool calls and one prompt. On a tool call the hook asks ``ljos-policyd``, the Trusted Computing Base (TCB), and then applies pack rules. A deny blocks. If policyd is missing, slow or crashes, the command runs and ljos logs a warning. .. code:: console $ echo '{"hook_event_name":"PreToolUse","tool_input":{"command":"git push --force"}}' | ljos hook {"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"ask","permissionDecisionReason":"git-force-push: it rewrites shared history, so the agent asks you first (ljos rule `git-force-push`)"}} An ``ask`` rule uses the caller's approval UI when that UI is supported. Otherwise the hook stops the command and prints a request id. Calling ``ljos_request_approval`` with that id shows a consent form in a client that can display one. Otherwise the person replies ``approve REQUEST_ID`` in the same conversation, where the prompt hook records the grant, or runs ``ljos approve REQUEST_ID`` in a terminal of their own. The command refuses under an agent runner or without a terminal. A request from another conversation is not granted from this one. An agent therefore cannot grant itself consent. Once the person has approved, the agent retries the original tool call. .. code:: console $ ljos approve REQUEST_ID Approved once: git push origin main Directory: /work/project Conversation: example-session Retry the original command. ``REQUEST_ID`` is the 32-character hexadecimal id printed by the hook, not an arbitrary command or a wildcard. The receipt names the exact command and the directory and conversation reported by the hook. A tool-level working directory takes precedence when the caller includes it. A caller that supplies only its conversation directory does not expose shell working-directory overrides to this check. The grant lasts for one matching attempt and expires fifteen minutes after the request. A changed command, reported directory, conversation, tool, or rule needs its own approval. Retrying an expired or consumed id is refused. Deny rules and policyd still run on every attempt and take precedence over consent. Native execution permissions still apply after the hook accepts the retry. Requests live in the private login runtime directory, under ``ljos/approvals/requests.json``. They are temporary consent records, not standing rules or memories. The file is locked while a grant is recorded or consumed; unavailable or invalid state keeps the command blocked. **Grok Build.** ``ljos onboard --harness grok`` writes ``~/.grok/hooks/ljos.json``. The hook searches on the prompt, keeps the text, and emits it on ``PostToolUse``. The page :doc:`Grok Build ` is the install. A policy daemon does the same with the argv: .. code:: console $ echo "cargo test --release" | ljos hook - [preference] Build and test on the cluster through Slurm, never on the laptop. - [lesson] The lme dense run needs 40G and sessions only; windows and sessions ran past four hours. $ ljos policy cargo test --release cargo test --release Remembered, oldest first (`ljos search` for more): Let policyd fix a command ========================= Coming in 0.35.0. policyd can answer a command with a fixed one instead of a refusal. The answer is one line of three tab-separated fields: ``allow``, ``rewrite`` and the new command. The first rewrite runs a command through ``uv run``. The new line runs only when the guard also allows it. Claude Code, Codex, Grok Build and Cursor's ``preToolUse`` take it through the hook's updated input. Other runners get a refusal that names the new command, so the old line never runs. ``ljos policy -- COMMAND`` prints the answer without running anything, so you can see a rewrite before an agent meets it. Each swap also shows a note naming both commands and leaves a row in ``$XDG_DATA_HOME/ljos/rewrite.jsonl``. Set ``rewrite = false`` in a repository's ``ljos.toml`` to run its lines as written. The log keeps a row for a swap it declined too. Choose which branches the push check covers =========================================== ljos keeps one push check. A push onto a protected branch asks when the ticket of your sitting has no settled consensus. The check asks and never denies. It covers ``develop`` and ``main`` unless ``consensus_branches`` in ``ljos.toml`` names others. The user and seat files set the list. The repository's ``ljos.toml``, ``LJOS_CONFIG`` and ``.ljos/gates.toml`` only add branches to it. .. code:: toml # ljos.toml consensus_branches = ["main", "release"] Everything else that concerns a push is policyd's. An ordinary push of a branch runs, and so does a lease push to a feature branch. A lease push or a force push onto a protected branch asks. For that ask, ``main``, ``master`` and ``develop`` are always protected, and ``protected_branches`` adds more. Deleting a remote branch and rewriting published history ask or deny. With one active seat in ``vissue.toml``, that seat's own vote settles consensus for the check. This is solo mode, and it is coming in 0.35.0. With two or more seats, the normal quorum applies. For a push that must never happen, protect the branch on the forge as well. Let Jev judge the prompt ======================== The prompt hook makes three judgments locally. A cross-encoder picks the claims that bear on the prompt. Two phrase lists spot a correction and a choice put to the agent. Jev, TypeSafe's decision model, can answer all three in one call, and a fourth route, ``review``, weighs a due claim. It stays off unless the machine turns it on, because the call sends the prompt and up to ten candidate claims off the machine. When the judge does not answer, the hook reranks with the local cross-encoder. A correction the hook files is a proposal, and a pasted or quoted prompt files none. A standing choice the person states ("from now on", "always", "never") is saved in their words, outside code and quotes. The same call asks two more questions, since a call costs its input tokens and each question adds about 70 of them to a median of 933. An ``injection`` question asks whether quoted or pasted text addresses the agent with instructions the person did not write; at ``cue_at`` the hook adds a note telling the agent to treat that text as data. An ``effort`` question scores the prompt from 0 (a lookup) to 3 (a design or a hard debug); it changes nothing in the hook and goes to ``$XDG_STATE_HOME/ljos/jev-log.jsonl`` as the record a router is scored on. Keep the key in ``pass`` and write the setting: .. code:: console $ pass insert api/typesafe/jev $ cat > ~/.config/ljos/jev.toml <<'TOML' enabled = true key_cmd = ["pass", "show", "api/typesafe/jev"] TOML $ ljos doctor --all | grep jev ok jev on jev jev-1.13.0 0 calls, 0 cached $0.0000 of $4.00 this month The hook runs the key command once per login, with no terminal and three seconds to answer, and keeps the key in ``$XDG_RUNTIME_DIR/ljos/jev-key`` (mode 0600). ``key_file`` names a file instead. The call goes out only where it can change the answer: ================== ======= ====================================== check default skipped prompt ================== ======= ====================================== ``min_words`` 4 "yes", "keep going" ``min_candidates`` 2 fewer claims than a choice needs ``monthly_usd`` 4.0 any, once the month's spend reaches it ================== ======= ====================================== A skipped prompt gets the lexical search and the phrase lists. With Jev on, the hook never loads the cross-encoder, so its memory stays free. A prompt past 2,000 characters goes out cut to that length. A prompt with ten candidates takes about 900 input tokens. At $0.042 per million input tokens, with output free, that arithmetic is a prompt cost of $0.00004. The hook records each call's tokens at ``usd_per_mtok_in``, or the API's own figure when OpenRouter sends one, in ``$XDG_STATE_HOME/ljos/jev-cost.toml``. For OpenRouter, set ``endpoint = "https://openrouter.ai/api/alpha/decisions"`` and ``model = "typesafe/jev-1.13"``. Jev answers each question with a probability. ``bears_at`` (0.5) is the one at which a claim goes in, and ``cue_at`` (0.5) the one at which a prompt counts as a correction or a choice. Raise ``bears_at`` to 0.7 when off-topic claims come through. Jev keeps no cache of its own, so ljos keeps one. An identical request within ``cache_days`` (7) is answered from ``$XDG_CACHE_HOME/ljos/jev`` at no cost, and the doctor row counts those answers as cached. A panel asked twice, a retried ballot and a repeated prompt with the same claims all hit it. ``cache_days = 0`` turns it off. Two or more judges can answer side by side. Each ``[judges.NAME]`` table names a judge. ``[route]`` says which judges answer each decision. Here a chat model on an OpenAI-compatible endpoint judges the prompt, Jev and that model pool their ballots, and the chat model reviews due claims: .. code:: toml enabled = true key_file = "~/.config/ljos/jev.key" # the default judge: Jev [judges.chat] backend = "chat" endpoint = "https://llm.example.org/v1" model = "gpt-oss-120b" key_env = "LLM_API_KEY" budget_ms = 6000 [route] prompt = ["chat"] ballot = ["default", "chat"] audit = ["chat"] review = ["chat"] A decision with two or more judges is answered by their pool. A probability is the weighted mean of the judges' log-odds. Two judges at 0.8 stay at 0.8, and judges at 0.9 and 0.1 meet at 0.5. A choice is the normalised weighted geometric mean of their distributions, and a score the weighted mean. ``weight`` in a judge's table moves its share. A question no judge answered is missing from the pool, so the caller falls back as it does on a partial reply. A caller with no judge at all takes its local path on any of these: no file, ``enabled = false``, no key, an error, or no answer inside ``budget_ms`` (2000). The log keeps every judge's answer beside the pool, so each judge can be scored once outcomes are known. These judges are fast and calibrated, and they do not reason. A decision they leave open goes to a persona that does: a persona with a runner reasons in a session of its own. .. code:: console $ 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; then `ljos consensus surf-ab12` The runner is any coding agent the machine runs: Claude Code, Codex, Grok Build, omp, hermes or Antigravity's ``agy``, each on whatever model it is set to, from Claude Opus or GPT-5 to ``gpt-oss-120b`` on an OpenAI-compatible endpoint. It is a ``[[harness]]`` in ``~/.config/ljos/harnesses.toml``. Its ``start`` and ``resume`` argv say how it opens a session and how it resumes the latest one in a directory, for example ``resume = ["grok", "--continue"]``. It starts in a pane the person can watch and talk in: a herdr workspace when herdr's server is up, else a window of the tmux session ``ljos-personas``. A ``[[tool]]`` the runners file declares is tried before either (see *Open persona panes in another tool*). A runner that exits non-zero is resumed in its pane, at most three times a minute. It works in the persona's home, ``$XDG_STATE_HOME/ljos/personas/NAME``, as the seat named after the persona. Its memories, ballots and trust rows are therefore the persona's. It reads the issue and the pack, writes its reasoning with ``ljos note``, and casts ``ljos vote --as NAME``. Each runner continues the latest session of the directory it starts in, so the next hand-off resumes the same conversation, and an open pane takes the next task in place. A task goes to the persona's inbox, and the pane is told the file in one line. ``ljos ask NAME "..."`` puts a question to the persona the same way. ``ljos supervise NAME --harness RUNNER --exit crash --at SECONDS`` says whether that child should be started again, under the restart policy (permanent, transient, temporary, three strikes in a minute), and prints the herdr line when the answer is yes. It does not start the pane. ``ljos panel --jev`` hands each unsure persona's brief to its own session when the panel splits. A persona with no runner gets a brief for a subagent, as before. ``ljos due --judge`` puts the due page to the ``review`` judges. Each claim goes with the newer claims the pack keeps on the same subject. A claim the pool puts at 0.9 or more is graded recalled. One at 0.1 or less is printed as contradicted, for the agent to supersede or withdraw, and stays due. Everything between stays due. No judge lapses a claim, because a lapse says that a reader forgot it. Open persona panes in another tool ================================== ``ljos doctor`` names the tool persona panes open in: .. code:: console $ ljos doctor --all | grep panes ok panes herdr opens persona panes; tmux also here herdr opens one workspace a persona, in the persona's home and as its seat, and types the pane script into the root pane. It hands the runner each task through ``herdr agent prompt``, or as typed keys when herdr does not know the runner as an agent. tmux gives each persona a window in the session ``ljos-personas``. ``LJOS_PANE_TOOL`` narrows the choice to the one it names. Any other tool is a ``[[tool]]`` table in ``~/.config/ljos/harnesses.toml``, with each verb but ``pane_pointer`` an argv or a list of argvs. A table named like a built-in shape replaces it. A terminal multiplexer with a command line takes a table like this: .. code:: toml [[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}"] ready_s = 8 A tool that refuses says so in the line ``ljos ask`` prints, with the tool's own words, and the next tool is tried. Wire the seat into Cursor ========================= .. code:: console $ ljos onboard --harness cursor ok cursor mcp set /mcpServers/ljos in /home/me/.cursor/mcp.json ok hook added the seat's hook on beforeShellExecution, preToolUse, beforeMCPExecution, beforeReadFile, beforeSubmitPrompt, postToolUse, postToolUseFailure, preCompact, stop, sessionEnd in /home/me/.cursor/hooks.json ok skill wrote /home/me/.cursor/skills/ljos/SKILL.md ok agents wrote 5 and removed 0 agent definitions in /home/me/.cursor/agents Cursor's IDE and its ``agent`` command line take the server from the same ``~/.cursor/mcp.json`` and the hooks from ``~/.cursor/hooks.json``. Cursor does not hand a prompt hook's context to the model, so the memories a prompt activates arrive with the first tool result instead, as on Grok Build. A shell command gets an ``ask`` rule's question as Cursor's own permission prompt. Cursor runs the Claude hooks itself where ``ljos onboard --harness claude`` already ran. The events Claude's file names stay there. ``beforeShellExecution``, ``preToolUse``, ``beforeMCPExecution``, ``beforeReadFile`` and ``postToolUseFailure`` are still written to ``~/.cursor/hooks.json``. If a hook fails, the command runs. The seat answers both files in Cursor's shape. Spawn a persona as a subagent ============================= .. code:: console $ ljos agents --harness grok ok agents grok wrote 5 and removed 0 agent definitions in /home/me/.grok/agents Every persona becomes ``~/.grok/agents/ljos-NAME.md``, and Grok's main agent can spawn it by name on a decision with ``spawn_subagent`` and ``subagent_type`` ``ljos-reviewer``. The persona reads ``ljos brief``, casts one ballot before reading the others, notes why, and stops. Grok runs it in ``capabilityMode: execute``, with commands and no edit tool. Claude Code reads the same definitions from ``~/.claude/agents``, and Cursor's copies go to ``~/.cursor/agents``; both ignore that mode, and the body tells the persona not to edit. ``onboard`` writes the definitions for a runner whose table names ``agents``, and a persona written later refreshes each such directory that exists. Show the seat in the status bar =============================== In ``~/.grok/config.toml``: .. code:: toml [ui.status_line] type = "command" command = "ljos statusline" In ``~/.claude/settings.json``: .. code:: json {"statusLine": {"type": "command", "command": "ljos statusline"}} The row reads ``ljos grok · surf-ab12 · 3 due``: the seat, the issue the conversation has open, the claims due for review. A session's line is cached for fifteen seconds. A pack that does not answer in 300 ms shows as ``pack down``. Put the judgments to a local model or a runner ============================================== The same four judgments go to any judge that answers in Jev's shape. ``backend`` in ``jev.toml`` picks it; the parsers, the cache, the ledger and the doctor row are shared, and every fallback above still applies. A chat-completions endpoint, hosted or a local llama-server, takes one JSON-mode chat completion per judgment set. ``endpoint`` is the base URL and ``model`` the name the server knows. A local server needs no key; ``usd_per_mtok_in = 0`` keeps the ledger exact for one that is free. .. code:: toml enabled = true backend = "chat" endpoint = "http://127.0.0.1:8089/v1" model = "Qwen2.5-7B-Instruct" usd_per_mtok_in = 0 budget_ms = 6000 A runner on the machine is the judge through ``command``: an argv that reads the request JSON on stdin (``state``, and ``questions``, each with a ``type`` of ``noul`` or ``choice``, ``instructions`` and ``criteria``) and prints ``{"answers": {...}}`` on stdout inside ``budget_ms``. A ``noul`` answer is ``{"noul": p}`` or a bare number; a ``choice`` answer is ``{"choice": KEY, "confidence": p, "probabilities": {...}}``, and a bare key means a sure one. A question with no answer refuses the whole reply, so the hook takes its local path rather than half a judgment. .. code:: toml enabled = true backend = "command" command = ["/path/to/judge.sh"] budget_ms = 20000 The hook budget bounds the prompt path, so a harness that takes seconds suits ballots and the stop audit more than the prompt hook; ``min_words`` and ``min_candidates`` keep the prompt calls few. The log carries the ``backend`` name, and ``ljos doctor`` prints it before the model. Give an agent the protocol without a runner =========================================== .. code:: console $ ljos protocol > instructions.md The text is plain markdown: which store answers which question, the order of verbs before, during and after the work, and the refusals worth knowing. Put it where the agent reads its instructions. Ask what a task involves ======================== .. code:: text $ ljos island "write the handbook tutorial for the seat" 1.000 seed ab8cb4d1bfca05ca60730141343dc177 3 weeks ago Encoding under the write lock capped the pack at 0.899 seed 51778929d855529fcb8df3f4d1c4d692 yesterday A new claim enters the review clock on write. Tr 0.890 seed 27e39df0198b67538f8487b1b44e9dfd today The decay slot reads the review clock when PACKS The first column is activation relative to the strongest; ``seed`` marks a claim search found itself, the rest were reached along the pack's links; the age column says when each was written. Add ``--fire`` when you go on to use the island: the strongest eight fire together and their links gain weight. Bump a recipe with eb-stack, and keep what the campaign taught ============================================================== One sitting on the ticket, one island per recipe before it is touched: .. code:: bash ljos sitting demo-319z ljos island "eOn 2.17.10 foss 2026.1" Then the ladder, each rung its own artifact: ``eb-stack recipe check``, ``eb-stack package bump`` (the lock under ``out/locks`` is ``resolves``), ``eb-stack recipe lint``, ``eb-stack target doctor``, ``eb-stack campaign run`` and ``campaign status`` (``builds``, ``binary-verified``). Over MCP the same verbs are ``eb_recipe_check``, ``eb_package_bump``, ``eb_recipe_lint``, ``eb_target_doctor``, ``eb_campaign_run``, ``eb_campaign_status``, beside ``ljos_sitting``, ``ljos_island``, ``ljos_remember`` and ``ljos_finish``. For a whole generation, put the bundle on the tracker before anybody builds, and let the graph hand out the modules, each up the same ladder: .. code:: bash ljos bump-plan out --project demo --parent demo-319z --dry-run ljos bump-plan out --project demo --parent demo-319z vissue ready -p demo The plan writes one child issue per module the lock builds, blocked by the modules built before it; ``ready`` is the frontier a seat can sit on, and ``ljos sitting`` refuses a module whose blockers are still open. The ids are a hash of the module and the generation, so running it again after the bundle changed adds the new modules and keeps the rest. When the campaign ends, its typed findings are the lessons: .. code:: bash ljos findings out/campaign.json ljos findings out/campaign.json --remember --issue demo-319z The first prints one line per finding. The second files one lesson per finding a person or a seat resolved, under the recipe's name, the package and the failure class, with origin ``tool-echo`` at L0 (``derived``). It does not write the atom. ``ljos accept ID`` promotes it to ``explicit`` and writes it as a standing lesson, and the command cites the state file on the issue as a deed. A finding a later attempt merely got past is skipped; ``--all`` takes it too. Once someone accepts the proposal, the recipe's next bump recalls this lesson from its island. :: GCCcore-15.2.0 on terra: compile failed in the build step with fatal error: linux/scc,h. Fix: applied the GCC 14 libsanitizer kernel headers patch in GCCcore-15.2.0. Keep a number ============= A number the seat measures again and again, a benchmark score, a hook latency, a count of open tickets, is a habit. A reading is one verb. The one below is an example: .. code:: console $ ljos habit mab-cr-all 0.535 --unit acc --every 7d --source example-0000 7b1c... habit due 2026-09-26T10:00:00.000Z habit mab-cr-all stands at 0.535 acc (example-0000). $ ljos habit mab-cr-all 0.579 --unit acc --every 7d --source example-0001 9e0a... habit due 2026-10-03T09:12:41.000Z habit mab-cr-all stands at 0.579 acc (example-0001). was 0.535 acc (8 days ago), now closed $ ljos habit mab-cr-all 0.579 acc +0.044 since 0.535 (8 days ago) today next reading in 7 days example-0001 The second reading supersedes the first, so the pack keeps one live value a habit and ``ljos search mab-cr-all --as-of 2026-09-15`` still answers what it stood at then. The cadence is the reading's review clock: when a week passes with no new reading, ``ljos due`` lists the habit and the hook says so once a session. The sheet a paper's table is written from is the pack, and it is dated. Ask when ======== .. code:: console $ ljos timeline proj-1a2b timeline of proj-1a2b: Ship the fuse change? 2026-08-30 13 days ago tracker created 2026-09-02 09:14 10 days ago +3 d memory [lesson] RRF lost two points to CombMNZ on turns. 2026-09-02 11:48 10 days ago same day tracker TODO -> STARTED 2026-09-02 11:48 10 days ago same day tracker claimed by you 2026-09-05 16:02 7 days ago +3 d deed deed-run-fuse-at-ten produced by you 2026-09-12 08:30 today +7 d tracker note: the fused panel held at ten conversations Three stores, one dated list, oldest first: the tracker's logbook, the deeds the issue cites with the time each was produced, and the memories the issue's title activates with the time each was written. Each line carries its date, its age, the gap since the line before, and the store it came from. The reader gets time as data rather than timestamps to subtract, and a later line supersedes an earlier one on the same matter. ``ljos sitting`` prints the last twelve as its ``timeline`` section. ``ljos search TOPIC --as-of 2026-09-01`` asks the pack as it was then: memories withdrawn since included, memories written since left out. The pack tombstones rather than erases, so the question has an answer. Start a sitting, and end one ============================ .. code:: console $ ljos sitting proj-1a2b == doctor ok vissue ... ok seat alice, holding as alice (from LJOS_SEAT) == cards == due unreviewed conclusion 3f9c... The lexical default is BM25+. 1 due; 4 scheduled, next at 2026-09-13T09:12:00Z == island: Ship the fuse change? 1.000 seed ab8c... 3 weeks ago CombMNZ over RRF for fusing two ballots. == blockers none open; the issue is workable == playbook sit A sitting on one issue. Name this recipe at open (`ljos sitting ISSUE --playbook sit` or `ljos playbook ISSUE sit`). The sitting prints this body before recall and holds the name until finish or release. ... == recall ... == timeline timeline of proj-1a2b: Ship the fuse change? 2026-08-30 13 days ago tracker created ... == reclaim no claim quiet longer than 900s == claim gen=2 One verb, in the protocol's order; it stops at the first store that does not answer and claims nothing. ``--playbook NAME`` copies a recipe (``sit``, ``arena``, ``land``, ``company-panel``, ``overnight``, ``feature``) into the playbook section before recall; absent a name, a closed-set token in the title else ``sit``. The name is a tracker ``playbook:`` note until finish or release. ``ljos panel`` refuses until one is bound. The ``start_a_sitting`` prompt gives the same order to a runner that prefers single tools. A claim refused as busy names the issue you still hold; ``ljos complete`` finishes it and ``ljos release`` hands it back. .. code:: console $ ljos finish proj-1a2b --lesson "CombMNZ held on turns. RRF lost two points." --outcome ship completed the session node for proj-1a2b as done remembered the lesson (id 51778929) as agent-derived; the hook shows it from the next prompt, and `ljos memories review` prunes it fired the island for "Ship the fuse change?": 8 memories. Those links gained weight under the seat, not under a persona. The next walk of this title follows them. Learned. 6 trust rows rewritten. ... persona anchors moved. The outcome is kept: once 5 issues have one, ljos consensus discounts voters who err together. ... No stated probability, so there is no Brier score. A hard vote is not a claim of certainty. the ticket proj-1a2b stays open and nobody holds it; a person closes it when the work is accepted Vote with personas ================== .. code:: console $ ljos persona reviewer --anchor 0.2 --view "Reads for what breaks in production." --about release $ ljos persona reader --anchor 0.8 --view "Reads as a first-time user of the docs." --about docs $ ljos brief reviewer $id $ ljos brief reader $id Each brief goes to its own subagent or run, which casts as its persona: the reviewer's runs ``ljos vote $id --for hold --as reviewer`` and the reader's ``ljos vote $id --for ship --as reader``. One session that cast both would be refused at the second, since a seat argument does not make another voice. The seat then runs ``ljos consensus $id``. Each persona is one atom in the pack; its ballots carry its name. The settle takes its anchor, never below the 0.5 every other voter on the ballot, the seat too, settles at: the reviewer at 0.2 settles at 0.5 and counts as much as a seat, and the reader at 0.8 moves most of the way toward the others. A trust row scoped with ``--about docs`` weighs only on issues whose title says ``docs``, and ``--about`` is also what seats a persona: ``ljos panel ISSUE`` and the ``run_a_panel`` prompt brief only the personas whose domains the issue's title or its island names, and every persona when none does. A persona keeps its own tree over the seat's facts. ``ljos remember --as NAME`` writes into the set ``persona-NAME``, where the duplicate and replacement rules run among its own conclusions and never against the seat's or another persona's; the seat still reads every set. ``ljos island CUE --as NAME`` walks the pack through that persona's link weights, and with ``--fire`` tightens the paths it walked under its own name (``link_weights_by``), leaving the seat's weights as they were. Two personas that read the same island differently end up with different islands over the same claims. Cast a reviewer's ballot from its own run ========================================= Each reviewer is its own subagent or run, such as a cloud agent or a CI job, and casts its own ballot rather than reporting a verdict in prose. A subagent's ballot binds the parent session plus the subagent, ``SESSION:AGENT``: the ``agent_id`` Claude Code's hook sees, or the Codex thread under its root session. An opencode subagent is a session of its own. Two subagents of one session count twice, the parent and a subagent count as two, and a subagent that wrote a candidate cannot vote. A run on another machine needs ljos and the tracker there. ``ljos cloud up`` installs ljos on that machine and clones the shared tracker repository that ``LJOS_TRACKER_REMOTE`` names, so the ballot is pushed where the other runs fetch it. .. code:: console $ ljos cloud up $ ljos vote $id --for ship --confidence 0.8 --used none The run's harness id is the ballot's session, so two runs count twice and a run or subagent that votes again counts once. When nothing names the run but the seat, ``ljos vote`` and the tally call the session unverified. Persona ballots and Jev ballots are advisory. The push check reads seat ballots only. See *Choose which branches the push check covers*. Cast persona ballots through Jev ================================ With Jev on, a persona's ballot can come from Jev instead of a subagent. The issue needs an ``Options: A, B`` line. ``ljos file "TITLE" --options A,B`` writes that line as the first line of the body. ``ljos options ISSUE A,B`` appends it to an issue that already exists, and refuses fewer than two. .. code:: console $ ljos vote ljos-abcd --as security-reviewer --jev judge:jev-1.13.0: cast age for security-reviewer at confidence 0.96 $ ljos panel ljos-abcd --jev 3 personas on ljos-abcd through Jev: all sure, all age; one judge ballot cast Jev reads the persona's brief and answers two choice questions: the persona's vote, and what the rest of the panel will pick. The option order is shuffled per persona. A cast ballot is recorded as ``judge:MODEL``, not as the persona, and ``--used`` names that judge. The chosen option's probability is the ballot's confidence. The forecast is the judge's prediction, and ``consensus`` does not read it. A note on the issue says the ballot came from Jev. ``learn`` updates the trust row whose ``to`` is that judge. Jev's confidence is ``(K·p_max − 1)/(K − 1)``, the same cut on every backend. Under ``escalate_below`` (0.8), ``vote --jev`` does not cast. It notes Jev's lean. A persona with a runner gets the ballot in its own session; for any other, the command names the brief to start a subagent from. A panel casts only when every seated persona is sure and all agree. An all-one-model panel is one voter, so it casts one ballot. A split or unsure panel casts nothing and writes a brief per seat for subagents. The metered model then spends only on the contested questions. A ballot costs about 2,000 input tokens, or $0.00008. Every Jev answer goes to ``$XDG_STATE_HOME/ljos/jev-log.jsonl``, with the claim ids, a hash of the prompt, the latency, a timeout or a failure, and the backend and model. The file does not keep the prompt or the claim text. ``ljos judge-score`` joins that log with outcomes and reports accuracy and calibration per decision. ``ljos learn`` scores a cast ballot's confidence as it does any voter's. Hold a done claim beside a red test =================================== ``Stop`` also blocks one turn, with no model in the loop, when the conversation has no issue and the turn used tools without touching the seat. The agent is told to file an issue and sit. The next stop of that same turn goes through. With Jev on, the ``Stop`` and ``SubagentStop`` hooks audit the turn once before the agent stops. The audit reads four parts of the turn, bounded to a few thousand tokens: - the person's last request - the shell commands the turn ran since that request - the latest test output, or the last output when no test ran - the final message from the agent Jev answers three questions about it: =================== ========================================================================== question blocks the stop at =================== ========================================================================== ``claims_complete`` 0.9 or above, with ``tests_green`` 0.1 or under and a test run in the turn ``tests_green`` (with the above) ``deferral`` 0.9 or above =================== ========================================================================== Whether a test ran is decided in code, from the command line. A held agent gets the reason and one more round; its second attempt to stop is never audited, so it cannot loop. A final message under 80 characters is not sent. The cuts sit in ``jev.rs`` beside the questions. A stop costs about 3,000 input tokens, or $0.00013. Move the trust rows without an outcome ====================================== .. code:: console $ ljos calibrate -p demo alice weighs bob at 0.917 alice weighs carol at 0.643 ... Every issue of the project with two or more ballots is an item; Dawid and Skene's estimate gives each voter an accuracy from how often it agrees with the answer the others make likely, and each accuracy becomes the weight every other voter gives that voter. Run it once a project has a few voted issues, and again when it has many more. Keep the cards ============== Cards are two files a person writes: ``USER.md`` and ``MEMORY.md``. The seat reads them and never writes them; ``ljos cards`` prints them. The pack is where the seat writes. Sign what you hand over ======================= .. code:: console $ head -c 32 /dev/urandom > ~/.config/deedar/host.key && chmod 600 ~/.config/deedar/host.key $ export DEEDAR_HOST_SIGNING_KEY=~/.config/deedar/host.key $ ljos doctor --all | grep 'host key' ok host key /home/you/.config/deedar/host.key (32-byte seed) ``handover`` then signs the manifest and the log head. A receiver adds ``signer = ed25519:`` to their deed store's ``layout`` and ``receive`` reports ``(accepted)``. Two seats share one ticket by this walk, in sequence: A sits and hands over, B imports and sits after A releases, both vote, consensus, finish closes. ``scripts/smoke.sh`` runs it on scratch stores; ``scripts/herd.sh`` is the concurrent contention check. Receive from the same sender again ================================== .. code:: console $ ljos receive /tmp/bag --keep ~/.local/share/ljos/peer.head # the first time ... 3 deeds proven against a log of 34 entries, root 9c1e... kept this head in /home/you/.local/share/ljos/peer.head; pass it to --since next time $ ljos handover --out /tmp/bag2 --issue proj-1a2b --since-size 34 # the sender, next time $ ljos receive /tmp/bag2 --since ~/.local/share/ljos/peer.head --keep ~/.local/share/ljos/peer.head ``--keep`` writes the head of the log the bag carried. Tell the sender its ``size``; ``handover --since-size`` puts the bridge from that size into the bag. ``--since`` then needs the bridge to start at the head you kept and end at the one in the bag, so a sender who rewrote the log in between fails the check. A bridge alone proves nothing, because the sender picks both of its ends. This needs deedar with ``check --keep``. Close what a later lesson replaced ================================== .. code:: console $ ljos consolidate closes 3f9c… The default fuse is Borda. for 51ee… The default fuse is CombMNZ. 1 of 97 live memories would close; `ljos consolidate --apply` closes them $ ljos consolidate --apply closes 3f9c… The default fuse is Borda. for 51ee… The default fuse is CombMNZ. 1 of 97 live memories closed A write closes the earlier claim it rewrites on arrival (same opening words, a new object; a correction; an explicit supersedes). A pack written before that rule, or filled by a handover, has pairs the rule never saw; ``consolidate`` reports them, ``--apply`` closes them. The closed claim keeps its window: ``ljos search --as-of`` finds it at the time it was live. Retire a claim with its reason ============================== .. code:: console $ ljos forget 3f9c... --why deed-review-2026-09 The pack tombstones the claim and records the deed that withdrew it. Disagreeing with a claim is not showing it wrong; a retraction names its evidence. Set trust by hand ================= .. code:: console $ ljos trust alice carol 0.9 --why deed-postmortem-2026-09 One row, one weight in (0, 1], the deeds it rests on. A later row for the same pair supersedes it. Settle under the pack's trust ============================= ``ljos consensus ID`` reads the live trust rows and passes them to the settle (and to the tracker's, with ``--compare``). To anchor voters to their own ballots (Friedkin-Johnsen) on the model crate alone, pass the rows as JSON tuples: .. code:: console $ ljos-consensus settle --issue ID --susceptibility 0.8 --trust '[["alice","carol",0.9]]' Check the seat ============== ``ljos doctor`` prints whether the seat is ready and the rows that need action, and exits 1 when a required part does not answer or the note queue is damaged. ``ljos doctor --all`` prints every row. The block is the ``ljos`` row that command printed, taken from ljos 0.32.1. ``ljos --version`` printed ``ljos 0.32.1``. The row is the shape when the binary on ``PATH`` is that version: no ``behind`` and no ``ahead of`` in front of ``crates.io``. A day cache that already holds the crates.io answer prints ``crates.io (cached)`` in that place. The path stands in for the binary. On this tree the same run also read vissue-cli 0.21.0 and packset 0.15.3 from crates.io. Those binaries were not on ``PATH``, so the sample does not show them as ``ok``. .. code:: text $ ljos doctor --all ok ljos /path/to/ljos 0.32.1 crates.io 0.32.1 Exit 1 when the tracker, the deed store, or the pack does not answer. The tracker row names how many commits origin lacks, and fails when that count sits through the push wait. It also fails when another remote of the tracker has a different head of the branch, as of the last fetch: two seats pushing to two remotes each read only their own writes. A sitting's own tracker push goes to every remote that carries the branch. A missing claim graph is reported and is not a failure: the first claim creates it. A seat nobody named is a derived seat, and a derived seat is a seat. The ``seat source`` row says where the name came from. A damaged note queue is a ``no`` row and fails: it holds lessons the pack has not taken. An empty or missing log of either is whole.