Skip to content

openRepoShape v1.2.0

Latest

Choose a tag to compare

@brettheap brettheap released this 23 Oct 11:07
· 94 commits to main since this release
961640c

openRepoShape v1.2.0

v1.2.0 is a minor release, the next after v1.1.1. It adds no command and no
flag. It adds refusals and findings, each under its own code, where a tool
read a path it could not read as absent, ended in a traceback, or said ok
and failed later, after a repository or a leg was made. It fixes how
adopt-project.py check rehearses the split, how the YAML reader takes a
key written twice and a block scalar, how bump-leg.py and family.py bump
fetch, how a holder's make bootstrap says where git's update stopped, and
what the scaffold, setup and both openRepoShape commands refuse before
they act. It changes pinned shape copies, so a project or a family holder
takes those fixes into its own gates by update-shape.py (below). It is
tagged on main.

At main 961640c, on 2026-10-22, 100 pull requests had landed on main
since v1.1.1's commit, 5017830, the last of them these notes (#1155). At
19:54Z, 91 of the v1.2.0 milestone's 91 issues were closed, and 32 merged
pull requests were in it.

What 1.2.0 fixes

  • A path this user cannot read is named, never read as absent. From
    Python 3.14, Path.is_file(), is_dir() and exists() answer False to
    any OSError, so a path this user could not search read as one that was
    not there; up to 3.13 they raise, and the run ended in a traceback. Every
    shipped tool now refuses such a path by name, on every Python, but at
    eleven sites left to #1110 and over a root's own .git (#1146, #1148;
    see the known limits), and a test holds each tool's guards to that rule
    (#934, #976, #975, #958, #956, #951, #955, #970, #1072, #1111, for #430).
  • adopt-project.py check finds what execute would meet. It
    rehearses the split in the work directory, with the source's object
    format and none of this machine's hooks; it names a symlink target no
    system can make (a NUL after its first byte, or over 4095 bytes), a link
    into the clone's .git and a pin's file reached through a kept link; and
    it refuses a hand-edited plan value read as a number or a boolean, each
    before either leg exists (#1031, #965, #1070, #1091, #1016). plan asks
    its flags' own refusals before it clones, and check and execute name
    the same first problem (#963, #1015).
  • The YAML reader refuses a key written twice, and reads a block
    scalar as YAML does or refuses it by its line (#974, #1021). The scaffold
    and adopt-project.py spell a neutral_product_pins: entry one way,
    quoted wherever a stock reader would misread it (#995, #1090).
  • Fetches that answer for a branch. bump-leg.py and family.py bump
    no longer move the mount's own branches, and leave out every refspec that
    can bring a tag, or that puts a branch under another name, through one
    rule both import (#937, #946, #1027, #1068, #1087). A commit only a tag
    reaches is named so, whichever kind of tag reaches it (#988).
  • A holder's make bootstrap fails by name where git's update
    stopped:
    at a gitlink with no url, at a commit a fetch did not bring,
    or where a signal ended git, where it said family bootstrap ok. A token
    reaches no trace, a member's make under make -jN no longer warns of the
    jobserver, and the FAILED line names the member's own exit code past a
    - make, or past || true and a - command (#889, #954, #1045, #1076,
    #1075, #950, #1023, #891). validate-family.py's remedies type as
    printed, in order (#889, #1025, #1069).
  • The scaffold and setup. A push or a seed fetch no longer waits at
    git's username prompt, a login or key the host refuses names its fix, and
    the help says what can still make the re-run wait (#971, #1012, #1101);
    the plan names the tracking branch and, on disk, the visibility (#969); a
    flag passed empty is refused by name (#972, #1010); a Windows device name
    is refused (#1010, #1041); and setup's credential-helper check and its
    step (4) read what the run will meet (#1008, #1022, #949, #1011).
  • Both openRepoShape commands. The one pipx places refuses what the
    bash one refuses wherever it prints a value or hands one to git (#947);
    under the C locale a letter past ASCII is no longer "a control
    character", the regression v1.1.1 named (#986); and the bash --doctor
    checks the ref before it clones (#967).
  • Lines a person pastes. make bootstrap's merge --ff-only,
    --continue, --abort and submodule update lines give a leg's path
    and an upstream as one shell word (#944, #973); the split message names
    each path as plan shows it (#1030); and every remedy that names
    family.py bump names --family-root (#1002).
  • A leg on a branch spelled like the tracking branch. make bootstrap
    leaves a leg whose branch folds to the tracking branch on macOS or
    Windows (Main, or another Unicode form), where it moved the branch
    under HEAD and left the index at the old commit (#948, #984, #1006).
  • CI. The full suite and the Python 3.9 jobs run on ubuntu-24.04
    (#1095), the release's install check runs the doctor over a scaffolded
    root (#945), and every release's notes are held to the one-line install
    (#887, #959).

Install and upgrade

  • The newest release, by the one line:

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

    It places the bash openRepoShape. The same line again upgrades.
    … | bash -s -- v1.2.0 names this release. openRepoShape --version
    prints openRepoShape (opensoft/openRepoShape @ v1.2.0).

  • From PyPI: pipx install openreposhape==1.2.0 for this release, and
    pipx upgrade openreposhape to upgrade one already installed.
    openRepoShape --version prints
    openRepoShape 1.2.0 (opensoft/openRepoShape @ 961640c681b54d9258e6c9d3eab8c6f46a26008b).
    v1.1.0's notes say where pipx comes
    from, and that the two installers do not replace each other's command.

  • A project's and a holder's shape copies. A copied file reaches no
    project by itself. v1.2.0 changes five of the copies an assembly root
    pins: scripts/validate-pins.py, scripts/validate-manifest.py,
    scripts/bootstrap.py, scripts/repo_shape.py and
    scripts/validate-repository-naming.py. It changes four of the copies a
    family holder pins: scripts/validate-family.py, scripts/bootstrap.py,
    scripts/siblings.py and scripts/repo_shape.py. No Makefile,
    .gitignore, .gitattributes, validate.yml or AGENTS-shape.md
    changes, nor the naming policy.
    From a checkout at the v1.2.0 release commit,
    update-shape.py check --root <path> --at 961640c681b54d9258e6c9d3eab8c6f46a26008b reports
    each as upstream-changed in a root pinned at v1.1.1 that has not
    edited it, and
    update-shape.py apply --root <path> --at 961640c681b54d9258e6c9d3eab8c6f46a26008b --yes --branch shape/update-<sha>
    re-copies and re-pins them, on the person's yes, as AGENTS.md's
    "Updating a project's shape" says. Until then the root's own gates and
    make bootstrap are v1.1.1's.

  • A holder that keeps its own scripts/bootstrap.py. The new
    siblings.py imports member_make_environment (#1023) and untraced
    (#1075) from the holder's bootstrap.py. From a pin at v1.1.1 or
    earlier, bootstrap.py changed upstream, so a kept copy reads both: a
    plain apply refuses update-conflict, and apply --accept-local scripts/bootstrap.py refuses update-accept-local-missing-name, both
    before any write. #1082's gap, where a kept copy reads unchanged and is
    not asked again, reaches only a holder pinned at a main commit between
    the two releases whose bootstrap.py is already v1.2.0's (from
    ff0c72eb up to #1075's 7e00834c). There a plain apply copies the
    new siblings.py, and make siblings, make park and make resume
    then end in ImportError (#1082, open). Merge the upstream's
    bootstrap.py into the kept copy by hand, or take the upstream's bytes;
    commit, and run apply again.

  • A YAML layout the new reader refuses. A project keeps its old
    repo_shape.py until update-shape.py apply, and the old copy reads the
    last of a key written twice without a word, so a project.yaml that
    holds a key twice passes its own validate-manifest.py until then. At
    apply the new copy refuses it, yaml-unparsable with the line of the
    second key, then update-validators-red, then ROLLED BACK. Delete the
    first of the two lines to keep the reading the project has now, and run
    apply again. A block scalar's indentation digit (|2, >-2, |2-)
    ends the same way; with the digit deleted the value reads as before,
    except where the header also holds - or +, or the body holds another
    layout #1021 refuses (#974, #917; #1021, #1004). A holder's family.yaml
    is read by the same reader.

  • The rendered READMEs, and a holder's cd line. A project's or
    holder's README.md is rendered once, when it is made, and
    update-shape.py apply rewrites only a holder's closing Shape: line.
    So a holder made with family.py init --family X --name Y before v1.1.0,
    where X and Y differ, keeps cd Y in its README, while a clone of it
    is the folder X/. Edit that line by hand, to the basename of
    family.yaml's repository:; apply leaves it as it is, by design
    (#921's U097). The comments above visibility: in project.yaml and
    family.yaml reach new roots only, for the same reason (#1102, #913).

  • What a script sees. Runs that passed on v1.1.1 and fail now, among
    them:

    • adopt-project.py plan --project NUL, Con, com1 or Lpt9, exit 2
      adopt-bad-project-name, where it wrote a plan and exited 0, and
      check of such a plan, exit 1 (#1041, #1014); the scaffold over such a
      --project, exit 2 scaffold-bad-project-name, where it made the
      three repositories (#1010, #989);
    • adopt-project.py plan with --org '', --elected-on '' or
      --reference '', exit 2, where it exited 0; the scaffold and
      family.py init with a flag passed empty, such as --id '' or
      --into '', exit 2 (#1016, #902; #972, #911; #1010, #999);
    • adopt-project.py check and execute over a hand-edited plan whose
      id:, tracking_branch: or adopt_branch: reads as a number or a
      boolean, or whose reference: is empty, refused where it was adopted
      (#1016, #902);
    • check over a symlink target holding a NUL after its first byte or
      over 4095 bytes, over a kept link into the clone's .git to a name that
      is not there when the shape is written, or over a pin's file reached
      through a kept link, exit 1, where it said plan ok (#965, #908; #1091,
      #904);
    • a path this user cannot read, exit 2, by name, in bump-leg.py,
      update-shape.py, a project's bootstrap.py and validate-manifest.py,
      family.py, validate-family.py, a holder's make bootstrap and
      make siblings, and adopt-project.py, where each ended in a
      traceback, exit 1, on 3.12, or on 3.14 read it as absent and could exit
      0; and the doctor over a root it cannot search, exit 3 (#934, #976,
      #958, #955, #956, #951, #970, #1072; #975);
    • a holder's make bootstrap, family bootstrap FAILED, where git's
      update stopped with every member checked out, at a gitlink with no url
      or a commit a fetch did not bring, or where a signal ended git, where it
      said family bootstrap ok (#889, #879; #954, #892; #1045, #996, #998;
      #1076, #1056);
    • family.py add over a commit that records the member's gitlink
      anywhere but members/<Project>, exit 2 member-gitlink-misplaced,
      where it exited 0 (#957, #920); family.py bump to a commit only a tag
      of the member's remote reaches, where the mount's own refspecs bring
      its tags under origin/, exit 2, where it pinned it (#946, #905;
      #1027, #1009);
    • a project's or a holder's YAML with a key written twice or a block
      scalar's indentation digit, yaml-unparsable, once its copies are
      re-pinned (#974, #917; #1021, #1004);
    • on Windows, a git that cannot be started, git-not-runnable, exit 2,
      where each gate ended in a traceback, exit 1 (#1028, #1001; #1062,
      #1040);
    • the package's openRepoShape --version over a $OPENREPOSHAPE_REPO
      holding one of the 41 code points the bash command refuses, exit 2,
      where it printed it and exited 0, and over a $OPENREPOSHAPE_REF such
      as a.lock/b, exit 2; a --doctor, scaffold or tool run over such a
      ref, exit 2 before any clone (#947, #939, #941).

    Runs that failed on v1.1.1 and pass now, among them: check over a
    SHA-256 source, which ended git-failed (#1031, #910); family.py bump
    and remove over a member on a branch that is not UTF-8, which ended in
    a traceback (#1072, #918); family.py bump where the member's remote
    holds a branch and a tag of one name and the mount's own refspecs bring
    its tags under origin/ (#946, #905; #1027, #1009); family.py add over
    a family.yaml whose members: is null, Null, NULL or ~, which
    was refused family-members-unrecognised (#957, #920); under the C
    locale, the bash openRepoShape, setup.sh and install.sh over a
    repository or a ref with a letter past ASCII (#986, #940); and
    update-shape.py apply and bump-leg.py over a pin or a manifest whose
    first line opens with a byte-order mark (#1050, #933).

  • A project's pin on the standard works as before. Follow
    How to pin 1.0.0 with the v1.2.0 release
    commit. A scaffold run through the package pins that commit and digest by
    itself.

What changed since v1.1.1

v1.1.1 was tagged on 5017830 on 2026-10-18. Everything below landed on
main after that commit, by area; release/v1.1, which holds v1.1.1, takes
none of it. The list runs to the commit v1.2.0 is tagged on. Each bullet
names its pull request, then the issue it closed or took a part of.

adopt-project.py

  • plan asks its flags first. plan asks the refusals that a flag's
    own value decides straight after --out, before it makes a work
    directory, reads the source with git or runs gh repo clone: --project,
    a passed --tracking-branch, the leg paths and their collisions, the
    --pin refusals, a passed --id, --org, --elected-on, --elected-by
    and --reference. Each keeps its code and its words; only when it is
    asked moved. An org/repo source is held to its own name before the
    clone too: --project Other against testorg/Thing is
    adopt-not-the-source, and an id derived from --project that is no id
    (--project North_Wind) is adopt-bad-id, with no gh call, where each
    was said after the clone. An unwritable --work-dir says why plan
    cannot clone into it ("and plan clones testorg/Thing into
    source-readonly/Thing there"), and a local source's --work-dir is no
    longer probed for a write (#963, #903).
  • An empty flag is a value. --org '' is unsafe-value, refused with
    the other flags. It was counted as given in one place and as not given in
    another, so a refusal named --org is '~x', which nobody typed, and
    adopt-not-the-source said "(--org not given)". --elected-on '' is
    election-date-malformed and --reference '' is
    adopt-manifest-invalid, in the validator's words; both exited 0 and
    recorded today's date or the default reference. A plan's own
    reference: "" is plan-manifest-invalid in check, and execute
    refuses it before any leg, where it wrote the default reference without
    a word (#1016, #902).
  • A plan value read as a number or a boolean. check and execute
    refuse a hand-edited plan whose id:, tracking_branch: or
    adopt_branch: the plan's reader reads as a number or a boolean, such as
    id: 007, tracking_branch: 007 or adopt_branch: true. The code is
    plan-bad-id, plan-bad-tracking-branch or plan-bad-adopt-branch, and
    the words say what was read ("id: was read out of the plan as the
    number 7, not as text"); execute refuses it before any leg exists.
    Before, id: 007 was adopted and project.yaml said id: "7",
    tracking_branch: 007 was adopted onto a source branch 7, and
    adopt_branch: true pushed the split to a branch True. The remedy is to
    write the value between double quotes, as typed, or to run plan again,
    which writes every value so that it reads back as typed (#1016, #902).
  • Id and name refusals say the rule. A plan with no id: whose
    legs.assembly, lowercased, is no id is plan-bad-id saying so ("id
    (not set in the plan, so legs.assembly lowercased) is 'north_wind'"), and
    its remedy says to change legs.assembly, where it named an id: nobody
    set. A passed --id, or a plan's id:, that is no id is told what an id
    may hold: lowercase letters, digits and hyphens, starting with a letter or
    digit, in the scaffold's words. Where the source is adopted in place, the
    naming refusal says the rule for the new name, one CamelCase token with no
    hyphen, underscore, dot or space, before it says to rename the source
    first; adopt-not-the-source and plan-not-the-source say that the
    source's own name is no remedy where the naming policy refuses it. A
    refused name speaks before a passed --id's. AGENTS.md's adoption step
    says that --org defaults to the organisation of an org/repo source or
    of a url origin, and that a path origin, or none, needs --org
    (#1016, #902).
  • One first problem in a hand-edited plan. check and execute ask a
    plan's org: and id: before its leg paths and branch names, so both
    name the same first problem where a plan has more than one. A plan with
    id: "North Wind" and legs.spec_path: "../spec" was plan-bad-id in
    check and plan-bad-leg-path in execute; it is plan-bad-id, exit
    2, in both, and an unsafe org: beside the same path is unsafe-value
    in both. A pins: entry that the pin reader names is that one finding: a
    pin edited to product: "a<TAB>" was three findings,
    plan-pin-malformed, plan-unwritable-value and plan-manifest-invalid,
    and is plan-pin-malformed alone. A pins: that is no list is said as
    the reader says every other key ("pins: is the boolean true, not a list
    of pins"), and a finding about a pin's digest_source: says that
    execute writes the pin file from the commit and the digest plan
    records, "and how it was read, and nothing else" (#1015, #901).
  • A name no Windows disk can make. plan --project NUL, and Con,
    com1 and Lpt9, in any case and with or without an extension, is
    refused as adopt-bad-project-name, exit 2, with the other flag
    refusals, before a work directory, git, gh or a plan file exists.
    Before, it wrote a plan and exited 0. --project names the assembly root
    and the folder execute clones it into, and no Windows disk can make a
    file or a folder of a device name. check finds the same name in a
    plan's legs.assembly as one FINDING, exit 1, and asks gh nothing,
    where it said plan ok after two gh repo view calls; execute refuses
    it. It is the one question scaffold-project.py and family.py init ask
    (#1010), and the first of the names execute cannot make. Where
    --project is the source's own name, the remedy gives the way out in
    place, to rename the source on GitHub first. A name the naming policy
    refuses as well, such as NUL.git, keeps the refusal it had:
    naming-unclassified with an --id, and adopt-bad-id for the derived
    id without one (#1041, #1014).
  • A source this user cannot reach. A --source, or a plan's recorded
    source.local_path, that this user cannot reach, one under a directory
    it cannot search, is source-unreadable, exit 2, with the chmod u+rx
    cure for the directory and every directory above it. It is never read as
    absent and never sent to gh: an org/repo spelling beside an
    unreadable ./org is not cloned, and a recorded source is neither
    plan-source-gone nor cloned again. On Python 3.12 these were a
    PermissionError traceback; on 3.14, where Path.exists() answers False
    to any OSError, a --source was source-unresolvable, an org/repo
    spelling was cloned from GitHub, and a recorded source was
    plan-source-gone or cloned again. A leg under --local-remote-dir
    whose stat is refused waits for the leg question, which refuses the
    folder by name, adopt-folder-cannot-be-made, where 3.12 ended in a
    traceback. plan-root-symlink says of a place it cannot read "cannot be
    read (Permission denied), so whether anything is there cannot be told
    until this user can search every directory above it (chmod u+rx)",
    where it said "and nothing is there". --source's help in docs/cli.md
    says so (#970, part of #430).
  • A source this user cannot enter. A --source directory that is
    itself at mode 000 is source-unreadable too, where plan ended in a
    PermissionError traceback, exit 1. A recorded source in that state,
    which check called a clone "no longer whole (git finds no repository
    there)" and cloned again with one gh call, is the same refusal, before
    anything is cloned or written. A plan written in UTF-8 and read under
    LC_ALL=C PYTHONUTF8=0 asks a recorded path that the locale cannot spell
    as the plan's UTF-8 bytes, so check says plan ok where it said
    plan-source-gone, as it does in UTF-8 mode; where those bytes name
    nothing either, the refusal says so, naming the locale ("this locale's
    file names, ascii, cannot spell it"). A one-byte locale such as Latin-1
    still asks only the spelling as written (#1072, #918).
  • check asks git as the split will meet it. The throwaway
    repositories check makes, to rehearse the split, to probe
    .gitattributes and to ask how files are stored, are made in the work
    directory (--work-dir, or the one check makes in TMPDIR, never
    TMPDIR itself) with the source's object format and none of this
    machine's hooks, and each names its branch. A SHA-256 source, which ended
    check in git-failed at the rehearsal's read-tree, is plan ok, and
    so is a SHA-1 source under GIT_DEFAULT_HASH=sha256. A path of the
    source commit that git refuses as a path (.., .Git/x,
    notes/.git/x) is the new finding plan-path-git-refuses, which names
    the paths and which execute refuses before it makes either leg, where
    check ended in git-failed. A repository git cannot make in the work
    directory is one finding, adopt-git-unasked, with git's reason, and
    execute refuses it before any leg. A symlink probe whose git init
    fails takes git to check symlinks out as symlinks and prints one NOTE
    with git's words, where check said "a git or gh command failed" (#1031,
    #910).
  • A symlink target that git cannot make. check names a symlink whose
    target holds a NUL after its first byte, or is longer than 4095 bytes, as
    plan-bad-symlink-target, before any leg exists. execute refuses it as
    it refuses any check finding, and plan still writes the plan and
    notes it. Before, check said plan ok and execute stopped after both
    legs existed, with a clone that exited 128, "File name too long", or with
    adopt-lost. The finding says what git makes of the blob ("a link to
    ci, not to the 5 bytes the blob holds"), or the bound; 4095 is Linux's,
    and macOS and XFS take less (see the known limits). A root-kept
    .github -> ci<NUL>xx is no longer plan-root-symlink saying it leads to
    ci\x00xx. A link at project.yaml is one finding, plan-empty-symlink
    or plan-bad-symlink-target, ending with where the link sits and what
    ends the manifest block, where plan-root-blocks-manifest named it again;
    a link at a copy under shape/, or at a pin's file, is one finding too,
    where plan-root-blocks-shape-copy or plan-root-holds-pin named it
    again with a remedy, to send it to a leg or drop it, that cannot help a
    link no clone makes. check reads the link targets in one
    git cat-file --batch, not one git cat-file blob for each link, walk
    and disk (#965, #908; #1070, part of #979).
  • A link into the clone's .git. check names what the clone's
    .git holds as git keeps refs: it asks a throwaway repository in the work
    directory whether this git uses the reftable format and writes reflogs. A
    root-kept link to a name that is not there when the shape is written,
    such as CLAUDE.md -> .git/refs/remotes/origin/main or
    -> .git/modules/nope, is refused as plan-root-symlink before any leg
    exists, saying that the shape's file would be written through the link
    into .git, where no commit holds it, or would find no directory there
    and stop after both legs. Before, check said plan ok and execute
    said adoption verified, with the shape's CLAUDE.md in a file of the
    work clone's .git. A link to a name inside the object store, a leg's
    store or the reftable tables is refused as one check cannot tell about;
    a link to a name git does make there, such as .git/refs/heads/main or
    .git/logs/refs/heads/main, is adopted with the shape's copy beside it.
    .git/refs/../HEAD and .git/../AGENTS.md, refused as "not a directory
    in the assembly root", are adopted the same way. Where git keeps refs as
    a reftable, a link to .git/reftable is adopted with the copy beside it,
    where it was refused, and a link to .git/packed-refs or .git/logs/HEAD
    is refused, where it passed and execute wrote the shape's file into
    .git or stopped after both legs (#1091, #904).
  • A pin's file through the links the root keeps. check asks a pin's
    file, the shape's own and each declared pin's, where the root walk
    reaches it through the links the plan keeps above it. A file
    governance/openglass-pin.yaml under contracts -> governance is
    plan-root-holds-pin, naming the link and saying to send it to a leg or
    to rename the file in the source. Before, check said plan ok and
    execute adopted, and the first bootstrap exited 2 with
    yaml-unparsable, or with neutral-pin-unreadable where the file is a
    link to another file; for the shape's own pin, execute stopped after
    both legs with adopt-shape-pin-unreadable.
    plan-root-blocks-shape-copy says what the materializer does at a link
    or a file where a copy goes: it stops there, materialize-copy-blocked,
    after both leg repositories were created and pushed, so execute refuses
    before either exists (#1091, #904).
  • A --work-dir that runs round a symlink loop. plan, check and
    execute refused a --work-dir that is a symlink round a loop as
    "making it a directory failed (File exists)", which names a file in the
    way, with the way out for a folder this user cannot write in. They now
    say that --work-dir <dir>/self "cannot be made: it is a symbolic link
    that leads round a loop", or, for a path that runs through one, that "the
    way to it leads round a loop, at the symbolic link /loop", naming the
    --work-dir as given and not as resolved. The way out says to remove the
    link, make it lead to a directory, or pass another --work-dir. A link
    to a file is named as one ("a symbolic link that leads to , which
    is not a directory"). A file at the --work-dir keeps the system's
    words. The code is still adopt-work-dir-unwritable, exit 2, and nothing
    is made (#1065, #978).
  • Names printed one way, and never raw. The split commit's message,
    which is also the pull request's body, lists each moved, dropped and kept
    path as plan's report shows it. A source that keeps a file named
    caf<0xE9>.md, which is not UTF-8, and one named cr<CR> had them
    listed as caf?.md and cr, two names of other paths; they read
    caf\udce9.md and cr\r, each between backticks. A backtick is spelled
    \x60 and a double quote \", as the plan's own table spells them, so a
    spelled name copied between the plan's double quotes is the entry it
    names, where the old spelling of a backtick kept its backslash and named
    another entry, and a bare " ended the quoting and left the plan
    unreadable. A name that begins and ends with a backtick, or ends in a
    space, which git commit strips, is spelled too. plan prints a path
    policy's question: and rule id the same way, where an ESC in either
    reached the terminal raw. The history scan's git-failed is one
    printable line, where a source folder named with a line feed and an ESC
    split it over two and sent the ESC raw. plan's table pads a name as
    stdout prints it, so under LC_ALL=C PYTHONUTF8=0 a source holding
    café.md, printed caf\xe9.md, no longer leaves its leg column 3
    columns to the right. A follow-up that names a file, the read-across one
    among them, spells it as the listing does (#1030, #900).
  • The Makefile follow-up says where the block waits. Where the source
    keeps a Makefile of its own at the root, plan's follow-up, check's
    output and the split message said "the root Makefile wires
    CONTRACTS_DIR". The shape's Makefile goes beside it as
    shape/Makefile, with the block, and the root wires nothing, so
    make contracts-dir at the root answered "No rule to make target". The
    follow-up now says that the shape's Makefile wires it, that the block
    waits in shape/Makefile until the two are merged, and that the run adds
    none of it to the root's Makefile (#964, #906). It also names a kept
    makefile or GNUmakefile. GNU make reads the first of GNUmakefile,
    makefile and Makefile it finds, so either comes before Makefile,
    and make there exports CONTRACTS_DIR only once that file does, or
    includes a Makefile that does. Where a disk ignores case, a kept
    makefile is Makefile, and the follow-up says the shape's goes to
    shape/Makefile there. A source that keeps only Makefile, or none of
    the three, gets the line it had. README says that the shape's Makefile
    exports CONTRACTS_DIR, and that it is the root's Makefile or, where the
    source keeps a Makefile of its own, shape/Makefile, which holds the
    export until a person merges the two (#1046, #977).
  • Four more printed sentences say what the run does (#964, #906):
    • Where a follow-up says a file must read across the assembly root,
      check's mount the legs at X/ and Y/ and the split message's
      mounted back here at X/ and Y/ end ", so from the code leg the spec
      leg is ../spec/", and, where contracts/ is there too, " and the spec
      leg's contracts/ is ../spec/contracts/".
    • A refusal that said execute "would stop there" says where: for a
      refused store or copy, "no later than committing the split, and sooner
      where a step before it meets the same rule", and for a refused
      checkout, "at its first clone of the source".
    • The sentence on a root .gitattributes link no longer says "a path
      names no attribute". Git reads a link's text as one line of rules, so
      it names an attribute where the text holds a space, a tab or a carriage
      return; for a link to a path, no line-ending rule reaches the pinned
      copies outside it.
    • Under --local-remote-dir, WHAT WILL HAPPEN prints
      create <Project>-spec and <Project>-code with no (private), since
      execute makes bare repositories. Against GitHub the line is as it was.
  • An identical drop, and answered_paths: read path by path.
    execute's verification counts a path dropped from the plan whose bytes
    the root also holds as drop, not as the root's landing: where an
    identical drop made the table read root 2 of 4 and drop 0 of 4, it
    reads root 1 of 4 and drop 1 of 4. answered_paths: is looked up by
    each path's own names, not by asking every entry of the plan: the same
    answer, in 0.26 s where it took 3.65 s, for 120,000 files and 300
    entries. README's verdict table and the handbook's name the exception in
    the MISPLACED (n paths) row: a path answered_paths: answers into that
    leg is not one (#1073, #914).
  • The Free-plan hint and an internal root. Where a root's plan is read
    as free and the root is internal, the hint said "acme/Atlas is
    private". It now says "acme/Atlas is internal, which this hint treats as
    private", then the hint's own words. The scaffold and family.py init
    print the same hint through the same function, so each says the same.
    repo_shape.py's hint is not touched, so no project re-pins (#1038,
    #994).

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

  • A path nobody may read is named, not taken for absent: bump-leg.py.
    From Python 3.14, Path.is_file(), is_dir(), exists() and
    is_symlink() answer False to a stat the system refused, where up to
    3.13 they raise. So a path this user could not search read as one that
    was not there. The worst case was in bump-leg.py: on 3.14, with a
    project's scripts/ at mode 444 or 000, it printed SKIPPED for both of
    the project's validators, committed the bump and exited 0. Now an
    unreadable validator is refused before anything is written, a dry run
    included, as bump-leg-validator-unreadable, exit 2, naming each file
    with the cure (chmod u+r, or the advice for a cause that is not a
    permission, such as restoring the file from the project's history). A
    validator that is not there is still SKIPPED. A leg's checkout it
    cannot search is bump-leg-submodule-unreadable, naming <path>/.git,
    and a root, or a directory above it, that it cannot search is
    bump-leg-manifest-unreadable, naming project.yaml, exit 2. Those two
    were bump-leg-uninitialized and bump-leg-root-not-a-project on 3.14,
    and a PermissionError traceback on 3.12 (#934, part of #430).
  • The same, for update-shape.py. A path in the root or in the
    upstream that this user cannot read is update-unreadable, named from
    the root, exit 2, with the same cure, on every Python. Before, a
    scripts/ it could list and not search ended check in a
    PermissionError traceback on 3.12, and on 3.14 read as five
    copy-missing, with apply refusing update-conflict; a contracts/
    at mode 000 read as update-pin-missing ("A project without a shape pin
    is adopted with adopt-project.py, not updated"); a shape/ it could
    not search made a pin row read as merged away, which apply would have
    dropped; and an --upstream under a folder at 000 was "neither a path on
    this machine nor an owner/repo name". apply reads every file it
    judges by before its first write. A validator it cannot stat is refused
    and the run rolled back, never SKIPPED. A rollback lists an --add
    file it could not remove, where on 3.14 it said the tree was exactly as
    it had been. A read refused in prepare closes the bare clone it made of
    a pin's owner/repo, where it left that clone behind (#976, part of
    #430).
  • The same, for a project's own bootstrap.py and
    validate-manifest.py.
    The assembly root's two pinned scripts name a
    path this user cannot read, relative to the root, and never read it as
    absent. With scripts/ at 000, bootstrap.py on 3.14 said SKIPPED for
    all three validators and ended bootstrap ok, exit 0, and on 3.12 ended
    in a traceback. It now refuses validator-unreadable: scripts/validate-repository-naming.py: cannot be read (Permission denied), exit 2, before any validator runs. A project.yaml behind a
    folder nobody may enter is yaml-unreadable, exit 2, before step (a),
    where on 3.14 no leg was placed and the run said bootstrap ok. A leg
    whose .git it cannot read is named and left as it is. A cherry-pick
    stopped in a leg whose sequencer/ it cannot read leaves the leg
    detached, named, where bootstrap.py put the leg on main on every
    Python. A governance/ it cannot enter heads step (c)
    register, if one is there:, says why, and claims nothing from it, where
    3.14 said authority is not wallet-carried in this org. In
    validate-manifest.py, a declared referent's pin file behind such a
    folder is yaml-unreadable, exit 2, where 3.14 gave the finding
    naming-referent-missing. Both scripts are pinned copies, so a project
    takes them with update-shape.py (#958, part of #430).
  • bump-leg.py's fetch. The fetch of a leg prunes the branches its
    remote deleted, so a dry run no longer says it would pin a commit no
    branch reaches any more, and it moves no branch of the leg's own. It runs
    git fetch -q --prune --no-prune-tags --refmap= <remote> <refspecs>.
    Before, a leg configured +refs/heads/*:refs/heads/* had its local
    main moved off an unpushed commit by a dry run, and with the leg on
    main the fetch failed (refusing to fetch into branch ... checked out)
    and the run answered from stale refs (#937, #898). The leg's own
    remote.origin.fetch refspecs are carried only where they write under
    refs/remotes/origin/, and now it leaves out every one of them that can
    bring a tag under origin/, and one that gives another namespace the
    branches' names. A branch and a tag of one name no longer fail the fetch,
    and a tag no longer answers for a branch. It is the rule family.py bump
    asks of a member, now one function, origin_fetch_refspecs in
    scripts/shape_materialize.py, that both tools import: the rule was two
    copies, and family.py's had been fixed alone (#1068, #1037).
  • The fetch for a branch's answer, in both tools. family.py bump and
    bump-leg.py leave out of the fetch every refspec of the repository's
    own onto the branches' names, whatever its source, and every refspec
    that puts a branch under a name not its own (x onto release/x, every
    branch into a folder), so a repository whose only refspec is
    +refs/heads/release/*:refs/remotes/origin/*, with the branches x and
    release/x on its remote, no longer fails where its own
    git fetch origin works. The tools' fetch died Cannot fetch both refs/heads/x and refs/heads/release/x to refs/remotes/origin/x, exit
    128, and both runs of both tools answered from stale refs. A refspec that
    keeps a branch under its own name, one branch or a folder of them, is
    still carried. The cost is the one that tags onto the branches' names
    already had: a ref the repository kept under another name is written back
    by its own next git fetch origin, and a refspec without + is refused
    there as non-fast-forward, exit 1, where the two commits are not in line
    (#1087, #1066).
  • A commit only a tag reaches. bump-leg-commit-local-only for such a
    commit says a tag of <repository> reaches it, but no branch of <repository> does, and to push it to a branch of the leg, since a bump
    pins only a commit a branch of the leg's remote reaches. It said
    somebody had committed inside the leg's mount and never pushed it (#937,
    #898). The check compares the commits the tags point to, not the tag
    objects, so a lightweight v1 in the leg against an annotated v1 on
    the remote at the same commit, two annotated v1 made apart, and a tag
    of a tag all reach it. family.py bump asks the same check and gets the
    same change (#988, #982).
  • A leg with no origin. A leg whose origin was renamed or removed
    is told so. The fetch's note says it has no remote named origin, and
    the refusal says to give the mount a remote named origin, for the
    manifest's owner/name, and run again. It told the person to fix the
    network or the credential. A commit the leg's own object store lacks is
    refused as bump-leg-commit-not-on-remote, naming the missing remote
    (#937, #898).
  • update-shape.py's kept-copy check and a handler that raises. The
    check behind update-accept-local-missing-name read an
    except ImportError: handler as a fallback for what it binds, and never
    asked whether the handler then raises. A kept copy that binds
    NEW = None and then re-raises, beside a repo_shape.py without NEW,
    was not refused, and Python stops on the ImportError. It is refused
    now. A bare raise hands the ImportError to the next try out, so a
    nested try whose outer handler binds the name still starts and is still
    kept. Any other raise, such as raise X, answers nothing, since
    whether an outer handler catches X depends on its list: a copy that
    raises an error of its own is refused where Python stops on it, and one
    whose outer except ImportError: would catch it, which Python starts, is
    refused too. A handler whose raise sits under an if, or inside a
    with, is still read as binding (#935, #897).
  • A byte-order mark, in the three line rewriters. Since v1.1.1 every
    reader takes a leading byte-order mark off. The three line rewriters did
    not, so update-shape.py apply refused a pin whose first line is
    commit: after a mark as update-pin-unrecognised ("0 commit
    line(s)"), and a manifest whose first line is shape: after one as
    update-manifest-no-shape, and bump-leg.py refused a leg's pin the
    same way as bump-leg-pin-unrecognised. Each finds its key after any
    number of marks on the first line and writes the marks back as they
    were. A mark on a later line is still refused as before. A marked
    shape: on line 1 beside a second shape: header is refused by name,
    update-manifest-unrecognised, as the reader refuses that file, where
    only the second block was seen and rewritten (#1050, #933).
  • make bootstrap's pasteable lines. The
    git -C <leg> merge --ff-only <upstream> line printed when a leg is
    moved back, and the --continue and --abort lines for an operation
    stopped in a leg, give the leg's path and the upstream as one shell word
    that a shell reads back as the same bytes. A name that prints as itself
    is quoted as it was. Any other, a byte that is not UTF-8, a control
    character, a U+2028, a bidirectional control or a name the stdout cannot
    write, is "$(printf -- '<octal escapes>')", which dash, bash and zsh
    read. An upstream of origin/a, the byte 0xff and b was printed as
    'origin/a\udcffb', which sh reads as the characters it holds, so the
    line named no ref (#944, #932). A word that begins with = is quoted, as
    zsh expands a bare =word to the path of a command. The choice between
    the quoted word and the printf word follows the bytes git is handed,
    which are UTF-8 on Windows, and not the stdout's codec alone: under a
    latin-1 stdout, origin/café was quoted and printed as the byte 0xe9,
    where the ref holds 0xc3 0xa9. And the NOT INITIALIZED command is
    git submodule update --init -- <word>, the leg's path one word after a
    --, with ./ in front of a path that begins with : so git does not
    read it as pathspec magic. Before, my spec was two pathspecs, sp'ec a
    syntax error in every shell, -spec an option and =spec failed in zsh.
    A name that ends in a line feed or holds a NUL has no such word and keeps
    the text it had (#973, #961). bootstrap.py is a pinned copy.
  • make bootstrap and a branch that differs from the tracking branch
    only in case.
    Where a leg's HEAD is on Main and the tracking branch
    is main, bootstrap.py leaves the leg where it is. No ref moves and
    none is made. It says where the leg is, why it is left, and the way out.
    On a disk that folds case, macOS or Windows, Main and main are one
    ref, so branch -f main moved the ref HEAD stood on and left the index
    at the old commit (git status read D NEW-CONTRACT.md and
    M README.md), while the run said the leg was on a branch that keeps
    its commits (#948, #896). The same holds where the tracking branch is
    missing and any refs/heads/ branch, packed or loose, folds to it, a leg
    detached at the pin included. Only HEAD's own ref was asked before, and
    making main over such a twin would shadow the twin's commits. A name
    that differs in Unicode form is a twin too: names are compared in NFC
    first, so the NFD spelling of café is a twin of the NFC one, as APFS
    reads the two as one file. Where HEAD is on the twin and main is there,
    behind the pin or at it, the way out is git -C <leg> switch <branch>,
    which moves no branch. Where main is missing, it is a rename,
    git -C <leg> branch -m Main Main-kept, which keeps the commits and
    moves HEAD with the branch. The new name is the first of <twin>-kept,
    <twin>-kept-2 and on that no branch of the leg folds to and none is
    stored under, so the pasted line works and the next run makes the
    tracking branch; Main-kept over Main-kept/x, which git refuses, is
    skipped (#984, #962; #1006, #1000). bootstrap.py is a pinned copy.

scaffold, setup, family and the shared readers

  • A key written twice is refused. The YAML reader in
    scripts/repo_shape.py, which the tools and the validators in a project
    and a holder read their YAML with, refuses the second of a key written
    twice in one mapping, block or flow, by that line
    (line 2: key 'a' is written twice), where it kept the last without a
    word. A flow collection is named by the line it is on. a and "a" are
    one key, and so are 1 and "1", because the reader reads every key as
    text. The same key in two mappings or two sequence items, or a repeated
    value, is not a key written twice and still reads. A block-scalar line
    indented less than the block's first line is refused by its line, where
    its first characters were cut; one that opens # ends the block as a
    comment, as YAML reads it. update-shape.py's refusal of a manifest
    holding two shape: blocks now says the reader refuses a key written
    twice, and no longer says it keeps the last (#974, #917).
  • A block scalar is read as YAML reads it, or refused by its line. A
    block scalar's indentation digit (|2, >-2, |2-, |0, |10) is
    refused by its header's line (line 1: a block scalar's indentation indicator is not supported: '|2'), where the digit was accepted and
    ignored, so k: |2 over a and b read 'a\n' and lost b. +
    keeps the empty lines at the end of the block, where it read as no
    indicator. A line of spaces deeper than the block is text. A blank line
    above the first line of text that holds more spaces than that line is
    refused by its line, and so is a tab left of the block's column in a line
    that holds nothing else. A folded block keeps the line breaks around a
    line that opens with a space or a tab, and a block with no line of text
    reads '', where it read '\n'. A seeded differential against PyYAML
    6.0.3, over two runs of about 15,000 and 23,000 generated documents,
    found no disagreement, where the reader before it disagreed on about
    6,000 in each. No tracked YAML file and no tool writes any of these
    layouts (#1021, #1004). "Install and upgrade" says what a project whose
    YAML holds one meets at apply.
  • validate-pins.py, --root and gh's words. --root is named as
    shown_path names a path, as it is where every character is printable
    and as a quoted repr otherwise, and gh's stderr is spelled as git's is,
    on one line, each control written as its escape, so an ESC or a line
    break in either no longer reaches the terminal or prints a forged line:
    the not-a-git-repo line, which every tool that calls find_repo_root
    prints, and the gh-unreadable skip, whose gh text is now one line (a
    line break prints as \n). The repository in every gh detail,
    gh-response-unreadable and gh-tree-truncated among them, is named the
    same way. gh's stderr that is not UTF-8 is a skip, where it ended in a
    UnicodeDecodeError traceback, exit 1. gh's stdout that is not UTF-8, or
    JSON that is not a tree listing (a list, null, a tree that is a
    number, a row with no mode), is a skip that names
    gh-response-unreadable, where [] ended in an AttributeError
    traceback, exit 1 (#952, #919; #985, #968).
  • What stands in a pin's place is named. Where a leg's pin, the shape
    pin, a neutral-product pin, project.yaml or a shape copy is not a file,
    validate-pins.py says what stands there (is a directory, not a file,
    a FIFO, a socket, a device, a symlink to nothing, a symlink loop, a
    symlink to a directory) and gives a remedy that fits, to move it aside
    and restore the file from the project's history, where it said the file
    does not exist under the remedy for a missing one. Where something that
    is not a directory stands where contracts should be, the line says
    which is a file, not a directory, or what else it is. A file that is
    not there keeps its words (#952, #919; #985, #968).
  • A git directory this user cannot open is refused by name. A leg's
    .git gitfile at mode 000, or a leg's git directory (.git/modules/spec,
    or spec/.git where it is a directory) that this user cannot search, is
    pin-submodule-unreadable, naming the path and the permission, with the
    checkout remedy, where validate-pins.py said pin-commit-unresolvable
    over git's own failure, under a lockstep remedy that does not fit:
    fatal: error opening ...: Permission denied for the gitfile, fatal: not a git repository for .git/modules/spec, and an empty exited 1
    for a spec/.git directory. A gitfile that ends in a CR and a line feed,
    and one that names nothing, are read as before (#952, #919; #985, #968).
  • A chain source that is there and cannot answer. A project's
    validate-manifest.py names a SHAPE_PIN_SOURCE_<LINK>, or a checkout
    beside the root, that is there and could not answer, with the reason
    (the openXdox beside this root, <path>, could not answer: it holds no project.yaml; SHAPE_PIN_SOURCE_OPENXDOX would let it be read), and
    offers only a source that is not there, where it offered the one that was
    there. A manifest beside the root that is not a declaration reads
    reached but could not be read as a declaration, where it said
    not reachable here. A SHAPE_PIN_SOURCE_* variable that two products
    of one run share answers for no chain, in the naming gate and in
    validate-manifest.py alike: a project with the pins openss and one
    that spells it with a sharp s, which share SHAPE_PIN_SOURCE_OPENSS,
    failed its gate with the variable set (the declared chain openss -> openQuill is broken, exit 1); it now gives the same answer, exit 0, with
    the variable set and unset, and offers it to no chain. A first chain link
    that is a --pin is recorded in the pin's spelling, so
    --pin openKdox@<sha> --referent-chain 'open<U+212A>dox,openInk'
    records openKdox -> openInk (#981, #909).
  • A --link-source, --pin-source or --link-remedy that holds =.
    A chain link or a declared pin whose name holds = can now be named. A
    --link-source key is the longest chain link before an = that leaves a
    path after it, so the offer the naming gate prints for a link holding =
    (--link-source open=y=<path>) verifies that link when typed as printed,
    where the gate cut the key at the first =, open, which no chain
    names, and left open=y declared-unverified under the same offer.
    open=y= with no path is refused link-source-no-path, exit 2, as
    openXdox= is. validate-pins.py reads a --pin-source key the same
    way, and refuses open=y= as pin-source-no-path; no scaffold writes a
    pin that holds =, so that reaches a hand-edited project.yaml alone,
    and a pin's digest already stopped a wrong source from passing.
    --link-remedy reads a link holding = whole, strips its words and
    notes a remedy no holder used; a link whose owner, the text before its
    last /, holds an = is no link, so
    --link-remedy=openXdox=a/openYdox=b gives openXdox the words
    a/openYdox=b, where it gave openYdox the words b. A set
    SHAPE_PIN_SOURCE_<LINK> that the run's guard passes over, because two
    products of the run share it, is named once by a link-source-collides
    note, in the gate and in validate-manifest.py (#949, #907; #1011, #990,
    #991; #1044, #980; #1067, #1053).
  • setup-project.py's step (4) reads what step (5) does. Step (4) ran
    the naming check from the standard's own folder, where a name on its
    command line takes that folder as the project root, so a checkout of
    openXdox beside the standard answered for a link before the person's
    --pin-source. It now runs from an empty folder made inside a temporary
    folder, so nothing stands beside it, and takes both away when the check
    is done. A temporary folder it cannot make is a refusal that says to
    point TMPDIR (TEMP on Windows) at a folder you can write in, where it
    would have been a traceback. --pin-source ./nowhere/, and a blank value,
    name the same path in step (4) as in step (5). The Windows hand clone
    that setup prints for finishing by hand passes -c core.autocrlf=false,
    as setup's own clone does (#949, #907, #926; #1011, #991).
  • The scaffold's plan says what it is approved for. The plan has a
    branch row, the tracking branch each of the three is created on, and,
    under --local-remote-dir, a visibility row (<visibility>, which project.yaml records; a bare repository on disk has none);
    family.py init's plan gets the same two rows, and setup's step (5)
    shows them. Where gh could not read the root's visibility (not where gh
    does not find the root, which is the ordinary --reuse-empty-repo run),
    a NOTE before the plan says, with no --visibility, that a leg this run
    creates is created private and does not inherit the root's, and to pass
    --visibility before any leg exists, and, with one, that the run cannot
    say whether the flag agrees with the root. A reused leg whose visibility
    could not be read is named in a NOTE of its own, where it was counted as
    the run's. The remotes row says that a private repository this token
    cannot see is not found either, and that gh repo create then fails for
    it. An internal root whose organisation's plan could not be read is told
    that GitHub gives internal visibility only to an organisation an
    enterprise account owns, so the plan is not what the Free-plan hint hangs
    on. family.py init prints its members' credential NOTE for every
    holder on GitHub, public or not, worded about the members: a member a
    person adds that is, or has a leg that is, private or internal needs a
    way for the holder to read it, where it went by the holder's own
    visibility (#969, #913).
  • What visibility: records. The comment above visibility: in
    project.yaml says the field is the visibility the run that made the
    file asked for, in the order the scaffold takes it: --visibility where
    one was given, else the visibility GitHub gave, before the plan, of an
    existing root the run reuses, else private (a root whose read failed
    too). The one in family.yaml says it is family.py init's
    --visibility, private unless the run was given another, not a
    reading of any repository, and the holder's alone. Both say that a
    repository the run reuses keeps the visibility it has, so the field can
    differ from what GitHub says of it. project.yaml's said that the field
    was all three repositories' visibility, and family.yaml's did not say
    what it records. Nothing that is recorded changes. Neither file is a
    pinned copy, so a project or a holder already made keeps the comment it
    was made with (#1102, #913).
  • An empty value is read as given. In the scaffold and
    family.py init, a flag passed empty was read as not given. --id '' is
    now scaffold-bad-id or family-bad-id
    (--id '' must match ^[a-z0-9][a-z0-9-]*$); --elected-on '' is
    election-date-malformed; and --name '', --reference '' and
    --elected-by '', with init's --name, --created-on and
    --created-by, are refused as scaffold-manifest-invalid or
    family-manifest-invalid in the validator's own words. With no identity
    at all, --elected-by '' is no longer told no --elected-by. The folder
    flags --work-dir and --local-remote-dir, and init's --into,
    passed empty, were read as ., the directory the run stood in, so
    init --into '' from the root of a checkout planned the holder inside it.
    Each is now refused by name, work-dir-no-path,
    local-remote-dir-no-path and into-no-path, before anything is made,
    dry run alike, and the five folder flags' --help say what an empty
    value and . mean (#972, #911; #1010, #999).
  • A passed --id and a name are told in the order a person can mend
    them.
    A passed --id that is no id is told what an id may hold,
    lowercase letters, digits and hyphens starting with a letter or digit,
    but only after the name passes: a name no assembly root or holder can
    have (naming-unclassified, naming-role-mismatch, a device name,
    GitHub's 100 characters) speaks first, so --project Atlas-spec --id foo_bar is refused naming-role-mismatch, where it was told to mend its
    --id and the retyped id then met that refusal, which no --id mends.
    --id=-north and --id 'north wind' are *-bad-id, where they were
    unsafe-value. --family open_glass is no longer sent to openGlass,
    which is an assembly root's form and no holder's. An empty target or
    holder folder that this user cannot list is told the permission to give,
    and that the run cannot say whether it holds a tree, where it was told
    There is no --force: re-running over a live tree is not a scaffold
    (#972, #911, #930).
  • A name no Windows disk can make a folder of. The scaffold refuses
    --project NUL, Con and the other Windows device names (CON, PRN,
    AUX, NUL, COM1 to COM9, LPT1 to LPT9, in any case, with or
    without an extension) as scaffold-bad-project-name, by the one question
    init asks of --family, before a passed id's alphabet and before any
    gh call, against GitHub and under --local-remote-dir alike, dry run
    included. It took them, and made the three repositories and their
    folders (#1010, #989).
  • The scaffold asks nothing at the terminal on a push or a seed fetch.
    Git's own terminal prompt is now off for each leg's push, the root's push
    and the --reuse-empty-repo seed fetch, as it was for the probe alone,
    so a first scaffold and a re-run fail in git's words
    (terminal prompts disabled) instead of stopping at Username for. A
    push git has no login for names the fix, gh auth setup-git. A leg git
    reaches over ssh, because a url.<base>.insteadOf rule sends
    https://github.com/ there, is refused with ssh's reason
    (git@github.com: Permission denied (publickey).), where the refusal gave
    git's generic Could not read from remote repository.; the probe runs
    under LC_ALL=C, so the line is found in any locale. A push refused for
    its stored login (Authentication failed), or for its ssh key
    (Permission denied (publickey)), says how to replace it, then the
    --reuse-empty-repo re-run, before the ruleset hint; for the login it
    says the re-run is refused too until git has one the host takes; for the
    key it names the ssh -T check, and does not offer gh auth setup-git,
    which changes nothing over ssh.
    Each push now runs under LC_ALL=C, because git translates
    Authentication failed, so git's own lines of a refused push print in
    English. A push refused for any other reason, a ruleset's above all,
    prints what it printed. Setup runs this scaffold, so the change reaches
    it (#971, #915; #1012, #993).
  • Where a --reuse-empty-repo re-run can still wait. Only git's own
    terminal prompt is off, and the run leaves the operator's own ssh,
    credential-helper and askpass setup as it is. The --reuse-empty-repo
    help, docs/cli.md, the README, the handbook and each remedy that sends
    a person into that re-run, but the ruleset hint (#1108, in the known
    limits), now say that it can still wait at an ssh passphrase or host-key
    prompt, at a credential helper that prompts, or at GIT_ASKPASS,
    SSH_ASKPASS or core.askPass, and how to stop that: load the key into
    an ssh agent, use a helper that answers without prompting, or set
    GIT_SSH_COMMAND='ssh -o BatchMode=yes' (with anything core.sshCommand
    adds) for the run. Forcing it would replace the operator's own ssh
    command, a key given by -i, a proxy or a port. Under
    --local-remote-dir no prompt can follow, and no remedy says so. A push
    refused for another reason than a login keeps the ruleset hint alone
    (#1101, #915).
  • A seed found only on another branch, and --dry-run's words. A
    --reuse-empty-repo re-run that takes a seed found only on a branch
    other than the tracking branch, such as seed/scaffold, the branch the
    old ruleset hint made, prints a NOTE that says the run creates the
    tracking branch at it and does not touch that branch, so it remains; on
    GitHub the NOTE adds that it may be the leg's default branch, and under
    --local-remote-dir it says nothing of GitHub. A seed on the tracking
    branch as well prints none. A branch name the remote gives is spelled as
    shown_path spells a path. --dry-run's help says that its answer about
    each folder is os.access's, which reads the mode bits and on Windows
    says yes for every folder, so a quota, a Windows ACL or a read-only drive
    on Windows can pass the dry run and be refused by the real run, and that
    the run leaves nothing that stays: it makes and removes temporary
    folders, creates no remote and pushes nothing, where it said
    make no folder. docs/cli.md is regenerated (#1052, #921, #923).
  • One spelling of a neutral_product_pins: entry. One function in
    shape_materialize.py writes the entry for the scaffold and for
    adopt-project.py, so the same --pin writes the same line from either.
    A --pin of 0x1F, 1_000, 1e3, 3dGlass or -x is double-quoted
    from the scaffold as from adopt; the scaffold wrote them bare, and PyYAML
    read 0x1F as 31 and 1_000 as 1000. Of the names of one or two
    characters, 605 were spelled differently by the two tools, and none are.
    Adopt's output is the same byte for byte (#995, #924). Then a name that
    YAML 1.1 or 1.2 reads as a bool, a null or a float (y, n, yes,
    no, on, off, true, false, null, .inf and .nan, each in
    the three cases YAML reads (yes, Yes, YES; .nan, .NaN,
    .NAN), so yEs stays bare, and a point before digits such as .5) is
    double-quoted by both tools too, so a stock YAML reader reads each back
    as the text. Of the 278,915 names of up to three characters that --pin
    admits, 132 were misread by PyYAML and 132, not the same names, by
    ruamel.yaml's YAML 1.2 reader, and none are. openGlass is written as it
    was (#1090, #912).
  • A tag whose commit the clone lacks. A clone that holds an annotated
    tag but not its commit cannot answer for it, and now says so as it does
    for an id it does not hold: adopt-project.py plan and the scaffold,
    --dry-run alike, refuse pin-source-unreadable with the remedy to
    point --pin-source at a clone that has the pinned commit, git fetch --all first, where they said pin-not-a-commit, is an annotated tag that names no commit; adopt-project.py check says
    plan-pin-source-unreadable; and bump-leg.py says
    bump-leg-commit-not-on-remote where it said
    bump-leg-target-not-a-commit. A tag of a tree or a blob, which the
    clone can peel, is still refused by name. Where check cannot ask a
    recorded commit what it names, because the recorded clone is gone or
    partial, or gh api read the digest, its NOTE says that the digest was
    not rechecked and that the commit: was not asked whether it names a
    commit, where it spoke of the digest alone; plan asks where it reads
    the digest (#1090, #912).
  • A template that is no mapping. Where the template's own YAML reader
    makes no mapping of the manifest a run would write, family.py init, the
    scaffold and adopt-project.py check say it is a defect in the copy of
    the standard that was run, not in the person's values, name the template
    (templates/assembly-root/project.yaml or
    templates/family-root/family.yaml) and say to restore it from the
    standard's repository or reinstall the standard, where they sent a person
    to choose a value the validator accepts. The codes are kept, and check
    no longer adds its sentence about correcting the plan's values to it
    (#1064, #1059).
  • setup-project.py's credential-helper check. A configured helper is
    not a stored login. Where every helper configured for github.com is
    store, and none of the files store reads holds a login for
    https://github.com, the preflight prints its [??], which says that
    git's credential helper for github.com is store, that no login for
    github.com is stored in the files it names, and that the first push over
    HTTPS will fail, and makes the gh auth setup-git offer, as it does for
    no helper, where the check said that store and every other helper
    answers git's question for github.com. With git's prompt off for the
    scaffold's pushes, such a push fails after the three repositories exist.
    The files are the ones git's store reads, ~/.git-credentials then
    $XDG_CONFIG_HOME/git/credentials (or ~/.config/git/credentials), or
    the one --file names, with ~ as git expands it; with HOME not set
    the sentence says git gives store no file to read, where it named files
    git never reads. Both warnings stay silent where an insteadOf rule
    sends github.com over ssh, which git's own ls-remote --get-url says
    without asking the network, or where a helper under another spelling of
    the key, such as credential.https://github.com/.helper, answers.
    git credential fill, which runs every helper, is never run (#1008,
    #992; #1022, #1013).
  • setup-project.py's self-bootstrap and $OPENREPOSHAPE_REPO. A
    $OPENREPOSHAPE_REPO holding U+202E, or another character the launcher
    refuses, is refused by code point and in the launcher's words, exit 2,
    before the run prints the value or hands it to git clone, where it
    printed the raw bytes in its clone line and failed in git, exit 128. The
    table is the launcher's own, entry for entry, and a test holds the two
    together. The one git rev-parse probe that every run makes still comes
    before the refusal. A value the launcher lets by, such as U+00E9, U+00A0,
    or U+200C and U+200D, goes on as it did (#1043, #953).
  • family.py init, and the words it refuses in. Where the holder's own
    YAML reader refuses the manifest init would write,
    family-manifest-invalid now names the value on the line it refuses,
    such as --org is '@x', and gives the reader's remedy. It named no
    value, and called the reader's words the validator's (#960, #929).
    init now asks a template's YAML reader and validator through the one
    implementation shape_materialize.py holds, which the scaffold and
    adopt-project.py already used, so the family's guards against a broken
    validator hold for the project's manifest too: a validator that exits
    while it loads or when it is asked, or answers with a finding that is not
    text or with no list, is a named refusal, where the scaffold ended with
    the validator's status and printed nothing, or in a traceback. Only a
    broken copy of the standard reaches it, and no code or word the person
    sees on a sound one changes (#1057, #925). init --dry-run's help and
    docstring said it makes no folder, remote or repository. They now say
    its answer about each folder is os.access's, which reads the mode bits,
    and that it leaves nothing that stays, as the scaffold's help has since
    #1052 (#1063, #923).
  • family.py add checks its own commit. A commit that records the
    member's gitlink anywhere but members/<Project> is taken back and
    refused, member-gitlink-misplaced, exit 2, naming the path git chose.
    add exited 0 and left the branch on a commit whose .gitmodules and
    family.yaml name members/<Project>, where a fresh clone would leave
    the member empty. The take-back moves the branch to where add found
    it, only where the commit is one commit on that and the branch still
    names it, and the mount is undone as for every refusal after it; where a
    hook's commit is on top, nothing is taken back, and the refusal says so.
    members: null, Null, NULL or ~ takes the first member as
    members: does, where it was refused family-members-unrecognised
    (#957, #920). Family.rewritten's refusal of two members: lines is
    tested again, and its docstring says that no verb reaches it since the
    reader refuses a doubled key; no behaviour changes (#1039, #1029).
  • family.py bump's fetch. bump's fetch of the member's origin no
    longer moves the mount's own branches: a mount with
    +refs/heads/*:refs/heads/* had a local feat, and the commit on it
    that was never pushed, moved by bump --dry-run. It works where the
    mount maps tags into origin/, where a branch and a tag of one name made
    it fail with note: could not fetch <member> from its origin and a
    member-to-local-only that said to fix the network. A commit only a tag
    reaches is refused in the branches' words, where it was pinned. It reads
    the mount's refspecs with git config --list -z, so a value holding a
    newline is one value, and asks git remote get-url origin first, so a
    mount with no origin is not fetched from a path of that name (#946,
    #905). Every refspec of the mount's whose source can name a tag is left
    out too, a pattern such as refs/*, a short name or an object id among
    them, so a mirror in a folder, +refs/*:refs/remotes/origin/all/*, no
    longer fails the fetch on a branch and a tag of one name, or pins a
    commit only a tag reaches (#1027, #1009). bump-leg.py asks the same
    rule (#1068, #1087, above).
  • family.py bump and remove, a member on a branch that is not UTF-8.
    For a member on a branch such as caf<0xE9>, both ended in a
    UnicodeDecodeError traceback, exit 1, before any write. They run now.
    Where a pre-commit hook refuses, they exit 2 with its refusal, and the
    rollback puts the member back on that branch, at its commit, with the
    holder as it was. A trailing U+00A0 is no longer stripped off a member's
    branch name, where it named another branch (#1072, #918). A rollback's
    by-hand checkout line for such a member spells the branch as a printf
    word of octal escapes, "$(printf 'caf\351')", where it printed
    Python's 'caf\udce9', which names no branch, so the line typed in a
    POSIX shell puts the member back on that branch (#1084, #1079).
  • Remedies that name family.py bump run as printed. The remedies of
    member-already-present and member-duplicate-id in family.py, and of
    bump-leg-assembly-is-not-a-leg, bump-leg-root-not-a-project and
    bump-leg-wrong-kind in bump-leg.py, name --family-root, where they
    named family.py bump --member ... --to ... and the line failed with
    the following arguments are required: --family-root. family.py
    quotes the holder for the host's shell; bump-leg.py has no holder to
    name, and prints <holder> (#1002, #927).
  • validate-family.py's remedies, typed as printed and in order. The
    lines the gate prints for the states it names put a .gitmodules that is
    gone from the work tree back once, and never over another line's write;
    stage it before a gitlink's git rm and after a section is written back;
    and name a checkout that git mv moved, to delete by hand before the
    update. So no remedy undoes another or exits 128 when typed (#889, #879).
    A hand mount's git rm line is git rm --cached where a plain git rm
    refuses a staged gitlink, dies on a clone no section names or refuses
    what is no checkout, with what it leaves on disk said, and the
    .gitmodules is put back before the git rm that takes a section out
    (#1025, #936). gitmodules-path-unnamed reads the file git reads where
    the work tree's .gitmodules is gone, the index's and then HEAD's, and
    its lines put that file back first, so a section that
    git submodule add staged is no longer committed past a passing gate
    (#1069, #1036).
  • Paths this user cannot read, in the family. On Python 3.14 a path
    this user cannot read was read as absent, and on 3.12 it ended the run in
    a traceback. Each family tool now refuses it by name, exit 2, on every
    Python. family.py refuses a holder, a member or its object store, as
    family-root-unreadable, siblings-script-unreadable or
    member-unreadable, where it said family-root-missing,
    member-uninitialized or "not on disk", and no rollback removes past one
    (#955, part of #430). The holder's siblings.py and bootstrap.py
    refuse a member's path as member-unreadable, so 3.14 no longer clones a
    member on its remote's default branch, fetches into a clone it never
    identified, or says siblings ok and family bootstrap ok over members
    it skipped as absent (#951, part of #430). validate-family.py refuses a
    member's checkout, a shape copy, the shape pin or family.yaml as
    <path>: cannot be read (<reason>), with its cure, and a member's
    project.yaml as the finding member-manifest-unreadable, exit 1, so
    there is no 3.12 traceback, and no 3.14 member-uninitialized, pinned but absent or does not exist, or line that updates a checkout that is
    there (#956, #430).
  • git-not-runnable, only where git could not be started.
    validate-family.py counts an OSError that names git, or a
    FileNotFoundError or PermissionError that names no file, as
    validate-pins.py does, and only around the git calls. A member's
    checkout that git could not enter, or a member check's own step, is no
    longer told to put git on PATH (#983, #928). On Windows, a git that
    cannot be executed, [WinError 193], an OSError that names no file, is
    git-not-runnable, exit 2, with the put-git-on-PATH remedy, in all four
    copies of the rule, validate-family.py, validate-pins.py and both
    bootstrap.py, where each ended in a traceback, exit 1; a test holds the
    four to one answer and one text (#1028, #1001). So is a git Windows
    cannot start for another reason that names no file: winerror 216,
    1260, 4551, 11 and 188 to 202 (#1062, #1040).
  • A holder's make bootstrap fails, by name, where git's update
    stopped.
    Where git's init in the holder stopped at a gitlink the holder
    records with no url, or at an unmerged .gitmodules, with every member
    checked out, the run fails, where it said family bootstrap ok beside
    git's exit 128 (#889, #879). So it does for a gitlink a member's own
    index gives no url, and for a member's commit that a fetch reaching its
    remote did not bring: each fails the run, naming where git stopped and
    the cure, with no credential advice. They are told from a missing
    credential by git's trace events and its exit code, never by its words
    (#954, #892). A git that git's update ran dying of a signal, exit 128
    plus the signal's number (129 to 128 plus the system's highest signal
    number, 192 on Linux), or git itself killed, fails the run by name,
    UPDATE KILLED, and not for want of a credential, with no credential
    advice, where the run ended family bootstrap ok; a higher code, such as
    git's 255, reads as before (#1045, #998). The signal is told wherever the
    update met it, read from git's trace, and not only where its code is the
    update's last, where no token is in use (see the known limits); where
    git's update exited 0, nothing is said (#1076, #1056).
  • A token reaches no trace. With SHAPE_LEGS_TOKEN or
    SHAPE_LEGS_APP_TOKEN in use, git's update in make bootstrap, and the
    clone and the fetch that carry the token in make siblings and
    family.py siblings, run with every GIT_TRACE* variable removed and
    trace2's three targets set to 0, so a trace the caller asks git for no
    longer holds the token. Where git's update exits 128 with a token in use
    and a member's store lacks its recorded commit, make bootstrap redoes
    the two fetches git's update makes there, in that same environment, and
    fails by name where the commit is still absent, UPDATE STOPPED, where
    it printed the advice to set a token that was already set, and
    family bootstrap ok (#1045, #996; #1075, #1055).
  • A member's make under make -jN. Under make -j2 bootstrap,
    validate, park or resume, a member's make no longer warns
    jobserver unavailable: using -j1. Add '+' to parent make rule. of a
    rule in a Makefile the person does not own. The holder's bootstrap.py
    and siblings.py take the jobserver, and the -j that comes with it,
    out of the MAKEFLAGS a member's make inherits, so each member's make
    runs its jobs one at a time and says nothing (#950, #888). Under GNU make
    4.4's named-pipe jobserver, a member's make that says it is 4.4 or later
    gets MAKEFLAGS as it is and runs its jobs together; every other make
    still gets no jobserver and no -j, and siblings.py takes its one
    member_make_environment from bootstrap.py (#1023, #987).
  • A member's own exit code, in the FAILED line. A holder's
    make bootstrap names the code of the member's innermost command. An
    (ignored) line from make, for any code or signal, now resets the depth
    fully, so a member's own exit 2 after a - make one or two levels
    below, or after || true and a - command that failed or that a signal
    killed, reads the member's command exited 2, where it read exited 1
    (#891, #890).

shape-doctor.py

  • A path this user cannot reach is named, on every Python. A root this
    user cannot search, or whose parent it cannot search, is refused before
    any row: REFUSED shape-doctor-root-unreadable: --root <root>: cannot be read (Permission denied), exit 3, with chmod u+rx as the cure. It
    ended in a traceback on 3.12, and on 3.14 said NOT A SHAPE ROOT for the
    root and shape-doctor-root-missing for a parent. A root it can search
    and not list, mode 111, is not refused. A project's own gate that it
    cannot reach, with scripts/ at 444, is named on the manifest, pins
    and family rows and nothing runs in its place; on 3.14 the standard's
    copy of the validator ran instead and passed. A leg's mount or a member's
    that it cannot list or read is named by the legs or members row:
    spec/ at 000 as spec: cannot be listed (Permission denied), so no HEAD in it is compared with the pin, and spec/ at 444 as spec/.git: cannot be read (Permission denied), with the same ending. On 3.14 the one at
    444 read as populated, but with no .git of its own, and a holder's
    members/ at 000 as members/IRRS: not populated; on 3.12 each was
    could not be run. The leg shape files and placement rows are notes
    that say so, and a leg file it cannot read is cannot be read, never
    absent with a cp NEXT. Over such a mount the verdict is INVALID,
    not DRIFTED: no leg is off its pin because its pin could not be read,
    and a row that could not ask is never DRIFTED. A mount this user can
    read whose git directory it cannot, such as .git/modules/spec at 000,
    still reads DRIFTED (see the known limits). A refused read out of any
    other row is that row's FINDING in the gate's words, where it said could not be run. shape_advisory.py says it could not be read (PermissionError) of a record whose stat is refused, on every Python.
    With update-shape.py naming the same paths, a scripts/ nobody may
    search is INVALID (manifest, pins) on 3.14, as on 3.12, where on 3.14
    the doctor said DRIFTED (5 copy-missing) (#975, #976, part of #430).
  • A project.yaml linked into a folder this user cannot search. The
    doctor asked is_file() of the root's project.yaml and family.yaml,
    so such a link ended the doctor in a PermissionError traceback on
    Python 3.12 and, under a shim that answers as 3.14 does, read
    NOT A SHAPE ROOT, exit 2, with adopt-project.py plan as its NEXT,
    over a project. It is now the manifest row's
    REFUSED yaml-unreadable: <path>: cannot be read (Permission denied),
    INVALID, exit 1, on every Python, as a project.yaml at mode 000
    already was (#1111, #430).
  • A .gitmodules that holds no section. The doctor asked nothing only
    of a .gitmodules of no byte (#655). Now "empty" means no section: no
    byte, or only what git's config reader passes over, a whole UTF-8
    byte-order mark at the start, blank lines and comments opened by # or
    ;. In a leg that holds no submodule, such a file asks nothing, and the
    placement row says nothing needs one and that git rm -f .gitmodules in
    its leg removes it. Before, a file of a newline, a CR LF pair, a mark,
    spaces or a comment was asked on every run, no record could answer it,
    and nothing said to delete it. A section, even one with no key, is still
    asked, and so are part of a mark, a vertical tab and a form feed, which
    git refuses with bad config line, exit 128 (#1073, #914).
  • A gitlink no section registers. A recursive clone of a leg with a
    gitlink that no .gitmodules section registers exits 0 and leaves the
    gitlink's directory empty, and git submodule status there exits 128
    with no submodule mapping found in .gitmodules. The doctor said
    nothing. Now the placement row names each such submodule, with its own
    way out, in three states: beside a .gitmodules of its leg that holds no
    section (#1073), beside one whose sections register other paths, and in a
    leg whose index tracks no .gitmodules, where the copy git reads
    instead, an untracked file on this disk or else HEAD's, does not register
    it either (#1092). The way out is a section for it with its path and
    url, or, once the .gitmodules is staged (git add .gitmodules),
    git rm --cached <its path in the leg> in its leg; where the index
    tracks none, git rm --cached alone, since git add .gitmodules fails
    with no file. A gitlink that git counts as registered is not named. It
    is said, never a finding: the row stays the note or ok it was. A
    leg's --json entry carries unregistered_submodules, and in the last
    two states unregistered_beside, which is other_sections or
    no_gitmodules (#1073, #914; #1092, #1080).
  • A file's row with a /. An answered_paths: row that names a file
    with the / only a directory's row ends with answers nothing. The leg's
    entry names it under the new --json key slashed, and the placement
    row says it answers nothing. It is never a finding, and never read as the
    file's row (#1073, #914).

Install, CI and the docs

  • The pipx command refuses what the bash command refuses, wherever it
    prints a value or hands one to git.
    The
    openRepoShape that pipx install openreposhape places printed a
    $OPENREPOSHAPE_REPO holding U+202E or U+200B as typed and exited 0 on
    --version, and it refused a ref holding either with the value quoted.
    It now refuses a repository, a ref or a --shape-ref holding any of the
    41 code points the bash command refuses (#837), exit 2, by code point, in
    the bash command's words and without echoing the value, before
    --version answers or any git call names it. So do --doctor, a
    scaffold run and the package's four tool commands, scaffold-project,
    adopt-project, update-shape and shape-doctor: a value with such a
    character makes no git call. The repository is asked first, as the
    bash command asks it. The package holds a ref to git's rule part by part,
    too: a.lock/b, a/.b, a//b, a/b/ and a. are refused before the
    standard is cloned, in the bash command's sentence, where the package
    took them, cloned the standard in full and ended in git's own refusal at
    checkout (#947, #939, #941). A run that never reads the values, such
    as --doctor beside a git checkout or --preflight, refuses neither.
  • A letter past ASCII under the C locale. Under the C or POSIX
    locale, a $OPENREPOSHAPE_REPO URL or path, or a ref, with a letter past
    ASCII, such as /srv/café/openRepoShape.git, was refused as "a control
    character", exit 2, by the bash openRepoShape, by setup.sh's two
    guards and by install.sh's repository, version and option checks,
    which gave the same words under C. v1.1.0's launcher ran such a value,
    a UTF-8 locale such as C.UTF-8 takes it, and v1.1.1 named the refusal
    a known limit. All seven checks now ask one rule, the byte rule that
    holds $OPENREPOSHAPE_BIN_DIR, under LC_ALL=C in every locale: every
    control character, a C1 control included, is refused as before, and a
    letter past ASCII is not one. So --version, a scaffold run, --install
    from a file, --doctor and --preflight take such a mirror in a shell
    at C (cron, a CI step, a minimal container). No refusal's words
    change, and setup.sh keeps refusing U+2028 and U+2029, by their bytes.
    A byte that is not UTF-8, such as a lone 0x9B or an overlong form of a
    control, now passes in every locale, as it does for the directory (#867,
    #858); a terminal that reads UTF-8 draws it as a character it cannot
    read. install.sh takes the tag in the redirect's location only when it
    is printable ASCII, so any other tag names no release and gh is asked
    next (#986, #940).
  • --doctor checks the ref before it clones. The bash
    openRepoShape --doctor cloned the standard and then checked
    $OPENREPOSHAPE_REF, so a ref it refuses (a.lock/b, a//b, one over
    255 characters) cost a whole clone. Where the clone failed, offline or
    with a mirror that is down, the run ended in "could not clone" and never
    named the ref. A refused ref now makes no clone and no temporary folder,
    and is refused in its own words even where $TMPDIR is not there. A ref
    the doctor takes still clones once and checks out once. The refusal's
    words are unchanged. tests.yml's comment on the Windows job's
    full-depth checkout now says no test in its three files has cloned the
    checkout since, and the setting stays (#967, #942).
  • The install check fails where the doctor's children cannot start
    under -S.

    shape-doctor.py starts each child as python -S, and the release's
    install check is the one run of the real doctor on macOS and Windows. It
    asserted the doctor's exit over an empty repository and where the
    machine row found git-filter-repo, so a child that cannot start under
    -S passed. The step over the empty repository now prints the machine
    row's preflight exit and every line it reported. It fails unless the
    report holds the closing line of the exit the preflight chose,
    [ok] this machine is ready. for 0 and
    [!!] this machine is not ready yet (see above). for 1, and the
    [ok] git, [ok] python and [ok] bootstrap runs as lines every runner
    passes. A new step has scaffold-project --local-remote-dir make a root
    on the runner's disk, with no network, no gh and no token, and runs the
    doctor over it: the naming, manifest and pins rows must each have
    run a child, and none may read "crashed before reaching a verdict".
    Whether the root is compliant is not asked. A dispatch of the workflow on
    a branch, which uploads nothing, passed on all four legs, and a copy
    whose setup-project.py imports a module only site reaches failed on
    all four (#945, #894).
  • Runners. ubuntu-latest now gives out Ubuntu 26.04, which has no
    Python 3.9 for setup-python. tests.yml's package job, which runs on
    3.9, failed there with "The version '3.9' with architecture 'x64' was not
    found for Ubuntu 26.04", and the release's install-check row for 3.9
    would have failed the same way. Both now run on ubuntu-24.04, and that
    row's job is named install check (ubuntu-24.04, Python 3.9). So does
    the tests job: on 26.04 the full suite ran out of disk under pytest's
    basetemp in /tmp (Disk quota exceeded, 2436 failed and 5579 errors),
    and it stays on 24.04 until the suite runs on 26.04 (#1096, open). The
    other jobs keep their runners. A test holds every setup-python step of
    both workflows to what 26.04 has: a step on ubuntu-latest may ask for
    3.x or 3.12 only, and a Python 3.9 step must be on ubuntu-24.04.
    requires-python stays >=3.9 (#1095, #1094).
  • The release notes are held to the one-line install.
    tests/test_repo_hygiene.py holds the one-line install byte for byte in
    the notes of every release since v1.0.1. It named v1.0.1's notes alone,
    so a mistyped copy in v1.1.0's passed. It now finds the notes in
    docs/release-notes/ when the tests are collected, so each release's
    notes are held the day they are added. v1.0.0's, which predate the line,
    are the one exception, and a .md in that directory that is no
    release's notes fails until it is named (#887, #885). And every line of
    the notes that runs the install, one that names both curl and the
    release's install.sh URL, must hold the one line, with nothing after it
    or the pinned form, -s -- v<major>.<minor>.<patch>. A | bsh beside a
    correct line, or a | bash -s -- --yes, fails by file and line. The
    failure's message offers the exception set only to a file that can use
    it (#959, #931).
  • Path guards, held by a test. Eight pull requests above moved each
    shipped tool's guards onto a local _is_a, which treats ENOENT, ENOTDIR,
    EBADF and ELOOP (and Windows' 21, 123 and 1921) as absent and raises
    every other error, so the caller refuses the path by name (#934, #976,
    #975, #958, #956, #951, #955, #970, for #430).
    tests/test_path_guard_hygiene.py now holds that. A bare guard in any
    shipped tool fails by file and line unless its _is_a or a reasoned
    allowlist covers it; a second test holds every tool's _is_a to that one
    set of absent errors and no other; and an allowlist entry that no longer
    names a call fails as stale. It found one more site, the doctor's
    project.yaml (above). Eleven sites that read such a link as absent are
    listed as open, for #1110 (see the known limits) (#1111, #430).
  • Tests that passed when they should not, or failed in the wrong
    place.
    None of these changes a shipped file.
    • Five adopt tests now fail when the thing they guard is removed. A
      helper hands git a link's target as bytes, with a test over a target
      holding a CRLF, a bare CR and a letter past ASCII. The stub gh is
      written as bytes, so a CRLF cannot break its #! line.
      test_git_is_held_to_the_disk_when_it_asks clears
      GIT_ALLOW_PROTOCOL, so a caller whose own environment says file
      cannot hide a guard lost. Each guard of two covering pairs in
      adopt-project.py is reached alone. The naming test runs over three
      name shapes (#1071, #916). That closes three test limits v1.1.0 named
      (#457's L4-54, L1-60 and L1-62).
    • Three rows hold --link-source's unchanged reading at the old
      first-= split's corners: a keyed path that begins with =, a keyed
      blank path and a key in capitals. Five mutants of the gate's reader
      that passed its five closest test files now fail it (#1058, #1054).
    • The re-run the ruleset hint offers is dated later than the first run,
      so a leg's seed made again, even in the same second, is another commit
      and fails "kept at its commit", where it passed by the clock (#943,
      #922).
    • The doctor's explanations are held clear of shell syntax over a root
      that needs quoting, my atlas and do ct'or; the old test held only
      because pytest's temporary roots need none. The file now says the
      PowerShell form of a line is checked as text, by two tests, and run by
      no job (#899, #415).
    • A second members: line above the block is refused by the YAML reader
      as a key written twice since #974, so the two rows that expected
      family-members-unrecognised, and had turned main's tests job red,
      take that refusal (#1026, #1024).
    • The row that names both bump-leg.py commands pins LANES_LANE blank
      in its own process, as run_script does for the doctor it runs, so a
      shell that exports LANES_LANE no longer puts a --trailer on the
      expectation alone (#1081, #1060).
  • Docstrings that said what is not so. The docstrings of five adopt
    test files and tests/gh_guard.py said check runs no gh, or
    misstated when it does. check asks gh repo view once of each leg once
    the plan's names and org: pass, which the session's guard answers off
    Windows. They now say that, and what execute's in-process runs replace
    (#881, #880). test_adopt_split_branch.py says it too, and "every
    machine" is said off Windows only (#966, #938). Two docstrings in
    tests/test_family_bump_fetch_refspecs.py said #946's first head carried
    a tag or mirror refspec into origin/, where only a tag refspec in a
    folder was carried, and the mirror was already left out and had no test
    (#1007, #997).
  • The docs. AGENTS.md and the handbook say step (3) of setup refuses
    what stands at <into>/[<Family>/]<Project>: with --family the clone
    lands one folder deeper, except where the run already stands in the
    family's own folder. They said <into>/<Project> alone. README.md, the
    handbook and setup-project.py's docstring say nothing that stays is
    created before the typed yes, where they said nothing is created: the
    run makes a few short-lived folders, such as
    .openreposhape-write-probe-* above the clone and openreposhape-seed-*
    and openreposhape-branch-* in the temporary folder, and takes each away
    again, and the checkout --keep-shape-checkout keeps is the one
    exception. The docstring of passed_values says what argparse takes as a
    flag's value that setup does not read as one, -, a negative number such
    as -5 and a word with a space such as -x y, so step (4) reports from
    fewer values than the scaffold reads (#1051, part of #921 and of #923).
    v1.1.1's notes in the repository now say, as runs at v1.1.0 and v1.1.1
    showed, what the doctor does over a tab line or a pattern that does not
    compile in a leg clone, and name the C-locale refusal above as a known
    limit (#893).

Known limits

v1.1.1's known limits, and the earlier ones they
carry, still hold where this release does not close them; this list is not
all of them. This release closes:

  • v1.1.1's line on the pipx command, which had none of the bash command's
    checks of $OPENREPOSHAPE_REPO and $OPENREPOSHAPE_REF and refused only
    .. and a .lock ending in a ref: wherever it prints a value or hands
    one to git, it refuses what the bash command refuses, and it holds a ref
    to git's rule part by part (#947, #939, #941). Two differences stay
    (below).
  • v1.1.1's regression under the C or POSIX locale, tracked as #940: a
    letter past ASCII in a repository, a ref, a version or an option is no
    longer "a control character" (#986, #940).
  • v1.1.1's --source directory this user cannot enter, #457's L1-105: it
    is source-unreadable, exit 2, where it ended in a PermissionError
    traceback (#970; #1072, #918).
  • v1.1.1's four lines for #879: with the work tree's .gitmodules gone,
    the wrong-case move that commits a .gitmodules holding one key, and
    family bootstrap ok beside git's exit 128; a stray gitlink's
    git rm --cached that exits 128 after a value finding's lines, and a
    checkout moved by git mv that no finding named (#889, #879).
  • v1.1.1's make bootstrap traceback on Python 3.12 and 3.13 over a leg
    directory or a governance/ nobody may enter: the leg is named and left
    as it is, and step (c) names the folder (#958, part of #430). The rest of
    that line, on setup's step (7) and siblings.py, still holds.
  • v1.1.1's "a handler that binds and re-raises", in update-shape.py's
    missing-name refusal: such a kept copy is refused (#935, #897).
  • v1.1.0's #457 L1-32: a failure of check's probe git init other than
    an OSError is one NOTE in git's words, where it stopped check with "a
    git or gh command failed" (#1031, #910). And three of v1.1.0's test
    limits, #457's L4-54, L1-60 and L1-62 (#1071, #916).
  • v1.1.0's #457 L1-17: with org: and adopt_branch: both outside the
    alphabet, execute now refuses org first, as check does (#1015,
    #901). Where the plan is held to the source's recorded repository,
    check still says unsafe-value and execute plan-not-the-source, as
    v1.1.1 said (#1020).
  • Of v1.0.0's: adopt-not-the-source coming only after the clone for an
    org/repo source (#963, #903); a YAML key written twice (#974, #917);
    the plan naming neither the tracking branch nor, in local mode, the
    visibility (#969, #913); a seed found only on seed/scaffold (#1052,
    #921); a --pin product the two tools spelled differently, or that only
    a standard YAML reader misreads, such as 3dGlass, 0x1F, yes, on
    or .5 (#995, #924; #1090, #912); a declared pin's file reached through
    a root-kept link; a link at a path the shape writes into the clone's
    .git, such as CLAUDE.md -> .git/refs/remotes/origin/main; one that
    climbs back out of .git, such as .git/refs/../HEAD; one to
    .git/reftable; plan-root-blocks-shape-copy's words for a link at a
    copy (#1091, #904); and a hand-edited pin whose product holds a tab,
    which check named in two findings, plan-pin-malformed and
    plan-unwritable-value (#1015, #901).

And narrows:

  • v1.0.0's line on a bare number or boolean in a hand-edited plan's text
    field: check and execute now refuse one in id:, tracking_branch:
    or adopt_branch: (#1016, #902); in elected_by:, reference: or
    org: it still passes (below).
  • v1.1.0's #459 L4-24, setup-project.py stripping only C0 controls and
    DEL from a value it prints: its self-bootstrap now refuses a
    $OPENREPOSHAPE_REPO holding one of the launcher's 41 code points
    (#1043, #953). A C1 control still passes setup's checks, and so does one
    of the 41 code points in --into, --local-remote-dir or an argument
    after --, which only PATH_RE or CONTROL_RE asks (read from the
    code); with OPENREPOSHAPE_REPO=acme/r<U+009B>31mforged the clone line
    prints the raw bytes, as before.
  • v1.1.0's bump-leg.py --dry-run exiting 0 over a pin its own validators
    refuse: unchanged, but a dry run now refuses a validator it cannot read
    (#934).
  • v1.1.0's line on the package on Windows: the release's install check now
    also scaffolds a project root with the package's scaffold-project on
    every leg, Windows and macOS included, and runs the doctor over it
    (#945).
  • v1.1.1's line on a holder's FAILED line in nested makes: a member's own
    exit 2 after a - make one or two levels below, or after || true and a
    - command that failed or that a signal killed, now reads exited 2
    (#891, #890).
  • v1.1.1's last line, on Windows and macOS: since #986, the checks of a
    repository, a ref, a version and an option read bytes under LC_ALL=C,
    as the checks of $OPENREPOSHAPE_BIN_DIR do, but for install.sh's
    read of the redirect's location; that read, and the multibyte re-read of
    $OPENREPOSHAPE_BIN_DIR through locale charmap and iconv, use the
    person's locale.

Ruled by Brett Heap at 2026-10-22T12:01:22Z, "take your recommendations on
#979, #917 and #921":

  • adopt-project.py check passes a symlink target of 1024 to 4095 bytes.
    check holds the bound at Linux's, 4095 bytes, but macOS and XFS (the
    default filesystem of RHEL-family systems) make at most 1023, so on a Mac
    or an XFS host execute's first clone fails, after both leg repositories
    exist. Both limits are read from XNU's and XFS's sources, not measured;
    #979 stays open for a measurement on a Mac and on XFS (#979's part (a);
    its part (b) landed in #1070).
  • The YAML reader is still a subset by design, as v1.1.0's notes say
    (#459's L4-156). Measured at main 038dafb: k: - a, k: [- ],
    k: [e?], k: [|a], @a: 1 and k: [?a] each read, where PyYAML
    refuses them or reads another thing (#917's U123 and U155).
  • A holder made with family.py init before v1.1.0, with a --family and
    a --name that differ, keeps a README cd line that names the folder a
    clone does not make. Edit it by hand, as "Install and upgrade" says;
    update-shape.py apply leaves it, by design (#921's U097).
  • family.py bump asks only the mount's own origin whether a branch
    reaches the commit, through refs/remotes/origin/*. So a mount whose
    origin is a fork passes a commit only the fork has: bump --to pins
    it, exit 0, validate-family.py passes, and a recursive clone of the
    holder fails not our ref (#921's U066, ruled 2026-10-16).
  • A holder pinned before #625 to a commit only its mount held stays
    pinned: neither validate-family.py nor the doctor asks whether a branch
    of the member's remote reaches it (#921's U068, ruled 2026-10-16).
  • family.py add --at <tag id> pins a release commit, which may be on no
    branch. Since #625, bump --to that commit, a repeated bump and
    validate-family.py's digest repair are each refused
    member-to-local-only, where v1.0.0's notes say a repeated bump says
    nothing to do. As ruled (#613's D2, option 1), push the commit to a
    branch of the member first; bump-leg.py refuses its twin alike (#921's
    U071).

New in v1.2.0, each an open issue with no milestone, left out of v1.2.0 by
Brett Heap's ruling of 2026-10-21T10:32:04Z, "leave the follow-ups out of
v1.2.0"; most have a fix in review that lands after the tag:

  • A name in a root that is a link into a folder this user cannot search is
    still read as absent at eleven sites, silently on Python 3.14 and with a
    PermissionError traceback on 3.12: family.py remove --dry-run says no
    section is removed, because the .gitmodules reads as having none;
    siblings.py reads no url and clones the member from its row's default;
    and the doctor's the way in sends the person to setup.sh, because the
    .git reads as no repository (#1110). A shape root whose own .git is
    such a link reads DRIFTED, its legs read as having no gitlink, and both
    its validators crash (#1146). A project's bootstrap.py, and a holder's
    validate-family.py, bootstrap.py and siblings.py, over such a
    root's .git, end in a traceback on 3.12, or say not-a-git-repo on
    3.14 (#1148).
  • adopt-project.py check can say plan ok where execute then stops
    after both legs exist: on git 2.43, over an init.defaultBranch git will
    not take (#1083); and over a parent's -c setting, such as
    core.protectHFS=true, or GIT_ATTR_SOURCE, which its throwaway
    repositories leave out (#1136). For a source with a submodule, two of
    check's steps still make their repositories in TMPDIR, run this
    machine's hooks and use a plain git init (#1083).
  • check writes into a partial-clone source: over a --filter=blob:none
    clone that lacks blobs check reads, its reads fetch them, running the
    upload-pack program the source's config names and writing a promisor
    pack into the source's objects/pack (#1145).
  • A recorded clone whose .git this user cannot read is called "no longer
    whole" and cloned again (#1099). plan or check on a repository whose
    .git is at mode 000 says only git-failed, and a readable .git whose
    HEAD, or whose git directory, is at mode 000 is cloned again (#1151). A
    --source too long to name is refused source-unreadable, but its
    remedy offers a https://github.com/<owner>/<repo> URL that can never
    clone (#1003).
  • Under a one-byte locale such as Latin-1, a plan written in UTF-8 is asked
    only as written, so a source that is there is plan-source-gone; #1072
    mended the C and POSIX locales only (#1078).
  • Under a one-byte locale, with a plan written in UTF-8 whose record is a
    clone plan made, and a work directory with a letter past ASCII, check
    does not take its own clone for the record, so it clones again on every
    run (#1112; its fix, #1153, lands after this release).
  • check and execute can still name different first problems for a bad
    id beside an unanswered entry, a stale source or a refused name (#1020),
    and for four pairs with no bad id (#1113). A pins: entry that is no pin
    is spelled with Python's repr, such as `pins:` entry 1 is 7
    (#1020), and so is a pin field that is not text, such as commit: 7 or
    True (#1114). elected_by:, reference: and org: in a hand-edited
    plan, read as a number or a boolean (elected_by: 007, org: true),
    still give check plan ok, and execute takes str() of what was
    read, so 007 is 7, true is True and 1.50 is 1.5 (#1016's
    body; #457).
  • execute's NEXT prints make bootstrap where GNU make at the root reads
    a kept makefile or GNUmakefile, which has no bootstrap rule
    (#1047). Two sentences, in shape_materialize.py and in the pinned
    AGENTS-shape.md, say a kept Makefile holds the shape's block back,
    and leave out a kept makefile or GNUmakefile, which GNU make reads
    first (#1048). execute's own NOTE, that the shape's targets arrive only
    once the follow-up merges the two, is untrue there too (#1048); and
    beside a kept Makefile and GNUmakefile it names python3 scripts/bootstrap.py as what make bootstrap runs, where make bootstrap exits 2 for want of a rule (#1047). README's make sentence
    leaves them out too (#1061). On a disk that ignores case, a kept
    makefile gets no merge follow-up, and a split made on a disk that keeps
    case can commit Makefile and makefile side by side (#1049, not
    measured).
  • Names adopt-project.py prints raw, so a bidi control in them reaches the
    terminal: plan's source line's branch (#1035); plan written to's
    --out path and the clone line's --work-dir (#1115); and the --plan
    path in check's and execute's finding lines and refusals, and
    --local-remote-dir in execute (#1152). Its spelling lets the
    default-ignorable code points through, such as U+034F and U+3164, so a
    branch ma, U+034F, in prints as main (#1128). plan's table pads a
    name by code points, so a wide or combining name stands out of line
    (#1034).
  • A case twin that sorts after a link at a shape copy, such as
    shape/readme.md beside a link at shape/README.md, is not named while
    the link sits there; a twin that sorts before the link is (#1138).
  • An update-shape.py apply does not ask again a copy an earlier apply --accept-local kept, where its row reads unchanged, so it can write a
    siblings.py that imports a name the kept bootstrap.py lacks. Nor does
    it ask the other way round: a kept copy that imports a name the module
    this run writes drops. Neither reaches a root pinned at v1.1.1 or
    earlier, whose kept copies read both (#1082; see "Install and
    upgrade").
  • A pin's commit: or tree_sha256: that ends in a line break passes as
    well formed, because COMMIT_RE and SHA256_RE in repo_shape.py, and
    COMMIT_40 in the assembly root's bootstrap.py, end in $ and are
    read with .match: validate-manifest.py says manifest ok,
    validate-pins.py gives pin-commit-unresolvable or
    pin-digest-mismatch rather than a malformed finding, and the doctor
    reads the commit with its line feed; adopt-project.py check passes such
    a pin's row, the assembly root's bootstrap.py prints its upstream
    notice, and a holder's validate-family.py passes a family.yaml
    shape.digests.tree_sha256 that ends in one (#1017). The same $ lets
    validate-family.py pass an id: that ends in a line feed, both
    validators a date, and validate-manifest.py a leg's repository:;
    update-shape.py refuses a pin's source_repository: that ends in one,
    but in words that name --upstream (#1139). Only hand-written YAML holds
    such a value; no tool writes one, and the limit is older than v1.2.0.
  • After bump-leg.py or family.py bump fetches in a repository whose
    origin has an unforced refspec that puts a branch under another name,
    the repository's own git fetch origin exits 1, non-fast-forward;
    git fetch --force origin, once, puts the ref back and loses no commit.
    A refspec such as +refs/heads*:refs/remotes/origin/h* beside a branch
    h/x, or +refs/pull/7/head:refs/remotes/origin/main, still clashes:
    the tools' fetch exits 128 where the repository's own exits 0. These two
    are older than v1.2.0 and unchanged by #1087, and the docstring of
    gives_a_branch_another_name says no such shape can clash (#1100).
  • Where a leg's mount has no remote named origin,
    bump-leg-commit-not-on-remote, for a --to tag the leg holds without
    its commit, says the tag "is not in the leg's own object store", which is
    untrue; the code is right. The same sentence answers a tag that leads
    through a tag the leg lacks. A tag the leg holds of a tree or a blob it
    lacks is not refused by name as bump-leg-target-not-a-commit: with no
    origin it gets that sentence and is told to push a commit the tag never
    named, and with an origin it is told <repository> has no branch containing it (#1103).
  • family.py's member-duplicate-id and member-unknown remedies name
    family.py remove and family.py add without --family-root, so each
    fails as printed (#1005). A failed bump's by-hand lines quote a leg
    mounted at a path that is not UTF-8 as Python's \udcXX, which names no
    path (#1086).
  • validate-family.py's gitmodules-path-twice, and its check for values
    git dies on, read the work tree's .gitmodules, or HEAD's, where git
    reads the index's (#1077). So a bad value staged in the index alone is
    family ok, a plain commit then records it, and a clone's
    git submodule update --init exits 128; with the value in both files,
    the printed --unset-all line has no stage line; and an index
    .gitmodules with a line git cannot parse after such a value is
    family ok too, though git submodule status exits 128 (#1150).
  • Where git's update is itself killed after a git it ran died of a signal,
    a holder's make bootstrap says that git's update "went on and ended
    with exit -N". No row reaches that line; an OOM killer that took a
    checkout and then the top git would (#1085).
  • The scaffold's ruleset hint sends a person into the --reuse-empty-repo
    re-run without #915's sentence on the prompts that can still wait, and a
    push refused for its ssh host key gets that hint alone (#1108). A re-run
    whose probe ssh refused for GitHub's host key is told "Let git read it"
    and sent to ssh -T, with no word on checking the key against GitHub's
    published fingerprints (#1137).
  • Beside a .gitmodules that a leg's index tracks but this disk lacks, the
    doctor does not name a gitlink that no section of the index's copy
    registers, though git reads that copy and git submodule status exits
    128 (#1093).
  • refused_paths with core.ignorecase=true and case twins on a disk that
    ignores case is untested, and not known to fail (#1033).
  • CI's tests job does not run on Ubuntu 26.04, since the full suite there
    fills /tmp under pytest's basetemp (#1096).

New in v1.2.0, from the pull requests that landed it:

  • The package's --version asks a ref its shape, where the bash command's
    --version asks only its characters (#866), so a ref such as v1café
    prints from the bash command and is refused by the package. The
    package's --doctor beside a git checkout, and --preflight, read
    neither variable, so they refuse neither (#947).
  • setup.sh has no copy of the 41 code points, so U+202E, U+200B and the
    rest, but U+2028 and U+2029, pass its two guards in every locale; the
    bash command, install.sh and the package refuse them first where they
    are the way in, and README's curl … setup.sh | bash -s -- is the way in
    without them (#986).
  • A byte that is not UTF-8 (a lone 0x9B, 0x85 or 0xE9, or an
    overlong form of a control such as C0 9B), U+0378 and U+FFFE pass the
    seven checks of a repository, a ref, a version and an option in every
    locale, and the line after the check prints the value, as v1.1.1 said of
    a lone 0x9B in $OPENREPOSHAPE_BIN_DIR. A multibyte locale that is not
    UTF-8, such as GBK, reads 0xC2 0x80 to 0xC2 0x9F as letters, and
    these checks refuse such a value as a C1 control: unlike the directory's
    check (#858), they do not read it again in the locale's charset (#986).
  • The release's install check does not start validate-family.py, or the
    doctor's fallback to the standard's template copies, under -S, so those
    children are unmeasured on macOS and Windows; and it does not ask whether
    the root it scaffolds is compliant (#945).
  • The short-lived folders setup and the scaffold make are removed on a best
    effort: a killed run can leave one, and a failed removal is a warning
    (the write probe), silent (the branch folder) or a traceback (the seed
    folder). The printed --dry-run: nothing was created. and each
    refusal's Remediation: nothing was created still say "nothing was
    created" (#1051, #1052).
  • A hand edit that leaves family.yaml with a second members: line is
    now the reader's one-line yaml-unparsable, with no remedy, where
    family-members-unrecognised said to fix it by hand, commit and run
    again (#1026, #1024).
  • After #974 and #1021, the YAML reader still reads 'a\n' for k: | over
    a where the file's last line has no line break, and PyYAML reads
    'a'; it refuses a block scalar as a sequence item (- |) as
    unexpected indentation; and it takes 1 and "1" for one key, so a
    mapping holding both is refused as a key written twice, where YAML reads
    two keys (#974).
  • plan still asks some refusals only after the source is read, for an
    org/repo source too: naming-unclassified, naming-role-mismatch, an
    empty --elected-by, a --project of 251 to 255 characters and a
    missing --path-policy. With a gh that cannot clone, each is
    source-unclonable for an org/repo source. A local source's
    --work-dir is still made, and refused where it cannot be (#963).
  • A root-kept link to a name inside the object store, a leg's store or the
    reftable tables, or under refs/heads, refs/remotes or their reflog
    folders in logs/ where the source's HEAD is on no branch, is refused as
    one check cannot tell about, though some such links would adopt. The
    reftable answers were measured on git 2.47.1 only; git 2.43 reads neither
    GIT_DEFAULT_REF_FORMAT nor init.defaultRefFormat (#1091).
  • check still treats every root link as giving no .gitattributes rule,
    though git reads a link's text as a rule line where it holds a space, a
    tab or a carriage return; what check's findings say for such a link is
    not measured (#964).
  • A --work-dir loop on Windows is named only where os.stat of the link
    reports ELOOP; Windows was not measured (#1065).
  • The setup preflight's credential-helper check reads the starting folder's
    .git/config, which the scaffold's pushes do not, so a rule kept only
    there silences both warnings for pushes it does not reach. A rule keyed
    to one repository, an organisation spelled in another case than
    --org's, and an insteadOf rule that rewrites to an https:// URL
    carrying its own login still warn. A helper keyed to another
    organisation beside an empty store now stays silent. Empty helper =
    lines are dropped, not read as git's list resets, and beside a helper
    other than store, cache among them, the check stays silent, since it
    cannot read what such a helper holds. Git for Windows' home from
    HOMEDRIVE and HOMEPATH was not measured (#1008, #1022).
  • A push refused over HTTPS with 403 keeps its words, since its cure is a
    right and not a login; family.py init's and adopt-project.py's pushes
    keep their own ruleset hints; and only OpenSSH's words are read, not
    another client's such as plink's (#1012). Git's own prompt is off for the
    scaffold's git alone, and ssh, a credential helper or askpass can still
    wait, as the help says (#971, #915; #1101); family.py's git and the
    clone setup makes of the standard were outside #915.
  • Before Python 3.13, a --pin-source that runs round a symlink loop is
    named as typed by the scaffold and joined but not resolved by step (4),
    and on Python 3.9 for Windows the two can name a relative path
    differently (reasoned, not run). Under self-bootstrap a relative
    --pin-source is resolved against the temporary checkout of the
    standard, not the folder the run started in, in step (5) and now in
    step (4) (#949).
  • A holder's make bootstrap: the FAILED line still names another
    command's code in the shapes MakeErrors' docstring lists, one more
    since #891: under make -k, an inner make that failed with 1 and a later
    - make with 3 read exited 3. A recipe that lets an inner make fail in
    the shell (|| true, or ;), or with a - in a make run with -s,
    which prints no (ignored) line, and then exits 2 itself with no
    (ignored) line between, still names the inner command's code, such as
    exited 1 (#891). Under GNU make 4.4's named-pipe jobserver, a member's
    make running its jobs together is read from 4.4.1's NEWS and shown with a
    stand-in make; no GNU make 4.4 or later ran (#1023). With a token in
    use the run is untraced, so a signal is told only from git's own exit
    code, and a signal before a later failure is not told (#1045, #1076).
    Past a member at another commit than its pin, a checkout the walk cannot
    stat or run git in, or a gitlink of a member whose own .gitmodules
    git cannot read whole, the run reads as before (advice, and family bootstrap ok); a git before 2.22 has no trace2, so there it reads as
    before too; and a GIT_TRACE2_EVENT the person sets for one command is
    overridden (#954). An unmerged .gitmodules now fails make bootstrap
    where the gate passes (#889).
  • Where the work tree has no .gitmodules, validate-family.py's value
    finding puts back HEAD's, not the index's, so a fix staged in the index's
    is lost (#889). A hand mount whose .git file names a git directory
    outside the holder's store, a linked worktree of an outside clone, gets a
    plain git rm that exits 0 and moves nothing, and --unset-all of every
    key can leave an empty [submodule "<name>"] header (#1025).
  • family.py bump's fetch no longer names a mount's refspec that maps
    tags into origin/, so its --prune drops a tag's copy under origin/
    that the person's own git fetch origin wrote, in the dry run too, and a
    +refs/*:refs/remotes/origin/* mount loses its mirror the same way; the
    next git fetch origin writes them back (#946). In both tools, a full
    ref name that is no pattern can still bring a tag, since git tries
    refs/tags/<name> (#1027, #1068); a refspec that puts pull requests in a
    folder of their own, such as
    +refs/pull/*/head:refs/remotes/origin/pr/*, beside a fetch of main
    alone, still clashes with a remote branch pr/7: the tools' fetch exits
    128 where the repository's own exits 0 (#1027, #1087); and with a leg's
    own refspecs carried, bump-leg.py's fetch exits 128 where one names a
    ref the remote no longer has, as the person's own git fetch origin does
    (#937, #1068). bump-leg.py reads a leg's remote.origin.fetch a line
    at a time, so a value holding a line break is misread (#1068).
  • family.py add's take-back does not compare the gitlink's commit with
    the pin, which the gate's member-gitlink-mismatch does; a misplaced
    gitlink under --dry-run exits 0; and members: null # none yet is
    still refused (#957).
  • A file whose stat works and whose open is refused, such as a member's
    project.yaml at mode 000 in a folder this user can search, is read by
    the holder's siblings.py and bootstrap.py as before; in
    validate-family.py, a member's .git file at mode 000 still reads
    member-commit-unresolvable with a fetch remedy, and a file at
    members/<Project> still reads member-uninitialized (#951, #955,
    #956). The PermissionError cases of #430's sweep were measured on
    Python 3.12 and under a shim that answers as 3.14 does; CI's tests job
    runs the newest Python 3 (3.x), on Linux alone.
  • bump-leg.py over a project.yaml at mode 000 in a root it can search
    refuses yaml-unreadable with no remedy, and over a validator at mode
    000 alone it is bump-leg-root-dirty, since git sees the mode change; its
    validator check runs after the fetch, and after nothing to do (#934).
  • update-shape.py apply rolls back a refused read after its first write,
    but any other OSError there, a file gone between stat and read or a
    full disk, still escapes with nothing put back; check does not read a
    manifest at mode 000, which apply then refuses as yaml-unreadable
    with its absolute path; and update-unreadable names only the first path
    it cannot read, where validate-pins.py names each (#976). A project's
    validate-manifest.py prints project.yaml's absolute path raw in
    manifest-missing, manifest-unreadable and its findings count (#958,
    read from the code).
  • update-shape.py's kept-copy check still reads as binding a handler
    whose raise is under an if or inside a with, and one that binds and
    then calls sys.exit(); a copy whose own raise X an outer
    except ImportError: would catch, which Python starts, is now refused
    (#935).
  • make bootstrap's pasteable lines keep the text they had for a leg path
    that ends in a line feed or holds a NUL, and git ignores a
    submodule.<name>.path that begins with -, so no command initializes a
    leg there (#944, #973). The case-twin rule compares with casefold(), so
    ss and a sharp s are twins though a disk may not fold them, and a
    reftable leg is left where Main and main are two refs; bump-leg.py
    and family.py compare a root's or a holder's branch with the tracking
    branch byte for byte, so on a disk that folds case a root or a holder on
    Main is not refused bump-leg-on-tracking-branch or
    family-on-tracking-branch (reasoned, not measured). Twins are named one
    at a time, one rename a run, and a twin whose last path component is 246
    bytes or more, or a stale reflog directory under the chosen name, is not
    handled (#948, #984, #1006).
  • The doctor names no gitlink whose path is not UTF-8 or holds a control
    character, nor one beside a .gitmodules that is unmerged, that git
    refuses, or that is a link or a directory on this disk (#1073, #1092).
  • The doctor over a leg at its pin whose git directory this user cannot
    read, .git/modules/spec at 000 under a mount it can read: the pins
    row is right, pin-submodule-unreadable, but the legs row says spec: populated, but git cannot read a HEAD in it, the placement row says
    spec: it is not a git repository, and the verdict is DRIFTED, on 3.12
    and under a shim that answers as 3.14 does, as before #975. A leg's shape
    file this user cannot read, such as spec/CLAUDE.md at 000, is the leg shape files row's note CLAUDE.md cannot be read (Permission denied),
    with no WHY and no NEXT, and the verdict is COMPLIANT, where every
    other path #975 names carries the gate's advice; before #975 the row
    crashed, INVALID (leg-shape-files) (#975).
  • Five branches of the doctor's legs and members rows for a mount this
    user cannot reach have no test, and are right by reading (#975). #1050's
    rows read and write through no tool's own path, so a reader that opens a
    file as utf-8-sig would pass them, and none holds that U+FEFF alone is
    cut (#1050).
  • Windows and macOS: CI runs on Linux only (#597), but for
    tests-git-bash, which runs the launcher's and install.sh's suites on
    Windows; the release's install check is the one run of the package there
    and on macOS (#945). A disk that folds case (#948, #984), Windows'
    CreateProcess errors (#1028, #1062) and a Windows --work-dir loop
    (#1065) are emulated in the tests, or reasoned, not run there.

Full list