diff --git a/AGENTS.md b/AGENTS.md index fa734fe1..4b94a0a7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,13 +1,12 @@ # AGENTS.md — scaffolding, adopting, updating, advancing a leg, families -You are an AI assistant a human has asked to "scaffold a new project with -this shape", or "convert this repository to it", or "put these services in a -family". This is the whole procedure; `README.md` says what the shape is. -Sections 1-3 scaffold a NEW project; the ones after them cover an EXISTING -repository (including one with no code yet), a project that declares descent -from a neutral product, a repository nobody is sure about, a project whose -copied shape files have fallen behind the upstream, a leg that has moved on, -and a FAMILY holder. +You are an AI assistant a human has asked to "scaffold a new project with this +shape", or "convert this repository to it", or "put these services in a family". +This is the whole procedure; `README.md` says what the shape is. Sections 1-3 +scaffold a NEW project; the ones after them cover an EXISTING repository +(including one with no code yet), a project that declares descent from a neutral +product, a repository nobody is sure about, a project whose copied shape files +have fallen behind the upstream, a leg that has moved on, and a FAMILY holder. `docs/cli.md` is the flag-level reference: every tool's own `--help`, and every subcommand's, in one file — the ones you run from a checkout of this @@ -115,11 +114,11 @@ the three names, `scaffold-project.py --dry-run`, an explicit human yes, ## Adopting an existing repository -`adopt-project.py` converts a repository that already exists, IN PLACE: it -keeps its name, its identity and its full history and becomes the assembly -root, and the two legs are new repositories extracted with `git filter-repo` -(a hard dependency; the preflight prints the install command). Seven steps, in -order, and step 4 is the one you must not skip. +`adopt-project.py` converts a repository that already exists, IN PLACE: it keeps +its name, its identity and its full history and becomes the assembly root, and +the two legs are new repositories extracted with `git filter-repo` (a hard +dependency: `execute` refuses without it, printing the install command). Seven +steps, in order, and step 4 is the one you must not skip. ```sh ./adopt-project.py plan --source --project @@ -162,11 +161,10 @@ only. Never suggest a direct push to the default branch, and never `--admin`. ### Adopting a repository that has NO code yet (or no spec) -A leg no plan entry assigns a path to is SEEDED from -`templates/-root/`, not extracted — `git filter-repo` over an empty -path list yields an empty history, not an empty repository. `plan` and -`check` both WARN; `check` still passes, because a spec-only repository is a -legitimate thing to adopt. +A leg no plan entry assigns a path to is SEEDED from `templates/-root/`, +not extracted — `git filter-repo` over an empty path list yields an empty +history, not an empty repository. `plan` and `check` both WARN; `check` still +passes, because a spec-only repository is a legitimate thing to adopt. ```sh ./adopt-project.py plan --source / --project \ @@ -178,14 +176,15 @@ legitimate thing to adopt. plan that lost its code paths to a bad edit is indistinguishable from a repository that has no code. Do not pass it because the tool asked for it; pass it because the human confirmed the repository genuinely has none. -2. Everything else is unchanged, step 7 included: the verification table - reads `code: 0 of N source paths (seeded from template)` and still - accounts for every source path by blob sha. +2. Everything else is unchanged, step 7 included: the verification table's + `code` row reads `0 of N source paths (seeded from template)`, and the table + still accounts for every source path by blob sha. ## Scaffolding a project that DECLARES descent -`MedxGlass` descends from `openGlass` only if it PINS it (2026-09-02). Pass -the pin and the scaffold records the descent and still gives it two legs: +`MedxGlass` descends from `openGlass` only if it PINS it, or pins a layer that +does and records the chain (`--referent-chain`; 2026-09-02, chains 2026-09-05). +Pass the pin and the scaffold records the descent and still gives it two legs: ```sh ./scaffold-project.py --org MedxSoft --project MedxGlass \ @@ -207,34 +206,36 @@ the tool, not scaffold. ## Checking a repository's compliance -`openRepoShape --doctor []` — or `./shape-doctor.py --root ` from -a checkout — answers ONE question about ONE repository: is it compliant with -this shape, and what is missing. Run it before you propose a change to a -repository you did not scaffold, and again after any procedure above. It -writes nothing into the repository and fetches nothing, so there is no -confirmation to get and no `--dry-run` to do first. The checks are a REGISTRY with an empty `fix` slot +`openRepoShape --doctor []` — or `./shape-doctor.py --root ` from a +checkout — answers ONE question about ONE repository: is it compliant with this +shape, and what is missing. Run it before you propose a change to a repository +you did not scaffold, and again after any procedure above. It writes nothing +into the repository and fetches nothing, so there is no confirmation to get and +no `--dry-run` to do first. 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. The one exception to "writes nothing" is `--placement-plan `, -which writes that file and nothing else — see 4. +today. The one exception to "writes nothing" is `--placement-plan `, which +writes that file and nothing else — see 4. Installed, `openRepoShape --doctor` +runs from a temporary clone of the standard, deleted on exit, and takes a path +and nothing else: to ACT on a row (`--json`, `--placement-plan`, its next +command) run `./shape-doctor.py --root ` from a checkout. 1. **Read the verdict, then read the rows.** `COMPLIANT`, `COMPLIANT, SHAPE - BEHIND`, `DRIFTED`, `MISPLACED`, `INVALID`, `NOT A SHAPE ROOT` — exits 0, - 1, 1, 1, 1, 2 — and `CANNOT ANSWER`, exit 3, which is THIS CHECKOUT - failing to ask, never - a statement about their repository. The verdict names the MOST SPECIFIC - finding and the TABLE names every one, so a report summarised from the - verdict line alone is a report you did not read. Relay the rows. + BEHIND`, `DRIFTED`, `MISPLACED`, `INVALID`, `NOT A SHAPE ROOT` — exits 0, 1, + 1, 1, 1, 2 — and `CANNOT ANSWER`, exit 3, which is THIS CHECKOUT failing to + ask, never a statement about their repository. The verdict names the MOST + SPECIFIC finding and the TABLE names every one, so a report summarised from + the verdict line alone is a report you did not read. Relay the rows. 2. **`note` is not a finding and you do not "fix" it.** A row says `note` when it found a difference nothing asserts — a leg file no pin names, a rendered `AGENTS.md`. It never moves the verdict. Report it; propose a change only if the human asks. 3. **A FINDING names its fix, and you run THAT.** Every one carries the exact - next command — `update-shape.py apply …`, `make bootstrap`, `bump-leg.py - …`, the project's own validator. Run the one the row names. Do not - improvise a repair, and never edit a pinned file, a digest or a manifest - to make a row go green: that is the hand-edited pin this standard spends a - section refusing. Each of those commands has its own section above and its - own refusals, and they all still apply. + next command, where one can be named — `update-shape.py check …` and its + `apply`, `git submodule update …` (in a holder, its `scripts/bootstrap.py`), + the project's own validator. Run the one the row names. Do not improvise a + repair, and never edit a pinned file, a digest or a manifest to make a row go + green: that is the hand-edited pin this standard spends a section refusing. + Each of those commands keeps its own refusals, and they all still apply. 4. **`placement` says what is in the wrong leg, and you never move a file to answer it.** It runs the adoption's own path policy over each mounted leg and names the paths it would have put elsewhere — code in the spec @@ -258,13 +259,12 @@ which writes that file and nothing else — see 4. which of them is wanted is a fact about their repository. Report the verdict and both commands; adopt or scaffold nothing on your own initiative. There is no `--force`, deliberately. -6. **The `machine` row is not about the repository.** It is `n/a` by status - and never changes the verdict: it says whether THIS workstation can run - the fixes the other rows named. Relay a missing prerequisite; - `openRepoShape --preflight` is what offers to install one, and the human - types that yes. -7. `--json` is the same report as one object, each row carrying its `id`, for - when you have to act on a row rather than quote it. +6. **The `machine` row is not about the repository.** It is `n/a` by status and + never changes the verdict: it says whether THIS workstation can run the fixes + the other rows named. Relay a missing prerequisite; `openRepoShape + --preflight` offers only the `gh` set-up, and the human types that yes. +7. `./shape-doctor.py --json` is the same report as one object, each row + carrying its `id`, for when you have to act on a row rather than quote it. ## Updating a project's shape @@ -282,10 +282,11 @@ git -C push -u origin shape/update- # then open a pull request ``` 1. **`check` first, always.** It writes nothing and prints one verdict per - PINNED file: `unchanged`, `upstream-changed`, `locally-modified` (the - project edited its own copy) or `both`. A file with no row in - `contracts/shape-pin.yaml` is not a shape copy and is none of your business - — an adopted project merged some of them away on purpose. + PINNED file: `unchanged`, `upstream-changed`, `locally-modified` (the project + edited its own copy), `both`, `already-at-target`, `upstream-removed`, + `unmapped` or `copy-missing`; `apply` refuses the last three. A file with no + row in `contracts/shape-pin.yaml` is not a shape copy and is none of your + business — an adopted project merged some of them away on purpose. 2. **Show the human the verdicts and let them say yes.** Re-pinning records which openRepoShape this project is a copy of. Do not pass `--yes` on your own initiative. @@ -352,7 +353,7 @@ git -C push -u origin # then open a pull request world cannot fetch is a root it cannot bootstrap. A red validator has already been rolled back; the finding above it is the thing to fix. 5. The leg is left DETACHED at the new commit. `make bootstrap` in the root - re-places it on its tracking branch AT the new pin; say so in your relay. + puts it on its tracking branch AT the new pin, or says why not; relay which. 6. **`--trailer ": "` puts a line on the commit this writes.** Repeatable, kept in the order given, as `update-shape.py apply` and `family.py bump` take it. `--dry-run` prints them after the `old -> new` @@ -431,11 +432,10 @@ make park # then leave make bootstrap && make resume # on the other machine, after a clone ``` -1. **The overlay is a PREREQUISITE and the refusal names it.** Both targets - exit 2 with `install it with: setup-openspeckit` when - `.specify/extensions/git/` is not there. Relay that line; never hand-roll - the WIP commit, the `git worktree add` or the soft reset yourself because - a target refused. +1. **The overlay is a PREREQUISITE and the refusal names it.** Both targets exit + 2 with `install it with: setup-openspeckit` when `.specify/extensions/git/` + is not there. Relay that line; never hand-roll the WIP commit, the `git + worktree add` or the soft reset yourself because a target refused. 2. **`resume` refusing on DIVERGENCE is not something to work around.** The remote tip is not the commit that was parked, which means somebody's work is on that branch; the refusal names both commits and the commands to diff --git a/README.md b/README.md index ec812f8e..1417b9ec 100644 --- a/README.md +++ b/README.md @@ -93,20 +93,19 @@ and is reported beside the advisory, and nothing fails on its account. Command Line Tools supply `git`, `make` and a `python3` — install them with `xcode-select --install`, or type `git` once on a fresh Mac and the installer offers itself; step 4 opens with a preflight that prints the `python3` it found -and refuses anything below 3.9. If it refuses the one the Tools supplied — -older macOS shipped 3.8 — install a newer one from - or with `brew install python`, and the -preflight will find it first on `PATH`. `gh` comes from Homebrew, which is its -own install if `brew` is absent (): on Apple Silicon Homebrew -lives in `/opt/homebrew`, which is on no `PATH` by default, and its installer -finishes by printing the line that puts it there — run that line, or the next -shell cannot find `brew`. Then `brew install gh`, and `brew install -git-filter-repo` only if you are ADOPTING an existing repository rather than -scaffolding a new one. The stock `/bin/bash` is 3.2 and is enough: `setup.sh` -and the `openRepoShape` command use no syntax newer than that, and CI runs the -whole suite on `macos-latest`, parsing both there with `/bin/bash` itself. That -the login shell is zsh changes nothing — both entry points are run under `bash` -explicitly. +and refuses anything below 3.9. If it refuses the one the Tools supplied — older +macOS shipped 3.8 — install a newer one from +or with `brew install python`, and the preflight will find it first on `PATH`. +`gh` comes from Homebrew, which is its own install if `brew` is absent +(): on Apple Silicon Homebrew lives in `/opt/homebrew`, which +is on no `PATH` by default, and its installer finishes by printing the line that +puts it there — run that line, or the next shell cannot find `brew`. Then `brew +install gh`, and `brew install git-filter-repo` only if you are ADOPTING an +existing repository rather than scaffolding a new one. The stock `/bin/bash` is +3.2 and is enough: `setup.sh` and the `openRepoShape` command use no syntax +newer than that, and CI runs the whole suite on `macos-latest`, parsing both +there with `/bin/bash` itself. That the login shell is zsh changes nothing — +both entry points are run under `bash` explicitly. @@ -172,15 +171,17 @@ run the four steps below INSIDE the WSL shell. > [!NOTE] > **The tool offers the rest.** The four steps below are the reference — what > each platform needs, and how to install it by hand. You do not have to: step 4 -> opens with a preflight that names what is missing, prints the command for THIS -> machine and asks once — `Type yes to continue:`, the same words it asks -> everything else with — then runs it and carries on. Nothing is installed -> without that typed `yes`; `--yes` does not answer it, and with no terminal -> attached there is no offer at all. Homebrew is the one install never run for -> you: on a Mac without `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. +> opens with a preflight that names what is missing and, for `gh`, its login and +> its credential helper, prints the command for THIS machine and asks once — +> `Type yes to continue:`, the same words it asks everything else with — then +> runs it and carries on. Nothing is installed without that typed `yes`; `--yes` +> does not answer it, and with no terminal attached there is no offer at all. +> Homebrew is the one install never run for you: on a Mac without `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). **1. Install the GitHub CLI**, `gh`. The official instructions are at (and ): @@ -211,13 +212,12 @@ sudo dnf config-manager addrepo --from-repofile=https://cli.github.com/packages/ sudo dnf install gh ``` -`git` and Python 3.9 or newer are needed too (see Requirements below). -Ubuntu under WSL2 ships both, so there `gh` is the only install; step 4 -opens with a preflight that names anything missing and where to get it. -`git` itself comes from `xcode-select --install` on macOS, `sudo apt-get -install -y git` on Debian and Ubuntu, `sudo dnf install -y git` on Fedora and -`winget install --id Git.Git -e` on Windows — the same commands that preflight -offers to run for you. +`git` and Python 3.9 or newer are needed too (see Requirements below). Ubuntu +under WSL2 ships both, so there `gh` is the only install; step 4 opens with a +preflight that names anything missing and where to get it. `git` itself comes +from `xcode-select --install` on macOS, `sudo apt-get install -y git` on Debian +and Ubuntu, `sudo dnf install -y git` on Fedora and `winget install --id Git.Git +-e` on Windows — the same commands `setup-project.py --preflight` offers to run. **2. Log in.** `gh auth login`, as the account that will own the act — a member of the organisation who can create repositories there. Accept the @@ -266,12 +266,12 @@ for, refusals and all. openRepoShape --org ``` -`` is one positional CamelCase name (e.g. `Atlas`); `--org` is -required — from the flag, `$OPENREPOSHAPE_ORG`, or the command's own prompt -— and add `--visibility private` if you want that. It shows the plan and -asks once, `Type yes to continue:`; type `yes`. Afterwards `./` is -the cloned root. The worked example below walks through what this produces, -prompt by prompt. +`` is one positional CamelCase name (e.g. `Atlas`); `--org` is required +— from the flag, `$OPENREPOSHAPE_ORG`, or the command's own prompt — and +`--visibility` defaults to `private`: add `--visibility public` (or `internal`) +if you want that. It shows the plan and asks once, `Type yes to continue:`; type +`yes`. Afterwards `./` is the cloned root. The worked example below +walks through what this produces, prompt by prompt. **Joining a family.** `--family ` lands the clone at `/` instead of `` — the plain folder that holds a @@ -300,17 +300,17 @@ run to read before an organisation is involved. It still reaches GitHub before the run starts — to fetch `setup.sh`, then for the self-bootstrap clone of this standard into a temporary directory; run the same flag on `./setup.sh` from a checkout instead and nothing touches the network at all. On Windows the same -flag goes on the Python entry point — -`py setup-project.py Atlas --org --local-remote-dir .\rehearsal` — -which, run from a download, still clones the standard into a temporary -directory first, and from a checkout clones nothing. What it leaves behind is -`./Atlas` and three bare repositories in `./rehearsal`, both of which you can -delete. `python3 setup-project.py --help` and, from a checkout, `setup.sh --help` -list every flag, this one included. From a download, `setup.sh --help` clones -nothing and lists none: it names `setup-project.py --help`, which answers -offline from that one file too, and [`docs/cli.md`](docs/cli.md), every -tool's help and every subcommand's in one file, generated from the tools -themselves. +flag goes on the Python entry point — `py setup-project.py Atlas --org + --local-remote-dir .\rehearsal` — which, run from a download, still +clones the standard into a temporary directory first, and from a checkout clones +nothing. What it leaves behind is `./Atlas`, three bare repositories in +`./rehearsal` and the scaffold's three working trees, in the temporary directory +its `Working trees are at` line names, all of which you can delete. `python3 +setup-project.py --help` and, from a checkout, `setup.sh --help` list every +flag, this one included. From a download, `setup.sh --help` clones nothing and +lists none: it names `setup-project.py --help`, which answers offline from that +one file too, and [`docs/cli.md`](docs/cli.md), every tool's help and every +subcommand's in one file, generated from the tools themselves. Without installing anything — no fork, no manual clone — one command: @@ -369,15 +369,14 @@ create repositories in. `make` is not used by the entry point — it runs Or run `openRepoShape --preflight`, which checks this machine, offers what is missing and creates nothing. -**2. Login.** `gh auth login`, as the account that will own the act: a member -of `Northwind` with permission to create repositories there, or an owner — +**2. Login.** `gh auth login`, as the account that will own the act: a member of +`Northwind` with permission to create repositories there, or an owner — `scaffold-project.py` creates the three with `gh repo create` under whatever account `gh auth status` shows. Accept the offer to configure git's credential -helper, or run `gh auth setup-git`: the first commits go up with a plain -`git push` over HTTPS, and without it the three repositories are created and -the push fails. `--elected-by` authenticates nothing — it is only the name -`project.yaml` records as whose act the election was, and it defaults to the -`gh` login. +helper, or run `gh auth setup-git`: the first commits go up with a plain `git +push` over HTTPS, and without it the three repositories are created and the push +fails. `--elected-by` authenticates nothing — it is only the name `project.yaml` +records as whose act the election was, and it defaults to the `gh` login. #### Install the command @@ -434,7 +433,7 @@ human says it rather than lets it default. bootstrap will run, and that `gh` is installed and authenticated — and, for `gh`, an offer to install it, log it in or set up git's credential helper, whichever is missing, which Dana answers `yes` to. (2) The three names checked against the naming -policy — `Atlas`, `Atlas-spec`, `Atlas-code`; a hyphenated or lowercase name is +policy — `Atlas`, `Atlas-spec`, `Atlas-code`; a hyphenated project name is refused here with the rule printed. (3) The scaffold plan, a dry run of everything about to be created. (4) `This will create THREE repositories in 'Northwind'.` and `Type yes to continue:` — Dana types `yes`. If an AI agent is driving, it runs the @@ -567,13 +566,14 @@ flowchart LR The six naming families live in `contracts/repository-naming.yaml`: neutral products `open`, domain descendants ``, installs -`-Install`, project legs as above, family holders (below), and one form -that is no repository of a project at all — a person's own workspace -repository `-wip` (#81, and -[below](#carrying-in-flight-work-to-another-workstation)). The leg suffixes -are lowercase and hyphenated precisely so they sit in a different visual class -from every other family, all of which are CamelCase words. Every repository of -a project also carries the GitHub topic `xf-project-`. +`-Install`, project legs as above, family holders (below), and one form that +is no repository of a project at all — a person's own workspace repository +`-wip` (#81, and +[below](#carrying-in-flight-work-to-another-workstation)). The leg suffixes are +lowercase and hyphenated precisely so they sit in a different visual class from +the neutral-product, domain-descendant and family forms, all of which are +CamelCase words. Every repository of a project also carries the GitHub topic +`xf-project-`. ### Reading private legs in CI: a GitHub App first, `SHAPE_LEGS_TOKEN` as fallback @@ -608,26 +608,26 @@ only to PUBLIC repositories, so on a private one `secrets.SHAPE_LEGS_APP_ID` is silently empty and `validate` goes GREEN with the pin check skipped. Set both per repository, or upgrade to Team; the creating tools check and say so. -The root repository is always readable by the workflow's own default token, -so `actions/checkout` never carries a `token:` override. Putting a legs -credential there was itself the next defect, on the first real use of the PAT -(MedxSoft/MedxEHR and MedxSoft/MedxGlass, runs 33821509948 and 33821512605): -a token correctly scoped to `contents:read` on the legs alone cannot read -the root, so `actions/checkout` itself failed with a 403 before any check -ran. Whichever credential resolves is read only inside the guarded "fetch the -legs (submodules)" step, scoped to that step's `env:`, and used through a -`git -c url.<...>.insteadOf=<...>` rewrite covering both HTTPS and SSH leg -URLs — never persisted onto the root checkout. - -`validate` tries the App first: a `mint a leg-reader token from the GitHub -App` step (`actions/create-github-app-token`, pinned by commit) runs when -both App secrets are present, scoped by `repositories:` to the legs -`.gitmodules` names that this repository's own owner also owns — an -installation token is per-owner, so a leg under a different owner is excluded -with a warning and can only be reached via `SHAPE_LEGS_TOKEN` — and falls back -to the PAT when the App is not configured. A configured App that fails to -mint is a misconfiguration, not a degrade, and fails the job outright, naming -both secrets and the required installation. +The root repository is always readable by the workflow's own default token, so +`actions/checkout` never carries a `token:` override. Putting a legs credential +there was itself the next defect, on the first real use of the PAT +(MedxSoft/MedxEHR and MedxSoft/MedxGlass, runs 33821509948 and 33821512605): a +token correctly scoped to `contents:read` on the legs alone cannot read the +root, so `actions/checkout` itself failed with a 403 before any check ran. +Whichever credential resolves is read only inside the guarded "fetch the legs +(submodules)" step, scoped to that step's `env:`, and used through a `git -c +url.<...>.insteadOf=<...>` rewrite covering both HTTPS and scp-form SSH +(`git@github.com:`) leg URLs — never persisted onto the root checkout. + +`validate` tries the App first: a `mint a leg-reader token from the GitHub App` +step (`actions/create-github-app-token`, pinned by commit) runs when both App +secrets are present, scoped by `repositories:` to the legs `.gitmodules` names +that this repository's own owner also owns — an installation token is per-owner, +so a leg under a different owner is excluded with a warning, and +`SHAPE_LEGS_TOKEN` is read only when the App is not configured, so that leg is +reachable only with the App secrets removed. A configured App that fails to mint +is a misconfiguration, not a degrade, and fails the job outright, naming both +secrets and the required installation. Without either credential the workflow still degrades instead of failing: it checks out the root without submodules, attempts `git submodule update --init @@ -678,22 +678,22 @@ is pinned by its descendants at the LAYER: `codexDox` pins `openXdox`, and family deliberately does not hold. The chain is RECORDED, never inferred — the descendant's own `naming.referent_chain: [openXdox, openDox]` names the pin it holds first and the referent its name claims last, and -`validate-repository-naming.py --referent-chain openXdox,openDox` asks the same -question for one name. A DIRECT PIN STAYS SUFFICIENT: this is an addition, and -`MedxChart` answers exactly as it did on 2026-09-02. - -Each link past the first is a declaration in ANOTHER tree — `openXdox` -declaring `neutral_product_pins: [openDox]` — and it is read where that tree -happens to be on the disk: `--link-source [PRODUCT=]`, -`SHAPE_PIN_SOURCE_`, or a checkout beside the root, the same lookup +`validate-repository-naming.py --pins openXdox --referent-chain +openXdox,openDox` asks the same of one name. A DIRECT PIN STAYS SUFFICIENT: this +is an addition, and `MedxChart` answers exactly as it did on 2026-09-02. + +Each link past the first is a declaration in ANOTHER tree — `openXdox` declaring +`neutral_product_pins: [openDox]` — and it is read where that tree happens to be +on the disk: `--link-source PRODUCT=`, `SHAPE_PIN_SOURCE_`, a +checkout beside the root, then a bare `--link-source `: the same lookup order `validate-pins.py` uses below for the same checkout. A link no tree -answers for is reported `declared-unverified` — a warning, never a finding, -and the name still classifies — because the recorded declaration is the -offline fact and the tree is the stronger check, available or not. A link that -IS readable and declares something else BREAKS the chain: the residual -assembly-root reading wins, the overlap is recorded, and the message names the -link that broke. `LedgerxWallet` -> `openXwallet` -> `openWallet` is the same -shape, and `openWallet` need not exist anywhere for that to be the answer. +answers for is reported `declared-unverified` — a warning, never a finding, and +the name still classifies — because the recorded declaration is the offline fact +and the tree is the stronger check, available or not. A link that IS readable +and declares something else BREAKS the chain: the residual assembly-root reading +wins, the overlap is recorded, and the message names the link that broke. +`LedgerxWallet` -> `openXwallet` -> `openWallet` is the same shape, and +`openWallet` need not exist anywhere for that to be the answer. **`validate-pins.py` rechecks the referent too, not only the declaration.** Each `neutral_product_pins:` entry's `contracts/-pin.yaml` @@ -717,14 +717,13 @@ confers nothing, so `openDox` mounting `openDox-spec` and `openDox-code` is a fact about LAYOUT and not a claim about neutrality: exactly the reasoning that already lets a declared descendant be a root. `open` in front still WINS the classification — the manifest records `form: neutral-product, role: assembly` -with `also_matches: [project-leg/assembly]`, so the overlap the scaffold used -to compute and then throw away is now recorded like every other one, and +with `also_matches: [project-leg/assembly]`, so the overlap the scaffold used to +compute and then throw away is now recorded like every other one, and `validate-repository-naming.py --explain --role assembly openDox` names the admission. The legs are untouched: `openDox-spec` carries the lowercase suffix -and is an ordinary project leg, a descendant of nothing and a neutral product -of nothing. `-Install` is admitted into nothing at all — it carries a -hyphen, so it satisfies no leg form to be admitted into, and is still refused -as any leg. +and is an ordinary project leg, a descendant of nothing and a neutral product of +nothing. `-Install` is admitted into nothing at all — it carries a hyphen, so +it satisfies no leg form to be admitted into, and is still refused as any leg. ## Adopting an existing repository @@ -732,8 +731,8 @@ 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, 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 it is ONE split commit, arriving on a +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 by pull request — the organisations this serves are pull-request only, and a tool needing a bypass cannot be used where it is needed. @@ -759,15 +758,16 @@ drifts. | `.specify/`, `AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, … , `.cursor/`, `.roo/` | **root** | project-level workflow tooling spanning both legs | | `examples/golden-run/` | **asked** | acceptance evidence the spec cites, or a test fixture? | -The contracts decision costs something, and the plan says so instead of -letting it be discovered later: `scripts/validate.py` reads `contracts/*.yaml` -from beside it and must now read ACROSS the root. `plan` finds every such file -with `git grep` and writes it into `follow_ups:`; the root Makefile it -materializes exports `CONTRACTS_DIR ?= $(CURDIR)/spec/contracts`. The tool -does not make the edit — naming it is the honest half, making it silently -would be the dangerous one. And `.specify/` and the ten identical assistant -stubs stay at the root because an assistant reading `CLAUDE.md` from inside -the code leg would be told about specifications it cannot see. +The contracts decision costs something, and the plan says so instead of letting +it be discovered later: `scripts/validate.py` reads `contracts/*.yaml` from +beside it and must now read ACROSS the root. `plan` finds every such file with +`git grep` and lists up to eight per directory in `follow_ups:`, counting the +rest; the root Makefile it materializes exports `CONTRACTS_DIR ?= +$(CURDIR)/spec/contracts`. The tool does not make the edit — naming it is the +honest half, making it silently would be the dangerous one. And `.specify/` and +the ten identical assistant stubs stay at the root because an assistant reading +`CLAUDE.md` from inside the code leg would be told about specifications it +cannot see. ### Ambiguous is an answer, and nothing is overwritten @@ -807,23 +807,22 @@ scaffold writes — and the split mounts and pins it like an extracted one. The plan records `seeding.code.seeded_from_template: true` and the template it would use; `check` WARNS and still passes, because a spec-only repository is a -legitimate thing to adopt; the verification table reads `code: 0 of N source -paths (seeded from template)` and still accounts for every source path by blob -sha. `execute` refuses without `--allow-empty-leg code`, in the plan or on its -own command line: a plan that lost its code paths to a bad edit looks -identical from here, so the consent is a human's word rather than an -inference. Where the recorded block and the entries disagree — somebody -answered an ambiguous path INTO the empty leg, which is the plan working — the -entries win and it is a note, not a refusal. +legitimate thing to adopt; the verification table's `code` row reads `0 of N +source paths (seeded from template)`, and the table still accounts for every +source path by blob sha. `execute` refuses without `--allow-empty-leg code`, in +the plan or on its own command line: a plan that lost its code paths to a bad +edit looks identical from here, so the consent is a human's word rather than an +inference. Where the recorded block and the entries disagree — somebody answered +an ambiguous path INTO the empty leg, which is the plan working — the entries +win and it is a note, not a refusal. ## Families: a holder for projects that ship separately Ruled by **Brett Heap on 2026-09-04**, about InkRouter: *"InkRouter is a set of -microservices and they deploy separately as api's. so maybe they each need -their own assembly repo. So probably InkRouter is only something that can -download all the others easily? like a holder folder and some utilities for -the family of services. Then IRRS would be assembly and IRRS-spec and -IRRS-code."* +microservices and they deploy separately as api's. so maybe they each need their +own assembly repo. So probably InkRouter is only something that can download all +the others easily? like a holder folder and some utilities for the family of +services. Then IRRS would be assembly and IRRS-spec and IRRS-code."* A **family** is a repository that pins other projects' **assembly roots** as submodules under `members/` and carries the utilities to fetch and bootstrap @@ -937,11 +936,10 @@ copy of without changing any existing name's answer. ## The double pin, and the lockstep invariant -Each leg is pinned TWICE, in the same commit: by the **gitlink** git records -for the submodule, and by **`contracts/-pin.yaml`** carrying -`revision_kind: commit`, the 40-hex commit, and a sha256 digest. A tag is never -the referent — a tag can be moved and a commit cannot — and a pin never -expresses a range. +Each leg is pinned TWICE, in the same commit: by the **gitlink** git records for +the submodule, and by **`contracts/-pin.yaml`** carrying `revision_kind: +commit`, the 40-hex commit, and a sha256 digest. A tag is never the referent — a +tag can be moved and a commit cannot — and a pin never expresses a range. Three things therefore move together, in ONE commit: @@ -955,16 +953,16 @@ for months, seven consecutive pin-syncs from 2026-08-25 moved the gitlink alone, and `validate` was red on every pull request for a day — unnoticed because the check runs on pull requests only, so `main` never reports it. -**What is digested.** `sorted-ls-tree-r-v1`: the sha256 of the sorted -`git ls-tree -r -z ` listing — each record ` SP SP -TAB ` with the path unquoted, sorted bytewise, joined with LF. Not -`git archive`: its tar bytes carry a pax header naming the commit, uname/gname, -an mtime, and `export-ignore` handling from `.gitattributes`, so the same -commit can digest differently across git versions and platforms — which is -disqualifying for a number two machines must agree on. The `ls-tree` listing is -a complete content address (mode catches permission changes, oid catches -content, `160000 commit ` catches a nested pin moving) and reproduces from -nothing but git's plumbing. +**What is digested.** `sorted-ls-tree-r-v1`: the sha256 of the sorted `git +ls-tree -r -z ` listing — each record ` SP SP TAB +` with the path unquoted, sorted bytewise, each followed by LF, the last +one included. Not `git archive`: its tar bytes carry a pax header naming the +commit, uname/gname, an mtime, and `export-ignore` handling from +`.gitattributes`, so the same commit can digest differently across git versions +and platforms — which is disqualifying for a number two machines must agree on. +The `ls-tree` listing is a complete content address (mode catches permission +changes, oid catches content, `160000 commit ` catches a nested pin moving) +and reproduces from nothing but git's plumbing. ### Advancing a leg: `scripts/bump-leg.py` @@ -972,7 +970,7 @@ Moving those three facts by hand is how they come apart, so one command moves them together, run from a checkout of this standard: ```sh -python3 scripts/bump-leg.py --root --leg spec --to <40 hex> # --dry-run prints the move and writes nothing +python3 scripts/bump-leg.py --root --leg spec --to <40 hex> # --dry-run prints the move; it fetches into the leg and writes nothing else ``` It fetches the commit into the leg, checks the leg out AT it, recomputes the @@ -988,11 +986,12 @@ remote contains (including one committed in the mount and never pushed — a pin the rest of the world cannot fetch is a root it cannot bootstrap), a dirty root, an untracked or ignored workflow file it would rewrite, a root standing on its own tracking branch (the next step is a pull request, never a push to the -default branch: `git switch -c ` first; `--dry-run` writes nothing, so -it runs there and prints that command), and a validator that goes red — rolling -the pin, the workflow files, the leg's checkout and the index back first, so a -refusal leaves the tree as it was found. The leg is left DETACHED at the new -commit; `make bootstrap` re-places it on its tracking branch AT the new pin. +default branch: `git switch -c ` first; `--dry-run` fetches into the leg +and writes nothing else, so it runs there and prints that command), and a +validator that goes red — rolling the pin, the workflow files, the leg's +checkout and the index back first, so a refusal leaves the tree as it was found. +The leg is left DETACHED at the new commit; `make bootstrap` re-places it on its +tracking branch AT the new pin, or says why not. ## Bootstrap is COPIED into the project, not fetched @@ -1041,8 +1040,10 @@ HAND: re-copy, re-pin, re-validate. `update-shape.py` is that, as one command: `check` prints one verdict per file the pin names, and the verdict is the product: **`unchanged`**, **`upstream-changed`** (the bytes differ between the pinned commit and the target), **`locally-modified`** (the project's bytes -differ from the pinned digest — drift it introduced) or **`both`**. It exits 1 -when there is anything to do, so it can be a scheduled job. +differ from the pinned digest — drift it introduced) or **`both`**; also +`already-at-target` (the root has the target's bytes) and `upstream-removed`, +`unmapped` and `copy-missing`; `apply` refuses the last three. It exits 1 when +there is anything to do, so it can be a scheduled job. `apply` copies the `upstream-changed` files, rewrites `contracts/shape-pin.yaml` (`commit`, `digests.tree_sha256` recomputed over the upstream tree under the @@ -1054,13 +1055,13 @@ either goes red. **What it refuses, and why the refusals are the feature.** A `locally-modified` file keeps its bytes and is named in a refusal unless the human passes `--accept-local ` — recomputing that row silently would turn a drift -finding into a digest that agrees with the project's own edited bytes, which -is the standard quietly recording someone's edit as if it were upstream's. A -file changed on **both** sides is refused outright: two -edits to one file is a merge, and a merge is a human's judgement. So is a copy -that was never verbatim — `adopt-project.py` appends a `CONTRACTS_DIR` block to -an adopted Makefile, and copying the upstream bytes over that would delete it -without saying so. +finding into a digest that agrees with the project's own edited bytes, which is +the standard quietly recording someone's edit as if it were upstream's. A file +changed on **both** sides is refused until a human merges it, commits the merge +and re-runs with `--accept-local `: two edits to one file is a merge, and +a merge is a human's judgement. So is a copy that was never verbatim — +`adopt-project.py` appends a `CONTRACTS_DIR` block to an adopted Makefile, and +copying the upstream bytes over that would delete it without saying so. **Only the pin's own rows are RE-SYNCED.** The file list is read from `contracts/shape-pin.yaml` and never re-derived from this repository's copy @@ -1135,28 +1136,29 @@ and a repository the advisory is not silent about gets the **Triad** row, status `advisory`: said once and stepped over by the verdict (a leg gets its root). Every row is `ok`, `note`, `FINDING` or `n/a` — or `advisory`, the Triad row -alone — and every one that is not `ok` names the exact command that fixes it, -except the Triad row, which names none in any status. **Only a `FINDING` -moves the verdict**, and `FINDING` means exactly *something else asserts this* — a -validator, a pin row, a manifest, a gitlink. A difference nothing asserts is -a `note`: printed, in `--json`, and stepped over by the verdict. That line -was drawn after this command answered `INVALID` about a live estate whose -every real gate was green, because a leg was missing a `.gitignore` that -entered the standard *after* that project was scaffolded. A doctor that -invents a rule to fail you by is not believed the next time. Then one -verdict, and its exit code: +alone — and a row that is not `ok` names the exact command that fixes it +whenever there is one to run, except the Triad row, which names none in any +status. **Only a `FINDING` moves the verdict**, and `FINDING` means exactly +*something else asserts this* — a validator, a pin row, a manifest, a gitlink. A +difference nothing asserts is a `note`: printed, in `--json`, and stepped over +by the verdict. That line was drawn after this command answered `INVALID` about +a live estate whose every real gate was green, because a leg was missing a +`.gitignore` that entered the standard *after* that project was scaffolded. A +doctor that invents a rule to fail you by is not believed the next time. Then +one verdict, and its exit code: | verdict | exit | when | |---|---|---| -| `COMPLIANT` | 0 | every row ok, and the pin names this standard's commit | -| `COMPLIANT, SHAPE BEHIND (n upstream-changed, m upstream-added)` | 1 | valid, and the standard has moved on | -| `DRIFTED (…)` | 1 | a copy was edited here, or a leg is off its pin | +| `COMPLIANT` | 0 | no row is a `FINDING`, and the pin names this standard's commit | +| `COMPLIANT, SHAPE BEHIND (n upstream-changed, m upstream-added)`, or `(pin -> , no copied file differs)` | 1 | valid, and the standard has moved on | +| `DRIFTED (…)` | 1 | a copy was edited, deleted or no longer maps onto the standard, or a leg (a member, in a holder) is off its pin | | `MISPLACED (n paths)` | 1 | a tracked path sits in a leg the path policy puts elsewhere, or a submodule in a leg other than the one its `submodules:` record names | | `INVALID ()` | 1 | a validator went red, or (`merge`) the index holds an unmerged path | | `NOT A SHAPE ROOT` | 2 | neither manifest is there — adopt, or scaffold | -Exit 3 is usage or environment — nothing about it is a statement about the -tree you pointed at: no such path, a `shape-doctor.py` not sitting in a +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 @@ -1173,19 +1175,20 @@ what turns the other rows red, and no fix commits until it is finished. adoption's own policy — `contracts/path-classification.yaml`, walked with `adopt-project.py`'s own `walk()`, so there is no second copy of that classification to drift — over every tracked path of every mounted leg, and -names the paths the policy would have put somewhere else, each with the rule -id that judged it. What belongs to **every** repository is never judged: the -four files `templates/spec-root/` and `templates/code-root/` themselves ship -(`README.md`, `AGENTS.md`, `CLAUDE.md`, `.gitignore`), read from those -templates at run time so the scaffold's own output can never be a finding, -plus `LICENSE`, `.gitattributes`, `CODEOWNERS`, `.github/**` and the rest of -the forge furniture — `--json` prints the whole list under `everywhere`, -because what a report declined to look at belongs in the report. A path the -policy will not call carries `review_required` and is a **`note`**: it is a -question for a human, and an unanswered question is never a finding. One -`git ls-files` per leg, one process for the classifying, and a directory whose -files agree is ONE path however many are under it — so a leg of several -thousand files answers in seconds and reads in a line. +names the paths the policy would have put somewhere else, each with the rule id +that judged it, and a submodule in a leg other than the one its `submodules:` +record names. What belongs to **every** repository is never judged: the four +files `templates/spec-root/` and `templates/code-root/` themselves ship +(`README.md`, `AGENTS.md`, `CLAUDE.md`, `.gitignore`), read from those templates +at run time so the scaffold's own output can never be a finding, plus `LICENSE`, +`.gitattributes`, `CODEOWNERS`, `.github/**` and the rest of the forge furniture +— `--json` prints the whole list under `everywhere`, because what a report +declined to look at belongs in the report. A path the policy will not call +carries `review_required` and is a **`note`**: it is a question for a human, and +an unanswered question is never a finding. One `git ls-files` per leg, one +process for the classifying, and a directory whose files agree is ONE path +however many are under it — so a leg of several thousand files answers in +seconds and reads in a line. **And the doctor still moves nothing.** A path changing legs is a pull request on the leg it leaves, a pull request on the leg it joins, and one pin bump in @@ -1200,14 +1203,13 @@ Its entries are `adopt-project.py plan`'s entries exactly — `path`, `leg`, file is in today) and an empty `resolution:`, so a person who has resolved an adoption plan has already resolved this one. It carries `kind: placement-plan` and **not** `adoption-plan`, deliberately: an adoption plan is the input to -`adopt-project.py execute`, which creates two repositories and rewrites -history, and a file that called itself one would be that command aimed at a -project that is already split. `adopt-project.py` refuses this kind by name, -and the refusal says which tool consumes one. A later `--fix --plan ` -will carry a **resolved** plan out by paired leg pull requests and a pin bump, -printing what it will do and asking first; until then the moves are yours. -`--placement-plan` is the only thing this command writes, and it writes only -the file you name. +`adopt-project.py execute`, which creates two repositories and rewrites history, +and a file that called itself one would be that command aimed at a project that +is already split. `adopt-project.py` refuses this kind by name, and the refusal +says which tool consumes one. A later `--fix --plan ` will carry a +**resolved** plan out by paired leg pull requests and a pin bump, printing what +it will do and asking first; until then the moves are yours. `--placement-plan` +is the only thing this command writes, and it writes only the file you name. **`machine` is the one row that is not about the repository**, so it is `n/a` by status and can never move the verdict: a workstation with no `gh` is not @@ -1264,23 +1266,23 @@ That flag is the only thing in this standard that writes `${AGENT_PROTOCOL_ROOT:-$HOME/.agents}/workspace.yaml` (default: `~/.agents`), and it writes it only because you named the repository. Afterwards `resume ` needs no flag. -**How the estate is found.** `` is a folder under your projects -directory: `~/projects/`, then `~/Projects/` — people spell it -both ways — or wherever `$PROJECTS_DIR` points. A FAMILY folder -(`//family.yaml`) wins over a standalone root -(`/project.yaml`) of the same name, because the holder is what drives -the members. With no `` the estate around the current directory is used: -the nearest holder beside you, or project root above you. With no estate -around it either, `park` PARKS EVERY ESTATE it finds under your projects -directory instead of refusing (Brett Heap's RULING of 2026-09-10, #91, -superseding ruling 3 of #82 for this one case): it lists what it found, then -runs itself on each one in name order, continuing past a refusal. `resume` -deliberately keeps that old refusal for its own bare form — rebuilding every -estate on a fresh machine by accident is the opposite risk from failing to -park the one you meant. `--repo ` names the estate by the -`origin` of a clone you ALREADY have, in any url spelling; it never clones -one, because `resume` is the command that clones and it clones from the -record. +**How the estate is found.** `` is a folder under your projects directory: +`~/projects/`, then `~/Projects/` — people spell it both ways — or +wherever `$PROJECTS_DIR` points. A FAMILY folder (`//family.yaml`) +wins over a standalone root (`/project.yaml`) of the same name, because +the holder is what drives the members. With no `` the estate around the +current directory is used: the nearest holder beside you, or project root above +you. With no estate around it either, `park` lists every estate it finds under +your projects directory and ASKS whether to park them all (with no terminal on +stdin it refuses and names `--all`); `park --all`, or `-a`, parks them all +unasked, in name order, continuing past a refusal and skipping a standalone root +with no Speckit overlay (Brett Heap's RULINGs of 2026-09-10, the later +superseding #91's unasked sweep). `resume` has no `--all`, and its bare form +still refuses — rebuilding every estate on a fresh machine by accident is the +opposite risk from failing to park the one you meant. `--repo ` +names the estate by the `origin` of a clone you ALREADY have, in any url +spelling; it never clones one, because `resume` is the command that clones and +it clones from the record. **What they leave behind, said out loud.** `park` finishes by printing what did NOT park and why none of it is park's to touch: commits on a `main` that @@ -1301,9 +1303,8 @@ left behind, which is the point of printing that report. **This repository implements neither, deliberately** (ruled by Brett Heap, 2026-09-09, #77). The WIP commit, the push, the `git worktree add`, the `.specify/feature.json` rewrite and the soft reset are the **Speckit git -extension's**, which `setup-openspeckit` installs — one implementation. The -two targets are three lines each and refuse BY NAME when the overlay is not -there: +extension's**, which `setup-openspeckit` installs — one implementation. The two +targets are three lines each and refuse BY NAME when the overlay is not there: make park needs the Speckit worktree overlay; install it with: setup-openspeckit @@ -1344,17 +1345,16 @@ which carries no feature branch at all. > a tool that discards somebody's pushed commit to make its own record fit. A FAMILY holder carries both verbs too and runs each member's own — **in the -members' WORKING CLONES BESIDE THE HOLDER**, through `scripts/siblings.py ---make park`, and never in `members/`. That is the layout's own -rule: the pinned copy is detached, for `bootstrap` and `validate`, and the -sibling is where a person works, so the sibling is where the features and the -worktrees are. A holder that parked the pinned copies would report "nothing -to park" for every member while the work sat untouched next door. The -dispatch clones nothing and fetches nothing; a member with no working clone is -reported and SKIPPED naming `make siblings`, a member whose verb refuses has -that refusal printed where it happened, and the holder exits non-zero if any -member was skipped or refused. A skip is not a pass, and a refusal is never -hidden. +members' WORKING CLONES BESIDE THE HOLDER**, through `scripts/siblings.py --make +park`, and never in `members/`. That is the layout's own rule: the +pinned copy is detached, for `bootstrap` and `validate`, and the sibling is +where a person works, so the sibling is where the features and the worktrees +are. A holder that parked the pinned copies would report "nothing to park" for +every member while the work sat untouched next door. The dispatch clones nothing +and fetches nothing; a member with no working clone is reported and SKIPPED +naming `make siblings`, a member whose verb refuses has that refusal printed +where it happened, and the holder exits non-zero if any member was skipped or +refused. A skip is not a pass, and a refusal is never hidden. > [!WARNING] > **Native Windows parks nothing.** The extension's `park` and `resume` are diff --git a/docs/handbook.html b/docs/handbook.html index 248034ef..ee0b9cc7 100644 --- a/docs/handbook.html +++ b/docs/handbook.html @@ -637,11 +637,16 @@

