Repository navigation
adopt-project.py: plan --pin takes product@<40 hex> and execute writes its pin file as the scaffold does, so an adopted root passes validate-pins.py (#273); a --pin-source that cannot answer is pin-source-unreadable (#339) - #508
Merged
Merged
Conversation
…ducts whose names differ only in `-`, `.` or `_` share one `SHAPE_PIN_SOURCE_<PRODUCT>`, so `--pin` refuses the pair and `validate-pins.py` reports it as `neutral-pin-source-collides` and reads the variable for neither, where one checkout answered for both pins and the other was a `neutral-pin-source-unreadable` finding (#275) `pin_source_env_name` maps every character outside `[A-Z0-9_]` to `_`, so `open-glass`, `open_glass` and `open.glass` are three legal names, three pin files and ONE variable. `scaffold-project.py --pin open-glass@<A> --pin open_glass@<B>` scaffolded the pair (exit 0); the project's `validate-pins.py` then gave whichever checkout `SHAPE_PIN_SOURCE_OPEN_GLASS` named to BOTH pins and reported the one it was not a checkout of as `neutral-pin-source-unreadable` (exit 1, a red `validate.yml`); with no source, both skip notes offered the same variable as the way to verify either. Every variable name is kept: an operator may have set one and the docs name them. The pair is refused where it is declared instead. `repo_shape.pin_source_env_collisions(products)` is the one pure answer, `{variable: [products]}` for each variable two or more DISTINCT products share, distinct after a casefold (the fold of the pin file, `--pin-source` and `--link-source`), so one product typed twice or in two cases is never a collision (that is #241's file collision). `pin_source_env_shared` is the set of casefolded names it implies. - `scaffold-project.py`: `_refuse_colliding_pins` names each variable's `--pin` arguments as typed after #241's sentences, in one `scaffold-pin-collides` refusal, dry run and real run alike, before any process starts and ahead of the leg-path refusal. #241's own wording and remediation are unchanged when only a file collides. - `validate-pins.py`: one `neutral-pin-source-collides` finding per variable, naming every product and the variable. A product whose variable is shared does not read it (rule 2 of `resolve_neutral_pin_source`) and its skip hint stops offering it, so the pin the variable was not a checkout of is no longer reported against the project. A variable one product has to itself reads and is offered exactly as before. - `repo_shape.py` chain readers: `link_pins_from_trees` does not read a variable two links of the chain share, and the `declared-unverified` hint stops naming it. Only a hand-written chain can have two such links. `adopt-project.py` is not changed: its `--pin` half arrives with #273, which routes adopt through `pin_source_env_collisions`. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…e scaffold does and records the commit and the digest, and `execute` writes `contracts/<product>-pin.yaml`, so an adoption that declares a pin no longer says `adoption verified` over a root whose own `validate-pins.py` refuses it as `neutral-pin-missing` (#273) Brett Heap ruled for #273's decision 1, first option ("go with your recommendation on #273"): adopt takes the scaffold's pin and writes the scaffold's pin file. This reverses the NAME-only `--pin` of #6. - scripts/shape_materialize.py: the scaffold's pin machinery moved here, beside `neutral_pin_path` and the materializer that writes the file: `PIN_ARG_RE`, `parse_pin`, `pin_digest_from_source`, `pin_digest_from_gh`, `declared_pin_values`, `declared_pin_products`, `PIN_COLLISION_REMEDIATION`, and `_refuse_colliding_pins`, split into the pure `pin_collisions` and `refuse_colliding_pins(refusal=...)`. Moved verbatim; `scaffold-project.py` imports what it calls under its old names, and its refusals and their words are unchanged. - adopt-project.py `plan`: `--pin`, `--pin-owner` and `--pin-source` mean what they mean for the scaffold. A name with no `@<commit>` is `pin-malformed`; `spec`, `code` and `shape` in any case, and one product twice, are `adopt-pin-collides` (#273 decisions 4 and 5); a leg mounted over a pin's file is `adopt-leg-path-collides`. The digest is read at `plan` time and the plan records product, repository, commit, digest and how it was read, so `check` and `execute` read no network for a pin. - `check` finds, and `execute` refuses before any leg exists (one list, `_entry_findings`), a pin the plan does not record whole: `plan-pin-unrecorded` (a name alone, from an old plan or an edit, or a mapping missing its commit or digest), `plan-pin-malformed`, `plan-pin-collides`, and `plan-root-holds-pin` (a kept source path at the pin's file, which `validate-pins.py` would read in place of the shape's). - `execute` writes each pin through `materialize_assembly_root(neutral_pins=)` and lists the product, owner stripped, in `neutral_product_pins:` (#273 decision 3). - `--help` and docs/cli.md say so; tests/test_adopt_pins.py covers every row of #273's table end to end, with the adopted clone's own validators and bootstrap, and every refusal and finding. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One conflict, in scaffold-project.py: #309 (#288) reworded `PIN_COLLISION_REMEDIATION` and its comment, which this branch moved to scripts/shape_materialize.py for #273. Resolved by keeping the move and carrying #309's words and comment into the moved constant, so the scaffold's refusal reads as main's does and adopt's `adopt-pin-collides` says the same. `docs/cli.md` is what the generator produces (`--check`). Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n-writes-pin-file: adopt's pins are refused, and found, when two products share one `SHAPE_PIN_SOURCE_<PRODUCT>` #275's scaffold half added `_colliding_pin_variables`, `_pin_collision_remediation` and `PIN_VARIABLE_REMEDIATION` to scaffold-project.py's `_refuse_colliding_pins`, which this branch moved to scripts/shape_materialize.py for #273. The one conflict is resolved by moving them with it, unchanged in their words: `pin_variable_collisions` (the same sentence, each pin named as its caller names it), `pin_collision_remediation` and `PIN_VARIABLE_REMEDIATION`, called from the shared `refuse_colliding_pins`. scaffold-project.py no longer imports `PIN_SOURCE_ENV_PREFIX` or `pin_source_env_collisions`, which only that code used. #275's adopt half, which implQ left for #273: `adopt-project.py plan --pin open-glass@<A> --pin open_glass@<B>` is `adopt-pin-collides` through the shared refusal, and `check` finds (and `execute` refuses before any leg exists) such a pair in a plan as `plan-pin-collides`, through `pin_variable_collisions`, the question the root's own `validate-pins.py` asks as `neutral-pin-source-collides`. tests/test_adopt_pins.py holds both. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…site in `_leg_findings` only, not again in `_entry_findings`'s docstring (#273) The paragraph repeated the comment beside `findings += _pin_findings(plan, tree)`. Where it sat, it was the one line in the branch that conflicted with the open #303/#305 branch, which adds its own paragraph to that docstring at the same place. `_entry_findings` is now as it is on main. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…336 and #268 merge with no conflict #268 clones the assembly root with no git template and verifies each mount's gitlink against the commit pushed to its leg; the pin file this branch writes is materialized after that clone, into the same split commit, and neither change reads the other's lines. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ate-pins.py conflicted in seven hunks where #312's PinSources replaced the keyed_sources/default_source parameters, kept #312's side in all seven and added #275's env_shared and shared parameters, the neutral-pin-source-collides finding and its docstring paragraph on top The one conflict was templates/assembly-root/scripts/validate-pins.py, in resolve_neutral_pin_source (signature and docstring), _recompute_neutral_pin_digest (signature and its resolver call), _check_one_neutral_product_pin (signature and its call) and _check_neutral_product_pins (docstring and body). #312 landed (8cdca5e) with the same file blob as the PR head implQ resolved against, so the resolution is implQ's: every hunk keeps #312's PinSources, its 3-tuple return and its _note_unused_pin_sources call, and adds #275's env_shared parameter (rule 2 reads no variable another pin shares), the shared frozenset, the skip hint that omits a shared variable, and _report_pin_source_collisions. scaffold-project.py merged without a conflict over #309's remediation text. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…b623dd) into fix/adopt-pin-writes-pin-file: adopt's pins sit on a validate-pins.py that composes #312's PRODUCT=PATH reading with #275's neutral-pin-source-collides, byte-identical to Q's, and on main through #270, #312, #317, #344 and #349 with no conflict The merge is clean. templates/assembly-root/scripts/validate-pins.py, scripts/repo_shape.py and tests/test_pin_source_env_collisions.py are byte-identical to fix/pin-source-env-collisions 34dbd86 (blob 54d8ac7 for the validator), so adopt's branch does not diverge from #275's there. adopt-project.py, scripts/shape_materialize.py, docs/cli.md and tests/test_git_output_decoding.py auto-merged over main's #270 (f795cf7, `unwritable_reason`), #312, #317, #344 and #349 (the session `gh` guard). Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ects `execute` to refuse a plan whose `pins:` holds the name `openX\ud800` under `plan-pin-unrecorded` first, where #273 reads a name-only pin as one nobody recorded, so the lone surrogate is still found as `plan-unwritable-value`, listed beneath it, and refused before any leg exists (#273, #255) #270 (f795cf7) wrote the case against a `pins:` list of names, the form every plan held before #273. On the merged tree `check` lists two findings for it: `plan-pin-unrecorded` (the entry is a name with no commit and no digest, Brett's ruling on #273) and `plan-unwritable-value` (the name holds a lone surrogate). The refusal's code is the first finding's, so only that one assertion changed. The finding assertions, `check` exit 1, `execute` exit 2, the finding listed under the refusal and "no leg repository was created" all still hold. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…t of `stat()` or git's `cwd=` into `pin-source-unreadable`, saying "no such directory" and "it is a file, not a directory" as `validate-pins.py` does, and its `Path.resolve()` no longer raises on a symlink loop, so a `--pin-source` that is missing, a file, under a parent this user cannot enter or a loop is exit 2 with its remediation from `scaffold-project.py` `--dry-run`, its real run and `adopt-project.py plan`, and no longer a traceback, exit 1 (#339) `pin_digest_from_source` caught only `Refusal`. A directory that is no clone is git's own failure, so it was always `pin-source-unreadable`, exit 2. A path `subprocess.run` cannot enter never reaches git: it is a `FileNotFoundError`, `NotADirectoryError` or `PermissionError` out of `cwd=`, and nothing caught it, so `--pin-source <missing>` ended `scaffold-project.py`'s `--dry-run` and its real run in a traceback, exit 1, and since #273 `adopt-project.py plan --pin` ended the same way, both calling the one function through `declared_pin_values`. What it does now: - `stat()` judges the source inside the guard, as #312 does in `validate-pins.py`: a missing path or one beneath a file is "no such directory", a file is "it is a file, not a directory", and any other `OSError`, from `stat()` or from git's `cwd=`, is the system's own reason (`strerror`: "Permission denied", "File name too long"). - Each is the existing `pin-source-unreadable` refusal, naming the path and the reason, with the remediation it always had, exit 2, before anything exists and identically under `--dry-run`. The not-a-clone refusal is unchanged. - `Path.resolve()` is guarded too: a symlink loop is a `RuntimeError` out of it before 3.13 and an `OSError` since, raised before the digest was asked for. As typed, `stat()` meets the loop and says so. - A source that answers is read exactly as before: a relative path resolves against the working directory, a symlink to a clone is followed, and the digest is `tree_digest`'s. `tests/test_pin_source_unreadable.py` runs each case through `scaffold-project.py --dry-run`, its real run and `adopt-project.py plan`: missing, a file, beneath a file, relative and missing, a parent and a source with no search bit (skipped on Windows and as root), a 300-character name, a symlink loop (skipped on Windows) and a directory that is no clone. Each asserts exit 2, the refusal naming the path and the reason, its remediation, no traceback and nothing created. It also holds `--dry-run` and the real run to the same stdout and stderr, the path printed escaped under an ASCII stderr and as a surrogate when it is not UTF-8, and a relative path and a symlink to a clone read as before. 38 of its 46 rows fail on d806e8f; the other 8 are the unchanged behaviour. Closes #339 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ote and no finding, and two declarations are one product when they share a pin file and not a casefold, so a project that already declares `open-glass` and `open_glass` takes `update-shape.py apply` again, and `openß` beside `openss` is found (#275) Fix round 1 on #275, from the review of 34dbd86. The first round made two declared pins that share one `SHAPE_PIN_SOURCE_<PRODUCT>` a FINDING. Main through ab623dd scaffolds such a project (`--pin open-glass@A --pin open_glass@B` exits 0), and `update-shape.py apply` runs the NEW `validate-pins.py` over the rewritten root, so the finding ended every apply as `REFUSED update-validators-red`, rolled back, for this update and every later one, until a person deleted a true declaration of descent, which AGENTS.md tells an agent never to do. It is a NOTE now, the lane's choice and one Brett can reverse: the guard stays, so the variable is read for neither pin and each pin verifies from its own source, and the note names both pins, the variable and the keyed `--pin-source <product>=<path>`, and says which declaration stays is the human's decision. `scaffold-project.py` still refuses a NEW pair, so the situation is not created anew. Two declarations are now one product when they are one pin file (`pin_product_key`, `lower()`, the fold `contracts/<product lowercased>-pin.yaml` is named by) and not when `casefold()` says so. `openß` and `openss`, `openſ` and `opens`, `openfi` and `openfi`, `openς` and `openσ` share a variable and have two pin files, and were never found; a hand-written `project.yaml` is the only way to declare them. The skip hint for a product two pins share offers the keyed `--pin-source <product>=<path>`, since the bare form answers for every pin with no more specific source. The guard now has a test that needs no `gh` (a sibling checkout beside the project for each pin, the variable naming one of them), so it runs on Windows, and the hint and the non-ASCII rows are tested in-process. A project that declares a pair takes `apply`, and the doctor's `pins` row stays `ok` before and after (`test_a_project_declaring_a_colliding_pair_takes_a_shape_update`). Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…fix/adopt-pin-writes-pin-file: `neutral-pin-source-collides` is the validator's note, and `repo_shape.pin_product_key` decides one product by its pin file, with no conflict, and `validate-pins.py`, `repo_shape.py` and `tests/test_pin_source_env_collisions.py` byte-identical to Q's Q's fix round (6494714 over 34dbd86) changes three files and no other: `templates/assembly-root/scripts/validate-pins.py`, `scripts/repo_shape.py` and `tests/test_pin_source_env_collisions.py`. None of them is one this branch changes over Q, so git merged all three with no conflict, and each blob in this commit equals Q's (`git rev-parse :<path>` against `6494714:<path>`: 5604007, 9833580, 5220787). Q left `scaffold-project.py` untouched, so the pin functions this branch moved out of it into `scripts/shape_materialize.py` are not reached by the merge; the next commit ports Q's `pin_product_key` there. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…351, #350, #345, #372, #371, #354, #380, #377 and the rest since ab623dd merge with no conflict, beside the pin functions this branch moved out of `scaffold-project.py` and its `plan --pin` in `adopt-project.py` Twelve commits of main since ab623dd, the base this branch last merged, reach five files this branch changes: `adopt-project.py` (161c6c9 #245, a983e6a #371, 92e29d9 #377), `scaffold-project.py` (d82a83e #351), `scripts/shape_materialize.py` (1f90a0f #380), `scripts/repo_shape.py` (161c6c9) and `docs/cli.md` (3ae0109 #345). Git merged every one with no conflict: - `scaffold-project.py`: #351's `_identity` value checks and its `unwritable_reason` import sit beside the imports that replaced the pin functions; pyflakes finds no undefined name in either tool (only the unused imports main itself has). - `adopt-project.py`: #245's `_entry_findings` now returns its NOTEs as a third value and takes `local_remote_dir`; `_pin_findings` is still read through `_leg_findings`, inside it. - `scripts/repo_shape.py` equals the tree `git merge-tree` gives for 6494714 with cd1fc79 (blob 091966b), and `validate-pins.py` and `tests/test_pin_source_env_collisions.py` are still Q's blobs. - `render-cli-reference.py --check`: ok. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…printable is named as `repr` spells it, no `gh` on PATH is `gh-not-found`, `plan` refuses a `--pin-source` `check` would refuse, and two pins share a variable by their pin files, so a refusal no longer prints an ESC or forges a `REFUSED` line, a pin read with no `gh` is no traceback, and `check` no longer refuses the plan `plan` just wrote (#273, #339) Fix round 1 of lane 5's review of this branch at 7a94b6b. - Q's `pin_product_key` (#275's fix round, merged in da2248f) is now how `pin_variable_collisions` keys each pin, where it casefolded. Q left `scaffold-project.py` untouched, and this branch had moved the function that used the casefold into `scripts/shape_materialize.py`, so the merge could not carry it. `openß` and `openss` are two pin files and one `SHAPE_PIN_SOURCE_OPENSS`: a casefold named each of the two pins twice. `refuse_colliding_pins`' docstring says, in Q's words, that the root's `validate-pins.py` now only NOTES such a pair. - S1: `pin_digest_from_source` names the path as typed unless it is not printable, and then as Python's `repr` spells it (`shown_source`), in the refusal's head and in git's own reason, which names the directory it ran in. Printed raw, an ESC reached the terminal, and a line break or a carriage return began a forged `REFUSED pin-source-unreadable` line, from the scaffold and from adopt; the traceback #339 replaced had shown a `repr`. A printable path is named byte for byte as before. - S2: `pin_digest_from_gh` with no `gh` on PATH is `gh-not-found`, exit 2, as `repo_shape.tree_digest_from_gh` names it, offering `--pin-source`. It was a `FileNotFoundError` traceback, exit 1, from the scaffold and, new with #273, from `adopt-project.py plan --pin`. - S3, the lane's decision (c): a commit or digest edited into the plan passes `check` and `execute`, by design: neither reads a network for a pin. `check` now says so once, after the pins it will write; the root's own `validate-pins.py` reports a digest that disagrees. No behaviour changed. - N2: `plan` asks `check`'s question (`one_printable_line`) of the `--pin-source` it records in each pin's `digest_source:`, before any digest is read, and refuses a path that fails it as `adopt-unwritable-value`, in #270's words, or "an invisible formatting character" where those name nothing. It was planned, exit 0, printed raw in the report, and `check` refused it as `plan-pin-malformed` with advice that wrote the same plan. The report's `pin` line and `check`'s `write` line spell the digest's source with `_shown`, as every path either prints. - N3: tests now kill each of the six hunks of this branch no test guarded: the two name guards (a `pins:` name holding an ESC and a forged line), the lower-casing of the commit and digest (an upper-cased hand edit), the `digest_source` printability check, the not-text check (`commit: 1234567` unquoted), and the gitlink filter (a submodule kept where the pin is written is `plan-submodule-root` alone). Tests: `tests/test_adopt_pins.py` and `tests/test_pin_source_unreadable.py` gain the rows above; one existing row of the latter now expects `adopt-unwritable-value` from `adopt-project.py plan` for a name that is not UTF-8, which `plan` cannot record. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s `tests/test_agents_md_codes.py`, which holds every code AGENTS.md names to a string a shipped tool holds, merges with no conflict and finds `scaffold-pin-collides` in `scripts/shape_materialize.py`, where this branch moved it 45e01f4 adds one file and changes none. The codes AGENTS.md names that this branch moved out of `scaffold-project.py` (`scaffold-pin-collides` among them) are string constants of `scripts/shape_materialize.py`, which `tests/test_repo_hygiene.py::SHIPPED` lists, so the new test reads them there. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… `plan` asks `check`'s manifest question of the plan it is about to write, whose `pins:` now holds each pin as the mapping `plan` writes since #273, and #314's key-for-key test pins `openGlass@<commit>` from a local stand-in, where this branch refuses a name alone #314 asks the project's YAML reader and `validate-manifest.py` about the manifest's values before anything exists, in both tools. One conflict, `adopt-project.py`, one hunk: this branch added `_refuse_an_unrecordable_pin_source` after `_checked_plan_inputs`, and #314 added its section "The project must be able to hold what `execute` writes into it (#261)" (`MANIFEST_FLAGS`, `_flag_plan`, `_manifest_values`, `_manifest_findings`, `_refuse_unholdable_values`) at the same place. Both are kept, this branch's function first; neither calls the other. `scaffold-project.py`, `scripts/shape_materialize.py` and `docs/cli.md` merged with no conflict (`render-cli-reference.py --check` ok). Where the two meet, as lane 5 composed them before (implT's recipe, `pr314-composition.diff`): - `_flag_plan` built `"pins": list(pins)`. `pins` here is `declared_pin_values`' `{product: PIN_* values}`, so that list held the products, where `plan` writes each pin as a mapping of `PIN_FIELDS`. It now builds those mappings, so the plan it asks about is the plan it writes; `_manifest_values` still reads products through `Plan.pins`. The two annotations say `dict`. - `tests/test_adopt_manifest_values.py::test_plan_asks_what_check_asks_of_the_same_keys` planned `--pin openGlass`, a name alone, which `plan` refuses since #273 as `pin-malformed`. It now plans `--pin openGlass@<commit>` with `--pin-source` naming a stand-in repository the test makes, so it reads no network. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… asks nothing of the path, and a printable `--pin-source` is printed as it is in `plan`'s `pin` line and `check`'s `write` line, so dropping the `args.pin and` guard, or spelling every digest source, no longer passes every test (#273, #339) Lane 5's second review of this branch (at 421e4e5) found two hunks of fix round 1 that no test failed without, and asked for a test for each. No code changes. - n1. `plan` asks N2's question (`one_printable_line`) of `--pin-source` only when a `--pin` records it (`adopt-project.py`, `if args.pin and args.pin_source is not None`). With the `args.pin and` dropped, an unused `--pin-source` holding an ESC or a zero-width joiner was refused in a sentence naming no file ("`execute` writes into , and ..."), and all 118 tests still passed. `test_a_pin_source_no_pin_records_is_not_asked_about` plans with `--pin-source` and no `--pin`: exit 0, `pins: []`, the path named nowhere in the plan or the report, in any spelling. The ESC row is POSIX-only, by the file's `POSIX_NAMES` mark; the zero-width joiner row runs everywhere. Whether an unused `--pin-source` should be refused or noted instead is #339's third decision, which this branch leaves open; the test holds today's answer, silence, so a change to it is deliberate. - n2. `plan`'s `pin` line prints `digest_source` through `_shown`. With `_spelled` in its place, which backticks and escapes every path, all 118 tests still passed, because only a no-break-space path was asserted. `test_a_printable_pin_source_is_printed_as_it_is` plans from a printable clone path and holds the `pin` line, and `check`'s `write` line for the plan `plan` wrote, to their exact text, ending `(local-clone (<path>))` with no backtick. Mutation proof, in a throwaway worktree at 421e4e5 with these tests, over `tests/test_adopt_pins.py` and `tests/test_pin_source_unreadable.py` less the five end-to-end adoptions: - `args.pin and` dropped: 2 failed (both new n1 rows), 118 passed; - `plan`'s `pin` line `_spelled`: 1 failed (the new n2 test), 119 passed; - `check`'s `write` line `_spelled`: 2 failed (the new n2 test and `test_an_upper_cased_hand_edit_is_written_lower_cased`); - `plan`'s `pin` line raw (fix round 1's R1): 1 failed, the existing no-break-space test, which is that hunk's killer; the new n2 test passes there, as it must. The review's third nit, a plan with two unwritable values refused in two runs (`--elected-by` first, then `--pin-source`), needs a code change and is not made here. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…pin-file: #409's four files take main's blobs, which are #409's final head's, `scaffold-project.py` keeps the pin code this branch moved out and gains main's own changes, and #445's `try` in `_commit_the_split` carries this branch's `neutral_pins=` argument beside #394's `append` #409 squash-landed as 9736424 (its final head b70a07d). This branch held #409 at 6494714, and after it #409 only merged main five times, none with a hand edit (`git show --remerge-diff` empty for bac7dee, 480c348, efb7322, 5bdcae3 and b70a07d). `git diff b70a07d 9736424 -- <file>` is empty for each file #409 touched: scaffold-project.py, scripts/repo_shape.py, templates/assembly-root/scripts/validate-pins.py and tests/test_pin_source_env_collisions.py. Conflicts, four files, one hunk each: - scaffold-project.py. #409's squash re-adds `PIN_COLLISION_REMEDIATION`, `PIN_VARIABLE_REMEDIATION`, `_refuse_colliding_pins`, `_colliding_pin_variables` and `_pin_collision_remediation`, which this branch moved to scripts/shape_materialize.py (#273). The hunk takes this branch's side, empty, and #409's auto-merged import of `PIN_SOURCE_ENV_PREFIX, pin_source_env_collisions` (and its two comment lines), which nothing in the file uses any more, is dropped. Not the whole file `--ours`: main changed scaffold-project.py beyond #409 since this branch's last merge (6c2a371's `--tracking-branch` check, a73b4e9's `RULESET_HINT`), and those lines stay. The result equals the file `git merge-tree` gives for this branch with main before the squash (518010b), blob fbe7215, and its diff from this branch's tested file has the patch-id of main's own 3ef059c..518010b change to the file. - templates/assembly-root/scripts/validate-pins.py. Main's side, which is #409's final blob (5f83eb7) with main's #359 workflow reader. This branch had no change of its own to the file beyond #409 at 6494714. - adopt-project.py, `_commit_the_split`. #445 wraps the function in its late-failure `try` with `step` names, and #394 builds an `append` with the plan's submodule record. This branch's only edits there were a comment and `neutral_pins=_recorded_pins(plan)` on the materializer call. Resolved with main's side, and the comment and the argument on main's call inside the `try`: `append=append, neutral_pins=_recorded_pins(plan)`. - docs/cli.md, generated: regenerated with scripts/render-cli-reference.py over the resolved tree (`--check` ok). It differs from main's only in `adopt-project.py plan`'s `--pin`, `--pin-owner` and `--pin-source` help. scripts/repo_shape.py and tests/test_pin_source_env_collisions.py auto-merged to main's blobs (82346be, 5220787). pyflakes reports the same four warnings on the merged adopt-project.py, scaffold-project.py and scripts/shape_materialize.py as before the merge. #445's tests/test_adopt_late_failure.py hands `_commit_the_split` a `SplitPlan`, a `dict` that adds `entries`, as its plan, which `_read_pins` cannot read yet; the next commit makes it read one. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mmit_the_split` takes the `SplitPlan` mapping #445's tests hand it as the plan and no longer stops at "committing the split" with an AttributeError, and a loaded plan reads exactly as before (#273) #445 (75052cb) tests `_commit_the_split` directly. In tests/test_adopt_late_failure.py, `split_arguments` passes `plan=SplitPlan()`, a `dict` subclass that adds `entries` (#394). Those are all the function read before this branch: `plan.get("follow_ups", [])` and `plan.entries`. Since #273 it also writes each declared pin, `neutral_pins=_recorded_pins(plan)`, and `_read_pins` read `plan.data.get("pins")`. On the merge commit before this one, three of #445's tests failed (3 failed, 83 passed in that file), each with "STOPPED committing the split by AttributeError: 'SplitPlan' object has no attribute 'data'": - test_a_split_that_fails_names_its_step_and_both_pushed_legs[commit] - test_a_split_that_fails_names_its_step_and_both_pushed_legs[push] - test_a_split_the_materializer_cannot_write_is_refused_with_the_notice `Plan.get(key, default=None)` returns `default` when the value is None, and the value otherwise. With no default it returns `plan.data.get("pins")` exactly, so a loaded plan's `pins:` reads as before, absent and `null` alike, and the `rows is None` branch below is unchanged. A mapping has the same `get`, so the test's plan reads too. This was the only `plan.data` in the file. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
Author
|
(lane openRepoShape-5, 2026-10-13T02:23Z) @codex review |
|
Sorry @brettheap, your pull request is larger than the review limit of 150,000 diff characters |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
brettheap
pushed a commit
that referenced
this pull request
Oct 13, 2026
No conflict: main's change is adopt-project.py, scaffold-project.py, scripts/shape_materialize.py, docs/cli.md and tests, and touches neither README.md, AGENTS.md nor docs/handbook.html. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 13, 2026
This was referenced Oct 13, 2026
brettheap
added a commit
that referenced
this pull request
Oct 13, 2026
… false are corrected to what the tools do on main (#483) (#505) **Closes #483.** #483 is the v1.0.0 "README and AGENTS.md accurate" criterion. Three read-only audits (AGENTS.md; README.md lines 1-727; README.md lines 711-end) checked every statement that names a command, a flag, an exit code, a refusal, a file or a step against the code at `ec4a7a0e` and ran the tools offline (local bare remotes, a stub `gh`, a blank git identity). They found 38 false or incomplete statements. This pull request corrects 28 of them (27 whole, and D2-F1 in part), plus two the independent review found (the `MISPLACED` text after #394, and `openRepoShape --preflight` "offers to install" git-filter-repo): 29 rows in all, D1-F9 and D3-F7 sharing one. The other ten, and the rest of D2-F1, were settled by pull requests that landed while this one was open: #484 and #465 settle six (D1-F8, D2-F7, D3-F6; D2-F13, D2-F14, D3-F8), and #491 and #468, to which this branch had routed them, settle four (D1-F1, D2-F5, D1-F2; D3-F11) and the rest of D2-F1. The two routed to #490 (D1-F9, D3-F7) were still false once it landed, and are corrected here. A last read of every line main has added to README.md and AGENTS.md since `ec4a7a0e` found nothing else false, so nothing is left open under #483. Two independent reviews, six rounds. The first head (`bfbd4eeb`) came back CHANGES NEEDED: two corrections that were themselves untrue (exit 3; "each of those tools has its own section") and nine gaps. The second (`ccf520bd`, on a merge of `7a8b981d`, where #394 landed) fixed them and came back with no blocker and three landing changes, which the third commit (`f35e59fa`, on a merge of `5a253877`, where #484 landed) makes: D2-F5 and D1-F1's block go back to #491 (at `2d59d141` its `setup-project.py` reads the yes from the controlling terminal, so the piped one-liner asks once it lands, and its new test strips `<...>` from README.md, which a `bash <(` line breaks); D3-F5 is fixed here now that #484 has landed; and the routes are re-derived at the current heads. The same commit leaves AGENTS.md's family steps 3 and 5 as main has them, so #486-#488 (`10c93ded`) still merges clean. The fourth round merges `48585e62`, where #465 landed (README.md at 699-709, 1028 and 1095 and the handbook beside them, none of which this branch edits), and re-reads the three #465 rows on main. The fifth merges `57c85e92`, where #468 landed (the doctor's `merge` row), as `fb6a9ab4`: the doctor's verdict table, in README.md and in the handbook, takes this branch's `COMPLIANT`, `SHAPE BEHIND` and `DRIFTED` rows and main's `MISPLACED` and `INVALID` rows, and the handbook footer keeps both clauses (#424, then #483) with its blob recomputed. `aa4e1f59` (#508) then merged clean, as `83cfad54`. The sixth merges `70d0d1b3`, where #491 landed (setup asks at the person's own terminal), as `94793aeb`: README.md's "What Dana sees" paragraph takes #491's text whole with this branch's D2-F6 ("a hyphenated name"), and the handbook footer keeps #476's and #492's clauses ahead of #483's, with its blob recomputed. The same round re-reads every routed row on main, corrects D1-F9 and D3-F7 (`ea634043`), and reads what main changed in both files since `ec4a7a0e`. The first reviewer's round four came back APPROVE WITH NITS (0/0/3) and asked for one word, made at `9ac90d16`: "a hyphenated project name", since the leg names are hyphenated and pass. **What.** `README.md` and `AGENTS.md` only, plus `docs/handbook.html` where it repeats a corrected sentence, with its footer's `README.md blob` recomputed (`git hash-object README.md` = `1417b9ec0235675da8f73ca023756411bc3fbe8a`). No tool, test, template or `docs/cli.md` changes. `README.md` hashes identically at `ec4a7a0e`, `437a360a`, `d222a124` and `7a8b981d`, so its audit line numbers held; `AGENTS.md` changed only by #394 (it adds 5 lines after line 154 and 3 more after the doctor's item 4); #484, #465 and #491 since edited both files, and #468 README.md, without changing either total. Each fix is the smallest true sentence, keeps the surrounding voice, and states only what the code or a run showed. **The line caps.** `tests/test_repo_hygiene.py` holds `AGENTS.md` to 490 lines (#394 raised it from 482) and `README.md` to 1422, and both files stand exactly at their caps, so every correction is line-neutral. To give back the lines the longer sentences need, paragraphs that no correction touches are re-wrapped to 80 columns with no word changed: - README.md, eight paragraphs no correction touches: macOS (`and refuses anything below 3.9`), `2. Login.`, the InkRouter ruling quote, `Each leg is pinned TWICE`, the neutral-product election paragraph (`Ruled by Brett Heap on 2026-09-05`), `Its entries are adopt-project.py plan's entries`, `This repository implements neither`, `A FAMILY holder carries both verbs too`; and the tail of the `--link-source` paragraph that D2-F12 edits. None is in README.md's family section (903-925 on main), which #486-#488 edits. - AGENTS.md, four paragraphs no correction touches: the opening paragraph, the NO-code seeding paragraph, doctor item 1 and the park overlay item; and the first half of the doctor intro, whose second half D1-F7 edits. The family steps are main's exact bytes: an earlier head re-wrapped steps 3 and 5 (`make siblings`) and #486-#488 edits both. - Three sentences this pass corrects give up a few words to keep a line: D2-F1's "offers to run" (no "for you"), D2-F11's "asks the same of one name", and AGENTS.md's `check` verdict list and doctor next-command item (D3-F9, D1-F7). Word-level diffs of both files (`rvD-worddiff.py`) show only the edits in the tables below, `rvD-hunks.py` marks each re-wrapped hunk `WHITESPACE-ONLY`, and `rvD-mdstruct.py` finds the Markdown block structure, links and HTML identical (AGENTS.md 320/320 blocks, README.md 678/678). "Where" is the audited `ec4a7a0e` for README.md (it hashes the same up to #394, and #484 and #465 changed its text and not its line count, so those numbers still point at the same places on main); AGENTS.md lines after 154 read 5 higher on main, and 8 higher after the doctor's item 4, since #394. "Evidence" is code at `7a8b981d` (the line numbers below are that file's, re-derived at the merged head `48585e62` for every tool file that #394, #484, #465 and the later landings moved), and "run" is the audit's or this pass's offline run. | finding | where | was | now | evidence | |---|---|---|---|---| | D1-F3 | AGENTS.md:121 | "the preflight prints the install command" for git-filter-repo | "`execute` refuses without it, printing the install command" | adopt-project.py:4148-4158 (`_require_filter_repo`, `filter-repo-missing` + `FILTER_REPO_HINT` at 261-267), only call at 5652; `--preflight` checks git, Python, bootstrap, gh, helper, autocrlf only (setup-project.py:1110-1134); openRepoShape:212-216 | | D1-F4 | AGENTS.md:176-178 | the row reads `code: 0 of N source paths (seeded from template)` | the table's `code` row reads `0 of N source paths (seeded from template)` | adopt-project.py:6107-6108 prints `f" {leg:<6} {counts[leg]:>5} of {len(before)} source " f"paths{note}"`, no colon; run: ` code 0 of 2 source paths (seeded from template)` | | D1-F5 | AGENTS.md:182 | "`MedxGlass` descends from `openGlass` only if it PINS it (2026-09-02)" | "...or pins a layer that does and records the chain (`--referent-chain`; 2026-09-02, chains 2026-09-05)" | scaffold-project.py:379-391; run: `--pin openXglass@<sha> --referent-chain openXglass,openGlass --dry-run` printed "classifies as domain-descendant / assembly" | | D1-F6 | AGENTS.md:205-210, 258-259 | `openRepoShape --doctor` and `./shape-doctor.py` presented as one tool; "`--json` is the same report" | installed, it runs from a temporary clone 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 <path>`; item 7 names `./shape-doctor.py --json` | openRepoShape:112-116 (EXIT trap), 329-352 (`shape_checkout` into `$WORKDIR/shape`), 379-392 (`run_doctor` clones when no checkout is beside it), 463-471 (`--doctor takes a path and nothing else`); run: the installed copy printed `NEXT python3 <tmp>/shape/adopt-project.py plan ...` and the directory was gone after exit; `--doctor <root> --json` printed `REFUSED: --doctor takes a path and nothing else`, exit 2 | | D1-F7 | AGENTS.md:226-232 | next commands are "`update-shape.py apply …`, `make bootstrap`, `bump-leg.py …`"; every one "carries the exact next command"; "each of those commands has its own section above" | "Every one carries the exact 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"; "Each of those commands keeps its own refusals" | shape-doctor.py:2813 and 2828 (check NEXT), 3075-3089 (`legs` NEXT is `git submodule update`, `make bootstrap` only in the trailing comment), 5044 (holder `members` NEXT); no row's NEXT is a `bump-leg.py` command (`grep -c -i 'bump.leg\|bump_leg' shape-doctor.py` was 0 at `7a8b981d`; since #495 the `pins` row's reason names two `bump-leg.py --to` lines, as text, at shape-doctor.py:1847-1856 and 1904-1922 of `ea634043`); rows with no next command: 1393, 1526-1532, 5758 (`could not be run: {exc}`); only update-shape has a section in AGENTS.md | | D1-F9, D3-F7 | AGENTS.md:344-346, README.md:994-995, and the handbook beside it | after a bump the leg is left detached, and "`make bootstrap` in the root re-places it on its tracking branch AT the new pin; say so in your relay" (README.md: "`make bootstrap` re-places it on its tracking branch AT the new pin.") | AGENTS.md: "`make bootstrap` in the root puts it on its tracking branch AT the new pin, or says why not; relay which."; README.md and the handbook: "...re-places it on its tracking branch AT the new pin, or says why not." | templates/assembly-root/scripts/bootstrap.py at `ea634043`, `checkout_tracking_branch` (393-448): a branch behind the pin is fast-forwarded (275-340), and one ahead of it with no commit of its own is moved back (343-390, 177-187), both since #490 (`7348c57b`); a branch with commits of its own, or one that diverged, is not moved, and bootstrap prints `not moving an existing branch` (444-448); a commit made on the detached leg is left where it is, said as "which is neither" (313-322); a branch another worktree has checked out is not moved, and git's refusal is printed (226-228, 238-243) | | D2-F1 (part) | README.md:175-178, 217-220 | step 4's preflight "prints the command for THIS machine and asks once"; git commands are "the same commands that preflight offers to run for you" | "for `gh`, its login and its credential helper"; "the same commands `setup-project.py --preflight` offers to run" | setup.sh:85 (`command -v git ... \|\| die`, exit 2 by setup.sh:40) and :76; setup-project.py:1829-1831 (`bad(GIT_MISSING); die(PREREQ_MISSING)` ahead of `preflight(opts)`); the git offer (setup-project.py:926-932, rows 787-796) is reached only by `setup-project.py --preflight` (1820-1821); the `gh`, login and `gh auth setup-git` offers at 982-1078; run, no git on PATH: `openRepoShape --preflight` printed `REFUSED: git is not installed`, exit 2, no offer. The third place (README.md:434-436) is routed to #491 | | D2-F2 | README.md:181-183 | `openRepoShape --preflight` "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)" | openRepoShape:446, 564 run setup.sh; setup.sh:76, 85 `die` (exit 2, setup.sh:40) before `preflight_only` (setup-project.py:1142-1166) | | D2-F3 | README.md:269-271 | "add `--visibility private` if you want that" | "`--visibility` defaults to `private`: add `--visibility public` (or `internal`) if you want that" | setup-project.py:428 (`self.visibility = "private"`), usage :138; scaffold-project.py:310-316; run: no `--visibility` printed `[ok] visibility private` | | D2-F4 | README.md:306-308 | the rehearsal leaves behind `./Atlas` and three bare repositories | "...and the scaffold's three working trees, in the temporary directory its `Working trees are at` line names" | scaffold-project.py:1570-1571 (`tempfile.mkdtemp(prefix="openreposhape-")`, never removed), 1611; run: `/tmp/openreposhape-q5eeici3` held `atlas atlas-code atlas-spec` | | D2-F6 | README.md:436-438 | "a hyphenated or lowercase name is refused here" | "a hyphenated project name is refused here" (the review's N1: `Atlas-spec` and `Atlas-code` are hyphenated and pass) | contracts/repository-naming.yaml:335-345 (`^[A-Za-z][A-Za-z0-9]*(?:-spec\|-code)?$`); run: `validate-repository-naming.py --explain atlas atlas-spec atlas-code` exit 0, `At-las` exit 1 `naming-unclassified`; the rehearsal for `atlas` ended `DONE. atlas is scaffolded and bootstrapped.` | | D2-F8 | README.md:573-574 | every other family "is CamelCase" | "from the neutral-product, domain-descendant and family forms, all of which are CamelCase words" | contracts/repository-naming.yaml:412 (`^[a-z0-9]+(?:-[a-z0-9]+)*-wip$`), 182 (`-Install` is hyphenated) | | D2-F9 | README.md:619-620 | the rewrite covers "both HTTPS and SSH leg URLs" | "both HTTPS and scp-form SSH (`git@github.com:`) leg URLs" | templates/assembly-root/.github/workflows/validate.yml:160-161 (two `insteadOf` rewrites) and :69-73 (`*) continue` skips any other spelling); run: `ssh://git@github.com/...` went to ssh unrewritten | | D2-F10 | README.md:625-628 | a leg under another owner "can only be reached via `SHAPE_LEGS_TOKEN`" | "`SHAPE_LEGS_TOKEN` is read only when the App is not configured, so that leg is reachable only with the App secrets removed" | validate.yml:146-154 (`if [ -n "$SHAPE_LEGS_APP_TOKEN" ] ... elif [ -n "$SHAPE_LEGS_TOKEN" ]`); run: both set printed `leg credential source: app` | | D2-F11 | README.md:680-682 | `validate-repository-naming.py --referent-chain openXdox,openDox` asks the same question for one name | `... --pins openXdox --referent-chain openXdox,openDox ...` asks "the same of one name" | scripts/validate-repository-naming.py:522-528 ("The first entry must be in `--pins`"); run: without `--pins`, `codexDox` classified `project-leg/assembly` (exit 0); with it, `domain-descendant` | | D2-F12 | README.md:687-689 | `--link-source [PRODUCT=]<path>`, env, or a checkout beside the root | `--link-source PRODUCT=<path>`, env, a checkout beside the root, then a bare `--link-source <path>` | scripts/repo_shape.py:2589-2621 (`resolve_link_source`, bare path 4th); templates/assembly-root/scripts/validate-pins.py:597-632 uses the same order | | D3-F1 | README.md:736 | "the only change to it is ONE split commit" | "the only change to its history is ONE split commit" | adopt-project.py:5355-5376 (`_set_the_topic` runs `gh repo edit <source> --add-topic` on all three), called at 5734; the topic is not history | | D3-F2 | README.md:764-765 | `plan` "writes it into `follow_ups:`" for every such file | "lists up to eight per directory in `follow_ups:`, counting the rest" | adopt-project.py:1329-1337 (`hits[:8]`, then `… and N more file(s) under the code leg naming ...`) | | D3-F3 | README.md:810-811 | the row reads `code: 0 of N source paths (seeded from template)` | the table's `code` row reads `0 of N source paths (seeded from template)` | as D1-F4; run: ` code 0 of 3 source paths (seeded from template)` | | D3-F4 | README.md:958-960 | `sorted-ls-tree-r-v1` records "joined with LF" | "each followed by LF, the last one included" | scripts/repo_shape.py:904-928 (`digest.update(record); digest.update(b"\n")`); run over `HEAD:templates/code-root`: LF-joined gave `f44766c9...`, LF-terminated and `tree_digest()` both `fca315e6...` | | D3-F5 | README.md:976 | `--dry-run` "prints the move and writes nothing", and (with #484) "`--dry-run` writes nothing, so it runs there" | "# --dry-run prints the move; it fetches into the leg and writes nothing else"; "`--dry-run` fetches into the leg and writes nothing else, so it runs there and prints that command" | scripts/bump-leg.py at `48585e62`: `fetch_leg` is called at :1453 (refspec `+refs/heads/*:refs/remotes/origin/*`, :854) and the `--dry-run` exit is at :1473, so the fetch comes first; the tool says so itself ("--dry-run: nothing was changed. The leg's object store was fetched into, which is how the digest above was computed", :1474-1477; docstring :447). Run (audit, offline): before the dry run the target object was absent from the leg and `origin/main` was f85673e; after it the object was present and `origin/main` was b9bce3e. #484 (landed) lets `--dry-run` run on the tracking branch (:1030 `and not dry_run`) and print `git switch -c`; it kept "writes nothing" at README.md:975 and in the refusal sentence, and the handbook (code comment and paragraph) says the same | | D3-F9 | README.md:1041-1044, AGENTS.md:285-288 | `check` prints `unchanged`, `upstream-changed`, `locally-modified` 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" (README; AGENTS.md appends the same four after `both`: `already-at-target`, `upstream-removed`, `unmapped` and `copy-missing`) | update-shape.py:239-256 (states, `CONFLICTS`, `ORDER`), 541-642 (`_classify_row`, `_row_verdict`) | | D3-F10 | README.md:1059-1063 | a `both` file "is refused outright" | "refused until a human merges it, commits the merge and re-runs with `--accept-local <path>`" | update-shape.py:2515-2535 (`_refuse_conflicts` excludes an accepted `both`), :1582 (`check` says "Merge it by hand, commit it, then `apply --accept-local <path>`"); run: `apply ... --accept-local Makefile` after the hand merge committed, exit 0 | | D3-F12 | README.md:1137-1139 | "every one that is not `ok` names the exact command that fixes it" | "a row that is not `ok` names the exact command that fixes it whenever there is one to run" | shape-doctor.py:1429, 3026, 5337 build `Row(..., None, ...)` with no fix; run: `--json` on a plain directory gave `contents n/a` with no next | | D3-F13 | README.md:1151-1153 | `COMPLIANT`: "every row ok"; one `SHAPE BEHIND` spelling; `DRIFTED`: "a copy was edited here, or a leg is off its pin" | "no row is a `FINDING`"; adds `(pin <a> -> <b>, no copied file differs)`; "edited, deleted or no longer maps onto the standard, or a leg (a member, in a holder) is off its pin" | shape-doctor.py:5942 (`COMPLIANT` is the end of the ladder, `n/a machine` is always there), 5875-5899 (`behind_verdict`), 5775-5795 (`drifted_file_reasons`), 5796-5824 (`drifted_reasons`) | | D3-F14 | README.md:1158 | "Exit 3 is usage or environment" | "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`)" | shape-doctor.py:6029-6051 (plain argparse, exit 2), 6061-6082 (the exit-3 paths, among them `except Refusal: return 3` for the flag), 4547-4553 (`shape-doctor-no-placement-audit`), docstring :227 (`3 usage or environment`); tests/test_shape_doctor.py:5844-5858 asserts `returncode == 3`; runs: `--bogus` rc 2, `--root <missing>` rc 3, `--root <plain dir> --placement-plan pp.yaml` rc 3 | | D3-F15 | README.md:1273-1278 | bare `park` "PARKS EVERY ESTATE it finds" | "lists every estate ... 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 ... 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` | openRepoTools `park` at GitHub's main `f87fd5a` (the review fetched it: nothing newer): header 17-41, usage 77-100, sweep skip at 802-808, `resume` refuses `--all` at 863-867; runs: bare `park </dev/null` outside an estate gave `REFUSED ... park --all parks every estate below without asking`, rc 2; `park --all --dry-run` gave `skipped Solo (no Speckit overlay)`, rc 0 | | MISPLACED (new, #394) | README.md:1176-1180 | `placement` "names the paths the policy would have put somewhere else" | "...and a submodule in a leg other than the one its `submodules:` record names" | since #394 (`7a8b981d`) a submodule in a leg other than the one `project.yaml`'s `submodules:` records for it is `misplaced`, counted in `MISPLACED`, exit 1: shape-doctor.py:3604-3624 (`recorded_placement`), 3754-3769 (`leg_submodules`); tests/test_shape_doctor.py:6770 (`test_a_submodule_in_the_other_leg_from_its_record_is_misplaced`), 7219. AGENTS.md already says it (#394's item 4). The verdict table's `MISPLACED` row sits directly above #468's `INVALID` row, so it is not edited here: #468 is asked for it | | N7 | AGENTS.md:259-262 (item 6) | "`openRepoShape --preflight` is what offers to install one" | "`openRepoShape --preflight` offers only the `gh` set-up" | the preflight never checks git-filter-repo (setup-project.py:1110-1135; openRepoShape:212-216) and setup.sh:85 exits 2 for a missing git before the preflight; the offers are `gh`, its login and its credential helper (982-1078) | `docs/handbook.html` repeats these and is corrected where it does: the preflight offers, `git` and the credential helper (four platform panels), the `openRepoShape --preflight` exit codes, the visibility default, the rehearsal leftovers, the CamelCase sentence, the credential rewrite and `SHAPE_LEGS_TOKEN`, the source repository's one change, the digest definition, `check`'s verdicts and the `both` refusal, the doctor's row sentence, verdict table and exit 3, the placement paragraph's submodule, and bare `park`. The handbook says more than the README in four places (the `git`-before-the-preflight sentences in the Linux panel, the Windows git offer "only under `py setup-project.py --preflight`", glosses for the three non-comparable `check` states, park "from anywhere"); all are true. Its footer's history prose gains "added (#483)". | finding | where | evidence at `71359744`, the merge of `48585e62` | |---|---|---| | D1-F8 | AGENTS.md:323-328, 337-339 | the block carries `git -C <path> switch -c <branch>` (AGENTS.md:335), item 3 says "A new project stands on its tracking branch; the dry run runs there and prints the `git switch -c`, but the real run is refused" (:348-349); scripts/bump-leg.py:1030 refuses on the tracking branch only `and not dry_run`; tests/test_bump_leg_dry_run_tracking.py | | D2-F7 | README.md:502-504 | README.md:502 `git -C ../Atlas switch -c <branch> # the tool will not commit on main` | | D3-F6 | README.md:974-977, 987-992 | README.md:988-991 "`git switch -c <branch>` first; `--dry-run` ... runs there and prints that command" | | D2-F13 | README.md:705-707 | #465 rewrote the sentence: README.md:705-707 now reads "Finding neither is a named SKIP, exit 0: no checkout and no network does not make a descent a lie. A checkout that cannot answer is the finding `neutral-pin-source-unreadable`", and the handbook says the same (1139-1141). True: a source that is found but cannot answer is a FINDING, templates/assembly-root/scripts/validate-pins.py:796; the SKIP is only "no source found and `gh api` fails" | | D2-F14 | README.md:708-709 | #465 deleted the whole sentence, so "a pin file the declaration names but that is not there, is reported as drift" is gone from README.md and the handbook (the missing file is the refusal `neutral-pin-missing`, validate-pins.py:699). It also dropped the true half, "A digest that recomputes differently is reported as drift" (the finding `neutral-pin-digest-mismatch`, validate-pins.py:853): not a false statement; see "Not holding #483 open" below | | D3-F8 | README.md:1028, 1095-1097 | #465 rewrote both: README.md:1027-1029 "a project runs its own gate, which never consults this standard (its validators' one possible network read is `validate-pins.py`'s `gh api` recheck of a neutral product's pin, above)" and :1095-1098 "That line adds no network call ... (Its pins gate may ask `gh api`; see above.)"; the handbook the same (1403, 1448). True: `validate-pins.py` falls back to `gh api` when no local checkout answers (scripts/repo_shape.py:931 `tree_digest_from_gh`) | Evidence is at `ea634043`, the merge of `70d0d1b3` with the D1-F9/D3-F7 correction; `9ac90d16` changes one README word in place and the footer blob, so the line numbers hold at this head. The runs are this round's, offline: local bare remotes, a stub `gh`, a blank identity. | finding | where | evidence | |---|---|---| | D1-F1 | AGENTS.md:22-24, 56-62, 78-80 | #491 reads the yes from the controlling terminal (setup-project.py:1322, `ask()` at 1325-1432; the confirmation at 1770-1779), and setup.sh hands the child the pipe (282-283). Run under a pty: the piped `cat setup.sh \| bash -s -- … --local-remote-dir …` asked `Type yes to continue:`, and `no` gave `REFUSED: not confirmed; nothing was created.`, exit 1. With no terminal at all (AGENTS.md:23, 83-86) it prints the plan and refuses (1770-1776): `REFUSED: this will create THREE repositories and there is no terminal to confirm on…`, exit 2 | | D2-F5 | README.md:315-336, 414-420 | the same read, for the README's three one-liner blocks | | D1-F2 | AGENTS.md:76-77 | #491 took the audit's text (AGENTS.md:73-75). `_choose_org` (setup-project.py:1244-1258) takes a fork's `origin` organisation (run: `[ok] organisation acmefork (from the \`origin\` remote…)`) and refuses opensoft's own without `--org` (run: `REFUSED: you are running from the upstream checkout (…); pass --org <your-org>.`, exit 2) | | D2-F1 (rest) | README.md:434-436 | #491's words, kept whole in the merge (README.md:432-445). The `gh` offers are the install (setup-project.py:992-1005, `INSTALL_OFFERS` at 795-798), the login (1009, 1015-1032) and git's credential helper (1051-1083, asked only once `gh` is ready, 1134-1137); git and Python are refused before any offer (setup.sh:76 and 85, exit 2 by :40). Run with no git on PATH: `openRepoShape --preflight` printed `REFUSED: git is not installed. Install it: https://git-scm.com/downloads`, exit 2, and offered nothing | | D3-F11 | README.md:1130-1133 | #468's sentence (README.md:1130-1134) names `agent files` for a holder and `machine` for a plain directory, as the doctor's `CHECKS` registry has them (shape-doctor.py:6115-6155, at 6135 and 6153). Run, `--json` on a plain directory: the rows naming, contents, way-in, machine | | `MISPLACED` table row (the review's) | README.md:1151, and the handbook's verdict table | #468's row reads "...or a submodule in a leg other than the one its `submodules:` record names" (README.md:1155, handbook:1509); shape-doctor.py:3843-3862, 3993-4008 | Every finding of the three audits is corrected above or settled by a pull request that has landed. A fourth read took what main changed in README.md and AGENTS.md since the audits' `ec4a7a0e` (#394, #484, #465, #468, #491: `git diff -U0 ec4a7a0 70d0d1b`), every line this branch does not itself rewrite, and asked the audits' question by their method. It found one false statement, D1-F9/D3-F7's own sentence as #484 kept it, corrected above, and checked the rest true: AGENTS.md 23, 63-67, 73-81, 83-86, 88-96, 155-158, 252-254, 335, 347-349 and 357-361; README.md 333-338, 343, 432-445, 503, 508, 699-709, 987-992, 1028-1033, 1095-1098, 1118-1136, 1155-1156 and 1163-1171 (line numbers at `ea634043`). Read in code only: the offers' real-GitHub paths and Windows (the real `gh` never ran). Not read: `park` and `resume`, which live in openRepoTools. **True but loose, left as they are.** AGENTS.md:83 and README.md:440 say a run with no terminal "stops after the plan"; a missing prerequisite stops it earlier, at the preflight (exit 2), which AGENTS.md:88 covers. README.md:1216-1218 says the machine check runs "with stdin closed"; the doctor also starts it in a session of its own (shape-doctor.py:1284-1285), which is what keeps the terminal out since #492. AGENTS.md's doctor item 3 does not say to ask the human which of #495's two `bump-leg.py --to` lines to run: an omission, not a false statement. **Outside these files.** `scripts/bump-leg.py:1513` still prints "re-places it on its tracking branch AT the new pin" after every bump. It is on #458's checklist with the rest of #490's follow-ups, and a docs-only pull request leaves it there. **Not holding #483 open: an omission.** #465 deleted "A digest that recomputes differently is reported as drift" together with its false clause. So README.md (698-709) and the handbook beside it no longer say what a recomputed neutral-pin digest that disagrees does. Nothing there is false, so it does not hold #483 open. Both files are at their caps, and the sentence needs about two more README lines that no untouched paragraph gives back, so it is not added here. Re-checked at `ea634043`: the finding is validate-pins.py:968-970 and 1140-1142 (exit 1), the refusal 814-816 and 1172-1175 (exit 2). A sentence for whoever next edits that paragraph: "A digest that recomputes differently is the finding `neutral-pin-digest-mismatch` (exit 1); a pin file the declaration names but that is not there is the refusal `neutral-pin-missing` (exit 2)." A robustness note on main's `test_the_documents_say_what_an_assistant_does_with_no_terminal` (#491): it strips `<...>` from README.md as well as from the HTML page, and a README line holding `<(` and no `>` swallows lines up to the next `>`; stripping tags from the `.html` page only would make that test indifferent to such a line. `git merge-tree --write-tree` of `ea634043` against the open pull requests that edit these files: clean with #503 (`6fe17be5`); #506 (`f184a51c`) and #486-#488's #507 (`708f3bbb`) each conflict in the handbook footer alone (its history prose and `README.md blob` line), and README.md and AGENTS.md merge clean with all three and stay at 1422 and 490 lines. Whichever lands later recomputes `git hash-object README.md` and keeps every "added (#n)" clause. #394, #484, #465, #468 (`57c85e92`), #490 (`7348c57b`), #508 (`aa4e1f59`) and #491 (`da197359`) are merged here. - At this head (`9ac90d16`): `tests/test_repo_hygiene.py tests/test_agents_md_codes.py tests/test_cli_reference.py tests/test_setup_project_py.py tests/test_setup_sh.py`, 277 passed; `scripts/validate-repository-naming.py --explain Atlas Atlas-spec Atlas-code` exits 0 (`project-leg`: assembly, spec, code) and `--explain At-las At-las-spec At-las-code` exits 1 (`FINDING naming-unclassified`), so only the project name's hyphen is refused. - At `ea634043`: `tests/test_repo_hygiene.py tests/test_agents_md_codes.py tests/test_cli_reference.py`, 122 passed (the same on the merge `94793aeb`); `tests/test_setup_project_py.py tests/test_setup_sh.py`, which read README.md and the handbook for #491's no-terminal route, 155 passed; `tests/test_free_plan_hint.py` with the AGENTS.md step 7 tests of `tests/test_adopt_late_failure.py` and `tests/test_adopt_github_exits.py`, 18 passed; and on the merge, `tests/test_bootstrap_after_bump.py`, 23 passed. A stub `gh` first on PATH (it logged no call), `GH_TOKEN` bogus, an empty `GH_CONFIG_DIR`, a blank identity. - `rvD-worddiff.py`, `rvD-hunks.py` and `rvD-mdstruct.py` over `ea634043` against its parent: four `WORDS CHANGED` hunks, all of them the D1-F9/D3-F7 sentence and the blob, and no re-wrap; block structure, links, HTML and code spans identical (README.md 678/678 blocks, AGENTS.md 322/322). The merge `94793aeb` against `70d0d1b3` changes the same lines as `83cfad54` against `aa4e1f59` (366 insertions, 336 deletions), apart from the two resolved hunks. - Review round four (the first reviewer, at `ea634043`): APPROVE WITH NITS (0 blockers, 0 should-fixes, 3 nits). - It ran `git show --remerge-diff` on every merge since round three (`71359744`, `fb6a9ab4`, `83cfad54`, `94793aeb`). Each hand resolution is a named conflict hunk, and no row, clause or fact was lost. - It reproduced the D1-F9/D3-F7 sentence with a real bump and `make bootstrap` in seven cases, and each placed the leg or said why not: a branch behind the pin; ahead with no commit of its own; with commits of its own; diverged; checked out in another worktree; a commit on the detached leg; a move a local change blocks. - It re-checked every routed row and ten moved-evidence rows, and read the whole of main's change since `ec4a7a0e`, finding nothing else false. - It ran the hygiene, codes, CLI-reference, setup, bootstrap-after-bump and free-plan tests: 314 passed. - Its nits: N1, the hyphenated project name, made at `9ac90d16`; N2, a run with no terminal stops at the preflight rather than after the plan when something is missing, which AGENTS.md:88 covers; N3, this description. - Two `bootstrap.py` defects it met while reproducing are on #458's checklist (issuecomment-6142614866). - At `83cfad54`: `tests/test_repo_hygiene.py tests/test_agents_md_codes.py tests/test_cli_reference.py`, 119 passed. On `fb6a9ab4`, the merge of `57c85e92`, the same three gave 119 passed, and `tests/test_shape_doctor.py tests/test_doctor_unfinished_merge.py tests/test_free_plan_hint.py tests/test_openreposhape_command.py tests/test_shape_advisory.py` gave 465 passed, 3 skipped (the doctor's PowerShell line, which prints only on a Windows host). The table's "no row is a `FINDING`" holds with the `merge` row: `shape-doctor.py` returns `COMPLIANT` only at :6171, after :6168-6170 answer `INVALID (<ids>)` for any `FINDING` row, and `merge` is a `FINDING` only for an unmerged path (:5879; a merge with none left is a `note`, :5862). All runs with a stub `gh` first on PATH, `GH_TOKEN` bogus, an empty `GH_CONFIG_DIR`. - At `71359744`, the merge of `48585e62`: the three files, 119 passed (a stub `gh` first on PATH, `GH_TOKEN` bogus, an empty `GH_CONFIG_DIR`; the stub logged no call). - The same three files on a throwaway merge of `f35e59fa` with #491's head (merge-tree, commit-tree, a detached worktree, footer resolved, then removed): 122 passed, and `tests/test_setup_project_py.py tests/test_setup_sh.py -k no_terminal`: 8 passed. The failure the second review found (`test_the_documents_say_what_an_assistant_does_with_no_terminal[README.md]`, 1 failed, 122 passed) came from the `bash <(` lines this commit removes. - `tests/test_adopt_late_failure.py::test_agents_md_step_7_names_the_exit_the_tool_prints` and `tests/test_adopt_github_exits.py -k "step_7 or agents"` (round two, on `ccf520bd`): 2 passed; they read AGENTS.md step 7, which is untouched. - `rvD-worddiff.py`, `rvD-hunks.py` and `rvD-mdstruct.py` over the third commit against its merge parent: every changed region is one of the corrections above (the `bash <(` blocks back to main's text, D3-F5, the park parenthetical, the placement sentence and the three word trims), and the two `WHITESPACE-ONLY` hunks are the family steps going back to main's bytes; block structure 320/320 and 678/678, links, HTML and code spans identical. - No test greps a phrase this pull request changed: the old phrases appear under `tests/` only in history comments and the doctor's docstring. The third head (f35e59f) was re-verified: APPROVE WITH NITS (0/0/4), all four in this description and fixed there. Main 48585e6 (#465) was then merged as 7135974, and 57c85e9 (#468) as fb6a9ab; aa4e1f5 (#508) merged clean as 83cfad5. Main 70d0d1b (#491) was merged as 94793ae, and ea63404 corrects D1-F9/D3-F7. A fourth review round at ea63404 covered every merge since f35e59f (remerge-diff: no row, clause or fact lost), the bootstrap sentence in seven reproduced cases, and the whole docs delta since ec4a7a0: APPROVE WITH NITS (0/0/3), and #483 can close. Its N1, "a hyphenated project name", is 9ac90d1. CI is green on Linux, macOS and Windows, with SonarCloud passing, at 9ac90d1. Main cc8f2da (#376), which landed after its last merge, touches none of its files, and the combined tree passed the docs and setup tests (277 passed). Closes #483 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap
added a commit
that referenced
this pull request
Oct 13, 2026
… text placeholders, so `--id 007`, `--tracking-branch 2026`, `--spec-path true` or `--org null` read back as typed, and `family.py add` quotes a member id alike (#259) (#395) Brett Heap ruled on 2026-10-12: "quote #259". The templates now write each text field a tool fills from a value it was given between double quotes: | template | fields | |---|---| | `templates/assembly-root/project.yaml` | `id`, `elected_on`, `visibility`, `tracking_branch`, the spec leg's `path`, the code leg's `path` | | `templates/assembly-root/contracts/spec-pin.yaml`, `code-pin.yaml` | `submodule_path` | | `templates/family-root/family.yaml` | `id`, `org`, `created_on`, `visibility`, `tracking_branch` | `render` already spells a value between a YAML template's double quotes with `repo_shape.double_quoted` (#284). The single-pass `render` from #316, on main as 34ecbc2, does the same through `YAML_PLACEHOLDER_RE`. So each value reads back exactly. An ordinary value renders as it did before, plus the two quote characters. `family.yaml`'s `org:` is quoted along with the four fields #259 names. It is the same defect in the same file: `family.py init --org 2026` and `--org null` pass `checked_value`, and on c7c02fa the holder then fails `family-org` (#307 lists the `--org null` row). Taking `org:` back out is not a one-line revert. It fails 14 of the new tests (the 8 `family.yaml:org` in-memory cases, and all 6 `family.py init` rows, because every row of `FAMILIES` passes a misread `--org`), and it would also take the `ORG` row out of `TEXT_FIELDS` and put plain `--org` values into `FAMILIES`. The lane keeps `org:` quoted, as the same defect in the same file, unless Brett says otherwise. `scripts/family.py`: `_member_lines` writes each member's own `project.yaml` id into the holder's `members:` block. That id now reads back exactly, so a bare `007` in the row came back as 7, and `validate-family.py` would report `member-id-mismatch` for a member it had just pinned. `_member_id` quotes an id only when the reader would not read the bare text back as the same text. No row whose id reads back as text moves: `irrs` is still `id: irrs`. Two edges: - A legacy row whose bare id the reader misreads moves once. A holder written before this change, by `family.py add` of a member scaffolded with `--id 2026`, holds `id: 2026`. The next `bump`, `add` or `remove` of that holder rewrites it as `id: "2026"`. That is harmless: `validate-family.py` compares with `str()` and passes before and after. - Mixed versions. A `family.py` from before this change, run against a holder this branch wrote, turns `id: "007"` back into `id: 007`, and `validate-family.py` then fails `member-id-mismatch` (the member declares `007`, `family.yaml` records `7`). A current `family.py` that rewrites the holder next then writes that row as `id: "7"`. Nothing in this branch can prevent it: run `family.py` from a current checkout on a holder that has such a member. Left unquoted on purpose: `topic` (`xf-project-<id>`), every `repository` (`<org>/<Name>`; the slash keeps it text), `SHAPE_REPOSITORY` and `DIGEST_DEFINITION` (constants), and the block or list placeholders (`NEUTRAL_PRODUCT_PINS`, `*_NAMING`, `SHAPE_FILES`). **Why.** Every reader of these files uses `repo_shape`'s YAML subset. It reads a bare `007` as 7, `2026` and `20261011` as numbers, `1.5` as a float, `true` as True and `null` as None. The checks on the flags (`PROJECT_ID_RE`, `checked_value`, the leg-path rule) accept every one of these. Reproduced on c7c02fa, with local bare remotes only: - `scaffold-project.py --id 2026|007|true|null|20261011` exits 0, and the project's own `validate-manifest.py` then fails `manifest-id`. - `--spec-path 2026 --code-path 007` fails `manifest-leg-path`, and `validate-pins.py` exits 2. - `--tracking-branch 007` pushes each leg on `007`, and the root's `bootstrap.py` then puts the legs on a local `7` with `fatal: no upstream configured`. `true` and `null` do the same on `True` and `main`. - `adopt-project.py execute` with `--tracking-branch 007` does the same; `--id 007|20261011` gives `manifest-id`; leg paths `2026`/`007` give `manifest-leg-path`. - `family.py init --id 2026|007|true|null|20261011` gives `family-id`; `--org 2026|null` gives `family-org`; `--tracking-branch 007|true|null` reads back as 7/True/None. On this branch every one of those rows reads back as typed, bootstraps onto the typed branch with `origin/<branch>` as upstream, and passes the root's own validators. **Every reader checked.** Every reader reads these fields through `load_yaml`/`parse_yaml` into a dict. None uses a line regex, `split(":")`, `startswith("<field>: ")` or a whole-line rewrite of them: - `templates/assembly-root/scripts/bootstrap.py`: `tracking_branch`, leg `path`. - `validate-manifest.py`: `id`, `elected_on`, `visibility`, leg `path`. - `validate-pins.py`: `submodule_path` against the manifest's `path`. The pin side is read as it stands and the manifest side goes through `str()`, so the two differ in type exactly when a bare value is misread (that is the `pin-path-mismatch` row above). With both lines quoted they are the same text. - `validate-repository-naming.py`: `str(id)` for the topic. - `templates/family-root/scripts/validate-family.py`: `id`, `org`, `created_on`, `visibility`, `str()` member-id compare. - `siblings.py`, family `bootstrap.py`. - `scripts/bump-leg.py`: `tracking`, leg `path`. Its `rewrite_pin` rewrites only `commit:` and `tree_sha256:`. - `update-shape.py`: `tracking_branch_of`. `rewrite_manifest` touches only the `shape:` block. - `scripts/family.py`: `tracking()`, the member's id, and the `members:` rewrite. The member-id writer is the one fixed here. - `shape-doctor.py`: leg `path`, member row `path`, working-clone `str(id)` compare. - `adopt-project.py`: plan reads and `_template_values`. - `setup-project.py`: `family.yaml` `name`/`kind` only. - `scripts/shape_advisory.py`: the single-repository record only. The `str()` fallbacks (`bootstrap.py`, `adopt-project.py`'s `checked_value`) stay, because projects made before this keep their bare lines. **Pins.** The four files are TEMPLATED, not COPIED, so no row of any project's `contracts/shape-pin.yaml` names them. `Atlas` was scaffolded and `family.py init Contoso` was run on c7c02fa and on this branch. Each `contracts/shape-pin.yaml` differs only in `commit:` and `digests.tree_sha256`, which identify the standard itself and change with every commit; every per-file `sha256` row is identical. `update-shape.py check` of the c7c02fa project and holder against this branch reports every copied file `unchanged` (11 and 10 rows): "no copied file differs; `apply` would move the pin alone". **Tests.** - New: `tests/test_template_text_placeholders.py`, 123 tests: - Each of the 13 fields is rendered with each of 8 values the reader misreads (`2026 007 1.5 true null 20261011 ~ False`), and must read back exactly. - 6 `scaffold-project.py` runs, then a recursive clone, `bootstrap.py`, the three validators, and `bump-leg.py`, which refuses on the tracking branch and finds the leg at its path on a branch of its own. - 6 `adopt-project.py` `plan`/`check`/`execute` runs into local bare remotes, then a clone, `bootstrap.py` and the validators. - 6 `family.py init` runs. - One `family.py add` of members whose ids are `007` and `true`. - **All 123 fail on c7c02fa.** - `true`/`null` through adopt: `plan` writes them bare into the plan, and the plan's reader then misreads them. That belongs to the plan writer, #181 (lane 1). Those two rows quote the value in the plan by hand, as #181's writer will, and test `check`/`execute`. - Mutation probes: reverting any one of the 13 quoted lines fails 8 cases of its field. Reverting `_member_id` fails the member test (the rows read 7/True). - The `try/except YamlError` in `_member_id` is guarded by the follow-up commit. The review found that calling the parse directly left the member test passing. Ids such as `[`, `{`, `&a`, `*a` and `!t` reach the `except` (they raise `YamlError` bare), and `_member_id` returns each one exactly, as it does 34 adversarial inputs in all (the review's in-process probe). `test_a_member_id_the_reader_refuses_bare_is_rewritten_between_quotes` (7 cases: `[`, `{`, `&a`, `*a`, `!t`, an unclosed `'` and an unclosed `"`) opens a hand-edited holder whose row holds the id between double quotes, rewrites the `members:` block with `Family.rewritten`, and asks that the id read back exactly and the text come out byte for byte. With the `try/except` removed all 7 fail with the reader's `YamlError`, and with the `except` returning the bare text all 7 fail too; the old member test still passes under the first of those mutants. - 14 existing tests that asserted or edited a bare rendered line now spell it quoted: `test_manifest_validator` ×5, `test_pin_validator`, `test_reference_default`, `test_scaffold_e2e` ×2, `test_setup_project_py`, `test_setup_sh`, `test_family`, `test_shape_doctor` ×2. - Targeted runs on this branch: | files | result | |---|---| | new file + `test_template_quoted_values`, `test_scaffold_e2e`, `test_scaffold_pin_and_reuse`, `test_scaffold_leg_paths`, `test_scaffold_leg_path_collisions`, `test_reuse_visibility`, `test_reference_default` | 405 passed | | the six adopt files | 491 passed, 1 skipped (the MedxEHR clone is not set) | | `test_family`, `test_family_add_refusals`, `test_family_member_path`, `test_manifest_validator`, `test_pin_validator`, `test_bump_leg`, `test_update_shape`, `test_update_shape_pr_base`, `test_setup_project_py`, `test_park_resume_targets`, `test_repo_hygiene`, `test_cli_reference`, `test_setup_sh` | 603 passed | | `test_shape_doctor` | 241 passed, 3 skipped (Windows-only), 1 failed; that one test was fixed, and with its sibling it reran at 2 passed | **The review.** An independent opus review at 233dc71 (the branch's first commit) found no code defect. Its verdict was CHANGES NEEDED (0 blockers, 2 should-fixes, 6 nits) because it asked for this description and its composition counts to be corrected, and they are. Its evidence: - **Readers and rewriters.** Every reader in the shipped tree reads the 13 lines through `load_yaml`/`parse_yaml`, and no line regex or whole-line rewrite touches them. It read `bump-leg.py` `rewrite_pin`, `update-shape.py` `rewrite_shape_pin` and `rewrite_manifest`, `family.py` `rewritten`, the three validators, `siblings.py`, both `bootstrap.py`, the doctor, `setup-project.py`, the workflows, the Makefile and `setup.sh`. No test among main's 17 commits since c7c02fa (through #245's 161c6c9) edits one of these lines bare. - **`_member_id`, the one rewriter changed.** A mutant that writes every id bare fails the member test (the rows read back as `{'IRRS': 7, 'IRSS': True}`). A mutant that quotes every id fails `b" id: irqs" in lines`. So the minimal-churn choice is tested both ways. - **Old projects and holders.** Against a control commit (c7c02fa plus an unrelated README edit), a project and a holder made on c7c02fa give identical `update-shape.py check` and `shape-doctor.py` output, up to the shas. `check --at 233dc71` reports all 11 project files and all 10 holder files `unchanged` and says `no copied file differs`. The doctor's verdict is `COMPLIANT, SHAPE BEHIND (... no copied file differs)` for the control and for 233dc71. This branch's `family.py add` and `bump` on a c7c02fa holder leave the old `atlas` row and the header byte for byte, apart from the legacy-id edge described at the top. - **Tests.** All 123 new tests fail on c7c02fa (`123 failed`) and pass on 233dc71 (`123 passed`). The 14 changed existing tests pass (49 passed). - **Not covered.** Windows and macOS were reviewed by reading and not run: the plan edit and the member-row check are in bytes, and nothing writes to git's stdin in text mode. Of `test_shape_doctor.py` and `test_family.py` the review ran only the changed tests. **Follow-up commit.** A second commit, `3894aa8`, answers two nits from the review, and changes no behaviour. The `_member_id` docstring said "no existing holder's `members:` block moves by a byte", which was false: the next `bump`, `add` or `remove` of a holder written before this change rewrites a legacy `id: 2026` row as `id: "2026"`, harmlessly, because `validate-family.py` compares with `str()`. The docstring now says exactly that, and a grep of every comment and docstring the branch added for "by a byte" and "moves" found one more copy, in the member test's docstring, corrected the same way. The `try/except YamlError` in `_member_id` is guarded by a new test (see Tests), so the new file holds 130 tests. Every count above that says 123 (123 new tests, all failing on c7c02fa and passing on 233dc71) is for the first commit; the follow-up commit adds the 7. The `except` also carries a comment saying which ids reach it. **Composition at the landing head (4984651).** `49846518` is GitHub's update-branch merge of origin/main `cc8f2da4` (#376, a `--trailer` value carried into the commit as typed) into `3220c9ee`. Git reported no conflict, and the merge has no hand edit. #376 changed `scripts/family.py` and `tests/test_family.py`, which this branch also changes. None of the lines it added under `tests/` names a key the templates now quote. The coordinator ran targeted tests on this head, Linux only, before CI: - main's changed test files (`tests/test_commit_trailers.py`, `tests/test_family.py`, `tests/test_bump_leg.py`, `tests/test_update_shape.py`) with `tests/test_family_add_member_id.py`: 421 passed, 1 skipped (a case-insensitive disk only); - this branch's template set (`tests/test_template_text_placeholders.py`, `tests/test_template_quoted_values.py`, `tests/test_scaffold_manifest_values.py`, `tests/test_bootstrap_spelling.py`, `tests/test_bootstrap_after_bump.py`) with the hygiene, CLI-reference and AGENTS.md-codes tests: 652 passed. **Composition at 3220c9e.** `3220c9ee` is GitHub's update-branch merge of origin/main `70d0d1b3` into `e5e768bc`. It brings in three commits: #491 (da19735), in which `setup-project.py` asks at the person's own terminal; #374 (7ab11cf), update-shape's NEXT quoting; and #384 (70d0d1b), adopt's `_spelled` plan values. Git reported no conflict, and the merge has no hand edit. #491 changed `tests/test_setup_project_py.py` and `tests/test_setup_sh.py`, which this branch also changes. A grep of the lines main added under `tests/` for the keys the templates now quote found two hits, and neither reads a rendered template line: `tests/test_update_shape_next_quoting.py` writes its own `project.yaml`, and `tests/test_adopt_question_and_submodule_spelling.py` edits a plan's `- path:`/`question:` entry, which `adopt-project.py` writes bare. The coordinator ran targeted tests on the same tree (`e5a74d19`), Linux only, before CI: - six of main's eight changed test files (the setup, update-shape, `openRepoShape` command and doctor-row tests): 365 passed; - main's other two (adopt's spelling and repo hygiene), `tests/test_adopt_github_exits.py` and nine more files of this branch's sixteen-file set: 943 passed. **Composition, refreshed again 2026-10-13 (head e5e768b).** `6d108d18` merges origin/main `56713e21` into `6cbbac60`. That brings in two commits: 5e02abe, the doctor's and `validate-pins.py`'s gitlink repairs, and 56713e2 (#479), in which adopt's `plan` takes the tracking branch from the source's default branch. Git reported no conflict and `docs/cli.md` auto-merged; the merge has no hand edit. Two kinds of test failed on it, and two commits fix them: - **`7968f902`** changes one expected literal, at `tests/test_adopt_github_exits.py:675`, for `test_a_source_on_another_branch_opens_its_pull_request_against_it[master]` and `[trunk]`. - The test reads the adopted root's `project.yaml`, which this branch's template renders as `tracking_branch: "master"`, so it now expects the quoted line. - The look-alike lines at `:743` and `tests/test_adopt_tracking_branch.py:73-76` read a plan, which `adopt-project.py` writes bare, and they stay as they are. - **`e5e768bc`** adds `git("branch", branch, cwd=source)` to `tests/test_template_text_placeholders.py::test_an_adoption_reads_back_every_value_it_was_given`. - All six rows had failed at `check` with `plan-tracking-branch-missing`: #479 refuses a tracking branch (`2026`, `007`, `1.5`, `20261011`, `true`, `null`) the source does not have, and the test's source had only `main`. - The source now carries each row's branch, in the flag rows and the by-hand rows alike. The test's assertions are unchanged. Targeted runs, Linux only: - On e5e768b: main's seven changed test files, 211 passed; this branch's sixteen-file set, 1223 passed, 2 skipped. - On 7968f90: the same sixteen files gave 6 failed, which are the six rows e5e768b fixes. The scaffold, setup, doctor and family set gave 1094 passed, 22 skipped, and twelve further template-grep files gave 1259 passed. - `render-cli-reference.py --check` is ok. **Composition, refreshed 2026-10-13 (head 6cbbac6).** `d651b594` merges origin/main `aa4e1f59` (#508) into `d526f768`. - **The one conflict.** Git conflicted in one file, `tests/test_adopt_manifest_values.py`, in four hunks, all against #356 (965420b, #293/#294). #356 refuses an `elected_on:` that `election_date` does not read, as `election-date-malformed`, before the validator is asked (`_manifest_values` returns None, adopt-project.py:2258). - **Resolved by what the merged tree prints**, measured with `adopt-project.py plan`/`check`/`execute` into local bare remotes: - `id: "007"` checks clean and `execute` writes it as typed, so it stays in `TEXT_THE_TEMPLATE_NOW_HOLDS`. - `visibility: "true"` gets the validator's `manifest-visibility: visibility is 'true'`. - `elected_on: "20261011"` gets #294's one finding, "the plan's `elected_on:` is the text '20261011'", and the validator is not asked. That row keeps this branch's list, `TEXT_THE_VALIDATOR_REFUSES`, with main's sentence. - Two tests git merged cleanly now ask `said_of`, as main's own rows do: `test_check_refuses_a_text_value_the_validator_refuses` and `test_where_a_misread_rule_judges_the_rest_the_validator_still_answers`. - **Two more failures that no hunk showed**, fixed in the same merge: - `[branch-tilde]` in `tests/test_scaffold_manifest_values.py` was this branch's own `xfail(strict=True)` until #355 landed as 6c2a371, so it XPASSed. The mark is off, and the row asserts `REFUSED scaffold-bad-tracking-branch:`. - #391's `tests/test_bootstrap_spelling.py` and #490's `tests/test_bootstrap_after_bump.py` matched ` path: spec` and `tracking_branch: main` in a scaffolded `project.yaml`, and nine cases failed on the merge. They now match the quoted lines. - **The #507 interplay.** `6cbbac60` switches the holder to `family/members` before the real `add`s in `test_a_member_whose_id_yaml_would_misread_is_pinned_under_that_id`, so the test holds once PR #507's `family-on-tracking-branch` refusal lands. - On the clean merge of 6cbbac6 with #507's head 708f3bb, that file, all 13 `tests/test_family*.py` and `tests/test_shape_doctor.py::test_a_sibling_at_the_right_origin_with_another_id_is_not_the_member` give 686 passed, 19 skipped. - Without the switch line, the member test fails `REFUSED family-on-tracking-branch`. - **Targeted runs on 6cbbac6, Linux only:** - the template, adopt-plan and manifest files (14): 995 passed, 2 skipped; - scaffold e2e, setup, the doctor, leg-path collisions and every family file: 1094 passed, 22 skipped; - the 15 further files a grep for template lines found: 1503 passed; - `render-cli-reference.py --check`: ok. - **Re-verified independently** at d651b59 and 6cbbac6: APPROVE WITH NITS (0/0/3), all three in this paragraph and fixed here. The re-verification also ran the three rows, five mutations, and 26 more files (834 passed; 291 passed, 1 skipped). - **Not run locally:** the full suite, macOS and Windows; CI's three jobs are the check. **Composition, as measured on 2026-10-12.** The merge trees below were computed for 233dc71; the follow-up commit 3894aa8 changes only `scripts/family.py` comments and adds tests. At 15:28Z main was 3ae0109 (#245, #351, #350 and #345 had landed since the merge trees): 3894aa8 merges clean with it, and the review's re-verify ran the family tests on that merge (61 passed). Every sha below is the one it had then; main and the other branches have moved since. `git merge-tree --write-tree --name-only`, run after a fetch with automatic maintenance off on 2026-10-12: - **origin/main 161c6c9**, 17 commits past c7c02fa (the base): clean, tree 7ba8246. #316 (34ecbc2), #317, #318, #311 and #245 were on main by then. - **The 46 other remote branch heads** (the 30 open PR heads and 16 branches with no open PR): clean for 44. Among them #314 (4b8da3d), #266 (3410be7), #355 (a03abcf), lane 1's `fix/adopt-plan-quote-yaml-keywords` (d38e143), `fix/adopt-record-submodule-placement` (70f4d29) and `fix/adopt-late-failure-notice` (9f1778d), #380 (73068a3), and `fix/family-init-values` (14e2d68, #307). The two that conflict, each in the same file as it does with c7c02fa and with origin/main on its own: lane 1's `fix/adopt-root-kept-symlinks` (6c44591, `adopt-project.py`) and #358 `fix/doctor-manifest-kinds-unreadable-pin` (ebffb08, `shape-doctor.py`). This branch touches neither file. - #314 and d38e143 each conflicted with origin/main on their own, in `adopt-project.py`. Merged with this branch, neither gained a conflict. **Composition with #314, which lands before this branch.** Its misread gate (`manifest_misreads`) reads the template, so once the template quotes a field it refuses nothing there, and some of #314's tests expected that refusal. Composing with #314 (landed as 3ef059c), whose misread gate asks the template how a value is written. With `project.yaml` quoting `id`, `elected_on`, `visibility` and `tracking_branch`, the gate refuses nothing on the shipped templates, and 45 of #314's tests flipped on the merge. Forty expected a refusal the value no longer needs. Thirty-four of them now assert that the value is accepted, and the six whose premise was a bare placeholder now assert that no shipped template placeholder is misread. For example: a scaffold of `--id null|true|false|007|2026`, or of `--tracking-branch true|True|TRUE|false|False|null|Null|NULL|007|2026|1.5|1.50|0`, exits 0, and the manifest holds the text it was given. A dry run prints the plan. An id derived from `--project True|Null` is held as typed. `--id=null` reaches `gh repo create`. Adopt's `id: "007"` checks clean and `execute` writes it as typed. The rule's own sentence, remediation and line-break wording are still asserted, on a copy of the standard whose `project.yaml` writes the four lines bare. Four stay refused by #314's validator gate, now in its words about the text: `visibility is 'true'` and `elected_on is '20261011'`. One is a real break. `--tracking-branch=~` reads back as `~`, which git refuses as a branch name, and it dies at `git init -b ~`, after `gh repo create`. That is #283, so the row is `xfail(strict=True)` with that reason, and it turns XPASS (and fails) once PR #355's `git check-ref-format --branch` refusal lands. `--help` for `--id` and `--tracking-branch` no longer says such values are refused, and `docs/cli.md` is regenerated. **Composition with the other branches.** Where another branch's rules meet this one, the tests were run on the merged tree. Quoting changes what some of their tests see, and the changes are not all one kind: some tests expected a refusal the value no longer needs, some are still refused under another rule, and some guard a case quoting does not fix. Whoever lands second must tell them apart, so each branch's flips are classified below. - **#266** (lane 2, `fix/leg-path-yaml-scalars`). 27 flips, all from one cause: its rule asks a hard-coded bare line, `LEG_PATH_LINE = "path: {}\n"`, so it refuses leg paths the quoted templates now read back. 15 are `test_the_rule_agrees_with_every_template_line_that_holds_a_leg_path` (null, Null, NULL, ~, true, True, TRUE, false, False, FALSE, 2026, 007, 0, 1.5, 1.50) and 12 are this branch's own end-to-end tests (6 scaffold rows fail with `REFUSED scaffold-bad-leg-path`, 6 adopt rows with `FINDING plan-bad-leg-path`). Counted on the merge with b2b42f8. #266's head was then 3410be7, whose rule reads each template line that holds a leg path, renders the value into it as `render` does and asks the reader, so it has no hard-coded line left. The merge with this branch was clean (tree 1e348c6); that head was read and the 27 were not re-run on it. At b2b42f8, whichever of #266 and #259 landed second had to drop or re-point the bare rule; 3410be7 re-points it already. - **Lane 1's #181** (`fix/adopt-plan-quote-yaml-keywords`, d38e143). Its gate reads the template too, so `plan --tracking-branch 2026|007|true` exits 0 on the composed tree (exit 2 `adopt-manifest-misread` on d38e143). On the merge, `tests/test_adopt_plan_yaml_quoting.py` and this branch's 6 adopt end-to-end tests give 70 failed, 371 passed: - 57 refusals that are now unnecessary: `plan_refuses` ×14, `plan_names_an_id` ×2, `check_names_an_id` ×4, `check_reports` ×14 and `execute_refuses` ×14 (every value except `elected_on` and `visibility`), `text_the_loader_reads_as_other_text` ×7, and the two tests that need a bare placeholder (`test_the_gate_follows_the_loader_when_the_loader_learns_a_keyword`, `test_the_template_line_says_whether_a_value_is_quoted`). - 11 guards #181 loses. See "Do not flip". - 2 that fail only by cascade: `execute_refuses[pins='null']` and `[pins='True']` pass alone (`2 passed`). An earlier case no longer refuses, and its `execute` pushes the split branch. - The writer half of #181 (`y` asking the reader, so `true`/`null`/`True` are quoted in the plan) is still needed and composes cleanly. With it, `plan --tracking-branch true` reaches the quoted template intact, and this branch's two hand-quoted adopt rows become flag-driven. - **Do not flip these 12 to "accepted".** Each still needs a refusal or a finding, and quoting does not supply it: - **`~` (1, #314).** `test_a_value_the_reader_misreads_is_refused_before_anything_is_made[branch-tilde]`. `--tracking-branch=~` is not a branch name git accepts, and #314 refused it only as a misread. On the merge it passes the plan and dies at `git init -q --bare -b ~ .../Atlas-spec.git` with `fatal: invalid initial branch name: '~'`, and against GitHub that is after `gh repo create` has made all three repositories. That is lane 2's #283 (PR #355). `~` must stay refused, by #283's ref-name check, which is why #314's row for it is not flipped to "accepted". - **A hand-edited plan value that is not text (7, #181).** `test_check_names_a_plan_value_that_is_not_text_by_what_was_read[tracking_branch='007']`, `[tracking_branch='TRUE']`, `[tracking_branch='1.50']`, `[tracking_branch='[a]']`, `[visibility='{a: b}']`, `[id='007']` and `[elected_on='20260902']`. The plan's reader reads each as something other than text, and on the merge `check` passes. Measured on the merge: `tracking_branch: 007` and `id: 007` are adopted silently as `"7"` with every validator passing; `visibility: {a: b}` pushes both legs and then fails `manifest-visibility`; `tracking_branch: [a]` is still refused at `execute` (`unsafe-value`) before any leg exists. On d38e143 alone, `check` exits 1 `plan-manifest-misread` for each of those four. Re-point the gate at "a plan value that is not a string" and these keep their guard. - **A quoted value the validator refuses (4, #181).** `test_check_reports_a_plan_carrying_one[elected_on='20260902']` and `[visibility='true']`, and `test_execute_refuses_a_plan_carrying_one_before_any_leg_exists[elected_on='20260902']` and `[visibility='true']`. The value stays invalid for the validator (`DATE_RE`, the visibility choices), and #181's misread gate caught it only incidentally. On the composed tree `check` passes and `execute` pushes the legs, and the gate fails afterwards. That is what main does today, and #314's validator gate (`plan-manifest-invalid`) would catch it. - **main** (merge with ab623dd, tree cdeec7f, run by the review): this branch's new file, `test_adopt_template_values.py` (#270), `test_template_values_single_pass.py` (#316) and `test_template_quoted_values.py` give 431 passed, 1 skipped. The skip is `test_adopt_template_values.py:223`, which reads `git config user.name` and skips where the shared repository's `.git/config` sets one; it is environmental. The changed tests with `test_family_add_refusals.py` (#317, #318), `test_pin_validator.py` and `test_manifest_validator.py` give 132 passed. #270's `unwritable_reason` is value-based, not template-relative, so it refuses as it did and nothing flips. Main then added #245 (`adopt-project.py` and `repo_shape.remote_key`), which adds no test that edits a bare quoted line; it was not run on that merge. - **#380** (lane 3, `fix/pin-rewrite-indentation`; it landed since as 1f90a0f). It rewrites `commit:` and `tree_sha256:` only, and its tests render the pins from the templates. On the merge at dc0fa7b, `test_pin_rewrite_layout.py` and the 6 scaffold end-to-end tests gave 121 passed. Its head was then 73068a3; that merge was clean and was not re-run. - **#397** (`fix/family-init-values`, #307; measured at 14e2d68): its tests skip themselves where the template quotes. 137 passed, 19 skipped, 0 failed. With its fix-round head abb27e6, this branch's 3894aa8, main 0f67f7b and #345 together: 442 passed, 20 skipped (the 19 rows, and one Windows-only row). - **#345** (run at 875d802; it has since landed as 3ae0109): `test_family_dry_run.py` and the member test, 22 passed. Its `" id: irrs"` assertion holds, because ordinary ids stay bare. **Left out.** - The adoption plan writer `y` still writes `true`/`null` bare into the plan. That is #181 (lane 1). - `templates/assembly-root/contracts/neutral-product-pin.yaml` still writes `product`, `source_repository` and `digest_source` bare, and `family.py` writes a member row's `project:` bare. Neither is a #259 field. - A plan value someone edits to a non-text type (`tracking_branch: 007` or `id: 007`, unquoted, in the plan) is read by the plan's reader as the number 7. `str()` makes it `"7"`, and the quoted template now writes it faithfully. For `id` this is a change: on c7c02fa a bare `id: 007` failed loudly (`validate-manifest: manifest-id`, manifest line `id: 7`), and on this branch it is adopted silently as id `"7"` and every validator passes. `tracking_branch: 007` was already adopted silently as `7`. The plan side is not this branch's: the guard against it belongs with #181 (adopt plan writes YAML keywords and non-text values unquoted), whose gate should refuse a plan value that is not a string. The case is narrow, because `plan --id 007` itself writes `id: "007"`; only a hand edit reaches it. - No docs change: README, AGENTS.md, the handbook and the template comments show none of these lines; `docs/cli.md` is unchanged because no `--help` text changed. Reviewed adversarially at 233dc71 (CHANGES NEEDED: 0 blockers, 2 should-fixes, 6 nits; the code and the tests were sound, and the should-fixes were the composition counts and the claim about a hand-edited plan `id`). Fix round 3894aa8 changes no behaviour: it corrects the description and the two false "moves by a byte" claims, and guards `_member_id`'s `except YamlError` with a test of 7 cases. Its re-verification at 3894aa8 found every finding handled: APPROVE WITH NITS (0 blockers, 0 should-fixes, 1 nit: shas that had moved since the description was written, refreshed here). What was added after that re-verification, main and the composition with #314, is described under "Composition with #314". An independent re-verification of that composition at 77cd0b2 confirmed it with nits only (0 blockers, 0 should-fixes, 5 nits): the 45 flips reproduce test for test, breaking the real rule or the real template's quoting turns the compose tests red, and the `[branch-tilde]` row XPASSes on a merge with #355. **Composition with #381.** #395's quoted `id: "{{PROJECT_ID}}"` composes with #381's `member-duplicate-id` test (`tests/test_family_add_member_id.py`), whose id rewrite now matches the id bare or quoted and keeps its quoting, so the full suite, which failed that one test on Linux at 1ef3310, no longer fails `test_a_member_whose_id_changes_between_the_probe_and_the_mount_is_unwound` on Linux. CI is green on Linux, macOS and Windows, with SonarCloud passing, at 4984651 (main cc8f2da merged in by update-branch). Main af01068 (#505), which landed after, touches none of its files, and the combined tree passed the template, hygiene, CLI-reference, AGENTS.md-codes and setup tests (807 passed). Closes #259 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap
pushed a commit
that referenced
this pull request
Oct 13, 2026
… list of mappings, so #386's gate asks it only whether it is a list, in #273's own finding (#386) Main since #356's squash 965420b changed `adopt-project.py` three times: 70d0d1b (#384, `_spelled` in three findings), 56713e2 (#493, the tracking branch from the source's default branch) and aa4e1f5 (#508, lane 5's #273 and #339). aa4e1f5 makes `plan --pin` take `product@<40 hex>` and write each pin as a MAPPING of `product:`, `repository:`, `commit:`, `tree_sha256:` and `digest_source:`, and its `_read_pins` judges each `pins:` entry beside the entries: `plan-pin-unrecorded` for a name alone, `plan-pin-malformed` for anything else `plan` never writes there, and for a `pins:` that is no list. Two hunks conflicted, both in `adopt-project.py`, both the ones aa4e1f5's message names: `render_plan`'s `pins:` line takes main's `_pin_lines`, which writes the product through `y` and the commit and digest quoted, so this branch's `y(pin)` is superseded; and `Plan.pins` takes main's `_pin_products`, which reads a list only. The rest is the reconciliation aa4e1f5 and lane 5's comment on #435 asked of whichever landed second. As this branch stood, its item check took only text, so every plan `plan --pin` now writes was `plan-bad-pins` in its own `check`: - `_plan_key_findings` no longer judges `pins:` entries: they are `_read_pins`' alone, in its words and codes. - A `pins:` that is no list is still asked first, beside the other four keys and before the work directory, because the naming policy reads the root's products from it. Its finding is `_read_pins`' own `plan-pin-malformed`, so it is one finding under one code, and `plan-bad-pins` is gone. - `PINS_REMEDIATION` names the mapping `plan` writes. - `PLAN_KEY_REFUSAL` begins "no leg repository was created and nothing was pushed", the words every refusal before a leg says (#445; conftest's `refused_before_any_leg` asserts them, and #273's not-a-list row now meets this refusal). `tests/test_adopt_execute_refusals.py` says the same: the `pins:` key rows expect `plan-pin-malformed` and gain a single mapping with no list around it; the entry rows move to a test that the gate is silent and `_read_pins` names them; the controls and the corrected end-to-end plan are whole pins; and the round trip runs `plan --pin <name>@<commit> --pin-source` for 2026, 007, 1.5 and openGlass, and holds `check` to `plan ok`. `git diff --stat cc8f2da` of the result lists only `adopt-project.py` and `tests/test_adopt_execute_refusals.py`. Before this commit: `tests/test_adopt_execute_refusals.py` 150 passed and `tests/test_adopt_pins.py` 70 passed. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap
added a commit
that referenced
this pull request
Oct 13, 2026
…s a runnable `scripts/family.py`, printed commands quote paths, and `add`, `bump` and `remove` refuse the tracking branch and print their branch's push (#488, #486, #487) (#507) Three defects from lane 3's v1.0.0 acceptance pass of AGENTS.md's "Creating a family, and adding a member", one commit each, in this order, then one review commit for #486 and one for #487. All three change `scripts/family.py` and `add`'s NEXT line, so they are one pull request. **What was wrong.** - AGENTS.md spelled the command `python3 scripts/family.py init --org <org> --family <Name>`, a path that runs from the standard's root. `--into` defaults to where the person stands. - So the command, followed exactly, printed `landing <standard>/Orbit/Orbit` with no word, and `make siblings` would then clone every member into the standard's checkout too. - `init`'s NEXT STEPS printed `python3 family.py add …`, built from `Path(__file__).name`. From the root that fails `can't open file`, exit 2. **What changed** (commit 1). - With neither `--into` nor `--work-dir`, `init` refuses a landing inside a checkout of openRepoShape: `family-landing-in-standard`. A checkout is a directory holding `scripts/family.py` beside `templates/family-root/`, found in the landing's ancestry, so another clone or an install counts too. - The refusal comes before anything is created, under `--dry-run` alike. - The remedy names `--into ..` from the root. From deeper inside the checkout it names the relative path that reaches the checkout's parent. - An explicit `--into` or `--work-dir` is not second-guessed. `family-holder-already-here` is still asked first. - NEXT STEPS, and the `family-holder-already-here` remedy, name this file relative to the directory `init` ran in. From the root that is `scripts/family.py`, as setup-project.py, validate-family.py, AGENTS.md and README spell it. It is absolute where no relative path exists. - AGENTS.md's init command passes `--into ..`, as README's already does, and step 1 says why. The edit is line-neutral: step 1 is one line shorter. - `init --into --help` says it too, and docs/cli.md is regenerated. **How tested.** `tests/test_family_init_landing.py`, 9 tests: - the old command from the root is refused, as a dry run, so nothing can land in the checkout; - a real run inside a fake checkout creates nothing; - the remedy from a subdirectory names the right parent; - an explicit `--into .` or `--work-dir .` passes; - outside a checkout the default is unchanged; - AGENTS.md's line, as written, lands beside the checkout; - the printed NEXT runs, both from the root and from elsewhere. On origin/main code 6 of them fail. Removing the refusal turns 3 red, and putting `Path(__file__).name` back turns 2 red. **What was wrong.** `family.py` and the holder's `siblings.py` printed every path in their pasteable commands bare. Under a holder at `…/my work/Sonata/Sonata`: - the `member-git-dir-cached` remedy `rm -rf <store>` split into `rm -rf …/my`, an absolute directory that is NOT the store, and a relative `work/…`; - `init`'s NEXT STEPS and `add`'s NEXT split `--family-root` and `git -C` (`fatal: cannot change to '…/my'`, exit 128); - the `mv` lines for a misnamed family folder pasted as four arguments. **What changed** (commit 2, and the review commit after #487's). - `scripts/repo_shape.py` gains `shell_quoted(value, platform=None)` and `shell_line(words, platform=None)`, in a new section at its end, the library `family.py` already imports. - It is the doctor's `quote_arg` rule (#101/#102): the same two alphabets, and the same two escapes, `sh`'s `'\''` and PowerShell's doubled apostrophe. The dialect comes from `os.name` or a `platform` argument. An ordinary word is left bare. - It does not flatten to ASCII, as the doctor's report does by contract. A command that deletes or moves a directory has to name that directory. So PowerShell's typographic single quotes are doubled too. - A word that begins with `=` is quoted, in both dialects. zsh, macOS's login shell, expands a bare `=word` to a command's path (`=ls` reaches the command as `/bin/ls`). This is the one place it differs from the doctor's rule on ASCII. - `family.py`: - `init`'s NEXT STEPS print the `add` on ONE line. It used to paste only into a POSIX shell, because of its `\` continuation. `cd <holder>` and `make siblings` are two lines, with no `&&`, which Windows PowerShell 5.1 rejects between two statements. - Those lines and the `family-holder-already-here` remedy quote every path. - `add`'s NEXT quotes `git -C <holder>`, and `member-path-taken` quotes each spelling it names. - `member-git-dir-cached`, and `remove`'s note about the same store, print the directory alone on its own line. Then comes `rm -rf` with its one argument quoted for the POSIX shell `rm -rf` belongs to, and the sentence names that shell: Git Bash on Windows. PowerShell refuses `-rf` and deletes nothing. - `siblings.py`: the parent-folder warning's `mv` and `mkdir -p` lines are quoted for a POSIX shell with the standard library's `shlex.join`, and the warning says which shell. It does NOT import the new quoter: a holder that kept its own `repo_shape.py` through `update-shape.py apply --accept-local` takes the new `siblings.py` without it, and an import of a name its copy lacks stopped `make siblings`, `park` and `resume` with an ImportError that `check` and the validator never reported. - `setup-project.py`'s family hand-over quotes the holder in its `add` line (and names the branch first, see #487). It runs before any checkout of the standard exists, so it carries its own copy of the rule, and a test holds that copy to `shell_quoted`. **How tested.** `tests/test_family_printed_quoting.py`, 69 tests: - The quoter round-trips through `shlex.split`, and through a PowerShell reader, for a space and an apostrophe, `é` and a typographic apostrophe, shell syntax, the empty string, and every printable ASCII character alone and before, inside and after a word. - The host's own `sh`, `bash` and `zsh` hand each quoted word back byte for byte through `printf`, a leading `=` among them (skipped where a shell is absent, and on Windows). - It agrees with the doctor's `quote_arg` on every one of those ASCII values but a leading `=`, and its two alphabets are the doctor's two patterns. - Under a holder in `my work` and in `Dana's work`, each of these splits into the argv it means: `init`'s NEXT (with `cd` and `make siblings` on their own lines), `add`'s NEXT, the cached-store remedy and note, the `mv` lines, and setup-project's `switch` and `add`. - The `rm -rf`, run as split, deletes the store, and the decoy `<tmp>/my` survives. - The `mv` lines, run as split, move the holder. - The holder's own `siblings.py` runs `--help`, the misnamed-folder warning and `--make park` beside a `repo_shape.py` that lacks every name this change added. - Three existing tests' expected lines follow the new wording. Mutations, each turning tests red: - a bare quoter: 35 red; - bare `mv` lines in `siblings.py`: 3 red; - a bare `rm -rf`, a bare `add` NEXT, a bare `init` `add` (round 1, on the 85-test file): 2 red each; - `siblings.py` importing `shell_line` again: 1 red (the older-`repo_shape.py` test); - `%` dropped from the POSIX alphabet: 2 red (the every-ASCII agreement and the alphabet test). The round-1 sample test passed it, 36 of 36; - a leading `=` left bare: 10 red, among them the real-`zsh` test; - `cd … && make siblings` back on one line: 2 red; - setup-project's holder unquoted: 2 red. **What was wrong.** - AGENTS.md says each act's ONE commit lands as a pull request, and no step made the branch that pull request needs. - After `init` the holder stands on `main`, and all three acts committed straight onto it. - `add` printed `NEXT git -C <holder> push -u origin <branch> && open a pull request`. No branch is called `<branch>`, so the push failed: `src refspec … does not match any`. - `bump` and `remove` printed no NEXT at all. - `bump-leg.py` refuses exactly this for a leg (`bump-leg-on-tracking-branch`). **What changed** (commit 3, and the review commit after it). - The real run of each act refuses the holder's tracking branch, the one `family.yaml` declares (`family-on-tracking-branch`), and a HEAD on no branch (`family-holder-detached`, which is what `bump-leg.py`'s `bump-leg-root-detached` refuses too). - The check comes after the refusals about the arguments and the holder, and before the member is read, fetched or cloned, and before anything is written. - The remedy is a quoted `git -C <holder> switch -c <branch>`. The branch is `family/add-<project>`, `family/bump-<id>-<sha12>` or `family/remove-<id>`, and a name that is not a plain branch word becomes `member`. - `--dry-run` runs on any branch. On a work branch it prints where the commit would land. On the tracking branch it prints that the real run refuses there, with the refusal's code, and the `switch` command on a line of its own. The module's EXIT CODES paragraph names this as the one refusal a dry run does not make. - Each act that commits ends with ONE pasteable line: `NEXT git -C <holder> push -u origin <the branch it committed on>`, quoted, then `then open a pull request against <tracking>; never push to it directly`. `bump` and `remove` gain that NEXT. - `init`'s NEXT STEPS open with the `switch` before the `add`. - `add`, `bump` and `remove` `--help` say the branch rule, and that each ends by PRINTING the push; docs/cli.md is regenerated. - AGENTS.md's family block gains the `switch` step before the `add`, and step 3 says the tracking branch is refused, as `family-on-tracking-branch`, and that the branch is pushed as its NEXT line says. README's InkRouter block gains the same step, and its paragraph says it. Both edits are line-neutral: AGENTS.md tightens steps 2 and 5, and README tightens the `make bootstrap` paragraph. `docs/handbook.html` mirrors the block and the sentence, and its footer names #487 and the new README blob. - The holder's own docs, `templates/family-root/AGENTS-shape.md` and `templates/family-root/README.md`, open their add/bump/remove block with the `switch` and say the act refuses the tracking branch. `setup-project.py`'s family hand-over prints the `switch` before its `add`. - Test fixtures that committed on `main` now commit on a branch: - `tests/test_family.py`'s module fixture grows the family on `family/members` and pushes that branch to the remote's `main`, as the merged pull request would leave it; - `test_shape_doctor.py`'s holder fixture and `test_family_add_excludes.py`'s `empty_holder` switch to a branch; - `test_family_add_refusals.py` changes in four places: the `NEXT` expectation, `unborn_holder`, the empty-family test, and the corrupt-HEAD test. In that last test, the tag state's branch check is answered in-process, because the test is about the rollback's reading of HEAD. **How tested.** `tests/test_family_tracking_branch.py`, 17 tests: - For each act, on `main`: refused, with the holder byte for byte as it was, and for `add` nothing cloned. Then the printed `switch`, run as split, makes the branch; the act goes through on it; and its printed NEXT, run as split, pushes the branch, which the remote then holds at the act's commit. - A detached HEAD is refused. - Dry runs on `main` and on a branch say what they should. - A family made with `--tracking-branch trunk` refuses `add` on `trunk`; on a branch named `main` the dry run says the commit lands there for a pull request against `trunk`, the real run commits, and its NEXT pushes `main`. - The suggested names pass `git check-ref-format --branch`. - `init`'s NEXT STEPS open with the `switch`. - AGENTS.md, README.md and the holder's two docs show the `switch` in the same block as, and before, the first `add`. Mutations, each turning tests red: the branch check letting `main` through, 4 red; the NEXT line naming `<branch>` again, 3 red; `init`'s NEXT STEPS without the `switch`, 1 red; the tracking branch hard-coded to `"main"`, 1 red (the `trunk` test; every family test passed it before); the holder docs without the `switch`, 2 red; setup-project without the `switch`, 2 red. An independent review at 10c93de gave CHANGES NEEDED (0 blockers, 1 should-fix, 6 nits). The should-fix was a holder that kept an older `scripts/repo_shape.py` through `apply --accept-local`: its new `siblings.py` could not import the shared quoter, so `make siblings`, `park` and `resume` died. The round at bb11b63 fixes it and all six nits, as described above. Re-verified at bb11b63: APPROVE WITH NITS (0 blockers, 0 should-fixes, 3 nits), with SonarCloud's quality gate OK and 0.0% new duplication. The nits were the holder README's pointer to the push (now "as its `NEXT` line says"), two Sonar smells (S1192: `refs/heads/` is now `HEADS_PREFIX`; S3415: the two expected-first assertions now put the actual value first, as the file's other 32 do), and this description's list of other pull requests. All three are fixed at 3451a56: commit 11ae0a8, then a clean merge of main 994be39. The family set gave 566 passed, 8 skipped there. GitHub's update-branch then merged main cac6bb2 as 8e1471c, and main aa4e1f5 (#508, after #490, #474 and #468) was merged as 708f3bb. There only the `docs/handbook.html` footer conflicted: it takes main's clauses, #468's (#424) included, with the README blob recomputed, and `docs/cli.md` auto-merged and passes `--check`. Main 47b1e06 (#505 and #395, after #376, #374, #384, #495, #493 and #491) was then merged as fcbc7a2, again with only the footer in conflict: main's clauses stay in main's order, this branch's (#487) moves to the end, after (#483), and the README blob is recomputed. Every run used `/usr/bin/python3 -m pytest -q -p no:cacheprovider`, with a stub `gh` first on PATH, a bogus `GH_TOKEN`, an empty `GH_CONFIG_DIR` and git's identity blanked. Run on Linux. At e42ef74 (main d41cc29, #402, merged in by GitHub's update-branch), on its tree 655c927: the first set listed below for fcbc7a2, plus every `tests/test_update_shape*.py`, gave **1242 passed, 22 skipped**; the second set, plus `tests/test_leg_path_yaml_scalars.py`, `tests/test_ruleset_hint_tracking_branch.py`, `tests/test_scaffold_reuse_seeded_legs.py`, `tests/test_scaffold_pin_and_reuse.py` and `tests/test_doctor_validator_rows.py`, gave **1260 passed, 12 skipped**. The skips are the same as at 5a2776d, below. At 654df49 (main 8cd7dbe merged in, with #506 and #347): the first set listed below for fcbc7a2, plus every `tests/test_update_shape*.py`, gave **1242 passed, 22 skipped**; the second set, plus `tests/test_leg_path_yaml_scalars.py`, `tests/test_ruleset_hint_tracking_branch.py`, `tests/test_scaffold_reuse_seeded_legs.py` and `tests/test_scaffold_pin_and_reuse.py`, gave **1231 passed, 12 skipped**. The skips are the same as at 5a2776d, below. At 5a2776d (main f391e58 merged in, with #266, #509, #496 and #435): the first set listed below for fcbc7a2, plus every `tests/test_update_shape*.py`, gave **1213 passed, 22 skipped** (the template-quoting skips below, and three NTFS-junction cases, Windows only); the second set, plus `tests/test_leg_path_yaml_scalars.py`, gave **1171 passed, 12 skipped** (the four Windows-only skips below, and eight in that file, main's new one, whose cases the quoted templates (#259) leave with nothing to refuse). At 22733ce (main 631f087, #503, merged in by GitHub's update-branch): the first set listed below for fcbc7a2, plus every `tests/test_update_shape*.py`, gave **1213 passed, 22 skipped** (the template-quoting skips below, and three NTFS-junction cases, Windows only); the second set gave **944 passed, 4 skipped**. At fcbc7a2 (origin/main 47b1e06 merged in, with #505 and #395): - every `tests/test_family*.py`, `tests/test_validate_family*.py`, `tests/test_park_resume_targets.py`, `tests/test_free_plan_hint.py` and `tests/test_template_text_placeholders.py`: **844 passed, 19 skipped** (`tests/test_family_init_values.py:219` and `:291`, which skip because the template now quotes the placeholder and `id:`). - `tests/test_repo_hygiene.py tests/test_agents_md_codes.py tests/test_cli_reference.py tests/test_shape_doctor.py tests/test_setup_project_py.py tests/test_setup_sh.py tests/test_template_quoted_values.py tests/test_stdout_encoding.py tests/test_windows_paths.py tests/test_commit_trailers.py`: **944 passed, 4 skipped** (the doctor's PowerShell line and `tests/test_stdout_encoding.py:286`, Windows only). At 708f3bb (origin/main aa4e1f5 merged in): - every `tests/test_family*.py`, `tests/test_validate_family*.py`, `tests/test_park_resume_targets.py` and `tests/test_free_plan_hint.py`: **718 passed, 8 skipped** (`tests/test_family_init_values.py:242`). - `tests/test_repo_hygiene.py tests/test_agents_md_codes.py tests/test_cli_reference.py tests/test_shape_doctor.py tests/test_update_shape_landing.py tests/test_doctor_unfinished_merge.py tests/test_template_values_single_pass.py tests/test_stdout_encoding.py tests/test_windows_paths.py`: **813 passed, 4 skipped** (the doctor's PowerShell line and `tests/test_stdout_encoding.py:286`, Windows only). Earlier, with origin/main 48585e6 merged in: - `tests/test_family*.py tests/test_validate_family*.py tests/test_stdout_encoding.py tests/test_windows_paths.py tests/test_repo_hygiene.py tests/test_agents_md_codes.py tests/test_cli_reference.py tests/test_park_resume_targets.py tests/test_bootstrap_spelling.py tests/test_free_plan_hint.py tests/test_template_values_single_pass.py`: **1303 passed, 9 skipped**. - `tests/test_shape_doctor.py tests/test_update_shape*.py tests/test_commit_trailers.py tests/test_tracking_branch_ref.py tests/test_scaffold_ignored_shape_files.py tests/test_ruleset_hint_tracking_branch.py tests/test_bump_leg_dry_run_tracking.py tests/test_setup_project_py.py`: **999 passed, 6 skipped**. - The new files alone: `test_family_init_landing.py` 9, `test_family_printed_quoting.py` 69, `test_family_tracking_branch.py` 17, all passed. The full suite was not run locally. CI's three jobs are the check. #505 and #395 have since landed, and both are merged here at fcbc7a2. Each of #505's family sentences was read against this branch's code, and none is false: AGENTS.md 3 and 233; README 569, 822, 1152, 1269 and 1348; the handbook 1126 and 1506. #395 landed with its test switching to `family/members` before its real `add`, as the last bullet below says, and the first run above includes it. #503 then landed as 631f087, and GitHub's update-branch merged it here as 22733ce with no conflict. Main f391e58 (#266, #509, #496 and #435) was then merged by hand as 5a2776d, again with only the footer in conflict: main's clauses stay in main's order, this branch's (#487) moves after #509's (#498), and the README blob is recomputed; `git show --remerge-diff` shows nothing else. #509's reworded sentences in AGENTS.md, README.md and the handbook were read beside this branch's, and none contradicts or repeats another. Its `setup-project.py` changes (the clone-path check, the plan line, the step-(7) backstop and `_main`) and this branch's `shell_word` lines in the hand-over do not meet, and `docs/cli.md` passes `--check`. Main 8cd7dbe (#506 and #347) was then merged by hand as 654df49, again with only the footer in conflict: this branch's (#487) moves after #506's (#462), the README blob is recomputed, and `docs/cli.md`, which both sides changed, still passes `--check`. #506's two `scripts/family.py` hunks (the `FAMILY_RULESET_HINT` import, and its print when the holder's push is refused) sit unchanged inside this branch's code, and that hint prints no path. Under `--reuse-empty-repo`, `init` still refuses its default `--into` inside a checkout of this standard (`family-landing-in-standard`) before it reads the remote, as a dry run with a stub `gh` showed. #506's sentences in AGENTS.md, README.md and the handbook are still true beside this branch's. Main d41cc29 (#402, for #277), which rewrote the doctor's NEXT lines and `tests/test_shape_doctor.py`, was then merged by GitHub's update-branch as e42ef74, with no conflict; the `holder` fixture still switches to `family/members` before its `add`. AGENTS.md stays at 490 lines and README.md at 1422. `git merge-tree --write-tree` of 708f3bb, at 04:47Z on 2026-10-13: - **Clean:** #503 (`fix/family-makefile-without-siblings`) and #395. Each merged tree keeps AGENTS.md at 490 lines and README.md at 1422. - **`docs/handbook.html` footer only:** #505 (`docs/v1-accuracy`) and #491. #468 has landed and is merged here. #506 conflicts there too, and in `scaffold-project.py`, as it does with main. README.md and AGENTS.md merge clean with #505, and `setup-project.py` merges clean with #491. Whichever lands second keeps every footer clause and recomputes the README blob. - **#395 merges clean but one of its tests fails with this branch.** `tests/test_template_text_placeholders.py::test_a_member_whose_id_yaml_would_misread_is_pinned_under_that_id` inits its own holder and runs a real `add` on `main`, which #487 refuses with `family-on-tracking-branch`. Whichever of the two lands second adds `git("switch", "-q", "-c", "family/members", cwd=root)` after that test's `init`; lane 5 is adding it on #395's own branch now, so this branch needs no change for it. (#395's head d526f76 also conflicts with this head in `tests/test_adopt_manifest_values.py`, a file this branch does not touch: that is #395 against main, which the same round resolves.) - On Windows `shell_quoted` speaks PowerShell, the native shell this standard documents (`setup-project.py`), as the doctor's NEXT lines do. `git -C …`, `python scripts\family.py …` and `cd …` are shell-agnostic, so they get the platform's quoting, and no printed line joins two commands with `&&`, which Windows PowerShell 5.1 rejects. - `rm -rf`, `mv` and `mkdir -p` are POSIX commands. They are quoted for a POSIX shell on every host, and the text says "in a POSIX shell (Git Bash on Windows)". PowerShell refuses `rm -rf`'s `-rf` and deletes nothing. - The tests split a host-quoted line with a PowerShell reader on Windows and with `shlex` elsewhere, and split a POSIX-only line with `shlex` everywhere. The real-shell test is skipped on Windows. - On Windows the tests do not execute `rm` or the `mv` lines. They delete the split path with `shutil.rmtree`, and stop after checking the `mv` argv. - Paths: the cached store keeps git's POSIX spelling (`as_posix`), as before. Test paths are `resolve()`d, which expands an 8.3 short name such as `RUNNER~1`. - Nothing here was run on Windows or macOS locally. The `tests-windows` and `tests-macos` CI jobs are the check; on macOS the real-shell test runs the shipped `zsh`. - **A shipped template that imports a newer shared helper can break a holder that kept an older copy.** This change no longer does (`siblings.py` uses `shlex.join`), but nothing stops the next one: `update-shape.py apply --accept-local scripts/repo_shape.py` keeps a `repo_shape.py` that lacks a name a newly copied file imports, and `check` and the validator then read the holder as fine. The general guard is for `apply` to refuse that, as rv456 found for `bootstrap.py` and `spelled` (#456). - **The doctor's `quote_arg` leaves a word that begins with `=` bare**, which zsh expands. `shell_quoted` quotes it; the doctor is left as it is here, and the agreement test excludes that one case. - **The doctor and `update-shape.py` keep their own quoters.** The doctor's `quote_arg` flattens to ASCII by its report's contract, so moving it onto `repo_shape.shell_quoted` is more than an import change. `update-shape.py`'s POSIX `shlex` lines are lane 3's open #374. A test holds `shell_quoted` and `quote_arg` to one answer on every printable ASCII value but a leading `=`. - **`family.py`'s by-hand rollback commands (#365) and its `git config -f .gitmodules` removals (#317, #360) keep `shlex.quote`.** They were already quoted, they are POSIX-only (`printf … | git update-index`), and they are worded for a POSIX shell. - **The holder's `validate-family.py` remedies that name `family.py add`** (`member-gitlink-absent`, `member-gitlink-unnamed`) print no branch step: they are a holder copy, and the `add` they name refuses the tracking branch with its own `switch` remedy. - **`remove`'s store note still computes `.git/modules/<path>`** rather than asking `git rev-parse --git-path`, as `add` does. In a linked worktree it therefore prints no note. That predates this change. - **PowerShell verbs** (`Remove-Item`, `Move-Item`) are not printed for the POSIX-only commands. - **README's `--into` sentence gives the default but not the refusal.** README.md (861) and the handbook (1246) say `--into <dir>` is the PARENT directory, "the directory you are standing in, by default". They do not say that `init` refuses that default inside a checkout of this standard (`family-landing-in-standard`). AGENTS.md item 1 says so, and the README's own example passes `--into ..`. README.md is at its 1422-line cap, so the sentence is on #459's checklist. - **`setup-project.py`'s own `cd <clone>` line and its step-(7) backstop lines print their paths unquoted.** This change quotes only the family lines it prints. Those two predate it and #509, and they are on #459's checklist. Reviewed independently at 10c93de: CHANGES NEEDED (0 blockers, 1 should-fix, 6 nits). The round at bb11b63 fixed the should-fix and all six nits, and its re-verification gave APPROVE WITH NITS (0/0/3), with SonarCloud's quality gate OK; those three are fixed at 3451a56. Main was then merged by GitHub's update-branch (8e1471c), by hand at 708f3bb and at fcbc7a2, where each time only the handbook footer conflicted, and by update-branch again at 22733ce (main 631f087, #503), with no conflict. CI was green there on Linux, macOS and Windows, with SonarCloud passing, but main had moved on: main f391e58 (#266, #509, #496 and #435) was merged by hand at 5a2776d, where again only the footer conflicted. CI was green there too, but main moved on again: main 8cd7dbe (#506 and #347) was merged by hand at 654df49, where once more only the footer conflicted. CI was green there as well, and main d41cc29 (#402) then came in by update-branch at e42ef74, with no conflict. CI is green on Linux and macOS, with SonarCloud passing, at e42ef74, a clean merge of main. All three OSes and SonarCloud passed at 654df49, and under Brett Heap's landing rule of 22:08:06Z (ruling 27) macOS and Windows carry over from that head. Main e6fbd74 (#515, for #514), which landed after it, touches none of its files, and the combined tree passed `tests/test_adopt_pins.py`, `tests/test_shape_doctor.py`, `tests/test_scaffold_leg_path_collisions.py`, the hygiene, AGENTS-code and CLI-reference tests, `tests/test_family_printed_quoting.py` and `tests/test_family_tracking_branch.py` (777 passed, 3 skipped). Closes #488 Closes #486 Closes #487 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Lane: openreposhape-5 (openRepoShape-5)
Claim: #273 (comment)
Claim: #339 (comment)
After #409 (#275)
Brought up to date after #409 squash-landed by merging origin/main (9736424, #409's squash) as f9ed46f. Its
scaffold-project.pyconflict was resolved to this branch's side oncegit diff b70a07d 9736424 -- scaffold-project.py(#409's final head against its squash) came out empty. So the file is the one tested on this branch plus main's own later changes to it, and holds nothing of #409's; details below.This branch carries #275's adopt half. It was built on
fix/pin-source-env-collisions, PR #409 (#275's scaffold half and its fix round), merged at 6494714 as 583f647, over the earlier 5690235 (34dbd86). After 6494714, #409 only merged main, five times, up to its final head b70a07d, and none of those merges has a hand edit (git show --remerge-diffis empty for each). Adopt's refusal of two products that share a source variable goes through #409's function, so this branch lands after #409. It was never rebased.git diff b70a07d 9736424is empty for each file #409 touched:scaffold-project.py,scripts/repo_shape.py,templates/assembly-root/scripts/validate-pins.pyandtests/test_pin_source_env_collisions.py. What f9ed46f resolves, four files with one hunk each:scaffold-project.py.PIN_COLLISION_REMEDIATIONthrough_pin_collision_remediation, which scaffold-project.py, validate-pins.py, repo_shape.py:open-glassandopen_glassshare oneSHAPE_PIN_SOURCE_<PRODUCT>, so--pinrefuses the pair andvalidate-pins.pynotes a declared one and reads the variable for neither (#275) #409's squash edits: it addsPIN_VARIABLE_REMEDIATION,_colliding_pin_variablesand_pin_collision_remediationand changes_refuse_colliding_pins. This branch moved all of that toscripts/shape_materialize.py. The hunk takes this branch's side. (The merge commit's message says "re-adds"; this is the exact account.)open-glassandopen_glassshare oneSHAPE_PIN_SOURCE_<PRODUCT>, so--pinrefuses the pair andvalidate-pins.pynotes a declared one and reads the variable for neither (#275) #409's auto-merged import ofPIN_SOURCE_ENV_PREFIX, pin_source_env_collisionsis dropped, because nothing in the file uses either name any more.git checkout --oursof the whole file. Main changed the file beyond scaffold-project.py, validate-pins.py, repo_shape.py:open-glassandopen_glassshare oneSHAPE_PIN_SOURCE_<PRODUCT>, so--pinrefuses the pair andvalidate-pins.pynotes a declared one and reads the variable for neither (#275) #409 since this branch last merged it: scaffold-project.py and family.py:--tracking-branchis asked ofgit check-ref-format --branchbefore anything exists, so~,a..b,x.lockanda/are refused under--dry-runand no longer die at the firstgit init -baftergh repo createhas made the repositories (#283) #355's--tracking-branchcheck (6c2a371) and a73b4e9'sRULESET_HINT. Those lines stay.git merge-treegives for this branch with main before the squash (518010b). Its diff from the file tested here has the same patch-id as main's own 3ef059c..518010b change to it.templates/assembly-root/scripts/validate-pins.py. Main's side, which is scaffold-project.py, validate-pins.py, repo_shape.py:open-glassandopen_glassshare oneSHAPE_PIN_SOURCE_<PRODUCT>, so--pinrefuses the pair andvalidate-pins.pynotes a declared one and reads the variable for neither (#275) #409's final blob, with main's templates/assembly-root/scripts/validate-pins.py: a workflow file under .github/workflows/ that is not UTF-8 ends the run in a UnicodeDecodeError traceback, exit 1, with no refusal naming the file, so the project's pins gate (make validate, the validate workflow) dies on a Latin-1 comment where a non-UTF-8 pin is a cleanREFUSED yaml-unreadable, exit 2 #359 workflow reader in it. This branch had no change of its own to the file beyond scaffold-project.py, validate-pins.py, repo_shape.py:open-glassandopen_glassshare oneSHAPE_PIN_SOURCE_<PRODUCT>, so--pinrefuses the pair andvalidate-pins.pynotes a declared one and reads the variable for neither (#275) #409.adopt-project.py,_commit_the_split.REFUSED <step>., what failed and one notice of what the run left, so a failedgit filter-repono longer exits 2 with an empty stderr (#171, #188, #177) #445 wraps the function in its late-failuretry, and adopt-project.py:executerecords inproject.yamlwhich leg the plan gave each submodule a leg keeps, and shape-doctor.py holds the submodule to that row, so one in the other leg is MISPLACED and a real adoption's placement readsok(#189) #394 builds anappendwith the plan's submodule record.neutral_pins=_recorded_pins(plan)on the materializer call.try:append=append, neutral_pins=_recorded_pins(plan).docs/cli.md. It is regenerated withscripts/render-cli-reference.py, and--checkis ok. It differs from main's only in the--pin,--pin-ownerand--pin-sourcehelp ofadopt-project.py plan.scripts/repo_shape.py,templates/assembly-root/scripts/validate-pins.pyandtests/test_pin_source_env_collisions.pyare main's blobs, which are #409's: this branch adds nothing to them. Nothing of #409 is lost. Its scaffold behaviour isshape_materialize.refuse_colliding_pinsandpin_variable_collisions, keyed by #409'spin_product_key.73d65c6, after the merge.
_read_pinsreadsplan.get("pins")where it readplan.data.get("pins").REFUSED <step>., what failed and one notice of what the run left, so a failedgit filter-repono longer exits 2 with an empty stderr (#171, #188, #177) #445'stests/test_adopt_late_failure.pyhands_commit_the_splitaSplitPlanas its plan, adictthat addsentries. On f9ed46f, three of its tests stopped at "committing the split" withAttributeError: 'SplitPlan' object has no attribute 'data'(3 failed, 83 passed in that file).Plan.getwith no default returnsplan.data.get(key)exactly, so a loaded plan reads as before.What was wrong (#273)
adopt-project.py plan --pintook a NAME only (--pin openGlass) and refused a trailing@<commit>.plan,checkandexecuteeach exited 0, andexecuteprintedadoption verified. The manifest then listed the name inneutral_product_pins:with nocontracts/<product>-pin.yamlbeside it, and on its first run the adopted root's ownvalidate-pins.pyrefused it asneutral-pin-missing.#273's table, reproduced on c7c02fa with the issue's own commands (local bare remotes, and a stub
ghthat exits 97):--pinopenGlassexecuteexit 0,- openGlass; clone'svalidate-pins.pyexit 2neutral-pin-missingplanexit 2pin-malformed, with the scaffold's remediation (--pin openGlass@<40 hex>); no plan is writtenopensoft/openGlass- opensoft/openGlass; exit 2neutral-pin-missingforcontracts/opensoft/openglass-pin.yamlplanexit 2pin-malformedspecvalidate-pins.pyexit 0, reading the leg'scontracts/spec-pin.yamlas the neutral pinspec@<sha>isadopt-pin-collidesatplanshapecontracts/shape-pin.yamlread as the neutral pinshape@<sha>isadopt-pin-collidesatplanopenGlass@<sha>,opensoft/openGlass@<sha>planexit 2adopt-pin-malformedplan,checkandexecuteexit 0, andghis called 0 times.contracts/openglass-pin.yamlis written and the manifest lists- openGlass. In the clone,validate-manifest.pyexits 0, andvalidate-pins.pyexits 0 withopenGlass: pin commit … digest recomputesWhat changed (#273)
Brett ruled for decision 1's first option ("go with your recommendation on #273"): adopt takes the scaffold's pin and writes the scaffold's pin file. That reverses the NAME-only
--pinfrom #6, and the comment that described it is replaced with the new contract.One implementation. The scaffold's pin code moved into
scripts/shape_materialize.py, next toneutral_pin_pathand the materializer that writes the file. What moved:PIN_ARG_RE,parse_pin,pin_digest_from_sourceandpin_digest_from_gh;declared_pin_values,declared_pin_productsandPIN_COLLISION_REMEDIATION(in AGENTS.md, README and scaffold-project.py --help: say that a --spec-path or --code-path that is, holds or lies inside a file the shape writes, and a --pin of spec, code or shape or of one product twice, are refused before anything is created, and name those three in the pin refusal's remediation, so an agent briefed from AGENTS.md can say so before it runs the scaffold (#288) #309's wording);pin_source_env_nameturns every character outside[A-Z0-9_]into_, so the neutral productsopen-glass,open_glassandopen.glassshare oneSHAPE_PIN_SOURCE_OPEN_GLASS, and a project that pins two of them gets aneutral-pin-source-unreadableFINDING (exit 1) for whichever one the variable does not name #275's branch,PIN_VARIABLE_REMEDIATIONand the variable check;_refuse_colliding_pins, split into the purepin_collisionsandpin_variable_collisions, andrefuse_colliding_pins(refusal=...), which raises one refusal for both.The code did not move verbatim.
_refuse_colliding_pinsbecame the three functions above;pin_digest_from_sourcewas rewritten for scaffold-project.py:pin_digest_from_sourcecatches onlyRefusal, so a--pin-sourcethat does not exist, is a file, or sits under a parent this user cannot enter ends--dry-runand the real run alike in aFileNotFoundError,NotADirectoryErrororPermissionErrortraceback (exit 1), where a directory that is not a clone is thepin-source-unreadablerefusal (exit 2) with its remediation #339 (below); and fix round 1 changed three of them (below):pin_variable_collisionskeys each pin by scripts/repo_shape.py:pin_source_env_nameturns every character outside[A-Z0-9_]into_, so the neutral productsopen-glass,open_glassandopen.glassshare oneSHAPE_PIN_SOURCE_OPEN_GLASS, and a project that pins two of them gets aneutral-pin-source-unreadableFINDING (exit 1) for whichever one the variable does not name #275'spin_product_key,pin_digest_from_sourcespells a path that is not printable, andpin_digest_from_ghrefuses a missinggh. The scaffold's behaviour is otherwise unchanged.scaffold-project.pyimports what it calls under its old names. Lane 5's review ran 34 scaffold cases (every pin refusal, theghstub's four modes, dry and real successes) on 34dbd86 and on this branch at 7a94b6b. rc, stdout, stderr, theghlog and the files made were identical, apart from the shape's own commit and digest and scaffold-project.py:pin_digest_from_sourcecatches onlyRefusal, so a--pin-sourcethat does not exist, is a file, or sits under a parent this user cannot enter ends--dry-runand the real run alike in aFileNotFoundError,NotADirectoryErrororPermissionErrortraceback (exit 1), where a directory that is not a clone is thepin-source-unreadablerefusal (exit 2) with its remediation #339's row. Its pin tests and scripts/repo_shape.py:pin_source_env_nameturns every character outside[A-Z0-9_]into_, so the neutral productsopen-glass,open_glassandopen.glassshare oneSHAPE_PIN_SOURCE_OPEN_GLASS, and a project that pins two of them gets aneutral-pin-source-unreadableFINDING (exit 1) for whichever one the variable does not name #275's pass as they are.tests/test_git_output_decoding.py's exemption forpin_digest_from_ghfollows the function to its new file.plan.--pin,--pin-ownerand--pin-sourcemean what they mean for the scaffold.planreads the digest from--pin-sourceorgh api, and records product, repository, commit, digest anddigest_sourcein the plan, socheckandexecuteread no network for a pin.planrefuses:@<commit>, aspin-malformed. That includes an empty--pin '', which adopt used to drop and now refuses, as the scaffold always has;spec,codeandshape, in any case, asadopt-pin-collides(adopt-project.py--pin <name>is accepted byplan,checkandexecute("adoption verified"), but writes only the name intoneutral_product_pins:and nocontracts/<name>-pin.yaml, so the adopted root's ownvalidate-pins.py(and itsvalidate.yml) refuses withneutral-pin-missingfrom the first run #273 decision 4), in the scaffold's sentence and remediation (scaffold-project.py--pin spec@<sha>(orcode@,shape@, or two--pins whose products differ only by case) passes--dry-run, then creates all three repositories, pushes both legs and dies withREFUSED materialize-collision, because the pin'scontracts/<product>-pin.yamlis a file the shape already writes #241, AGENTS.md, README and scaffold-project.py --help: say that a --spec-path or --code-path that is, holds or lies inside a file the shape writes, and a --pin of spec, code or shape or of one product twice, are refused before anything is created, and name those three in the pin refusal's remediation, so an agent briefed from AGENTS.md can say so before it runs the scaffold (#288) #309);SHAPE_PIN_SOURCE_<PRODUCT>names, such asopen-glassandopen_glass, the same way, through scripts/repo_shape.py:pin_source_env_nameturns every character outside[A-Z0-9_]into_, so the neutral productsopen-glass,open_glassandopen.glassshare oneSHAPE_PIN_SOURCE_OPEN_GLASS, and a project that pins two of them gets aneutral-pin-source-unreadableFINDING (exit 1) for whichever one the variable does not name #275's check;adopt-leg-path-collides;--pin-sourcepath thatcheckwould refuse indigest_source:(fix round 1), asadopt-unwritable-value. It is asked only when a--pinrecords the path;ghon PATH and no--pin-source,gh-not-found(fix round 1).checkandexecute. These are newcheckfindings, andexecuterefuses each one before any leg exists. They come from_leg_findings, the listcheckand_refuse_what_check_findsshare:plan-pin-unrecorded: a name alone (an old plan'spins: [openGlass], or a hand edit), or an entry missing its commit or digest;plan-pin-malformed;plan-pin-collides, which covers two pins at one file, a pin at a file the shape writes, and two products that share one source variable;plan-root-holds-pin: a source path kept in the root at the pin's file. The materializer would write the shape's pin beside it undershape/, andvalidate-pins.pywould read the source's file instead.How each finding ends:
plan-pin-unrecordedandplan-pin-malformedend with theplan --pin <product>@<40 hex commit>that records the pin whole.plan-pin-collidesends with re-runningplanwith each product once, or, for two products that share a source variable, with one product of each such group.plan-root-holds-pinends with the kept path's own remedy: send that entry to a leg,dropit, or rename it in the source and re-runplan.A leg moved over a pin's file in the plan is
plan-leg-path-collides.Also part of adopt-project.py:
Plan.__init__,allowed_empty_legs,namesandopen_sourceiterate or call.geton a hand-edited plan'spins:,allow_empty_legs:,paths:,legs:andsource:without asking their type, so a scalar endscheckandexecutein a TypeError or AttributeError traceback, exit 1, andpins: openGlasspassescheck, adopts, and writes eight one-letter neutral-product pins that the project's own validate-pins.py refuses #386: itspins:row. A hand-editedpins:that is not a list (a name such aspins: openGlass, a number, a mapping, a quoted string) is nowplan-pin-malformedincheck(`pins:` is 'openGlass', not a list of pins), andexecuterefuses it before any leg exists._read_pinsinadopt-project.py; the test row isplan-pin-malformed-not-a-listintests/test_adopt_pins.py.checksaidplan okfor the name, the mapping and the string, and gave a traceback for the number (reproduced by lane 5's review on cb7063f). adopt-project.py:Plan.__init__,allowed_empty_legs,namesandopen_sourceiterate or call.geton a hand-edited plan'spins:,allow_empty_legs:,paths:,legs:andsource:without asking their type, so a scalar endscheckandexecutein a TypeError or AttributeError traceback, exit 1, andpins: openGlasspassescheck, adopts, and writes eight one-letter neutral-product pins that the project's own validate-pins.py refuses #386 records thatexecutethen wrote the name as eight one-letter pins.Plan.__init__,allowed_empty_legs,namesandopen_sourceiterate or call.geton a hand-edited plan'spins:,allow_empty_legs:,paths:,legs:andsource:without asking their type, so a scalar endscheckandexecutein a TypeError or AttributeError traceback, exit 1, andpins: openGlasspassescheck, adopts, and writes eight one-letter neutral-product pins that the project's own validate-pins.py refuses #386's other keys,allow_empty_legs:,paths:,legs:andsource:, so adopt-project.py:Plan.__init__,allowed_empty_legs,namesandopen_sourceiterate or call.geton a hand-edited plan'spins:,allow_empty_legs:,paths:,legs:andsource:without asking their type, so a scalar endscheckandexecutein a TypeError or AttributeError traceback, exit 1, andpins: openGlasspassescheck, adopts, and writes eight one-letter neutral-product pins that the project's own validate-pins.py refuses #386 stays open. Lane 2's adopt-project.py:checknames, andexecuterefuses before the work directory is made, a plan whosesource:orlegs:is not a mapping or whoseallow_empty_legs:orpaths:is not a list of whatplanwrites, and asks the same first of apins:that is not a list, so a number or a boolean in any of the four no longer ends both in a traceback andallow_empty_legs: codeis no longer read as four legs (#386) #435 takes all of adopt-project.py:Plan.__init__,allowed_empty_legs,namesandopen_sourceiterate or call.geton a hand-edited plan'spins:,allow_empty_legs:,paths:,legs:andsource:without asking their type, so a scalar endscheckandexecutein a TypeError or AttributeError traceback, exit 1, andpins: openGlasspassescheck, adopts, and writes eight one-letter neutral-product pins that the project's own validate-pins.py refuses #386,pins:included, and itspins:rule keeps only text items where this branch writes mappings; whichever lands second reconciles them (adopt-project.py:checknames, andexecuterefuses before the work directory is made, a plan whosesource:orlegs:is not a mapping or whoseallow_empty_legs:orpaths:is not a list of whatplanwrites, and asks the same first of apins:that is not a list, so a number or a boolean in any of the four no longer ends both in a traceback andallow_empty_legs: codeis no longer read as four legs (#386) #435 (comment), and "Where the meaning meets" below).execute. It writes each pin throughmaterialize_assembly_root(neutral_pins=...). The manifest lists the product with the owner stripped (decision 3), so the entry and the file agree.Docs.
--helpis updated anddocs/cli.mdregenerated (render-cli-reference.py --checkok).A
--pin-sourcethat cannot answer (#339)pin_digest_from_sourcecaught onlyRefusal. A directory that is no clone is git's own failure, so it was alwayspin-source-unreadable, exit 2. A pathsubprocess.runcannot enter never reaches git: it is aFileNotFoundError,NotADirectoryErrororPermissionErrorout ofcwd=, and nothing caught it.scaffold-project.py --pin ... --pin-source <that>ended--dry-runand the real run alike in a traceback, exit 1. Since the move above,adopt-project.py plan --pinreads its digest through the same function and ended the same way. Both reach it throughdeclared_pin_values, andcheckandexecuteread no source, so that call is the only reader.The issue's table, run on ab623dd for the scaffold and on d806e8f for
adopt-project.py plan(adopt has no--pin-sourceon ab623dd), then on this head.ghis a stub that logs and exits 97, withGH_TOKENbogus and an emptyGH_CONFIG_DIR; it was called 0 times in every run:--pin-sourcepin-source-unreadable, exit 2FileNotFoundErrortraceback, exit 1pin-source-unreadable, exit 2:<path> could not answer for opensoft/openGlass @ <40 hex>: no such directoryNotADirectoryError, exit 1... it is a file, not a directoryPermissionError, exit 1... Permission deniedRuntimeErrorout ofPath.resolve()on 3.12, exit 1... Too many levels of symbolic linksThe real run and
--dry-rungive the same stdout and stderr, and nothing exists afterwards (no remote directory, no work directory; for adopt, no plan file).What changed, in
scripts/shape_materialize.pyonly:pin_digest_from_sourcecatchesOSErroras well asRefusaland raises the existingpin-source-unreadablewith its remediation unchanged. The message names the path and the reason.stat()judges the source inside the guard, as validate-pins.py: a--pin-sourcevalue isPRODUCT=PATHonly when the text before its first=names a declared neutral-product pin, so a bare stand-in undera=b/recomputes the digest instead of falling togh apiand askip, an explicit source that does not exist or is not a directory is theneutral-pin-source-unreadablefinding instead of aFileNotFoundErrortraceback, and a value no pin used is noted (#295) #312 does invalidate-pins.py, and the sentences are that file's: a missing path, or one beneath a file, is "no such directory"; a file is "it is a file, not a directory"; any otherOSError, fromstat()or from git'scwd=, is the system's own reason (strerror). It is notexists()/is_dir(), which before 3.14 swallow only a missing path. A sourcestat()can read butcwd=cannot enter, a directory with no search bit, fails intree_digestand the same guard answers it.declared_pin_valuesresolves the path through a guarded_resolved_pin_source.Path.resolve()raises aRuntimeErroron a symlink loop before 3.13 (anOSErrorsince), before the digest was asked for. As typed,stat()meets the loop and says so.digest_sourcereadslocal-clone (<path as typed>), and the digest istree_digest's.tests/test_pin_source_unreadable.py(46 tests in the #339 commit; fix round 1 adds 17 for S1 and moves one, below) runs each case throughscaffold-project.py --dry-run, its real run andadopt-project.py plan: missing, a file, beneath a file, relative and missing, a parent and a source with no search bit (skipped on Windows and as root, by a mark, so before any fixture builds a directory), a 300-character name, a symlink loop (skipped on Windows) and a directory that is no clone. Each asserts exit 2, the refusal naming the path and the reason, its remediation, no traceback and nothing created. It also holds--dry-runand the real run to the same stdout and stderr, checks the path is printed escaped under an ASCII stderr and as a surrogate when it is not UTF-8, and checks that a relative source and a symlink to a clone read as before. The sessionghguard fails any test whose run calledgh.Main, and
validate-pins.pyorigin/main ab623dd reached this branch through implQ's branch: 34dbd86 is a30091f merged with ab623dd. One file conflicted there,
templates/assembly-root/scripts/validate-pins.py, in seven hunks, because #312 (8cdca5e) replacedkeyed_sourcesanddefault_sourcewithPinSourcesand #275 addedenv_sharedandshared. #312's blob is the same at its PR head, at the squash and at ab623dd, so it landed the file implQ resolved against. The resolution keeps #312's side in all seven and adds #275's on top:resolve_neutral_pin_sourcesignaturesources: PinSourcesand 3-tuple return, plus #275'senv_shared: bool = FalseReturns (path, how, typed)text, plus #275's "Rule 2 is skipped whenenv_shared" paragraph_recompute_neutral_pin_digestsignaturesources, plus #275'sshared: frozenset[str]afterreportresolve_neutral_pin_source(...)call inside itenv_shared = product.casefold() in sharedand the extra argument_check_one_neutral_product_pinsignaturesources, plus #275'sshared_recompute_neutral_pin_digestsources, report, shared_check_neutral_product_pinsdocstring and bodyPinSources(...)and_note_unused_pin_sources, plus #275's paragraph,_report_pin_source_collisions(products, report)andshared = frozenset(pin_source_env_shared(products))This branch's own merge of 34dbd86 (5690235) had no conflict, and neither had its merge of #409's fix round, 6494714 (583f647). That fix round made
neutral-pin-source-collidesa note and keyed "one product" byrepo_shape.pin_product_key. At 421e4e5,templates/assembly-root/scripts/validate-pins.py,scripts/repo_shape.pyandtests/test_pin_source_env_collisions.pywere byte-identical to the treegit merge-treegives for 6494714 with origin/main 3ef059c (blobs 5604007, 091966b, 5220787), so this branch added nothing to those three files beyond #409. The 34dbd86 merge brought in #335, #312, #317, #270 (f795cf7), #344 and #349 with no conflict. Earlier merges of origin/main (87373a6, for #309's reworded constant, and e43fa95) are unchanged.Fix round 1 merged origin/main three more times:
.gitmodulesurl the plan keeps in a leg is resolved from the source's remote and from the leg's, and where the two differcheckfinds it andexecuterefuses before any leg exists, so a recursive clone no longer fails afteradoption verified(#190) #245, scaffold-project.py:--elected-by,--reference,--nameand the elector's fallback are refused before anything exists when no file can hold them, so a byte that is not UTF-8 no longer creates repositories before a traceback (#282) #351, validate-family.py:member-duplicateis decided frommembers:alone before the--no-membersreturn and comparesproject:case-folded, so afamily.yamlthat names one row twice no longer passes the run a private family's CI falls back to, and rowsIRRSandirrsare one finding naming both, sincemembers/IRRSandmembers/irrsare one directory on macOS and Windows (#331) #350, family.py:bump --dry-runfetches the member, resolves--toand prints theold -> newrow and its trailers without checking the member out or rewritingfamily.yaml, andremove --dry-runprints the row, gitlink,.gitmodulessection and directory it would remove and changes nothing, so the module'sEXIT CODESline is true of both (#326) #345, single-repository.yaml: openRepoShape records that it stays a single repository, as Brett Heap decided on 2026-10-11, so the Triad advisory is no longer said here (#337) #372, adopt-project.py, AGENTS.md:executeverifies before it exits on a refusedgh pr create, and step 7 keys its way out to the exit it prints, so anIS pushedexit 2 is finished by hand, not re-run into a second pair of legs (#304) #371, shape-doctor.py: thelegsandmembersNEXT is a script-freegit submodule update --init --recursiveover what a checkout cures, and the lockstep gate for what none does, so a root that lostscripts/bootstrap.pyno longer pastescan't open file, and an empty mount or a leg committed past its pin on its tracking branch no longer pastes a bootstrap that leaves the row red (#276) #354, update-shape.py and bump-leg.py: a pin rewrite keeps each line's own indentation, spacing and trailing comment and reads its result back before writing it, so adigests:child indented four spaces takes the update instead ofupdate-pin-unrecognised/bump-leg-pin-unrecognised, and a# commentoncommit:ortree_sha256:survives every pin rewrite,rewrite_manifestincluded (#323) #380, adopt-project.py:planasks about every source pathexecutewill not extract as it is named, with the reason androotordropas its only answers, so-notes.mdis no longer plannedleg: specforcheckto refuse, and #166's split question says the same of such an entry (#319) #377 and the rest since ab623dd, with no conflict.scaffold-pin-collides, or the code renamed inscaffold-project.pywhile AGENTS.md keeps its old spelling, fails naming the token and the document instead of leaving an assistant to relay a code no tool prints (#348) #396'stests/test_agents_md_codes.py. That test findsscaffold-pin-collidesas a string constant ofscripts/shape_materialize.py, whichSHIPPEDlists.--id null,--tracking-branch trueand a blank--elected-byare refused (#258, #261) #314. One conflict, one hunk inadopt-project.py. This branch's_refuse_an_unrecordable_pin_sourceand scaffold-project.py and adopt-project.py ask the project's YAML reader and validate-manifest.py about the manifest's values before anything exists, so--id null,--tracking-branch trueand a blank--elected-byare refused (#258, #261) #314's section "The project must be able to hold whatexecutewrites into it (adopt-project.py and scaffold-project.py accept values the new project's own validate-manifest.py refuses (a blank--elected-byor--reference,--elected-on 2026-W41-6or20261011on Python 3.11+,--org test+org, a plan'svisibility: secret), soadoption verifiedor a scaffold exit 0 is followed by a failed first bootstrap; andplanadmits a 251-255-character--projectthatcheckrefuses #261)" were added at the same place; both are kept. Where the two meet, lane 5's earlier recipe applies:_flag_planbuilds each pin as thePIN_FIELDSmappingplanwrites, notlist(pins)of products. So the planplanasks the validator about is the plan it writes.--id null,--tracking-branch trueand a blank--elected-byare refused (#258, #261) #314'stest_plan_asks_what_check_asks_of_the_same_keysplans--pin openGlass@<commit> --pin-source <stand-in>where it planned the name alone, whichplannow refuses.Then f9ed46f, after #409 squash-landed, and 73d65c6 after it (top of this description).
#270's
pins-surrogatecaseOn the merged tree exactly one test failed:
tests/test_adopt_template_values.py::test_check_finds_and_execute_refuses_a_hand_edited_value[pins-surrogate]. It plans, then hand-edits the plan topins: ["openX\ud800"], a name-only pin with a lone surrogate. Before this branch the refusal's headline wasREFUSED plan-unwritable-value. A name-only pin in a plan is nowplan-pin-unrecorded(#273), socheckhas a second, independent finding for that very plan, and the headline is the first finding's:Nothing about #270's contract changed. The lone surrogate is still found as
plan-unwritable-value, listed under the refusal, andexecutestill refuses before any leg exists.checkbuilds_leg_findings, where_pin_findingssits, before it calls_value_findings, so the pin finding comes first. d806e8f changes one assertion for that row,pinsonly: the headline isREFUSED plan-pin-unrecorded:where it wasPLAN_REFUSED. The other 15 rows and the finding assertions, including theplan-unwritable-valueline, are untouched.Fix round 1
Lane 5's review of 7a94b6b found 3 should-fix and 5 nits. Each is answered here:
open-glassandopen_glassshare oneSHAPE_PIN_SOURCE_<PRODUCT>, so--pinrefuses the pair andvalidate-pins.pynotes a declared one and reads the variable for neither (#275) #409'spin_product_key(review N1). The merge of scaffold-project.py, validate-pins.py, repo_shape.py:open-glassandopen_glassshare oneSHAPE_PIN_SOURCE_<PRODUCT>, so--pinrefuses the pair andvalidate-pins.pynotes a declared one and reads the variable for neither (#275) #409's fix round could not carry it, because this branch had moved the function that used it out ofscaffold-project.py.pin_variable_collisionsnow keys each pin by that function, where it casefolded.openßandopenssare two pin files and oneSHAPE_PIN_SOURCE_OPENSS, and a casefold named each of the two pins twice. No--pinor recordedpins:entryPIN_ARG_REadmits can spell either, so the test asks the function directly.refuse_colliding_pins' docstring now says, in scaffold-project.py, validate-pins.py, repo_shape.py:open-glassandopen_glassshare oneSHAPE_PIN_SOURCE_<PRODUCT>, so--pinrefuses the pair andvalidate-pins.pynotes a declared one and reads the variable for neither (#275) #409's words, that the root'svalidate-pins.pyonly notes such a pair.--pin-sourcepath that is not printable.pin_digest_from_sourcenames the path as typed unless it is not printable (str.isprintable), and then as Python'sreprspells it (shown_source). That covers the refusal's head and git's own reason, which names the directory it ran in. Printed raw, an ESC reached the terminal, and a line break or a carriage return began a line of the path's choosing, a forgedREFUSEDline among them, from the scaffold and from adopt. The traceback scaffold-project.py:pin_digest_from_sourcecatches onlyRefusal, so a--pin-sourcethat does not exist, is a file, or sits under a parent this user cannot enter ends--dry-runand the real run alike in aFileNotFoundError,NotADirectoryErrororPermissionErrortraceback (exit 1), where a directory that is not a clone is thepin-source-unreadablerefusal (exit 2) with its remediation #339 replaced had shown arepr. A printable path is named byte for byte as before.ghon PATH.pin_digest_from_ghrefuses asgh-not-found, exit 2, asrepo_shape.tree_digest_from_ghnames it, and offers--pin-source. It was aFileNotFoundErrortraceback, exit 1, from the scaffold and, new with this branch, fromadopt-project.py plan --pin.commit:ortree_sha256:. The lane's decision is to accept it and document it (option (c)); no behaviour changed. A pair that disagrees passescheckandexecute(adoption verified), because neither reads a network for a pin, by design. The adopted root's ownvalidate-pins.pythen exits 1 withneutral-pin-digest-mismatchwherever it can read the product.checknow says so once, after the pins it will write: "each pin asplanread it, not read again here: the root's ownvalidate-pins.pyrecomputes its digest wherever it can read the product". Options (a),executerecomputes before it creates the legs, and (b),checkrecomputes from a clone still on the machine, are filed as adopt-project.pycheckandexecutewrite a neutral pin'scommit:andtree_sha256:exactly as the adoption plan records them and read neither again, so a hand edit that makes the two disagree isplan okandadoption verified, and the adopted root's ownvalidate-pins.pythen exits 1 withneutral-pin-digest-mismatchon its first run #449.--pin-sourcethatplancannot record.planaskscheck's question (one_printable_line) of the path it records in each pin'sdigest_source:, before any digest is read. It refuses one that fails asadopt-unwritable-value, in adopt-project.py: a valueexecutewrites into a file is asked whether a file can hold it, inplan, incheckand inexecutebefore any leg exists, so a byte that is not UTF-8 in--elected-by,--referenceor a local--sourcedirectory no longer pushes both legs beforewrite_lfraises (#255) #270's words (unwritable_reason), or as "an invisible formatting character" where those name nothing. Before, it was planned, exit 0, and printed raw in the report.checkthen refused it asplan-pin-malformedand said to re-run the sameplan. The report'spinline andcheck'swriteline now spell the digest's source with_shown. The scaffold half is pre-existing (a line break in the path is written into the pin file; a byte that is not UTF-8 dies after all three repositories exist) and is filed as scaffold-project.py--pin-source <path>: the path is written as it stands into the new root'scontracts/<product>-pin.yaml(digest_source: local-clone (<path>), a bare placeholder) and is asked nothing first, so a clone at a path holding a line break scaffolds, exit 0, a pin file with a line of the path's choosing, and one at a path that is not UTF-8 creates all three repositories and then dies inwrite_lfwith aUnicodeEncodeErrortraceback, exit 1 #450.pins:name holding an ESC and a forged line);digest_sourceprintability check;commit: 1234567unquoted);plan-submodule-rootalone).pin_digest_from_sourcecatches onlyRefusal, so a--pin-sourcethat does not exist, is a file, or sits under a parent this user cannot enter ends--dry-runand the real run alike in aFileNotFoundError,NotADirectoryErrororPermissionErrortraceback (exit 1), where a directory that is not a clone is thepin-source-unreadablerefusal (exit 2) with its remediation #339's third decision. A--pin-sourcethat nothing reads is still silent, now in adopt too. This PR does not decide it. scaffold-project.py and adopt-project.pyplan: a--pin-sourcethat nothing reads (no--pin, and for the scaffold no--referent-chainlink) is accepted in silence, even a path that does not exist, so--dry-runsaysnothing was created.andplanwrites a plan, each exit 0 with no word that the flag changed nothing, where the root's ownvalidate-pins.pynotes such a value since #295 #451, filed, lays out refuse, note and leave, so closing scaffold-project.py:pin_digest_from_sourcecatches onlyRefusal, so a--pin-sourcethat does not exist, is a file, or sits under a parent this user cannot enter ends--dry-runand the real run alike in aFileNotFoundError,NotADirectoryErrororPermissionErrortraceback (exit 1), where a directory that is not a clone is thepin-source-unreadablerefusal (exit 2) with its remediation #339 loses nothing.Fix round 2
Lane 5's second review, of 421e4e5, found no blocking item, 2 should-fix items and 3 nits. The two should-fix items are this body: the landing after #409 (above) and #386 (above). The code needed no change for them. f63d1ed adds two tests and no code:
planasks nothing of a--pin-sourceno--pinrecords. With theargs.pin andguard dropped, such a path holding an ESC or a zero-width joiner was refused in a sentence naming no file ("executewrites into , and ..."), and every test still passed.test_a_pin_source_no_pin_records_is_not_asked_aboutholds the plan to exit 0 andpins: [], with the path named nowhere in the plan or the report. The ESC row is POSIX-only by the file'sPOSIX_NAMESmark; the zero-width joiner row runs everywhere. It holds scaffold-project.py and adopt-project.pyplan: a--pin-sourcethat nothing reads (no--pin, and for the scaffold no--referent-chainlink) is accepted in silence, even a path that does not exist, so--dry-runsaysnothing was created.andplanwrites a plan, each exit 0 with no word that the flag changed nothing, where the root's ownvalidate-pins.pynotes such a value since #295 #451's answer today, silence, so a decision there changes this test on purpose.--pin-sourceis printed as it is. With_spelledin place of_showninplan'spinline, every test still passed.test_a_printable_pin_source_is_printed_as_it_isholdsplan'spinline, andcheck'swriteline for the planplanwrote, to their exact text, ending(local-clone (<path>))with no backtick.Evidence
Fix round 2, on f63d1ed. Each run had a
ghstub first on PATH (it logs and exits 97),GH_TOKENbogus, an emptyGH_CONFIG_DIR,git-filter-repo2.47.0 next on PATH, and the identity blanked withGIT_CONFIG_COUNT; the sessionghguard came first inside each run.pin_digest_from_sourcecatches onlyRefusal, so a--pin-sourcethat does not exist, is a file, or sits under a parent this user cannot enter ends--dry-runand the real run alike in aFileNotFoundError,NotADirectoryErrororPermissionErrortraceback (exit 1), where a directory that is not a clone is thepin-source-unreadablerefusal (exit 2) with its remediation #339's file, scripts/repo_shape.py:pin_source_env_nameturns every character outside[A-Z0-9_]into_, so the neutral productsopen-glass,open_glassandopen.glassshare oneSHAPE_PIN_SOURCE_OPEN_GLASS, and a project that pins two of them gets aneutral-pin-source-unreadableFINDING (exit 1) for whichever one the variable does not name #275's tests, the pin and neutral-pin validators, git output decoding, adopt template values, CLI reference, scaffold pin and reuse, scaffold pin collisions, scaffold-project.py and adopt-project.py ask the project's YAML reader and validate-manifest.py about the manifest's values before anything exists, so--id null,--tracking-branch trueand a blank--elected-byare refused (#258, #261) #314's two files and tests/test_agents_md_codes.py: every backticked code-shaped name in AGENTS.md must be an exact string constant in a shipped tool, so a misspeltscaffold-pin-collides, or the code renamed inscaffold-project.pywhile AGENTS.md keeps its old spelling, fails naming the token and the document instead of leaving an assistant to relay a code no tool prints (#348) #396's file. The skips aretest_adopt_plan.py:403(no MedxEHR clone) andtest_adopt_template_values.py:223(this host readsgit config user.namefrom the repository, not fromGIT_CONFIG_GLOBAL). That is fix round 1's 809 passed, plus the three new rows.tests/test_adopt_pins.pyandtests/test_pin_source_unreadable.pyless the five end-to-end adoptions:args.pin andguard dropped: 2 failed, both n1 rows;plan'spinline_spelled: 1 failed, the n2 test;check'swriteline_spelled: 2 failed, the n2 test andtest_an_upper_cased_hand_edit_is_written_lower_cased;plan'spinline raw: 1 failed, the no-break-space test that already guarded that hunk.On the merged tree, 73d65c6 (the merge f9ed46f and the
_read_pinscommit), with the same harness:REFUSED <step>., what failed and one notice of what the run left, so a failedgit filter-repono longer exits 2 with an empty stderr (#171, #188, #177) #445'stest_adopt_late_failure.py,test_adopt_git_reads.pyandtest_adopt_filter_repo_failure.pyand adopt-project.py:checkfinds, andexecuterefuses before any leg exists, a source symlink with an empty target and the source's ownproject.yamlkept in the root, andplanno longer offersrootfor that file (#303, #305) #387'stest_adopt_empty_symlinks.pyandtest_adopt_root_manifest.py: 988 passed, 2 skipped. The skips are the two above.checkfinds, andexecuterefuses before any leg exists, a source symlink with an empty target and the source's ownproject.yamlkept in the root, andplanno longer offersrootfor that file (#303, #305) #387's too), leg-path collisions, clone template, ignored shape files and submodules._read_pinscommit):tests/test_adopt_late_failure.pygave 3 failed, 83 passed, the threeAttributeErrorrows above.render-cli-reference.py --checkis ok on the merged tree. pyflakes reports the same four warnings onadopt-project.py,scaffold-project.pyandscripts/shape_materialize.pyas before the merge.Fix round 1, on 421e4e5. Each run had
git-filter-repo2.47.0 on PATH and the identity blanked withGIT_CONFIG_COUNT. Aghstub that logs and exits 97 was first on PATH, withGH_TOKENbogus and an emptyGH_CONFIG_DIR, and the sessionghguard came first inside each run.--elected-by,--reference,--nameand the elector's fallback are refused before anything exists when no file can hold them, so a byte that is not UTF-8 no longer creates repositories before a traceback (#282) #351's free-text values.--id null,--tracking-branch trueand a blank--elected-byare refused (#258, #261) #314 merge, on 74ce4d9's tree: the first two sets gave 577 passed, 2 skipped and 782 passed, 3 skipped. On 2bcfa69's tree (the merges alone, before the fix) they gave 542 passed, 2 skipped and 782 passed, 3 skipped. After the 45e01f4 merge,tests/test_agents_md_codes.py, repo hygiene and CLI reference: 110 passed.PYTHONPATHseparator no longer runs at all; a neutral product's digestvalidate-pins.pyskipped is anotenaming the product, from a child that can no longer reachghor a proxy; and a root whose manifest does not load and whosescripts/repo_shape.pyis gone is told to restore that file before its gate (#278, #297, #298) #357'stests/test_doctor_validator_rows.py. All passed, with only the skips above.Proof that fix round 1's tests test something:
On 2bcfa69's tree, with this head's two test files laid over it, 31 of the new rows fail:
pin_product_keyport: 2;check's S3 line: 1;adopt planrow: 1.The N3 rows pass there, as they must: they guard hunks this branch already had.
Mutation probes. Each hunk was reverted alone, in a throwaway worktree, and both test files were run: 18 of 18 were caught.
repr'd, printable or not (27).except(3).plancheck (11); only adopt-project.py: a valueexecutewrites into a file is asked whether a file can hold it, inplan, incheckand inexecutebefore any leg exists, so a byte that is not UTF-8 in--elected-by,--referenceor a local--sourcedirectory no longer pushes both legs beforewrite_lfraises (#255) #270's question asked (1); the formatting-character reason said otherwise (1); the report'spinline raw (1);check'swriteline raw (1)._pin_fixname guard (1); the_read_pinspath guard (1); the lower-casing (1); thedigest_sourcecheck (1); the not-text check (1); the gitlink filter (1).The issue rows reproduced. S1, S2 and N2 were reproduced on 2bcfa69's tree and shown gone on the fix (a probe driving both tools;
ghwas called 0 times).Before fix round 1. Runs were on 7a94b6b, with
git-filter-repo2.47.0 first on PATH and the repository-localuser.nameblanked withGIT_CONFIG_COUNT(a workstation whose standard checkout sets one failstest_the_scaffold_refuses_with_no_electoron main itself; CI sets none). 7a94b6b differs from ffd1297 intests/test_pin_source_unreadable.pyonly, so the three suites that do not read that file ran on ffd1297:pin_source_env_nameturns every character outside[A-Z0-9_]into_, so the neutral productsopen-glass,open_glassandopen.glassshare oneSHAPE_PIN_SOURCE_OPEN_GLASS, and a project that pins two of them gets aneutral-pin-source-unreadableFINDING (exit 1) for whichever one the variable does not name #275's tests, naming policy, repo hygiene, CLI reference, scaffold leg-path collisions: 571 passed;Earlier on 5690235 and d806e8f, before the #339 commit: the adopt suites (e2e, execute refusals, verify, spec-only, leg paths, root shape paths, plan escapes, leg-path collisions, clone template) 740 passed, and
tests/test_adopt_template_values.pywith #273's other adopt tests 489 passed, 2 skipped. They were not re-run on 7a94b6b: the #339 diff touchespin_digest_from_sourceand the call indeclared_pin_values, which those suites reach throughtest_adopt_pins.pyonly. Nothing was run on macOS or Windows.Proof the new tests test something:
OSErrorcatch (16 fail), the catch narrowed toOSErroronly (21), the missing-path clause (9), the clause narrowed toFileNotFoundError(3), theS_ISDIRjudgement (3), the loop guard (4), no resolve at all (3), the old.resolve()call (4), the reason asstr(exc)(12), and the file sentence as the OS's (3).--pin <name>is accepted byplan,checkandexecute("adoption verified"), but writes only the name intoneutral_product_pins:and nocontracts/<name>-pin.yaml, so the adopted root's ownvalidate-pins.py(and itsvalidate.yml) refuses withneutral-pin-missingfrom the first run #273's own 52 tests still fail on c7c02fa (51 fail or error; the one that passes checks the test's own table), and its 18 mutation probes were each caught.Re-verified after the merge. An independent re-verification of f9ed46f and 73d65c6 found the code correct:
open-glassandopen_glassshare oneSHAPE_PIN_SOURCE_<PRODUCT>, so--pinrefuses the pair andvalidate-pins.pynotes a declared one and reads the variable for neither (#275) #409 is carried whole. A 30,000-case fuzz of--pinlists through scaffold-project.py, validate-pins.py, repo_shape.py:open-glassandopen_glassshare oneSHAPE_PIN_SOURCE_<PRODUCT>, so--pinrefuses the pair andvalidate-pins.pynotes a declared one and reads the variable for neither (#275) #409's final refusal and this branch's port differed in 0 cases.scaffold-project.pyequals the tested file plus main's own changes that are not scaffold-project.py, validate-pins.py, repo_shape.py:open-glassandopen_glassshare oneSHAPE_PIN_SOURCE_<PRODUCT>, so--pinrefuses the pair andvalidate-pins.pynotes a declared one and reads the variable for neither (#275) #409's (same patch-id)._commit_the_splitkeeps adopt-project.py: every failure once a leg repository exists flushes stdout, then printsREFUSED <step>., what failed and one notice of what the run left, so a failedgit filter-repono longer exits 2 with an empty stderr (#171, #188, #177) #445'stryand adopt-project.py:executerecords inproject.yamlwhich leg the plan gave each submodule a leg keeps, and shape-doctor.py holds the submodule to that row, so one in the other leg is MISPLACED and a real adoption's placement readsok(#189) #394'sappend._read_pinsreads a loaded plan as before.Its findings were all in this description (the #506 bullet and three stale facts), now fixed. Runs on the head and on its merge with main 48585e6: 988 passed, 2 skipped; and 967 passed, 3 skipped.
Composition with open branches
git merge-tree --write-treewas run at 01:48Z on 2026-10-13, comparing hunks per file. Each PR head, fetched fromrefs/pull/<n>/headwith nogh, was merged with 73d65c6 and with origin/main 9736424 alone. Two sets were asked about:mergeFINDING naming the paths, withgit -C <root> statusas its NEXT, and the verdictINVALID (merge), exit 1, on a rung above every other but CANNOT ANSWER, so a mergegit commitrefuses no longer reads COMPLIANT, exit 0, oncecheckout --ourstakes the markers out, or DRIFTED about a leg that never moved (#424) #468 whose head main does not contain: docs/release-notes/v1.0.0.md: the v1.0.0 release notes, a draft for Brett to amend before he tags, saying what 1.0 contains and promises, how to pin it by commit, what changed on the way grouped by the five tracking areas, and every known limit as the input that triggers it and what the person sees (#472) #473, update-shape.py:apply --branchcompares the pin, the manifest and every pinned copy with HEAD before its first write and refuses a difference asupdate-branch-uncommitted, so an in-placeapplyleft uncommitted no longer ends in a commit its own validate-pins.py refuses, in git's "nothing to commit", or in "nothing to do" with no branch made (#436) #474, templates/assembly-root/scripts/bootstrap.py: a tracking branch behind the pin is fast-forwarded to it, and a fresh clone's branch past the pin with no commit of its own is moved back to it, sobump-leg.pythenmake bootstrap, andgit clone --recurse-submodulesthenmake bootstrap, leave the leg on its branch AT the pin, no longer detached undernot moving an existing branch(#475) #490, setup-project.py: every question - the yes, each install offer, the project name - is asked at the person's own terminal, read from the controlling terminal when stdin is the pipe the script arrived down, and a run with no terminal at all refuses naming that terminal as the way on and--yesonly as the person's own answer, so the README'scurl ... | bash -s --asks in the person's terminal instead of refusing, and an assistant's shell is no longer sent to re-run with the flag its first rule forbids it (#476, #492) #491, adopt-project.py: the tracking branch defaults to the source's default branch, topics are set before the pull request, and NEXT names scripts/bootstrap.py when the shape's Makefile went aside (#479, #480, #481) #493, repo_shape.py, validate-pins.py, family.py: a refusal built without a remedy prints itsREFUSEDline alone, the assembly root'svalidate-pins.pystill adds the lockstep remedy to its own, andaddrefuses a.gitmodulesgit cannot rea (#370) #494, shape-doctor.py and validate-pins.py: a gitlink that is not its pin is offered both repairs,bump-leg.py --tothe gitlink's commit to keep the move and--tothe pin's to undo it, so a gitlink committed by hand no longer ends at a gate t (#478) #495, shape-doctor.py: a conflicted shape pin's NEXT takes the pin alone and gives a copy conflicted beside it to a person to merge by hand, so pasting it no longer drops an edit kept with--accept-localand then says COMPLIANT (#485) #496, tests.yml: the Windows job installs git-filter-repo 2.47.0, so the 23 test files that skip without it run there and an adoption is executed on Windows for the first time, and the comments and docstrings that said that job installs none now say why it does (#471) #497, family Makefile:siblings,parkandresumerefuse by name whenscripts/siblings.pyis absent, and update-shape.py says the Makefile it copies calls it, so a holder updated without--addno longer fails oncan't open file(#489) #503, adopt-project.py:checkfollows each symlink the plan keeps where the shape writes its files, andexecuterefuses one the shape cannot write through, or one it writes beneath that git checks out as a file, before any leg exists, so such a link no longer breaks the run after both legs are pushed or has the shape's files written outside the assembly (#183, #178) #504 and README.md and AGENTS.md: 28 statements the v1.0.0 accuracy pass found false are corrected to what the tools do on main (#483) #505.Open state, read with
ghby the coordinator at 02:00Z and refreshed at 02:25Z after a re-verification on main 48585e6. Every pull request named below is open. #184, #94, #96, #98, #100 and #161 are closed, and #465 landed as 48585e6.fix/adopt-plan-quote-yaml-keywordshas no pull request. #506 (fix/scaffold-reuse-seeded-legs) opened after this list was made, and the re-verification reads it.read_pinrefusesupdate-pin-row, and the pinned validate-pins.py and validate-family.py reportshape-pin-row, when a shape pin'sfiles:is not a list, sofiles: 5,trueor1.5no longer endscheck,apply, shape-doctor.py and the project's own validator inTypeError: ... is not iterable(#290) #343, update-shape.py:apply --addrefuses a path under a file the root keeps asupdate-add-not-a-directorybefore it writes anything, and a write that fails after the copies isupdate-write-failed, rolled back, so neither leaves a rewritten copy and an unmoved pin behind a traceback (#285) #346, scaffold-project.py and family.py:--tracking-branchis asked ofgit check-ref-format --branchbefore anything exists, so~,a..b,x.lockanda/are refused under--dry-runand no longer die at the firstgit init -baftergh repo createhas made the repositories (#283) #355, shape-doctor.py: themanifest kindsrow puts back a shape or leg pin undercontracts/that does not load with the git commandpin_restorepicks from its own state, shows a copy the shape pin has a row for or a neutral product pin with a read-onlygit diff, and leaves any other file to a person whatever it is called, so its NEXT no longer tells the reader to delete the pin (#291) #358, shape_materialize.py:RULESET_HINTnames the run's tracking branch where it namedmain, as the pull request's base and the ref the scaffold and the family holder pushed (#324) #373, adopt-project.py:_spelledwrites a raw byte that is not UTF-8 as\udcNNand a character as\xNN, as the plan's ownpath:does, so the byte 0x85 and the character U+0085 no longer both read\x85, and 34 pairs of different names no longer read as one in a question or a finding (#329) #378, adopt-project.py:checkreports the split branch an earlier run left asexecute'sadopt-branch-exists, and a branch in git's way of it asplan-split-branch-taken, whichexecutenow refuses before any leg exists, so a source branchadoptno longer has both legs made beforegit filter-repofails (#320) #382, adopt-project.py:checkfinds, andexecuterefuses before any leg exists, a source symlink with an empty target and the source's ownproject.yamlkept in the root, andplanno longer offersrootfor that file (#303, #305) #387, adopt-project.py:executerecords inproject.yamlwhich leg the plan gave each submodule a leg keeps, and shape-doctor.py holds the submodule to that row, so one in the other leg is MISPLACED and a real adoption's placement readsok(#189) #394, family.py:initasks the holder's reader and validate-family.py about family.yaml before creating anything,--dry-runtoo, so--id null,--tracking-branch true, a blank--created-byand--created-on 20261011are refused (#307) #397, adopt-project.py:plan --helpsays what--spec-pathand--code-pathare, the default and key each records, and the two refusalsplanmakes on them, sodocs/cli.mdno longer lists the two flags that choose where a leg is mounted as bare lines (#340) #401, shape_advisory.py and shape-doctor.py: anoriginURL that is not UTF-8 names nothing, and themembersrow reads it asmake siblingsdoes, so a single repository is still advised, a leg clone is still sent to its root, andshape-doctor.py --rootno longer ends in a UnicodeDecodeError traceback (#385) #404, scaffold-project.py, validate-pins.py, repo_shape.py:open-glassandopen_glassshare oneSHAPE_PIN_SOURCE_<PRODUCT>, so--pinrefuses the pair andvalidate-pins.pynotes a declared one and reads the variable for neither (#275) #409, validate-pins.py (assembly-root template): a workflow file under.github/workflows/that is not UTF-8, or that this user cannot open, is ONEworkflow-unreadablerefusal naming every such file, exit 2, so the project's pins gate no longer ends in a UnicodeDecodeError traceback, exit 1, with no line naming the file (#359) #419, setup.sh:--helpfrom anywhere but a checkout is the shim's own few lines and clones nothing, and render-cli-reference.py refuses a tree that is not a checkout, so asking a download for help is no longer a clone of GitHub (#341) #452, Both roots' scripts/bootstrap.py:mainescapes what stdout cannot encode and spells each value it prints, soname: "a\ud83db"or日本underPYTHONIOENCODING=asciino longer endsmake bootstrapin a traceback (#391) #456, README.md, docs/handbook.html and the comments in bump-leg.py and update-shape.py say when a project's own gate reaches a network:validate-pins.pyasksgh apifor a neutral product's pin with the operator'sghcredentials, andmake bootstrap,bump-leg.pyandupdate-shape.py applyrun it in the operator's environment, so none of them says any more that a project runs its own gate offline or thatapplyis otherwise offline (#411) #465, family.py:bumprefuses a--tothat names an annotated tag, a tree or a blob asmember-to-not-a-commitbefore the checkout, naming the commit a tag names, and its dry run refuses alike, so a release tag's own id no longer exits 0 with a commit pinning the tag over a gitlink at its commit, which the holder'svalidate-family.pyrefused asmember-gitlink-mismatch(#405) #466 and bump-leg.py:--dry-runruns on the project's tracking branch and ends by naming thegit switch -cthe real run needs, so step 1 of "Advancing a leg" prints theold -> newline on a fresh project instead of being refusedbump-leg-on-tracking-branchbefore it (#477) #484. adopt-project.py:executerecords inproject.yamlwhich leg the plan gave each submodule a leg keeps, and shape-doctor.py holds the submodule to that row, so one in the other leg is MISPLACED and a real adoption's placement readsok(#189) #394's_commit_the_splitconflict and adopt-project.py:plan --helpsays what--spec-pathand--code-pathare, the default and key each records, and the two refusalsplanmakes on them, sodocs/cli.mdno longer lists the two flags that choose where a leg is mounted as bare lines (#340) #401'sdocs/cli.mdconflict are both resolved in f9ed46f, above.null,~,true,False,2026,1.5) is refused before anything is created, under--dry-runalike, through the one definition adopt-project.py asks, so--spec-path 2026no longer creates three repositories whose first bootstrap failsmanifest-leg-path, andadopt-project.py executeno longer saysadoption verifiedbefore that bootstrap fails (#225) #266, update-shape.py: a pinned copy,contracts/shape-pin.yamlor the manifest kept as a symlink, or under one, is refused asupdate-rewrite-symlinkbefore anything is written, andcheckcalls such a copycopy-is-link, soapplyno longer writes the standard's bytes at the link's end and reports a commit that does not hold them (#286) #347, adopt-project.py:checkfinds, andexecuterefuses before any leg exists, a planfollow_ups:that is not a list of text and anelected_on:thatelection_datedoes not read, with or without areference:, sofollow_ups: 5is no longer a traceback andelected_on: somedayis no longer refused under a flag neither command takes (#293, #294) #356, update-shape.py: every NEXT line a run prints quotes its root, upstream,--accept-localpaths, branch and lane trailer withshlex.quote, socheck'sapply,apply --branch'sgit -C <root> push && gh pr createandapply's owngit -C <root> checkout -b && git -C <root> commitpaste whole from a root undermy projects/,it's here/or$HOME/(#325) #374, update-shape.py, bump-leg.py and family.py:--trailercarries a non-ASCII value into the commit as typed under any locale and on a git withoutcommit --trailer, and refuses one whose bytes are not UTF-8 by name before anything is staged, soCo-Authored-By: JoséunderLC_ALL=C PYTHONUTF8=0is no longer refused as "not a trailer" and a direct caller's is no longer committed asJos??(#322) #376, adopt-project.py:checkandexecutespell a plan's hand-editedquestion:inplan-unresolved, itssource: commit:inplan-staleand a submodule name inmissing value for '<key>'with_spelled, so a line feed in any of them no longer forges aFINDINGline and an ESC no longer reaches the terminal (#328) #384, templates:project.yaml, the leg pins andfamily.yamlquote their text placeholders, so--id 007,--tracking-branch 2026,--spec-path trueor--org nullread back as typed, andfamily.py addquotes a member id alike (#259) #395, validate-repository-naming.py, repo_shape.py: a--link-sourcevalue isPRODUCT=PATHonly when its key names a chain link, and a link source that cannot be entered is a WARNING with the system's reason, not a traceback (#338) #453, setup-project.py: the root clone of--local-remote-diris--no-localand its two leg clones are rewritten tofile://, and the two session fixtures that clone a scaffold do the same, so a maintenance run packing the just-pushed objects mid-walk no longer diesfailed to copy file to ... No such file or directory(#440) #467, shape-doctor.py: a root whose index holds an unmerged path gets amergeFINDING naming the paths, withgit -C <root> statusas its NEXT, and the verdictINVALID (merge), exit 1, on a rung above every other but CANNOT ANSWER, so a mergegit commitrefuses no longer reads COMPLIANT, exit 0, oncecheckout --ourstakes the markers out, or DRIFTED about a leg that never moved (#424) #468, docs/release-notes/v1.0.0.md: the v1.0.0 release notes, a draft for Brett to amend before he tags, saying what 1.0 contains and promises, how to pin it by commit, what changed on the way grouped by the five tracking areas, and every known limit as the input that triggers it and what the person sees (#472) #473, update-shape.py:apply --branchcompares the pin, the manifest and every pinned copy with HEAD before its first write and refuses a difference asupdate-branch-uncommitted, so an in-placeapplyleft uncommitted no longer ends in a commit its own validate-pins.py refuses, in git's "nothing to commit", or in "nothing to do" with no branch made (#436) #474, templates/assembly-root/scripts/bootstrap.py: a tracking branch behind the pin is fast-forwarded to it, and a fresh clone's branch past the pin with no commit of its own is moved back to it, sobump-leg.pythenmake bootstrap, andgit clone --recurse-submodulesthenmake bootstrap, leave the leg on its branch AT the pin, no longer detached undernot moving an existing branch(#475) #490, setup-project.py: every question - the yes, each install offer, the project name - is asked at the person's own terminal, read from the controlling terminal when stdin is the pipe the script arrived down, and a run with no terminal at all refuses naming that terminal as the way on and--yesonly as the person's own answer, so the README'scurl ... | bash -s --asks in the person's terminal instead of refusing, and an assistant's shell is no longer sent to re-run with the flag its first rule forbids it (#476, #492) #491, adopt-project.py: the tracking branch defaults to the source's default branch, topics are set before the pull request, and NEXT names scripts/bootstrap.py when the shape's Makefile went aside (#479, #480, #481) #493, repo_shape.py, validate-pins.py, family.py: a refusal built without a remedy prints itsREFUSEDline alone, the assembly root'svalidate-pins.pystill adds the lockstep remedy to its own, andaddrefuses a.gitmodulesgit cannot rea (#370) #494, shape-doctor.py and validate-pins.py: a gitlink that is not its pin is offered both repairs,bump-leg.py --tothe gitlink's commit to keep the move and--tothe pin's to undo it, so a gitlink committed by hand no longer ends at a gate t (#478) #495, tests.yml: the Windows job installs git-filter-repo 2.47.0, so the 23 test files that skip without it run there and an adoption is executed on Windows for the first time, and the comments and docstrings that said that job installs none now say why it does (#471) #497, family Makefile:siblings,parkandresumerefuse by name whenscripts/siblings.pyis absent, and update-shape.py says the Makefile it copies calls it, so a holder updated without--addno longer fails oncan't open file(#489) #503 and README.md and AGENTS.md: 28 statements the v1.0.0 accuracy pass found false are corrected to what the tools do on main (#483) #505. validate-repository-naming.py, repo_shape.py: a--link-sourcevalue isPRODUCT=PATHonly when its key names a chain link, and a link source that cannot be entered is a WARNING with the system's reason, not a traceback (#338) #453, shape-doctor.py: a root whose index holds an unmerged path gets amergeFINDING naming the paths, withgit -C <root> statusas its NEXT, and the verdictINVALID (merge), exit 1, on a rung above every other but CANNOT ANSWER, so a mergegit commitrefuses no longer reads COMPLIANT, exit 0, oncecheckout --ourstakes the markers out, or DRIFTED about a leg that never moved (#424) #468 and setup-project.py: every question - the yes, each install offer, the project name - is asked at the person's own terminal, read from the controlling terminal when stdin is the pipe the script arrived down, and a run with no terminal at all refuses naming that terminal as the way on and--yesonly as the person's own answer, so the README'scurl ... | bash -s --asks in the person's terminal instead of refusing, and an assistant's shell is no longer sent to re-run with the flag its first rule forbids it (#476, #492) #491 were updated onto main since the first read and now merge clean. family.py:initrefuses to land in the standard's checkout and names a runnablescripts/family.py, printed commands quote paths, andadd,bumpandremoverefuse the tracking branch and print their branch's push (#488, #486, #487) #507 (lane 5's family.py and siblings.py print commands with unquoted paths, so under a folder with a space themember-git-dir-cachedremedy'srm -rfwould delete the wrong directory, andadd's push line fails #486-family.py init: AGENTS.md's own command lands the holder inside the openRepoShape checkout, and init's NEXT printspython3 family.py add, which cannot run #488) shares only the generateddocs/cli.mdwith this branch and merges clean with it.#is the row'swhy, on aWHYline of its own and"why"in--json, so a NEXT pasted into an interactive zsh -- which reads that#as a word -- no longer hands the comment to the command as arguments, runs what follows its;, or runs themake bootstrapin its backticks, and the template validator's line isenv PYTHONPATH=... python3 ...(#277) #402:shape-doctor.py5;--accept-localand then says COMPLIANT (#485) #496:shape-doctor.py10 andtests/test_shape_doctor.py1.Conflicts this branch adds:
checknames, andexecuterefuses before the work directory is made, a plan whosesource:orlegs:is not a mapping or whoseallow_empty_legs:orpaths:is not a list of whatplanwrites, and asks the same first of apins:that is not a list, so a number or a boolean in any of the four no longer ends both in a traceback andallow_empty_legs: codeis no longer read as four legs (#386) #435 (3efcc7a, lane 2'sfix/adopt-plan-field-types, for adopt-project.py:Plan.__init__,allowed_empty_legs,namesandopen_sourceiterate or call.geton a hand-edited plan'spins:,allow_empty_legs:,paths:,legs:andsource:without asking their type, so a scalar endscheckandexecutein a TypeError or AttributeError traceback, exit 1, andpins: openGlasspassescheck, adopts, and writes eight one-letter neutral-product pins that the project's own validate-pins.py refuses #386).adopt-project.py: 5 here, against 3 with main alone. They arerender_plan'spins:line andPlan.pins._pin_linesand_pin_products. The rest is under "Where the meaning meets".checkfollows each symlink the plan keeps where the shape writes its files, andexecuterefuses one the shape cannot write through, or one it writes beneath that git checks out as a file, before any leg exists, so such a link no longer breaks the run after both legs are pushed or has the shape's files written outside the assembly (#183, #178) #504 (37ef29a,fix/adopt-root-kept-symlinks).adopt-project.py: 2 here, against 1 with main alone. It is the materializer call in_commit_the_split.append=append, neutral_pins=_recorded_pins(plan), spell=_spelled.--reuse-empty-reporeuses a leg that is empty or whose one commit is the scaffold's own seed, and the ruleset hint offers that re-run, by an operator holding the bypass right, as its one exit, so a project a ruleset refused part-way is finished by one re-run, not refusedscaffold-remote-existsfor its own legs or sent to a pull request with no base (#462) #506 (119d8c4,fix/scaffold-reuse-seeded-legs).scaffold-project.py. With main alone, scaffold-project.py and family.py init:--reuse-empty-reporeuses a leg that is empty or whose one commit is the scaffold's own seed, and the ruleset hint offers that re-run, by an operator holding the bypass right, as its one exit, so a project a ruleset refused part-way is finished by one re-run, not refusedscaffold-remote-existsfor its own legs or sent to a pull request with no base (#462) #506 conflicts only indocs/handbook.html.--reuse-empty-reporeuses a leg that is empty or whose one commit is the scaffold's own seed, and the ruleset hint offers that re-run, by an operator holding the bypass right, as its one exit, so a project a ruleset refused part-way is finished by one re-run, not refusedscaffold-remote-existsfor its own legs or sent to a pull request with no base (#462) #506 insertsLEG_TEMPLATESright afterdeclared_pin_products, which this branch moved out, and rewrites_reusable_remotesfurther down. Keeping both sides breaks the file with an unterminated string literal.--reuse-empty-reporeuses a leg that is empty or whose one commit is the scaffold's own seed, and the ruleset hint offers that re-run, by an operator holding the bypass right, as its one exit, so a project a ruleset refused part-way is finished by one re-run, not refusedscaffold-remote-existsfor its own legs or sent to a pull request with no base (#462) #506'sLEG_TEMPLATESblock. Drop scaffold-project.py and family.py init:--reuse-empty-reporeuses a leg that is empty or whose one commit is the scaffold's own seed, and the ruleset hint offers that re-run, by an operator holding the bypass right, as its one exit, so a project a ruleset refused part-way is finished by one re-run, not refusedscaffold-remote-existsfor its own legs or sent to a pull request with no base (#462) #506'sdeclared_pin_values/declared_pin_products, which moved here, and this branch's three-line_reusable_remotesopener, which scaffold-project.py and family.py init:--reuse-empty-reporeuses a leg that is empty or whose one commit is the scaffold's own seed, and the ruleset hint offers that re-run, by an operator holding the bypass right, as its one exit, so a project a ruleset refused part-way is finished by one re-run, not refusedscaffold-remote-existsfor its own legs or sent to a pull request with no base (#462) #506 rewrites below. Resolved that way, pyflakes is clean and scaffold-project.py and family.py init:--reuse-empty-reporeuses a leg that is empty or whose one commit is the scaffold's own seed, and the ruleset hint offers that re-run, by an operator holding the bypass right, as its one exit, so a project a ruleset refused part-way is finished by one re-run, not refusedscaffold-remote-existsfor its own legs or sent to a pull request with no base (#462) #506's scaffold tests pass (88).fix/adopt-plan-quote-yaml-keywords(ed9968f; a branch with no pull request).adopt-project.py: 3 here, against 1 with main alone.render_plan'spins:line. Keep_pin_lines, which already sends product, repository anddigest_sourcethroughy._refuse_unholdable_values' signature. Keeppins: dict, with that branch's_validator_judgedand_named_as_readabove it.fix/adopt-late-failure-noticeis no longer on origin. adopt-project.py: every failure once a leg repository exists flushes stdout, then printsREFUSED <step>., what failed and one notice of what the run left, so a failedgit filter-repono longer exits 2 with an empty stderr (#171, #188, #177) #445, the late-failure notice, is on main and merged here.Where the meaning meets:
checknames, andexecuterefuses before the work directory is made, a plan whosesource:orlegs:is not a mapping or whoseallow_empty_legs:orpaths:is not a list of whatplanwrites, and asks the same first of apins:that is not a list, so a number or a boolean in any of the four no longer ends both in a traceback andallow_empty_legs: codeis no longer read as four legs (#386) #435 and adopt-project.py:Plan.__init__,allowed_empty_legs,namesandopen_sourceiterate or call.geton a hand-edited plan'spins:,allow_empty_legs:,paths:,legs:andsource:without asking their type, so a scalar endscheckandexecutein a TypeError or AttributeError traceback, exit 1, andpins: openGlasspassescheck, adopts, and writes eight one-letter neutral-product pins that the project's own validate-pins.py refuses #386. The coordinator has said this on adopt-project.py:checknames, andexecuterefuses before the work directory is made, a plan whosesource:orlegs:is not a mapping or whoseallow_empty_legs:orpaths:is not a list of whatplanwrites, and asks the same first of apins:that is not a list, so a number or a boolean in any of the four no longer ends both in a traceback andallow_empty_legs: codeis no longer read as four legs (#386) #435: adopt-project.py:checknames, andexecuterefuses before the work directory is made, a plan whosesource:orlegs:is not a mapping or whoseallow_empty_legs:orpaths:is not a list of whatplanwrites, and asks the same first of apins:that is not a list, so a number or a boolean in any of the four no longer ends both in a traceback andallow_empty_legs: codeis no longer read as four legs (#386) #435 (comment)checknames, andexecuterefuses before the work directory is made, a plan whosesource:orlegs:is not a mapping or whoseallow_empty_legs:orpaths:is not a list of whatplanwrites, and asks the same first of apins:that is not a list, so a number or a boolean in any of the four no longer ends both in a traceback andallow_empty_legs: codeis no longer read as four legs (#386) #435's_plan_key_findings(plan-bad-pins) keeps only text items inpins:, and itsPINS_REMEDIATIONtells a person to writepins: [openGlass].plan-pin-unrecorded.checknames, andexecuterefuses before the work directory is made, a plan whosesource:orlegs:is not a mapping or whoseallow_empty_legs:orpaths:is not a list of whatplanwrites, and asks the same first of apins:that is not a list, so a number or a boolean in any of the four no longer ends both in a traceback andallow_empty_legs: codeis no longer read as four legs (#386) #435's code, not run: on a tree with both, the planplan --pinwrites isplan-bad-pinsincheck._plan_key_findingstakes this branch's mappings, or leavespins:items to_read_pins;pins:that is not a list gives one finding, not bothplan-bad-pinsandplan-pin-malformed;PINS_REMEDIATIONsays the mappingplanwrites.tests/test_adopt_pins.py::test_a_plan_with_a_whole_pin_checks_okfails.--id null,--tracking-branch trueand a blank--elected-byare refused (#258, #261) #314. It landed, and it is merged here (above).fix/adopt-plan-quote-yaml-keywords. Its pin tests are written against the name-only--pin. They need thename@commitform, and their expectedpins: [...]line becomes_pin_lines's mapping.Left out
Two unwritable values are refused in two runs (lane 5's second review, n3).
plan --elected-by 'A<LF>B' --pin openGlass@<sha> --pin-source <a path holding an ESC>refuses the elector alone, exit 2. Re-run with the elector retyped, it refuses the source; adopt-project.py: a valueexecutewrites into a file is asked whether a file can hold it, inplan, incheckand inexecutebefore any leg exists, so a byte that is not UTF-8 in--elected-by,--referenceor a local--sourcedirectory no longer pushes both legs beforewrite_lfraises (#255) #270's refusal names every unwritable value at once. Reproduced on 421e4e5, withghcalled 0 times. One refusal would need a code change, not made here:_refuse_an_unrecordable_pin_sourcewould split into a function that returns the sentence;_checked_plan_inputswould ask it before adopt-project.py: a valueexecutewrites into a file is asked whether a file can hold it, inplan, incheckand inexecutebefore any leg exists, so a byte that is not UTF-8 in--elected-by,--referenceor a local--sourcedirectory no longer pushes both legs beforewrite_lfraises (#255) #270'sunwritable_valuesraise, and raise oneadopt-unwritable-valuethat names both, with both remediations;N2's question is
one_printable_line, notunwritable_reason, so it cannot simply join adopt-project.py: a valueexecutewrites into a file is asked whether a file can hold it, inplan, incheckand inexecutebefore any leg exists, so a byte that is not UTF-8 in--elected-by,--referenceor a local--sourcedirectory no longer pushes both legs beforewrite_lfraises (#255) #270's list.Docs. The README, AGENTS.md and
docs/handbook.htmlare unchanged, because none of them calls adopt's--pina name. A sentence saying adopt now takes the scaffold's--pinis a follow-up. It costs an AGENTS.md line-cap raise, and a README edit forces the handbook's README blob to change.A product spelled
null,trueorfalse. The scaffold already accepts--pin null@<sha>: it exits 0 and builds a root whosevalidate-pins.pyexits 1neutral-pin-entry-malformed. Adopt'splanwritesproduct: nullthroughy, andcheckthen findsplan-pin-malformedand refuses it before any leg exists. The one fix for both tools is inparse_pin, and it would change the scaffold's behaviour.Wording shared with the scaffold.
digest_sourceis written bare into the pin file, so a--pin-sourcepath holding#is cut short there.templates/assembly-root/contracts/neutral-product-pin.yamlandvalidate-pins.py'sneutral-pin-missingremediation name only the scaffold as the writer. Two comments inscripts/repo_shape.pystill name the scaffold'spin_digest_from_gh, in a digest-pinned shipped copy.Filed as follow-ups:
checkandexecutewrite a neutral pin'scommit:andtree_sha256:exactly as the adoption plan records them and read neither again, so a hand edit that makes the two disagree isplan okandadoption verified, and the adopted root's ownvalidate-pins.pythen exits 1 withneutral-pin-digest-mismatchon its first run #449: S3, a hand-editedcommit:ortree_sha256:that disagrees, with options (a) and (b);--pin-source <path>: the path is written as it stands into the new root'scontracts/<product>-pin.yaml(digest_source: local-clone (<path>), a bare placeholder) and is asked nothing first, so a clone at a path holding a line break scaffolds, exit 0, a pin file with a line of the path's choosing, and one at a path that is not UTF-8 creates all three repositories and then dies inwrite_lfwith aUnicodeEncodeErrortraceback, exit 1 #450: the scaffold half of N2, a--pin-sourcepath no file can hold;plan: a--pin-sourcethat nothing reads (no--pin, and for the scaffold no--referent-chainlink) is accepted in silence, even a path that does not exist, so--dry-runsaysnothing was created.andplanwrites a plan, each exit 0 with no word that the flag changed nothing, where the root's ownvalidate-pins.pynotes such a value since #295 #451: scaffold-project.py:pin_digest_from_sourcecatches onlyRefusal, so a--pin-sourcethat does not exist, is a file, or sits under a parent this user cannot enter ends--dry-runand the real run alike in aFileNotFoundError,NotADirectoryErrororPermissionErrortraceback (exit 1), where a directory that is not a clone is thepin-source-unreadablerefusal (exit 2) with its remediation #339's third decision (N5), a--pin-sourcethat nothing reads.validate-pins.pyprints a--pin-sourcepath raw inneutral-pin-source-unreadable, the twin of S1. It is a shipped template that scaffold-project.py, validate-pins.py, repo_shape.py:open-glassandopen_glassshare oneSHAPE_PIN_SOURCE_<PRODUCT>, so--pinrefuses the pair andvalidate-pins.pynotes a declared one and reads the variable for neither (#275) #409 and other open PRs edit, so it is left for its own change. No issue is filed for it yet.Windows and macOS were not run. Two assertions that depend on what Windows answers for a path (a name past the filesystem's limit, a path beneath a file) check only the refusal and its remediation there. Fix round 1's rows that need a control character or a byte that is not UTF-8 in a path, or a symlink to
git, are skipped on Windows by a mark, and so is fix round 2's ESC row. The rows for a no-break space and a zero-width joiner run everywhere, and so does the in-processgh-not-foundtest.Closes #273
Closes #339
🤖 Generated with Claude Code