Repository navigation
adopt-project.py: check names, and execute refuses before the work directory is made, a plan whose source: or legs: is not a mapping or whose allow_empty_legs: or paths: is not a list of what plan writes, and asks the same first of a pins: that is not a list, so a number or a boolean in any of the four no longer ends both in a traceback and allow_empty_legs: code is no longer read as four legs (#386) - #435
Conversation
…er a file can hold it, in `plan`, in `check` and in `execute` before any leg exists, so a byte that is not UTF-8 in `--elected-by`, `--reference` or a local `--source` directory no longer pushes both legs before `write_lf` raises (#255) Since #247 the plan writer keeps a lone surrogate (a byte that is not UTF-8, as Python decodes argv and paths) as `\udce9`, and the reader gives it back. `plan --elected-by b'Jos\xe9'`, `--reference b'ref-\xe9'` and a source under a directory `caf\xe9` with local remotes therefore passed `plan` and `check`, and `execute` created and pushed both leg remotes before `write_lf` raised `UnicodeEncodeError` (exit 1). Before #247 `plan` raised and nothing was created. A hand-edited `\ud800` in `elected_by`, `elected_on`, `reference`, `visibility`, a pin or a follow-up ended the same way, and a NUL or a line break there went into the manifest. `shape_materialize.unwritable_reason` asks the question, beside `one_printable_line`: one line, no lone surrogate, no control character. It is `one_printable_line` less `Cf`, and it takes the empty value: a Persian name joins with U+200C, which a file holds and `execute` adopted before #247 and since, and an empty `elected_by` is what `plan` records when `git config user.name` names nobody. - `plan` refuses `--elected-by` and `--reference` as `adopt-unwritable-value` and writes no plan. - `check` reports `elected_by`, `elected_on`, `reference`, `visibility`, each pin and each follow-up as `plan-unwritable-value`, through `_entry_findings`, so `execute` refuses the same findings in `check`'s words through `_refuse_what_check_finds`, before `_create_leg_remotes`. - `execute` refuses a local clone URL (the source's own path under `--local-remote-dir`) as `adopt-unwritable-value`, right after `_repository_urls`. `check` cannot know it will be written, and against GitHub it is an https URL, so `plan` and `check` still pass such a directory, as #247 made them. - `Source.open` refuses a `local_path` that does not exist and is not text as `source-unresolvable`, where a NUL or a surrogate reached `print` or `subprocess` as a traceback. - `check`'s WHAT WILL HAPPEN shows the plan's visibility, adopt branch, leg paths and follow-ups through `_shown`, so a surrogate there no longer raises on a strict stdout before the finding is printed. tests/test_adopt_template_values.py covers each argv route, the local source directory, each hand-edited value, `local_path`, the printed values, and controls (`José`, U+200C, a `café` source directory) that still adopt. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…value `execute` writes is refused before anything exists, and the clone-URL refusal's remediation moves the source and plans again, so no README line is shown reversed and following the remediation adopts instead of raising (#255) Fix round 1 on PR #270, answering lane openRepoShape-2's review at aed9190 (F1, F2, F3, and N1's test point). F1. `unwritable_reason` was `one_printable_line` less all of `Cf`, so U+202A..U+202E and U+2066..U+2069 passed `plan`, `check` and `execute` and went raw into `project.yaml`, the README's `Elected by` line and the split commit's message; a viewer showed the rest of that line reversed. Issue #255's question, `one_printable_line`, refuses them. The question is now `one_printable_line` less `Cf` other than those nine (`BIDI_CONTROLS`, by code point), plus the empty value. The joiners U+200C and U+200D, U+00AD, U+FEFF and the marks U+200E, U+200F and U+061C stay writable: a Persian name needs U+200C, and a mark sets no span's direction. `plan` refuses such a flag as `adopt-unwritable-value`, `check` finds such a plan value as `plan-unwritable-value`, `execute` refuses it before any leg exists, and a local clone URL holding one is `adopt-unwritable-value`. F2. The test file held a raw U+202E, which SonarCloud's text:S6389 flags, and other invisible literals a reader cannot see. Each is now an escape; only `é` and `日本` stay literal. The two composite assertions SonarCloud's python:S9073 flagged are four, each with a message. F3. The clone-URL refusal said to clone the source to a UTF-8 path and pass it to `execute` with --source. Followed, that raised: the clone's `origin` is the old path, and `_remote_repository` reads it as UTF-8. The remediation now says to rename or move the source and run `plan --source <its new path>`, `check` and `execute` again, and the test follows it to `adoption verified`, for a byte that is not UTF-8 and for a bidi override in the directory's name. N1. The same-sentence test now asserts both exit codes and that no run raised. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…er.name` as `--elected-by (not given, so `git config user.name`)` when it refuses it, so the refusal no longer names a flag the person never passed (#255) Fix round 1 on PR #270, N2 of lane openRepoShape-2's review at aed9190. With no --elected-by, `plan` records `git config user.name`. A TAB there was refused as `--elected-by is 'Jo\tSmith'`, naming a flag the operator never passed. It is now named as #181's `FLAG_DEFAULTS` names a value `plan` filled in, and the remediation says to pass the flag or correct `git config user.name`. A test runs `plan` under `GIT_CONFIG_GLOBAL` holding a TAB name, and skips where a repository's own `user.name` answers first. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the manifest back through the reader instead of matching its line, so it holds on main, which since #250 writes a YAML template's value as `double_quoted` spells it (#255) Fix round 1 on PR #270. Built on the merge with main be2790e, the U+200C control failed: `execute` adopted, but main b30595c (#250) renders a value between a YAML template's double quotes through `double_quoted`, so `project.yaml` holds `"AliReza"` with the character spelled as an escape, and the test looked for it raw. The reader gives back U+200C either way, so the control now asks the reader, and also asks for the name in the shape's README, which is not YAML and holds it as it stands on this branch and on main. GitHub's checks run on the pull request's merge with main, so they would have failed here. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lvable` before anything runs, so `gh repo clone` is no longer handed it as its first argument, where gh's own parser read it as one of its options (#255) SonarCloud's S8705 on #270 asked whether a value reaches a command as an option. The lane's re-verify found one that does: `plan --source=-u/x` is no path that exists and holds a `/`, so `Source.open` took it for an `org/repo` name and ran `gh repo clone -u/x <dir> -- --quiet`. gh read `-u` as `--upstream-remote-name` with the value `/x`, and `--source=--upload-pack=x/y` as a flag it does not know. A hand-edited `local_path:` or `repository:` reaches the same call from `check` and `execute`. It cannot reach git as an option (gh hands git only what follows its `--`), and main does the same. The guard sits in the condition this PR already edits. No GitHub owner name begins with `-`, so nothing gh could clone is refused, and an existing path that begins with `-` is still taken as a path. The tests put a stub `gh` first on PATH that records each call: `plan` with either value, and `check` and `execute` on a plan whose `local_path:` holds it, each exit 2 `source-unresolvable` with no traceback and no leg, and the stub records nothing. Against 7e9cdc3's code all four fail with the stub's record of `repo clone -u/x ...` and `repo clone --upload-pack=x/y ...`. A control shows the stub records the clone of `testorg/Thing`. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s `_entry_findings` composed with #255's, and this PR's pair renamed `unwritable_findings`/`unwritable_remediation` #231 (6f45bf9) puts each entry's path first in `_entry_findings` and returns `unsafe_remediation` ahead of the rest; this branch appends each value `execute` writes into a file that no file can hold. The two hunks are its docstring's order sentence and its return. The sentence keeps main's order and appends this PR's clause; the return is `(findings + submodule_findings + unwritable_findings, unsafe_remediation + remediation + unwritable_remediation)`, so the paths still come first, as #196 needs. This PR's local pair `value_findings, value_remediation` is renamed `unwritable_findings, unwritable_remediation`. #261's branch (ca21b57) binds `value_findings, value_remediation` in the same function, from `_manifest_findings`: a resolution keeping both lines under one name would silently drop one PR's findings. With the rename it cannot. The rest of main (#254/#281, #238/#287, #203/#236, #204/#227, #249/#250, #241, #215 and the earlier merges) merges with no conflict. The resolution is the lane's re-verify one (`resolve-main.py`, 385 passed on 6f45bf9), rename aside. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s `_entry_findings` order sentence composed with #255's #313 (71b58a1, #262 and #263) makes `check` find a kept root path where the shape writes a directory or its `shape/` copy, through `_root_shape_findings` called from `_leg_findings`, and makes `shape_materialize` refuse a copy whose own path is taken. This branch appends each value `execute` writes into a file that no file can hold to `_entry_findings`. The one conflict is that function's docstring, the sentence naming the order of its findings: it keeps #313's words (each entry's `leg:`, the two mount paths and what the root keeps where the shape writes, then the source's own submodules) and appends this PR's clause, ", then each value `execute` writes into a file that no file can hold (#255)". Nothing else in the file conflicts: the return still reads `(findings + submodule_findings + unwritable_findings, unsafe_remediation + remediation + unwritable_remediation)`, and `_leg_findings` carries #313's findings inside `findings`. Main's c0ed615 (#308, family.py) comes with it and touches none of this branch's files. Main has since moved on to c7c02fa (#315, family.py); this merge stops at 71b58a1. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… exists, a plan `follow_ups:` that is not a list of text, so `follow_ups: 5` is no longer a `TypeError` traceback and `abc` is no longer written as three follow-ups (#293) `plan` writes `follow_ups:` as a block list, and its three readers (`_plan_values`, `_print_what_will_happen`, `_commit_the_split`) iterated the value with no test that it was one. A hand-edited `follow_ups: 5`, `true`, `false` or `1.5` ended `check` in `TypeError: ... is not iterable`, exit 1, with no FINDING naming the key. On main `execute` died the same way in `_commit_the_split`, after both legs were pushed; since #270 it dies in `_plan_values`, before any leg, but still as a traceback. `abc` went into the split commit's message as `- a`, `- b`, `- c`, and an item `- true` or `- null` as `True` or `None`. The plan is untrusted input (`cmd_execute`'s own comment), so the value is asked before it is used: - `_plan_follow_ups` is the one read for the three readers: the items of text of a `follow_ups:` list, and nothing for anything else. - `_follow_ups_findings` is a `plan-bad-follow-ups` FINDING for a `follow_ups:` that is present and not a list, naming what it holds (`the number 5`, `the text 'abc'`, `a mapping of 1 key(s)`), and one for each item that is not text. An empty list and a bare key stay valid. - They join `_entry_findings` through `_plan_field_findings`, last, so `check` reports them and `execute` refuses them through `_refuse_what_check_finds`, before `_create_leg_remotes`, under the finding's own code, with a remediation that says how `plan` writes the list. Tests in tests/test_adopt_execute_refusals.py: `check` (a finding, exit 1, no traceback) and `execute` (the refusal, no leg, no split branch) for nine edits, three controls, and one run through `main` with `git filter-repo` that is refused with exit 2 and then adopts from the corrected plan. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ted_on:` whether or not the plan has a `reference:`, and `execute` refuses what it finds before any leg exists, so `elected_on: someday` is no longer refused after both legs are created, under `--elected-on`, a flag neither command takes (#294) `check` never read `elected_on:`. `execute` read it in `_template_values` only for a plan with no `reference:`, where `default_reference` asks `election_date`. That runs after `_create_leg_remotes`, so a hand-edited `elected_on: someday` passed `check` (`plan ok`). `execute` then created both leg repositories and only afterwards refused `election-date-malformed`. The refusal named `--elected-on` and said to re-run with it, but only `plan` takes that flag. A second `execute` met `leg-remote-exists`. With `reference:` kept, the date was never read, and `execute` wrote `someday` into the manifest. `_elected_on_findings` asks `election_date` about `str` of what the plan holds. That is the value `_template_values` takes, and the rule `plan` asks of `--elected-on`. It asks whether or not the plan has a `reference:`. A date it refuses is a FINDING `election-date-malformed` that names the plan's `elected_on:` and no flag, with a remediation that says to edit the key in the plan file. It joins `_plan_field_findings` ahead of the follow-ups, in the order the plan file holds the two keys. `execute` refuses it through `_refuse_what_check_finds`, before any leg exists. The rule stays `election_date`'s, unchanged. It reads `2026-W41-6` and `20261011` from Python 3.11, and the project's validator refuses both. That is #261's to rule on (PR #314 holds `election_date` to the validator's `DATE_RE`). Asking the same function keeps `plan`, `check` and `execute` in agreement now, and asks the tighter rule with no edit once it lands. Only the words are this caller's: `election_date`'s refusal names `--elected-on`, which is right for `plan` and wrong here. Only that refusal is a finding about the date. Anything else `election_date` raises is raised as it is, so a date it never judged is not reported as malformed. An example is #314's `manifest-validator-unusable`, when the validator it consults is missing. Tests in tests/test_adopt_execute_refusals.py: - `check` and `execute` for five dates (four with no `reference:`, one with it kept), and four controls; - `check`'s verdict equals `election_date`'s on the running interpreter, for `2026-W41-6`, `20261011`, a date and a word; - a stricter `election_date`, stood in by monkeypatch, refuses `2026-W41-6` before any leg; - another refusal from `election_date` is raised as it is; - both findings in one plan; - one run through `main` that is refused with exit 2 and no leg, then adopts from a corrected plan with no `reference:` and records the reference its date chooses. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#270 (fix/adopt-template-values-utf8) landed on main as the squash f795cf7. Its final head was 3cee853, which had merged main e43fa95. This branch was stacked on #270 and carries an older copy of #270's commits, so a plain merge of main would conflict in adopt-project.py, where the squash and those commits touch the same lines. Merging #270's final head comes first so the next merge, of origin/main, can take this branch's side of the files #270 changed. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ter #270 landed as the squash f795cf7 #270 landed on main as the squash f795cf7, and this branch was stacked on it. The previous merge, ead7716, brought #270's final head 3cee853 into this branch. This merge brings in main. adopt-project.py conflicted, and this merge takes this branch's own version of the whole file. That is right because the squash's content of adopt-project.py (and of tests/test_adopt_template_values.py) is 3cee853's, which the previous merge already brought in: `git diff 3cee853 f795cf7` over those files is empty. Of the main commits not in 3cee853, only f795cf7 touches adopt-project.py, tests/test_adopt_template_values.py or scripts/shape_materialize.py. So main adds nothing to those files beyond what this branch already carries, and no other file conflicted. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…g is made, a plan whose `source:` or `legs:` is not a mapping or whose `pins:`, `allow_empty_legs:` or `paths:` is not a list of what `plan` writes, so a scalar no longer ends both in a traceback and `pins: openGlass` no longer adopts with eight one-letter pins (#386) `Plan` read five keys of a plan file without asking their type: the constructor iterated `paths:` and `pins:`, `allowed_empty_legs` iterated `allow_empty_legs:`, `names` called `.get` on `legs:`, and `source_commit` and `open_source` on `source:`. The file is edited between `plan` and `execute` on purpose, and `x or []` rescues only the falsy values. So a number or a boolean ended `check` and `execute` in a `TypeError` or an `AttributeError`, exit 1, naming Python's error and not the key. Text, which is iterable, was read one character at a time: `pins: openGlass` passed `check`, adopted, and wrote eight pins, `G a e l n o p s`, that the root's own `validate-pins.py` refused. An item of `pins:` that is not text was written as Python spells it (`True`, `{'a': 1}`), and `allow_empty_legs: code` consented for four legs, `c`, `o`, `d` and `e`. Each key is now read through one reader that takes only what `plan` writes (`_plan_list`, `Plan.source`, `Plan.names`), and `_plan_key_findings` names what the reader left out in #293's words: a code per key (`plan-bad-source`, `plan-bad-legs`, `plan-bad-pins`, `plan-bad-allow-empty-legs`, `plan-bad-paths`), `_read_as` for what the file holds, one finding per item of `pins:` that is not text and of `allow_empty_legs:` that is not `spec` or `code`, and each key's remediation said once. An absent or bare key is not a finding. They are asked FIRST, right after `Plan.load`, and not in `_entry_findings` beside #293's and #294's. These keys are what the rest of the run is built from: the source is opened from `source:`, the names `execute` refuses on first come from `legs:`, and the constructor builds the entries and pins. Every later finding would be a statement about what the reader made of them (a `paths:` that is not a list reads as every source path uncovered). So `check` prints them, with #293's and #294's beside them so that every bad key of the plan is named in one run, and exits 1; `execute` refuses the same list in `_refuse_what_check_finds`' shape (the first finding's code, each finding as `check` prints it, capped), exit 2, before `_work_root` makes the work directory and before the source is opened or a leg exists. `legs:` is still kept by the constructor as it stands: the doctor's placement plan carries it as a list, and `tests/test_shape_doctor.py` hands that to `Plan` and `_leg_findings`. Only `check` and `execute`, reading a plan file, name it. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ter #245 and #371 landed Main took #245 (161c6c9) and #371 (a983e6a) since this branch last merged it, and both changed `_entry_findings` where this PR does, so GitHub could not merge the branch. `adopt-project.py` was the only file that conflicted, in 2 hunks, both in `_entry_findings`; every other file came from main as it is. - The docstring: main's text, with its ending "and last each relative url of a kept submodule that names another repository from its leg (#190)" changed to "then each relative url ... (#190), and last each plan value `execute` reads late and `_plan_field_findings` asks about (#293, #294)", so this PR's findings still come last. - The return: main's three-tuple, with `notes` kept. The findings are `findings + split + unwritable_findings + moved + field_findings` and the remediations are the same order, ending in `field_remediation`. `cmd_check` and `_refuse_what_check_finds` already unpack three values on main, and both keep their `local_remote_dir=None` defaults, so this PR's tests that call `_refuse_what_check_finds(plan, source)` need no edit. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…merged main a983e6a #386's branch is stacked on #356's fix/adopt-check-hand-edited-plan and was built at 5a5011d. #356 has since merged main a983e6a, which brings #245 (a relative `.gitmodules` url resolved from the source's remote and the leg's) and #371 (`execute` verifies before it exits on a refused `gh pr create`) into adopt-project.py, with #351, #350, #345 and #372 in other files. The merge is clean. `cmd_check` and `cmd_execute` still ask `_plan_type_findings` and `_refuse_a_mistyped_plan` first, right after `Plan.load` and before `_work_root`, so nothing #245 or #371 added runs on a mistyped `source:`, `legs:`, `pins:`, `allow_empty_legs:` or `paths:`. #245's new reader, `_kept_relative_urls`, takes `plan.entries`, which `_plan_list` already reads only from a list, and every reader of `plan.legs` asks its type. #245 gives `_refuse_what_check_finds` a `local_remote_dir` and rewrites its first line; `_refuse_a_mistyped_plan` shares only its shape and `REFUSED_FINDINGS_SHOWN`, not its text, so neither needs a change. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ted_on:` is "no date", so an empty text, `false`, `0`, `[]` and `{}` stay findings in `check` and refusals before any leg, and `if not elected_on:` no longer passes the suite (#294)
Review of 5a5011d (F1): `_elected_on_findings` returns nothing for `elected_on is None` only, and `""` and the other falsy values reach `election_date`, so `check` exits 1 with `FINDING election-date-malformed` and `execute` exits 2 with no leg. No test pinned it: changing `is None` to `not elected_on` passed all 81 tests, and under that change `check` says `plan ok` and `execute` creates both legs before it refuses.
`empty-text` is a row of BAD_DATES, so the `check` test and the `execute` test both take it. A `check` test of its own takes `false`, `0`, `[]` and `{}`, because BAD_DATES' assertion says "the text" and these are not text. Under the mutant the six new cases fail, `check` exiting 0 and `execute` having made the leg remotes; at this commit they pass. Tests only: adopt-project.py is unchanged.
Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…` writes ("the form `execute` writes into the project's `project.yaml`"), and `_plan_field_findings` says the order `plan` writes the two keys, so neither overstates what the code does (#294)
Review of 5a5011d (N1, N2), both wording in this PR's own text.
N1: the docstring said the findings come "in the order the plan file holds the two keys". The order is fixed, date then follow-ups, and it is the order `plan` writes them (`render_plan` emits `elected_on` before `follow_ups`). It now says so, and so does the both-findings test's docstring.
N2: the finding called the date "the one form `execute` reads". That does not hold on Python 3.11 and later until #314 lands: `20261011` and `2026-W41-6` pass `election_date`, and `execute` writes them into `project.yaml`. It now says "the form `execute` writes into the project's `project.yaml`", which is true on every Python. The `check` test pins the new words and that the old ones are gone: at the reviewed adopt-project.py its six rows fail, and they pass here. The two `startswith` assertions match only up to "YYYY-MM-DD" and did not change.
Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…writes every other value, so `plan --pin 2026` no longer writes a plan its own `check` refuses as `plan-bad-pins`, and `--pin 007` is no longer declared as the pin `7` (#386) #386's item check names an item of `pins:` that the reader does not give back as text. `render_plan` wrote the line as `"pins: [" + ", ".join(pins) + "]"`, the one value in the plan not written through `y`, and `PIN_NAME_RE` takes names that are digits: `repo_shape.parse_yaml` reads a bare `2026` as the number 2026, `007` as the number 7 and `1.5` as a float. So at 4d8a149, `plan --pin 2026` (or `007`, or `1.5`) exited 0 and the plan it wrote failed `check` with `FINDING plan-bad-pins: item 1 of pins: is the number 2026, not text`. Before #386 `2026` was declared as `str(2026)`, and `007` silently as `7`. `y` double-quotes a name the reader would take for a number, so each comes back as the text `--pin` was given, which is the invariant the writer's own comment states: everything it emits is read back by `check`. A bare YAML keyword (`--pin true`, `--pin null`) is still written bare, as `y` writes one everywhere (#181); `check` now names it where the pin `True` used to be declared, or the pin dropped. The new `test_a_pin_plan_writes_reads_back_as_the_name_it_was_given` (`2026`, `007`, `1.5`, and `openGlass` as the control) runs `plan --pin`, answers the plan's questions and asks `check` and `Plan.load`: at 4d8a149 the three numeric names fail (`check` exit 1, `plan-bad-pins`); on #356's 0aff5ae `007` fails (`['7'] == ['007']`); all four pass here. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s fix round #386's branch is stacked on #356's fix/adopt-check-hand-edited-plan, last merged at 0aff5ae. #356's fix round added f8e6de1 (tests only: the empty-text BAD_DATES row and a `check` test for an `elected_on:` of false, 0, [] and {}) and dbfad70 (the `elected_on:` finding says "the form `execute` writes into the project's `project.yaml`", and `_plan_field_findings`' docstring says the order `plan` writes the keys). The merge is clean and adds exactly 0aff5ae..dbfad70. No #386 test asserts the `elected_on:` finding's text: `test_every_key_that_is_wrong_is_named_in_one_run` puts #293's `follow_ups:` beside #386's keys and asserts its code and its remediation's first words, which dbfad70 does not change. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Sorry @brettheap, you've used your own review budget of 250,000 diff characters for the last 7 days. You can request another review in 19 hours and 10 minutes by commenting |
Reviewer's GuideThe PR hardens plan parsing by validating the five structurally significant keys through type-aware readers, reporting all malformed values in Sequence diagram for early plan validation in check and executesequenceDiagram
participant CLI
participant Plan
participant Validation as _plan_type_findings
participant WorkRoot as _work_root
participant Source as open_source
CLI->>Plan: load(plan_path)
CLI->>Validation: _plan_type_findings(plan)
alt malformed plan keys or items
alt check
Validation-->>CLI: findings and remediation
CLI-->>CLI: exit 1
else execute
Validation-->>CLI: findings and remediation
CLI-->>CLI: Refusal(first finding code)
CLI-->>CLI: exit 2
end
else valid plan structure
CLI->>WorkRoot: _work_root(args)
CLI->>Plan: open_source(override, work_root)
Source-->>CLI: source
end
Flow diagram for type-aware plan readers and findingsflowchart TD
Load["Plan.load"] --> Validate["_plan_type_findings"]
Validate --> Source["source: mapping"]
Validate --> Legs["legs: mapping"]
Validate --> Lists["paths:, pins:, allow_empty_legs: lists"]
Lists --> PinItems["pins: text items"]
Lists --> EmptyLegItems["allow_empty_legs: spec or code"]
Source --> Findings["Named plan-bad findings"]
Legs --> Findings
Lists --> Findings
Findings --> Check["check: print findings, exit 1"]
Findings --> Execute["execute: refuse before side effects, exit 2"]
Validate -->|no malformed values| Continue["Continue normal source and work-root processing"]
Flow diagram for quoting generated pin namesflowchart LR
Pin["--pin name"] --> Render["render_plan"]
Render --> Y["y(pin)"]
Y --> YAML["pins: [quoted YAML values]"]
YAML --> Reader["Plan.pins"]
Reader --> Check["check accepts numeric-looking names as text"]
File-Level Changes
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
@codex review |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Codex Review: Didn't find any major issues. What shall we delve into next? Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
…landed, with one guard in _manifest_values and #314's 5 elected_on: rows retargeted to election-date-malformed #314 (scaffold-project.py and adopt-project.py ask validate-manifest.py about the manifest's values) landed on main as 3ef059c. Its adopt-project.py is a5438a5's merged with 92e29d9, the composition pr-body-v3.md planned. adopt-project.py conflicted in 2 hunks of _entry_findings, both kept: - the docstring lists #261's validator findings, then #190's relative urls, and last this PR's plan values (#293, #294); - the return is findings + split + unwritable_findings + value_findings + moved + field_findings, the remediations in that order, and notes. Main's auto-merged paragraph said the relative urls of #190 come "last"; it now says the plan values come after them. The guard: _manifest_values returns None when _elected_on_findings(plan) has a finding, right after its _check_names refusal. Without it, a plan with no reference: had check exit 2 on election_date's own refusal, raised from _template_table through default_reference and naming --elected-on, before this PR's finding was listed; with reference: kept, the date was two findings, the validator's and this PR's. In plan the guard is a no-op: _checked_plan_inputs asks election_date of --elected-on first. #314's 5 rows for an elected_on: that election_date now refuses get this PR's one election-date-malformed finding, so they are retargeted to it in tests/test_adopt_manifest_values.py (said_of gives each row's finding, REFUSED prefix and remediation), and none is dropped: - elected-on-iso-week in test_check_finds_a_value_the_validator_refuses, test_execute_refuses_it_before_any_leg_exists and test_check_finds_it_from_a_tree_with_no_git_as_in_a_checkout; - elected-on-basic-format in test_execute_refuses_a_text_value_the_reader_misreads_before_any_leg and test_a_text_value_the_reader_misreads_is_one_finding_on_this_rule_alone, where _manifest_findings is now ([], []) and _plan_field_findings holds the one finding. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…landed: `_commit_the_split` keeps #445's reported steps and reads `follow_ups:` through `_plan_follow_ups` Main moved from dd8d0ea to 580d7df: decff87 (#267), dba5fb0 (#403), 245d775 (#446), 75052cb (#445), ec8e09a (#406) and 580d7df (#416). Only #445 touches adopt-project.py, and it conflicted in one hunk of `_commit_the_split`. #445 wraps that function's body in a `try` that names each step (cloning the source for the split, mounting the legs, committing the split, pushing the split branch), so a failure once both legs are pushed flushes stdout, prints `REFUSED <step>.`, what failed and one notice of what the run left. This branch reads `follow_ups:` there through `_plan_follow_ups`, the one read of it (#293). The resolution keeps #445's `try`, steps and reports whole, and carries this branch's read into its "committing the split" step: git had kept main's `[str(f) for f in plan.get("follow_ups", [])]` inside the `try`. The two contracts meet without overlap. `plan-bad-follow-ups` and `election-date-malformed` are findings of `_entry_findings`, which `execute` refuses through `_refuse_what_check_finds`. That runs before #445's `_refuse_an_occupied_work_dir`, `_refuse_a_split_branch_left_behind` and `_create_leg_remotes`, so these are refusals before any leg exists: exit 2, `REFUSED <code>:` and the remediation that says no leg repository was created and nothing was pushed, as #445's own refusals before the legs say, and no notice. #445's report covers the failures once a leg exists, which these values no longer reach. The guard in `_manifest_values` is unchanged; #445 does not touch it. #445's comment over `_template_values` in `cmd_execute` named "an `elected_on` that is not a date" as a refusal the table can raise. With #294's finding, `_refuse_what_check_finds` refuses such a date first, so the comment now says the date no longer reaches the table. #445's test of a table refusal stubs `_template_values` and is unchanged. tests/test_adopt_execute_refusals.py: the two end-to-end runs through `main` (#293, #294) now also assert that the refusal says no leg was created and that #445's notice ("NOTHING has been rolled back") does not follow it. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… before its date guard, so `check` and `execute` refuse a plan with a bad `elected_on:` and a bad organisation or id for the same first problem (#294) The guard this PR added to `_manifest_values` (return None when `_elected_on_findings` has a finding, so #314's validator is not asked about a date this PR already reports) returned before the function's `checked_value("org", ...)` and before `_template_table` read `plan.project_id`. Those two are where `check` meets a bad organisation and a bad id; `execute` meets the organisation earlier, in `_repository_urls`, and the id here, through `_refuse_what_check_finds`. So with `elected_on: someday` beside them: - `org: "-evil"`: `check` exited 1 listing only the date, and `execute` exited 2 `REFUSED unsafe-value`; - `id: "North Wind"`: `check` exited 2 `REFUSED plan-bad-id` (from the topic line it prints later), and `execute` exited 2 `REFUSED election-date-malformed`. On main 580d7df both commands name `unsafe-value`, and `plan-bad-id`, for those plans. The organisation is now refused, and the id read, before the guard, so both commands name the same first problem again, as on main, and a date-only plan is still one finding in the plan key's own words, not `plan-manifest-invalid` beside it. The docstrings of `_manifest_values` and `_manifest_findings` now also say they return nothing for such a date. tests/test_adopt_execute_refusals.py: `test_a_bad_date_does_not_change_what_check_and_execute_refuse_first`, for the organisation and the id, with `reference:` kept and removed: `check` exits 2 on the code `execute` refuses with, and no leg is created. All 4 fail on 88c430c's code and pass here. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Lane: openreposhape-5 (openRepoShape-5) T (#508, for #273 and #339) landed at 04:37Z as aa4e1f5. Against main, this PR now has 6 conflict hunks in
|
#435 was stacked on #356, which squash-landed on main as 965420b. Its final head 2fdfbed is a server-side merge of main into #356's 5982bbe, so it brings #356's last fix (the `org:` refusal and `id:` read ahead of the election-date guard), #445's REFUSED contract and #401's help text. This is step 1 of bringing a stacked branch onto main after its parent's squash: `git merge-tree 2fdfbed 3efcc7a` was rc 0, and this merge is clean. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…this branch's side of #356's three files (#386) #356 landed on main as 965420b. `git diff 2fdfbed 965420b` over its three files, `adopt-project.py`, `tests/test_adopt_execute_refusals.py` and `tests/test_adopt_manifest_values.py`, is empty, so main holds those files exactly as #356's final head 2fdfbed left them, and this branch already merged 2fdfbed. The conflicts in `adopt-project.py` and the refusal tests are #356's own hunks meeting themselves, and `git checkout --ours` for the three files is #356's final version plus #435's change and nothing else. Proof: `git diff 965420b` of the result over the three files has the same patch-id (76b3015) as #435's own change, `git diff dbfad70 3efcc7a`, and no other file differs from 965420b. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… 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>
check names, and execute refuses before anything is made, a plan whose source: or legs: is not a mapping or whose pins:, allow_empty_legs: or paths: is not a list of what plan writes, so a scalar no longer ends both in a traceback and pins: openGlass no longer adopts with eight one-letter pins (#386)check names, and execute refuses before the work directory is made, a plan whose source: or legs: is not a mapping or whose allow_empty_legs: or paths: is not a list of what plan writes, and asks the same first of a pins: that is not a list, so a number or a boolean in any of the four no longer ends both in a traceback and allow_empty_legs: code is no longer read as four legs (#386)
|
|
(lane openRepoShape-2, 2026-10-13T12:24Z) This lane reviewed its own PR at 2795063, read-only in its own detached worktrees, and authored nothing. MERGEABLE WITH NITS, once the three-OS CI is green. The gate does what #386 asks. In 56 hand-edited plans (nested lists, a mapping for a list and the reverse, empty containers, null, a pin missing Nits:
Not this PR's, and the same on main: Tests at 2795063: 38 files, 3035 passed, 4 skipped, 0 failed. Merge results: rc 0 with main and with 14 open PR heads. #504 conflicts only where it conflicts with main alone. The report is |
…471's PR carries main's #509 and #435 and its footer reads right (#471) One conflict, docs/handbook.html's footer, which every PR in the doc train rewrites. Resolved by the doc-train rule: main's text is kept whole, including #509's clause "a taken clone path refused up front, added (#498)", and this PR's own clause, "the line-ending block an adoption appends to a root .gitattributes the source keeps, and its verification as the source's bytes plus that block, added (#471)", follows it in the same "; ... added (#NNN)" form before " — a page cannot name the commit". Only the touched lines were re-wrapped, to the footer's 100-column width. The footer's `README.md blob` moves from c61ac37 (this branch before the merge; main's own was 43df8f4) to e6c41c9, `git hash-object README.md` of the merged README.md. README.md stays at 1422 lines and AGENTS.md at 490. Every other file merged without a conflict, including adopt-project.py, which both sides changed (main's #435 shape checks on the plan, this PR's block). Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…itory with conftest's `rmtree`, which clears the read-only bit git gives its objects, so the Windows job, which refuses to unlink them, no longer ends 12 adoption-pin tests in an error at setup (#471) #471 folds in #497's `git-filter-repo` install, so the adoption suites ran on a Windows runner for the first time, and the run was green but for 12 errors, all at the module fixture `adopted` of tests/test_adopt_pins.py. The fixture copies a repository with `shutil.copytree` to give the scaffold a pin source, and deletes the copy with `shutil.rmtree` before `check` so the digest is proved to have been read from it. Git writes its loose objects and packs READ-ONLY, and Windows refuses to unlink a read-only file, so the delete raised `PermissionError: [WinError 5]` there and succeeded everywhere else. `tests/conftest.py`'s `rmtree` clears the read-only bit and retries the one call that failed, and is what every other test that deletes a tree holding a `.git` already uses. The fixture now calls it. Test-only: no tool changes. The other `shutil.rmtree` calls under tests/ were each read and none needs the same: test_cli_reference.py:391 and test_setup_sh.py:496 pass `ignore_errors=True` over a scratch directory of generated text and over a checkout the Windows job never runs, test_family.py:1580 removes a submodule's working directory, which holds a `.git` FILE and no objects, and test_update_shape_landing.py:1284 removes a plain directory the test made. Checked against main's f391e58 (#435), which asks the keys of a plan file their types before anything is read: nothing this PR adds reads `source:`, `legs:`, `paths:`, `allow_empty_legs:` or `pins:` of a plan, and its one plan-writing change, the `.gitattributes` follow-up, is a string in the same `follow_ups:` list, which the new checks accept (`plan ok`). A plan holding `source: 5`, `legs: []`, `paths: {}` and `pins: true` is named by key by `check` (exit 1) and refused by `execute` (exit 2) before a work directory is made. Nothing changed for that. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…e: docs/handbook.html's footer keeps main's text and adds this PR's clause after #498's, and names the merged README.md's blob (#499) Five commits of main since this branch's base 47b1e06 merged, among them #509 (cdcd2f0, #498), which rewrote the handbook footer, and #435 (f391e58, #386), which gates a plan's `source:`, `legs:`, `allow_empty_legs:`, `paths:` and `pins:` by type at the top of `adopt-project.py`'s `check` and `execute`. AGENTS.md, README.md and adopt-project.py merged without a conflict. docs/handbook.html conflicted in its footer only. The resolution keeps main's text, "a taken clone path refused up front, added (#498)" included, adds this PR's clause after it in the same form, "an adoption’s <code>--project</code> as the source’s own name, added (#499)", and re-wraps only the two lines that clause touches, to the footer's 100 columns, now three. The PR's own body edit at "It KEEPS ITS NAME" merged as it was. `README.md blob` moves from main's 43df8f4 and this branch's d19633d to 045250e, `git hash-object README.md` of the merged README.md, which `tests/test_repo_hygiene.py` checks. README.md stays at 1422 lines and AGENTS.md at 490. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ute` puts it into the repository's URL, so `org: 2026`, `org: true`, `org: ~` or a deleted `org:` line is `plan-not-the-source` instead of `plan ok` and a `git ls-remote` of `https://github.com/2026/Atlas.git`, and a source whose `origin` is a path is told to rename the repository at that path, not to run `gh repo rename` (#499) Lane openRepoShape-3's review of 9f3b3df, SHOULD-1 and NIT-3, folded. NIT-1 is left as it was; why is below. SHOULD-1. `_not_the_source` compared the plan's `org:` only when it was text and returned None otherwise, while `_repository_urls` reads it through `checked_value`, which converts with `str()`. A plan edited to `org: 2026`, `org: true` or `org: ~`, or with its `org:` line deleted, checked `plan ok`, and `execute` ran `git ls-remote` on `https://github.com/2026/Atlas.git`, `.../True/...` or `.../None/...`, so "a hand edit is caught too" was false for exactly these. `_plan_org` is now the one read of `org:` for both, `str()` of what the file holds, and the repository `check` and `execute` compare with the source is the one `execute` acts on. Each of the four is `FINDING plan-not-the-source` from `check`, exit 1, and `REFUSED plan-not-the-source` from `execute`, exit 2, before any `git ls-remote`, push or `gh` command. Where `org:` is not text, the detail says what the file holds beside how `execute` spells it: "the plan's `org:` (the number 2026, which `execute` spells 2026)". #435 asks the type of five other keys and not `org:`; nothing is added to its checker here. NIT-3. For a source whose `origin` is a path, `adopt-not-the-source` said the repository read from it "names no repository on GitHub by itself" and then, where the policy refused that name, to rename it "on GitHub (`gh repo rename`)"; `_check_names` said the same of `--project` given as that name. `_rename_first` stays the one text of rename-first and takes the path (`_origin_path`): it then says to rename the repository at that path, or to plan instead from a clone of the repository on GitHub, and never `gh repo rename`. `_check_names` takes the `Source` rather than its repository name so that it can say so too. NIT-1, left. `_places_check_asks` compares `source.repository` with the plan's `<org>/<assembly>` by `.lower()`, and `_is_the_source` by `casefold()`. They agree on every name `checked_value` lets through, but `source.repository` is read from an `origin` and never goes through it: a path origin `.../straße/Atlas.git` planned with `--org strasse` plans (casefold), and `_places_check_asks` does not take `strasse/Atlas` for the source's origin (`.lower()`). Calling `_is_the_source` there would change what `check` asks of that source. tests/test_adopt_project_is_the_source.py: `test_an_org_that_is_not_text_is_read_as_execute_reads_it`, over the four shapes, and `test_a_name_read_from_a_path_is_renamed_at_that_path`, over `adopt-not-the-source` and `naming-unclassified`; all six fail on the code before this commit. `edited` can delete a line. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s/<product>-pin.yaml`, through the symlinks the plan keeps in the root, as `place()` writes it after the copies, so a link only a pin's file runs through is refused before any leg and no longer stops `execute` after both legs are pushed (#183, #178) rv183g's P1 on d92b747 (SHOULD-FIX). Since #273 (aa4e1f5, merged in d92b747), `_commit_the_split` hands the materializer `neutral_pins=_recorded_pins(plan)`, and `place()` writes each pin's file after the copies and before the shape's own pin. `_PlannedRoot` walked `assembly_root_paths()` with no pins. So with `--pin openGlass@<commit>`, a source that keeps `contracts -> governance` and a `governance/openglass-pin.yaml` that leads to an absolute path where nothing is, to `../../outside-pin.yaml`, or to itself got `plan ok` from `check`, and `execute` stopped after both legs with `materialize-outside` or `materialize-symlink-loop` (rv183g_pinprobe.py). `main` wrote the pin outside the assembly, or raised `OSError` after the legs. - `_PlannedRoot` reads the plan's pins as `main`'s own `_leg_collision_findings` reads them, `_declared_pins(plan)`, each product once, in the plan's order: the products of `_recorded_pins`, the mapping `execute` hands the materializer. `walks()` follows `assembly_root_paths(self.pins)`. - A symlink AT a declared pin's file is #273's `plan-root-holds-pin`. `_links_at_a_pin` puts it in `seen`, as `_links_at_a_copy` does for #262, asked as `_kept_pin_findings` asks it: one link, one finding. - A `pins:` that is no list, or an entry that is no pin, gives no product, so it adds no path and raises nothing in the walk. `check` names it (`plan-pin-malformed`, `plan-pin-unrecorded`); `execute` refuses it before any leg and writes no file for it. #435 (main f391e58, merged next) asks a `pins:` that is no list first, before the source is opened, so `check` and `execute` stop there before any walk. - rv183g's side effect: with a pin, `contracts/shape-pin.yaml -> openglass-pin.yaml` was refused as leading to what "is not in the source tree". `place()` writes the pin's file just before the shape's pin, so it finds a file there and writes the shape's pin beside it, under `shape/`, as it does wherever the root holds that path. The walk now says the same, so `check` prints no link finding for it. The run then fails verification after both legs, as on `main` (`adopt-shape-pin-unreadable`, exit 1; bootstrap 1): that is what the root keeps at the shape's pin, which #305 refuses at `project.yaml` and nothing refuses yet at the shape's pin (the PR's D13 through a link above), not where this link leads. For #457's checklist, not widened here. - A link only a pin's file runs through that leads inside to where nothing is (`governance/openglass-pin.yaml -> nowhere.yaml`) is now refused, as AT-dangle-dir-present is: `main` writes the pin through it. Tests (tests/test_adopt_root_symlinks.py, 149 tests, was 130): - rv183g's three cases and the one that dangles inside: one `plan-root-symlink`, naming `contracts` and the pin's file, and `execute` refuses with that line before any leg (4); - the same sources with no pin: `check` and `_refuse_what_check_finds` pass (4), and end to end the absolute one adopts, the link kept (1); - a link AT the pin's file in a real `contracts/`, and its case twin: exactly one `plan-root-holds-pin`, no `plan-root-symlink`, and `execute` refuses with #273's line (2); - `pins: 7`, `pins: openGlass`, an entry `7`, a name alone and an entry with no commit: `check` names the `pins:` alone with no traceback, `execute` refuses with it, no leg, and the walk adds no path and no finding (5); - the walk's pins are `_recorded_pins`' products, once each, and its paths are what the materializer writes when handed them, in order (1); - the side effect, in the walk (1) and on the disk (1). Revert rows, each in a clone of d92b747 with this commit's two files, the whole file run with git filter-repo on PATH: - M0, no change: `149 passed`. - M75, the walk handed no pins (`assembly_root_paths()` as at d92b747): `6 failed`, the four refused cases, the order test and the side effect's walk. - M76, `seen` without `_links_at_a_pin`: `2 failed`, the two AT cases. - M77, `_links_at_a_pin` without the disk that ignores case: `1 failed`, the case twin. - M78, the walk reading `plan.pins` (the products of entries that are no pin included): `2 failed`, the name alone and the entry with no commit. - M79, the walk reading the raw `pins:`: `5 failed`, the five blocks. - M80, the pins not deduplicated: `1 failed`, the order test. - The old table's M11 (every link a walk meets refused) and M21 (every kept link walked), on this round's line: `60 failed, 4 errors` and `23 failed`, the four no-pin controls and the end-to-end one among them; M46 (`place` writes through a link AT a path): `9 failed`, the side effect on the disk among them. Every new test fails under at least one row. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ype gate on the plan's keys comes first in `check` and `execute`, before the source is opened, and the walk that now follows #273's pin files reads only the pins `_read_pins` takes, so a `pins:` that is no list is named once and never walked Merged by sha d41cc29, the nine commits of main past af01068 (47b1e06 to d41cc29). The brief named 8cd7dbe; main had moved to d41cc29 (#402, shape-doctor.py) by the fetch at 21:19:04Z, so the newest was merged. No conflict: the tree is `git merge-tree --write-tree a79936f d41cc29`'s, d0b610c. The commits that meet this branch's files: - f391e58 (#386 via #435): `Plan` reads `paths:` and `allow_empty_legs:` through `_plan_list`, `source:` through `Plan.source`, and `legs:` only as a mapping in `names()`; `_plan_key_findings` names a `source:` or `legs:` that is no mapping and a `pins:`, `allow_empty_legs:` or `paths:` that is no list (`plan-bad-source`, `plan-bad-legs`, `plan-pin-malformed`, `plan-bad-allow-empty-legs`, `plan-bad-paths`). `cmd_check` prints them, with #293/#294's field findings, before it makes the work directory or opens the source, and exits 1; `cmd_execute` refuses them first (`_refuse_a_mistyped_plan`). - 191105a (#225 via #266) and d52b100 (#462 via #506) change scripts/shape_materialize.py: the leg-path rule refuses a leg path the YAML reader reads back as another type, and `RULESET_HINT` offers one exit. `leg_path_problems` is what `_PlannedRoot._mount` and `_leg_findings` ask, so a plan it refuses has no mounts in the walk, as before; no line this branch changed in the materializer is touched. - 47b1e06 (#259 via #395): the templates quote their text placeholders. - 8cd7dbe (#286 via #347): update-shape.py refuses a pinned copy, the pin or the manifest kept as a symlink as `update-rewrite-symlink`, and shape-doctor.py calls such a copy `copy-is-link`. Neither is this branch's file, and the lines it adds name neither the materializer nor adopt-project.py. - cdcd2f0, fc48854, 631f087 and d41cc29: setup-project.py, tests/conftest.py, shape-doctor.py, the family Makefile and update-shape.py, none of this branch's files. How they compose with this branch: - The walk reads the plan's pins through `_declared_pins`, as `_leg_collision_findings` does: only the entries `_read_pins` takes as pins. A `pins:` that is no list is #435's first finding, so neither command reaches `_entry_findings`, and so the walk, with one; were it reached (the tests ask the walk directly), it adds no path and raises nothing. A list whose entry is no pin passes #435's gate and is `_read_pins`' finding in `_leg_findings`, and the walk adds no path for it. - A `paths:` that is no list reads as no entries (`_plan_list`), and a `legs:` that is no mapping as the default mount paths (`_plan_leg_path`): both are #435's findings first, so neither reaches the walk from `check` or `execute`. The order, as read in the merged code: - `check`: #435's key findings, with #293/#294's beside them, alone and first; else `plan-stale`, #320's split-branch findings, then `_entry_findings`: #196's unsafe paths, #303's empty symlinks, coverage, `_leg_findings` (#273's pin findings last), #166's submodules, `plan-root-symlink`, #255, #261, #479, #190, #293/#294. - `execute`: `_refuse_a_mistyped_plan` (#435) before the work directory is made; then `_check_names`, `_refuse_an_unrunnable_plan`, `adopt-name-too-long`, #255's clone URL, `_refuse_what_check_finds`, `adopt-symlinks-off`, seeding, `adopt-work-dir-occupied`, #479's remote check, `adopt-branch-exists`, #320. All before any leg. Checked on the merged tree: no top-level name bound twice in adopt-project.py, scaffold-project.py, scripts/repo_shape.py, scripts/shape_materialize.py, update-shape.py or shape-doctor.py but main's own (`YamlError`/`parse_yaml` in the materializer, `sys` in the doctor); no name loaded and bound nowhere; each side's change reappears line for line in the other side's diff to the merge, in every file. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…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>
Merged by sha, e6fbd74, fetched at 22:23Z, on top of 180902f. It brings seven commits since 191105a, oldest first: cdcd2f0 (#509), fc48854 (#496), f391e58 (#435, the plan's field types), d52b100 (#506), 8cd7dbe (#347), d41cc29 (#402) and e6fbd74 (#515 for #514: `execute` writes each `neutral_product_pins:` entry so it reads back as the text `plan` recorded). No conflict: the tree is `git merge-tree`'s (8178281). One function changed on both sides, `_template_table`: this branch spells three plan keys by their constants (`ELECTED_ON_KEY`, `REFERENCE_KEY`, `ELECTED_BY_KEY`), and #514 takes the pins' list from `_neutral_product_pins(pins)`. Both are kept. No top-level name is bound twice in adopt-project.py; shape_materialize.py and family.py import `parse_yaml` and `YamlError` twice on both parents, the same objects. #435 comes first. `check` returns on `_plan_type_findings` before any of this branch's questions, and `execute` calls `_refuse_a_mistyped_plan` before it opens the source. Probed: `source:` or `legs:` set to 7, true or a list, and `allow_empty_legs:`, `paths:` or `pins:` set to something that is not a list (16 plans). Each is one finding at `check`, and `execute` refuses it before the work directory or a remote exists. None prints `manifest-misread` or #181's remediation, and none ends in a traceback. #514 quotes the one line this gate still found a misread on. This gate asks `_template_table` (`_pin_field`), so it asks the quoted line with no edit here. At this merge the quoting file, main's tests/test_adopt_manifest_values.py and tests/test_adopt_pins.py and tests/test_template_text_placeholders.py give 14 failed and 803 passed. They are the 14 rows round K4 named, each asking what #514 changes. The next commit makes them true. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>



Lane: openreposhape-2 (openRepoShape-2)
Claim: #386 (comment)
#356 landed as 965420b; this branch merged its final head 2fdfbed and main (cc8f2da). The PR targets main. Since #273 landed (aa4e1f5),
pins:is a list of mappings that #273's reader judges, and this PR asks it only whether it is a list (below).What
Planread four keys of a plan file without asking their type, and, until #273, a fifth,pins:. The constructor iteratedpaths:,allowed_empty_legsiteratedallow_empty_legs:, andnames,source_commitandopen_sourcecalled.getonlegs:andsource:. A plan is edited betweenplanandexecuteon purpose, andx or []rescues only the falsy values. Each key is now read through a reader that takes only whatplanwrites, andcheckandexecutename whatever that reader leaves out before they read anything else.Readers.
_plan_list(data, key)returns the key when it is a list and[]otherwise. It is the one read ofpaths:andallow_empty_legs:.allowed_empty_legs()keeps only the text items.Plan.sourceissource:when it is a mapping and{}otherwise.source_commitandopen_sourceread it.names()readslegs:only when it is a mapping.Plan.pinsis 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's_pin_products: theproduct:of each mappingplanwrites, or an old plan's name, out of a list only.No reader iterates a number or reads text one character at a time.
Plan.__init__still keepslegs:as it stands. This is the issue's constraint:shape-doctor.py --placement-planwriteslegs:as a list, andtests/test_shape_doctor.pyhands that list toPlanand_leg_findings. Every reader ofplan.legsasks its type.Findings, one code per key, as 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's key hasplan-bad-follow-ups:plan-bad-source,plan-bad-legs,plan-bad-allow-empty-legsandplan-bad-paths.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's_read_aswords: "paths:is the text 'src/', not a list".allow_empty_legs:, every item that is notspecorcodegets its own finding: "item 1 ofallow_empty_legs:is the text 'Code', not a legexecutecan seed".pins:is 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's.plan --pin <product>@<40 hex>writes each pin as a mapping ofproduct:,repository:,commit:,tree_sha256:anddigest_source:, and 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's_read_pinsjudges each entry beside the entries: a name alone isplan-pin-unrecorded, anything elseplannever writes there isplan-pin-malformed. This PR does not judge the entries. It asks only whetherpins:is a list, first, beside the other four keys, because the naming policy reads the root's products from it. The finding is_read_pins' own, "pins:is 'openGlass', not a list of pins", so there is one finding under one code, andPINS_REMEDIATIONnames the mappingplanwrites.Asked first. These keys are what every later finding is read from. Without them, a
paths:that is not a list would be reported as every source path uncovered.cmd_checkasks_plan_type_findingsright afterPlan.load, before the work directory or the source. It prints these findings with adopt-project.py:checkandexecuteiterate a plan'sfollow_ups:without asking for a list, so a hand-edited plan withfollow_ups: 5(ortrue) endscheckin aTypeError: ... is not iterabletraceback, andexecutein the same one after both leg repositories are created and pushed #293's and adopt-project.py:checknever reads a plan'selected_on:when the plan has noreference:, soplan okis followed by anexecutethat creates both leg repositories and only then refuseselection-date-malformed, naming--elected-on, a flag neither command takes #294's beside them, so one run names every key that is not whatplanwrites. It exits 1.cmd_executerefuses the same list through_refuse_a_mistyped_plan, exit 2. The refusal has the shape of_refuse_what_check_finds: the first finding's code, then each finding ascheckprints it, capped atREFUSED_FINDINGS_SHOWN. Its remediation begins "no leg repository was created and nothing was pushed", as every refusal before a leg exists does (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), and no notice of what the run left follows it. It comes before_work_rootmakes the work directory, before the source is opened, and before any--yesquestion.Why
On main cc8f2da, with local remotes and a stub
ghthat was never called (tools/s435-repro-main-cc8f2da.log):allow_empty_legs:orpaths:endscheckandexecuteinTypeError: ... object is not iterable, fromallowed_empty_legsorPlan.__init__. Exit 1.legs:orsource:ends both inAttributeError: ... has no attribute 'get', fromnames,open_sourceor, with--source,source_commit. Exit 1. Withpins: 5besidelegs: 5, the traceback is all either command prints.allow_empty_legs: code,[Code]and[5]each getplan okand adopt. Text is read one character per leg,c,o,dande; on 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's 5a5011d, a spec-only source'sallow_empty_legs: codepassedcheck, andexecutethen refusedadopt-empty-leg-unconsentedfor the leg the person had named.paths: abcis reported as threeplan-uncoveredfindings, one per source path.pins:is already named on main by 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's reader, andexecuterefuses it after the work directory is made.The issue was first reproduced on #356's 5a5011d (
tools/repro-base-5a5011d.log), wherepins: openGlassalso passedcheck, adopted, and wrote eight one-letter neutral-product pins. #273 fixed that row on main.On this head, every one of those rows is a named finding from
check, exit 1, and a refusal under the same code fromexecute, exit 2, with no leg and no branch created. For each of the four keys, and for apins:that is not a list, that is before the work directory is made; an entry of apins:list is refused where #273 refuses it.pins: 5besidelegs: 5names both in one run. The controls still adopt (tools/s435-repro-head-42253cb.log).Tests
All tests are in
tests/test_adopt_execute_refusals.py, beside #356'sfollow_ups:andelected_on:tests and using its helpers: 59 tests.checkandexecuteon 16 values that are not the key's type, one test of each per value (32 tests). The values are a number, a boolean and text forallow_empty_legs:andpaths:; a number, a boolean, text and a single mapping with no list around it forpins:; a number and a list forlegs:; a number and text forsource:; and bad items inallow_empty_legs:(Code,5). Each case asserts:Tracebackand noplan ok;execute, no leg, no work directory, and a source still holding onlymain.legs:,pins:andpaths:, plus adopt-project.py:checkandexecuteiterate a plan'sfollow_ups:without asking for a list, so a hand-edited plan withfollow_ups: 5(ortrue) endscheckin aTypeError: ... is not iterabletraceback, andexecutein the same one after both leg repositories are created and pushed #293'sfollow_ups:): all are named in one run.executeuses the first finding's code.source: 5with--source: still a finding.allow_empty_legs: code: named, not read as four legs.pins:,allow_empty_legs:andpaths:; one and two whole pins asplanwrites them; and both legs inallow_empty_legs:. An emptypaths:isplan-uncovered, as before.pins:list (4): a name besidetrue,2026,nulland{a: 1}. This gate says nothing of them, andchecknames each in 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's words (plan-pin-unrecorded,plan-pin-malformed).executerefuses under the first one's code, and no leg is created.5,True,1.5,'openGlass'and{'a': 1}; the products read out of a list holding names, mappings and junk; and the doctor's listlegs:still constructs.--yes, the refusal is the key's own, notadopt-unconfirmed.plan --pinwrites (4):plan --pin <product>@<commit> --pin-source <a local clone>for2026,007,1.5andopenGlass.pins:is one mapping with the product given,_plan_key_findingsand_read_pinsfind nothing,checksaysplan ok, andPlan.pinsreads the product back.git filter-repo:pins: openGlassis refused before the work directory, with no notice of what the run left. The corrected plan, with thepins:blockplan --pin openGlass@<commit>writes, adopts with the one pin and writescontracts/openglass-pin.yaml.Head 2795063 is 42253cb with main 47b1e06 merged in by GitHub's update-branch (#395, templates and tests only);
git diff 42253cb 2795063 -- adopt-project.py tests/test_adopt_execute_refusals.pyis empty. The lane's review ran these files at 2795063, plustests/test_template_text_placeholders.py: 38 files, 3035 passed, 4 skipped, 0 failed (review435-27950638/tools/per-file-2795063.txt).Runs at 42253cb:
tests/test_adopt_*.py,tests/test_shape_doctor.py,tests/test_scaffold_leg_path_collisions.py,tests/test_cli_reference.pyandtests/test_repo_hygiene.py, withgit filter-repoon PATH so no adoption test skips for want of it). The skips:test_adopt_plan.py:403(SHAPE_MEDXEHR_CLONEunset, 1) andtest_shape_doctor.py:8373(the PowerShell line exists only on a Windows host, 3).test_adopt_execute_refusals.py150: main's 91 and this PR's 59.test_adopt_pins.py(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's) 70.test_shape_doctor.py352,test_adopt_leg_paths.py349,test_adopt_gitmodules_values.py187,test_adopt_submodules.py167,test_adopt_spelled_unambiguous.py145,test_adopt_finding_spelling.py128,test_adopt_root_shape_paths.py120,test_adopt_question_and_submodule_spelling.py106,test_scaffold_leg_path_collisions.py104,test_adopt_leg_path_collisions.py95.test_adopt_late_failure.py86,test_adopt_entry_paths.py79,test_adopt_manifest_values.py79,test_adopt_template_values.py78,test_adopt_plan_escapes.py59,test_adopt_plan.py57,test_repo_hygiene.py53,test_adopt_history_gitlinks.py45,test_adopt_plan_leg_path_help.py45,test_adopt_split_branch.py37.test_adopt_relative_submodule_url.py32,test_adopt_clone_template.py31,test_adopt_ignored_shape_files.py30,test_adopt_empty_symlinks.py28,test_adopt_root_manifest.py27,test_adopt_e2e.py26,test_adopt_mount_ignored.py26,test_cli_reference.py22,test_adopt_github_exits.py19,test_adopt_git_reads.py17.test_adopt_verify.py16,test_adopt_literal_names.py14,test_adopt_spec_only.py14,test_adopt_filter_repo_failure.py11,test_adopt_tracking_branch.py6.scripts/render-cli-reference.py --check: ok.Earlier runs, before #273 landed: the first 57 tests (d53602b), run against #356's code at 5a5011d, gave 44 failures, each for the issue's reason, and 13 controls passed (
tools/new-tests-on-base-5a5011d-reasons.txt). 11 mutants of the fix at 5f177c9 were each killed (tools/mutants-5f177c9.log); three of them were of code #273 has since replaced.Merged #356 and main (after #356 landed)
#356 (issues #293, #294) squash-landed on main as 965420b. This branch was stacked on it, so three merges bring it up to main, with no force-push and no rebase:
2fdfbed, 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's final head, as 1e6e9b8. It was clean, and its tree is the onegit merge-tree 2fdfbed 3efcc7agave (468826c).965420b, 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's squash, as ed5d54a.git diff 2fdfbed 965420bover 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's three files (adopt-project.py,tests/test_adopt_execute_refusals.py,tests/test_adopt_manifest_values.py) is empty, sogit checkout --oursfor them is 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's final version plus this PR's change. The result differs from 965420b by exactly this PR's change: its patch-id is the same asgit diff dbfad70 3efcc7a's.Main cc8f2da, as 42253cb, a real merge. Since 965420b, main changed
adopt-project.pyin 70d0d1b (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), 56713e2 (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) and aa4e1f5 (adopt-project.py:plan --pintakesproduct@<40 hex>andexecutewrites its pin file as the scaffold does, so an adopted root passesvalidate-pins.py(#273); a--pin-sourcethat cannot answer ispin-source-unreadable(#339) #508, lane 5's 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 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). Two hunks conflicted, both inadopt-project.pyand both the ones aa4e1f5's description names:render_plan'spins:line takes main's_pin_lines, which writes the product throughyand the commit and digest quoted. This PR'sy(pin)change is superseded and gone.Plan.pinstakes main's_pin_products.The rest is the reconciliation that aa4e1f5 and lane 5's comment on this PR asked of whichever landed second. As this branch stood, its item check took only text, so every plan
plan --pinnow writes wasplan-bad-pinsin its owncheck. Now_plan_key_findingsleavespins:entries to_read_pins. Apins:that is not a list is still asked first, in_read_pins' ownplan-pin-malformedfinding, soplan-bad-pinsis gone.PINS_REMEDIATIONnames the mapping, andPLAN_KEY_REFUSALsays "no leg repository was created", whichconftest.refused_before_any_legasserts of 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's not-a-list row, now refused here. The tests above changed with it.git diff --stat cc8f2da 42253cblists onlyadopt-project.pyandtests/test_adopt_execute_refusals.py, andgit merge-tree --write-tree cc8f2da 42253cbis rc 0. flake8 reports the same findings for both files as on main, with the same messages, andscripts/render-cli-reference.py --checkis ok; no--helptext changed.Mutants of the reconciliation, each killed (
tools/mutants-s435-42253cb.log, run overtests/test_adopt_execute_refusals.pyandtests/test_adopt_pins.py):pins:entries again and takes only text, as at 3efcc7atest_a_plan_with_a_whole_pin_checks_okand the fourplan --pinround tripspins:wholly to_read_pins, so it is not asked firstpins:that is no list named under a code of the gate's ownplan-pin-malformed-not-a-listrowplan-pin-malformed-not-a-listrowPlan.pinsreads text items only, as at 3efcc7aComposes with
git merge-tree --write-tree 42253cb <X>(tools/s435-merge-tree-42253cb.txt), against the shared store's main and the open pull requests' heads at 11:21Z: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, templates and tests, after af01068): merged into the branch by update-branch at 11:48Z, making 2795063. The review merged 2795063 with main and with 14 of the 15 open pull requests' heads at rc 0; 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 conflicts only where it conflicts with main alone.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, 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, 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, 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, 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 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.#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.py, 5 hunks), 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 (shape-doctor.py10 andtests/test_shape_doctor.py1) and 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 (adopt-project.py, 4), the conflict regions are line for line the ones each makes with main alone. 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's are in code this PR does not touch. 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 (docs/handbook.html, 1) conflicts with main cc8f2da alone in the same region.Left out
{}) or absentlegs:is read as nolegs:, as before.checkthen reportsnaming-role-mismatchabout'None', as it does on main.pins:list are 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's_read_pins' to judge, andexecuterefuses them where it refuses every entry finding, after the work directory is made and before any leg.paths:that are not mappings are still dropped without a word.mode:andkind:already have refusals of their own._refuse_a_mistyped_planshares a shape with_refuse_what_check_finds, not code. The latter's text is about entries and leg remotes, and since adopt-project.py: a relative.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 it also names the remotes this run would create.Closes #386
🤖 Generated with Claude Code