Skip to content

v0.5.2

Latest

Choose a tag to compare

@github-actions github-actions released this 22 Aug 19:09
· 89 commits to main since this release

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_rev advanced
    only on push, so a clone that only ever fetches — a review-only agent, or any
    pod between pushes — stayed at None forever and re-derived the whole corpus
    on every fetch. Measured on a 25,000-entity corpus: warm_delta_fetch for 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_push goes 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 submit publishes 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 --approve finishes 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 the jjj* 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|dismiss and
    solution lgtm accept --json like new, list and show already did, and
    emit the updated entity so a caller need not follow up with show.

  • problem dissolve accepts --rationale, the spelling every other command
    that records a decision uses.

  • The MSRV job now tests the MSRV. rust-toolchain.toml overrides 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 lgtm works 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 --mine 76 times, because next --mine and critique list --mine both do.
    Agents probe an API far more systematically than people, and they had inferred
    the consistent surface jjj lacked. Now:

    • --mine on problem list, solution list and events (it already existed
      on next, status and critique list)
    • events --user <actor> — "what did agent-03 do?" is the first question
      anyone asks of a swarm's log, and it had no answer
    • solution lgtm --rationale "..." — a sign-off asserts the work is correct,
      so it should carry the evidence, as approve already does
  • jjj next spreads 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 --claim now records
    claimed_at, and jjj next no 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 explicit jjj problem assign has no lease and never
    expires — handing work to a person is a decision, not a claim. Default one
    hour; set claim_ttl_minutes under [settings].

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 whole db rebuild with
    FOREIGN KEY constraint failed, which also fails push, 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, which push runs 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 called solution attach
    with no argument twenty times in an hour.

  • solution new now 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}, not jjj/{pod} (breaking, for
    anyone whose pod pushes were working — which is nobody, see below). A git ref
    is a path: refs/heads/jjj is a file, so refs/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 plain jjj push with 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 the jjj* 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 in tools/swarm/ on its first run, and now
    covered by pod_and_bare_bookmarks_coexist_on_a_remote.

  • Linux release binaries are now statically linked (musl). The 0.5.1
    *-unknown-linux-gnu artefacts were built on ubuntu-latest and link against
    that runner's glibc 2.39, so they refuse to start on Debian 12, Ubuntu 22.04,
    RHEL 9 or Amazon Linux with version 'GLIBC_2.39' not found. Linux targets are
    now *-unknown-linux-musl built via cross, and the release verifies the
    result is not dynamically linked. install.sh fetches the musl assets.
    Found while building the swarm-trial container image against Debian bookworm.