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.
+
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.
@@ -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
| 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 <a> -> <b>, 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 (<rows>) | 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 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.