Skip to content

levain 0.6.0

Choose a tag to compare

@phillipclapham phillipclapham released this 04 Oct 22:28
· 4383 commits to main since this release

Team context: one project's decisions shared across a team of engineers, with git as the wire and a Claude Code hook that puts a recorded decision in front of the agent at the edit it governs.

Added

  • levain team (init, join, record, retire, sync, status, verify, consolidate, export, install, doctor, member add, pack-sync). An orphan levain-ledger branch in the project's repository holds team.toml, one append-only SHA-256-chained JSON-lines file per author per clone (ledger/<author>/<device>.jsonl, so two clones never write one file), and the owner-generated PROJECT.md. Every read and write goes through a private, locked git worktree of that branch under <git common dir>/levain-team/. Entry schema v1: v id ts author agent session type kind mode paths owner words summary reason recheck supersedes refs pack rule_id prev hash; types decision constraint finding question tension ack retire; a ruling needs an owner and the decider's own words. Refused at write: missing fields, unknown supersede targets, text that looks like a secret, field sizes above the limits anneal-memory's import enforces.

  • A Claude Code PreToolUse hook (Edit|Write|MultiEdit|NotebookEdit) and a SessionStart line, wired by levain team init/join/install into .claude/settings.local.json of every working tree of the clone (git-excluded; it names your interpreter). Someone else's ruling denies the first edit per session with the full record as the reason, and the retry is allowed and recorded as an ack; block mode always denies; a tension on the path always denies; practices, findings, questions and your own rulings are context only. The hook never emits allow (that would bypass the user's own permission prompt). Every failure is fail-open with one visible [team] ledger unavailable: ... line. LEVAIN_TEAM_SESSIONSTART_RULINGS=off drops the per-ruling lines from the session-start context so enforcement rests on the edit-time hook.

  • Authority. Only an entry's own author, or the team.toml owner, may supersede or retire it, and replacing a ruling takes a ruling (or a retire) carrying the decider's words; links that break these rules are reported and ignored, whether written through the CLI or pushed by hand. Only the owner creates the ledger, consolidates the canon, seeds pack rules or changes membership.

  • The ledger is read from git history, not the working tree: every line ever added under ledger/ on the levain-ledger ref, in the order it was added, each attributed to the author email of the commit that added it. Removing or rewriting a line changes nothing and is reported; a line added to a file that belongs to someone else (the owner, for pack files) is not enforced and is reported. Reads take no lock and always see a completed commit. If team.toml at the tip does not parse, the newest version that does is used, with a warning. levain team verify also reports team.toml and PROJECT.md changes not made by the owner of the version before them.

  • judgment.toml in a pack: [pack] name/version plus [[rule]] id paths kind owner words reason recheck mode. levain team pack-sync (and init --pack) seeds the rules as entries authored pack:<name>; an upgrade supersedes changed rules and retires dropped ones; a rule the team retired is not resurrected by re-syncing unchanged text.

  • levain team export --jsonl: every verified entry, per file in file order, for anneal-memory's team import (--in-force for the current view). tests/test_team_golden.py pins the canonical-JSON and hash bytes.

  • Optional anneal import. At session start, if the installed anneal-memory ships anneal_memory.team and a store is configured (--anneal-db, or <repo>/.levain/memory.db), the ledger is imported once per ledger tree with --link-authority <owner>; otherwise it is skipped and levain team doctor says which. LEVAIN_TEAM_ANNEAL_IMPORT=off disables it.

  • A forged ledger line is one reported problem, never a failed read. Lines are parsed through one bounded entry point: nesting past 32, lines past 256K characters, stray closers, CPython's integer-digit limit and lone surrogates are each refused for that line alone, its neighbours verify exactly as if it were absent, and the PreToolUse hook therefore cannot be stopped by one forged line on the levain-ledger branch. (A line nested ~200,000 brackets deep otherwise raises RecursionError out of every read, and has run for minutes in json.loads on Windows.) The misfiled-entry filter is linear in a hostile file. Review: three lineages over three rounds, each round's pre-registration in the fan-in log.

Changed

  • Levain now requires anneal-memory 0.9.36 or later (anneal-memory>=0.9.36,<0.10), and 0.9.36 is the version it is tested against: it is the first published release carrying anneal-memory team-import, the other end of levain team export --jsonl. No migration-manifest entry and no schema changed, so existing installs' seeds are unchanged.

Known limits

  • Identity is the author email on each commit, mapped through team.toml. Anyone can set it, so levain detects and reports; it does not authenticate. A teammate who sets their git email to the owner's can act as the owner. Authentication is the git host's job (branch protection, required signed commits); levain does not check signatures.
  • Any member can record a tension on a broad path, which stops every edit there until its author or the owner retires it. That is deliberate (a stop is cheap to lift and expensive to miss) and visible in levain team status.
  • levain team init must be run by the owner named in --owner. A clone whose .git was copied from another machine relinks its ledger worktree to itself, and two clones sharing a device id refuse to rebase rather than drop an entry: run levain team join --new-device in the copy.
  • Bash writes are not intercepted. Two rulings that disagree are not detected; a tension entry is how a team marks one. A nested repository or submodule is governed only by its own ledger.
  • Freshness is a fetch at session start and at most every fetch_interval (default 300 s) from the hook.
  • POSIX only (fcntl). Claude Code started with --bare loads no settings hooks, so the team layer is absent there.
  • levain doctor does not yet check the team layer; levain team doctor does.
  • Needs git 2.31 or later (rev-parse --path-format).
  • The ledger branch is kept linear (levain only rebases and cherry-picks). A merge commit pushed to it by hand is reported, and lines that exist only in a merge resolution are not read.
  • If two clones that share a device id have BOTH committed entries, levain team join --new-device does not move the copy's already-committed entries to its new file: reset the copy's ledger to the remote (git -C .git/levain-team/worktree reset --hard origin/levain-ledger) and record them again.
  • Two pack-sync runs from two different owner clones at the same time can leave two versions of one pack rule in force until the next pack-sync (one run at a time per clone is enforced).
  • If a member's email changes in team.toml, lines they added under the old email stop being enforced until the change is reverted or the entries are recorded again.
  • When a sync must replay local commits after a team.toml/PROJECT.md conflict: in a clone whose .git was copied from another (shared device id) AND that has rerere.enabled with rerere.autoupdate, a local entry can be skipped as "already upstream". Run levain team join --new-device in a copied clone, and do not enable rerere autoupdate for the ledger worktree.
  • In that same replay, commits whose clocks were skewed can replay out of order, and a cherry-pick that fails for an unrelated reason (a stale index.lock) is reported with the wrong cause. The sync stops and nothing is lost; levain team doctor and the worktree at .git/levain-team/worktree show the state.
  • A member with push access who recommits another member's line under their own name makes that line count as theirs, so it is not enforced, and reports it. Part of the trust boundary above.