Harnesses

Wire ljos-mcp

One server, one skill, one hook. ljos onboard installs all three; ljos and ljos-mcp must be on PATH.

One verb per runner

Describe the runners on the machine once, in ~/.config/ljos/harnesses.toml. A runner registers MCP servers through a command of its own (the first table) or through a config file it reads (the other two). A table names one of those two, the directory it loads skills from, and the settings file it reads hooks from. ljos onboard --example prints the shape.

[[harness]]
name = "runner-c"
config = "~/.runner-c/config.toml"
marker = "[mcp_servers.ljos]"
snippet = "\n[mcp_servers.ljos]\ncommand = \"{server}\"\nargs = []\nstartup_timeout_sec = 30\nenv = { LJOS_SEAT = \"{name}\", GROK_SESSION_ID = \"${GROK_SESSION_ID}\" }\n"

[[harness]]
name = "runner-a"
register = ["runner-a", "mcp", "add", "-s", "user", "ljos", "-e", "LJOS_SEAT={name}", "-e", "GROK_SESSION_ID=${GROK_SESSION_ID}", "--", "{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 = []\nenv = { LJOS_SEAT = \"{name}\", GROK_SESSION_ID = \"${GROK_SESSION_ID}\" }\n"
skills = "~/.runner-b/skills"
hooks = "~/.runner-b/hooks.json"

Then, for each:

$ 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
$ ljos doctor | grep runner

Three things land. The server, so the runner has the seat's tools. The skill is the protocol ljos protocol prints: which store answers which question, the order of verbs in a sitting, the refusals worth knowing. The memory hook runs before each prompt. The runner pipes the prompt to ljos hook. The memories it activates come back as context, preferences first, each once per session. Do not add PreToolUse to search the pack. A turn issues many tool calls and one prompt. On a tool call, ljos hook only decides rules (deny a force-push). Searching on every tool is what made Grok wait 20s and print Ljos Ljos Due. --dry-run reports and writes nothing.

{server} in a table is the path to ljos-mcp, {name} the runner's name. Passing LJOS_SEAT={name} to the server, as the tables above do, makes each runner claim and vote under its own name. Two runners on one host share the one pack and the one tracker. ljos sitting without --assignee takes that name. Neither is refused as busy for what the other holds. A persona named with as speaks over both.

Any other runner

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

Paste the entry where the runner keeps its MCP servers. Cursor, Pi, and the rest take this shape. Put the protocol where the agent reads its instructions. If the runner has hooks, a command hook on prompts that runs ljos hook completes the set. Grok: ljos onboard --harness grok writes the frozen hook file. The prompt's pack is held and emitted on PostToolUse. Later cargo binstall ljos is the new hook. Name ljos-mcp on PATH; bump LJOS_MCP_GENERATION to respawn. Crate page: Grok Build.

The two verbs an agent runs

ljos sitting <issue> --assignee <name>       # 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>

Citation is not a merge. Completing a session node does not close the ticket.