Skip to content

openRepoShape v1.1.0

Choose a tag to compare

@brettheap brettheap released this 17 Oct 21:56
· 264 commits to main since this release
190133d

openRepoShape v1.1.0

v1.1.0 puts openRepoShape on PyPI. pipx install openreposhape is a second
way in, beside the one-line install, and the first on native Windows, where
PowerShell cannot run the one line (#563). This release also carries v1.0.1's
one-line install into main (#564), fixes limits that v1.0.0 listed, and runs
CI on Linux only (#597).

At main 190133d, on 2026-10-17, 123 pull requests had landed on main since
v1.0.0's commit, bfed143; at 21:55Z, 133 of the v1.1.0 milestone's 133
issues were closed.

What 1.1.0 adds

  • openRepoShape on PyPI, as openreposhape (#590, #599, #563). One
    command installs it:

    pipx install openreposhape

    It places five commands. openRepoShape has the launcher's modes,
    <Project>, --preflight, --doctor, --version and --help, written
    again in Python, so native Windows gets the command, as an .exe.
    scaffold-project, adopt-project, update-shape and shape-doctor each
    run the tool of that name with every argument unchanged, so docs/cli.md
    is their reference. git-filter-repo 2.47.0 comes as their dependency,
    and each command puts it first on the PATH of the tools it starts.

    The wheel is thin. It carries the release's commit and the
    sorted-ls-tree-r-v1 digest of its tree, and the tools run from a full
    clone of this repository at that commit, made once in your per-user cache
    and kept. Before each run the clone must be at that commit, unchanged, and
    digest to that value, so a project scaffolded through it pins exactly this
    release. $OPENREPOSHAPE_REF names another ref for one run, with no digest
    checked. The package's --install refuses and names
    pipx upgrade openreposhape. Python 3.9 or newer.

    The version is the tag's, so 1.1.0 is the first version on PyPI. The
    v1.0.x line is not published there.

  • The one-line install, on main (#564, carried into main by #587):

    curl -fsSL https://github.com/opensoft/openRepoShape/releases/latest/download/install.sh | bash

    It installs the newest release's bash openRepoShape, pinned to the
    release tag, and ends with openRepoShape --preflight, as
    v1.0.1's notes say. Since v1.0.1:

    • it installs git-filter-repo with uv where there is no pipx, and never
      with pip install --user, which an externally managed Python refuses
      (PEP 668). With neither, it prints one command for this machine: brew's,
      pipx's from apt or dnf, pacman's, or uv's (#598, #595);
    • it ends with two lines, adoption: and then done:, so a machine that
      is ready to scaffold but not to adopt is told so (#598, #595);
    • Git Bash, the bash that Git for Windows installs, runs it and the
      openRepoShape it places, tested in CI but for a typed yes, which the
      preflight cannot take there (#596, #585);
    • it refuses a target that is a symbolic link, such as the
      openRepoShape pipx places, and so does the launcher's --install
      (#563).
  • CI on Linux only (#597). Brett Heap decided on 2026-10-15 to drop the
    macOS and Windows test jobs across the estate. tests.yml runs the whole
    suite on ubuntu-latest, on every push to main, every pull request and
    every dispatch. Since #593 each push run on main is grouped by its own
    commit, so the next landing no longer cancels it, and every landing keeps
    its own Linux result. One exception, on his word the same day: a
    tests-git-bash job runs the launcher's and install.sh's suites, and
    nothing else, on Windows under Git Bash (#596, #585). The rest of the
    suite no longer exercises Windows file-system behaviour, and the stock
    macOS bash 3.2 is a goal the bash entry points are written for rather
    than a checked claim. The release workflow still
    installs the package on Linux, macOS and Windows before it reaches PyPI.

Install and upgrade

  • The newest release, by the one line: the line above. The same line
    again upgrades. … | bash -s -- v1.1.0 names this release.
    openRepoShape --version prints openRepoShape (opensoft/openRepoShape @ v1.1.0).
  • From PyPI: pipx install openreposhape, or
    pipx install openreposhape==1.1.0 for this release.
    pipx upgrade openreposhape upgrades it. pipx itself comes from
    sudo apt-get install -y pipx on Debian and Ubuntu and
    brew install pipx on macOS; on Windows, python -m pip install --user pipx,
    then python -m pipx ensurepath and a new terminal.
    openRepoShape --version prints
    openRepoShape 1.1.0 (opensoft/openRepoShape @ 190133d5e704e380c430f87ca9c4e31746de0012), then the
    version, tag, commit, digest and the clone it would use.
  • One installer or the other. Both place ~/.local/bin/openRepoShape.
    pipx does not replace a command it did not place, and install.sh and the
    launcher's --install refuse the link pipx places, naming
    pipx upgrade openreposhape or removing the link.
  • Native Windows: pipx install openreposhape, then
    openRepoShape <Project> --org <your-org>. Downloading setup-project.py
    and running py setup-project.py <Project> --org <your-org> still works,
    and installs nothing. Git Bash runs the one line and the bash
    openRepoShape it places, tested in CI but for a typed yes, which the
    preflight cannot take there (#596, #585).
  • A project's pin on the standard works as before. Follow
    How to pin 1.0.0 with the v1.1.0 release
    commit. A scaffold run through the package pins that commit and digest by
    itself.
  • A family holder made before #642. A holder made with
    --family X --name Y before #642 (the fix for #629) keeps cd Y in its
    own README, and update-shape.py apply never re-renders a README, so
    change that one line by hand to cd X: the holder's directory is named
    after its repository, X, never after the display name. The same README
    draws its layout as Y/Y and calls the doubled Y/Y deliberate; read
    X/X there, which is what make siblings checks the folder against.

What changed since v1.0.1

v1.0.1 was cut from release/v1.0 and added only the one-line install.
Everything below landed on main after v1.0.0's commit, by area. The list
runs to main 6f64eea.

adopt-project.py

  • The source's origin, read one way. plan reads <org>/<repo> from
    every origin git accepts, a local clone or mirror path and a URL ending
    / included, so a local clone's right --org is no longer refused
    (#591, closing #521 and #427).
  • Refused before any leg exists. check finds, and execute refuses
    before any leg exists, a file the root keeps at the shape's own pin path
    and a --work-dir the user cannot write in. Both used to fail only after
    both legs were pushed (#581, #524). check recomputes each neutral pin's
    digest at the plan's commit, in the clone plan read it from, where that
    clone is still on the machine and is no partial clone, so a commit or a
    digest edited into the plan no longer reaches the root as
    adoption verified over a red validate-pins.py (#589, #449). Before any
    leg too: the branch names execute failed on, such as
    adopt_branch: a..b, a source branch Adopt or one hidden by hideRefs
    (#583, #520, #420); a kept nested .gitattributes that outranks the
    root's line-ending rule for a pinned copy, and a root .gitattributes
    that git add would re-encode (#619, #530); and a kept submodule url on
    this disk where git ls-remote reaches no repository, which used to end
    adoption verified over a recursive clone that fails (#618, #418).
  • Names outside ASCII. execute hands git rm each path the split
    moves or drops as the bytes the tree holds, so such a name under
    LC_ALL=C PYTHONUTF8=0 no longer stops the run with UnicodeEncodeError
    after both legs are pushed (#614, #527).
  • The work directory. A run without --work-dir removes the work
    directory it made where it holds no file, and execute names the one it
    keeps. check and execute clone the plan's source.repository again
    where the clone it records is gone, so neither leaves an empty
    openreposhape-adopt-* directory in the system temp folder, nor refuses
    source-unclonable once that folder is cleaned (#594, #526).
  • Answers kept. execute records the plan's answers under
    answered_paths:, and a moved submodule names its row, so an answered
    examples/ is not asked again, and the doctor names no plan for a
    deliberate move (#617, #525).
  • Leg paths that are not the defaults. The Makefile block execute
    appends, and the cross-leg follow-up, name each leg where the plan mounts
    it, so --spec-path legs/spec exports a CONTRACTS_DIR that exists and a
    nested --code-path gets the right ../ path (#572, #522).
  • A leg path edited by hand. The read-across follow-up names
    legs.code_path and legs.spec_path and no path, and the contracts/
    rule's reason names none, so the hand edit of a leg's mount that plan
    asks for leaves no plan-time path in check, the split commit or the pull
    request (#628, #600). A plan written before v1.1.0 whose legs.spec_path
    or legs.code_path was edited by hand keeps its old value-form
    read-across follow-up. check accepts it, because follow_ups: is free
    text, and execute carries the stale line into the split commit and the
    pull request's body. Re-run plan for such a plan.
  • A source's own single-repository.yaml is asked drop or root, drop by
    default, under a rule of its own, and no longer gets the general top-level
    YAML question (#566, #528).
  • What a run says is what it does. The plan's last follow-up is a review
    of the pull request execute opens itself. A failed verification names
    the pull request and the topic it left in place. Under
    --local-remote-dir, NEXT names the split's branch. AGENTS.md says that
    check passes an answer with no resolution:, and the ruleset hint no
    longer offers the seed branch the scaffold dropped (#582, #523). With no
    --elected-by and no git config user.name, the refusal says where the
    empty value came from (#567, #529).
  • An id plan derived. With no --id, plan derives one from
    --project, and adopt-bad-id now says so: --project North_Wind no
    longer says --id 'north_wind' must match over a flag nobody passed
    (#620, #601).
  • Empty and derived values. plan refuses --id '' and
    --elected-by '' by name, a refused derived id says to change
    --project (or, adopting in place, to rename the source first), and an
    organisation read from a url is named as not given (#699, #658).
  • --work-dir. A --work-dir that is a file or cannot be made is
    refused by name in plan, check and execute, and --help says what
    --work-dir is and where --org comes from (#719, #650).
  • --source, by its spelling. An org/repo that is also a directory
    here is refused source-ambiguous, naming both; any other org/repo, in
    any spelling, is cloned by its name. The plan records source.cloned,
    and a clone no plan records is removed, so a recorded clone that is gone
    or emptied is refused, or cloned again, for what it is (#749, #652).
  • Names in a C locale. Under LC_ALL=C PYTHONUTF8=0, plan, check
    and execute hand git and gh each name, url and branch as the UTF-8
    bytes it spells, and the leg description, whose em dash stopped every
    execute against GitHub at the first leg, so none of them ends in a
    UnicodeEncodeError before any leg exists (#727, #653).
  • A default branch that is not UTF-8. A source whose HEAD or
    origin/HEAD is on a branch such as caf<e9> is refused, or adopted past
    it, instead of ending plan, check and execute in a traceback (#717,
    #425).
  • The Free-plan secrets hint. adopt-project.py and
    scaffold-project.py go by the assembly root's own visibility on GitHub
    and name the root as the reason, so a public source adopted with private
    legs is no longer told that its organisation secrets resolve to EMPTY
    (#621, #602). The hint, in the scaffold, the adoption and family.py,
    goes by what GitHub says of the root or holder and of its organisation's
    plan, and names any answer GitHub did not give, so
    init --reuse-empty-repo no longer calls a public holder private (#728,
    #660).
  • Caught by check, not after both legs. check asks this machine's
    git what the split stores for each file the plan keeps or moves and each
    pinned copy, so one it would store as other bytes is a finding (#743,
    #654). A source symlink whose target begins with a NUL byte is
    plan-empty-symlink, which execute refuses before any leg (#720,
    #388).
  • check --local-remote-dir. check takes --local-remote-dir and
    resolves each relative submodule url a leg keeps from the bare
    repositories execute --local-remote-dir would create, so under that test
    path check no longer refuses an adoption execute verifies, nor passes
    one execute refuses (#718, #417).
  • adoption verified means a shape root. The verification fails a
    split whose root project.yaml is not the shape's manifest
    (adopt-manifest-not-shape), so it no longer passes an assembly the
    doctor calls NOT A SHAPE ROOT (#722, #389).
  • Answers that stay answered. A plan's answer against a rule is
    recorded, and an empty .gitmodules asks nothing, so docs/ answered
    code is not MISPLACED after adoption verified, nor is a .gitmodules
    emptied by git rm asked on every run (#721, #655).
  • Symlinks checked out as files. check's finding for a link kept above
    a path the shape writes says that the clone holds it as a file (#584,
    #531).
  • Installing git-filter-repo. filter-repo-missing names its install
    in install.sh's order: pipx install git-filter-repo==2.47.0, else
    uv tool install git-filter-repo==2.47.0, else brew's, pipx's from apt or
    dnf, or pacman's. It led with a pip install that an externally managed
    Python refuses (PEP 668). It says that pipx install openreposhape brings
    it for the adopt-project command it places (#563).

update-shape.py, bump-leg.py and a project's own scripts

  • A kept copy that would break bootstrap. apply --accept-local
    refuses, before anything is written, to keep a helper that lacks a name a
    newly written copy imports from it (#562, #536), or a copy that imports a
    name the module written beside it no longer defines (#616, #604). Both
    used to pass the validators and then end make bootstrap in an
    ImportError.
  • Hard links and rollback. apply refuses to rewrite a pinned file that
    has a second, hard-linked name. A rollback that cannot put a byte back
    names that file with the line that finishes the job, and puts back the
    rest (#560, #422, #428).
  • Headers, branches and messages. A files: or shape: header with a
    trailing comment is rewritten and keeps the comment, --branch is judged
    the same from any directory, and apply's messages say what is true
    (#558, #437, #431, #535).
  • apply's lines, its branch and its headers. A rollback line for a
    file that is gone, under a directory this user cannot write in, says to
    make that directory writable first. committed on <branch>: names what
    the commit holds, where it named project.yaml beside a pin that moved
    alone. A branch named like a tag is read, and pushed in the run and in
    both printed push lines, by its full name, where the push failed
    matches more than one. A files: or shape: header spelled
    "files": or files : is found as the reader finds it, where apply
    refused the pin that check had sent it to (#810, #674). Of v1.0.0's
    known limits, this closes the quoted header.
  • bump-leg.py --dry-run gives the real run's answer. --to an
    annotated tag's own id is peeled to its commit in both runs, and a pin
    layout the rewrite refuses is refused by the dry run too (#561, #534,
    #438).
  • bump-leg.py's branch and its lines. bump-leg.py reads the branch
    in full, so a tag of the same name no longer hides the tracking branch;
    its push line names the branch on both sides, so a branch beginning +
    is no longer pushed as a forced update; it spells each file name it
    lists; its committed line lists what the commit holds, and a gitlink
    moved by hand is no longer reported X -> X; and a dry run says it runs
    no validator, where its behaviour is unchanged (#746, #672). With #810,
    that closes v1.0.0's known limit on the paths bump-leg.py and apply
    listed as staged, and on X -> X. A dry run that exits 0 over a pin its
    own validators refuse stays one, which the dry run now says.
  • A git switch -c line that is no dead end. bump-leg.py and
    setup-project.py say under their git switch -c line what to do where
    an earlier act left that branch, so already exists is no dead end, as
    family.py says since #637 (#657, #647).
  • Paste lines. bump-leg.py and update-shape.py quote the roots and
    paths in the lines they print, the doctor quotes a leading = for zsh,
    and the README says the printed commands are for a POSIX shell (#588,
    #532).
  • Bootstrap. A leg detached off its pin is left where it is, and said,
    so a commit made there is no longer stranded (#556, #533). A leg on
    another branch whose tracking branch is behind the pin is fast-forwarded
    and checked out, and the shipped text says bootstrap places a leg where
    that loses no commit (#615, #603). make bootstrap moves a leg only where
    nothing the person has is lost, says which branch it left, and refuses by
    name when git cannot run (#745, #704, #694). With no git on PATH, both
    shipped bootstrap.py scripts are refused git-not-runnable, exit 2,
    where they ended in a FileNotFoundError traceback, which v1.0.0 listed
    as a known limit.
  • A branch name git cannot take. A tracking_branch: or a leg's
    branch: that holds a NUL or a lone surrogate is a validate-manifest.py
    finding, and make bootstrap does not hand it to git, so it no longer
    ends in a traceback under a manifest ok (#644, #455).
  • The pinned validators. THE LOCKSTEP RULE is printed only where a leg
    or member is out of step. A lone digest or workflow-reference mismatch
    names its bump-leg.py line, and a holder's remedies name the branch to
    make first (#575, #539). One leg's gitlink off its pin, beside another
    leg's digest or workflow-reference mismatch, names the step that comes
    first: put that gitlink back by hand, then run the other leg's
    bump-leg.py line (#702, #677).
  • The pinned shape copies, in one bump. AGENTS-shape.md is true of
    every root, the scaffold's chain WARNING offers only its own sources and
    spells --pin-source, the root's not-used note spells it too, THE
    LOCKSTEP RULE's bump runs as printed, and repo_shape.py's git_out
    spells the folder it names and git's stderr (ESC, CR, BEL, C1 controls)
    for every caller (#634, #610).
  • A workflows directory the pins gate cannot read. validate-pins.py
    refuses a .github/workflows/ directory this user cannot list or search
    as workflow-unreadable, naming it, exit 2, where it ended in a
    PermissionError traceback, exit 1 (#633, #434).
  • A pin list that is not a list. A project's neutral_product_pins: or
    a leg's naming.referent_chain: that is not a list is
    validate-manifest.py's finding for that field in every gate that reads
    it, so 5 no longer ends make validate in a TypeError, and a scalar
    openXdox is no longer read as its letters (#631, #443).

scaffold, setup, family and the shared readers

  • Dry runs ask the real run's question. scaffold-project.py --dry-run
    and family.py init --dry-run ask whether the remotes already exist, so
    scaffold-remote-exists and family-remote-exists come at the plan, and
    a missing gh is gh-not-found, not a traceback (#573, #537).
  • Folders, asked before they are made. scaffold-project.py and
    family.py init ask whether each folder they make can be made, and take
    only gh's not-found answer as absent, so --dry-run refuses a file
    where a folder goes, or a gh repo view that fails another way, by name,
    as the real run does, and a refused init leaves no empty family folder
    (#622, #609).
    Those refusals, and scaffold-target-exists, family-target-exists and
    family-operation-in-progress, spell a path holding a character that is
    not printable as Python's repr does, so an ESC or a line break in a
    folder's name no longer reaches the terminal (#731, #692).
  • --tracking-branch. The tracking branch is pushed by its full name,
    so --tracking-branch +x scaffolds a project and initialises a family
    instead of failing at the push after every remote exists (#643, #421).
    family.py add, bump and remove print their push line with the
    branch named in full on both sides, so a branch beginning + or named
    like a tag pushes as itself, where the line force-pushed another branch
    over origin's, or failed. A branch name with a part longer than a disk
    holds (a last part over 250 bytes, which git locks as <part>.lock) is
    refused before anything exists by scaffold-project.py, family.py init
    and adopt-project.py, --dry-run alike, where the first commit failed
    with every remote made (#813, #689).
  • --local-remote-dir. A --local-remote-dir whose clone URL is not
    text a README can hold, such as a directory named in a legacy code page,
    is refused before any remote or folder exists, --dry-run alike, where
    it ended in a UnicodeEncodeError traceback after the bare remotes were
    made (#640, #433). family.py add --local-remote-dir refuses a member
    clone URL that the holder's .gitmodules cannot hold as text,
    family-unwritable-value, --dry-run alike, before the member is read,
    where it committed the URL raw and make siblings read another path
    back. Each clone-URL remedy says that the URL is the path the flag
    resolves to, so the directory to rename may be a link's target, or the
    one a relative path was given from, and names a lone surrogate. The
    READMEs the scaffold and family.py init write quote a local clone URL
    that needs it for a POSIX shell, so the git clone line under a b/
    pastes as one command (#812, #693).
  • Names that are not UTF-8. In bump-leg.py, scaffold-project.py and
    family.py, a file name that is not UTF-8 in git status is
    bump-leg-root-dirty or the scaffold's DIRTY warning, and a
    git config user.name that is not UTF-8 is refused
    family-unwritable-value, naming --created-by, where each ended in a
    UnicodeDecodeError traceback (#641, #423, #426).
  • What family.py init records and commits as. It asks whether
    family.yaml can hold the creator whichever of --created-by,
    GIT_AUTHOR_NAME or git config user.name gave it, and the remedy names
    that source; it asks git, before the remote exists, whether the holder's
    first commit can be made with the GIT_AUTHOR_* and GIT_COMMITTER_*
    values set (family-bad-commit-identity); and it refuses a --family
    that is a Windows device name, such as CON (family-bad-holder-name).
    --dry-run refuses alike, where a creator holding a line break or a
    control character was taken and a name git refuses failed after the
    remote and the work directory existed (#809, #688).
  • A re-run's legs. A leg that gh sees and git cannot read is refused
    scaffold-remote-exists, with git's terminal prompt off, and the reuse
    re-run's visibility lines go by each leg as it stands (#565, #542).
  • --pin-source. A pin file records digest_source: local-clone, never
    the path, in the scaffold and in adopt-project.py, and a --pin-source
    nothing reads is refused pin-source-unused (#570, #540, #451, #450).
  • A --pin naming a tag. In scaffold-project.py and
    adopt-project.py, a --pin that names an annotated tag's own id pins
    the commit the tag names, and one that names a tree or a blob is refused,
    so a pin file no longer records a tag's id as its commit (#725, #651).
  • A derived id, named as derived. scaffold-bad-id and
    family-bad-id name an id derived from --project or --family as
    derived and say to change that flag, so --project North_Wind no longer
    sends the person to an --id the naming policy then refuses (#671,
    #659).
  • setup-project.py. Its paste lines quote each path, the scaffold's NEXT
    STEPS wait for step (7), step (3) refuses a file above the clone,
    -- --dry-run ends at the plan, step (4) hears the pins passed to it, and
    the upstream-organisation guard ignores case (#569, #538).
  • setup-project.py's checks before a run. Step (3) refuses a parent
    folder nobody can write in and a drive that is not there, step (4)
    verifies a chain from --pin-source, --pin no longer takes a flag as
    its value, and step (5) says when a printed plan was refused (#623,
    #608).
  • Pin chains. A bare --link-source is the tree of the chain's first
    link alone, and a source that names no path is refused (#576, #441,
    #442).
  • A chain link no tree answers for. The root's validate-manifest.py,
    the naming gate and setup's step (4) each offer what that caller reads,
    so neither the validator nor setup offers --link-source, which neither
    takes, and the gate spells every --link-source value and path it prints.
    The naming gate takes --link-source-label and --link-remedy, so setup
    passes its own flag and words for a holder (#744, #661).
  • One product, one spelling. A chain link, a declared pin and a
    --link-source key name one product exactly when they share a pin file,
    so a tree declaring openss no longer verifies a chain that says
    openß, a chain naming both is two links and not a cycle, and
    --link-source openss= no longer answers for openß (#638, #444).
  • The YAML reader. The standard's own reader in repo_shape.py, which
    the tools use to read project.yaml and the pin files, refuses a plain
    value opening with an indicator, an empty flow entry and a single pair in
    a flow sequence, so k: ] is not the text ], pins: [,a] is not
    ['a'], and k: [a: b] is not the string 'a: b' (#635, #392, #393).
    A value refused for opening with an indicator is named by its line, and
    the refusal says to write it in double quotes, spelled as the reader
    reads it back, or to reword it. plan --org @x and
    scaffold-project.py --org @x still name --org, now in the reader's
    words and under its remedy, and check reports a tracking_branch
    holding a NUL or a lone surrogate once, as unsafe-value, without the
    validate-manifest.py finding above (#644), which names no plan key
    (#817, #708).
  • family.py. add refuses, before the member is read and on every disk,
    any path under another spelling of members/, with a remedy per entry
    that never moves the members' checkouts (#574, #362, #363, #364). add
    and remove refuse a .gitmodules the work tree does not hold as the
    index does (#571, #407). bump --to asks add's duplicate-id question,
    peels an annotated tag's own id as add --at does, and says to push first
    a commit the member does not hold (#568, #541). It also refuses a commit
    that only the member's mount holds, reachable from no branch of the
    member's remote, as bump-leg.py refuses its twin (#625, #613).
    make siblings asks about the family folder before it clones, clones each
    member on its tracking branch, and family.py siblings runs the holder's
    own copy (#578, #543).
  • bump --to, reachable or not. family.py bump fetches every branch
    of the member's origin, whatever fetch refspec the mount has, and
    prunes the remote-tracking branches origin has deleted, so a commit a
    branch holds is no longer refused for a refspec narrowed by hand, and one
    only a deleted branch held is no longer pinned. A commit only a tag of the
    remote reaches is told that a tag is not a branch, and a mount with no
    remote named origin is told so, where each was told the pin cannot be
    fetched, or to fix the network (#795, #686).
  • family.py refuses before it writes. add, bump and remove refuse,
    before their first write, a merge, cherry-pick, revert, rebase or
    git am in progress, a .gitmodules, members/ or family.yaml that
    the sparse checkout leaves out, and a file named members. Each failed
    after writing and was rolled back, or was committed in the wrong place
    (#626, #612).
  • More holder states, named. add, bump and remove name, before
    their first write and --dry-run alike, a git bisect in progress, a
    rebase directory that holds no rebase, non-cone sparse rules that hold
    no pattern, a merge conflicted in family.yaml itself, and a members
    file that HEAD holds and the work tree no longer does, each with the way
    out that works there. They refuse a family.yaml holding a change HEAD
    does not, staged or not (family-manifest-uncommitted), which their
    commit took in beside their own row, so a bump an earlier run staged
    and did not commit is told to put back HEAD's copy and run again (#816,
    #684).
  • The switch -c remedy. Where an earlier act left the branch,
    family.py and validate-family.py say to give the new branch another
    name, or to go back to the old one only if it holds work not merged yet,
    and the docs spell it family/add-<Project> (#637, #607).
  • A holder's gitlinks and its .gitmodules. A gitlink that no section
    of HEAD's .gitmodules names is validate-family.py's finding
    member-gitmodules-absent, family.py remove refuses a holder with no
    .gitmodules before it deinitialises anything, and two gitlink remedies
    name what they missed (#639, #611).
  • validate-family.py's remedies, typed as printed. After the git rm
    that member-gitlink-unnamed prints, and before the commit, the finding
    says the removal is staged and to commit it, and prints no git rm,
    where it printed the same command again, which then failed.
    member-gitlink-absent's remedy puts the gitlink back by hand, in the
    index at the commit the row pins, with its .gitmodules section where
    HEAD's gives none, where it named a family.py add that refuses the row.
    The mv for a directory under another spelling of the row's path moves
    nothing where the disk takes the two names for one directory or the
    directory holds a .git, so it no longer moves a member's own checkout.
    In both validators, a shape-pin files: row whose path: is absent,
    empty or not text is the finding shape-pin-row, where it was reported
    as the path None (#747, #681). The first and the last were known
    limits of v1.0.0.
  • A family's name, printed. make siblings, park and resume print
    the family's name and every member row, and no longer end in a traceback
    on a name such as "a\ud83db" or on a dash under
    PYTHONIOENCODING=ascii. The holder's README names its folder after the
    repository, so --family Quill --name QuillWorks lays out as its README
    says (#642, #454, #629). setup-project.py names a family folder after
    the holder's repository too, so the Quill/Quill that family.py init --name QuillWorks makes is detected with no flag (#703, #696).
  • make siblings's inputs. The move it prints to put the holder in a
    folder named after the family runs as typed past a staging name
    something already holds, and says first to move aside a link to nothing
    in its way. A member whose project: or url holds a NUL or a lone
    surrogate is a finding, NOT AN ARGUMENT, in the dry run too, and the
    members after it are still placed, where the run ended in a traceback. It
    reads the holder's .gitmodules through one git config -z --list, as
    validate-family.py and the doctor do, so a url holding NEL or U+2028,
    or a section named members/a=b, gives the url git uses. Its warning
    spells the holder, the folder and the family, and a tracking_branch:
    that is only a tag of the member's remote is said to leave the clone
    DETACHED (#805, #690).
  • A member's gitlink under another spelling. Where a member's only
    gitlink sits under another spelling of its row's path, family.py add's
    refusal moves it back to that path in the index alone, so the printed
    remedy no longer takes the member's gitlink away from its row (#670,
    #646).
  • A members/ renamed with git mv. family.py add's
    family-members-dir-variant refusal renames it back in two steps through
    a name nothing holds, git mv -- Members members.renaming, then
    git mv -- members.renaming members, which takes each checkout and each
    section of .gitmodules with it on every disk, where the move back left
    each section naming the other spelling. It says first to put back what
    the disk lacks under it, and to stage, or put back, the work tree's
    .gitmodules. A printed git mv --sparse says it needs git 2.38 or
    later, and the move back's git rm --sparse git 2.34, so no remedy fails
    as typed (#767, #685).
  • A holder's make bootstrap that left a member unchecked-out. It no
    longer says family bootstrap ok there: a subset clone, a member with
    update = none, a gitlink git stops at (a stray Members/<P> beside
    members/<P>, a member mounted by hand with no row, a .gitmodules
    section with no url), a .gitmodules that git reads otherwise than
    git config does (two sections for one path, a section name git
    ignores, a value beginning with -), and a .gitmodules a merge left
    unmerged now fail, and the line names the cause and the cure. This
    changes an exit code a script can see: ok became exit 1 (#682, #648).

shape-doctor.py

  • Both ways in. A git repository that is not a shape root is told of
    the adoption and the scaffold, and the adoption line ends
    --org <your-org> wherever plan can read no organisation from origin
    (#553, #545; #591).
  • What it writes, and its clone. openRepoShape --doctor says it writes
    nothing into the repository but a --placement-plan file you name, keeps
    its clone for the NEXT lines that name it, and refuses a second
    --placement-plan or a second path (#580, #546).
  • A validator's verdict. A row's verdict is a printed FINDING or
    REFUSED, and any other non-zero exit is a crash, not a finding about the
    project. A crash of the standard's own naming script is CANNOT ANSWER,
    and a pass that printed WARNING or note lines is a note that quotes
    them (#557, #410, #447, #448, #460).
  • A root that lost repo_shape.py. Every validator the doctor runs
    ignores site-packages, so a .pth file in the user's or a virtualenv's
    site-packages no longer stands in for the scripts/repo_shape.py a root
    lost: the manifest row is the gate's own ModuleNotFoundError, as in
    CI (#636, #342). An own-copy validator row whose crash is that loss names
    git -C <root> checkout HEAD -- scripts/repo_shape.py as its NEXT, or
    none where HEAD no longer holds it (#737, #697).
  • A family holder's .gitmodules. --root on a holder reads a member
    url that is not UTF-8 as make siblings does, so a member mounted from a
    legacy-code-page directory gets a verdict, where the doctor ended in a
    UnicodeDecodeError traceback, exit 1 (#632, #432). It reads the file
    through one git config -z --list, so a section name holding a space or a
    byte that is not UTF-8, or a url holding NEL or U+2028, gives the mount
    url git and make siblings use, under any locale (#700, #668).
  • The members row. It says, member by member, what make siblings
    does with the directory beside the holder, and spells every value it
    quotes, so an ESC or a CR in a url no longer reaches the terminal (#705,
    #669, #673). It says make siblings places the clones only from a folder
    named after the holder's repository (#624, #605).
  • NEXT lines. The legs and members NEXT names every detached commit
    it would move, and sends a mount with no .git, an unborn HEAD or no url
    to a line that cures it, or to none (#559, #413, #414). manifest kinds
    prints one command for its NEXT, and counts a manifest validated only at a
    path its gate reads (#555, #548, #439). A conflicted shape pin's NEXT takes
    whole each copy both sides hold as the standard wrote it, and a leg pin put
    back from before its bump says to commit it and run the pins row's line
    (#586, #547); it reads both sides' pins, so a copy that only the side not
    taken has a files: row for is named for a person to merge, and never
    taken (#736, #687).
  • The machine row of a machine that is not ready names the preflight
    checks that failed (#552, #549).
  • A partial clone of the standard. Run from one, the shape currency
    row asks, without fetching, whether the clone holds what the comparison
    reads, and where it does not it is CANNOT ANSWER, exit 3, with the fetch
    for a person to run; update-shape.py refuses a read that fails
    (update-upstream-unreadable), never calling the copy both (#742,
    #683).
  • A naming policy that does not load. A checkout whose own
    contracts/repository-naming.yaml does not load is CANNOT ANSWER, exit 3,
    where it was INVALID (naming), exit 1, and the EXIT CODES list names an
    unfinished merge (#701, #678).
  • The pins row and REFUSAL AHEAD. The row keeps its gate where
    update-shape.py check says REFUSAL AHEAD beside a merged-away row, so it
    no longer says the apply that check prints drops the row when that
    apply is refused, and the shape currency counts name every state
    check counts (#751, #676).
  • placement and a leg's single-repository.yaml. The row asks a
    leg's top-level single-repository.yaml the staying-single record's
    question only where kind: makes it the record, as plan asks it
    (#734, #691).
  • A signal to openRepoShape --doctor. A signal sent to the command's
    shell alone waits for the doctor, so its clone of the standard is never
    removed while shape-doctor.py runs from it, and a doctor ended by a
    signal leaves nothing behind (#740, #680).

Install, CI and the docs

  • PyPI (#590, #599, #563) and the one-line install (#587, #564;
    #598, #595), above.
  • The launcher in Git Bash. Its machine: row finds python, a
    drive-letter $OPENREPOSHAPE_REPO is read as a path, a Windows-spelled
    $OPENREPOSHAPE_BIN_DIR is respelled or refused, and the tests-git-bash
    job runs install.sh's cases from #598 too (#656, #649).
  • $OPENREPOSHAPE_BIN_DIR on Windows. install.sh respells a
    Windows-spelled $OPENREPOSHAPE_BIN_DIR, or refuses it, before it fetches
    anything; --install refuses one holding : or a newline, where it
    placed the command and printed a line that cannot be used; and the
    export PATH=... note single-quotes a directory holding a double quote,
    a $, a backquote, a backslash or ! (#726, #667).
  • CI on Linux only (#597), with a result for every landing on main
    (#593, #592), and a Git Bash job on Windows (#596, #585). Every test that
    runs git clone on a just-pushed local remote now clones it in a way git's
    background maintenance cannot race (#554, #544), and the test that holds
    the doctor's NEXT lines to no # comment tail reads each string whole
    (#698, #679).
    The test session turns git's automatic maintenance off, in command scope
    and in a global config file that includes the person's own, so a test's
    copy of a repository it has just committed in or pushed to no longer
    meets a repack deleting its loose objects (#713, #645).
    The five git submodule update --init runs that still cloned a test's
    bare remotes by plain path, four of them the runs v1.0.0 listed among its
    test-fixture limits, now read them through git's transport too, by the
    url.<file URI>.insteadOf rewrite that clone_just_pushed uses, now
    built by one helper, and a pasted line's environment keeps the session's
    own git settings (#748, #662). With #554 and #578, that closes v1.0.0's
    limit on fixtures that clone a just-pushed remote by plain path.
    The tests that need a file name or a branch that is not UTF-8 ask one
    pair of helpers whether this system can carry one, and skip on Windows
    and on a disk that refuses such a name, as macOS's does; the helpers are
    held to Windows' answer on Linux, with its os.fsencode emulated (#723,
    #663).
    Tests now fail if validate-pins.py's workflow scan reads a directory, a
    dangling symlink or a looping symlink named .yml, or if siblings.py's
    headers print the holder's root and folder raw (#706, #664).
    scripts/render-cli-reference.py sets PYTHONDONTWRITEBYTECODE for the
    --help children it runs, so the generator leaves no .pyc files in the
    template tree, which v1.0.0 listed among its test-hygiene limits (#724,
    #665). Comments and docstrings in adopt-project.py, shape-doctor.py
    and a test say what the code beneath them does (#730, #666).
  • The docs. README.md and the handbook say how to install from PyPI,
    first on native Windows, and that the one line installs git-filter-repo or
    prints the command for the machine (#563), and that Git Bash runs the one
    line, tested in CI but for a typed yes (#596, #585). Each handbook section
    records the hash of every README section it reads, and README's line cap is
    a base plus one file per raise in tests/readme-cap.d/ (#551, #519). The
    by-hand setup commands dry-run the scaffold with the --visibility and
    --elected-by setup passes, and apply --add's five refusals are listed by
    their codes (#579, #550, #429).
  • The doctor's exit 3, listed whole. README.md and the handbook name
    every reason the doctor cannot answer: a pin's commit the checkout lacks,
    a partial clone missing what is compared, a read that fails, a validator
    of its own that crashed or could not be run, and its own naming policy
    not loading (#630, #683).
  • Three sentences made true. README.md and the handbook say that a
    source's own single-repository.yaml record arrives as leg: drop, that
    the adopted Makefile's CONTRACTS_DIR follows the spec leg's mount, and
    that a second setup run is refused at the plan (#627, #606).
  • validate-pins.py's codes for a neutral-product pin. README.md and the
    handbook say again that a digest that recomputes differently is the
    finding neutral-pin-digest-mismatch (exit 1), whose absence v1.0.0
    listed as a known limit, and that a declared pin file that is not there is
    the refusal neutral-pin-missing (exit 2) (#630, #460).

Known limits

v1.0.0's known limits that this release does not
close still hold; this list is not all of them. Two that work on this
release named and left open:

  • The README's and the handbook's --into sentence for family.py init
    gives its default, the directory you are standing in, but not that
    init refuses that default inside a checkout of the standard
    (family-landing-in-standard) (#695).
  • bump-leg.py --dry-run over a pin its own validators already refuse,
    such as one with a stray tab line, exits 0 and prints the old -> new
    line, where the real run exits 2 bump-leg-validators-red and rolls
    back. The dry run now says that it runs no validator (#746, #672).

New in v1.1.0:

  • A root scaffolded or adopted with --pin-source before v1.1.0 records
    the source's local path in digest_source: in
    contracts/<product>-pin.yaml. Editing it to local-clone removes the
    path from the tip, and git history keeps it.
  • A NEXT line pasted as python3 <clone>/adopt-project.py … runs outside
    the package, so on a machine whose only git-filter-repo is the package's,
    it is refused filter-repo-missing. The refusal says the package's
    adopt-project has it.
  • The package on Windows: CI runs its tests on Linux only (#597). The
    release's install check runs --version, --preflight, --doctor, the
    four tools' --help and, under PowerShell, openRepoShape --version
    there, and no test scaffolds through it on Windows.
  • Git Bash on Windows: the preflight that the one line ends with cannot take
    a typed yes there, so none of its offers can be accepted, and CI runs
    only the launcher's and install.sh's suites there. Git Bash's ln -s
    makes a copy unless MSYS=winsymlinks:nativestrict is set, and a copy of
    the command has no openRepoShape.ref beside it, so it follows main, not
    the installed release: link it with that setting (Windows lets a
    non-administrator make a symlink only in Developer Mode), or put
    ~/.local/bin on PATH (#596, #585).

Kept as known limits by the checklist triage of the L4 lines on #457 and
#459, which Brett Heap approved at 2026-10-16T13:36:07Z; each line's text
stays on its issue (#630):

  • Windows: adopting a source whose path holds a character NTFS refuses, such
    as a TAB or a control character, is unverified there, because such a path
    cannot be checked out (#457's L4-05).
  • Windows: adopting a source whose path is not UTF-8, such as b'caf\xe9',
    is unverified there, because such a path cannot be made (#457's L4-06).
  • Windows: the submodule plan of a source whose tree names hold a newline, a
    TAB or a backslash is unverified there, because such a name is not kept in
    a tree (#457's L4-07).
  • Windows: adopting a source that tracks symlinks is unverified there,
    because they check out as plain files unless core.symlinks=true
    (Developer Mode or an elevated shell) (#457's L4-08).
  • A root adopted before #471's fix, from a source that kept its own root
    .gitattributes, has no line-ending block, so on Windows its pinned
    copies still check out CRLF and make bootstrap reports
    shape-copy-drift (#457's L4-17).
  • adopt-project.py compares an origin with the source by Python's
    casefold, which is broader than GitHub's ASCII-only folding, so a path
    origin .../straße/Atlas.git and --org strasse are taken for one
    repository (#457's L4-43).
  • check says plan ok for a source whose own repository is literally
    None/<name>, False/<name> or 0/<name>, though execute still
    refuses it (#457's L4-45).
  • A test, tests/test_adopt_ignored_shape_files.py, feeds
    git hash-object --stdin a link target as text, not bytes; harmless
    today, because no target holds a newline or a non-ASCII byte (#457's L4-54).
  • setup-project.py strips only C0 control characters and DEL from a value
    it prints, so C1 controls and direction-override characters still reach
    the terminal (#459's L4-24).
  • The YAML reader is a subset by design, and still reads, where PyYAML
    refuses or reads another thing, k: - a, k: [- ], k: [e?], k: [|a],
    a block key that opens with an indicator, and a flow ?a (#459's L4-156).

Kept as known limits by the checklist triage of the L1 lines on #457,
which Brett Heap approved at 2026-10-16T11:21:17Z; each line's text stays
on #457 (#457):

  • The --pin-source digest reader runs git with lazy fetch on and the
    terminal prompt allowed, so a partial clone can fetch and a private https
    remote can prompt, where the help says it uses no network (#457's L1-02).
  • update-shape.py apply --add and the doctor read no git attributes, so a
    nested .gitattributes added after adoption makes CRLF clones drift with
    no finding that names the cause (#457's L1-08).
  • A setting passed only through a parent git's -c, such as
    core.attributesFile, reaches the split's git add but not check's
    re-encoding question, so check passes and execute refuses adopt-lost
    after both legs exist (#457's L1-10).
  • A transfer.hideRefs or receive.hideRefs pattern that covers the split
    branch passes check; execute makes both legs, and the split push is
    then denied (#457's L1-13).
  • A tracking branch the source hides from ls-remote passes check, and
    execute refuses it plan-tracking-branch-missing before any leg
    (#457's L1-14).
  • With org: and adopt_branch: both outside the alphabet, check
    refuses org first and execute refuses adopt_branch first
    (#457's L1-17).
  • Two branch names that differ "only by case" are judged with Python's
    casefold, which also folds ß to ss and ſ to s; how APFS, NTFS
    and GitHub treat such pairs is not established (#457's L1-18).
  • Under core.symlinks=false, a link at the shape/ copy of a path the
    root holds through a link above it gets a finding whose every fix leads on
    to adopt-symlinks-off (#457's L1-29).
  • Under core.symlinks=false, .github -> ci with ci -> "" reports only
    plan-empty-symlink for ci and never names .github, so the fix takes
    two rounds (#457's L1-30).
  • Under core.symlinks=false, check passes a link above a directory the
    root keeps (scripts -> ci), and execute then refuses
    adopt-symlinks-off (#457's L1-31).
  • A failure of check's probe git init other than an OSError stops it
    with "a git or gh command failed" instead of a finding (#457's L1-32).
  • A url whose organisation is refused is a dead end:
    https://example.invalid/café/Atlas.git with --org acme is refused as
    not the source, and the suggested --org café is refused unsafe-value
    (#457's L1-40).
  • An origin that url.<base>.insteadOf rewrites to a local path reads as
    a path, so nothing is compared (#457's L1-41).
  • A url's host is never checked, so a GitLab url is compared as GitHub's
    (#457's L1-42).
  • A drive-relative origin such as C:foo/bar reads as git's host:path
    form, though git on Windows reads it as a path (#457's L1-44).
  • Only plan prints a NOTE for an origin that is a path; check and
    execute print none (#457's L1-45).
  • https://github.com/acme//Atlas, with a doubled slash, names no
    repository, so nothing is compared and plan exits 0 (#457's L1-46).
  • A path origin that is not UTF-8 gets no NOTE (#457's L1-47).
  • plan does not follow a path origin to that repository's own origin,
    which would catch more mistyped --org values (#457's L1-48).
  • A submodule url naming an empty bare repository answers ls-remote, so
    check passes and execute verifies, and the recursive clone then fails
    with upload-pack: not our ref (#457's L1-50).
  • An absolute local-path submodule url that answers on this machine, kept
    in a leg pushed to GitHub, works only on machines that have that path, and
    nothing notes it (#457's L1-51).
  • A path on a mounted network filesystem (NFS, a mapped drive) looks like
    this disk to git and is asked; its spelling cannot tell it apart
    (#457's L1-52).
  • The NOTE for a submodule url that is not asked gives its network reason
    for a bare relative url such as dependency, which names no network
    (#457's L1-53).
  • On Windows, a UNC --local-remote-dir (\\server\share\remotes) makes
    every relative submodule url resolve to a UNC path, which is never asked
    and gets no NOTE (#457's L1-54).
  • A test, tests/test_adopt_pins.py, writes its stub gh with
    Path.write_text, so a CRLF emulation run fails one case; not a product
    bug (#457's L1-60).
  • #618's guard test does not clear GIT_ALLOW_PROTOCOL, so a caller's
    environment could mask a missing guard; not a product bug (#457's L1-62).

Full list