Skip to content

Releases

openRepoTools v1.2.0

Choose a tag to compare

@brettheap brettheap released this 10 Nov 20:56
ecd1bce

openRepoTools v1.2.0

v1.2.0 makes every register write fetch before it edits and confirm on origin after it pushes, writes a lane's handoff by that same act, and stops the lane tools from losing a lane they could not read whole. It is openRepoTools as it stands at ecd1bce7b1079ef81215d6166b6db9ee7d481adf in opensoft/openRepoTools-code, tagged on 2026-11-10. 24 pull requests landed on the code leg's main between v1.1.0's commit, 2a057a6d, and this release's: #2 to #25, the last of which made that main ecd1bce. 11 more landed on the spec leg's main, in opensoft/openRepoTools-spec, after v1.1.0's notes: opensoft/openRepoTools-spec#6 to opensoft/openRepoTools-spec#18, all but opensoft/openRepoTools-spec#14 and opensoft/openRepoTools-spec#17, which are open.

v1.2.0 is the second version on PyPI, after 1.1.0. It is published from opensoft/openRepoTools-code, as 1.1.0 was, and its tag is made there. Feature 007, the SQL register (opensoft/openRepoTools#165), is not part of it: on Brett Heap's word of 2026-11-10 it moves to the v1.3.0 milestone, and no part of it ships here.

What 1.2.0 adds

  • Every register write fetches first, and says pushed only once origin has it (opensoft/openRepoTools#198; #17 with opensoft/openRepoTools-spec#15). This is act 2 of lane-collision-protocol Amendment 23. Each lanes-edit.sh writer, routed or not, now fetches and fast-forwards its checkout under the checkout's lock before it reads the file it edits. Where the checkout cannot be brought to origin's tip, it refuses with exit 2, nothing written, and names the checkout and its recovery: a fetch that failed, LANES_NO_FETCH=1 among them; an operation in progress or unmerged entries; HEAD off the branch; staged entries that are not the write's own; unpushed commits; a diverged branch; a pending change to a path the write commits; or a fast-forward git refused. The write then commits by pathspec, pushes with no pull, and reads origin back. pushed origin/<branch> (attempt N) and exit 0 now mean that origin holds the commit, and a push that went, or timed out, and cannot be confirmed is exit 3, pushed, not confirmed: <sha>, never success. A push refused because origin moved replays the one edit on origin's new tip, with the verb's own checks run again, a claim's rescan among them, up to six attempts in all. At that bound, or where a push is refused with origin unmoved, the commit is kept on HEAD and named, with exit 6, and every later write from that checkout refuses until the lane publishes or archives it. lanes-edit.sh commit with nothing staged is the publish act: it sends the commits origin lacks by a fast-forward push only, and refuses (2) a stack that origin has moved past, naming the archive and the reset --keep that settle it.
  • A lane's handoff is written by one verb, under the lock (opensoft/openRepoTools#198; #18 with opensoft/openRepoTools-spec#16, #20 and #19). lanes-edit.sh write-handoff <lane> writes a lane's handoff in the checkout its writes go through, as a Rule 3 stamp (--stamp), a new top block (--prepend) or a whole file (--whole), under that checkout's lock and with the fetch, the publish, the read-back and the replay that every other write has. With --prepend it proves that the block and the old file both survive before it replaces the file. lanes-edit.sh handoff-path <lane> [--create] says where that file is. lane-start writes its Rule 3 stamp, and lane-handoff its PAUSED block, each with one write-handoff call, and the handoff skill writes its new top block with one write-handoff --prepend call, so none of the three commits, pulls or pushes on its own any more. After its first write, lane-start refuses a launch only at a restart's own fences; where the stamp could not be written it says so and prints the write-handoff --stamp call that writes it. A launch reads only the file handoff-path names, and where that file is not there it goes on with no first prompt and names the file. Where lane-handoff or the skill could not write the top block, the block is kept and printed with the one write-handoff --prepend call that puts it in. Under LANES_NO_GIT=1, lane-start and lane-handoff say that the handoff is written and nothing is committed or pushed.
  • Each lane can write through its own clone of the register, by opt-in (opensoft/openRepoTools#198; #16 and #20). W is the checkout of the register that workspace.yaml's path: names. With LANES_ROUTE_WRITES=1 and no LANES_REPO, every write goes through the lane's own clone, <W>-lanes/<lane>/, made on first use, while reads still read W. W then takes no commit: a LANES_REPO that names W refuses every write (2), and W moves only by lanes-edit.sh refresh-workspace, a fast-forward, which lane-start runs before and after its writes. lanes-edit.sh lane-checkout <lane> [--create] names a lane's clone, or makes it. lane-worktrees' report lists <W>-lanes/<lane> under "Register checkouts (Amendment 23 S1)", with what each holds that origin lacks, and never under FOREIGN. Without LANES_ROUTE_WRITES=1, which is how this release ships, every write goes through LANES_REPO, W by default, and every lock is that checkout's, as in 1.1.0.
  • lane-end --sweep-checkouts removes the clones nothing holds (opensoft/openRepoTools#198; #21). It lists the clones under <W>-lanes/ that CLOSED lanes left, and the clones of the register beside W, and it is a dry run by default: exit 3 where something is removable, and 0 where nothing is. With --yes, run as the lane LANES_LANE names, it removes only those nothing holds, each decided again under its own lock after a fetch. W, a live lane's clone, the runner's own, the one a caller's LANES_REPO names and a symbolic link are never candidates. Work origin lacks, uncommitted or untracked files, a stash, an operation in progress, a repository that borrows the clone's objects, a linked worktree, a process working inside it or naming it in LANES_REPO, and a file system mounted in it each hold a clone. What it removed is recorded in one NOTED line in the runner's own log, and exit 1 is a read that failed, or a removal that did not finish or was not recorded. It refuses (2), the dry run too, from a window whose LANES_WORKSPACE_ROOT names a lane's own checkout, and --yes refuses (2) while that variable names any directory but workspace.yaml's path:: run it with the variable unset.
  • The lane tools no longer lose a lane they could not read whole (the five issues of the v1.2.0 milestone, carried to the legs as part of opensoft/openRepoTools#186):
    • A lane whose recorded directory holds a space is no longer left out of worktrees --all, of worktrees <lane> and of the inventory index: worktree_rows hands the directory on quoted where it holds a space, as lane-start writes it (opensoft/openRepoTools#295, #3).
    • A lane whose recorded directory holds a TAB or a backslash is listed, indexed and reached as recorded. The listing's directory column is escaped as lane-worktrees escapes a porcelain field, and lanes, lane and worktrees decode it. Every read of a stored sub-field gives back what was written, because payload_subfield no longer passes it through awk -v. Such a lane's lifecycle and trees are now looked for beside its checkout, and records the old read filed under $PROJECTS_ROOT/.lane-state or a sibling directory stay there, unread, with no migration. The binding and pause checks judge the text that is written and not its awk -v expansion, so host a\101b is no longer passed as aAb, and request-handoff refuses a requesting host or container outside letters, digits, ., _, - and :, on the request and --force paths and on either one's --dry-run, with exit 2 and "Nothing was written" (opensoft/openRepoTools#301, #4 with opensoft/openRepoTools-spec#7).
    • A lane's control root, or a directory above it, that this account cannot search is read as not read, not as no trees: lane-state and lane-trees exit 10, not 8, and say why, worktrees names the lane UNREAD so that lanes-index sync keeps its rows, lane-reconcile is indeterminate, and a write under such a root fails instead of landing under $PROJECTS_ROOT (opensoft/openRepoTools#291, #5 with opensoft/openRepoTools-spec#8).
    • A tree record that names no absolute path no longer swaps its tree's row in the index for one keyed by the record's id: worktrees names the lane UNREAD, so lanes-index sync and reconcile --source inventory keep the lane's rows and say why, and one such record holds every row of its lane until it is put right (opensoft/openRepoTools#302, #6 with opensoft/openRepoTools-spec#9).
    • A named worktrees <lane> read says UNREAD, with exit 0, where the lane's log or the register could not be read, where it used to say none (8). The lanes listing says what it could not read and exits 1 where it names no lane for that reason, a tree's writer is unknown and not 0 where the lane's binding was not read, and lane-state exits 10, not 8, over a log it could not read, so lanes-index holds that lane's rows (opensoft/openRepoTools#290, #7 with opensoft/openRepoTools-spec#10). A known gap remains (opensoft/openRepoTools#303): most other reads of a lane's log still cannot tell a failed read from a lane with no log.

What else changed since v1.1.0

  • The openRepoShape pin moved. This release pins openRepoShape at v1.1.1, 5017830 on its main, where 1.1.0 pinned v1.1.0, 190133d. Four of the eight files the suite mounts...
Read more

openRepoTools v1.1.0

Choose a tag to compare

@brettheap brettheap released this 28 Oct 21:41
2a057a6

openRepoTools v1.1.0

v1.1.0 puts openRepoTools on PyPI. It is openRepoTools as it stands at 2a057a6d84e58c8acb4bda20d2b7b667b552c3fe in opensoft/openRepoTools-code, tagged on 2026-10-28. pipx install openrepotools is a second way in, beside the one-line install and a checkout's --install, and openRepoTools --version from the package names the release it runs. 60 pull requests landed between v1.0.0's commit, aaa7bc8, and this release's: 59 on main in opensoft/openRepoTools, up to 33ce26d, the commit the triad's legs were carved from, and then #1 on the code leg's main. The split itself, opensoft/openRepoTools#344, which made opensoft/openRepoTools the triad's assembly root, landed there after them and is not one of the 60.

v1.0.0 is not on PyPI, and 1.1.0 is the first version there. This release is published from opensoft/openRepoTools-code, after the triad migration (opensoft/openRepoTools#186), and its tag is made there. v1.0.0 stays in opensoft/openRepoTools, where it was made, and no tag is carried from one repository to the other.

What 1.1.0 adds

  • openRepoTools on PyPI, as openrepotools (opensoft/openRepoTools#228). https://pypi.org/project/openrepotools/1.1.0/ is its page. Two commands install it:

    pipx install openrepotools
    openRepoTools --install

    The package places one command, openRepoTools. It runs the packaged openRepoTools with the first bash on your PATH and your arguments unchanged, and with no bash it refuses and names it. It needs Python 3.9 or newer and declares no Python dependencies. The toolset's own requirements are unchanged: bash, git, and jq for --install.

    The wheel carries everything --install places, the sixteen commands and repos.tsv, the three skills and the three command files, and the openRepoShape pin, each byte for byte as the release commit holds it. So openRepoTools --install run from the package reaches no network. pipx owns openRepoTools in its bin directory, ~/.local/bin by default, as a link into its own environment, and --install leaves that one link alone: it places the other sixteen, says 1 left to pipx, and writes 28 receipt rows where a full install writes 29.

    The version is the tag's, and no file in the repository carries a version number: the release build stamps it into the wheel.

  • --version names the release (opensoft/openRepoTools#228). From the package it prints five lines:

    openRepoTools 1.1.0 (opensoft/openRepoTools-code @ 2a057a6d84e58c8acb4bda20d2b7b667b552c3fe)
      version   1.1.0
      tag       v1.1.0
      commit    2a057a6d84e58c8acb4bda20d2b7b667b552c3fe
      digest    31123e2d08efe36583e72956ed8b8dbbe4b968c75402b59af5b8952be83ef6d5 (sorted-ls-tree-r-v1)
    

    A package built outside a release prints openRepoTools 0.0.0 (an unstamped package) and says so. A checkout and the one-line install print a repository and ref, as in v1.0.0, and no version number: a checkout's ref is main by default, and the one-line install, which since the triad migration (opensoft/openRepoTools#186) runs through the assembly root's front door, names the opensoft/openRepoTools commit it resolved main to. A package whose stamp is broken refuses every invocation, --help included, and names the fix, pipx reinstall openrepotools.

  • --install meets each other installer by a rule (opensoft/openRepoTools#228):

    • pipx's link is skipped, by whichever --install meets it: the package's, a checkout's or the one-line install's. A checkout's or the one-line install's count line then says where the other sixteen now come from.
    • pip's console script at the target openRepoTools, which pip install --user openrepotools leaves, is refused in every mode before anything is placed, and the refusal names pipx as the one package route.
    • Any other link at a target still refuses the whole install, and no refusal advises removing pipx's link.
    • After a package-mode install, a note names the first openRepoTools on your PATH when it is not the package's, so a stale bash file ahead of pipx's link is not mistaken for the release.
  • The hook pair waits for its target (opensoft/openRepoTools#237, opensoft/openRepoTools#247). openRepoTools --install and openRepoTools wip init add the pair of hook entries, SessionStart and the name guard (guard), to ~/.claude/settings.json only once ~/projects/xFactory/lanes-edit.sh exists, is executable and answers guard. Until then they say why, and how to complete it, and our exact guard entry is taken back out of a file that already carries it; our SessionStart entry, which ends in || true, and every hook of yours stay. So a workstation upgrading from v1.0.0 whose ~/projects/xFactory/lanes-edit.sh does not answer guard loses that one entry on its next --install, until openRepoTools wip init, or link-estates and --install again, completes the pair. Before this, a fresh workstation got both entries before workspace initialization had provided that file, so the name guard could fail with a command-not-found error at every prompt.

  • CI proves the package, and a release workflow publishes it (opensoft/openRepoTools#228).

    • tests.yml runs on every push to main and to every release/v* branch, as well as on pull requests, nightly and by dispatch, and every run has four jobs: guard-launch-mode, tests, tests-no-submodule and package. The package job builds the wheel twice at the commit's time and requires the two to be one file, checks every packaged payload file against the commit's blob in bytes and in mode, checks the wheel's .dist-info, installs the wheel with pipx, runs the unstamped --version and an offline --install beside pipx's link, and runs the package's tests, failing if one is skipped.
    • release.yml is run by hand, on an annotated tag, with the version. It is adapted from openRepoShape v1.1.0's. Its build refuses unless the ref is the version's annotated tag and the tag names the run's commit; that commit is on main or on release/vX.Y; the testpypi and pypi environments each name a required reviewer and limit deployments to v* tags; the commit's four CI jobs ran in a push or dispatch run of tests.yml and the newest succeeded; and the tree digest computed here equals the one computed from GitHub's tree listing. It then builds twice from git archive, stamps the build, and requires one file.
    • It uploads by trusted publishing, with no token: to TestPyPI, then an install check on Ubuntu, on macOS (run again under macOS's own bash 3.2) and on Ubuntu with Python 3.9, then to PyPI. Each upload waits for Brett Heap's approval in its environment. A pre-release X.Y.ZrcN reaches TestPyPI only. Only opensoft/openRepoTools-code may publish a final version, and nothing uploads to PyPI from opensoft/openRepoTools.
  • Releases are annotated tags (opensoft/openRepoTools#228). A release is an annotated tag vX.Y.Z on a commit on main or on release/vX.Y, never a branch and never a version number committed to a file, and a tag is never moved or reused. release/vX.Y is cut at vX.Y.0 and carries the patch tags. A fix lands on main first, then reaches the release branch by a fast-forward, a merge, or a pull request of git cherry-pick -x. A version is uploaded once, and a bad upload is fixed by the next patch.

  • The lane tools name what they could not read in a triad's legs (opensoft/openRepoTools#255, opensoft/openRepoTools#271). It closes two gaps in opensoft/openRepoTools#246, which taught the lane tools to look inside a triad's legs; each broke that change's rule that a read that failed is never an empty one. lane-handoff names every tree-inventory record that is not a whole schema-1 one in its NOT POLLED note, and the lane stays SWAPPING. A leg directory this account cannot search is unreadable and never absent: lane-worktrees sweep exits 3, lane-end refuses over it, lane-reconcile gives it an unreadable row, and lane-handoff withholds its tree. A dependency nested in a leg is unreadable to the sweep too where this account cannot search its directory, or a directory above it inside the leg: the sweep exits 3, and lane-end refuses over it. A leg directory that is not there at all is still absent. A known gap remains (opensoft/openRepoTools#272): the sweep also reads a declared leg beneath a directory this account cannot search as absent, so lane-end does not hold on it. These readers run only in an assembly, a checkout whose project.yaml declares a leg, so a checkout with no project.yaml sees no change.

  • lane-handoff reads the lane's lifecycle whole (opensoft/openRepoTools#280, opensoft/openRepoTools#283). It used to pipe lanes-edit.sh lane-state into an awk that exits as soon as it has printed the one field it wants. Under pipefail, a lane-state that wrote again after that died of SIGPIPE, so now and then the read exited 141, the handoff said the lifecycle could not be read, and it recorded no RUNNING to SWAPPING to SWAPPED transition. That failed safe, but whether it happened depended on timing alone, and it could turn a pull request's tests job red at random. The handoff now takes lane-state's whole output first and parses it afterwards, so its status is lane-state's own. The read of the operation of a lane that is still SWAPPING, which only names that operation in a note, is made the same way.

What else changed since v1.0.0

  • The openRepoShape pin moved. This release pins openRepoShape at v1.1.0, 190133d on its main, the commit its tests were verified against (opensoft/openRepoTools#233). v1.0.0 pinned 7f84ca4.
  • The estate commands (park, resume and status):
    • resume and status hold each leg to the branch bootstrap puts it on: the leg row's own branch:, else the root's tracking_branch (opensoft/openRepoTools#238).
      ...
Read more