Reference
Command line#
Verb |
Habitat |
Does |
|---|---|---|
|
all |
open a sitting: doctor, sync when the repository names a scope, cards, due, island, blockers, playbook, workflow when the issue carries one, panel when the issue is a decision, recall, timeline, reclaim, claim; reclaim hands back claims
quiet longer than |
|
all |
close a sitting: complete the session node, file |
|
consensus, pack |
trust rows from the project’s voting history (Dawid-Skene accuracy) |
|
pack |
one lesson, stored as written, origin |
|
pack |
review the memories agents wrote on their own: |
|
pack, user config |
one standing preference, origin |
|
pack |
promote one open proposal from L0 ( |
|
pack |
tombstone a claim, naming the deed that withdrew it |
|
pack |
tombstone the note memories an older |
|
pack |
ranked claims with how many scorers named each and its age; |
|
tracker, deed store, pack |
the logbook, the cited deeds and the activated memories as one dated list, oldest first, each line with its age and the gap since the one before |
|
pack |
the pairs where a later claim rewrites an earlier one; |
|
pack |
candidate contradictions by geometry: the lowest passes between single memories, read by the optional |
|
pack |
the claims this seat’s memory turns on, highest first: a weighted PageRank over the links |
|
pack |
the memories a task activates: search hits spread along the links; |
|
pack |
sweeps the pack first (reviews left due past twice their interval lapse; never-recalled claims missed three times are forgotten), then the claims whose review is due, unreviewed ones first, one line on the clock, and one on what the sweep moved |
|
none |
exec sibling |
|
pack |
with a value, take a reading: the earlier reading closes and is kept as what it was, the next is due one cadence on; without, list each habit’s latest reading with the change since the one before |
|
pack |
one review graded |
|
pack |
one trust row; scoped when |
|
pack |
a voter with a view; |
|
pack |
withdraws the persona, so it casts no more ballots; only the conversation that defined it may, unless |
|
pack |
the roster: name, anchor, domains, view, one per line |
|
pack, runner |
every persona as an agent definition, |
|
pack |
one line for a runner’s status bar (Grok’s |
|
persona session |
hands the persona a question or a task in its own pane, opening it (and resuming its session) when it is closed |
|
runner |
classify one harness exit and print |
|
release |
installs a published release beside the running |
|
pack |
bind a recipe to the issue and copy its full body; the name is a tracker |
|
pack |
the closed set sit, arena, land, company-panel, overnight, feature; pack latest per name, the built-in seed when missing |
|
tracker |
a parent’s workflow: stage, round and next step; |
|
pack, tracker |
what a subagent playing the persona starts from: view, the bound playbook’s full recipe, five named principles, the arena rubric, what the seat knows on its domains, the working set |
|
pack, tracker |
one brief per persona whose domains the issue speaks to as |
|
tracker, runner |
open a panel for a decision prompt: file or reuse the issue, sit it, and start one headless member per brief. The prompt is already in the file, so the hook can return. |
|
tracker, pack |
reweigh voters by what turned out right: by default each voter’s record of hits and misses as log-odds weights, its accuracy shrunk toward the panel’s pooled accuracy; |
|
deed store |
intact; still the tip |
|
none / tracker |
|
|
tracker |
file work found while sitting: a child of the held issue unless |
|
tracker |
a dated progress note on the issue, committed; on an issue another seat holds it says so: the note is an assist, and |
|
tracker |
take back this seat’s own ballot, or with |
|
tracker |
put a finished or cancelled issue back to the state the close took it from, whoever closed it; a person has to be present; the close stays on the logbook with the reopen under it, and the caller does not take the claim |
|
tracker |
tracker commits by one seat. The subject is |
|
tracker |
how many tracker commits each seat made from that day on |
|
tracker, deed, pack |
one view of what each seat did, newest first: tickets filed, claimed and closed, votes, consensus, deeds, claims, guard refusals and rewrites, and memory. last continues from the previous run |
|
pack |
write to one seat, or with |
|
pack |
mail this seat has not read, then receipts on what it sent that it has not been shown; listing writes no receipt; |
|
pack |
write a read receipt for one message; a second read writes nothing |
|
pack |
answer the sender, keep the issue, and carry |
|
pack |
list a group, or add and drop members; membership is atoms and travels in the sealed log |
|
machine |
how mail wakes a seat that is idle: a shell line this machine runs with |
|
tracker |
cite with |
|
tracker |
who has claimed what: |
|
tracker |
the working set |
|
tracker |
cast as this seat, never another, or read the tally; |
|
pack |
forecast the others’ shares; two or more and |
|
pack, user config |
argv law the hook and |
|
pack |
the rules the hook and |
|
consensus, tracker |
first what the seat ballots alone decide for the push check; then the settle under the pack’s rows that apply to the issue, with every persona’s anchor, as advice; an issue tagged |
|
claim graph |
claim a session node. With |
|
claim graph |
hand the session node back unfinished: ready, generation moved; a node another conversation of this seat holds (a worker it started, or the one that started it) is released under that name |
|
claim graph |
release claims idle past a window and mail each seat. Without |
|
claim graph |
finish the session node; does not close the ticket; |
|
cards |
print |
|
runtime |
record explicit consent for one pending hook request; one matching attempt, fifteen-minute lifetime, deny rules still apply |
|
policy, pack |
the line as it would run, the trusted computing base (TCB) answer if |
|
policy, pack |
hook JSON or argv on stdin; |
|
git |
install, remove, or show the global git hooks that book commits. |
|
tracker |
one child issue per module the bundle’s lock builds, under |
|
pack, deed store, tracker |
the typed findings of an eb-stack campaign state, one per line: id, status, class/disposition, stage, recipe, the fix or the summary; |
|
tracker, pack |
share the seat’s memory across machines through the tracker repository: pull and take other machines’ sealed logs, then write and push this machine’s; |
|
tracker |
re-export this machine’s atoms over a conflicted tracker log. git calls this as the |
|
all |
pack, seal, sign; |
|
all |
check, and import the atoms with origin |
|
all |
whether ljos is ready, and the rows that need action; exit 1 when a required part does not answer or the note queue is damaged. |
|
tracker |
who is sitting; |
|
none |
print the sitting protocol |
|
runner |
alone, print the one |
|
runner |
restore runner files changed by |
The manifest and backups are under ${XDG_STATE_HOME:-~/.local/state}/ljos/onboard.
They are removed after every recorded runner is offboarded.
ljos file "TITLE" --options A,B writes Options: A, B as the first body line.
ljos options ISSUE A,B appends that line and refuses fewer than two.
Withdrawing a ballot writes ballot-withdrawn: VOTER HARNESS NONCE.
The tally leaves that ballot out, and so does the push check.
A later cast from that session says the ballot was withdrawn.
send SEAT TEXT writes to one seat. A seat the pack has never heard
from is refused, with the seats it knows, since mail waits under the name
it was sent to; --new-seat writes to one that has not started yet.
send --group NAME TEXT writes to the other members of that group. --interrupt leads the next prompt.
--issue ID threads the message on that vissue issue, and the atom takes
the issue’s scope so ljos sync carries it in that repository’s sealed
log. inbox lists unread mail 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. When the writer reports a version, the error names that
version. read ID writes one receipt. reply ID TEXT
answers the sender and keeps the issue. group NAME --add SEAT records
a member. ljos hook --prompt shows at most eight unread messages,
interrupts first, and writes one receipt for each message it shows.
The rest stay unread, and the note says how many remain.
The receipt is an atom whose text is four lines: read ID, by SEAT,
at STAMP and msg ID. The sender’s inbox prints that receipt once,
as the message id, the reader and the stamp. A second prompt writes
nothing. A shell writes the receipt before the hook exits. Cursor holds
the note: the prompt prints nothing and writes no receipt, and the
receipt is written when the note is delivered, on postToolUse as
additional_context, or on Stop when the turn ran no tool, as
followup_message. When the pack does not answer, the hook prints
mail could not be checked and still exits 0. A shell seat polls ljos inbox.
send and reply start the recipient’s wake, when it has one, after the message is written.
ljos wake --command CMD runs CMD under sh -c with LJOS_SEAT set to the seat, so ljos inbox in it reads that seat’s 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.
ljos wake --webhook URL --key-file FILE posts the envelope (seat, sender, id, issue, interrupt, and via when a prefix matched) to an https URL with the key in that file as a bearer token, and never the body.
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.
The sender waits on neither, and a wake that fails or runs past its timeout is a line in ~/.local/state/ljos/mail/wake.log.
On Stop, unread interrupt mail holds the turn once and is receipted, so a running session reads it before it goes idle.
A seat the pack has never heard from is refused with the known seat it most likely meant.
ljos deed --remove ACCESSION --ticket ID drops that citation. The removal is appended to the ticket. The heading is not rewritten.
remember and prefer write origin user-declared. finish --lesson
files a proposal at L0, packset’s derived level, with origin
agent-derived, and writes the lesson at once as user-declared when
the person types it in a terminal of their own and no runner marks
the environment. findings --remember files a proposal at L0 with
origin tool-echo, because a campaign file is tool output. The prompt
hook files a correction as a proposal in the person’s words, origin
user-declared, and does not write it. A proposal from tool output or
another seat (peer) stays at L0 until ljos accept ID promotes it to
explicit and writes it. An agent’s own lesson starts at L0, and the
same accept writes it. Accept keeps the origin. Under an agent runner
accept takes only a proposal another conversation filed, such as a
worker’s. The writer records the acceptance. The same text typed with
remember or prefer writes it as user-declared and satisfies the
proposal, and the held proposal is closed. An atom taken by
receive --import or by sync has origin peer, whatever origin the
sender wrote.
The claim graph records the held ticket too: a tracker id maps to one node (FNV-1a 128 of the id) and a name to one actor; a 32-hex id passes through.
On Linux the pack client reads who owns the socket on its port from
/proc/net/tcp. A PACKSET_PORT that another user listens on is
refused. With nothing set and 8761 another user’s, the seat uses port
18761 plus its user id modulo 10000 and exports it as PACKSET_PORT, so
packset ensure starts its writer there. PACKSET_URL is used as given.
Without XDG_RUNTIME_DIR, the hud socket and seat records go in
ljos-UID under the temporary directory, mode 0700, or in
~/.cache/ljos/run when that name is another user’s or a symlink.
User rules#
ljos prefer --user TEXT --why REASON writes one user rule.
The file is $XDG_CONFIG_HOME/ljos/user-rules.json, or ~/.config/ljos/user-rules.json when that directory is unset.
Every seat on this machine reads it. The row does not decay and it is not reviewed, and the command does not also store a seat preference.
--why is required with --user.
--promote ID copies that lesson into a user rule and leaves the lesson in the pack.
A promoted rule is stated ahead of a prompt only when the pack answers for that lesson within 400 ms, under the same id, stored as the person’s; a pack that is slow, down or answers otherwise leaves it out of the prompt.
The row keeps a hash of the lesson’s kind, text, ts, valid_from and stored origin, and a record under that id that hashes otherwise, such as an id retracted and posted again, is another lesson; a rule promoted before the hash was kept is not stated until it is promoted again.
The read goes only with this seat’s pack token, and only after the same read without it was refused, so a listener that answers without the token, or a packsetd run with PACKSET_AUTH=off, confirms no promoted rule.
ljos rules lists a promoted rule the pack does not confirm apart from the user rules, as unverified with the reason.
The hook still enforces such a rule’s pattern and its mention and reply denials, and the denial says the rule is unverified and why.
--forget ID leaves a tombstone so a later sync does not bring the row back.
--sync pulls and pushes user rules through the tracker repository. Nothing leaves the machine unless that repository’s .ljos/sync.toml names a scope. Then each machine’s rules are sealed with age to the scope’s recipients as .ljos/user-rules/<host>.json.age, and a plaintext <host>.json this machine wrote before is removed. Plaintext files other hosts left are still read. A write and the session start run the same pass and follow LJOS_TRACKER_GIT; --sync always pushes.
--deny-mention HANDLE, --deny-name NAME, --deny-reply-to HANDLE and --deny-reply-name NAME may be repeated.
ljos rule --user PATTERN --verdict deny|ask --why TEXT writes the argv law into the same file instead of the seat pack.
Without --user, both commands keep writing the seat pack.
Threat model#
The pack token is sent only to the socket address the writer’s URL resolved to, and only after that address has refused an unknown atom id with no token and with a token that is not this seat’s. The request that carries the token may be the next connection; a later request proves again. A redirect is not followed, and a proxy in the environment is not used. A process of the same user that reads the token file can still answer as the pack. The proof cannot tell that process from packsetd; a writer that signs its answers, or that runs as another user, would.
Git hooks#
ljos git-hooks install writes a stub for every standard git hook and sets the global core.hooksPath to that directory.
The directory is $LJOS_GIT_HOOKS when that is set, otherwise $XDG_DATA_HOME/ljos/git-hooks, otherwise ~/.local/share/ljos/git-hooks.
It prints installed and the directory.
A core.hooksPath that was already set is saved as previous-hooks-path in that directory.
When core.hooksPath still names that directory, ljos git-hooks uninstall puts the saved path back, or unsets it. It then removes the stubs and prints uninstalled.
ljos git-hooks status prints hooks: and the directory, or not installed, then core.hooksPath: and the configured value, or (unset).
ljos onboard --harness grokbot installs the same hooks.
Each stub chains to the repository’s .git/hooks/<name> when that file is executable, otherwise to the saved path’s hook of the same name, and keeps that hook’s exit status.
post-commit and post-rewrite also run ljos hook --git-commit --git-hook NAME.
Booking never fails the hook. A line it prints goes to stderr, and a habitat that did not answer is a line in the book log.
LJOS_OFF set to 1, true, yes or on, in either case, skips the booking, and the stub still chains.
ljos hook --git-commit books the commit git just created.
post-commit reads no stdin.
post-rewrite reads the old and new shas and moves the citation onto the new commit. A dropped commit, an all-zero new sha, loses its citation.
--git-hook is post-commit or post-rewrite.
An amend or a rebase moves the deed onto the new sha and does not leave the replaced sha cited.
The sitting’s lesson is written again for the new sha and closes the one that named the replaced sha.
A post-commit in the middle of a rebase or a merge does not book. post-rewrite does.
A commit already cited on an open sitting in that checkout is not opened as a second book.
The hook books as a seat. The holder is SEAT@TAG, the same name ljos claim uses for that conversation.
LJOS_SESSION_ID alone is kept as written.
LJOS_SEAT wins when it is set and is not seat, you, agent, or a name that slugs to runner.
Otherwise ljos derives the name from the markers the runner leaves in the environment, and a derived name is a seat.
When nothing named the seat, an ANTIGRAVITY_CONVERSATION_ID books as antigravity and an OPENCODE_SESSION_ID books as opencode.
A Claude Code or Codex session, named by CLAUDECODE, CODEX_THREAD_ID or CODEX_SESSION_ID, is not booked.
An opencode session is booked by this hook, including a commit from its shell or a subprocess.
An agy session is booked the same way when no seat was named.
Without these hooks, Stop and SessionEnd cite each commit HEAD’s reflog says the checkout wrote since the sitting began, so a commit from a script, or one chained before a step that failed, still has a deed.
A pull, a merge or a checkout adds none.
The env file ljos onboard writes sets LJOS_GIT_HOOKS to ${LJOS_GIT_HOOKS:-${XDG_DATA_HOME:-$HOME/.local/share}/ljos/git-hooks}, the same directory the hooks install into.
ljos doctor reports an env file that sets a different directory.
Commits under [git_hooks] skip in ljos.toml are not booked, and neither are /tmp, /var/tmp, /dev/shm, the tracker (VISSUE_ROOT or ISSUE_ROOT), CLAIMDAG_DIR, and the deedar store.
The table is read from $LJOS_CONFIG, from ./ljos.toml, from ~/.config/ljos/ljos.toml, and from ~/.config/ljos/seats/$LJOS_SEAT/ljos.toml.
A reinstall rewrites the hook scripts and leaves that file alone.
examples/ljos.toml shows the table.
MCP tools#
The seat serves these over the Model Context Protocol (MCP).
Writers: ljos_sitting, ljos_finish, ljos_calibrate, ljos_persona, ljos_playbook, ljos_remember, ljos_prefer, ljos_forget, ljos_trust,
ljos_learn, ljos_graded, ljos_island, ljos_deed, ljos_vote, ljos_predict, ljos_rule, ljos_request_approval, ljos_claim,
ljos_release, ljos_complete, ljos_consolidate, ljos_findings (a reader without remember), ljos_bump_plan (a reader with dry_run), ljos_handover, ljos_receive, ljos_send, ljos_read, ljos_reply, ljos_group. Readers: ljos_search (with as_of), ljos_conflicts, ljos_inbox,
ljos_timeline, ljos_digest, ljos_due, ljos_habit (a writer with a value), ljos_evidence, ljos_current, ljos_recall,
ljos_consensus, ljos_brief, ljos_personas, ljos_playbooks, ljos_cards, ljos_policy, ljos_doctor. Resources:
ljos://protocol, ljos://cards/USER.md, ljos://cards/MEMORY.md. Prompts:
start_a_sitting, run_a_panel, check_a_handover. Every tool description opens with
when to call it.
By default the server lists the core set, in this order: ljos_rule,
ljos_request_approval, ljos_prefer, ljos_remember, ljos_search,
ljos_start, ljos_guard_check, ljos_file, ljos_options, ljos_note, ljos_recall,
ljos_sitting, ljos_finish, ljos_claim, ljos_release, ljos_send,
ljos_inbox, ljos_reply, ljos_vote, ljos_consensus and ljos_doctor.
LJOS_MCP_TOOLS=all lists every tool above: the core set first, then the rest
by name, then the persona and playbook tools.
LJOS_MCP_TOOLS also takes names split on commas or spaces, with or
without the ljos_ prefix, where core stands for the core set. A call
to a tool the server does not list is refused with the shell command to
run instead. A tool that fails answers with isError set and the reason
as text, so the model reads it as a tool result.
Environment#
Variable |
Read by |
|---|---|
|
the pack client; unset, |
|
the pack workspace; unset, |
|
override for who is sitting. Unset, the seat is the MCP client that connected, a runner’s own seat variable, or the login user. The process tree is not a seat. The holder is that seat tagged with the conversation’s session. When |
|
the tracker |
|
the deed store; the key signs handovers, and |
|
the claim graph |
|
the server’s cards directory |
|
the tools |
|
path to |
|
summon socket; else |
|
|
|
seconds the tracker push after a claim or finish waits; 5 when unset |
|
seconds |
|
|
|
the tracker checkout user rules sync through; unset, |
|
milliseconds each user-rule sync may take; 4000 after a write, 1500 at session start, 8000 for |
|
|
|
where |
|
the one |
|
the runner a decision panel’s members run on, through its |
|
how many decision-panel members start at once; unset, the machine’s parallelism clamped to 4; |
|
where the Jev state, cache and runtime key live (see Jev settings); |
~/.config/ljos/env is loaded for the keys above that the shell leaves unset.
A temporary HOME or working directory#
ljos treats a HOME under /tmp, /var/tmp, /private/tmp, /dev/shm or $TMPDIR as a test sandbox. There the hook’s command check is off, and the memory writer does not start on its own either. A memory command then says so and names packset ensure, which starts the writer by hand. A working directory under one of those, with a HOME outside them, is not a sandbox: cd /tmp && rm -rf ~/proj is judged. LJOS_GUARD=on keeps the check on under a temporary HOME, which is how a test suite exercises it; LJOS_OFF still switches it off. ljos doctor shows a guard row marked warn whenever the check is off, from LJOS_OFF or from a temporary HOME, the session start line says command guard off with the reason, and install.sh says so at the end of an install into a temporary HOME.
A runner’s table may also name start and resume, the argv that open
a session in a persona’s home and resume the latest one there; a
persona whose --runner is that table’s name reasons in that session
(ljos ask, vote --jev). hooks_named names the key a hooks file of
named hooks takes the seat’s hooks under, each command told its event
with ljos hook --event. A steps runner reads a permission answer that omits
decision as a deny. The seat answers allow when no rule matches.
The pane script resumes a runner that exits non-zero, at most three times
a minute, and leaves one that exits 0. agents names the directory the
runner reads agent definitions from: ~/.grok/agents, ~/.claude/agents
or ~/.cursor/agents. headless is the argv a decision panel’s member
runs, with {prompt_file}, {prompt}, {cwd} and {persona} filled;
when it is empty, the member runs grok with the seat’s fallback flags.
hooks_format = "cursor" writes hooks in Cursor’s flat shape: version
1, and one {command, timeout} entry an event. The seat registers the
permission check, the prompt, both tool results, compaction, stop and session end
there. Events ~/.claude/settings.json already runs stay in that file.
beforeShellExecution, preToolUse, beforeMCPExecution,
beforeReadFile and postToolUseFailure are still written here, since
Cursor runs that file’s hooks too and Claude’s file does not name these.
If one of these hooks fails, the command runs. ljos doctor counts the
hook installed when those five are present and Claude’s file carries
the seat hook, or when Cursor’s file carries every event.
The hook reads a Cursor call off cursor_version in its stdin and answers
in Cursor’s fields: permission on a permission check, with user_message and
agent_message; additional_context on a tool result; followup_message
on a held stop. It still answers allow when there is no verdict, since
Cursor blocks the command when the answer to a permission check is not JSON. An ask on
preToolUse, which Cursor does not enforce, becomes a deny.
The IDE and the agent CLI read the same ~/.cursor/mcp.json.
A table with shell = true registers no server and no hook.
ljos onboard --harness grokbot writes the skill and an env file beside
harnesses.toml. Source that file. LJOS_SEAT is the runner’s name,
because a shell-only agent has no MCP client, so the env file
names its seat. --skills DIR is where the skill goes.
ljos hook --prompt reads stdin as the prompt.
ljos hook --fail-on-deny and ljos policy --fail-on-deny
exit 1 when the answer is a deny. Without the flag the
exit stays 0.
[[tool]] tables in the same file declare the tools a runner lives in.
Two come as shapes, herdr and tmux. A table of the same name replaces one.
Every verb but pane_pointer is an argv or a list of argvs, with
{name}, {label}, {session}, {home}, {script}, {script_q},
{pane} and {line} filled. A refusal carries the tool’s own words.
Verb |
What it does |
|---|---|
|
exits 0 when the tool answers |
|
ways to open the persona’s pane; the first that exits 0 wins |
|
a JSON pointer to the pane id in |
|
types the pane script into a pane that starts as a shell |
|
restarts the script in a pane whose runner exited |
|
hands the runner a line |
|
types the line raw when |
|
exits 0 while the pane is there |
|
exits 0 once the runner takes input, retried for |
A runner’s table in harnesses.toml may name hooks, a JSON settings
file of the shape
{"hooks": {"<Event>": [{"matcher": "...", "hooks": [{"type": "command", "command": "..."}]}]}}.
onboard merges ljos hook into it on the prompt and session-end events
(matcher *) by default, or on the events the table’s hook_events lists
(PreToolUse takes the matcher Bash|Edit|Write|MultiEdit|NotebookEdit|apply_patch,
and a second group ^mcp__.+__ljos_.+$ for the seat’s MCP tools, and a
third group for a vissue write tool),
once each, and drops it from events no longer listed. A hook that fails, times out or cannot
start lets the action run, and ljos logs a warning. On SessionEnd the
hook fires the memories it injected during the session together and clears
the session’s record. The hook reads the runner’s JSON on stdin
(hook_event_name, tool_input.command, prompt) and answers
{"hookSpecificOutput": {"hookEventName": ..., "additionalContext": ...}},
or nothing when the pack has nothing on the cue. On PreToolUse a TCB
or pack deny is permissionDecision deny and blocks. Plain text on
stdin is an argv line and answered in plain lines.
Event |
The hook does |
|---|---|
|
answers with the memories the prompt activates, preferences first, each once between compactions; with Jev on, asks which claims bear on the prompt and whether it corrects the agent or puts a choice |
|
fires the memories injected during the session together and clears the session’s record |
|
emits the held prompt note once, on the first tool result, for a runner that discards prompt-hook stdout (grok sends this event) |
|
searches the pack on the failed command and its error and answers with up to three standing claims that two scorers named and that share at least two content words with the cue, each once between compactions |
|
fires up to eight of the memories injected so far, drops all of them from the session’s record so a later prompt can bring them back, and keeps a note naming the issue the conversation still has claimed, for the next delivery |
|
after a compaction ( |
|
applies the TCB and pack rules to the command (matcher |
|
speaks only when the turn ran no tool; blocks one turn that was asked for a decision and cast no ballot; with no issue held, blocks one turn that used tools and never touched the seat; with Jev on, audits the turn once from the runner’s transcript; then holds one turn for unread interrupt mail ( |
|
names the issue the subagent’s conversation has claimed and asks for the ballot on a decision; with Jev on, audits the turn once |
The hook’s book names the runner that owns its sitting, and the next
session or MCP call finishes the sitting once that process is gone. The
owner is the nearest harness ljos knows by name above the hook, else the
nearest program that is neither a shell nor a wrapper, else the outermost
shell. The walk stops below a terminal, a multiplexer or init, none of
which owns a sitting. The book keeps the owner’s pid and start time, so a
pid used again by another process is gone. The walk reads /proc and
holds on Linux only. On macOS and other systems the hook’s parent owns
the sitting: a runner starts the hook through sh -c, which exits with
it, so there the next MCP call finishes the hook’s sitting, and
ljos_start files a new claim instead of keeping the one a Write took.
What a rule reads#
A rule is tried on every command a line runs, the first and the rest. The
line is split on &&, ||, ;, |, & and new lines
outside quotes, and each piece is read again inside: the bodies of
$(...), backticks, <(...), >(...) and subshells; the script of
sh -c, eval, env -S, trap, alias, su -c, script -c,
flock -c and nix-shell --run; a here-string or here-document fed to a
shell, and echo or printf piped into one; the commands of find -exec; and the line ssh runs on its host. Leading NAME=value words,
wrappers (sudo, doas, env, time, nohup, exec, command,
nice, ionice, timeout, setsid, stdbuf, xargs, flock,
chronic, unbuffer, watch), case patterns and function
definitions come off, and quotes, $'...' and escapes come off each
word, so git 'push', $'git' push and git -c alias.p=push p read as
git push. A line nested more than eight deep is refused whole.
A rule reads the line as written. It does not see a command whose name
comes from a variable ($g push), a script run from a file, an alias or
git alias defined in an earlier call, a recipe in a Makefile or justfile,
or a call made from inside another language (python -c, perl -e). A
rule stops an agent’s mistakes; it is not a sandbox. For a push that
must never happen, protect the branch on the forge.
Checks ljos keeps#
policyd answers every command with one of four answers. ljos keeps two checks of its own, because only ljos knows the tracker and the sitting.
Answer |
What happens |
|---|---|
|
the command runs |
|
the runner’s permission prompt, or |
|
the command does not run, and the reason says why |
|
coming in 0.35.0: the command runs in a fixed form |
On the wire, a rewrite is one line of three tab-separated fields: allow, rewrite and the new command.
The new line runs only when the guard also allows it. The hook hands it back as the updated input for
Claude Code, Codex, Grok Build and Cursor’s preToolUse. A runner without that field gets a refusal that
names the new command, so the old line never runs. The first rewrite runs the command through uv run.
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.
If ljos-policyd is missing, slow or crashes, the command runs and ljos logs a warning.
Nothing in ljos turns a fault into a refusal.
The tracker write check refuses a command, or a file tool, that writes the tracker’s files by hand. Ticket text is data. A ticket body that names a path or a command is never read as one.
The push check asks, and never denies, on a push onto a protected branch when the ticket of
the current sitting has no settled consensus. It covers develop and main unless
consensus_branches in ljos.toml names others. With one active seat in vissue.toml, that seat’s own vote
settles it. This solo mode is coming in 0.35.0. With two or more seats, the normal quorum applies.
Every other push rule is policyd’s.
Push |
Answer |
|---|---|
an ordinary push of a branch, a tag, or a lease push to a branch that is not protected |
runs |
a lease push or a force push onto a protected branch |
asks |
a push onto a protected branch, with no settled consensus on the sitting’s ticket |
asks |
deleting a remote branch, or rewriting published history |
asks or denies, by policyd’s table |
A cloud seat has one push rule. It may push the branches named in its ljos.toml and develop, with no force
and no delete. Opening or merging a pull request is not refused. Branch protection on the remote does the rest.
The remote is the one the command names, else the branch’s upstream,
else origin, read in the directory a cd or git -C before the push
names.
Sync files#
File |
Contents |
|---|---|
|
the repository’s scope and its age recipients; a |
|
one sealed log per machine: a base |
|
one file of user rules per machine, sealed to the scope’s recipients ( |
Jev settings#
~/.config/ljos/jev.toml. The file is absent or enabled = false: Jev is off.
Key |
Default |
Meaning |
|---|---|---|
|
|
turn Jev on; the call sends the prompt and claims off the machine |
|
|
which judge answers: |
|
unset |
the argv of the |
|
unset |
file holding the key, one line, mode 0600 |
|
unset |
command that prints the key on its first line, as an argv list; asked once per login |
|
|
model name; |
|
2000 |
how long the hook waits for the answer |
|
|
API endpoint; |
|
4.0 |
the month’s spend in US dollars past which the hook stops asking |
|
4 |
a prompt with fewer words is skipped |
|
2 |
fewer candidate claims than this is skipped |
|
0.042 on |
US dollars per million input tokens, for an API whose answer carries no cost. A |
|
0.5 |
probability at which a claim bears on the prompt |
|
0.5 |
probability at which the prompt counts as a correction or a choice |
|
0.8 |
a persona ballot under this confidence goes to a subagent |
|
7 |
days an identical request is answered from the cache; 0 turns it off |
|
unset |
environment variable holding the key |
|
none |
a further judge: |
|
every decision to |
which judges answer each decision: |
Path |
Contents |
|---|---|
|
the month’s calls and spend |
|
every Jev answer: claim ids, a prompt hash, latency, timeout or failure, backend and model. |
|
the answer cache: a hash of the request and the reply, directory mode 0700, file mode 0600. The prompt and the claim text are not stored |
|
the default judge’s key from |
The four routes are prompt, ballot, audit and review. backend = jev posts to endpoint (TypeSafe’s API, or OpenRouter at https://openrouter.ai/api/alpha/decisions with model typesafe/jev-1.13). backend = chat posts one JSON-mode completion to endpoint plus /chat/completions and must set usd_per_mtok_in. With backend = command, the request stays on the machine. TypeSafe’s terms say they will not train on customer data. They keep a perpetual licence to derive telemetry from inputs. Zero data retention needs an enterprise agreement and is off by default. This seat does not train on Jev’s answers. A judge ballot is a trust row with to judge:MODEL, and learn updates it.
The learning rules#
learn adds the outcome to each voter’s record of hits and misses. A
voter’s accuracy is read off its record and shrunk toward the panel’s
pooled accuracy by empirical Bayes (Efron and Morris,
doi:10.1080/01621459.1975.10479864). The prior weakens as the records
spread beyond binomial noise; the first outcome leaves every voter alike,
and a long record keeps the differences it shows.
The weight every other voter gives a voter is the log odds of that
accuracy, Nitzan and Paroush’s weight (doi:10.2307/2526438); the best
voter weighs one and none weighs less than 0.01. A panel with nobody above
chance weighs every voter 0.01. Rows are written complete, so the settle
sees the whole graph, and each carries its voter’s record. learn also
writes an outcome atom, the issue and the option it closed on.
--rule hedge runs the multiplicative weights rule of Hedge
(doi:10.1006/jcss.1997.1504) instead. Every voter whose ballot the outcome
refuted shrinks in every other voter’s row by beta (default 0.5),
floored at 0.01. A vindicated voter keeps its weight, and a missing row
starts at 1.
consensus reads each issue’s ballots beside the option it closed on once
five issues have an outcome, and asks ljos-consensus correlation how far
the voters share their mistakes. A pair counts only when its correlation
passes the one-sided test of independence at five percent. A voter’s
inbound weight in the settle is multiplied by
1 / (1 + the sum of its counted correlations) (settle --discount-of);
exact clones count as one voice under it if their record has a hit and a
miss, and a looser cluster as more than one. A line before the settle
names the outcomes read, the independent voices the panel has and each
voter discounted. Nothing is passed or printed when no pair counts.
A row carries the domains it is scoped to. An unscoped row applies to
every issue; a scoped row applies when one of its domains is a word of the
issue’s title. learn writes its rows scoped to the entities of the island
the issue’s title activates, eight at most, so a voter refuted on one topic
keeps its standing on the rest; a scoped learn starts from the unscoped row
when it has none of its own and leaves the unscoped row standing. When
both apply, the scoped row is the one the settle sees. --share S then lifts
every row by S of its distance to one, the fixed-share rule of Herbster
and Warmuth (doi:10.1023/A:1007424614876). A voter refuted long ago is not
held down forever, and the best voter can change. The default is zero,
plain Hedge. An outcome also
moves the anchor of every persona it refuted toward one by 1 - beta of
the gap: a persona that keeps being wrong listens more.
A persona is a persona atom: a name, an anchor in [0, 1], a view, and the
domains it speaks to. consensus passes the anchor of each persona on the
ballot to both settles as --susceptibility-of, and gives every other voter
on that ballot, the seat included, an anchor of 0.5. A ballot with no
persona settles as plain DeGroot. At 1 a persona keeps none of its
ballot. An anchor below 0.5 settles at 0.5: under Friedkin-Johnsen with
equal trust, the share of its ballot a voter keeps is its weight, and a
stubborn voter pulls the rest toward it (Acemoglu, Como, Fagnani and
Ozdaglar, doi:10.1287/moor.1120.0570). A persona at 0 would count as two
seats and tie a 2-to-1 vote on its own. Trust rows still weigh voters as
they say. Before the default, the seat sat at 1, so any anchored persona
took the whole settle. The settle prints in words: the shares, who leads
and by how much, polarization, each voter’s influence, the share of its
own ballot each voter keeps. Above the settle, consensus prints what the
seat ballots decide: the option every seat ballot names and the
seat ballots it counted, or why it decides nothing, with the persona and
judge ballots it left out. This is what the push check reads. With one active seat in vissue.toml, that
seat’s own vote settles it. This solo mode is coming in 0.35.0. The settle below is advice and decides no push.
When trust rows exist, the standing beside the settle names the voters on
this issue; the other names in the rows are counted, and --json lists
them.
consensus ID --json prints one JSON document, and nothing else on
stdout. The seat tally stays at the top level under push_cite, with the
keys it already uses (issue, pusher, stands, and choice with
counted, or refusal). The model crate’s settle is under settle.
surprising and reputation are present when those readings ran.
consensus ID --compare also runs vissue consensus, the tracker’s
settle, and that output is under tracker in the same document. The
tracker also runs when ljos-consensus is not installed. Without
--compare, and with the model crate installed, the document has no
tracker key. An issue with no ballots is refused before any settle
runs, in the command and in the MCP tool, and the message says how to
cast one (ljos vote ID --for OPTION, or ljos panel ID).
A vissue with --file-dissent files dissent children only under
that flag; an older one files them on every run.
A ballot’s --confidence is recorded and weighs no settle and no seat
tally. consensus says on how many ballots it was stated. learn ID --outcome OPTION scores it once the outcome is known: Brier and log score
and calibration per voter.
Without an outcome, calibrate estimates each voter’s accuracy from the
project’s issues with two or more ballots, by expectation maximisation
over the items’ hidden answers (Dawid and Skene, doi:10.2307/2346806). It
then writes the log odds of each accuracy as the weight every other voter
gives that voter. The most reliable voter weighs one, and no weight
falls below 0.01.
Crates#
Crate |
Carries |
|---|---|
|
the library and |
|
the MCP server |
|
DeGroot and Friedkin-Johnsen settles, Seldon export (separate repository) |