Repository navigation
Sync is delta-proportional
A swarm of agents working on jjj itself found and fixed the two paths that made
sync cost scale with how much history exists rather than how much changed.
-
Fetch no longer pays a cold start every time.
last_synced_revadvanced
only on push, so a clone that only ever fetches — a review-only agent, or any
pod between pushes — stayed atNoneforever and re-derived the whole corpus
on every fetch. Measured on a 25,000-entity corpus:warm_delta_fetchfor 100
changed files goes from 6.9s to 0.88s. -
Push validates and copies incrementally. Validation reloaded every entity
file into SQLite and the sync workspace rewrote every.md, on every push,
however small the delta. Both now do work proportional to what changed:
delta_pushgoes from 12.5s to 3.6s. A first push is faster too, 18.3s to
13.6s, because it routes through the bulk path rather than building the search
index a row at a time.The tempting shortcut here is wrong and was rejected twice: skipping
validation when the cache looks clean, or keying the skip on(mtime, size),
both let a stale cache publish a dangling reference or a conflict-marked body
to every clone. Timestamp-preserving copies (rsync -t,cp -p,tar)
defeat the second by design. The cache is keyed on content.
Review works between clones
-
jjj solution submitpublishes the code. Attaching recorded a change id
but never shared the commit, so a reviewer who fetched saw a solution pointing
at a revision that did not exist for them. Submit now pushes a
review-s-<id>branch, and reviewers can read the actual diff. -
jjj solution lgtm --approvefinishes a review in one step. Signing off
did not approve, and a reviewer who stopped there left the work stranded —
eight reviewed solutions and nothing merged, in one trial. It still refuses
while any critique is open.
Every entity body is writable without an editor
problem, solution, critique and milestone creation all take --body
(and --body - to read stdin). The only way to write a description, an
approach or an argument used to be an interactive $EDITOR, which a scripted
or headless caller does not have — so arguments ended up in titles.
Fixed
-
Review branches no longer collide with metadata bookmarks. They were named
jjj-s-<id>, which matches thejjj*glob that finds metadata bookmarks, so
a solution's code commit was merged into the metadata commit: the sync
commit picked up the whole source tree, conflicted, and jj refused to push it.
84% of metadata pushes failed this way in one trial, while the remote stayed
perfectly clean — the contamination existed only in the commit being built. -
State transitions are scriptable.
critique address|validate|dismissand
solution lgtmaccept--jsonlikenew,listandshowalready did, and
emit the updated entity so a caller need not follow up withshow. -
problem dissolveaccepts--rationale, the spelling every other command
that records a decision uses. -
The MSRV job now tests the MSRV.
rust-toolchain.tomloverrides whatever
rustup is told to default to, so the job installed the declared MSRV and then
built with something else. It has proved nothing for as long as it has
existed; the declared 1.88 is, as it happens, true.
Added
-
jjj solution lgtmworks for pull-based review. It previously required a
review critique already assigned to you, which only fits a push model — an
author names a reviewer, the reviewer signs off. Teams and fleets that pull
from a queue instead were stuck: 49 of 92 sign-offs failed in one trial, and
the error advised filing a critique against work the reviewer believed
correct, filling the objection record with fake objections. Signing off on
self-selected work now records the review in one step, with--rationale
carrying the evidence.Two guards come with it: only submitted work can be signed off, and an
author can never sign off their own solution — making self-selected review
easy must not make self-approval easy, since that gate is what stops a fleet
approving its own homework. -
CLI consistency:
--mine,events --user,lgtm --rationale. A
nine-agent trial produced 274 failing invocations, and roughly 150 were agents
calling flags that did not exist but plausibly should have —solution list --mine76 times, becausenext --mineandcritique list --mineboth do.
Agents probe an API far more systematically than people, and they had inferred
the consistent surface jjj lacked. Now:--mineonproblem list,solution listandevents(it already existed
onnext,statusandcritique list)events --user <actor>— "what did agent-03 do?" is the first question
anyone asks of a swarm's log, and it had no answersolution lgtm --rationale "..."— a sign-off asserts the work is correct,
so it should carry the evidence, asapprovealready does
-
jjj nextspreads a fleet instead of stampeding it. Category, priority and
age are identical for every agent, so a strictly-ordered queue handed all of
them the same head of the list and they all took it — measured at nine agents,
twenty-three claim attempts, three distinct items out of eight. Within a
priority band, work is now ordered by a hash of the actor and the entity id:
each actor gets a stable but different order, so they spread out without
coordinating, and an agent does not reshuffle between turns. Priority still
decides what matters; strict oldest-first within a band is deliberately given
up, which for one worker is cosmetic and for a fleet is the difference between
parallel and serial. -
Claims are leases (design decision 15).
jjj next --claimnow records
claimed_at, andjjj nextno longer offers work another agent is actively
holding. Two problems this solves, both measured in the swarm trial:- Every agent was shown the same top item, so a fleet starting together all
claimed one problem — four of four, on the first run. - A claim was permanent, so an agent that died mid-task held its work forever.
Over a long run that is the expected failure, not a rare one.
A lapsed lease returns the item to the pool and the reclaim is reported. Your
own claim refreshes each time you re-claim, so an agent that keeps working
keeps its work. An explicitjjj problem assignhas no lease and never
expires — handing work to a person is a decision, not a claim. Default one
hour; setclaim_ttl_minutesunder[settings]. - Every agent was shown the same top item, so a fleet starting together all
Fixed
-
A not-yet-fetched reference no longer breaks the cache. The SQLite cache
enforced foreign keys, but it is a derived view of an eventually-consistent
distributed store: a fetch legitimately delivers a critique before the solution
it references, or a solution whose problem is still on another pod's unmerged
bookmark. That is convergence, not corruption — and enforcing it made
convergence fatal. One such reference failed the wholedb rebuildwith
FOREIGN KEY constraint failed, which also failspush, so a clone could
neither heal its cache nor publish its work — and rebuild was the very thing
that would have healed it. A one-hour swarm hit this 140 times. Genuinely
dangling references are still refused, in the right place and as a report
rather than a brick:jjj db validate, whichpushruns before publishing. -
An empty entity reference no longer resolves to an arbitrary entity.
Resolution falls back to a case-insensitive substring match on the title, and
every title contains the empty string — so""matched every entity, and in
a repository holding exactly one it matched that one successfully and
silently. Since the usual source of an empty argument is an unset shell
variable,jjj solution approve "$SID"would approve something arbitrary, and
problem dissolve ""would dissolve something. Empty references are now
rejected with a message naming the likely cause. (The deferred audit item 4.2,
in its pathological form.) Found when a swarm agent calledsolution attach
with no argument twenty times in an hour. -
solution newnow says what to do about a duplicate. Its only advice was
"use --force to create anyway", which is precisely wrong in the common case:
when two people race onto the same work, forcing creates the duplicate that was
just detected. It now distinguishes the two situations, since they call for
opposite actions — take different work, or give a genuinely rival approach a
title that says how it differs. -
Per-pod push bookmarks are
jjj-{pod}, notjjj/{pod}(breaking, for
anyone whose pod pushes were working — which is nobody, see below). A git ref
is a path:refs/heads/jjjis a file, sorefs/heads/jjj/{pod}requires the
same path to also be a directory and git rejects it with
cannot lock ref ... 'refs/heads/jjj' exists. Since a plainjjj pushwith no
pod creates the bare bookmark, every real repository had it, so per-pod
push failed everywhere it mattered and Break #5's fix — the entire remedy for
the ~quadratic ref contention measured in M0 — was inoperative. The GitHub PR
branch (jjj/s-{id}->jjj-s-{id}) had the identical defect. Both names
still match thejjj*glob used for tracking and head discovery, so fetch is
unchanged. No test caught this because none pushed from a pod to a real
remote; found by the swarm trial intools/swarm/on its first run, and now
covered bypod_and_bare_bookmarks_coexist_on_a_remote. -
Linux release binaries are now statically linked (musl). The 0.5.1
*-unknown-linux-gnuartefacts were built onubuntu-latestand link against
that runner's glibc 2.39, so they refuse to start on Debian 12, Ubuntu 22.04,
RHEL 9 or Amazon Linux withversion 'GLIBC_2.39' not found. Linux targets are
now*-unknown-linux-muslbuilt viacross, and the release verifies the
result is not dynamically linked.install.shfetches the musl assets.
Found while building the swarm-trial container image against Debian bookworm.