diff --git a/AGENTS.md b/AGENTS.md index 3e8105c6..207bdd26 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,9 +23,9 @@ hand-edited; `tests/test_cli_reference.py` fails when the two disagree. types `yes` at its prompt; you never answer it for them. No terminal: section 2. **Never answer an install prompt.** The preflight asks `Type yes to continue:` -for each thing it offers to run — an install, `gh auth login`, `gh auth -setup-git`. The human types it; you never install software or log in for them. -The one-line install (`install.sh`) is theirs to run: relay it, never run it. +for each offer — an install, `gh auth login`, `gh auth setup-git` — and the +human types it; you never install software or log in for them. The one-line +`install.sh` and `pipx install openreposhape` are theirs: relay, never run. **Never pass `--allow-upstream-org` on your own initiative.** It guards only `--org opensoft`, the namespace of openRepoShape's upstream owner. If setup.sh @@ -60,11 +60,13 @@ curl -fsSL https://raw.githubusercontent.com/opensoft/openRepoShape/main/setup.s --visibility --elected-by '' ``` -`openRepoShape --org --visibility <…> --elected-by ''` -is the same run through the installed command, still without `--yes`. With no -arguments it prints its usage and exits 1, and `--install` ends with a -read-only `machine:` block naming anything missing; neither installs anything. -Relay the block; the human runs `--preflight` if they want the offers. +`openRepoShape --org --visibility <…> --elected-by ''` is +the same run through the installed command, still without `--yes`, whether +`install.sh` or `pipx install openreposhape` (native Windows too) placed it; +pipx adds `scaffold-project`, `adopt-project`, `update-shape` and +`shape-doctor`, the tools below by name. Bare, it prints its usage and exits 1; +the bash one's `--install` ends with a read-only `machine:` block naming +anything missing, and installs nothing: relay it; the human runs `--preflight`. On Windows without WSL2, `Invoke-WebRequest https://raw.githubusercontent.com/opensoft/openRepoShape/main/setup-project.py -OutFile setup-project.py` then `py setup-project.py --org --visibility <…> --elected-by ''` @@ -217,7 +219,7 @@ no `--dry-run` to do first. The one exception to "writes nothing" is The checks are a REGISTRY with an empty `fix` slot in each of them, which is where a repair mode will hang; nothing in it writes today. Installed, `openRepoShape --doctor` takes `--json` and `--placement-plan ` as well, -and KEEPS its temporary clone of the standard for the NEXT lines that name it: +and KEEPS its clone of the standard for the NEXT lines that name it: its last line names that directory, the human's to delete once those have run. 1. **Read the verdict, then read the rows.** `COMPLIANT`, `COMPLIANT, SHAPE diff --git a/README.md b/README.md index 4a4e78fa..f0761220 100644 --- a/README.md +++ b/README.md @@ -129,7 +129,10 @@ its own `contracts/shape-pin.yaml`, so a CRLF checkout reports drift in files nobody edited. The clone `setup-project.py` makes is configured correctly whatever the machine says; the global setting is for every clone after it. -Then two commands, run in the directory the new project should land in: +Then `pipx install openreposhape` (where `pipx` is missing: `python -m pip +install --user pipx`, then `python -m pipx ensurepath` and a new terminal): +step 3 says what it places, and step 4 runs it. Or, installing nothing, two +commands, run in the directory the new project should land in: ```powershell Invoke-WebRequest https://raw.githubusercontent.com/opensoft/openRepoShape/main/setup-project.py -OutFile setup-project.py @@ -149,13 +152,13 @@ and refuses. Downloading the file first leaves stdin on your terminal. `setup-project.py` is the flow itself, and `setup.sh` a shim over it: the same preflight, the same naming check, the same plan, the same one `yes`, the same self-bootstrap outside a checkout. It never needs `make` and never looks for a -`python3` — it runs the interpreter that started it. That two-liner IS the -Windows way in from PowerShell, which has no twin of step 3's `openRepoShape` -command. Git Bash runs that command and step 3's one line, tested in CI but for -a typed `yes`, which its preflight cannot take there. Printed commands are -quoted for a POSIX shell, so paste them into Git Bash. Only `shape-doctor.py`'s -lines, `setup-project.py`'s family lines and `family.py`'s `NEXT`, `switch -c`, -`add` and `git rm -r --cached` lines are quoted for PowerShell, on Windows. +`python3` — it runs the interpreter that started it. That two-liner is the +Windows way in that installs nothing; pipx's `openRepoShape` runs the same flow. +Git Bash runs step 3's bash command and its one line, tested in CI but for a +typed `yes`, which its preflight cannot take there. Printed commands are quoted +for a POSIX shell, so paste them into Git Bash. Only `shape-doctor.py`'s lines, +`setup-project.py`'s family lines and `family.py`'s `NEXT`, `switch -c`, `add` +and `git rm -r --cached` lines are quoted for PowerShell, on Windows. @@ -180,8 +183,8 @@ run the four steps below INSIDE the WSL shell. > `brew` the preflight names and stops there. To check a > machine and stop, creating nothing, run `openRepoShape --preflight` — > `py setup-project.py --preflight` on Windows — which exits 0 when everything -> is present and 1 when it is not (`openRepoShape --preflight` exits 2 when -> `git` or a Python 3.9 or newer is missing). +> is present and 1 when it is not (the bash `openRepoShape --preflight` exits +> 2 when `git` or a Python 3.9 or newer is missing). **1. Install the GitHub CLI**, `gh`. The official instructions are at (and ): @@ -275,6 +278,14 @@ things it does, one line each: install it, check the machine scaffold a project. Type any scaffold argument and you get the run you asked for, refusals and all. +**The second way, from PyPI:** `pipx install openreposhape`, wherever `pipx` +is (`sudo apt-get install -y pipx` on Debian and Ubuntu, `brew install pipx` on +macOS, then `pipx ensurepath`). It places the same `openRepoShape` (its +`--install` refuses) and `scaffold-project`, `adopt-project`, `update-shape` +and `shape-doctor`, with `git-filter-repo` 2.47.0; each runs a cached clone at +the release's commit, digest-checked every run. `pipx upgrade openreposhape` +upgrades. Use one installer, not both: each places `~/.local/bin/openRepoShape`. + **4. Run it:** ```sh @@ -730,10 +741,12 @@ one is found — `--pin-source [PRODUCT=]`, `SHAPE_PIN_SOURCE_` i the environment, or a checkout beside the assembly root named for the pinned repository — else by `gh api`, with the operator's `gh` credentials, when `gh` can read it. Finding neither is a named SKIP, exit 0: no checkout and no network -does not make a descent a lie. A checkout that cannot answer is the finding -`neutral-pin-source-unreadable`. `make bootstrap`, `bump-leg.py` and -`update-shape.py apply` run this validator in the operator's environment, so -each makes that call too; none takes `--pin-source`. +does not make a descent a lie. A digest that recomputes differently is the +finding `neutral-pin-digest-mismatch` (exit 1), and a checkout that cannot +answer is the finding `neutral-pin-source-unreadable`; a declared pin file that +is not there is the refusal `neutral-pin-missing` (exit 2). `make bootstrap`, +`bump-leg.py` and `update-shape.py apply` run this validator in the operator's +environment, so each makes that call too; none takes `--pin-source`. ### A neutral product may elect the shape and be its own assembly root @@ -755,8 +768,8 @@ it satisfies no leg form to be admitted into, and is still refused as any leg. ## Adopting an existing repository A repository that already exists is converted **in place**, by `adopt-project.py`, -and this is a ruling (**Brett Heap, 2026-09-02**): it KEEPS ITS NAME, which -`--project` must be, ITS IDENTITY AND ITS FULL HISTORY and becomes the assembly +and this is a ruling (**Brett Heap, 2026-09-02**): it KEEPS ITS NAME (which +`--project` must be), ITS IDENTITY AND ITS FULL HISTORY, and becomes the assembly root, while `-spec` and `-code` are NEW repositories extracted with history-preserving filters. The source is never deleted, never renamed and never force-pushed; the only change to its history is ONE split commit, arriving @@ -770,10 +783,10 @@ only, and a tool needing a bypass cannot be used where it is needed. ./adopt-project.py execute --plan adoption-plan.yaml --yes ``` -`git filter-repo` is a **hard dependency** — `pip install git-filter-repo`, or -the same name from apt or brew. It is deliberately not vendored: history -extraction has one correct implementation, and a copy is a second one that -drifts. +`git filter-repo` is a **hard dependency**: step 3's one line installs it or +prints the command for this machine, and `pipx install openreposhape` brings it +for its `adopt-project`. It is deliberately not vendored: history extraction +has one correct implementation, and a copy is a second one that drifts. ### The worked example: MedxEHR — 167 files, 25 commits, three arguments @@ -1148,8 +1161,8 @@ openRepoShape --doctor # the repository you are standing in It **writes nothing into the repository but a `--placement-plan` file you name, and fetches nothing from it**, and it reimplements nothing: each row is a check this file documents. It compares against the checkout it is RUN FROM, so the -verdict is offline. Installed, it KEEPS its temporary clone of the standard for -the NEXT lines naming it: its last line names it, safe to delete after they run. +verdict is offline. Installed, it KEEPS its clone of the standard for the NEXT +lines naming it: its last line names it, safe to delete after they run. For an assembly root the rows are **naming** (this standard's own `contracts/repository-naming.yaml`), **manifest** and **pins** (the project's @@ -1194,16 +1207,17 @@ one verdict, and its exit code: Exit 3 is usage or environment (a flag argparse rejects is exit 2) — nothing about it is a statement about the tree you pointed at: no such path, a refused -`--placement-plan`, a `shape-doctor.py` not sitting in a -checkout of the standard, or a pin naming a commit **this** checkout does not -carry (`CANNOT ANSWER`). **Drift outranks a red validator** on the verdict -line, deliberately — an edited shape copy is exactly what makes -`validate-pins.py` red, so `INVALID (pins)` would send the reader at the -symptom while the fix is `update-shape.py`; the pins row still prints its own -next command. `MISPLACED` sits between the two: a path in the wrong leg is a -fact about the shape no validator, pin or manifest here can see. **An -unfinished merge outranks all three**, as `INVALID (merge)`: the conflict is -what turns the other rows red, and no fix commits until it is finished. +`--placement-plan`, a `shape-doctor.py` outside a checkout of the standard, or +**this** checkout unable to answer (`CANNOT ANSWER`): a pin's commit it lacks, a +partial clone missing what is compared, a read that fails, a validator of its +own that crashed or could not be run, or its own naming policy not loading. +**Drift outranks a red validator** on the verdict line, deliberately — an edited +shape copy is what makes `validate-pins.py` red, so `INVALID (pins)` would send +the reader at the symptom while the fix is `update-shape.py`; the pins row still +prints its own next command. `MISPLACED` sits between the two: a path in the +wrong leg is a fact no validator, pin or manifest here can see. +**An unfinished merge outranks all three**, as `INVALID (merge)`: the conflict +is what turns the other rows red, and no fix commits until it is finished. `--json` is the same report as one object on stdout, each row carrying its `id`. **`placement` is the row that reads what is *inside* the legs.** Brett Heap, @@ -1426,8 +1440,9 @@ scripts/shape_materialize.py the ONE materializer, used by both tools scripts/validate-repository-naming.py openRepoShape installs itself as a command; runs setup.sh install.sh the one-line install: a release, pinned +src/openreposhape/ the pipx package (pyproject.toml); stdlib only setup.sh the shim: find an interpreter, hand over -setup-project.py THE FLOW, and the Windows way in (no bash there) +setup-project.py THE FLOW, and a Windows way in from PowerShell scaffold-project.py creates the three repositories and the pins adopt-project.py converts an EXISTING repository in place update-shape.py re-syncs a root's copies and re-pins @@ -1450,9 +1465,9 @@ scripts/render-cli-reference.py renders docs/cli.md; never hand-edit that file tests/ pytest; scaffolds into bare repos in /tmp ``` -**Python 3 standard library only.** No `pip`, no packaging, no -engineering-domain dependency — the shape must run where nothing can be -installed. `tests/test_repo_hygiene.py` enforces it. +**Python 3 standard library only.** No tool needs `pip` or any dependency, so +the shape runs where nothing can be installed; the `openreposhape` package is a +second way in, never a requirement. `tests/test_repo_hygiene.py` enforces it. ## Licence diff --git a/adopt-project.py b/adopt-project.py index 1322b537..5b11b325 100755 --- a/adopt-project.py +++ b/adopt-project.py @@ -891,12 +891,22 @@ def _pin_digest_findings(plan: "Plan") -> tuple[list[str], list[str]]: SEED_TEMPLATE = {"spec": "templates/spec-root", "code": "templates/code-root"} EXTRACTED_LEGS = ("spec", "code") +#: In install.sh's order (#595): with the pipx or uv already there, else +#: brew's, else pipx from apt or dnf, else Arch's own package. No `pip +#: install --user`: Debian 12, Ubuntu 23.04 and Arch mark their own Python +#: externally managed (PEP 668) and refuse it. The openreposhape package +#: depends on git-filter-repo and puts it on PATH for the `adopt-project` it +#: places, but not for this file run by its path (#563). FILTER_REPO_HINT = ( - "Remediation: install it — `pip install git-filter-repo`, or " - "`apt install git-filter-repo`, or `brew install git-filter-repo`; it is " - "one file on PATH named `git-filter-repo`. It is NOT vendored here on " - "purpose: history extraction has one correct implementation and a copy of " - "it is a second one that drifts." + "Remediation: install it — `pipx install git-filter-repo==2.47.0`, or " + "`uv tool install git-filter-repo==2.47.0` where there is no pipx; with " + "neither, `brew install git-filter-repo`, or `sudo apt-get install -y " + "pipx && pipx install git-filter-repo==2.47.0 && pipx ensurepath` (`dnf` " + "for `apt-get` on Fedora), or `sudo pacman -S --needed git-filter-repo` " + "on Arch; it is one file on PATH named `git-filter-repo`. `pipx install " + "openreposhape` brings it for the `adopt-project` command it places. It " + "is NOT vendored here on purpose: history extraction has one correct " + "implementation and a copy of it is a second one that drifts." ) diff --git a/docs/cli.md b/docs/cli.md index ad2cfa9b..957a1733 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -53,7 +53,7 @@ one today, and the generator refuses to write this file if one ever appears. ## Run from a checkout of the standard -Every command below is typed at the root of a clone of this repository. `openRepoShape` is also the one file `--install` places on `PATH`, and is the only entry point that needs no checkout at all. +Every command below is typed at the root of a clone of this repository. `openRepoShape` is also the one file `--install` places on `PATH`, and needs no checkout at all. `pipx install openreposhape` (#563) places a Python `openRepoShape` with the same modes, whose own `--help` says what differs (its `--install` refuses and names `pipx upgrade openreposhape`), and four commands that need no checkout either: `scaffold-project`, `adopt-project`, `update-shape` and `shape-doctor` each run the tool of that name below, with every argument unchanged, from a clone of the standard at the package's own release commit, so their flags are the ones below. ### `setup.sh` diff --git a/docs/handbook.html b/docs/handbook.html index 0c63875c..fadb789d 100644 --- a/docs/handbook.html +++ b/docs/handbook.html @@ -472,9 +472,9 @@ - + - + @@ -504,8 +504,10 @@

The fastest path — macOS, Linux, WSL2, Git Bash

in-flight work to another workstation, are opensoft/openRepoTools’ and have a one-liner of their own.

-

In PowerShell, without WSL2 or Git Bash, the way in is two commands - instead, because there is nothing to install — see Start +

The second way, from PyPI, is pipx install openreposhape: + the same command and four more, named for the tools, with git-filter-repo + as their dependency. In PowerShell, without WSL2 or Git Bash, it is the first way, and + two commands that install nothing are the other — see Start here.

@@ -610,7 +612,7 @@

The one thing to read before electing it

- +

Setup

Start here

@@ -708,24 +710,30 @@

The tool offers the rest

What this machine needs

-

Python is the one manual install. Install Python 3.9 or newer from +

Python is the one manual install the flow needs. Install Python 3.9 or newer from python.org/downloads, ticking Add python.exe to PATH — its own checkbox, and a different one from py launcher, which the installer ticks for you. Then git from git-scm.com/downloads, and gh - with winget install --id GitHub.cli -e.

+ with winget install --id GitHub.cli -e. The commands need one more manual + install, pipx: python -m pip install --user pipx, then + python -m pipx ensurepath and a new terminal.

The tool offers the rest

The preflight names anything missing and, for gh, its login and its credential helper, prints the command for THIS machine and asks once — Type yes to continue: — then runs it and carries on. On Windows those offers are the winget ones: winget install --id GitHub.cli -e for gh and, only under - py setup-project.py --preflight, winget install --id Git.Git -e - for git: a run refuses a missing git before its preflight starts. - There is no openRepoShape command to install in PowerShell: that two-liner - IS the Windows way in there. Git Bash, the bash that Git for Windows installs, runs the - openRepoShape command and the one-line install, and CI tests both there, but - for a typed yes: CI has no terminal, and in Git Bash the preflight that the - one-line install ends with cannot take one, so none of its offers can be accepted there. + py setup-project.py --preflight or pipx’s + openRepoShape --preflight, which runs the same preflight, + winget install --id Git.Git -e for git: a run refuses a missing + git before its preflight starts. + In PowerShell the openRepoShape command comes from + pipx install openreposhape, a native openRepoShape.exe beside + four tool commands, and the two-liner below is the way in that installs nothing. Git + Bash, the bash that Git for Windows installs, runs the bash openRepoShape + command and the one-line install, and CI tests both there, but for a typed + yes: CI has no terminal, and in Git Bash the preflight that the one-line + install ends with cannot take one, so none of its offers can be accepted there. Printed commands are quoted for a POSIX shell, so paste them into Git Bash. Only shape-doctor.py’s lines, setup-project.py’s family lines and family.py’s NEXT, switch -c, @@ -777,7 +785,7 @@

Make one machine setting first

  • Install the openRepoShape command, once

    Install the openRepoShape command, once, inside WSL

    -

    Fetch the flow, in the directory the new project should land in

    +

    Install with pipx, fetch the flow, or use Git Bash’s one line

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

    install.sh is attached to every release and installs the newest, with no gh and no login. Its command is PINNED to the @@ -801,6 +809,14 @@

    Fetch the flow, in the directory the new project should

    Or, where raw downloads are blocked, the same bytes through the API:

    gh api repos/opensoft/openRepoShape/contents/openRepoShape \
         -H 'Accept: application/vnd.github.raw' | bash -s -- --install
    +
    pipx install openreposhape
    +openRepoShape <Project> --org <your-org>
    +

    That places a native openRepoShape.exe with the same + modes as on the other tabs, four commands that run the tools of those names, and + git-filter-repo as their dependency; the second line is then the whole run, + and openRepoShape --preflight the machine check. Or, installing nothing, + fetch the flow into the directory the new project should land in, and the steps below + run it:

    Invoke-WebRequest https://raw.githubusercontent.com/opensoft/openRepoShape/main/setup-project.py -OutFile setup-project.py

    Two commands rather than one pipe, on purpose. Windows PowerShell 5.1 — still the default shell on a stock install — re-encodes piped text as @@ -810,6 +826,10 @@

    Fetch the flow, in the directory the new project should an exhausted stdin, so it cannot ask for the one yes before three repositories exist — it refuses instead. Downloading the file first leaves stdin attached to the terminal you are typing at.

    +

    In Git Bash, the one-line install runs too:

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

    It places the bash openRepoShape, whose preflight cannot + take a typed yes in Git Bash, so none of its offers can be accepted there.

    Each way installs ONE command into ~/.local/bin, idempotently — a second run prints unchanged — and prints the export PATH=… line if that directory is not on PATH. @@ -836,6 +856,19 @@

    Fetch the flow, in the directory the new project should its usage and exits 1 — the four things it does, one line each: install it, check the machine, diagnose a repository, scaffold a project.

    +

    The second way, from PyPI, wherever + pipx is (sudo apt-get install -y pipx on Debian and Ubuntu, + brew install pipx on macOS, then pipx ensurepath):

    +
    pipx install openreposhape
    +

    It places an openRepoShape with the same + modes, in Python, whose --install refuses and names pipx upgrade + openreposhape, and four commands that run the tools of those names with every + argument unchanged — scaffold-project, adopt-project, + update-shape, shape-doctor, so docs/cli.md is + their reference — with git-filter-repo 2.47.0 as their dependency. + They run a clone of the standard at the release’s commit, kept in your cache and + checked against its digest before every run. Use one installer or the other: both place + ~/.local/bin/openRepoShape.

  • @@ -843,17 +876,19 @@

    Check the machine, creating nothing

    openRepoShape --preflight
    py setup-project.py --preflight

    It checks this machine, offers what is missing and creates nothing. It exits 0 when - everything is present and 1 when it is not; it exits 2 instead, before any check, when - git or a Python 3.9 or newer is missing.

    -

    It exits 0 when everything is present and 1 when it is not, and creates nothing; it - exits 2 instead, before any check, when git or a Python 3.9 or newer is - missing. This is also where a python3 below 3.9 is named.

    + everything is present and 1 when it is not; the bash command exits 2 instead, before any + check, when git or a Python 3.9 or newer is missing.

    +

    It exits 0 when everything is present and 1 when it is not, and creates nothing; the + bash command exits 2 instead, before any check, when git or a Python 3.9 or + newer is missing. This is also where a python3 below 3.9 is named.

    It exits 0 when everything is present and 1 when it is not. python setup-project.py --preflight is the same command on a machine with no py: Python from the Microsoft Store installs python and no - launcher at all.

    -

    It exits 0 when everything is present and 1 when it is not; it exits 2 - instead, before any check, when git or a Python 3.9 or newer is missing.

    + launcher at all. With pipx’s command, openRepoShape --preflight runs + the same check.

    +

    It exits 0 when everything is present and 1 when it is not; the bash command + exits 2 instead, before any check, when git or a Python 3.9 or newer is + missing.

  • @@ -879,6 +914,8 @@

    Rehearse first, creating nothing

    scripts/bootstrap.py — and gh is never called. On Windows the same flag goes on the Python entry point:

    py setup-project.py Atlas --org <your-org> --local-remote-dir .\rehearsal
    +

    With pipx’s command, the same flag on + openRepoShape Atlas --org <your-org>.

    openRepoShape Atlas --org <your-org> --local-remote-dir ./rehearsal

    --local-remote-dir <dir> runs the whole of the real thing against three BARE repositories in <dir>: the same preflight (where @@ -938,7 +975,9 @@

    Run it

    and asks once, Type yes to continue:; type yes. Afterwards ./<Project> is the cloned root.

    python setup-project.py <Project> --org <your-org> is the - same command on a machine with no py. setup-project.py is the + same command on a machine with no py, and pipx’s + openRepoShape <Project> --org <your-org> the same run again. + setup-project.py is the flow itself, and setup.sh a shim over it: the same preflight, the same naming check, the same plan, the same one yes, the same self-bootstrap outside a checkout. It never needs make and never looks for a @@ -1048,7 +1087,7 @@

    What you have afterwards — the same on every platform

    - +

    Setup

    If it fails part-way

    @@ -1085,9 +1124,9 @@

    If it fails part-way

    - + - +

    Setup

    Reading private legs in CI

    @@ -1147,9 +1186,9 @@

    Reading private legs in CI

    - + - +

    Reference

    The three legs, and naming

    @@ -1203,7 +1242,10 @@

    The three legs, and naming

    assembly root named for the pinned repository — else by gh api, with the operator’s gh credentials, when gh can read it. Finding neither is a named SKIP, exit 0: no checkout and no network does not make a descent a lie. A - checkout that cannot answer is the finding neutral-pin-source-unreadable. + digest that recomputes differently is the finding + neutral-pin-digest-mismatch (exit 1), and a checkout that cannot answer is the + finding neutral-pin-source-unreadable; a declared pin file that is not there is + the refusal neutral-pin-missing (exit 2). make bootstrap, bump-leg.py and update-shape.py apply run this validator in the operator’s environment, so each makes that call too; none takes --pin-source.

    @@ -1227,12 +1269,12 @@

    The three legs, and naming

    - +

    Reference

    Adopting an existing repository

    -

    A repository that already exists is converted in place. It KEEPS ITS NAME, - which --project must be, ITS IDENTITY AND ITS FULL HISTORY and becomes the assembly root, while +

    A repository that already exists is converted in place. It KEEPS ITS NAME + (which --project must be), ITS IDENTITY AND ITS FULL HISTORY, and becomes the assembly root, while <Project>-spec and <Project>-code are NEW repositories extracted with history-preserving filters. The source is never deleted, never renamed and never force-pushed; the only change to its history is ONE split commit, arriving on a branch @@ -1241,10 +1283,11 @@

    Adopting an existing repository

    # ... a human or an AI answers every `review_required` entry in the plan ./adopt-project.py check --plan adoption-plan.yaml ./adopt-project.py execute --plan adoption-plan.yaml --yes
    -

    git filter-repo is a hard dependency — - pip install git-filter-repo, or the same name from apt or brew. It is - deliberately not vendored: history extraction has one correct implementation, and a copy is a - second one that drifts.

    +

    git filter-repo is a hard dependency: the one-line install + installs it or prints the command for this machine, and + pipx install openreposhape brings it for its adopt-project. It is + deliberately not vendored: history extraction has one correct implementation, and a copy is + a second one that drifts.

    Ambiguous is an answer, and no source line is overwritten. contracts/path-classification.yaml maps path globs to spec | code | root | ambiguous, first match wins, each with a reason. The fourth @@ -1388,7 +1431,7 @@

    The InkRouter example

    - + @@ -1488,7 +1531,7 @@

    Bootstrap is COPIED into the project, not fetched

    - +

    Reference

    Keeping a project’s shape current

    @@ -1550,7 +1593,7 @@

    Keeping a project’s shape current

    - +

    Reference

    Is this repository compliant? openRepoShape --doctor

    @@ -1565,8 +1608,8 @@

    Is this repository compliant? openRepoShape --doctor

    the one that writes at all, and it writes one file, at the path you give: a relative one lands in the directory you stand in, inside the repository when you stand there. It compares against the checkout it is RUN FROM, so the verdict is offline. Installed, it - clones the standard into a temporary directory and KEEPS it for the NEXT lines that name - it: its last line names it, safe to delete after they run.

    + clones the standard and KEEPS the clone for the NEXT lines that name it: its last line + names it, safe to delete after they run.

    For an assembly root the rows are naming, manifest and pins (the project’s OWN validators when it carries them — they are the pinned copies, which is the point — else the template copies, and the row says @@ -1613,8 +1656,10 @@

    Is this repository compliant? openRepoShape --doctor

    Exit 3 is usage or environment (a flag argparse rejects is exit 2) — nothing about it is a statement about the tree you pointed at: no such path, a refused --placement-plan, a shape-doctor.py not in a checkout of the - standard, or a pin naming a commit this checkout does not carry - (CANNOT ANSWER). Drift outranks a red validator on the + standard, or this checkout unable to answer (CANNOT ANSWER): a + pin’s commit it lacks, a partial clone missing what is compared, a read that fails, a + validator of its own that crashed or could not be run, or its own naming policy not + loading. Drift outranks a red validator on the verdict line, deliberately: an edited shape copy is exactly what makes validate-pins.py red, so answering INVALID (pins) would send the reader at the symptom while the fix is update-shape.py. The pins row is printed @@ -1685,7 +1730,7 @@

    Is this repository compliant? openRepoShape --doctor

    - + @@ -1840,7 +1885,7 @@

    The degrade rule

    - +

    Reference

    Layout of this repository

    @@ -1855,8 +1900,9 @@

    Layout of this repository

    scripts/validate-repository-naming.py
    answers the naming question for one name
    openRepoShape
    installs itself as a command; runs setup.sh
    install.sh
    the one-line install: a release, pinned
    +
    src/openreposhape/
    the pipx package (pyproject.toml); stdlib only
    setup.sh
    the shim: find an interpreter, hand over
    -
    setup-project.py
    THE FLOW, and the Windows way in (no bash there)
    +
    setup-project.py
    THE FLOW, and a Windows way in from PowerShell
    scaffold-project.py
    creates the three repositories and the pins
    adopt-project.py
    converts an EXISTING repository in place
    update-shape.py
    re-syncs a root’s copies and re-pins
    @@ -1878,8 +1924,9 @@

    Layout of this repository

    scripts/render-cli-reference.py
    renders docs/cli.md; never hand-edit that file
    tests/
    pytest; scaffolds into bare repos in /tmp
    -

    Python 3 standard library only. No pip, no packaging, no - engineering-domain dependency — the shape must run where nothing can be installed. +

    Python 3 standard library only. No tool needs pip or any + dependency, so the shape runs where nothing can be installed; the + openreposhape package is a second way in, never a requirement. tests/test_repo_hygiene.py enforces it. Changes to this repository are tested with python3 -m pytest tests -q, which scaffolds into bare repositories in a temporary directory; never by creating a real GitHub repository.

    @@ -1904,8 +1951,8 @@

    For AI assistants

  • Never answer an install prompt. The preflight asks Type yes to continue: for each thing it offers to run — an install, gh auth login, gh auth setup-git. The human types it; you never - install software or log in for them. The one-line install (install.sh) is - theirs to run: relay it, never run it.
  • + install software or log in for them. The one-line install (install.sh) and + pipx install openreposhape are theirs to run: relay them, never run them.
  • Never pass --allow-upstream-org on your own initiative. If setup.sh refuses because the organisation you were given resolves to opensoft, STOP and relay what it said; the fix is almost always an explicit --org <their-org>.
  • diff --git a/docs/release-notes/v1.1.0.md b/docs/release-notes/v1.1.0.md new file mode 100644 index 00000000..d39b35bc --- /dev/null +++ b/docs/release-notes/v1.1.0.md @@ -0,0 +1,954 @@ +# openRepoShape v1.1.0 + + + +v1.1.0 puts openRepoShape on PyPI. `pipx install openreposhape` is a second +way in, beside the one-line install, and the first on native Windows, where +PowerShell cannot run the one line (#563). This release also carries v1.0.1's +one-line install into main (#564), fixes limits that v1.0.0 listed, and runs +CI on Linux only (#597). + +**[placeholder: the counts at tag time.]** At main `6f64eea`, on 2026-10-17, +122 pull requests had landed on main since v1.0.0's commit, `bfed143`; at +20:15Z, 132 of the `v1.1.0` milestone's 133 issues were closed. + +## What 1.1.0 adds + +- **openRepoShape on PyPI, as `openreposhape`** (#590, #599, #563). One + command installs it: + + ```sh + pipx install openreposhape + ``` + + It places five commands. `openRepoShape` has the launcher's modes, + ``, `--preflight`, `--doctor`, `--version` and `--help`, written + again in Python, so native Windows gets the command, as an `.exe`. + `scaffold-project`, `adopt-project`, `update-shape` and `shape-doctor` each + run the tool of that name with every argument unchanged, so `docs/cli.md` + is their reference. `git-filter-repo` 2.47.0 comes as their dependency, + and each command puts it first on the `PATH` of the tools it starts. + + The wheel is thin. It carries the release's commit and the + `sorted-ls-tree-r-v1` digest of its tree, and the tools run from a full + clone of this repository at that commit, made once in your per-user cache + and kept. Before each run the clone must be at that commit, unchanged, and + digest to that value, so a project scaffolded through it pins exactly this + release. `$OPENREPOSHAPE_REF` names another ref for one run, with no digest + checked. The package's `--install` refuses and names + `pipx upgrade openreposhape`. Python 3.9 or newer. + + The version is the tag's, so 1.1.0 is the first version on PyPI. The + v1.0.x line is not published there. +- **The one-line install, on main** (#564, carried into main by #587): + + ```sh + curl -fsSL https://github.com/opensoft/openRepoShape/releases/latest/download/install.sh | bash + ``` + + It installs the newest release's bash `openRepoShape`, pinned to the + release tag, and ends with `openRepoShape --preflight`, as + [v1.0.1's notes](v1.0.1.md) say. Since v1.0.1: + - it installs git-filter-repo with `uv` where there is no `pipx`, and never + with `pip install --user`, which an externally managed Python refuses + (PEP 668). With neither, it prints one command for this machine: brew's, + pipx's from apt or dnf, pacman's, or uv's (#598, #595); + - it ends with two lines, `adoption:` and then `done:`, so a machine that + is ready to scaffold but not to adopt is told so (#598, #595); + - Git Bash, the bash that Git for Windows installs, runs it and the + `openRepoShape` it places, tested in CI but for a typed `yes`, which the + preflight cannot take there (#596, #585); + - it refuses a target that is a symbolic link, such as the + `openRepoShape` pipx places, and so does the launcher's `--install` + (#563). +- **CI on Linux only** (#597). Brett Heap decided on 2026-10-15 to drop the + macOS and Windows test jobs across the estate. `tests.yml` runs the whole + suite on `ubuntu-latest`, on every push to main, every pull request and + every dispatch. Since #593 each push run on main is grouped by its own + commit, so the next landing no longer cancels it, and every landing keeps + its own Linux result. One exception, on his word the same day: a + `tests-git-bash` job runs the launcher's and `install.sh`'s suites, and + nothing else, on Windows under Git Bash (#596, #585). The rest of the + suite no longer exercises Windows file-system behaviour, and the stock + macOS bash 3.2 is a goal the bash entry points are written for rather + than a checked claim. The release workflow still + installs the package on Linux, macOS and Windows before it reaches PyPI. + +## Install and upgrade + +- **The newest release, by the one line:** the line above. The same line + again upgrades. `… | bash -s -- v1.1.0` names this release. + `openRepoShape --version` prints `openRepoShape (opensoft/openRepoShape @ v1.1.0)`. +- **From PyPI:** `pipx install openreposhape`, or + `pipx install openreposhape==1.1.0` for this release. + `pipx upgrade openreposhape` upgrades it. `pipx` itself comes from + `sudo apt-get install -y pipx` on Debian and Ubuntu and + `brew install pipx` on macOS; on Windows, `python -m pip install --user pipx`, + then `python -m pipx ensurepath` and a new terminal. + `openRepoShape --version` prints + `openRepoShape 1.1.0 (opensoft/openRepoShape @ )`, then the + version, tag, commit, digest and the clone it would use. +- **One installer or the other.** Both place `~/.local/bin/openRepoShape`. + pipx does not replace a command it did not place, and `install.sh` and the + launcher's `--install` refuse the link pipx places, naming + `pipx upgrade openreposhape` or removing the link. +- **Native Windows:** `pipx install openreposhape`, then + `openRepoShape --org `. Downloading `setup-project.py` + and running `py setup-project.py --org ` still works, + and installs nothing. Git Bash runs the one line and the bash + `openRepoShape` it places, tested in CI but for a typed `yes`, which the + preflight cannot take there (#596, #585). +- **A project's pin** on the standard works as before. Follow + [How to pin 1.0.0](v1.0.0.md#how-to-pin-100) with the v1.1.0 release + commit. A scaffold run through the package pins that commit and digest by + itself. +- **A family holder made before #642.** A holder made with + `--family X --name Y` before #642 (the fix for #629) keeps `cd Y` in its + own README, and `update-shape.py apply` never re-renders a README, so + change that one line by hand to `cd X`: the holder's directory is named + after its repository, `X`, never after the display name. The same README + draws its layout as `Y/Y` and calls the doubled `Y/Y` deliberate; read + `X/X` there, which is what `make siblings` checks the folder against. + +## What changed since v1.0.1 + +v1.0.1 was cut from `release/v1.0` and added only the one-line install. +Everything below landed on main after v1.0.0's commit, by area. The list +runs to main `6f64eea`. + +### adopt-project.py + +- **The source's `origin`, read one way.** `plan` reads `/` from + every `origin` git accepts, a local clone or mirror path and a URL ending + `/` included, so a local clone's right `--org` is no longer refused + (#591, closing #521 and #427). +- **Refused before any leg exists.** `check` finds, and `execute` refuses + before any leg exists, a file the root keeps at the shape's own pin path + and a `--work-dir` the user cannot write in. Both used to fail only after + both legs were pushed (#581, #524). `check` recomputes each neutral pin's + digest at the plan's commit, in the clone `plan` read it from, where that + clone is still on the machine and is no partial clone, so a commit or a + digest edited into the plan no longer reaches the root as + `adoption verified` over a red `validate-pins.py` (#589, #449). Before any + leg too: the branch names `execute` failed on, such as + `adopt_branch: a..b`, a source branch `Adopt` or one hidden by `hideRefs` + (#583, #520, #420); a kept nested `.gitattributes` that outranks the + root's line-ending rule for a pinned copy, and a root `.gitattributes` + that `git add` would re-encode (#619, #530); and a kept submodule url on + this disk where `git ls-remote` reaches no repository, which used to end + `adoption verified` over a recursive clone that fails (#618, #418). +- **Names outside ASCII.** `execute` hands `git rm` each path the split + moves or drops as the bytes the tree holds, so such a name under + `LC_ALL=C PYTHONUTF8=0` no longer stops the run with `UnicodeEncodeError` + after both legs are pushed (#614, #527). +- **The work directory.** A run without `--work-dir` removes the work + directory it made where it holds no file, and `execute` names the one it + keeps. `check` and `execute` clone the plan's `source.repository` again + where the clone it records is gone, so neither leaves an empty + `openreposhape-adopt-*` directory in the system temp folder, nor refuses + `source-unclonable` once that folder is cleaned (#594, #526). +- **Answers kept.** `execute` records the plan's answers under + `answered_paths:`, and a moved submodule names its row, so an answered + `examples/` is not asked again, and the doctor names no plan for a + deliberate move (#617, #525). +- **Leg paths that are not the defaults.** The Makefile block `execute` + appends, and the cross-leg follow-up, name each leg where the plan mounts + it, so `--spec-path legs/spec` exports a `CONTRACTS_DIR` that exists and a + nested `--code-path` gets the right `../` path (#572, #522). +- **A leg path edited by hand.** The read-across follow-up names + `legs.code_path` and `legs.spec_path` and no path, and the `contracts/` + rule's reason names none, so the hand edit of a leg's mount that `plan` + asks for leaves no plan-time path in `check`, the split commit or the pull + request (#628, #600). A plan written before v1.1.0 whose `legs.spec_path` + or `legs.code_path` was edited by hand keeps its old value-form + read-across follow-up. `check` accepts it, because `follow_ups:` is free + text, and `execute` carries the stale line into the split commit and the + pull request's body. **Re-run `plan` for such a plan.** +- **A source's own `single-repository.yaml`** is asked drop or root, drop by + default, under a rule of its own, and no longer gets the general top-level + YAML question (#566, #528). +- **What a run says is what it does.** The plan's last follow-up is a review + of the pull request `execute` opens itself. A failed verification names + the pull request and the topic it left in place. Under + `--local-remote-dir`, NEXT names the split's branch. AGENTS.md says that + `check` passes an answer with no `resolution:`, and the ruleset hint no + longer offers the seed branch the scaffold dropped (#582, #523). With no + `--elected-by` and no `git config user.name`, the refusal says where the + empty value came from (#567, #529). +- **An id `plan` derived.** With no `--id`, `plan` derives one from + `--project`, and `adopt-bad-id` now says so: `--project North_Wind` no + longer says `--id 'north_wind' must match` over a flag nobody passed + (#620, #601). +- **Empty and derived values.** `plan` refuses `--id ''` and + `--elected-by ''` by name, a refused derived id says to change + `--project` (or, adopting in place, to rename the source first), and an + organisation read from a url is named as not given (#699, #658). +- **`--work-dir`.** A `--work-dir` that is a file or cannot be made is + refused by name in `plan`, `check` and `execute`, and `--help` says what + `--work-dir` is and where `--org` comes from (#719, #650). +- **`--source`, by its spelling.** An `org/repo` that is also a directory + here is refused `source-ambiguous`, naming both; any other `org/repo`, in + any spelling, is cloned by its name. The plan records `source.cloned`, + and a clone no plan records is removed, so a recorded clone that is gone + or emptied is refused, or cloned again, for what it is (#749, #652). +- **Names in a C locale.** Under `LC_ALL=C PYTHONUTF8=0`, `plan`, `check` + and `execute` hand git and `gh` each name, url and branch as the UTF-8 + bytes it spells, and the leg description, whose em dash stopped every + `execute` against GitHub at the first leg, so none of them ends in a + `UnicodeEncodeError` before any leg exists (#727, #653). +- **A default branch that is not UTF-8.** A source whose HEAD or + `origin/HEAD` is on a branch such as `caf` is refused, or adopted past + it, instead of ending `plan`, `check` and `execute` in a traceback (#717, + #425). +- **The Free-plan secrets hint.** `adopt-project.py` and + `scaffold-project.py` go by the assembly root's own visibility on GitHub + and name the root as the reason, so a public source adopted with private + legs is no longer told that its organisation secrets resolve to EMPTY + (#621, #602). The hint, in the scaffold, the adoption and `family.py`, + goes by what GitHub says of the root or holder and of its organisation's + plan, and names any answer GitHub did not give, so + `init --reuse-empty-repo` no longer calls a public holder private (#728, + #660). +- **Caught by `check`, not after both legs.** `check` asks this machine's + git what the split stores for each file the plan keeps or moves and each + pinned copy, so one it would store as other bytes is a finding (#743, + #654). A source symlink whose target begins with a NUL byte is + `plan-empty-symlink`, which `execute` refuses before any leg (#720, + #388). +- **`check --local-remote-dir`.** `check` takes `--local-remote-dir` and + resolves each relative submodule url a leg keeps from the bare + repositories `execute --local-remote-dir` would create, so under that test + path `check` no longer refuses an adoption `execute` verifies, nor passes + one `execute` refuses (#718, #417). +- **`adoption verified` means a shape root.** The verification fails a + split whose root `project.yaml` is not the shape's manifest + (`adopt-manifest-not-shape`), so it no longer passes an assembly the + doctor calls NOT A SHAPE ROOT (#722, #389). +- **Answers that stay answered.** A plan's answer against a rule is + recorded, and an empty `.gitmodules` asks nothing, so `docs/` answered + `code` is not MISPLACED after `adoption verified`, nor is a `.gitmodules` + emptied by `git rm` asked on every run (#721, #655). +- **Symlinks checked out as files.** `check`'s finding for a link kept above + a path the shape writes says that the clone holds it as a file (#584, + #531). +- **Installing git-filter-repo.** `filter-repo-missing` names its install + in `install.sh`'s order: `pipx install git-filter-repo==2.47.0`, else + `uv tool install git-filter-repo==2.47.0`, else brew's, pipx's from apt or + dnf, or pacman's. It led with a `pip install` that an externally managed + Python refuses (PEP 668). It says that `pipx install openreposhape` brings + it for the `adopt-project` command it places (#563). + +### update-shape.py, bump-leg.py and a project's own scripts + +- **A kept copy that would break bootstrap.** `apply --accept-local` + refuses, before anything is written, to keep a helper that lacks a name a + newly written copy imports from it (#562, #536), or a copy that imports a + name the module written beside it no longer defines (#616, #604). Both + used to pass the validators and then end `make bootstrap` in an + `ImportError`. +- **Hard links and rollback.** `apply` refuses to rewrite a pinned file that + has a second, hard-linked name. A rollback that cannot put a byte back + names that file with the line that finishes the job, and puts back the + rest (#560, #422, #428). +- **Headers, branches and messages.** A `files:` or `shape:` header with a + trailing comment is rewritten and keeps the comment, `--branch` is judged + the same from any directory, and `apply`'s messages say what is true + (#558, #437, #431, #535). +- **`apply`'s lines, its branch and its headers.** A rollback line for a + file that is gone, under a directory this user cannot write in, says to + make that directory writable first. `committed on :` names what + the commit holds, where it named `project.yaml` beside a pin that moved + alone. A branch named like a tag is read, and pushed in the run and in + both printed push lines, by its full name, where the push failed + `matches more than one`. A `files:` or `shape:` header spelled + `"files":` or `files :` is found as the reader finds it, where `apply` + refused the pin that `check` had sent it to (#810, #674). Of v1.0.0's + known limits, this closes the quoted header. +- **`bump-leg.py --dry-run` gives the real run's answer.** `--to` an + annotated tag's own id is peeled to its commit in both runs, and a pin + layout the rewrite refuses is refused by the dry run too (#561, #534, + #438). +- **`bump-leg.py`'s branch and its lines.** `bump-leg.py` reads the branch + in full, so a tag of the same name no longer hides the tracking branch; + its push line names the branch on both sides, so a branch beginning `+` + is no longer pushed as a forced update; it spells each file name it + lists; its `committed` line lists what the commit holds, and a gitlink + moved by hand is no longer reported `X -> X`; and a dry run says it runs + no validator, where its behaviour is unchanged (#746, #672). With #810, + that closes v1.0.0's known limit on the paths `bump-leg.py` and `apply` + listed as staged, and on `X -> X`. A dry run that exits 0 over a pin its + own validators refuse stays one, which the dry run now says. +- **A `git switch -c` line that is no dead end.** `bump-leg.py` and + `setup-project.py` say under their `git switch -c` line what to do where + an earlier act left that branch, so `already exists` is no dead end, as + `family.py` says since #637 (#657, #647). +- **Paste lines.** `bump-leg.py` and `update-shape.py` quote the roots and + paths in the lines they print, the doctor quotes a leading `=` for zsh, + and the README says the printed commands are for a POSIX shell (#588, + #532). +- **Bootstrap.** A leg detached off its pin is left where it is, and said, + so a commit made there is no longer stranded (#556, #533). A leg on + another branch whose tracking branch is behind the pin is fast-forwarded + and checked out, and the shipped text says bootstrap places a leg where + that loses no commit (#615, #603). `make bootstrap` moves a leg only where + nothing the person has is lost, says which branch it left, and refuses by + name when git cannot run (#745, #704, #694). With no git on `PATH`, both + shipped `bootstrap.py` scripts are refused `git-not-runnable`, exit 2, + where they ended in a `FileNotFoundError` traceback, which v1.0.0 listed + as a known limit. +- **A branch name git cannot take.** A `tracking_branch:` or a leg's + `branch:` that holds a NUL or a lone surrogate is a `validate-manifest.py` + finding, and `make bootstrap` does not hand it to git, so it no longer + ends in a traceback under a `manifest ok` (#644, #455). +- **The pinned validators.** THE LOCKSTEP RULE is printed only where a leg + or member is out of step. A lone digest or workflow-reference mismatch + names its `bump-leg.py` line, and a holder's remedies name the branch to + make first (#575, #539). One leg's gitlink off its pin, beside another + leg's digest or workflow-reference mismatch, names the step that comes + first: put that gitlink back by hand, then run the other leg's + `bump-leg.py` line (#702, #677). +- **The pinned shape copies, in one bump.** `AGENTS-shape.md` is true of + every root, the scaffold's chain WARNING offers only its own sources and + spells `--pin-source`, the root's not-used note spells it too, THE + LOCKSTEP RULE's `bump` runs as printed, and `repo_shape.py`'s `git_out` + spells the folder it names and git's stderr (ESC, CR, BEL, C1 controls) + for every caller (#634, #610). +- **A workflows directory the pins gate cannot read.** `validate-pins.py` + refuses a `.github/workflows/` directory this user cannot list or search + as `workflow-unreadable`, naming it, exit 2, where it ended in a + `PermissionError` traceback, exit 1 (#633, #434). +- **A pin list that is not a list.** A project's `neutral_product_pins:` or + a leg's `naming.referent_chain:` that is not a list is + `validate-manifest.py`'s finding for that field in every gate that reads + it, so `5` no longer ends `make validate` in a `TypeError`, and a scalar + `openXdox` is no longer read as its letters (#631, #443). + +### scaffold, setup, family and the shared readers + +- **Dry runs ask the real run's question.** `scaffold-project.py --dry-run` + and `family.py init --dry-run` ask whether the remotes already exist, so + `scaffold-remote-exists` and `family-remote-exists` come at the plan, and + a missing `gh` is `gh-not-found`, not a traceback (#573, #537). +- **Folders, asked before they are made.** `scaffold-project.py` and + `family.py init` ask whether each folder they make can be made, and take + only `gh`'s not-found answer as absent, so `--dry-run` refuses a file + where a folder goes, or a `gh repo view` that fails another way, by name, + as the real run does, and a refused `init` leaves no empty family folder + (#622, #609). + Those refusals, and `scaffold-target-exists`, `family-target-exists` and + `family-operation-in-progress`, spell a path holding a character that is + not printable as Python's `repr` does, so an ESC or a line break in a + folder's name no longer reaches the terminal (#731, #692). +- **`--tracking-branch`.** The tracking branch is pushed by its full name, + so `--tracking-branch +x` scaffolds a project and initialises a family + instead of failing at the push after every remote exists (#643, #421). + `family.py add`, `bump` and `remove` print their push line with the + branch named in full on both sides, so a branch beginning `+` or named + like a tag pushes as itself, where the line force-pushed another branch + over origin's, or failed. A branch name with a part longer than a disk + holds (a last part over 250 bytes, which git locks as `.lock`) is + refused before anything exists by `scaffold-project.py`, `family.py init` + and `adopt-project.py`, `--dry-run` alike, where the first commit failed + with every remote made (#813, #689). +- **`--local-remote-dir`.** A `--local-remote-dir` whose clone URL is not + text a README can hold, such as a directory named in a legacy code page, + is refused before any remote or folder exists, `--dry-run` alike, where + it ended in a `UnicodeEncodeError` traceback after the bare remotes were + made (#640, #433). `family.py add --local-remote-dir` refuses a member + clone URL that the holder's `.gitmodules` cannot hold as text, + `family-unwritable-value`, `--dry-run` alike, before the member is read, + where it committed the URL raw and `make siblings` read another path + back. Each clone-URL remedy says that the URL is the path the flag + resolves to, so the directory to rename may be a link's target, or the + one a relative path was given from, and names a lone surrogate. The + READMEs the scaffold and `family.py init` write quote a local clone URL + that needs it for a POSIX shell, so the `git clone` line under `a b/` + pastes as one command (#812, #693). +- **Names that are not UTF-8.** In `bump-leg.py`, `scaffold-project.py` and + `family.py`, a file name that is not UTF-8 in `git status` is + `bump-leg-root-dirty` or the scaffold's DIRTY warning, and a + `git config user.name` that is not UTF-8 is refused + `family-unwritable-value`, naming `--created-by`, where each ended in a + `UnicodeDecodeError` traceback (#641, #423, #426). +- **What `family.py init` records and commits as.** It asks whether + `family.yaml` can hold the creator whichever of `--created-by`, + `GIT_AUTHOR_NAME` or `git config user.name` gave it, and the remedy names + that source; it asks git, before the remote exists, whether the holder's + first commit can be made with the `GIT_AUTHOR_*` and `GIT_COMMITTER_*` + values set (`family-bad-commit-identity`); and it refuses a `--family` + that is a Windows device name, such as `CON` (`family-bad-holder-name`). + `--dry-run` refuses alike, where a creator holding a line break or a + control character was taken and a name git refuses failed after the + remote and the work directory existed (#809, #688). +- **A re-run's legs.** A leg that `gh` sees and git cannot read is refused + `scaffold-remote-exists`, with git's terminal prompt off, and the reuse + re-run's visibility lines go by each leg as it stands (#565, #542). +- **`--pin-source`.** A pin file records `digest_source: local-clone`, never + the path, in the scaffold and in `adopt-project.py`, and a `--pin-source` + nothing reads is refused `pin-source-unused` (#570, #540, #451, #450). +- **A `--pin` naming a tag.** In `scaffold-project.py` and + `adopt-project.py`, a `--pin` that names an annotated tag's own id pins + the commit the tag names, and one that names a tree or a blob is refused, + so a pin file no longer records a tag's id as its commit (#725, #651). +- **A derived id, named as derived.** `scaffold-bad-id` and + `family-bad-id` name an id derived from `--project` or `--family` as + derived and say to change that flag, so `--project North_Wind` no longer + sends the person to an `--id` the naming policy then refuses (#671, + #659). +- **setup-project.py.** Its paste lines quote each path, the scaffold's NEXT + STEPS wait for step (7), step (3) refuses a file above the clone, + `-- --dry-run` ends at the plan, step (4) hears the pins passed to it, and + the upstream-organisation guard ignores case (#569, #538). +- **setup-project.py's checks before a run.** Step (3) refuses a parent + folder nobody can write in and a drive that is not there, step (4) + verifies a chain from `--pin-source`, `--pin` no longer takes a flag as + its value, and step (5) says when a printed plan was refused (#623, + #608). +- **Pin chains.** A bare `--link-source` is the tree of the chain's first + link alone, and a source that names no path is refused (#576, #441, + #442). +- **A chain link no tree answers for.** The root's `validate-manifest.py`, + the naming gate and setup's step (4) each offer what that caller reads, + so neither the validator nor setup offers `--link-source`, which neither + takes, and the gate spells every `--link-source` value and path it prints. + The naming gate takes `--link-source-label` and `--link-remedy`, so setup + passes its own flag and words for a holder (#744, #661). +- **One product, one spelling.** A chain link, a declared pin and a + `--link-source` key name one product exactly when they share a pin file, + so a tree declaring `openss` no longer verifies a chain that says + `openß`, a chain naming both is two links and not a cycle, and + `--link-source openss=` no longer answers for `openß` (#638, #444). +- **The YAML reader.** The standard's own reader in `repo_shape.py`, which + the tools use to read `project.yaml` and the pin files, refuses a plain + value opening with an indicator, an empty flow entry and a single pair in + a flow sequence, so `k: ]` is not the text `]`, `pins: [,a]` is not + `['a']`, and `k: [a: b]` is not the string `'a: b'` (#635, #392, #393). + A value refused for opening with an indicator is named by its line, and + the refusal says to write it in double quotes, spelled as the reader + reads it back, or to reword it. `plan --org @x` and + `scaffold-project.py --org @x` still name `--org`, now in the reader's + words and under its remedy, and `check` reports a `tracking_branch` + holding a NUL or a lone surrogate once, as `unsafe-value`, without the + `validate-manifest.py` finding above (#644), which names no plan key + (#817, #708). +- **family.py.** `add` refuses, before the member is read and on every disk, + any path under another spelling of `members/`, with a remedy per entry + that never moves the members' checkouts (#574, #362, #363, #364). `add` + and `remove` refuse a `.gitmodules` the work tree does not hold as the + index does (#571, #407). `bump --to` asks `add`'s duplicate-id question, + peels an annotated tag's own id as `add --at` does, and says to push first + a commit the member does not hold (#568, #541). It also refuses a commit + that only the member's mount holds, reachable from no branch of the + member's remote, as `bump-leg.py` refuses its twin (#625, #613). + `make siblings` asks about the family folder before it clones, clones each + member on its tracking branch, and `family.py siblings` runs the holder's + own copy (#578, #543). +- **`bump --to`, reachable or not.** `family.py bump` fetches every branch + of the member's `origin`, whatever fetch refspec the mount has, and + prunes the remote-tracking branches `origin` has deleted, so a commit a + branch holds is no longer refused for a refspec narrowed by hand, and one + only a deleted branch held is no longer pinned. A commit only a tag of the + remote reaches is told that a tag is not a branch, and a mount with no + remote named `origin` is told so, where each was told the pin cannot be + fetched, or to fix the network (#795, #686). +- **family.py refuses before it writes.** `add`, `bump` and `remove` refuse, + before their first write, a merge, cherry-pick, revert, rebase or + `git am` in progress, a `.gitmodules`, `members/` or `family.yaml` that + the sparse checkout leaves out, and a file named `members`. Each failed + after writing and was rolled back, or was committed in the wrong place + (#626, #612). +- **More holder states, named.** `add`, `bump` and `remove` name, before + their first write and `--dry-run` alike, a `git bisect` in progress, a + rebase directory that holds no rebase, non-cone sparse rules that hold + no pattern, a merge conflicted in `family.yaml` itself, and a `members` + file that HEAD holds and the work tree no longer does, each with the way + out that works there. They refuse a `family.yaml` holding a change HEAD + does not, staged or not (`family-manifest-uncommitted`), which their + commit took in beside their own row, so a `bump` an earlier run staged + and did not commit is told to put back HEAD's copy and run again (#816, + #684). +- **The `switch -c` remedy.** Where an earlier act left the branch, + `family.py` and `validate-family.py` say to give the new branch another + name, or to go back to the old one only if it holds work not merged yet, + and the docs spell it `family/add-` (#637, #607). +- **A holder's gitlinks and its `.gitmodules`.** A gitlink that no section + of HEAD's `.gitmodules` names is `validate-family.py`'s finding + `member-gitmodules-absent`, `family.py remove` refuses a holder with no + `.gitmodules` before it deinitialises anything, and two gitlink remedies + name what they missed (#639, #611). +- **`validate-family.py`'s remedies, typed as printed.** After the `git rm` + that `member-gitlink-unnamed` prints, and before the commit, the finding + says the removal is staged and to commit it, and prints no `git rm`, + where it printed the same command again, which then failed. + `member-gitlink-absent`'s remedy puts the gitlink back by hand, in the + index at the commit the row pins, with its `.gitmodules` section where + HEAD's gives none, where it named a `family.py add` that refuses the row. + The `mv` for a directory under another spelling of the row's path moves + nothing where the disk takes the two names for one directory or the + directory holds a `.git`, so it no longer moves a member's own checkout. + In both validators, a shape-pin `files:` row whose `path:` is absent, + empty or not text is the finding `shape-pin-row`, where it was reported + as the path `None` (#747, #681). The first and the last were known + limits of v1.0.0. +- **A family's name, printed.** `make siblings`, `park` and `resume` print + the family's name and every member row, and no longer end in a traceback + on a name such as `"a\ud83db"` or on a dash under + `PYTHONIOENCODING=ascii`. The holder's README names its folder after the + repository, so `--family Quill --name QuillWorks` lays out as its README + says (#642, #454, #629). `setup-project.py` names a family folder after + the holder's repository too, so the `Quill/Quill` that `family.py init + --name QuillWorks` makes is detected with no flag (#703, #696). +- **`make siblings`'s inputs.** The move it prints to put the holder in a + folder named after the family runs as typed past a staging name + something already holds, and says first to move aside a link to nothing + in its way. A member whose `project:` or url holds a NUL or a lone + surrogate is a finding, `NOT AN ARGUMENT`, in the dry run too, and the + members after it are still placed, where the run ended in a traceback. It + reads the holder's `.gitmodules` through one `git config -z --list`, as + `validate-family.py` and the doctor do, so a url holding NEL or U+2028, + or a section named `members/a=b`, gives the url git uses. Its warning + spells the holder, the folder and the family, and a `tracking_branch:` + that is only a tag of the member's remote is said to leave the clone + DETACHED (#805, #690). +- **A member's gitlink under another spelling.** Where a member's only + gitlink sits under another spelling of its row's path, `family.py add`'s + refusal moves it back to that path in the index alone, so the printed + remedy no longer takes the member's gitlink away from its row (#670, + #646). +- **A `members/` renamed with `git mv`.** `family.py add`'s + `family-members-dir-variant` refusal renames it back in two steps through + a name nothing holds, `git mv -- Members members.renaming`, then + `git mv -- members.renaming members`, which takes each checkout and each + section of `.gitmodules` with it on every disk, where the move back left + each section naming the other spelling. It says first to put back what + the disk lacks under it, and to stage, or put back, the work tree's + `.gitmodules`. A printed `git mv --sparse` says it needs git 2.38 or + later, and the move back's `git rm --sparse` git 2.34, so no remedy fails + as typed (#767, #685). +- **A holder's `make bootstrap` that left a member unchecked-out.** It no + longer says `family bootstrap ok` there: a subset clone, a member with + `update = none`, a gitlink git stops at (a stray `Members/

    ` beside + `members/

    `, a member mounted by hand with no row, a `.gitmodules` + section with no url), a `.gitmodules` that git reads otherwise than + `git config` does (two sections for one path, a section name git + ignores, a value beginning with `-`), and a `.gitmodules` a merge left + unmerged now fail, and the line names the cause and the cure. This + changes an exit code a script can see: ok became exit 1 (#682, #648). + +### shape-doctor.py + +- **Both ways in.** A git repository that is not a shape root is told of + the adoption and the scaffold, and the adoption line ends + `--org ` wherever `plan` can read no organisation from `origin` + (#553, #545; #591). +- **What it writes, and its clone.** `openRepoShape --doctor` says it writes + nothing into the repository but a `--placement-plan` file you name, keeps + its clone for the NEXT lines that name it, and refuses a second + `--placement-plan` or a second path (#580, #546). +- **A validator's verdict.** A row's verdict is a printed `FINDING` or + `REFUSED`, and any other non-zero exit is a crash, not a finding about the + project. A crash of the standard's own naming script is `CANNOT ANSWER`, + and a pass that printed `WARNING` or note lines is a `note` that quotes + them (#557, #410, #447, #448, #460). +- **A root that lost `repo_shape.py`.** Every validator the doctor runs + ignores site-packages, so a `.pth` file in the user's or a virtualenv's + site-packages no longer stands in for the `scripts/repo_shape.py` a root + lost: the `manifest` row is the gate's own `ModuleNotFoundError`, as in + CI (#636, #342). An own-copy validator row whose crash is that loss names + `git -C checkout HEAD -- scripts/repo_shape.py` as its NEXT, or + none where HEAD no longer holds it (#737, #697). +- **A family holder's `.gitmodules`.** `--root` on a holder reads a member + url that is not UTF-8 as `make siblings` does, so a member mounted from a + legacy-code-page directory gets a verdict, where the doctor ended in a + `UnicodeDecodeError` traceback, exit 1 (#632, #432). It reads the file + through one `git config -z --list`, so a section name holding a space or a + byte that is not UTF-8, or a url holding NEL or U+2028, gives the mount + url git and `make siblings` use, under any locale (#700, #668). +- **The `members` row.** It says, member by member, what `make siblings` + does with the directory beside the holder, and spells every value it + quotes, so an ESC or a CR in a url no longer reaches the terminal (#705, + #669, #673). It says `make siblings` places the clones only from a folder + named after the holder's repository (#624, #605). +- **NEXT lines.** The `legs` and `members` NEXT names every detached commit + it would move, and sends a mount with no `.git`, an unborn HEAD or no url + to a line that cures it, or to none (#559, #413, #414). `manifest kinds` + prints one command for its NEXT, and counts a manifest validated only at a + path its gate reads (#555, #548, #439). A conflicted shape pin's NEXT takes + whole each copy both sides hold as the standard wrote it, and a leg pin put + back from before its bump says to commit it and run the `pins` row's line + (#586, #547); it reads both sides' pins, so a copy that only the side not + taken has a `files:` row for is named for a person to merge, and never + taken (#736, #687). +- **The `machine` row** of a machine that is not ready names the preflight + checks that failed (#552, #549). +- **A partial clone of the standard.** Run from one, the `shape currency` + row asks, without fetching, whether the clone holds what the comparison + reads, and where it does not it is CANNOT ANSWER, exit 3, with the fetch + for a person to run; `update-shape.py` refuses a read that fails + (`update-upstream-unreadable`), never calling the copy `both` (#742, + #683). +- **A naming policy that does not load.** A checkout whose own + `contracts/repository-naming.yaml` does not load is CANNOT ANSWER, exit 3, + where it was `INVALID (naming)`, exit 1, and the EXIT CODES list names an + unfinished merge (#701, #678). +- **The `pins` row and REFUSAL AHEAD.** The row keeps its gate where + `update-shape.py check` says REFUSAL AHEAD beside a merged-away row, so it + no longer says the `apply` that `check` prints drops the row when that + `apply` is refused, and the `shape currency` counts name every state + `check` counts (#751, #676). +- **`placement` and a leg's `single-repository.yaml`.** The row asks a + leg's top-level `single-repository.yaml` the staying-single record's + question only where `kind:` makes it the record, as `plan` asks it + (#734, #691). +- **A signal to `openRepoShape --doctor`.** A signal sent to the command's + shell alone waits for the doctor, so its clone of the standard is never + removed while `shape-doctor.py` runs from it, and a doctor ended by a + signal leaves nothing behind (#740, #680). + +### Install, CI and the docs + +- **PyPI** (#590, #599, #563) and **the one-line install** (#587, #564; + #598, #595), above. +- **The launcher in Git Bash.** Its `machine:` row finds `python`, a + drive-letter `$OPENREPOSHAPE_REPO` is read as a path, a Windows-spelled + `$OPENREPOSHAPE_BIN_DIR` is respelled or refused, and the `tests-git-bash` + job runs `install.sh`'s cases from #598 too (#656, #649). +- **`$OPENREPOSHAPE_BIN_DIR` on Windows.** `install.sh` respells a + Windows-spelled `$OPENREPOSHAPE_BIN_DIR`, or refuses it, before it fetches + anything; `--install` refuses one holding `:` or a newline, where it + placed the command and printed a line that cannot be used; and the + `export PATH=...` note single-quotes a directory holding a double quote, + a `$`, a backquote, a backslash or `!` (#726, #667). +- **CI** on Linux only (#597), with a result for every landing on main + (#593, #592), and a Git Bash job on Windows (#596, #585). Every test that + runs `git clone` on a just-pushed local remote now clones it in a way git's + background maintenance cannot race (#554, #544), and the test that holds + the doctor's NEXT lines to no `#` comment tail reads each string whole + (#698, #679). + The test session turns git's automatic maintenance off, in command scope + and in a global config file that includes the person's own, so a test's + copy of a repository it has just committed in or pushed to no longer + meets a repack deleting its loose objects (#713, #645). + The five `git submodule update --init` runs that still cloned a test's + bare remotes by plain path, four of them the runs v1.0.0 listed among its + test-fixture limits, now read them through git's transport too, by the + `url..insteadOf` rewrite that `clone_just_pushed` uses, now + built by one helper, and a pasted line's environment keeps the session's + own git settings (#748, #662). With #554 and #578, that closes v1.0.0's + limit on fixtures that clone a just-pushed remote by plain path. + The tests that need a file name or a branch that is not UTF-8 ask one + pair of helpers whether this system can carry one, and skip on Windows + and on a disk that refuses such a name, as macOS's does; the helpers are + held to Windows' answer on Linux, with its `os.fsencode` emulated (#723, + #663). + Tests now fail if `validate-pins.py`'s workflow scan reads a directory, a + dangling symlink or a looping symlink named `.yml`, or if `siblings.py`'s + headers print the holder's root and folder raw (#706, #664). + `scripts/render-cli-reference.py` sets `PYTHONDONTWRITEBYTECODE` for the + `--help` children it runs, so the generator leaves no `.pyc` files in the + template tree, which v1.0.0 listed among its test-hygiene limits (#724, + #665). Comments and docstrings in `adopt-project.py`, `shape-doctor.py` + and a test say what the code beneath them does (#730, #666). +- **The docs.** README.md and the handbook say how to install from PyPI, + first on native Windows, and that the one line installs git-filter-repo or + prints the command for the machine (#563), and that Git Bash runs the one + line, tested in CI but for a typed `yes` (#596, #585). Each handbook section + records the hash of every README section it reads, and README's line cap is + a base plus one file per raise in `tests/readme-cap.d/` (#551, #519). The + by-hand setup commands dry-run the scaffold with the `--visibility` and + `--elected-by` setup passes, and `apply --add`'s five refusals are listed by + their codes (#579, #550, #429). +- **The doctor's exit 3, listed whole.** README.md and the handbook name + every reason the doctor cannot answer: a pin's commit the checkout lacks, + a partial clone missing what is compared, a read that fails, a validator + of its own that crashed or could not be run, and its own naming policy + not loading (#630, #683). +- **Three sentences made true.** README.md and the handbook say that a + source's own `single-repository.yaml` record arrives as `leg: drop`, that + the adopted Makefile's `CONTRACTS_DIR` follows the spec leg's mount, and + that a second setup run is refused at the plan (#627, #606). +- **`validate-pins.py`'s codes for a neutral-product pin.** README.md and the + handbook say again that a digest that recomputes differently is the + finding `neutral-pin-digest-mismatch` (exit 1), whose absence v1.0.0 + listed as a known limit, and that a declared pin file that is not there is + the refusal `neutral-pin-missing` (exit 2) (#630, #460). + +## Known limits + +v1.0.0's [known limits](v1.0.0.md#known-limits) that this release does not +close still hold; this list is not all of them. Two that work on this +release named and left open: + +- The README's and the handbook's `--into` sentence for `family.py init` + gives its default, the directory you are standing in, but not that + `init` refuses that default inside a checkout of the standard + (`family-landing-in-standard`) (#695). +- `bump-leg.py --dry-run` over a pin its own validators already refuse, + such as one with a stray tab line, exits 0 and prints the `old -> new` + line, where the real run exits 2 `bump-leg-validators-red` and rolls + back. The dry run now says that it runs no validator (#746, #672). + +New in v1.1.0: + +- A root scaffolded or adopted with `--pin-source` before v1.1.0 records + the source's local path in `digest_source:` in + `contracts/-pin.yaml`. Editing it to `local-clone` removes the + path from the tip, and git history keeps it. +- A NEXT line pasted as `python3 /adopt-project.py …` runs outside + the package, so on a machine whose only git-filter-repo is the package's, + it is refused `filter-repo-missing`. The refusal says the package's + `adopt-project` has it. +- The package on Windows: CI runs its tests on Linux only (#597). The + release's install check runs `--version`, `--preflight`, `--doctor`, the + four tools' `--help` and, under PowerShell, `openRepoShape --version` + there, and no test scaffolds through it on Windows. +- Git Bash on Windows: the preflight that the one line ends with cannot take + a typed `yes` there, so none of its offers can be accepted, and CI runs + only the launcher's and `install.sh`'s suites there. Git Bash's `ln -s` + makes a copy unless `MSYS=winsymlinks:nativestrict` is set, and a copy of + the command has no `openRepoShape.ref` beside it, so it follows main, not + the installed release: link it with that setting (Windows lets a + non-administrator make a symlink only in Developer Mode), or put + `~/.local/bin` on `PATH` (#596, #585). + +Kept as known limits by the checklist triage of the L4 lines on #457 and +#459, which Brett Heap approved at 2026-10-16T13:36:07Z; each line's text +stays on its issue ([#630](https://github.com/opensoft/openRepoShape/pull/630#issuecomment-6217406337)): + +- Windows: adopting a source whose path holds a character NTFS refuses, such + as a TAB or a control character, is unverified there, because such a path + cannot be checked out (#457's L4-05). +- Windows: adopting a source whose path is not UTF-8, such as `b'caf\xe9'`, + is unverified there, because such a path cannot be made (#457's L4-06). +- Windows: the submodule plan of a source whose tree names hold a newline, a + TAB or a backslash is unverified there, because such a name is not kept in + a tree (#457's L4-07). +- Windows: adopting a source that tracks symlinks is unverified there, + because they check out as plain files unless `core.symlinks=true` + (Developer Mode or an elevated shell) (#457's L4-08). +- A root adopted before #471's fix, from a source that kept its own root + `.gitattributes`, has no line-ending block, so on Windows its pinned + copies still check out CRLF and `make bootstrap` reports + `shape-copy-drift` (#457's L4-17). +- `adopt-project.py` compares an `origin` with the source by Python's + `casefold`, which is broader than GitHub's ASCII-only folding, so a path + `origin` `.../straße/Atlas.git` and `--org strasse` are taken for one + repository (#457's L4-43). +- `check` says `plan ok` for a source whose own repository is literally + `None/`, `False/` or `0/`, though `execute` still + refuses it (#457's L4-45). +- A test, `tests/test_adopt_ignored_shape_files.py`, feeds + `git hash-object --stdin` a link target as text, not bytes; harmless + today, because no target holds a newline or a non-ASCII byte (#457's L4-54). +- `setup-project.py` strips only C0 control characters and DEL from a value + it prints, so C1 controls and direction-override characters still reach + the terminal (#459's L4-24). +- The YAML reader is a subset by design, and still reads, where PyYAML + refuses or reads another thing, `k: - a`, `k: [- ]`, `k: [e?]`, `k: [|a]`, + a block key that opens with an indicator, and a flow `?a` (#459's L4-156). + +Kept as known limits by the checklist triage of the L1 lines on #457, +which Brett Heap approved at 2026-10-16T11:21:17Z; each line's text stays +on #457 ([#457](https://github.com/opensoft/openRepoShape/issues/457#issuecomment-6214768828)): + +- The `--pin-source` digest reader runs git with lazy fetch on and the + terminal prompt allowed, so a partial clone can fetch and a private https + remote can prompt, where the help says it uses no network (#457's L1-02). +- `update-shape.py apply --add` and the doctor read no git attributes, so a + nested `.gitattributes` added after adoption makes CRLF clones drift with + no finding that names the cause (#457's L1-08). +- A setting passed only through a parent git's `-c`, such as + `core.attributesFile`, reaches the split's `git add` but not `check`'s + re-encoding question, so `check` passes and `execute` refuses `adopt-lost` + after both legs exist (#457's L1-10). +- A `transfer.hideRefs` or `receive.hideRefs` pattern that covers the split + branch passes `check`; `execute` makes both legs, and the split push is + then denied (#457's L1-13). +- A tracking branch the source hides from `ls-remote` passes `check`, and + `execute` refuses it `plan-tracking-branch-missing` before any leg + (#457's L1-14). +- With `org:` and `adopt_branch:` both outside the alphabet, `check` + refuses `org` first and `execute` refuses `adopt_branch` first + (#457's L1-17). +- Two branch names that differ "only by case" are judged with Python's + `casefold`, which also folds `ß` to `ss` and `ſ` to `s`; how APFS, NTFS + and GitHub treat such pairs is not established (#457's L1-18). +- Under `core.symlinks=false`, a link at the `shape/` copy of a path the + root holds through a link above it gets a finding whose every fix leads on + to `adopt-symlinks-off` (#457's L1-29). +- Under `core.symlinks=false`, `.github -> ci` with `ci -> ""` reports only + `plan-empty-symlink` for `ci` and never names `.github`, so the fix takes + two rounds (#457's L1-30). +- Under `core.symlinks=false`, `check` passes a link above a directory the + root keeps (`scripts -> ci`), and `execute` then refuses + `adopt-symlinks-off` (#457's L1-31). +- A failure of `check`'s probe `git init` other than an `OSError` stops it + with "a git or gh command failed" instead of a finding (#457's L1-32). +- A url whose organisation is refused is a dead end: + `https://example.invalid/café/Atlas.git` with `--org acme` is refused as + not the source, and the suggested `--org café` is refused `unsafe-value` + (#457's L1-40). +- An `origin` that `url..insteadOf` rewrites to a local path reads as + a path, so nothing is compared (#457's L1-41). +- A url's host is never checked, so a GitLab url is compared as GitHub's + (#457's L1-42). +- A drive-relative `origin` such as `C:foo/bar` reads as git's `host:path` + form, though git on Windows reads it as a path (#457's L1-44). +- Only `plan` prints a NOTE for an `origin` that is a path; `check` and + `execute` print none (#457's L1-45). +- `https://github.com/acme//Atlas`, with a doubled slash, names no + repository, so nothing is compared and `plan` exits 0 (#457's L1-46). +- A path `origin` that is not UTF-8 gets no NOTE (#457's L1-47). +- `plan` does not follow a path `origin` to that repository's own `origin`, + which would catch more mistyped `--org` values (#457's L1-48). +- A submodule url naming an empty bare repository answers `ls-remote`, so + `check` passes and `execute` verifies, and the recursive clone then fails + with `upload-pack: not our ref` (#457's L1-50). +- An absolute local-path submodule url that answers on this machine, kept + in a leg pushed to GitHub, works only on machines that have that path, and + nothing notes it (#457's L1-51). +- A path on a mounted network filesystem (NFS, a mapped drive) looks like + this disk to git and is asked; its spelling cannot tell it apart + (#457's L1-52). +- The NOTE for a submodule url that is not asked gives its network reason + for a bare relative url such as `dependency`, which names no network + (#457's L1-53). +- On Windows, a UNC `--local-remote-dir` (`\\server\share\remotes`) makes + every relative submodule url resolve to a UNC path, which is never asked + and gets no NOTE (#457's L1-54). +- A test, `tests/test_adopt_pins.py`, writes its stub `gh` with + `Path.write_text`, so a CRLF emulation run fails one case; not a product + bug (#457's L1-60). +- #618's guard test does not clear `GIT_ALLOW_PROTOCOL`, so a caller's + environment could mask a missing guard; not a product bug (#457's L1-62). + +## How this release is cut (remove at tag time) + + + +`.github/workflows/release.yml` publishes the wheel, and its header's +"RELEASING TO PyPI" is the authority for these steps. The release follows acts +made once, on Brett Heap's behalf: the trusted publishers on pypi.org and +test.pypi.org, which he made, and the `testpypi` and `pypi` +environments and the tag ruleset, which lane openRepoShape-3 made as brettheap +on his word (below); the build job checks the environments before every +dispatch. It also follows a rehearsal, a dispatch on a branch that passes +every job it runs, which no workflow checks: it is this process's step, done +before the tag. + +On Brett Heap's word at 2026-10-16T12:06:40Z ("can you do the github work +with my gh creds?"), lane openRepoShape-3 made, as brettheap, the +environments `testpypi` (12:07:31Z) and `pypi` (12:07:39Z), each with +brettheap as its required reviewer, no admin bypass and one deployment rule, +the tags `v*`; and the tag ruleset 25237399 (12:08:14Z), under which only +the repository admin role creates, moves or deletes a `v*` tag. The +rehearsal passed: `gh workflow run release.yml --ref l3-rehearsal-v1.1.0 +-f version=1.1.0`, on a branch at main `a5245427`, run +[38898735926](https://github.com/opensoft/openRepoShape/actions/runs/38898735926), +from 12:10:53Z to 12:12:00Z. Its build and verify job passed, both +environment checks and the tests gate (main's push run 38894482092 at that +commit) included, and so did the install check on ubuntu-latest, +macos-latest and windows-latest with Python 3.14.8, and on ubuntu-latest +with Python 3.9.25. Both uploads were skipped, as on every branch. +Brett Heap added the pending trusted publishers on pypi.org and +test.pypi.org, and said so at 2026-10-16T13:04:43Z ("the pypi are added"). +At 13:07:56Z he confirmed that "they match what you listed": each names the +project `openreposhape`, the owner `opensoft`, the repository +`openRepoShape` and the workflow `release.yml`, with the environment `pypi` +on pypi.org and `testpypi` on test.pypi.org. + +1. **The release commit passes `tests` on itself**, in a `push` or + `workflow_dispatch` run of `tests.yml` whose head is that commit. The + build job refuses any other commit, and a pull request's run, or a run on + another commit whose tree is the same, does not count: v1.0.1's + tree-equality check is not this release's gate. On main, the push run + that landed it counts when it finished green; since #593 the next landing + no longer cancels it. Otherwise, and always on a `release/v` branch, + which no push run tests, dispatch `tests.yml` before tagging on a branch + that points at the release commit, and wait for `tests` to pass: + + ```sh + git push origin :refs/heads/ + gh workflow run tests.yml --ref -R opensoft/openRepoShape + ``` + + A `release/v` branch serves while it is still at that commit. + `--ref main` tests main's current head, not an older commit. If the tag + is pushed already and the gate refused it, dispatch on the tag, + `gh workflow run tests.yml --ref v1.1.0`, wait for it, and re-run the + release run. That a tag's dispatch runs at the tag's commit is GitHub's + documented behaviour, not yet seen here; a branch at the commit is the + proven route. +2. **Tag that commit, annotated, and push the tag.** The commit must be on + `main`, or on `release/v1.1`: + + ```sh + git tag -a v1.1.0 -m "openRepoShape v1.1.0" + git push origin refs/tags/v1.1.0 + ``` + +3. **Dispatch the release workflow on the tag, with the version:** + + ```sh + gh workflow run release.yml --ref v1.1.0 -f version=1.1.0 \ + -R opensoft/openRepoShape + ``` + +4. **Approve the `testpypi` deployment.** The install check then runs on + ubuntu, macOS and Windows and on Python 3.9. Approve the `pypi` + deployment only once it has passed. A failed job is re-run with "Re-run + failed jobs", never by re-running the whole run or dispatching the + version again. +5. **Only then create the release, with `install.sh` attached,** from a + checkout at ``, with these notes cut down to their + published sections and every placeholder filled: + + ```sh + gh release create v1.1.0 install.sh --verify-tag --latest \ + --title "openRepoShape v1.1.0" --notes-file -R opensoft/openRepoShape + ``` + + so the notes never name a version that PyPI lacks. +6. **Check the published asset, by redirect and by bytes,** as v1.0.1's + step 4 does, with `v1.1.0`: + + ```sh + curl -fsSI --proto '=https' --proto-redir '=https' \ + https://github.com/opensoft/openRepoShape/releases/latest/download/install.sh + curl -fsSL --proto '=https' --proto-redir '=https' \ + https://github.com/opensoft/openRepoShape/releases/latest/download/install.sh \ + | cmp - <(git show v1.1.0:install.sh) + ``` + + The first must answer with a `location:` naming + `…/releases/download/v1.1.0/install.sh`; the second, run in bash or zsh, + must print nothing and exit 0. + +A version is uploaded once. A bad upload is fixed by the next patch, never +by moving a tag. + +## Before tagging (remove at tag time) + + + +- **``** stands for the release commit wherever it appears + above. +- **Not landed at drafting time:** #630, the docs pull request for #563, + which lands last before the release, because `pipx install openreposhape` + works only once 1.1.0 is on PyPI. #590 (the package, `fe237ec`) and #599 + (the release workflow, `32c22b0`) have landed. +- **[placeholder: re-read at tag time.] Open `v1.1.0` milestone issues at + main `6f64eea`, 2026-10-17T20:15Z (1 open, 132 closed), each with a pull + request open:** #563 (#630). #684, which #816 fixed but for its + reftable case, and #695 (#738) are on the `v1.1.1` milestone now. + + Each that lands before the tag gets its line above, and each that does not + is a known limit or moves to the next milestone. + +## Full list + +- Merged pull requests: +- The `v1.1.0` milestone: diff --git a/scripts/render-cli-reference.py b/scripts/render-cli-reference.py index ee8c0330..92d6a835 100755 --- a/scripts/render-cli-reference.py +++ b/scripts/render-cli-reference.py @@ -215,7 +215,14 @@ class Tool(NamedTuple): ("Run from a checkout of the standard", CHECKOUT_TOOLS, "Every command below is typed at the root of a clone of this " "repository. `openRepoShape` is also the one file `--install` places on " - "`PATH`, and is the only entry point that needs no checkout at all."), + "`PATH`, and needs no checkout at all. `pipx install openreposhape` " + "(#563) places a Python `openRepoShape` with the same modes, whose own " + "`--help` says what differs (its `--install` refuses and names `pipx " + "upgrade openreposhape`), and four commands that need no checkout " + "either: `scaffold-project`, `adopt-project`, `update-shape` and " + "`shape-doctor` each run the tool of that name below, with every " + "argument unchanged, from a clone of the standard at the package's own " + "release commit, so their flags are the ones below."), ("Shipped into every project (copied, digest-pinned)", PROJECT_TOOLS, "These six are COPIES. `scaffold-project.py` writes them into a " "scaffolded project or a family holder and records each one by sha256 " diff --git a/tests/readme-cap.d/460.txt b/tests/readme-cap.d/460.txt new file mode 100644 index 00000000..e0062f9f --- /dev/null +++ b/tests/readme-cap.d/460.txt @@ -0,0 +1,11 @@ ++2 +2026-10-16 +Two lines for #460's L1, which Brett Heap gave lane 3 at +2026-10-16T12:24:41Z and which rides with #563's docs pull request, #630. +README's `validate-pins.py` paragraph says again what a digest that +recomputes differently is, the finding `neutral-pin-digest-mismatch` +(exit 1), the true half of a sentence #465 removed, and what its false half +got wrong: a declared pin file that is not there is the refusal +`neutral-pin-missing` (exit 2), not drift. That paragraph is where a reader +learns what the pins gate says about a neutral-product pin, and v1.0.0's +notes listed the missing sentence as a known limit. diff --git a/tests/readme-cap.d/563.txt b/tests/readme-cap.d/563.txt new file mode 100644 index 00000000..1b86e38d --- /dev/null +++ b/tests/readme-cap.d/563.txt @@ -0,0 +1,23 @@ ++12 +2026-10-15 +Twelve lines for the second way in, `pipx install openreposhape` (#563), +which Brett Heap approved as decision 6 of #563's design at +2026-10-15T13:32:51Z ("approve all six for #563"): the one-line install stays +first, pipx comes second, and first on native Windows. +Step 3 of the quick start gains the second way, one paragraph: the command, +where pipx comes from on Debian, Ubuntu and macOS, and what it places that the +one line does not: four commands named for the tools, git-filter-repo as their +dependency, a cached clone at the release's commit checked against its digest, +how it upgrades, and that both installers place ~/.local/bin/openRepoShape, so +a reader uses one of them (+8). +The native Windows block leads with the same command and with how pipx is +installed there, because pipx is the one install that gives PowerShell an +`openRepoShape`; the two-command download stays after it, as the way that +installs nothing (+3). +The Layout block names `src/openreposhape/`, the package #590 adds (+1). +Reworded at no cost in lines: git-filter-repo under "Adopting" says step 3's +one line installs it or prints the command for this machine, where it led with +a `pip install` that an externally managed Python refuses (PEP 668), and that +the package brings it; the standard-library sentence says the package is a +second way in, never a requirement, where it said "no packaging"; the native +Windows paragraph keeps #596's Git Bash sentence beside the pipx one. diff --git a/tests/readme-cap.d/683.txt b/tests/readme-cap.d/683.txt new file mode 100644 index 00000000..21670aa4 --- /dev/null +++ b/tests/readme-cap.d/683.txt @@ -0,0 +1,16 @@ ++1 +2026-10-17 +One line for the doctor's exit 3, taken in #630 for #683 (W683, and RV-f's +N4 on #742, which landed the partial-clone case). README's list of what +makes `shape-doctor.py` exit 3 named only a pin whose commit this checkout +does not carry. shape-doctor.py's own EXIT CODES block names more: a +partial clone lacking what the comparison reads (#683, added by #742), a +validator of its own that crashed or cannot be given its `PYTHONPATH` +(#278, #410), and its own naming policy not loading (#678, added by #701); +and the code refuses a read of the standard that fails +(`update-upstream-unreadable`, #742) as CANNOT ANSWER too. A reader who +gets `CANNOT ANSWER` looks the exit up in this paragraph, and it now lists +every cause. Reworded so the five causes cost two lines, and one of +them given back by cutting "exactly" and "about the shape" from the +drift paragraph that follows, whose last line held one word (RV630's +nit on #630). diff --git a/tests/test_agents_md_codes.py b/tests/test_agents_md_codes.py index 171db24a..915da971 100644 --- a/tests/test_agents_md_codes.py +++ b/tests/test_agents_md_codes.py @@ -128,17 +128,31 @@ #: `-wip` are not names and never need an entry below. CODE_SHAPE = re.compile(r"[a-z]+(-[a-z0-9]+)+") +#: The four commands the openreposhape package places beside its +#: `openRepoShape` (#563), from `pyproject.toml`'s `[project.scripts]`: +#: AGENTS.md names them as commands an assistant runs, and no tool prints +#: one alone. The reason, spelled once, for their four entries below. +PACKAGE_COMMAND = ( + "a command the openreposhape package places (#563, `pyproject.toml`'s " + "`[project.scripts]`), named so an assistant knows it runs the tool of " + "that name; a command, not a code, and no tool prints it alone") + #: A code-shaped name `AGENTS.md` spells that is NOT a code a tool prints, #: each with its reason. Verdict words a tool prints are checked, never #: listed here. The test fails an entry that a tool does print and an entry #: no document names any more, so the list cannot rot into a place where a -#: misspelling hides. It may be EMPTY: the day `AGENTS.md` stops citing the -#: ruling id below, the entry goes and the dict is `{}`. +#: misspelling hides. It may be EMPTY: an entry goes the day `AGENTS.md` +#: stops naming it (the ruling id, or one of the package's commands), and +#: once none is left the dict is `{}`. NOT_A_CODE_A_TOOL_PRINTS = { "prefer-triad-project-shape": ( "an openxFactory ruling id (Brett Heap, 2026-10-06), cited so a " "reader can find the ruling; the tools name it in comments and " "docstrings and print it nowhere"), + "scaffold-project": PACKAGE_COMMAND, + "adopt-project": PACKAGE_COMMAND, + "update-shape": PACKAGE_COMMAND, + "shape-doctor": PACKAGE_COMMAND, } #: Where a failure says to put a name that is no code, so the three messages diff --git a/tests/test_repo_hygiene.py b/tests/test_repo_hygiene.py index 4d3184bc..00fc68b9 100644 --- a/tests/test_repo_hygiene.py +++ b/tests/test_repo_hygiene.py @@ -649,9 +649,22 @@ def test_agents_md_is_short_enough_to_be_read(): question, and `check` passes a default, so an assistant briefed from this file alone would let it through unread. The lines buy that it is a question too, which `execute` takes as written, answered like the - others.""" + others. + + 2026-10-15: 492 -> 494 — the second way in, `pipx install + openreposhape` (#563), decision 6 of #563's design, which Brett Heap + approved that day ("approve all six for #563"). The argument is #38's: + a way in this file does not name is a way in performed from memory. + Both lines are in section 2: the installed command may be pipx's as + well as `install.sh`'s, on native Windows too, and pipx places + `scaffold-project`, `adopt-project`, `update-shape` and `shape-doctor` + beside it, the tools this file's procedures run, by name, so an + assistant on a machine that holds the package and no checkout can + still follow them. The first rule's sentence that the one-line install + is the human's to run now names the pipx install too, and was reworded + to cost no line.""" lines = (REPO / "AGENTS.md").read_text().splitlines() - assert len(lines) <= 492, f"AGENTS.md is {len(lines)} lines; the cap is 492" + assert len(lines) <= 494, f"AGENTS.md is {len(lines)} lines; the cap is 494" def test_claude_md_points_at_agents_md(): @@ -1562,6 +1575,19 @@ def test_the_one_line_install_is_the_same_everywhere(name): f" {ONE_LINE_INSTALL}") +def test_every_handbook_copy_of_the_one_line_install_is_the_same(): + """The handbook shows the one line in three code blocks: the masthead's, + step 3's on the Linux, macOS and WSL2 tabs, and step 3's on the Windows + tab, for Git Bash (#461's line "The handbook's step 3 has no Git Bash + path", settled by #630). The test above finds ONE copy; this one holds + every copy, so a line edited on one tab alone fails here.""" + page = (REPO / "docs" / "handbook.html").read_text(encoding="utf-8") + copies = [line for block in re.findall(r"(.*?)", page, re.S) + for line in block.splitlines() if "download/install.sh" in line] + assert len(copies) >= 3, copies + assert set(copies) == {ONE_LINE_INSTALL}, copies + + #: Where each document describes `openRepoShape --doctor`: the file, the #: line its passage begins with, what ends it, and whether it describes the #: installed form's kept clone. `docs/cli.md` has two: the doctor's purpose in