What this machine needs

sudo dnf install gh

git itself comes from sudo apt-get install -y git on Debian and Ubuntu and sudo dnf install -y git on Fedora — the same commands - that preflight offers to run for you.

+ setup-project.py --preflight offers to run for you; the run and + openRepoShape need git before their preflight starts.

The tool offers the rest

-

The run opens with a preflight that names what is missing, prints the command for THIS - machine and asks once — Type yes to continue:, the same words it asks - everything else with — then runs it and carries on.

+

The run opens with a preflight that names what is missing and, for gh, its + login and its credential helper, prints the command for THIS machine and asks once — + Type yes to continue:, the same words it asks everything else with — + then runs it and carries on. A missing git is refused before the preflight + starts, with where to get it (setup.sh does the same for a Python older than + 3.9), so only setup-project.py --preflight, run directly, offers to install + it.

@@ -673,8 +678,9 @@

What this machine needs

That the login shell is zsh changes nothing — both entry points are run under bash explicitly.

The tool offers the rest

-

The preflight names what is missing, prints the command for THIS machine and asks once - — Type yes to continue: — then runs it and carries on. +

@@ -693,10 +699,12 @@

What this machine needs

git-scm.com/downloads, and gh with winget install --id GitHub.cli -e.

The tool offers the rest

