Release notes
Unreleased
Symbol engine 17: new syntax is not refused
A grammar is older than its language. Lonca's C# grammar, for one, does not
read C# 14's extension blocks, and until now a change with such code in a
Rust, Python, TypeScript, JavaScript, C# or Go file failed the syntax check
as if it were a mistake (#246).
From engine 17 a parse error in any language means that the grammar could
not read the file, as it already did for Java, Kotlin, C, C++, Ruby, PHP and
Swift: the change is not refused, the file merges by its lines, and a
proposal that leaves a file unreadable, or adds one, waits for a person with
the reason unread_file:<path>.
What this costs: a real syntax error in those six languages is no longer
stopped by Lonca itself. Set build and test in [verification] so that
your compiler is the judge. Entries of older engines are recomputed as they
were written.
Languages: what each grammar reads
A new page of the guide, Languages, says for each
language which grammar this build has, which versions' syntax was tried and
what is known not to be read yet, and what a file with such syntax goes
through (#247). The table
on it is written by a test from 210 small samples, so it can not drift from
the build: 29 of them are not read today, among them C# 14's extension
blocks, Java 25's module imports, Kotlin's guard conditions and C++23's
deducing this. None of this is refused (engine 17, above); such a file
merges by its lines until a release brings a newer grammar.
Presence on Linux: FIDO security keys
A human's key can now live on a FIDO security key, so a hardened repository
no longer needs --without-presence on Linux
(#17):
lonca human keygen ayse --security-key # touch the key when it blinks
lonca human rotate --repo demo --security-key
The key signs through OpenSSH (ssh-keygen, 8.2 or later with FIDO
support); Lonca itself only verifies, and carries no USB or FIDO code.
Every signature needs a touch, and one the device made without a touch is
refused. lonca doctor has a presence line: what your key proves and what
the machine could use. Two new kinds of human key, sk-p256 and
sk-ed25519, and log format 28; human.enroll now checks the form of a
key as a rotation does. Older logs replay as they were, and an older
lonca refuses a log that has reached format 28.
The verifier agrees with OpenSSH 10.5's own (ssh-keygen -Y verify) and the
command line is tested against a software stand-in for the device. Signing
with a physical key has not been exercised by the project yet: see
Profiles and the
threat model for what a
security key does and does not show.
A recording of two agents on one file
The README, the home page and the quick start show a
terminal recording: two agents lease different methods of one file, a lease
that is held and a write outside a session's paths are refused, a human
approves, both changes merge and lonca-audit verifies the log. It is
recorded from the released binaries by
docs/demo/demo.sh,
which anyone can run: it uses a home of its own and nothing in it is canned.
The site names the release, and a page for Claude Code
The site said nothing of which release it described, and the install guide's
examples still named 0.2.0. The version now comes from the workspace's
Cargo.toml when the site is built and shows in the navigation bar and on the
home page, and the examples name 0.2.2. A new page,
Lonca with Claude Code, says what each hook event and
MCP tool does for a Claude Code session, how far each observation level goes,
and where the other harnesses stand. The home page and the README have
diagrams of the same.
What the verifier's limits do not cover, said
Verification and the limitations on What Lonca is now say what the per-process limits leave open: a command's total disk in the scratch directory, its memory as a whole and, on macOS, its number of processes. Bounds for these are planned.
A threat model
Threat model gathers in one English page what was spread over the design and the decisions: the assets, who Lonca defends against, what each mode prevents and what it only detects, what the auditor does not catch, the known limits and what is out of scope.
Homebrew 7 asks to trust the tap
Homebrew 7 refuses a formula from a tap that is not its own until it is
trusted. The README, the install guide and the
site now put brew trust --formula selcukgural/lonca/lonca between
brew tap and brew install; an older Homebrew needs no such step.
0.2.2 (2026-11-09)
lonca doctor reads like flutter doctor
The text form was one flat, colourless line per check. It now groups the
checks under headings ([✓] Mode, [✗] Daemon, [!] Hooks, Tools,
Human, Repository <repo>), each marked with its worst check, says what
to do under each problem, and ends with how many categories have issues.
Colour only on a terminal, never with NO_COLOR or TERM=dumb. --json
is unchanged; in text mode the summary line replaces check(s) failed on
stderr, and the exit code is still 1 (#227).
session start into a full directory starts nothing
lonca session start --workspace DIR with a DIR that is not empty was
refused only after the daemon had started the session: the log had an
allow, and status listed an active session whose token nobody held
until its TTL ran out (#223). The directory is now checked first, and
anything that fails after the session started (making the workspace,
writing its token) ends that session again.
Symbol engine 16: a parse within a budget
A tree-sitter grammar can take far more work and memory than a file is long: the fuzzer found a short Kotlin file that took gigabytes, so a proposal holding one could take the daemon down. From engine 16 a parse stops after a budget of work that grows with the file's length (256 steps and one per 4 bytes, at most 8192), counted in parse actions and not in time, so the auditor gets the same answer. A file past it goes by its lines, as one over 2 MiB does: code people write stays far below it. Entries of older engines are recomputed as they were written.
Fuzzing
The parsers and the merge are fuzzed (docs/fuzzing.md): stored log
entries, whole logs as the auditor reads them, daemon requests, hook
events, the symbol engine, the three-way merge, and repository
configuration and policy. Every saved input runs under cargo test. A git
id in a request or a log entry is now refused unless it is 40 lowercase
hex digits, as Lonca writes them. A C or C++ file with a broken scoped name
such as a:::b no longer makes the symbol engine panic: the request that
read it got no answer, and the connection was dropped. Such a name now has
one scope per ::, read from the left.
lonca policy check, and a guide to writing policies
lonca policy check --policy policy.cedar (or --config, or both) tries
a policy before lonca policy set: it decides every entry of the log again
with it and lists the decisions that would change, writing nothing. It also
warns when the new policy would deny an enrolled human a way back, such as
policy.update, unfreeze or approve. The new
Writing policies page lists what a Cedar rule sees
(principals, actions, every context key), what @escalate and
@approvals do, why an evaluation error denies, and examples.
A lost Mac comes back with the recovery key
When the Mac is lost, the daemon's key and your Secure Enclave key go with
it. A repository restored onto a new daemon then accepted only
daemon.recover, signed with an enrolled human's key, so the recovery key
alone could not bring it back. Now lonca human recover does it: on a
restored repository it sends a daemon.recover signed with the recovery
key, which moves the repository onto the new daemon and installs your new
key in one entry (log format 27). Every session ends; the recovery key is
used up. See Recovery.
Short change ids work
lonca pending and lonca log show the first 12 letters of a change id,
but lonca diff, lonca change and lonca advise refused them
("invalid change id"). They now take any 4 or more letters that name one
change; when several changes start with them the answer lists those. The
daemon has a new read method for it, change.resolve.
undo and revert read the whole log
The daemon answers log a page at a time (1000 entries), and lonca undo
and lonca revert read only the first page. On a longer log revert
planned nothing after entry 999 and said "nothing to revert", and
undo of a policy.update past entry 1000 put back the last policy of the
first page instead of the one before it. Both now read every page.
More of the guide
New pages: Concepts, Reviewing agents' work, Troubleshooting and Recovery.
Live runs for this release
Claude Code 2.1 (haiku), as docs/release-checklist.md asks: a session
leased, proposed and integrated after approval, and lonca diff listed no
generated file; two sessions at once on one file, on different symbols, both
integrated; one that stopped without proposing was sent back by the stop
hook, proposed, and left no unproposed work. lonca-audit verify passed on
the log. Not run: Codex 0.167 and OpenCode 2.2. Their free models on
Ollama's cloud are retired and the local one was not used. No harness
profile has changed since 0.2.1; Codex last ran live before 0.2.0 (Codex
0.165), with Claude Code on one file, and both integrated.
0.2.1 (2026-11-09)
Its tests ran on Ubuntu, macOS and in the bubblewrap sandbox; as for 0.2.0, the mutation run did not gate it (docs/decisions.md, "Mutation testing"), and its survivors are fixed in a later 0.2.x.
Auditing a restore on a hardened install
lonca restore ended with Audit it: lonca-audit verify <home>/repos/demo.git, a placeholder, and on a hardened install a store
the developer can not read. Now it prints the store's real path (the
daemon's repo.restore result has a new path), or on a hardened daemon
how to verify the source it was restored from. lonca-audit names
"permission denied" for a path it can not read instead of "does not appear
to be a git repository", and points at verifying a backup bundle. The quick
start says which store path is the cooperative one.
A closed pipe no longer panics the CLI
lonca status | head, lonca log | grep -q ... and the like ended in a
panic (failed printing to stdout: Broken pipe). Now the command finishes,
drops the output no one reads, and exits as it would have (0 on success).
Another write error on stdout (a full disk) is reported and exits 3. The
hook blocks the call (exit 2) when the harness can not read its answer,
unless it runs with --fail-open. See docs/decisions.md, "A closed stdout
ends a command quietly (#207)".
A newer proposal replaces the pending one
A workspace that proposed again while its first proposal waited for a
human had two pending changes, the second holding the first's edits:
approving both landed the work twice, the second time as an empty commit.
Now the newer proposal withdraws the older in the same entry and names it
in its reasons (replaces:<change>). The older reads withdrawn, leaves
status.pending and can not be approved; its pinned leases pass to the
newer. A queued change is left to land. Log format 26; older logs replay as
they were. See docs/decisions.md, "A newer proposal replaces the pending
one (#107)".
Hardened mode, installed for real on Linux
The Linux installer ran for real with sudo (Debian 12, systemd), and what
broke is fixed. A hardened install has an import directory
(/Library/Lonca/import, /var/lib/lonca-import) where you leave a Git
bundle for lonca init --import or lonca restore: before, a hardened
daemon could read neither a project of yours nor a cooperative backup. The
daemon copies the bundle before git reads it (lonca daemon --import-dir).
Running a newer archive's installer is the upgrade: it stops the daemon,
replaces the binaries and starts it again (on Linux the old daemon kept
running before). Linux has uninstall-hardened.sh too. ping says
hardened. A hardened daemon no longer starts with --local-root. macOS has not been installed for real yet. See
docs/decisions.md, "Hardened mode in the field (#10)".
status names the policy's checks
status has policy_checks: the names of the named checks the policy in
force defines (verification.checks), sorted, null when the policy can
not be read. A runner can refuse a check name nobody defined when it is
written, not at the human's yes. Names only; a check's command never
reaches a client. The protocol number stays 1: a field was added.
Crates and Homebrew
Every published crate has a README on crates.io; lonca-client and
lonca-net show it on docs.rs as well, with an example that is compiled
as a test. Homebrew is shown as brew tap selcukgural/lonca, then
brew install lonca, so the short name works for brew upgrade lonca
too. The repository's README is a short front page; what it held is on
the site (backups in the quick start, limitations on "What Lonca is").
0.2.0 (2026-11-08)
The first release. Its tests ran on Ubuntu, macOS and in the bubblewrap sandbox; the mutation run did not gate it (docs/decisions.md, "Mutation testing"), and what it finds afterwards is fixed in 0.2.x.
Releases: prebuilt binaries, crates.io and Homebrew
The first release with prebuilt binaries: lonca and lonca-audit for macOS
(arm64, x86_64) and Linux (x86_64, arm64, static), with lonca-se on macOS,
checksums, a CycloneDX SBOM and a build provenance attestation per archive
(gh attestation verify lonca-<target>.tar.gz --repo selcukgural/lonca).
Install with curl -fsSL https://selcukgural.github.io/lonca/install.sh | sh,
brew install selcukgural/lonca/lonca or cargo install --locked lonca;
every crate of the workspace is on crates.io at the same version. The CLI's
package is now lonca (was lonca-cli), and the minimum Rust is 1.90. Each
archive carries the hardened install scripts. See docs/decisions.md,
"Releases and install paths (#7, #8)".
A client crate for harnesses
lonca-client is what a harness or runner needs to talk to the daemon, and nothing else: the
protocol's request and response types and PROTOCOL, the socket client with the daemon checks
(socket owner, peer uid, the hardened config), framing, an optional timeout (past it the call
fails timed_out and the connection is dropped, since the request may have run), typed errors
that tell rate_limited, busy and closed apart (is_transient), and the human session file
reader, which refuses a link, another user's file or one others can read. It depends on
lonca-core without its Git store, which is now the default feature store. A protocol number
change is a major version of the crate (0.x: minor). lonca no longer uses a session file
others can read, and a login writes an old file back as 0600. See docs/decisions.md, "A
client crate for harnesses (#192)".
Checks a change owes, named per change
[verification] takes named commands, checks = { api = "cargo test --offline -p api" },
which run only for a change that owes them: a human names them when approving
(lonca approve SEQ --also api,e2e, signed in approve, log format 25), or
checks_by_path = { "api/**" = ["api"] } does for a path. They run in the queue's sandbox
after build and test, a failure rejects with verification_failed:<name>, and a check owed
is owed even with required = false. A new version of the change keeps them.
status.changes[], status.pending[] and change.get add checks (asked) and runs
(what the queue will run). See docs/decisions.md, "Checks a change owes, chosen per
change (#191)".
A session whose hooks do not run is marked and tainted
lonca mcp now tells the daemon which tool it runs (mcp.call). On Claude Code, whose
PreToolUse fires for MCP tools too, the hook's event must come first; a call with none
means the harness is not running the session's hooks, and the session is marked
unhooked: logged once, a security event, tainted, unobserved from then on. What it
proposes waits for a person (unhooked_session) and a critical path is refused
(unhooked_critical_path). status.sessions[] adds bound_ms, last_hook_ms and
unhooked, so a runner can stop a session the daemon has not seen. Log format 24. Other
harnesses are checked once a run shows the same (events.before_tool_mcp in a profile).
See docs/decisions.md, "A session whose hooks do not run (#190)".
A clean stop is on the record
status.sessions[].unproposed was null both for a session that stopped or ended with nothing
left and for one that never reported, so a runner could not tell a workspace safe to remove
without a live token. A clean report now reads {"paths": [], "total": 0, "rounds": 0, …}, and
null means nobody said since the session began or last proposed. lonca status lists only
sessions that left work, as before. See docs/decisions.md, "Stopping with unproposed work"
(#189).
Merge: a change already in trunk, and deletions at the top
Symbol engine 15. A change whose every edit trunk already has merges into trunk unchanged; it
used to be a conflict on the file when trunk had also added something at the same place. A
function or method deleted at the top of a file or of a class no longer leaves a blank line
there, which a format check in verification would refuse. Found by the new merge property tests.
Older entries are recomputed with the engine they name. See docs/decisions.md, "Symbol engine
versions", Engine 15.
A delegated lease comes back when the child expires
A lease set handed to a child session with cap.attenuate went back to the parent only when the
child ended. Now it goes back as well when the child's time runs out or it misses its heartbeat,
with the time the set had, if the parent is still there; until now it ended, and another session
could take it first. Log format 23. See docs/decisions.md, "A delegated lease comes back (#40)".
Insert leases reserve names; a new file needs its path
An insert lease item can list the names of the members it adds ("names", lonca lease acquire --kind insert --name throttle). Two inserts into one container that reserve one name
conflict, an addition outside the reserved names is denied with lease_missing, and adding a
name another session reserved is denied with name_reserved, whatever lease covers it. An insert
without names otherwise works as before; lease_needed now offers inserts with the names the
change adds. A change that adds a file now needs an edit lease on its path#<file>, so two
agents can not create one path; until now a new file needed no lease. A path trunk created
after the change's base is created_since_base: sync first. The agent rules say all of this.
Additions at one place are merged in queue order, the last integrated nearest to its neighbour.
Log format 22. See docs/decisions.md, "Insert leases (#45)".
Shared leases
A new lease kind, shared: several agents can hold one symbol at once, where a signed
policy has [leases] shared = true, which needs verification.required = true and a build
or test command. edit and rename stay exclusive and conflict with it. It covers a body,
not a signature. When two holders change one function, the merge (symbol engine 14) joins
their lines; lines both changed are conflicted, naming the function. Log format 21. See
docs/guide/verification.md, "Shared leases".
A quota on verification
[quota] session_seconds and day_seconds cap the time verification may take for a session
(with the sessions made from it or continuing it) and for the repository in a UTC day. Past
either, a proposal is denied with verification_quota:session or :day, and what the session
queued before is rejected without a run. Both are 0, off, by default. An agent's
change.withdraw of a change whose checks are running is refused with verifying. duration_ms in the
evidence now covers every attempt of a retried check. status shows verification_ms per
session and verification_day_ms. See docs/guide/verification.md, "Quota".
Nothing of a verification outlives it
A build or test command now runs as a session whose process group is killed when it ends, however it ends, so in a sandbox a test can no longer leave a server running, and a background process that keeps the output open no longer holds the queue. A check out of time is an error with evidence and the next change goes on. The log keeps the last 64 KiB of stdout and stderr as they were written. The program a command runs directly can not make a process group of its own.
Each process of a command now has limits, in existing repositories too, since a lonca.toml
without the keys takes the defaults: limit_processes 16384 (Linux), limit_file_bytes 8 GiB,
limit_open_files 4096. limit_cpu_seconds and limit_address_bytes (Linux) are off unless set.
path sets the commands' PATH. See docs/guide/verification.md, "Limits".
Limits on the socket
One client can no longer tie the daemon up. Connections are capped (the longest-quiet one makes
room), a quiet connection closes after 15 minutes, and a request line has 60 seconds to arrive. A
request line is at most 64 KiB, or 96 MiB for the methods that carry files, and a long line must
name method and token before params. A session and those made from it get [sessions] ops_per_minute, 600 by default; past it requests are rate_limited and the log does not grow.
queue.run now only wakes the worker, log answers at most 1 000 entries, and ping counts
what was refused. See docs/protocol.md.
A level reads the proxy's port and every hook
A harness table can now say is = "proxy_port": the value must be the port the daemon's
own proxy listens on, and with no proxy running the check does not hold. The built-in
Claude Code table uses it for httpProxyPort, so the managed settings and --proxy must
agree; the hardened installers already write the same port to both. Every Lonca hook in
every list must now have its program in root-owned directories. ping returns proxy.
A session names its harness (log format 20)
session start --harness <name> now puts the name in what you sign, and an observation
level is read only for that harness. A session started without it is unobserved on any
machine, so a hardened setup must add the flag; --no-setup names the harness and writes
nothing in the workspace. Once set, a level can not rise for the rest of the session.
status lists each session's harness.
Batches in the merge queue (log format 17)
[queue] batch_size in a signed policy, default 1, verifies the changes at the head of the queue
as one and bisects a failure: change.integrate_batch integrates them with one entry, and trunk
moves straight to the verified tip. A tree that passed is not verified again while the daemon runs.
Seven more languages (symbol engine 10)
Java, Kotlin, C, C++, Ruby, PHP and Swift have symbols: leases, merges and facts by method, and
field where the language has them. What a language means is now a table,
crates/lonca-symbols/src/languages.toml. A file one of these grammars can not read gets what a
file with no grammar gets: a line merge, listed under unparsed_files, and no syntax failure.
A proposal that leaves a file unreadable that read before, or adds one, escalates (unread_files,
log format 16): facts can not see into it after that.
Files no parser reads can be shared (symbol engine 8, log format 15)
With [leases] unparsed_files = "optimistic" in a signed policy, an edit of a text file
with no grammar and no sections (Java, Kotlin, C, Ruby, shell, ...) needs no lease: the
line merge at integration decides, and a real overlap is conflicted. The default stays
"file", one <file> lease and one agent at a time. status shows the setting, and
preview and lease_needed list such files under no_lease.
Only an edit of lines goes without a lease: a change of mode or of a link, text with a NUL and a
change that takes away more than half of a file keep the file's lease (symbol engine 9). Lines
now merge by their own rules, and a change among equal lines is a conflict rather than a guess.
lonca status and lonca lease needed say what the setting does.
After a conflict lonca sync brings in the rest of trunk and leaves both sides in the file
between <<<<<<< local and >>>>>>> trunk, a symbol whole or by lines where there is no symbol.
propose refuses while any marker stands in a changed file, and a later sync reports the conflict
again. A conflicting symbol used to keep the local text with nothing to show what trunk did to it.
Renames are tracked (symbol engine 7, log format 14)
Facts now have renames: [[old, new]]: a deleted and an added symbol in one list whose
code is the same with the name left out, or at least 80% alike. A rename lease on the
old name is all such a change needs. Once it lands, a lease on the old name and one on the
new name conflict, so a session on an older base and one on the trunk can not hold one
symbol under two names, also after an overload's parameters changed. A proposal from an
older base is checked against leases by either name. The merge keeps a renamed symbol in
its place. deletes and adds stay, so claims do not change.
Overloads keep their identity (symbol engine 6, log format 13)
Symbols that share a name are now told apart by what they take, not by their place
in the file: Client.Charge(int,int), P.x[setter], V.v[get],
impl:S.From(u8), instead of Charge~2. An overload added above another renames
nothing, so nobody is told to lease a symbol they did not touch. A symbol alone with
its name has no key. Exact duplicates such as Go's init() keep ~2.
path#Name now stands for every symbol of that name, in leases and scopes: a lease
on Client.Charge conflicts with one on Client.Charge(int), so two sessions can
not hold one overload under two names. A ; behind a TypeScript class member belongs
to the member, so adding a field is an add, not an edit of <def>.
For clients: symbol ids can now hold (, ), [, ], , and spaces: quote them
in a shell, and take them from symbols or preview instead of building them.
--edits and the like split on commas outside brackets only. Entries already in a
log keep their recorded engine and meaning.
More of a file can be leased on its own (symbol engine 5)
Each spec in a Go const (...), var (...) or type (...) group is now a symbol,
and so is each declarator of a C# int a, b;, what stands in a Rust extern block,
a Rust macro call at item level (m!), JavaScript exports.x =,
module.exports.x = and the methods of module.exports = { … }, and functions in a
Python top-level if or try that holds only definitions. Two agents can now work
in one such group at once. A TypeScript method's decorators belong to the method.
The members of a second partial class C in a file are C.x, not C~2.x.
A comment with a blank line under it no longer belongs to the next symbol: it is code
of the file or the type, so editing a licence or a section header needs <module>
(or Type.<def>), not a lease on an unrelated symbol. Adding a method to a Python
class with one method is now an add, not an edit of the class. New symbols stay in
the group, block or namespace they were written in when merged. Entries already in
a log keep their recorded engine.
A wide change gets its turn, and every lease says why (log format 12)
A waiting lease.interest now goes first: for leases.interest_block_ms
(default 10 minutes) other sessions can not take a new lease that conflicts with
it, so small leases do not keep a change to a whole type or file waiting while
what stands in its way finishes. The denial is lease_wanted:<session>, and it
says who waits, and why; lease_needed reports it too. The time counts per held
set, however many sessions wait. After it the interest stays in line without
stopping anyone.
lease.acquire and lease.interest now need an intent; a blank one is denied
intent_required, and lonca lease acquire and lonca lease interest need
--intent. A client of its own that sent a blank intent has to send one now.
For a wide change, a rename or a delete, the rules ask why it needs that much and
for the issue number. The rules now also say to read status before leasing.
Agents can reach each other
A message sent with negotiate now reaches the agent it is for. It arrives with
the answer of any lonca mcp tool, and on Claude Code, Codex and Gemini CLI after
any tool call, while that agent is editing. events carries what the message
says, which it did not before. A lease denial says in a sentence who is on it and
to settle it between you (advice). events from next now finds the approval of
a request older than from. A runner of its own can take the same news from the
new inbox method. A profile of your own in [harnesses.<name>] needs
tool_context to get the after-tool way in.
Code that changes places among symbols is a change (symbol engine 4)
A statement moved past a function or a method now shows in the facts, as an
edit of <module> or of Type.<def>, and takes that lease. The merge treats it
like a moved symbol: it goes to the line merge, which brings it in or reports a
conflict. Copy claims from preview as before.
A type's fields and header can be leased apart from its methods (#41)
path#Type.<def> is a type's own text: a struct's fields, a class header, its
decorators and class-level statements, and for Rust every impl header. One
agent can hold it while others edit methods and add new ones, in Rust, Go,
Python, TypeScript and C#, and all of them integrate, in whatever order they
reach the queue. Facts now report Type.<def> where they said Type, and an
impl that is taken out as an edit of Type plus deletes of its members
(symbol engine 3), so copy claims from preview as before. A lease on Type
still covers all of it, and old logs audit as they did. symbols shows the id
as def. A lease on impl:Type no longer covers what facts report for that
block: lease Type.<def>.
A workspace holds regular files
A symlink at a tracked path, or on the way to one, now stops lonca diff,
propose and sync with an error naming it. Replace it with a file, or restore
the directory, and run it again. Paths in .lonca/workspace.json must stay
inside the workspace.
An agent's workspace is a git repository (#111)
session start now gives the workspace a .git with one commit of its base, so
git status and git diff work and codex exec no longer needs
--skip-git-repo-check. lonca sync moves it with the base. A commit there
stays there: only propose delivers, and the rules say so. Lonca writes .git
itself and never runs git on it, since the agent can change its config.
--no-git keeps a plain directory; a person's workspace stays plain. lonca fetch and propose --commit now refuse to run inside a workspace.
A critical path can wait for a person instead of being refused (#103)
An unobserved or tainted session was denied any change to [paths] critical,
with no way to ask anyone, so a cooperative worker could not add a dependency.
[observation] unobserved_critical and [taint] critical take deny (the
default) or escalate, separately, through a signed policy update. A policy
that uses them can not be read by an older release.
An agent that stops with unproposed work is told so (#121)
An agent could edit, say "done" and stop, with the work left in its workspace.
Now a harness's stop event sends it back with what is unproposed and what to
do next, up to [sessions] stop_rounds times (3). After that status lists
the session under unproposed work, so a runner can tell "stopped" from
"done". lonca session end records the same for a harness with no stop event.
Claude Code, Codex, Gemini CLI and OpenCode have it, through lonca harness setup; for your own profile, events.stop and stop_deny.
Log format 11: this release can read older logs, older releases refuse a log it has written to.
OpenCode, and fixing a harness before Lonca does
OpenCode joins the built-in profiles, tested with 2.2. It has no command hooks,
so setup writes a plugin that calls lonca hook. lonca harness show <name>
prints a built-in profile as a table for lonca.toml, where it replaces the
built-in one when a harness changes before Lonca has.
Rules and setup for every harness (#110)
lonca harness setup <name> and session start --harness <name> give a
harness the rules, the MCP server and the hooks, from its profile; --print
writes nothing and shows the flags. lonca rules prints the rules, and MCP
instructions now carry all of them. MCP tools have annotations. A tool that
changes something refuses an argument it does not take, where before it ignored
it; a tool that only reads runs and warns. A denied propose says what was
claimed and what Lonca computed.
Hooks for any harness (#109)
lonca hook --harness <name> reads a harness's events and tools from a
profile. Claude Code, Codex and Gemini CLI come built in; [harnesses.<name>]
in lonca.toml adds another or replaces one, with no new Lonca
(docs/guide/runners.md, "Any other harness"). A hook event the harness does
not have now exits 2. Tools Lonca does not know taint the session by default:
if a runner uses one that reads nothing from outside, list it in
[taint] safe_tools or set unknown_tools = false with a signed policy
update. .gemini/** joins the default paths.critical; existing repositories
keep their policy until they update it.
Symbol engine 2: code is compared exactly
Changes that engine 1 did not see outside symbols, inside a class's own text
or in the order of symbols and imports are now facts: they need a lease, a
claim and a merge like any other edit. A runner that claimed nothing for such
a file gets claim_mismatch; facts.preview, lease_needed and
propose --accept-facts say what to claim. Moving a function reports the
symbols that changed place. Old log entries are recomputed with the engine
they recorded.
Hardened installs: run the installer again (#52)
Clients now check the daemon's uid against a root-owned daemon.toml. A
hardened install from before this change has no such file, so its clients
expect their own uid and refuse the daemon until the installer has run again.
Markdown, TOML, JSON and YAML have sections (#46)
From symbol engine 1 a change to one of these files is a change to its
sections (README.md#Install.Linux, Cargo.toml#dependencies.serde), not to
path#<file>. A runner that claims path#<file> for such a file gets
claim_mismatch; facts.preview, lease_needed and propose --accept-facts
say what to claim and lease. A lease on path#<file> still covers every
section. Old log entries are recomputed with the engine they recorded.
Existing repositories: new critical paths (#55)
The default paths.critical now covers agent instruction and tool
configuration files: CLAUDE.md, AGENTS.md, GEMINI.md, .mcp.json,
.claude/**, .cursor/**, .codex/**, .cargo/config.toml, .envrc,
.vscode/**, .devcontainer/**, Dockerfile, .imece/** and more; the list
is in crates/lonca-policy/src/config.rs.
This applies to new repositories. An existing repository keeps the
lonca.toml on its refs/lonca/policy, which changes only through a signed
policy.update, so until you update it, agents can still change these files
without a human. The same goes for a lonca.toml that does not set
critical. To adopt the list, merge it into your lonca.toml and run:
lonca policy show --repo NAME # the current lonca.toml and policy.cedar
lonca policy set --repo NAME --config lonca.toml --policy policy.cedar
The reasons are in docs/decisions.md, "Critical paths: what instructs an
agent".