-

The preflight names anything missing, 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 Git.Git -e for - git and winget install --id GitHub.cli -e for gh. +

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 here: that two-liner IS the Windows way in, and the openRepoShape command is a macOS, Linux and WSL2 convenience with no Windows twin, because on Windows there is nothing to install.

@@ -724,9 +732,9 @@

What this machine needs

&& sudo apt update \ && sudo apt install gh -y

The tool offers the rest

-

The preflight names anything missing and where to get it, prints the command for THIS - machine and asks once — Type yes to continue: — then runs it and - carries on.

+

The preflight names anything missing and where to get it 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.

@@ -799,14 +807,17 @@

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 0 when everything is present and 1 when it is not, and creates nothing. This - is also where a python3 below 3.9 is named.

+ 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.

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 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.

  • @@ -856,8 +867,9 @@

    Rehearse first, creating nothing

    fetch setup.sh, then for the self-bootstrap clone of this standard into a temporary directory. Run the same flag on ./setup.sh from a checkout instead and nothing touches the network at all. What it leaves behind is - ./Atlas and three bare repositories in ./rehearsal, both of - which you can delete.

    + ./Atlas, three bare repositories in ./rehearsal, and the + scaffold’s three working trees in the temporary directory its + Working trees are at line names, all of which you can delete.

    Honestly: it still reaches GitHub before the run starts — to fetch setup.sh, then for the self-bootstrap clone of this standard into a temporary directory. Run the same flag on ./setup.sh from a checkout @@ -878,8 +890,10 @@

    Run it

    py setup-project.py <Project> --org <your-org>

    <Project> is one positional CamelCase name (e.g. Atlas); --org is required — from the flag, - $OPENREPOSHAPE_ORG, or the command’s own prompt — and add - --visibility private if you want that. It shows the plan and asks once, + $OPENREPOSHAPE_ORG, or the command’s own prompt — and + --visibility defaults to private: add + --visibility public (or internal) if you want that. It shows + the plan and asks once, Type yes to continue:; type yes. Afterwards ./<Project> is the cloned root.

    <Project> is one positional CamelCase name (e.g. @@ -1053,7 +1067,8 @@

    Reading private legs in CI

    that fails to mint is a misconfiguration, not a degrade, and fails the job outright, naming both secrets and the required installation.
  • An installation token is per-owner, so a leg under a different owner is excluded with a - warning and can only be reached via SHAPE_LEGS_TOKEN.
  • + warning. SHAPE_LEGS_TOKEN is read only when the App is not configured, so + that leg is reachable only with the App secrets removed.
  • The root repository is always readable by the workflow’s own default token, so actions/checkout never carries a token: override — putting a legs credential there was itself the next defect, because a token scoped to the legs alone @@ -1061,8 +1076,10 @@

    Reading private legs in CI

    ran.
  • Whichever credential resolves is read only inside the guarded “fetch the legs (submodules)” step, scoped to that step’s env:, and used through a - git -c url.<...>.insteadOf=<...> rewrite covering both HTTPS and SSH - leg URLs — never persisted onto the root checkout.
  • + git -c url.<...>.insteadOf=<...> rewrite covering both HTTPS + (https://github.com/…) and scp-form SSH + (git@github.com:…) leg URLs — never persisted onto the root + checkout.
  • Without either credential the workflow degrades instead of failing: it still runs the naming and manifest checks and skips validate-pins.py with a warning explaining why. It fails outright only if a credential is configured and the fetch @@ -1106,8 +1123,9 @@

    The three legs, and naming

    — <user>-wip, a person’s own workspace repository, unambiguous by construction because nothing else here spells that suffix. The leg suffixes are lowercase and hyphenated precisely so they sit in a different visual class - from every other family, all of which are CamelCase words. Every repository of a project also - carries the GitHub topic xf-project-<id>.

    + from the neutral-product, domain-descendant and family forms, all of which are CamelCase + words. Every repository of a project also carries the GitHub topic + xf-project-<id>.

    A descendant form is a claim, and a claim needs a referent: <Domainx><Product> counts as a descendant only if it PINS open<Product>, declared as neutral_product_pins: in the @@ -1153,8 +1171,8 @@

    Adopting an existing repository

    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 it is ONE split commit, arriving on a branch by pull - request.

    + never force-pushed; the only change to its history is ONE split commit, arriving on a branch + by pull request.

    ./adopt-project.py plan --source MedxSoft/MedxEHR --project MedxEHR
     #   ... a human or an AI answers every `review_required` entry in the plan
     ./adopt-project.py check   --plan adoption-plan.yaml
    @@ -1314,7 +1332,8 @@ 

    The double pin, and the lockstep invariant

    What is digested. sorted-ls-tree-r-v1: the sha256 of the sorted git ls-tree -r -z <commit> listing — each record <mode> SP <type> SP <oid> TAB <path> with the path - unquoted, sorted bytewise, joined with LF. Not git archive: its tar bytes carry a + unquoted, sorted bytewise, each followed by LF, the last one included. Not + git archive: its tar bytes carry a pax header naming the commit, uname/gname, an mtime, and export-ignore handling from .gitattributes, so the same commit can digest differently across git versions and platforms — disqualifying for a number two machines must agree on.

    @@ -1322,7 +1341,7 @@

    Advancing a leg

    Moving those three facts by hand is how they come apart, so one command moves them together, run from a checkout of this standard:

    python3 scripts/bump-leg.py --root <path-to-the-project> --leg spec \
    -    --to <40 hex commit>       # --dry-run prints the move and writes nothing
    + --to <40 hex commit> # --dry-run prints the move; it fetches into the leg and writes nothing else

    It fetches the commit into the leg, checks the leg out AT it, recomputes the sorted-ls-tree-r-v1 digest from the leg’s own objects, rewrites commit: and digests.tree_sha256, rewrites every TRACKED workflow @@ -1340,13 +1359,14 @@

    Advancing a leg

    and the remedy of making it readable and running the command again; a root standing on its own tracking branch (the next step is a pull request, never a push to the default branch: git switch -c <branch> first; - --dry-run writes nothing, so it runs there and prints that command); and a + --dry-run fetches into the leg and writes nothing else, so it runs there and + prints that command); and a validator that goes red — rolling the pin, the workflow files, the leg’s checkout and the index back first, so a refusal leaves the tree exactly as it was found.

    The leg is left DETACHED at the new commit; make bootstrap re-places it on its - tracking branch AT the new pin. Editing the pin file by hand to make the validator agree is - what the validator exists to refuse.

    + tracking branch AT the new pin, or says why not. Editing the pin file by hand to make the + validator agree is what the validator exists to refuse.

    Full text — the double pin @@ -1394,8 +1414,12 @@

    Keeping a project’s shape current

    check prints one verdict per file the pin names, and the verdict is the product: unchanged, upstream-changed (the bytes differ between the pinned commit and the target), locally-modified (the project’s bytes differ - from the pinned digest — drift it introduced) or both. It exits 1 when there - is anything to do, so it can be a scheduled job.

    + from the pinned digest — drift it introduced), both or + already-at-target (the root already holds the target’s bytes) — or, + where there is nothing to compare, upstream-removed (the standard no longer has + the file), unmapped (no file there corresponds to it) or + copy-missing (the root has no such file), each of which apply + refuses. It exits 1 when there is anything to do, so it can be a scheduled job.

    apply copies the upstream-changed files, rewrites contracts/shape-pin.yaml and mirrors the two fields into project.yaml. Then it runs the project’s OWN validate-pins.py @@ -1406,8 +1430,9 @@

    Keeping a project’s shape current

    human passes --accept-local <path> — recomputing that row silently would turn a drift finding into a digest that agrees with the project’s own edited bytes.
  • -
  • A file changed on both sides is refused outright: two edits to one file - is a merge, and a merge is a human’s judgement. So is a copy that was never +
  • A file changed on both sides is refused until a human merges it by hand, + commits the merge and re-runs with --accept-local <path>: two edits to + one file is a merge, and a merge is a human’s judgement. So is a copy that was never verbatim.
  • Only the pin’s own rows are RE-SYNCED. The file list is read from contracts/shape-pin.yaml and never re-derived from this repository’s copy @@ -1466,9 +1491,9 @@

    Is this repository compliant? openRepoShape --doctor

    preference said once and stepped over by the verdict (a leg gets its assembly root).

    Every row is ok, note, FINDING or - n/a — or advisory, the Triad row alone — and every one - that is not ok names the exact command that fixes it, except the Triad row, - which names none in any status. Only a FINDING moves the verdict, and + n/a — or advisory, the Triad row alone — and a row that + is not ok names the exact command that fixes it whenever there is one to run, + except the Triad row, which names none in any status. Only a FINDING moves the verdict, and FINDING means exactly something else asserts this — a validator, a pin row, a manifest, a gitlink. A difference nothing asserts is a note: printed, in --json, and stepped over by the verdict. That line was drawn after @@ -1478,16 +1503,17 @@

    Is this repository compliant? openRepoShape --doctor

    - - - + + +
    verdictexitwhen
    COMPLIANT0every row ok, and the pin names this standard’s commit
    COMPLIANT, SHAPE BEHIND (n upstream-changed, m upstream-added)1valid, and the standard has moved on
    DRIFTED (…)1a copy was edited here, or a leg is off its pin
    COMPLIANT0no row is a FINDING, and the pin names this standard’s commit
    COMPLIANT, SHAPE BEHIND (n upstream-changed, m upstream-added), or (pin <a> -> <b>, no copied file differs)1valid, and the standard has moved on
    DRIFTED (…)1a copy was edited, deleted or no longer maps onto the standard, or a leg (a member, in a holder) is off its pin
    MISPLACED (n paths)1a tracked path sits in a leg the path policy puts elsewhere, or a submodule in a leg other than the one its submodules: record names
    INVALID (<rows>)1a validator went red, or (merge) the index holds an unmerged path
    NOT A SHAPE ROOT2neither manifest is there — adopt, or scaffold
    -

    Exit 3 is usage or environment — nothing about it is a statement about the tree - you pointed at: no such path, a shape-doctor.py not in a checkout of the +

    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 verdict line, deliberately: an edited shape copy is exactly what makes @@ -1507,7 +1533,8 @@

    Is this repository compliant? openRepoShape --doctor

    contracts/path-classification.yaml, walked with adopt-project.py’s own walk(), so there is no second copy of that classification to drift — over every tracked path of every mounted leg, and names - the paths the policy would have put somewhere else, each with the rule id that judged it. + the paths the policy would have put somewhere else, each with the rule id that judged it, + and a submodule in a leg other than the one its submodules: record names. What belongs to every repository is never judged: the four files templates/spec-root/ and templates/code-root/ themselves ship (README.md, AGENTS.md, CLAUDE.md, @@ -1599,15 +1626,17 @@

    park <Name> and resume <Name>, from an (<Name>/project.yaml) of the same name, because the holder is what drives the members. With no <Name> the estate around the current directory is used: the nearest holder beside you, or project root above you. With no estate around it either, - park PARKS EVERY ESTATE it finds under your projects directory instead of - refusing (Brett Heap’s RULING of 2026-09-10, #91, superseding ruling 3 of #82 for this - one case): it lists what it found, then runs itself on each one in name order, continuing - past a refusal. + park lists every estate it finds under your projects directory and ASKS whether + to park them all (with no terminal on stdin it cannot ask, so it refuses and names + --all); park --all, or -a, parks them all unasked, + from anywhere, in name order, continuing past a refusal and skipping a standalone root that carries no + Speckit overlay (Brett Heap’s RULINGs of 2026-09-10: #91’s unasked sweep, + superseded the same day by this question). --repo <owner/name> names the estate by the origin of a clone you ALREADY have, in any url spelling; it never clones one, because resume is the command that clones and it clones from the record.

    -

    Refuses resume keeps the old - refusal: with no <Name> and no estate around it either, +

    Refuses resume has no + --all: with no <Name> and no estate around it either, resume’s own bare form still REFUSES and lists the estates it found — rebuilding every estate on a fresh machine by accident is the opposite risk from park failing to park the one you meant.

    @@ -1840,9 +1869,10 @@

    For AI assistants

    added (#411); the doctor’s merge row, and INVALID (merge) above drift, added (#424); the assistant’s route when its shell has no terminal, added (#476); the one-liner asking at the person’s own terminal, never reading the answer - from the pipe, added (#492) — a page cannot name the commit that contains it, and - the blob below is the fact a test checks. - README.md blob ec812f8e9b4b3292f60932e7e9b86aea2e0a708b. Every command, flag, exit code + from the pipe, added (#492); the statements the v1.0.0 accuracy pass found false, + corrected here as in the README, added (#483) — a page cannot name the commit + that contains it, and the blob below is the fact a test checks. + README.md blob 1417b9ec0235675da8f73ca023756411bc3fbe8a. Every command, flag, exit code and file name on this page is quoted from that README and from AGENTS.md.

  • The preflight names what is 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. Homebrew is the one install never run for you: on a Mac without brew the preflight names brew.sh and stops there.