Skip to content

adopt-project.py: y() quotes what the plan's loader would read as a keyword, so --spec-path null, --id null and --tracking-branch true adopt as typed, and plan, check and execute refuse, before anything exists, a value the project's own loader would read back as something else from the line execute writes - #518

Merged
brettheap merged 55 commits into
mainfrom
fix/adopt-plan-quote-yaml-keywords
Oct 14, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Lane: openreposhape-1 (openRepoShape-1)
Claim: #181 (comment)

What

One rule, applied in two places: a value is never written where the project's own YAML loader (scripts/repo_shape.py) will read it back as something else. The loader decides each case. Nothing here keeps a list of words.

The plan writer (76a6153). y() writes a string bare only if the plan's loader gives the same string back. It asks in both places the writer puts a string: after key: , and as a - item (- a: is a one-key mapping, k: a: is not). A line the loader refuses counts as not read back. Anything else is quoted with #199's repo_shape.double_quoted.

The gate on what execute writes (b26841b to 94242a2, and 180902f). execute puts the plan's values into the project's own YAML files. Each command refuses a value the project's loader would read back as something else from the line execute writes:

What it finds on main now: nothing, on the shipped templates. Main's 47b1e06 (#259), merged here in 5c04ca2, writes id, elected_on, visibility, tracking_branch, both leg paths and the leg pins' submodule_path between double quotes. There render spells a value with double_quoted, and the loader reads it back as typed. Main's e6fbd74 (#514), merged here in f54dce7, writes an item of the manifest's neutral_product_pins: list between double quotes where the project's loader would misread it bare, and 89c6a6c (#516), merged in 8ce572b, quotes the neutral pin file's product:. The gate asks the line execute writes, so on main's templates it finds no value misread: the class fuzz under Tests names none. Before those merges it refused --tracking-branch true, null and 007, and then --pin null@<commit>, True@<commit> and 007@<commit>; now each is planned, checked and written as given (READ_BACK, under Tests). On a line written bare, as the templates were before #259 and the pins' list before #514, it says:

REFUSED adopt-manifest-misread: the product of a --pin is `null`, which `execute` writes into project.yaml's neutral_product_pins as `- null`, where the project's own YAML loader reads it back as no value at all and not as that text: choose another value.

check says the same of "the product of a pin in pins". The remediation says execute writes these values "into the project's own YAML files as they stand", not "into project.yaml and leg pins": a pin's product goes into the manifest and the product's own pin file, not a leg pin.

Why the gate stays whole (the lane's ruling in round K4):

How the gate decides.

The project's own validator is asked first (f38d19a; rv181f's G1 and G2). Main's #314 asks the project's own validate-manifest.py about the same values: plan-manifest-invalid at check and execute, adopt-manifest-invalid at plan. This gate's remediation says that execute writes the values it names as they stand, and that the run would still end adoption verified. So a value is this gate's only where all three hold:

A value execute never gets as far as writing, in every mode, is not asked about (_execute_gets_to_write). A sentence about it would say that execute writes it and that the run would still end adoption verified, and neither would be true:

Since #259 the loader reads every tracking branch and visibility back from its quoted line, and since #514 every pin's product from the list, so these clauses change what the gate says only of a line written bare.

How each refusal reads (5aee63a, the NITs of review181).

An explicit null leg path in a plan. This is spec_path: null, ~, or the key with nothing after it, which is what main's plan --spec-path null writes.

Why (repro on main 5ed3e5d)

Each case runs plan, check, execute --local-remote-dir, git clone --recurse-submodules, then the clone's own scripts/bootstrap.py, on the suite's standard source. A branch given to --tracking-branch is one the source has (#479). A stub gh stood first on PATH; its only calls came from a bootstrapped root's own validate-pins.py, asking for the digest of a pin it has no checkout of. Since #259 the templates quote what execute writes, so on main --spec-path 2026, 1.5 and ~, --tracking-branch 007 and --id 2026 adopt and bootstrap as typed, and since #514 and #516 so does --pin 007@<commit>. What is left on main is the plan: its writer puts null, true and True into the plan bare, and the plan's own loader reads them back as no value or a boolean. On main 5ed3e5d the driver's output is 89c6a6c's line for line, once temp paths and shas are normalized.

adoption verified and bootstrap ok, but with another value, and nothing says so:

  • --spec-path null (also Null, NULL): the plan says spec_path: null, check says plan ok, and the leg is mounted at spec. --code-path null mounts at code.
  • --tracking-branch null: both legs are on main, and so is the manifest's tracking_branch.
  • --id null: the project's id is northwind.
  • --project Null: the plan's legs.assembly and id are read as no value, and the manifest says id: none.

Refused at check, about a value nobody typed:

  • --tracking-branch true: plan-tracking-branch-missing, "tracking_branch is True, which is no branch of the source (its branches: main, true; ...)".
  • --spec-path true or False: plan-bad-leg-path, "... read as true or false, and a path is written as text, so quote it".
  • --id true and --project True: plan-bad-id, "id: is 'True' and must match ^[a-z0-9][a-z0-9-]*$".
  • --pin null@<commit>: plan-pin-malformed, "pins: entry 1 records no product:", from a plan that says product: null; --pin True@<commit>: "records product: True, which is not text".
  • --elected-by null: plan-manifest-invalid, "elected_by is ''", from a plan that says elected_by: null.

On this branch (7d6e13c, the same driver, whose output is 9efe95a's line for line, normalized), every value above adopts and bootstraps as typed. --spec-path null, Null, NULL, true and False mount the leg at null/, Null/, NULL/, true/ and False/, and --code-path null at null/. --tracking-branch true and null put both legs on true and null, each with its origin/ upstream. --id null and --project Null make the id null, --id true and --project True make it true, and --elected-by null is elected_by: "null". --pin null@<commit> and True@<commit> adopt with the manifest's - "null" and - "True" and the pin files' product: "null" and product: "True". --spec-path legs/null --tracking-branch 2026-q4 --id yes --elected-by Null adopts and bootstraps too, each leg on 2026-q4; on main the same run stops at check with elected_by is ''.

Tests

tests/test_adopt_plan_yaml_quoting.py is new, with 524 tests, all through the real repo_shape loader:

The composition fuzz, every value class (7d6e13c against main 5ed3e5d). 286 values (rv181f's 244 candidates and 42 more) in eleven classes: the id given and derived, the tracking branch given and taken from the source's default branch, the elector, the election date, the reference, the visibility, both leg paths and a pin's product. Each runs through plan, given by its flag, and through check of a hand-edited plan carrying it as the head's y writes it: 3146 rows, 2933 run. The 213 skipped are 210 tracking-branch values git or checked_value will not take, and three a source could not be made for.

Other files: main's tests/test_adopt_manifest_values.py keeps its own tests, with misreads_elsewhere (gone since f38d19a) asked as _misread_findings(plan) == []; test_a_value_the_reader_reads_as_another_kind_is_not_this_questions is main's again, less main's visibility row. tests/test_adopt_leg_paths.py: the not-text sentence has no "quote" and covers None. Main's tests/test_template_text_placeholders.py: a comment that said plan writes true and null bare, which it does not here; #516's pin-file rows beside it are main's.

Revert table, round K5, measured on 9efe95a, and not again at 7d6e13c: the code its rows revert is unchanged since, except where main's merges changed the same functions (_checked_plan_inputs, _entry_findings, y and, at #511's, cmd_plan; under Merges). Each change was reverted alone in a scratch clone of the head; the quoting file and main's tests/test_adopt_manifest_values.py were run whole (601 tests), and the seven files touched were restored (sha256-checked). A row marked "with" runs with git-filter-repo on PATH, so its execute tests run; the others run without it, which skips those 44 tests alone. Unreverted, with it: 601 passed. The rows from K1 on are rv181g's, text for text.

change reverted tests that fail
y() back to main's test (76a6153's change), with 68: the writer's own tests (18), READ_BACK's rows whose value the plan reads as a keyword, at plan/check and at execute (30), the bare branches true and null and the bare pins null and True at check and at execute (8), #479's three shipped tests and their execute (4), the three bare-line #479 cases, the accepted end-to-end run, the keyword-learning test, the pin writer test, the keyword-named source file and the doctor's placement plan
#514: _template_table writes the pins' list bare again, with 9: READ_BACK's three pins at plan/check and at execute, and the keyword-learning, template-line and table tests
bare_pins leaves #514's quoting in place 7: the three bare pins at plan and at check, and the G2 test
the gate asks a copy of a pin's line, not _template_table (180902f) 7: READ_BACK's three pins at plan/check, and the keyword-learning, template-line, table and pin agreement tests
check and execute ask about no pin, with 6: the three bare pins at check and at execute
plan asks about no pin 3: the three bare pins at plan
a pin's line read whole (neutral_product_pins: and its item as one) 17: the three pins at plan, at check and in READ_BACK, the keyword-learning, template-line, table and pin agreement tests, the three ordinary-pin writer cases, and main's test_plan_asks_what_check_asks_of_the_same_keys
plan builds its plan a second time for the pins 1: main's test_plan_asks_what_check_asks_of_the_same_keys
a pin asked about beside a #273 finding 1: test_a_pin_beside_a_273_finding_is_not_named_misread
every product the plan names asked about, not the pins _read_pins records 3: that test, and both cases of test_a_pin_ending_in_whitespace_is_273s_finding_alone
plan names a pin's product --pin 3: the three bare pins at plan
check names a pin's product a pin in pins 3: the three bare pins at check
#259's tracking_branch: written bare again, with 12, and the 61 tests over bare_template error, because its fixture asserts #259's quotes are in place. The 12: READ_BACK's four branch rows at plan/check and at execute (8), check passing a branch read as not text (3), and the template-line test
#259's id: written bare again, with 19, and the same 61 errors: READ_BACK's --id and --project rows twice (8), the four derived ids check passes, check passing id: 007, and six of main's tests/test_adopt_manifest_values.py cases
#259's leg paths written bare again (both path:s and both submodule_path:s), with 18, and the same 61 errors: READ_BACK's nine leg paths at plan/check and at execute
bare_template leaves #259's quotes in place 28: every test over it that needs a bare line and runs without git-filter-repo: the three bare branches at plan and at check (6), the two derived-id tests (5), the not-text rule (4), git's and the source's questions and the default branch's name (3), plan asking the validator first, the seven UNESCAPED values, the keyword-learning test and #266's composition
a value read as not text named by str of it again 4: test_the_misread_rule_names_a_plan_value_that_is_not_text_by_what_was_read, every row
a defaulted tracking branch named --tracking-branch 1: test_plan_names_a_tracking_branch_it_took_from_the_source
_execute_gets_to_write says yes to every value 3: the never-writes rule's two branches ('"CORP\tom"', "~") and the test of what git is asked. Its visibility rows still pass: the validator, asked first, judges each
the misread question asked of a leg path #266's rule refuses 1: test_a_leg_path_written_bare_is_266s_finding_alone
a validator finding about no one value judged as about none 1: test_the_validator_says_which_values_it_judges[a-finding-about-no-one-value]
a manifest the validator's reader refuses judged as about no value (G2) 1: test_a_manifest_its_reader_refuses_is_the_validators_alone
K1: plan asks git only about a misread branch, not the source 3: test_a_branch_execute_does_not_push_in_every_mode_is_never_misread, each case
K2: check and execute pass no unwritten to this gate 3: the same three cases
K3: _pushed_in_every_mode without #479's question 2: origin-lacks-it of that test and of test_the_source_says_whether_execute_pushes_the_legs_to_a_branch
K3b: _pushed_in_every_mode without the source's own branches 2: only-origin-has-it of the same two tests
K5: the source asked about every branch, misread or not 2: test_the_source_is_asked_only_about_a_branch_the_loader_misreads, and test_the_misread_rule_names_an_id_plan_derived_by_where_it_came_from, which gives plan no source
V2b: G1 at plan, this gate's question asked first, the plan built once 1: test_plan_asks_the_validator_before_the_misread_rule
V1: G1 at check, a value the validator judges not left to it none alone
V3: _execute_gets_to_write's visibility clause off none alone
V1 and V3 together 15: NEVER_WRITTEN's fifteen visibility rows over bare_template. Each of the two guards keeps those values from this gate on its own
V4: no manifest the validator can be asked about leaves no value 1: test_no_value_passes_a_validator_that_is_not_asked
V5: FLAG_DEFAULTS without the id 1: test_the_misread_rule_names_an_id_plan_derived_by_where_it_came_from
V6: a derived id named plain id 4: test_the_misread_rule_names_an_id_it_derived_from_the_assembly, every row

Earlier revert tables, each measured on the head named, are kept whole in this description's edit history: round K4 (180902f, 580 tests), the fix round (edf9163, with rows again on ed9968f, f911d81 and 94242a2), round K3 (94242a2, 491 tests) and the NIT round (edf9163, 543 tests). A row about a value #259 or #514 quotes describes that head, not this one. Each row failed at least one test except these, named there: rv181g's K1, K2, K3, K3b and V2b at 180902f (B1, fixed in 9efe95a); a boolean spelled with its own literals (S1192 only); and a branch checked_value refuses, asked about (4 with H1's guard reverted too).

Counts at 7d6e13c, this head (git 2.43.0, Python 3.12.3, pytest 9.1.1), measured before #511 landed on a scratch merge of 42362de with #511's predicted squash, whose tree, 88c22f0a, is this head's. Clean worktrees, a stub gh first on PATH in both modes, and six sets, each run with git-filter-repo on PATH and without it, each with exit 0, that commit named in its log and the worktree clean before and after it:

render-cli-reference.py --check is ok. The full suite was not run here: the three-OS CI gate decides it.

Windows. No Windows machine is reachable from the lane. A pytest plugin kept outside the repository makes text-mode writes in the test process write \r\n, as on Windows: open and io.open with no newline, so Path.write_text too, and a text-mode input= to a process. At #504's 9e8c672, run without git-filter-repo, it fails exactly the 17 tests of tests/test_adopt_root_symlinks.py the Windows job failed, with its message ("the plan was written with no pin"), and nothing else in that file; with git-filter-repo it also fails test_without_a_pin_that_source_adopts, which needs disk symlinks and so never runs on Windows. At 7d6e13c, over this branch's four changed test files, tests/test_adopt_root_symlinks.py and #511's tests/test_adopt_attributes_eol_block.py, it fails nothing: 1280 passed with git-filter-repo, and 1207 passed, 73 skipped without it. Main 5ed3e5d, over the five of those files it has, gives 755 passed with it, and 713 passed, 42 skipped without it. Since #511 the Windows job installs git-filter-repo, so the quoting file's 31 tests that need it run on Windows for the first time, and the run with it is the realistic one. Git's own line endings and file names on Windows are not emulated. This branch's added test lines hold no os.name or sys.platform branch, no chmod, no text-mode stdin, no path split and no read_bytes. Its two write_text calls (answer, bare_template) are read back only as text, never as bytes, which is the pattern #504's Windows failure had. So nothing here changes for Windows; the three-OS CI is the gate.

Reviews

Lane 2 at 76a6153: CHANGES NEEDED, one P2 (#181 (comment)). It confirmed the plan-file fix: 339,485 strings round-trip through y() and the loader, y() differs from main on exactly 63 of them, and ordinary plans are byte-identical.

Lane 1's adversarial re-verification at a0ff15b (review181): PR-READY, 7 NITs. It re-ran lane 2's 26 scenarios, fuzzed the gate (2,440 flag rows and 3,872 hand-edited plan lines: 0 false refusals, 0 crashes), and found ordinary adoptions byte-identical to main.

Lane 1's re-verifications (rv181f).

Lane 1's re-verification at 180902f (rv181g, round K4): CHANGES NEEDED. Its own fuzz judge found 0 false clauses and 0 double findings, against main 191105a and on a scratch merge with main 8cd7dbe, and ordinary adoption byte-identical.

Lane 1's re-verification at 9efe95a (rv181g, round K5): PR-READY. Its own fuzz, byte identity, pin files and targeted runs agreed with this branch's.

Codex at 9efe95a: "Didn't find any major issues". SonarCloud at 9efe95a: the gate passed, with no S8705 and nine new smells: seven S9073 (a composite assert) and one S5778 (two calls inside pytest.raises) in the quoting test file, and one S1192, a third "project.yaml" in adopt-project.py, which was this branch's MANIFEST_YAML. 6c9e585 fixes all nine (under Tests and Merges).

Merges

The branch was brought up to date by merging, never by rebasing: main has been merged in 31 times. Round K6's four are 8beb3d9 in a22b7f9 (one hunk, resolved), b0826e0 in b602c16 (#504, three hunks, resolved), 3121945 in 42362de (#494, clean) and 5ed3e5d in 7d6e13c (#511, clean).

Main is 5ed3e5d at 16:09:26Z (git ls-remote), merged here in 7d6e13c.

After the merges, no top-level name is bound twice in adopt-project.py, scaffold-project.py, scripts/repo_shape.py, update-shape.py, shape-doctor.py or setup-project.py (an AST scan, imports included), none is read and never bound, and no name adopt-project.py imports goes unread. In scripts/shape_materialize.py and scripts/family.py, YamlError and parse_yaml are each bound twice by a second import of them, exactly as on main. The branch's diff against main 5ed3e5d is six files: adopt-project.py, scripts/shape_materialize.py, tests/test_adopt_leg_paths.py, tests/test_adopt_manifest_values.py, tests/test_adopt_plan_yaml_quoting.py and tests/test_template_text_placeholders.py.

Composes with #266 (landed on main as 191105a, merged here)

Lane 2's #266 (#225) asks the same question in the shared leg-path rule, leg_path_problem, of every template line that holds a leg path, rendered by render. plan, check and execute ask that rule before this gate and before the validator. Since #259 those lines are quoted, so on the shipped templates neither rule refuses a leg path: #266's own tests/test_leg_path_yaml_scalars.py refuses only the values a real template line misreads, and skips a test whose values "the templates read ... back (#259 quotes the leg-path lines)", and READ_BACK here plans, checks and writes each of the nine leg paths as given. On a bare copy of the templates, given to both rules, test_a_leg_path_written_bare_is_266s_finding_alone asks the composition: leg_path_problem refuses null, _misread_findings names nothing (_plan_fields asks this gate about a leg path only where that rule has no answer), and check prints one finding, #266's plan-bad-leg-path. The 28 rows round K3 said whichever landed second would adjust are gone with #259: the leg paths are READ_BACK rows, and the leg-path JUDGED_ONCE row is deleted (under Tests). #266's tests run in the third set under Counts.

Composes with #514 (landed on main as e6fbd74, merged here)

Lane 2's #514 quotes an item of the manifest's neutral_product_pins: list that the project's own reader would misread (_neutral_product_pins, _neutral_pin_entry). It landed as e6fbd74 (#515, 21:42:03Z, from its head cd18a1b) and is merged here in f54dce7. Its one line on this gate's path is _template_table's NEUTRAL_PRODUCT_PINS entry. This gate asks that very table (_pin_field, through _manifest_values), so it asks the quoted line with no edit to the rule. At the bare merge exactly the 14 tests round K4 named failed: the three REFUSED pins at plan, at check and at execute (9), JUDGED_ONCE's pin-text-misread and pin-the-reader-refuses, JUDGED_BY_THE_VALIDATOR's its-reader-refuses-the-file, the template-line test and the table test. #181 is the second to land, so 9efe95a makes them true (under Tests): the pins are READ_BACK rows; the refusals are asked where the list is written bare (bare_pins), and so is G2's pin ? x; and the two line tests expect - "Null" and - "null".

9efe95a's one adopt-project.py change is to #514's own docstring on _neutral_product_pins. It said that a keyword y leaves bare reaches the list "only from a plan edited by hand, because check refuses the bare product: true that plan writes". With this branch's y, plan writes product: "true" and "null", check passes them, and execute writes - "true"; the docstring says so now.

With #514 and #516 landed, this gate finds nothing on main's templates, and stays as the backstop #314's scaffold twin is: a line a later template or table writes bare is refused again, with no edit to the rule (the tests over bare_template and bare_pins; writing the list bare again in _template_table fails 9 tests).

Composes with #513 (landed on main as cf118ff, merged here in a22b7f9)

#513 makes --org and --project name the repository the source names: plan refuses another as adopt-not-the-source, check finds a plan edited to name another as plan-not-the-source, and execute refuses it. Its _refuse_another_repository runs in _checked_plan_inputs before this gate's two refusals; check adds plan-not-the-source after _entry_findings, which holds this gate's finding; and execute refuses #513's cause before it asks check's findings. Probed at 7d6e13c in this process, as at 42362de, with the pins' list written bare (bare_pins) so that this gate names pin null, on a source whose origin is under testorg/Northwind:

  • check of a plan whose org: is edited to otherorg: exit 1, plan-manifest-misread then plan-not-the-source, one each, and this gate's remediation once;
  • execute: REFUSED plan-not-the-source alone, with no remote made and no split branch;
  • plan --org otherorg --pin null@<commit>: REFUSED adopt-not-the-source alone, and no plan written.

So each cause is one finding at check, and plan and execute refuse #513's first. M1 (under Tests) was the one test change the merge needed.

Composes with #504 (landed on main as b0826e0, merged here in b602c16)

#504 (#183, #178) has check follow each symlink the plan keeps where the shape writes its files, and execute refuse one the shape cannot write through before any leg exists. Its plan-root-symlink findings are part of _entry_findings, the one list check prints and execute refuses, after the source's own submodules, so after this gate's finding. Probed at 7d6e13c in this process, as at 42362de, with a source that keeps scripts -> . in the root (the shape's scripts would land in the root itself, #504's R6 case) and, under bare_pins, a plan carrying pin null:

With #513's cause added too, check gives the three in that order, one each, and execute refuses #513's first. #504's test files run in the Counts above, and the Windows emulation runs tests/test_adopt_root_symlinks.py too.

Composes with #511 (landed on main as 5ed3e5d, merged here in 7d6e13c)

#511 (#471) appends one marked block of /<copy> text eol=lf lines to a source's own root .gitattributes, and verifies that file as the source's bytes followed by exactly that block. It writes nothing into the project's YAML, and adds no finding and no refusal before any leg exists (its materialize-attributes-path is for a pinned copy whose name git would read as a pattern, and the standard has none). Probed at 7d6e13c on a source that keeps its own .gitattributes:

  • --spec-path null --tracking-branch true, through the shipped code as a process: check passes, and execute --local-remote-dir appends the block and ends adoption verified; the manifest reads the spec leg's path back as null and the tracking branch as true, and both legs are on true;
  • in this process under bare_pins, pin null: check gives this gate's finding alone, with its remediation once, and execute refuses it before any leg exists, the source's .gitattributes untouched;
  • a .gitattributes that is a symlink: check finds nothing without the pin, and this gate's finding alone with it.

With the default flags, that source adopts byte for byte as on main (33 split files).

Not in this PR

Closes #181

🤖 Generated with Claude Code

brettheap and others added 30 commits October 9, 2026 18:50
…uld read back as something else, so `plan --spec-path null` no longer mounts the leg at `spec` and `--tracking-branch true` no longer reaches `execute` as a boolean (#181)

`y()` wrote a string bare whenever it looked like a plain word, and `repo_shape`'s reader takes `null`, `Null`, `NULL`, `true`, `True`, `TRUE`, `false`, `False` and `FALSE` for a null and a boolean. On main `plan --spec-path null` wrote `spec_path: null`, which loads as no value, so `check` said "mount the legs at spec/" while the plan's own follow-up said `null/contracts` and `execute` registered the gitlink at `spec`; `--tracking-branch true` was read back as a boolean and the legs were created on a branch called `True`; and a source file named `null` was a plan entry with no path.

`y()` now asks the loader instead of a list of words. A candidate is written bare only if `parse_yaml` gives the same string back in both places the writer puts one, after `key: ` and as a `- ` item (`- a:` is a one-key mapping, `k: a:` is not), and a line the reader refuses is a value it does not read back. The trailing-colon rule is that second question, so nothing else changed for a string that round-trips: a default plan on a fixed source is byte-identical to main's, and so is a plan of this repository. `yes`, `on`, `.inf` stay bare because this loader reads them as text.

The same defect was in the one list of strings the plan writes without `y`: `--pin null` was written `pins: [null]`, which `Plan.load` drops, so the pin vanished. It goes through `y` too; `openGlass` and `opensoft/openGlass` are unchanged.

`null` is a canonical one-segment leg path and not a Windows device name, so #169's check accepts it and `execute` mounts the leg at a directory called `null`: what was typed.

Closes #181

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…te` would write into `project.yaml` or a leg pin that the project's own loader reads back as something else, so `--spec-path null`, `--id true` and `--tracking-branch true` no longer end `adoption verified` before a bootstrap that fails or puts the legs on another branch (#181)

76a6153 made the PLAN say what was typed: `y()` quotes any string the plan's loader would misread. But `execute` writes the same values into the assembly root's templates as they stand, and `project.yaml` and `contracts/{spec,code}-pin.yaml` carry most of them bare (`id: {{PROJECT_ID}}`, `path: {{SPEC_PATH}}`, `tracking_branch: {{TRACKING_BRANCH}}`). So once the plan kept `null` as text, `--spec-path null` ended `adoption verified` and the first bootstrap failed on `path is None`; `--id true` and `--project True` on `id is True`; `--pin null` on `neutral-pin-entry-malformed`; and `--tracking-branch true` pushed the legs on `true` while bootstrap put them on a local `True` with no upstream (lane 2's review of 76a6153, P2-1). `2026`, `1.5` and `~` did the same on main already (#187's review).

THE LOADER DECIDES. `loader_reading(line)` is the one question, asked by `y()`'s round trip (`_reads_back_as_text`, rewritten onto it; old and new bodies agree on 260,880 strings) and by the new gate `manifest_misreads`. The gate puts each value into every line of a templated YAML file (`shape_materialize.TEMPLATED`, the `.yaml` ones) that carries its placeholder, exactly as `execute` would, and asks the loader to read the line back; a pin is asked as the `  - <pin>` item `_template_values` writes. There is no list of words and no list of which placeholders are quoted: `elected_by: "{{ELECTED_BY}}"` is asked as the quoted line it is, so `Null` passes there and `CORP\tom` does not (the loader makes `\t` a tab). The values asked about: the id (named as derived when `plan` lowercased `--project`, or when the plan has no `id:`), the tracking branch, visibility, the elector, the election date, the reference, both leg paths and every pin.

- `plan`: `REFUSED adopt-manifest-misread`, exit 2, before the plan is written; each sentence names the flag (and where `plan` took a value it filled in itself), the value, the line `execute` would write, what the loader reads it as, and "choose another value".
- `check`: `FINDING plan-manifest-misread`, exit 1, with the remediation once after.
- `execute`: refused through `_entry_findings` and #168's `_refuse_what_check_finds`, before any leg repository exists.

Quoting the template placeholders instead was considered and not done: the same templates render every scaffolded project, and quoting is not enough on its own (`elected_by:` is quoted today and still misreads `CORP\tom`).

THE REVIEWER'S QUESTION, TAKEN. A plan carrying an explicit null leg path (`spec_path: null`, `~`, or the key with nothing after it, which is what a plan written by main's `plan --spec-path null` says) was read as the default and mounted at `spec` while its own follow-ups said `null/contracts`. `_plan_leg_path` now hands back the None it holds, as #169 already does for an explicit empty value, and `check` and `execute` report `plan-bad-leg-path: legs.spec_path is not text: its value was read as no value at all`. `check`'s mount line leaves out a path that is not text instead of printing `None/`. The not-text sentence no longer says "so quote it": quoted, every such spelling is text the gate now refuses.

P3-2: the test module's docstring said main refused a name ending in a line break as unparsable; on main 20e13f0 that plan reads the entry back as `trail` and `check` reports `plan-uncovered` and `plan-empty-entry`. Corrected.

Tests: tests/test_adopt_plan_yaml_quoting.py replaces the old end-to-end block. Each refused value is refused at `plan` (18 flag values, plus an id `plan` derived from `--project` and an elector it took from `git config`), at `check` (20 plan values, plus an id derived from `legs.assembly`) and at `execute` before any leg exists (the same 20, needs_filter_repo). What is still accepted (`--spec-path legs/null`, `--tracking-branch 2026-q4`, `--id yes`, `--elected-by Null`) goes through `execute`, the recursive clone and `bootstrap.py`, and each leg is on `2026-q4` with `origin/2026-q4` as its upstream. The gate follows the loader when the loader learns a keyword, reads the template line for its quoting, and agrees with the three files rendered whole by `render` on every candidate. An explicit null leg path is a finding for `null`, `~` and nothing. tests/test_adopt_leg_paths.py: the not-text sentence has no "quote" and covers None.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in #198 (20e13f0: #172 + #191, a leg's mount path must not be, hold or lie inside a path the assembly root holds, and `_leg_findings(plan, tree)` takes `tree` with no default), #201 (8df21e6: #179, the leg-path rule moved verbatim from adopt-project.py into scripts/shape_materialize.py, where scaffold-project.py asks it too) and #200 (a5b09c3: #174, the leg mount passes `--force` behind `_refuse_an_unmountable_path`). One merge, no rebase. Six conflicts, all in adopt-project.py:

1. Module docstring, `check`'s list: main's clause (neither equal to, around or inside a path the shape writes into the root, `shape/`, or a source path the plan keeps there), then this branch's (every value `execute` writes into the project's YAML one its loader reads back as that same text, #181), in one sentence.
2. The first `from shape_materialize import`: this branch's `TEMPLATED` and main's `assembly_root_paths`, both, in main's line layout.
3. The #169 section ("A leg's mount path is CANONICAL, or it is refused"): main replaced it with a pointer to `shape_materialize`, where the code now lives, and this branch had edited it in two places. Main's pointer is taken, and both edits are re-applied to the moved code in scripts/shape_materialize.py, unchanged in substance: `NOT_TEXT_KINDS` gains `type(None): "no value at all"` (an explicit null leg path, the reviewer's question on 76a6153), and `leg_path_problem`'s not-text sentence says "a leg path is text: write the path you mean, or remove the key to mount the leg at its role's own name" instead of "so quote it", with the comment saying why. Only a value read out of a plan file reaches that sentence: `plan`'s flags and scaffold-project.py's are always text, so the scaffold prints nothing new.
4. Two new sections inserted at one place after `_plan_leg_paths`: main's "A leg's mount path overlaps nothing the assembly root holds (#172)" first, beside the leg-path helpers it extends, then this branch's "What `execute` writes into the project is read back as written (#181)". Neither section is edited.
5. `_entry_findings`'s docstring: main's "and the two mount paths", then this branch's sentence on the misread findings.
6. `_entry_findings`'s body: main's `_leg_findings(plan, tree)` (#198 made `tree` required; dropping it raises TypeError on every `check` and `execute`), then this branch's `plan-manifest-misread` findings, whose remediation is still printed ahead of the submodule remediation.

Outside the conflicts: main's second `from shape_materialize import` also takes `NOT_TEXT_KINDS`, which this branch's `_the_loader_made` reads, and its comment no longer says "`leg_path_problem` is not called here", because this branch's `_plan_fields` calls it. Everything else merged cleanly, tests/test_adopt_leg_paths.py included (main's `_leg_findings(plan, None)` and this branch's not-text test touch different lines). An explicit null leg path never reaches main's collision check or its mount guard: `_leg_findings` asks `_leg_collision_findings` only when `leg_path_problems` found nothing, and None is a `plan-bad-leg-path` there, and `_checked_plan_values` refuses it before `checked_value` and the mount. The resolution adds no line over 79 columns that neither parent has.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in #173 (#165: verification passes over the two leg mounts and no other gitlink, with `adopt-root-submodule`, `adopt-unexpected-gitlink`, `adopt-duplicated` and `adopt-mount-missing`), which landed after this branch merged a5b09c3. No conflicts: git merged adopt-project.py and tests/test_adopt_submodules.py itself, and tests/test_adopt_verify.py is new. This branch's change set against main is line for line the one it had against a5b09c3. No rebase.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…way it was given and the way it was read, so `id: null` is no longer called "not in the plan", `CORP\tom` no longer looks like its own misreading, and a plan's `tracking_branch: 007` is no longer echoed as `'7'` (#181)

The NITs of review181 (PR-READY at a0ff15b). Nothing is refused that
was accepted, or accepted that was refused: only the sentences change.

N3. A plan with `id: null` (what `main`'s `plan --project True --id null`
writes) was named "id (not in the plan, so legs.assembly lowercased)". It
is in the plan and sets nothing, and `Plan.get` reads it exactly as a
missing key, so the derived id is now "not set in the plan" (`DERIVED_ID`,
built by the new `_id_field`).

N1. `--elected-by 'CORP\tom'` was refused as "is 'CORP\\tom' ... reads it
back as the text 'CORP\tom'": `repr` doubled the backslash the person
typed and spelled the loader's tab as the `\t` they typed, so each looked
like the other. A value is now shown between backticks as it is typed,
and a text reading names what a terminal shows as something else:
"is `CORP\tom` ... reads it back as the text `CORP`, then a tab, then
`om`" (`_worded`). The helper is not named `_spelled`: that is #166's
path spelling, which escapes backslashes on purpose and is unchanged.

N2. A hand-edited `tracking_branch: 007` or `TRUE` was named by `str` of
what the loader read (`'7'`, `'True'`), which the plan never said. A plan
value that is not text is now named by what was read, with its kind,
then the text `execute` writes: "tracking_branch was read out of the plan
as the number 7, ... and not as the text `7`". The same holds for an
`id: 007` (now asked as read, not as `Plan.project_id`) and for a pin,
which is taken as read rather than from `Plan.pins` and named "a pin in
pins": the list was not what was read as true.

N5. The import comment says `_the_loader_made` uses `NOT_TEXT_KINDS` only
for a list or a mapping; the module docstring says `null`, `true` and
`2026` misread where a template writes them bare, which `elected_by:`
does not. The section comment gives the true reason the placeholders stay
unquoted: it is left to a follow-up, because the same templates render
every scaffolded project and quoting them changes both tools.

N6. `'elected_by'`, `'elected_on'`, `'reference'`, `'PROJECT_ID'` and
`' and '` reached three uses in this file: they are constants now, read
by `_template_values` and `_refuse_unconsented_seeding` too (same text).
The test file's repeats are constants as well.

Tests: 405 in tests/test_adopt_plan_yaml_quoting.py (was 390). Each change
was reverted alone in a scratch clone and the whole file run: N3 fails 4,
the value as `repr` 69, a text reading as `repr` 11, a plan value made
text first 8, the id as `Plan.project_id` 1, the pins from `Plan.pins` 1,
a pin named `pins` 5.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in #199 (4835fb4: the plan writer escapes every character that is
not printable from the one table `repo_shape`'s reader resolves), #213
(404760f: `_split_flow` takes the character after a backslash inside a
double-quoted flow item) and #208 (e7d1720: update-shape.py keeps the
manifest's indentation).

No textual conflict: this commit's tree is `git merge-tree --write-tree`
of its two parents (0ac0750), and `_leg_findings(plan, tree)` keeps
`tree`. `y` now asks `_reads_back_as_text` (this branch) and quotes with
`repo_shape.double_quoted` (#199), and `_PLAIN_RE` ends at `\Z` (#199).

Three SEMANTIC conflicts, fixed in the commit after this one so that this
tree stays the mechanical merge:
- `_shown` is defined twice: #199's `_shown(path)` for the plan report,
  and this branch's `_shown(line)` for a misread sentence. The later
  definition, this branch's, replaces #199's for all four of its callers.
- #199's comment above `y` says a bare `null`, `true` or `false` is
  written unquoted, "#181's to fix": this branch quotes them.
- tests/test_adopt_plan_yaml_quoting.py's docstring says the loader
  resolves `\n` and `\t` only and that `trail\n` splits over two lines;
  #199's reader also resolves `\r` and the code-point escapes, and writes
  `trail\n` on one line.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…so after the merge of #199 it no longer replaces #199's `_shown(path)` for the plan report, and the comments say what the merged writer and reader do (#181)

The merge of main 4835fb4 had no textual conflict and three semantic
ones, named in its message; this commit resolves them.

- `_shown` was defined twice. #199's `_shown(path)` prints a name in
  `plan`'s and `check`'s report, raw unless it is not printable; this
  branch's `_shown(line)` quotes a template line in a misread sentence,
  between backticks. The later definition, this branch's, won for all
  four of #199's callers: at the merge commit 6b104cf five tests of
  tests/test_adopt_plan_escapes.py fail (the four names `plan` then
  `check`, and the source directory that is not UTF-8). The misread
  helper is `_line_shown` now; no top-level name is defined twice.
- #199's comment above `y` said a bare `null`, `true` or `false` is
  written unquoted, "#181's to fix". `y` quotes them, asking the reader;
  the comment says so.
- tests/test_adopt_plan_yaml_quoting.py's docstring said the loader
  resolves `\n` and `\t` only, and that `trail\n` splits over two lines.
  Since #199 the reader resolves `\r` and the code-point escapes too, and
  `y` writes `"trail\n"` on one line; the docstring says so.

The gate needed no change to follow #199's reader: it asks the loader,
so `--elected-by 'CORP\ralph'` and `--reference 'docs\x41.md'` are now
refused, with the carriage return named (`CHARACTER_NAMES` gains it) and
`docsA.md` shown as read. Both are test rows. `_print_what_will_happen`
held two locals named `where`, #199's source and this branch's mount
line; the second is `mounted`.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in #197 (b538015: scaffold-project.py refuses a leg path over what
the shape writes, through the overlap definition adopt-project.py asks,
which moved into scripts/shape_materialize.py), #202 (150c41d:
update-shape.py's `apply --add`) and #229 (39ac59c: the scaffold's leg
mount passes `--force`).

No textual conflict: this commit's tree is `git merge-tree --write-tree`
of its two parents (da9dad2), and `_leg_findings(plan, tree)` keeps
`tree`. No semantic one either: no top-level name in adopt-project.py,
repo_shape.py, shape_materialize.py or scaffold-project.py is bound twice
(imports included); #197's moved helpers keep the names this file called
them by; and none of the new tests or scaffold lines asks the not-text
leg-path sentence, `NOT_TEXT_KINDS` or the misread gate this branch
changes.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in #214 (06b5204: adopt-project.py and five other tools escape a
name their stdout cannot encode, set once at the start of `main()`) and
#217 (60d08a1: `shape_materialize.run()` and 26 other git and gh calls
decode what the child wrote as UTF-8 whatever the locale).

No textual conflict: this commit's tree is `git merge-tree --write-tree`
of its two parents (8c198f2), and `_leg_findings(plan, tree)` keeps
`tree`. No semantic one either: no top-level name in adopt-project.py,
repo_shape.py, shape_materialize.py or scaffold-project.py is bound twice
(imports included). #214 changes stdout's error handler only, and the
misread sentences this branch writes go to stderr unchanged. #217 changes
how a child's output is decoded, not the loader the gate asks.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in #209 (2727b08: update-shape.py `apply --branch` prints the
root's tracking branch as the `gh pr create --base`), #252 (4b2add8:
repo_shape.py refuses a flow-mapping entry opening `? ` and reads an
empty flow-mapping value as null), #195 (aaa40eb: adopt-project.py finds
a `.gitmodules` git cannot read when the history holds a gitlink the tip
does not), #210 (d89fffa: family.py `bump` to the current pin says
`nothing to do`), #211 (eb6df75: shape-doctor.py's `manifest kinds`
finding) and #248 (c3f4987: repo_shape.py keeps a block scalar's key and
body as a pair). One merge, no rebase.

One conflict, in adopt-project.py:

1. The import block: this branch's `import itertools` (`_worded` groups
   a value's printable characters with `itertools.groupby`) and #195's
   `import os` (`_exported_gitlink` passes git an `os.environ` with
   `GIT_GRAFT_FILE` set). Both are kept, in alphabetical order. This
   tree is `git merge-tree --write-tree` of the two parents (66cf0a2)
   with only that hunk's three marker lines removed.

No semantic conflict:
- No top-level name is bound twice in adopt-project.py, scaffold-project.py,
  scripts/repo_shape.py or scripts/shape_materialize.py, imports
  included. The names the two sides add to adopt-project.py are disjoint
  (33 here, 11 from #195), and the merged set is exactly the base's
  plus both.
- `_leg_findings(plan, tree)` keeps `tree`.
- #195's `_first_gitlink` calls #166's `_spelled(path)`, which this
  branch does not rebind: its own helpers are `_worded` and `_line_shown`.
- #248 removes `_BLOCK_SENTINEL` from repo_shape.py; nothing here names it.
- The gate still asks the real loader. `loader_reading` calls the
  `repo_shape.parse_yaml` that `execute` copies into the project
  (`COPIED_FROM_SHAPE`), and neither #252 nor #248 changed that function's
  interface. On both parents the gate gives exactly `parse_yaml`'s
  answer. It follows the new answers with no edit here:
  `tracking_branch: {a: }` is now a mapping where it was a refusal,
  `{? a: b}` is now refused where it was a mapping, and an item spelled
  as the old block-scalar marker is now the text it is.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in #212 (5751460: `check` and `execute` spell every path a
FINDING or a refusal names with `_spelled`, and `first_and_more` takes
an optional `spell`), #251 (23e16ae: scaffold-project.py refuses a
malformed `--referent-chain` link) and #206 (60588d5: family.py finds a
member in any case). Main moved while the runs at the previous merge
were going, and #212 changes adopt-project.py and
scripts/shape_materialize.py, which this branch changes too, so it is
merged again. One merge, no rebase.

No textual conflict: this commit's tree is `git merge-tree --write-tree`
of its two parents (7fe3ef6), and `_leg_findings(plan, tree)` keeps
`tree`. No semantic one either:
- No top-level name is bound twice in adopt-project.py, scaffold-project.py,
  scripts/repo_shape.py or scripts/shape_materialize.py, imports
  included. #212 adds no top-level name to adopt-project.py, and #251's
  four in scaffold-project.py are not this branch's.
- #212 is about paths. This branch's `plan-manifest-misread` names a value
  and a template line, never a source path. The value goes through
  `_worded`, which names each character `str.isprintable` refuses, and
  the line through `_line_shown`, so the finding stays one line, which is
  #212's point. It still sits in `_entry_findings`, between #212's
  respelled leg findings and the submodule findings, and `execute`
  refuses on that same list.
- This branch does not call `first_and_more`, whose new `spell` defaults
  to the old output.
- None of the three commits changes scripts/repo_shape.py. The gate
  gives the same answers here as at the previous merge.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in #207 (018ce0d: bump-leg.py rewrites and commits only the
workflow files git tracks), #272 (67a398a: the descendant scaffold tests
run validate-pins.py with --pin-source) and #205 (d94d155: shape-doctor.py
calls a project.yaml or family.yaml that does not load INVALID). None of
the three touches this branch's files. The lane's coordinator asked for
the branch to be on current main, so it is merged. #212 (5751460) was
already merged at 77218c8. One merge, no rebase.

No textual conflict: this commit's tree is `git merge-tree --write-tree`
of its two parents (8bfd548), and `_leg_findings(plan, tree)` keeps
`tree`. No semantic one either:
- No top-level name is bound twice in adopt-project.py, scaffold-project.py,
  scripts/repo_shape.py, scripts/shape_materialize.py or shape-doctor.py,
  imports included. #205 adds five names to shape-doctor.py, none of them
  this branch's.
- tests/test_shape_doctor.py, which #205 extends, still hands the
  doctor's placement plans, whose `legs:` is a list, to adopt's `Plan`
  and `_leg_findings(plan, None)`. This branch's `_plan_leg_path` reads
  a list as it did, as the role's own name.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in #215 (c2ff2b0: adopt-project.py finds a `.gitmodules` value
git's submodule reader refuses), #241 (c3a9645: scaffold-project.py
refuses a `--pin` whose file the shape writes itself), #284 (b30595c:
repo_shape.py reads a quote as opening a quoted scalar only where one
begins, and the materializer's `render` spells a value between a YAML
template's double quotes with `double_quoted`) and #204 (be2790e:
update-shape.py names the step that fails after `apply --branch`). One
merge, no rebase.

No textual conflict: this commit's tree is `git merge-tree --write-tree`
of its two parents (405a63c), and `_leg_findings(plan, tree)` keeps
`tree`. No top-level name is bound twice in adopt-project.py,
scaffold-project.py, scripts/repo_shape.py, scripts/shape_materialize.py
or shape-doctor.py, imports included; #215's eleven names, #284's three
reader helpers and `double_quoted` in the materializer are none of
this branch's.

One SEMANTIC conflict, fixed in the commit after this one so that this
tree stays the mechanical merge. `manifest_misreads` asks the loader
about the line `execute` writes, and builds that line in `_written` by
putting the value in its placeholder's place as it stands. #284's
`render` no longer does that between a YAML template's double quotes:
`elected_by: "{{ELECTED_BY}}"` and `reference: "{{REFERENCE}}"` get
`double_quoted(value)`, which the loader reads back as typed. So at
this tree the gate refuses `--elected-by 'CORP\tom'`, saying `execute`
writes `elected_by: "CORP\tom"`, where it writes `"CORP\\tom"`. And
it newly refuses `Robert "Bob" Smith` and `trail\`, which the previous
head accepted, because #284's reader refuses the raw line the gate
imagines.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…` as the materializer's `render` writes them since #284, the value spelled by `double_quoted` between the manifest's quotes, so `--elected-by 'CORP\tom'` and a name holding a `"` are no longer refused for a line `execute` does not write (#181)

The merge of main be2790e brought #284, whose `render` spells a value
between a YAML template's double quotes with `double_quoted`, the writer
`repo_shape`'s reader takes back unchanged. `_written` still built that
line with the value pasted in raw, so the gate asked about a line
`execute` no longer writes. It refused `CORP\tom`, `docs\new.md` and
`docs\x41.md` with a sentence about that line. It also newly refused
`Robert "Bob" Smith` and a name ending in `\`, which the previous head
accepted, because #284's reader refuses the raw line.

- `_written` takes `render`'s two steps for one placeholder, in order:
  `"{{KEY}}"` becomes `double_quoted(value)`, then any other `{{KEY}}`
  the value as it is. Every templated file it reads is `.yaml`, which is
  what makes `render` take the first step. Since #284, no value between
  the manifest's quotes is misread; the bare placeholders are asked as
  before.
- `FLAG_DEFAULTS` no longer names the elector `plan` takes from
  `git config user.name`: the manifest writes it between quotes, so no
  elector can be misread and the entry could not be reached.
- The section comment says what the quoted line now reads back as.

Tests (tests/test_adopt_plan_yaml_quoting.py), each where #284 changed
what is true:
- `REFUSED` loses its two rows `project.yaml` writes between quotes
  (`--elected-by 'CORP\tom'`, `--reference 'docs\new.md'`): 16 rows, so
  16 refused at `plan` and 18 at `check` and `execute`.
- `BETWEEN_THE_QUOTES`, new: the values the quotes misread before #284,
  `"`, a last `\` and a carriage return among them, are each asked as
  `render` writes them (`double_quoted`) and passed, for `elected_by` and
  `reference` (20 cases).
- The `git config user.name` elector `CORP\tom` is taken by `plan`,
  written `"CORP\\tom"` into the plan, and passed by `check`.
- `UNESCAPED`, which pins how a sentence shows a value the loader reads
  as OTHER TEXT (review181's N1), moves to the bare `tracking_branch:`
  placeholder with the value's own double quotes (a hand-edited plan's
  `'"CORP\tom"'`), where the loader still resolves the escape. It gains
  `a #b`, read as `a`. Its six escapes, the tab, the line break and the
  carriage return a sentence names among them, are the same.
- The drift guard that renders the files whole with `render` and compares
  every candidate with the gate covers `reference` too.

Revert lines, each measured alone on this commit's bytes, running
test_adopt_plan_yaml_quoting, test_adopt_plan_escapes,
test_adopt_leg_paths and test_template_quoted_values (900 tests):
- `_written` pasting a quoted placeholder's value raw again: 23 fail
  (the 20 `BETWEEN_THE_QUOTES` cases, the `git config` elector, and the
  drift guard for `elected_by` and `reference`).
- `FLAG_DEFAULTS` naming the elector again: 0 fail. No elector reaches a
  misread sentence since #284, which is why the entry goes.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in 6d8d88b (family.py `add` asks every refusal before it mounts
anything) and ede15ce (shape-doctor.py's validator rows print the
`PYTHONPATH` they ran under). Neither touches this branch's files; the
lane's coordinator asked for the newest main, and tests/test_shape_doctor.py,
which hands the doctor's placement plans to adopt's `Plan` and
`_leg_findings`, is in this branch's targeted set. One merge, no rebase.

No textual conflict: this commit's tree is `git merge-tree --write-tree`
of its two parents (fc78b0b), and `_leg_findings(plan, tree)` keeps
`tree`. No semantic one either: no top-level name is bound twice in
adopt-project.py, scaffold-project.py, scripts/repo_shape.py,
scripts/shape_materialize.py, shape-doctor.py or scripts/family.py,
imports included, and neither commit touches the reader or `render`.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in 6f45bf9 (#196: `check` and `execute` judge an entry's path by
one test, `entry_path_problem`, and `plan-unsafe-path` comes first),
b421e6b (#254: bump-leg.py and update-shape.py hand the validators an
explicit PYTHONIOENCODING), c0ed615 (#242: family.py `add` and `bump`
under the operator's excludes) and 71b58a1 (#262, #263: `check` finds,
and `execute` refuses, a path kept in the root where the shape writes).
6f45bf9 and 71b58a1 change adopt-project.py and
scripts/shape_materialize.py, this branch's files. Merged by sha, one
merge, no rebase.

Three conflicts, all in adopt-project.py, each resolved by keeping both
sides; the tree is otherwise `git merge-tree --write-tree`'s:
- The module docstring's `check` paragraph: main adds "no path kept in
  the root where the shape writes" (71b58a1), this branch "every value
  `execute` writes into the project's YAML ... one its loader reads back
  as that same text (#181)". Both clauses kept, main's first, in the
  order `check` asks them.
- `_entry_findings`'s docstring, which states the order of the findings:
  main's (each entry's path first, #196; what the root keeps where the
  shape writes, 71b58a1) with this branch's misread findings in their
  place, after the mount paths and before the source's own submodules.
- `_entry_findings`'s return: main's `unsafe_remediation` and this
  branch's `MANIFEST_MISREAD_REMEDIATION`, both printed once, in the
  order of the findings they follow: the unsafe path's, then the
  misread's, then the submodules'.

No semantic conflict found:
- No top-level name is bound twice in adopt-project.py,
  scaffold-project.py, scripts/repo_shape.py, scripts/shape_materialize.py,
  shape-doctor.py, scripts/family.py, scripts/bump-leg.py or
  update-shape.py, imports included. Main adds 18 names to adopt-project.py
  and 5 to shape_materialize.py, the branch none in common with them.
  #196's `_misread(path)` is a path git filter-repo would misread, not
  this branch's YAML question.
- Main changes neither scripts/repo_shape.py nor templates/, nor `render`,
  `double_quoted` or `write_lf`, so the gate still asks the reader
  `execute` copies into every project, about the lines `render` writes.
- This branch's edits to scripts/shape_materialize.py (None in
  `NOT_TEXT_KINDS`, the not-text sentence) are kept.
- The one call main's new code makes into this branch's code is
  `_unsafe_path_findings`'s `y(entry.get('leg'))`, which names the leg as
  the plan writer spells it.
- `_root_shape_findings` reads the plan's entries, never its leg paths,
  so an explicit null leg path (this branch's `plan-bad-leg-path`) does not
  reach it.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… writes it into a plan, so the merge of #215's `BOOLEAN_WORDS` no longer leaves the literal `"false"` written three times (S1192) (#181)

The merge of main be2790e (3352be4) brought #215's `BOOLEAN_WORDS`,
which spells `"false"` beside `y`'s own. `_the_loader_made`, this
branch's, spelled it a third time: `"the boolean " + ("true" if reading
else "false")`. The hand count of Sonar's rules was not made again after
that merge, so it went unseen until the merge of 71b58a1. Counted on
2405670 against main 71b58a1: `'false'` 3x, main 2x.

`y` writes a boolean as `true` or `false`, so every sentence is the same
as before: `_the_loader_made` gives "the boolean true" and "the boolean
false", which the REFUSED, CARRIED and READ_AS_NOT_TEXT rows assert.
Counted again: in adopt-project.py the only literals of five or more
characters used three times or more are the three main already used that
often (`'utf-8'`, `'visibility'`, `'tracking_branch'`), and no function
is over 15.

Revert line: reverting this commit makes no test fail, because no
sentence changes. It brings back `'false'` 3x, main 2x (S1192).

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ute` writes, whitespace after the pin and all, so a hand-edited `pins: ["openGlass "]` is no longer said to be written as `- openGlass` (#181)

rv181f's F1, found at e337f44 and there since b26841b. `_pin_field`
stripped the item line after the pin was in it, so the pin's own trailing
whitespace was gone from the line the sentence names. `execute` writes the
item as it stands (`write_lf` writes the text as given), so `check` said:

  a pin in pins is `openGlass `, which `execute` writes into
  project.yaml's neutral_product_pins as `- openGlass`, ...

`_written` strips a template line before the value goes in, which keeps
the value whole. `_pin_field` now strips only the item's indent, the same
way. The decision does not change: the loader drops the whitespace either
way, and that is why the pin is refused (rv181f: 0 of 87 decisions
differ; the line shown was wrong in 29 of 29).

Test: `test_a_pin_is_shown_in_the_line_execute_writes_whitespace_and_all`
runs `check` on a hand-edited plan, for a pin with a space after it (the
line shown between backticks as `- openGlass `) and one with a tab (shown
as Python spells it, '- openGlass\t'). No test covered this before:
rv181f's PINSTRIP revert row failed nothing.

Revert line: `.lstrip()` back to `.strip()` fails both cases (2 failed).

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…er `chars`, so its loop no longer shadows the module's `run()` (#181)

rv181f's F4. `itertools.groupby`'s groups were bound to `run`, the name of
adopt-project.py's own subprocess helper. `_worded` calls no `run`, so
nothing broke; the next edit to the function could have.

A rename only: every sentence is the same. No test can fail with it
reverted, and none is added.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in, by sha, the twelve commits after 71b58a1: c7c02fa (#299),
346079d (#216, adopt-project.py `plan`'s follow-up `git grep`), 34ecbc2
(#260, shape_materialize.py `render` in one pass), 86da808 (#253),
6f4ce47 (#302), 7491530 (#288), 7591455 (#279, tests/conftest.py),
87373a6 (#306), 8a14ac9 (#204), c1b6732 (#239), e43fa95 (#243,
adopt-project.py's clone template and `adopt-mount-commit`) and a1b3a4b
(#321). Three of them touch this branch's files. One merge, no rebase.

No textual conflict: this commit's tree is `git merge-tree --write-tree`
of its two parents (28c351f). No top-level name is bound twice in
adopt-project.py, scaffold-project.py, scripts/repo_shape.py,
scripts/shape_materialize.py, shape-doctor.py, scripts/family.py,
scripts/bump-leg.py or update-shape.py, imports included. Main adds four
names to adopt-project.py and `YAML_PLACEHOLDER_RE` to
shape_materialize.py, none of them this branch's, and no function is
changed on both sides.

One SEMANTIC conflict, fixed in the next commit so that this tree stays
the mechanical merge. #260's `render` now replaces each placeholder in one
pass, so a value is never searched again. `_written` still builds
`execute`'s line in #284's two steps: first the quoted placeholder, and
then the bare one over that output, which holds the value spelled by
`double_quoted`. So for `--elected-by` or `--reference` holding its own
placeholder, the gate asks about a line `execute` does not write, and
refuses a value that reads back as typed:
- `"{{ELECTED_BY}}"`: the gate builds
  `elected_by: "\""{{ELECTED_BY}}"\""`, and `render` writes
  `elected_by: "\"{{ELECTED_BY}}\""`;
- `x{{ELECTED_BY}}`: `"xx{{ELECTED_BY}}"` against `"x{{ELECTED_BY}}"`;
- `"{{REFERENCE}}"` for `--reference`, the same way.
Every other probe value, and every bare placeholder, builds `render`'s
line.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r` for the line `execute` writes, so since #260's one-pass `render` an `--elected-by` or `--reference` holding its own placeholder, such as `"{{ELECTED_BY}}"`, is no longer refused for a line `execute` never writes (#181)

The merge of main a1b3a4b (bf6d656) brought #260 (34ecbc2): `render`
puts each value in once and never searches it again. `_written` copied
#284's rules in two steps, first the quoted placeholder and then the
bare one, and the second ran over the first's output, which held the
value. So a value holding its own token was put in twice, and the gate
refused it, though `render` writes it as typed. At bf6d656, main's own
`test_adopt_records_each_free_text_value_as_typed[every-key]` failed:
`plan` refused with `adopt-manifest-misread`.

- `_written` hands each template line that carries the placeholder to
  `render` itself, the same way the gate hands the line it builds to
  `loader_reading`. No copy of the materializer's rules is left to go
  stale. The table is the value, plus `_as_they_stand`: every other
  placeholder on the line given back as its own token, which `render`
  writes as it was, `double_quoted` spelling `{{KEY}}` as `"{{KEY}}"`.
  So the line differs from the template's by the one value, and `render`
  is never asked for a key it would refuse.
- `render` and `PLACEHOLDER_RE` are imported from the materializer.

Test (tests/test_adopt_plan_yaml_quoting.py): `HOLDING_A_TOKEN`, five
values (each field's own token after a letter and between double quotes,
and `{{NOPE}}`). Each is asked as `render` writes it, for `elected_by:`
and for `reference:`, and passes (10 cases).

Revert line: `_written` back to the two steps fails 5. These are the
four own-token cases, `x{{ELECTED_BY}}` and `"{{ELECTED_BY}}"` for
`elected_by` and the same two for `reference`, and main's
`test_adopt_records_each_free_text_value_as_typed[every-key]`. With
this commit, tests/test_template_values_single_pass.py is 164 passed.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s as far as writing, so a `tracking_branch` it refuses as `unsafe-value`, or a `visibility` gh takes no flag for, is no longer called misread under a remediation saying the run would end `adoption verified` (rv181f's F2, F2b)

`_execute_gets_to_write(key, value)`, rv181f's prototype, is asked before
`_plan_fields` builds a field for a MANIFEST_FIELDS key:

- A `tracking_branch` must pass `checked_value`. `_checked_plan_values`
  refuses one it refuses as `unsafe-value` before `_refuse_what_check_finds`
  asks the gate (F2: 44 of rv181f's 540 sentences, all `tracking_branch`).
- A `visibility` must be one of VISIBILITY_CHOICES. `_create_leg_remotes`
  hands any other to `gh repo create` as a flag before anything is written
  (F2b). `check` is not told whether `execute` will run with
  `--local-remote-dir`, so such a visibility is asked about in neither mode.

TRACKING_KEY and VISIBILITY_KEY name the two keys once.

tests/test_adopt_plan_yaml_quoting.py:
- READ_AS_NOT_TEXT's list and mapping rows move from `tracking_branch` and
  `visibility` to `elected_on`, which `execute` writes as it stands.
- UNESCAPED's 7 rows move from the TRACKING_BRANCH placeholder to
  ELECTED_ON (lines `elected_on: ...`), which a plan edited by hand carries
  as far as the gate.
- EDITED_ONLY drops its `visibility: "true"` row (CARRIED 18 -> 17).
- New, no marker: `check` names neither `tracking_branch: '"CORP\tom"'` nor
  `visibility: "true"` as misread, and prints no remediation for them.
- New, with filter-repo: `execute` refuses that branch as `unsafe-value`
  before any leg exists, and `check` says nothing misread of it.
- REMOTES names the local remotes directory once (S1192).

The quoting file: 436 passed (435, less 2 CARRIED rows, plus 3 new).

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merged by sha: origin/main at 16:30Z. Past a1b3a4b it brings these, among
others:
- f795cf7 (#255): `plan-unwritable-value`.
- 161c6c9 (#245): relative `.gitmodules` urls.
- a983e6a: `execute` verifies before it exits on a refused `gh pr create`.
- 1f90a0f: the pin rewrite helpers in scripts/shape_materialize.py.
- ab623dd: the session `gh` guard in tests/conftest.py.

adopt-project.py conflicted in four hunks. Each resolution keeps both sides:
1. The module docstring's `check`. #181's clause about every value `execute`
   writes into the project's YAML now comes before the source's own
   submodules, which is where its finding comes. Main's relative url clause
   ends the sentence.
2. The `_entry_findings` docstring. This is main's text, with #181's misread
   values named after the root and before the submodules.
3. The `_entry_findings` return.
   - Findings: unsafe, coverage, legs and shape, misread (#181), submodules,
     unwritable (#255), relative url (#245).
   - Remediations: unsafe, misread, submodules, unwritable, relative url.
   - Main's NOTEs are third.
4. The mount line in `_print_what_will_happen`. #181's filter stays, so a
   leg path that is not text prints no `None/`, and main's `_shown` now
   wraps each mount and the branch.

Everything else merged clean. At this merge, tests/test_adopt_template_values.py
and tests/test_adopt_plan_yaml_quoting.py gave 514 passed.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, so a hand-edited `elected_on: "2026-09-02\nx"` or pin `"a\nb"` is one `plan-unwritable-value` finding, and `execute` refuses it under that code instead of `plan-manifest-misread`

At the merge of main 1f90a0f (e8e0026), `check` named such a value twice.
One finding was #255's `plan-unwritable-value`. The other was #181's
`plan-manifest-misread`, under a remediation saying the run would still end
`adoption verified`. That claim was false: `execute` refuses the plan before
any leg exists. `execute` also refused it as `plan-manifest-misread`, the
code of the first finding.

tests/test_adopt_template_values.py did not show this (514 passed at the
merge, with the quoting file): none of #255's rows is a value the loader
also misreads.

- `_execute_gets_to_write` returns False for a value `unwritable_reason`
  refuses. A `tracking_branch` goes through `checked_value` as before, which
  refuses every such value. A visibility goes through VISIBILITY_CHOICES as
  before, which hold no such value.
- `_plan_fields` now asks `_execute_gets_to_write` about each pin too.
- TRACKING_KEY in `_checked_plan_values`, and the four key constants in
  WRITTEN_PLAN_VALUES, replace their literals (S1192).

tests/test_adopt_plan_yaml_quoting.py:
- New, no marker: each of these is one `plan-unwritable-value` finding, with
  no misread finding and no misread remediation:
  - `elected_on: "2026-09-02\nx"`;
  - `visibility: "private\nx"`;
  - a pin `"a\nb"`;
  - a pin `"openGlass\t"`.
  At e8e0026 the date and both pins failed this test.
- New, with filter-repo: `execute` refuses the date as
  `plan-unwritable-value`, with no misread finding, before any leg exists.
  At e8e0026 it refused the date as `plan-manifest-misread`.
- PIN_WITH_A_TRAIL's tab row (rv181f's F1) is now a no-break space. Since
  #255, a pin holding a tab is #255's finding alone, so the tab moved to the
  table above. #255 writes a no-break space, the loader drops it, and the
  line is shown as Python spells it, just as the tab's line was.
- `branches_of` names the split-branch check once (S1192).

The quoting file and tests/test_adopt_template_values.py: 519 passed.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merged by sha: origin/main at 16:31Z, one commit past the 1f90a0f merged at
e8e0026. That commit is 92e29d9 (#319 via #377): `plan` asks about every
source path `execute` will not extract as it is named. It touches
adopt-project.py, tests/test_adopt_entry_paths.py,
tests/test_adopt_literal_names.py, tests/test_adopt_plan_escapes.py and
tests/test_adopt_submodules.py.

No conflict: #319 changes the module docstring's `plan`, the entry
questions and `cmd_plan`, none of which this branch changes.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merged by sha: origin/main at 19:37Z. Past 92e29d9 it brings twelve commits.
These four reach this branch's files or the reader its gate asks:
- 3ef059c (#314, for #258 and #261): `plan-manifest-invalid`, asked of the
  project's own validate-manifest.py, through `_manifest_findings`.
- cb7063f: the reader refuses a bracket in a plain flow scalar.
- f5cb616: docstrings in scripts/shape_materialize.py.
- dd8d0ea: `_elector` keeps a `user.name` byte that is not UTF-8, for #255.

adopt-project.py conflicted in two hunks, both in `_entry_findings`. Each
keeps both sides:
1. The docstring. The order is this branch's misread values (#181), the
   submodules, #255's unwritable values, then main's validator (#261), and
   the relative urls (#190) last.
2. The return.
   - Findings: unsafe, coverage, legs and shape, misread, submodules,
     unwritable, validator, relative url.
   - Remediations: the same order.
   - `_manifest_findings(plan)` is called as main calls it.

Everything else merged clean. That includes `_checked_plan_inputs` and
`_template_values`, which main split into `_template_table`.

At this merge a value both gates judge is two findings: `id: "007"`,
`id: "null"` and `elected_on: "20260902"`. Main's own
`test_a_text_value_the_reader_misreads_is_one_finding_in_the_list_check_prints`
fails for `id-007` and `elected-on-basic-format`, as its docstring says it
will on a merge that does not compose the two. A `visibility` outside the
three choices that is not text (`true`, `[secret]`) is no finding at all.
The next commit composes them.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… plan value once: the validator is not asked about a value #181 reported, and is asked about every other, so `id: "007"` is one finding and not two, and `visibility: true` or `[secret]` is one and not none

At the merge of main dd8d0ea (4c6e856), `_entry_findings` called
`_manifest_findings(plan)` as main does. The two rules then disagreed:
- **Two findings for one value.** `id: "007"`, `id: "null"` and
  `elected_on: "20260902"` were both #181's `plan-manifest-misread` and the
  validator's `plan-manifest-invalid`. Main's own
  `test_a_text_value_the_reader_misreads_is_one_finding_in_the_list_check_prints`
  failed for two of its rows.
- **No finding for one value.** #181 does not judge a `visibility` outside
  the three choices (rv181f's F2b). Main's validator left every value the
  plan reads as not text to "the misread rule". So `visibility: true`,
  `[secret]` and `{a: b}` got no finding at all.
- **The trap.** Main's own hook, `misreads_elsewhere=True`, would have
  dropped `visibility: "true"` too: it leaves out every value the reader
  misreads, not the values #181 reported.

The composition:
- `_plan_fields` returns each field with the placeholder it is about.
- `_misread_findings` returns #181's FINDINGs and the placeholders they
  report.
- `_manifest_findings(plan, reported)` leaves out exactly those
  placeholders and asks the validator about every other value.
- A value the plan reads as not text is named by what was read
  (`_named_as_read`): "visibility, read out of the plan as the boolean
  true, is 'True', which the project's own validate-manifest.py refuses:
  manifest-visibility: visibility is True, ...". The validator's sentence
  quotes `str` of the value, which the plan never said (review181's N2).
- `_execute_gets_to_write`'s docstring now says the validator names such a
  visibility in either mode. `--local-remote-dir` no longer writes it
  unasked.
- `misread_placeholders` is no longer imported.
- MANIFEST_PLAN_KEYS and MANIFEST_FLAGS are derived from #181's
  ID_PLACEHOLDER and MANIFEST_FIELDS, so each placeholder and each key is
  spelled once (S1192: the merge had made each placeholder 4 uses against
  main's 3).

Probe of `check` over 26 edited values, before and after:
- Every misread or invalid value is now exactly one finding.
- A value #255 refuses (a line break) is still listed by #255 and the
  validator both, as main's `_entry_findings` docstring rules (#255 before
  #261).
- A `tracking_branch` that `checked_value` refuses is still no finding in
  `check`, and `execute` refuses it as `unsafe-value`, as on main.

Tests:
- tests/test_adopt_plan_yaml_quoting.py
  - New, no marker: JUDGED_ONCE, 8 values. Each is one finding from the
    rule that is true of it.
  - New, no marker: NOT_TEXT_VISIBILITY, 2 cases, covering the naming.
  - New, with filter-repo: `execute` refuses `visibility: "true"` under
    the validator's code before any leg exists.
  - CARRIED's `check` and `execute` tests and READ_AS_NOT_TEXT now also
    assert no `plan-manifest-invalid`.
  - EDITED_ONLY's comment: since #261, `plan` refuses
    `--elected-on 20260902` on every Python.
- tests/test_adopt_manifest_values.py (main's)
  - Three calls move from `misreads_elsewhere=True` to `reported`.
  - `visibility: [secret]` leaves
    `test_a_value_the_reader_reads_as_another_kind_is_not_this_questions`:
    it is the validator's now, named by what was read (JUDGED_ONCE).

The quoting file, tests/test_adopt_manifest_values.py and
tests/test_cli_reference.py: 543 passed.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main 75052cb, merged by sha: #188 with #171 and #177 (PR #445), on top of
decff87 (#237), dba5fb0 (#383) and 245d775 (#412). Only 75052cb touches
this branch's files, in adopt-project.py. It changes neither the reader
(scripts/repo_shape.py), scripts/shape_materialize.py nor a YAML template,
so the gate asks the same reader of the same lines.

One conflict, in adopt-project.py, resolved keeping both sides:
- The `from shape_materialize import (...)` list. This branch added
  PLACEHOLDER_RE, TEMPLATED and render, which `_written` and
  `_as_they_stand` use. Main dropped RULESET_HINT: 75052cb prints the
  adoption's own ADOPT_RULESET_HINT instead (#177), and nothing else uses
  the scaffold's. The list is main's, without RULESET_HINT, plus the three,
  each name once.
The tree is otherwise `git merge-tree`'s.

No function is changed on both sides. The top-level names are exactly
both sides' (this branch's additions, main's 25, RULESET_HINT and
`misread_placeholders` gone), and none is bound twice in adopt-project.py,
scaffold-project.py or scripts/repo_shape.py. scripts/shape_materialize.py
binds YamlError and parse_yaml twice, as main does.

How #188 meets the gate. `_refuse_what_check_finds` is unchanged in place:
it still runs before #188's `_refuse_an_occupied_work_dir` and
`_refuse_a_split_branch_left_behind`, which run before the template table
and the leg repositories. So this branch's `plan-manifest-misread`, and
#261's `plan-manifest-invalid`, still refuse before anything exists,
whatever the work directory or the source's branches hold. Probed: a plan
carrying `id: "true"` or `visibility: "true"`, with a --work-dir that
already holds the assembly's directory, or with a split branch in the
source that the push cannot fast-forward, is refused under the gate's
code, exit 2, with no leg repository made and the source's branches as
they were. With a valid plan, the same two states are refused as
`adopt-work-dir-occupied` and `adopt-branch-exists`. #188's late-failure
reporting covers the steps once a leg repository exists, where this gate
never refuses.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brett Heap and others added 3 commits October 13, 2026 22:31
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>
Merged by sha, 89c6a6c, fetched at
22:55Z, on top of the merge of e6fbd74 (f54dce7). It brings two commits:
751cb2f (#507: scripts/family.py, two functions appended to
scripts/repo_shape.py, `shell_quoted` and `shell_line`, setup-project.py,
the family root's templates and tests) and 89c6a6c (#517 for #516:
scaffold-project.py writes each `neutral_product_pins:` entry so it reads
back, and `templates/assembly-root/contracts/neutral-product-pin.yaml`
writes `product: "{{PIN_PRODUCT}}"` for both tools).

No conflict: the tree is `git merge-tree`'s (f30b07d). One file changed
on both sides, tests/test_template_text_placeholders.py, in different
places, and both are kept: this branch's comment above `ADOPTIONS`, which
says `plan` quotes `true` and `null` now, and #516's docstring paragraph,
`neutral_pin_path` import, `TEXT_FIELDS` row for the pin's `product:` and
the adoption test of the pin file. adopt-project.py,
scripts/shape_materialize.py and this branch's other test files are not
touched by either commit. The YAML reader in repo_shape.py is unchanged:
#507 appends two shell-quoting functions after it.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…True@` and `007@` are planned, checked and adopted as given, and the misread gate's refusals, #479's branch questions and the validator-first order at `plan` are asked of lines written bare again, so no test of the gate passes with its change reverted (#181)

Main's e6fbd74 (#514) quotes an item of the manifest's
`neutral_product_pins:` list the project's reader would misread, and
89c6a6c (#516) quotes the pin file's `product:`. The gate asks the table
`execute` writes the list from (`_pin_field`), so it asks the quoted line
with no edit, and on the shipped templates it now finds nothing. The 14
tests that asked the bare line become true:
- `--pin null@`, `True@` and `007@` move from `REFUSED` to `READ_BACK`:
  `plan` writes `product: "null"`, `check` passes it, and `execute`
  writes `- "null"` into the manifest and `product: "null"` into
  `contracts/null-pin.yaml`, each read back as the text given.
- `test_plan_refuses_a_value_the_projects_loader_would_misread`,
  `test_check_reports_a_plan_carrying_one` and
  `test_execute_refuses_a_plan_carrying_one_before_any_leg_exists` run in
  this process over a line written bare: `REFUSED` is the branches `true`,
  `null` and `007` over `bare_template` and the pins `null`, `True` and
  `007` over the new `bare_pins`, the pins' list written as before #514.
  The check test also asks that the finding is the only one judged and
  the remediation is printed once.
- `JUDGED_ONCE` loses `pin-text-misread` (its successor is the check
  test above) and `pin-the-reader-refuses`, and `JUDGED_BY_THE_VALIDATOR`
  loses `its-reader-refuses-the-file`. No value of any key the manifest is
  rendered from makes a manifest the validator's reader refuses on the
  shipped templates (probed, 3384 plans), so G2 is asked of the same pin
  `? x` over `bare_pins`, in
  `test_a_manifest_its_reader_refuses_is_the_validators_alone`.
- The template-line test expects `- "Null"` and no finding. The table
  test drops its first assertion. Its stub that quoted each item would
  now quote an item twice, so it asks the real table (`- "null"`, passed)
  and then the list written bare (`- null`, refused).
- The keyword-learning test fixed the pin's line before teaching the
  reader, so it refused a line `execute` no longer writes. It now asks
  the pin after teaching (`- "yes"`, passed) and a branch over
  `bare_template` (refused).
- The two #273 pin tests that K7 and K8 revert run in this process over
  `bare_pins`, where a pin asked about would be named.

rv181g's B1 at 180902f: since #259 no test held #479's composition or the
validator-first order at `plan`. Added:
`test_a_branch_execute_does_not_push_in_every_mode_is_never_misread` (the
source lacks `true`, its `origin` lacks it, only `origin` has it; `plan`
and `check` in this process over `bare_template`),
`test_the_source_says_whether_execute_pushes_the_legs_to_a_branch`
(`_pushed_in_every_mode` for four sources) and
`test_plan_asks_the_validator_before_the_misread_rule` (`--id true`, with
the validator given the same bare copy). The shipped #479 tests say they
pass there because the line is quoted.

#514's docstring on `_neutral_product_pins` said keywords `y` leaves bare
reach it "only from a plan edited by hand, because `check` refuses the
bare `product: true` that `plan` writes". With this branch's `y` that is
false: `plan` writes `"true"` and `"null"` and `check` passes them. It
says so now. That docstring is the only adopt-project.py change since the
merges.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Oct 14, 2026

Copy link
Copy Markdown

Sorry @brettheap, your pull request is larger than the review limit of 150,000 diff characters

@brettheap

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-14T01:03:01.873199Z 9efe95a Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. What shall we delve into next?

Reviewed commit: 9efe95a981

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

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

Brett Heap and others added 2 commits October 14, 2026 03:13
Merged by sha, 8beb3d9, fetched at
03:08Z, on top of 9efe95a. It brings three commits since 89c6a6c, oldest
first: 173c00e (#510 for #500: `openRepoShape --doctor`), cf118ff (#513
for #499: `--org` and `--project` must name the repository the source
names) and 8beb3d9 (#512 for #501 and #502: update-shape.py,
shape-doctor.py and `collision_follow_up` in
scripts/shape_materialize.py).

One conflict, one hunk, in adopt-project.py `_checked_plan_inputs`, the
only unit both sides changed. Main passes `source` to `_check_names` for
#513's own-name rule; this branch follows that call with #181's comment
and its two refusals (`_refuse_unholdable_values`, then
`_refuse_misread_values`). Resolved as main's call with `source`, then
this branch's comment and two calls, unchanged in order. Main's other
change to the function, `_refuse_another_repository` before any pin's
digest is read, merged cleanly above it. So #513 refuses a plan for
another repository before #181 asks any value.

The comment in the hunk is brought up to date here (rv181g's N-7): since
#259 and #514 a value the loader would misread is only one on a line
written bare, no longer a pin's product. No code line differs from
rv181g's proposed resolution (d55b975, the same merge with cf118ff,
whose adopt-project.py is byte for byte 8beb3d9's).

Every other path is `git merge-tree --write-tree 9efe95a 8beb3d9`'s.
scripts/shape_materialize.py merges cleanly: #512 changes
`collision_follow_up` alone, a function this branch does not touch. No
top-level name is bound twice in adopt-project.py, and no name is read
and never bound; shape_materialize.py and family.py import `parse_yaml`
and `YamlError` twice, as on main.

With this merge alone five tests of this branch's quoting file fail:
their source's `origin` is a path ending in `seed`, which #513 reads as
the repository and refuses `plan --org testorg --project Northwind` as
`adopt-not-the-source`. The next commit puts that `origin` under
`testorg/Northwind`, as cf118ff did for tests/test_adopt_tracking_branch.py.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…` puts the source's `origin` under `testorg/Northwind`, so the five tests that plan from it are no longer refused as `adopt-not-the-source`, and adopt-project.py's comments no longer say a pin's product is written bare (#181)

M1 (rv181g, K5). #513 reads a path `origin`'s last two directories as
the source's repository and refuses a plan for another. `with_origin`
made `origin` a directory named `seed`, so `plan --org testorg --project
Northwind` was refused as `adopt-not-the-source` before #181 asked
anything, and five tests failed at the merge:
test_a_misread_branch_origin_lacks_is_479s_finding_alone,
test_a_misread_branch_only_origin_has_is_named_by_neither,
test_execute_refuses_a_branch_only_origin_has_under_a_local_remote and
test_a_branch_execute_does_not_push_in_every_mode_is_never_misread
[origin-lacks-it] and [only-origin-has-it]. `origin` is now
`<tmp>/testorg/Northwind`, as main's cf118ff did for
tests/test_adopt_tracking_branch.py. The text is rv181g's scratch commit
163fef9's, blob for blob.

The stale comments (rv181g's N-1 to N-6 and N-8; N-7 is in the merge).
Each said a pin's product is still written bare, or that a pin `? x`
makes a manifest its reader refuses. Since #514 neither is true of the
shipped code. Each old text occurred exactly once. None is printed: only
the module docstring's first line feeds `--help`, and
`render-cli-reference.py --check` is ok. With docstrings blanked, the
module's AST is unchanged.
- N-1, the module docstring: "where a line is written bare (since #259
  and #514 no line `execute` writes them into is)".
- N-2, N-3 and N-4, the section comment above the gate.
- N-5, `_pin_field`'s docstring.
- N-6, `_misread_findings`' docstring.
- N-8, the #261 section comment.

Sonar's new smells on 9efe95a.
- S9073: seven composite asserts in the quoting file are split into
  plain asserts with the same meaning. They are in `assert_reads_back`,
  test_a_float_a_bool_an_int_and_none_are_still_typed_values (two),
  test_the_writer_follows_the_loader_when_the_loader_learns_a_keyword,
  test_plan_refuses_a_value_the_projects_loader_would_misread,
  test_the_misread_rule_names_an_id_it_derived_from_the_assembly and
  test_a_source_file_named_like_a_keyword_is_one_entry_with_that_path.
  `assert_reads_back` builds its message once, and both asserts give it.
- S5778: the `pytest.raises` block in
  test_the_misread_rule_names_an_id_plan_derived_by_where_it_came_from
  held two calls. The `argparse.Namespace` source is now built before
  the block.
- S1192: the third `"project.yaml"` was this branch's own line,
  `MANIFEST_YAML = "project.yaml"`. Nothing has read it since 180902f
  took the pin's line from `_template_table`, so it is removed and the
  module is back to main's two. Pointing it at `SHAPE_MANIFEST` instead
  would fail at import, because that constant is bound more than 1,600
  lines later.

Windows. A pytest plugin outside the repository made text-mode writes in
the test process write `\r\n`, as on Windows: `io.open` and `open`
with no `newline`, so `Path.write_text` too, and text-mode `input=` to a
process. At PR #504's 9e8c672 it fails exactly the 17 tests of
tests/test_adopt_root_symlinks.py the Windows job failed, with its
message, and nothing else in that file. Over this branch's
four changed test files, in both modes, it fails nothing. No `os.name`
branch, `chmod`, text-mode stdin or path split is among their added
lines. So nothing here changes for Windows.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merged by sha, b0826e0, fetched at
12:45Z, on top of 6c9e585. It brings one commit since 8beb3d9: b0826e0
(#504 for #183 and #178: `check` follows each symlink the plan keeps
where the shape writes its files, and `execute` refuses one the shape
cannot write through before any leg exists).

Three conflict hunks, all in adopt-project.py. Each keeps both sides:
- The `from shape_materialize import (...)` list. This branch imports
  `PLACEHOLDER_RE`, `TEMPLATED` and `render`; #504 imports
  `SYMLINK_HOPS` (moved there from this module), `TOO_MANY_SYMLINKS` and
  `template_files`. Each name is imported once, in the list's own order
  (constants, then classes, then functions, each alphabetical), and each
  is read in the module.
- `_entry_findings`' docstring, the order of the findings: the values
  `execute` would write for the loader to read back as something else
  (#181) after the mount paths, then the source's own submodules, then
  each symlink kept in the root that a shape file cannot be written
  through (#178, #183), then the rest as both sides have it.
- `_entry_findings`' return. This branch's comment and its `tracking`
  and `misread` lines are kept. The findings are `findings + misread +
  split + _root_symlink_findings(plan, source, tree) + unwritable_findings
  + value_findings + tracking_findings + moved + field_findings`. The
  remediations are this branch's list, unchanged: #504 adds a finding and
  no remediation to it.

Outside the hunks git merged both sides cleanly where both touched one
unit: the module docstring holds #181's sentence on the values `execute`
writes and #504's on symlinks kept in the root, and `y` holds #181's
`_reads_back_as_text` question and #504's `FALSE_WORD`. #504 puts
`work_root` before `cloned` in `Source.__init__`. Its own `Source.open`
passes both, and every other call (all in tests) passes four arguments
by position, so none shifts. scripts/shape_materialize.py merges
cleanly, and no unit of it changed on both sides.

Every other path is `git merge-tree --write-tree 6c9e585 b0826e0`'s,
and adopt-project.py is its blob outside the three conflict blocks. No
top-level name is bound twice in adopt-project.py, none is read and
never bound, and no imported name goes unread. shape_materialize.py and
family.py import `parse_yaml` and `YamlError` twice, as on main.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 14, 2026
…`, `eol_line` and `with_eol_block`, are a `from shape_materialize import` statement of their own, so a pull request that adds names to the shared list above it merges without a hand resolution (#471)

The first `from shape_materialize import (...)` statement is the list every
change to this file's imports edits: #504 added `SYMLINK_HOPS`,
`TOO_MANY_SYMLINKS` and `template_files` to it, and #518 (#181) adds
`PLACEHOLDER_RE`, `TEMPLATED` and `render`. Git treats two changes to
adjacent lines of one block as one conflict, so #471's names on lines of that
same list made #518 conflict there a second time, over names it never
touched, and whoever merged it had to rewrite the list by hand.

The four names now sit in a statement of their own, after the one that
imports #172's shape half, with that statement and a comment between them
and the shared list. The shared list reads as main holds it. Nothing else
changes: the set of names `adopt-project.py` takes from `shape_materialize`
is the same 48, parsed with `ast` before and after, and every other
statement of the module is the same.

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Merged by sha, 3121945, fetched at
13:35Z, on top of b602c16. It brings one commit since b0826e0: 3121945
(#494 for #370: a refusal built without a remedy prints its `REFUSED`
line alone, because `Refusal`'s default remediation is now none, and the
assembly root's `validate-pins.py` gives its own refusals the lockstep
remedy).

No conflict. The tree is `git merge-tree --write-tree b602c16 3121945`'s
(4a502cd) on every path, and no top-level unit of adopt-project.py,
scripts/repo_shape.py, scripts/family.py or shape-doctor.py changed on
both sides. #494 changes adopt-project.py in `_failed_loudly` alone.

This branch's refusals each name their own remediation, so none falls
back to the new empty default: `plan`'s `_refuse_unholdable_values`
through `refuse_manifest_values` (`f"{remedy} {tail}"`), `plan`'s
`_refuse_misread_values` (`MANIFEST_MISREAD_REMEDIATION`, then "No plan
was written."), and `execute`'s `_refuse_what_check_finds`, whose list
carries the misread remediation after its own. Every other `Refusal` in
a unit this branch changed passes one too.

No top-level name is bound twice in adopt-project.py, none is read and
never bound, and no imported name goes unread. shape_materialize.py and
family.py import `parse_yaml` and `YamlError` twice, as on main.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-yaml-keywords: clean, and its tree is 88c22f0, the tree of the scratch merge a9439715 every Phase A run was made on

Main 5ed3e5d is #511 (lane 4) for #471,
landed at 16:08:28Z, merged here by sha. The split appends one marked block
of `/<copy> text eol=lf` lines to a source's own root `.gitattributes` and
verifies that file as the source's bytes followed by exactly that block;
`update-shape.py apply --add` extends it; the Windows job installs
git-filter-repo 2.47.0.

No conflict. The tree is 88c22f0, which is
`git merge-tree --write-tree 42362de 5ed3e5d`, and the tree of
a9439715bfb50186565526cdddc828f57b14409f, the scratch commit made before
#511 landed from 42362de and #511's predicted squash bd78e56e (tree
d58ea4b, which 5ed3e5d's tree is). So every Phase A result at a9439715
holds here:
- one unit changed on both sides, `cmd_plan`, keeping this branch's
  validator-first comment and #511's `attributes_link`; every other unit
  is the one side's that changed it; no name in adopt-project.py is bound
  twice;
- #511 adds no `check` finding and no refusal before any leg exists, and
  changes no template, no `render` and no line `execute` writes into the
  project's YAML;
- the targeted sets, both modes, exit 0: 1928/1833 in 14 files, 3869/3686
  in 65 more, 963/953 in 11, 364 in 10, 248/223 in five, and 37 in #511's
  tests/test_adopt_attributes_eol_block.py; the quoting file is 524;
- the Windows newline emulation over this branch's four test files,
  test_adopt_root_symlinks and test_adopt_attributes_eol_block: 1280
  passed, and 1207 passed with 73 skipped, 0 failed;
- byte identity for the three flag sets and for a source with its own
  `.gitattributes`; the class fuzz (3146 rows, #181 names none, no false
  or double clause); the why, pin-file and residual drivers unchanged;
- the composition probes with #504, #513 and #511.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

brettheap pushed a commit that referenced this pull request Oct 14, 2026
…5 cites the six pull requests merged since #402, section 6 drops the `--help` verdict line #510 fixed and AT-family N2, which #501's ruling decides, and adds 28 checklist lines from the parents, and section 7 lists the 3 open milestone issues (#472)

Refresh round 2. GitHub was read from 2026-10-14T03:11:22Z to 03:18:32Z;
main was 8beb3d9 (#512) throughout. Every merge commit of the 223
merged pull requests is an ancestor of it.

Section 5 adds, from each squash message and PR body:
- adopt-project.py: #515 (a pinned `007` listed as text) and #513 (a new
  "The source's own name" bullet: `adopt-not-the-source`,
  `plan-not-the-source`, rename first);
- update-shape.py and bump-leg.py: #512 (a new bullet: no NEXT passes
  `--accept-local`; `merged-away` rows dropped by `apply`);
- setup, scaffold, family and the shared helpers: #517 (a `--pin` product
  listed as text) and #507 (`family-landing-in-standard`,
  `family-on-tracking-branch`, a quoted NEXT that pushes the branch);
- shape-doctor.py: #510 (the installed doctor keeps its clone, and passes
  `--json` and `--placement-plan` through). The bullet's "writes nothing
  into the repository" now adds "but a `--placement-plan` file you name":
  a relative plan file given from inside the repository is written there,
  as #460's new line says.

Section 6 drops two lines and adds 28; no known-limit issue closed, so
the 47 issues stay:
- #457, 16 lines: L2-14, which #515 makes true of main; 10 of lane 4's 14
  lines from #513 (00:06Z), two of them, a backslash `origin` and a
  trailing slash, in one bullet; and L1-K1, K2, K3, K7, K9 and K10 from
  lane 1 (01:17Z).
- #458: AT-family N2 is dropped, as lane 4's status note there says
  (00:48Z); 2 lines added from #512.
- #459, 3 lines that #507 and #517 make true of main: the validate-family
  remedies line (03:27Z), the README `--into` line (11:46Z) and L2-15.
- #460: the `openRepoShape --help` verdicts line is dropped (#510 fixed
  it); the `shape currency` line is reworded, because since #402 the
  `apply --branch` is the row's WHY and since #512 it has no
  `--accept-local`, but under `--branch` it is still refused
  `update-branch-uncommitted` before `update-local-drift`; 7 of lane 4's
  8 lines from #510 (23:34Z) are added.

Left out, by round 1's rules: #513's `insteadOf` line and README sentence
(wording), its comments line (comment wording), its extra `git config`
read (no effect a person meets); L1-K4 (cannot arise today); L1-K5, K6
and K8, which describe #518's and #504's branches; and #510's `--help`
synopsis column (cosmetic). #458's 03:26Z restatement is folded into its
existing `repo_shape.py` line: #507 removed the instance it added.

Section 7 lists #370, #471 and #472, the milestone's three open issues of
47, and says #178, #181 and #183 are not in the milestone.

What was read at 8beb3d9, by `git show` or `git grep` of the code or
text each line cites:
- section 5: all six new sentences (adopt-project.py:6986-7030 and
  :7812-7960; update-shape.py:287 and :1939-1969; shape-doctor.py:3238-3243
  and :3298-3314; scaffold-project.py:1293-1330 and the neutral pin
  template's line 30; scripts/family.py:620-720 and :925-960;
  openRepoShape:97-111, :489-490 and :540-558);
- #457: L2-14, the backslash and trailing-slash line
  (`_remote_repository`, adopt-project.py:1164-1182), `check`'s missing
  rename-first remedy (:5137), L1-K3 (scripts/repo_shape.py:2048), L1-K9
  (adopt-project.py:3316-3333);
- #458: both new lines (shape-doctor.py:2005-2060 and :3222-3227), and N2's
  drop (update-shape.py:2915-2919 and `apply_command`);
- #459: all three (validate-family.py:356-358 and :1066-1071; README.md:861-
  862, docs/handbook.html:1247; scaffold-project.py:1313-1330 against
  adopt-project.py:7017-7030);
- #460: the dropped and reworded lines (openRepoShape:97-100, docs/cli.md:
  160-163; shape-doctor.py:3306-3314 and `cmd_apply`'s refusal order), the
  adoption NEXT with no `--org` (shape-doctor.py:6141-6143), the two
  `--placement-plan` line (openRepoShape:540-558, shape-doctor.py:6828),
  the "writes nothing" line (openRepoShape:55 and :605, README.md:1112,
  docs/handbook.html:1467) and the kept-clone line (openRepoShape:490);
- kept lines whose code a landing touched: #458's "not hand-edited" commit
  and title-less `gh pr create` (update-shape.py:2349-2353, :3796-3797),
  AT-family N3, #460's `=word` line (`UNQUOTED_POSIX` admits `=`), and
  #459's unquoted setup `cd` and step (7) (setup-project.py:2023-2029,
  :2169).

The rest rest on the posting lane's measurement, not re-read here: of
#457's lines, lane 4's mirror path, `--source` after the clone, recorded
`source:`, `ATLAS`, fork, `straße` and null-`org:` lines (measured at
cf118ff; adopt-project.py is the same at 8beb3d9), and lane 1's L1-K1,
K2, K7 and K10 (measured on 8beb3d9); of #460's, the TERM, interrupted-run
and drifted-project-test lines (measured at 173c00e; `openRepoShape` is the
same at 8beb3d9).

The file is 7,354 words, from 6,611: section 5 is 2,291 (2,085), section
6 4,351 (3,607) and section 7 109 (316).

No test names `docs/release-notes/` (`git grep -n release-notes
origin/main -- tests/` finds nothing), but two tests in
tests/test_repo_hygiene.py read every tracked `.md` through `git
ls-files`: the host-absolute-path test (`HOST_ABSOLUTE_PATH`, :1496-1527)
and the "<word> naming families" count (:1545-1601). Their two patterns,
applied to this commit's file, match nothing. No test was run.

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 14, 2026
…5 cites #504 and #494, section 6 drops #178 and #183, which #504 closed, adds the four limits #504 disclosed for #457 (J1, J2, J3, reftable) now that they describe main, and narrows two lines #504 and #494 made partly untrue, and section 7 lists the 2 open milestone issues (#472)

Refresh round 3a, against main 3121945 (#494, merged
2026-10-14T13:21:57Z). Two pull requests merged after round 2's
8beb3d9, and both merge commits are on main, as are all 225:

- #504 (b0826e0, lane 1, closes #178 and #183) under adopt-project.py,
  as a new bullet, "Symlinks the root keeps": `plan-root-symlink` and
  `adopt-symlinks-off`, both before any leg.
- #494 (3121945, lane 3, closes #370) under the shared helpers, where
  #370's parent #459 puts it: a refusal with no remedy of its own prints
  its `REFUSED` line alone, `validate-pins.py` keeps the lockstep remedy,
  and `family.py add` refuses `gitmodules-unreadable`. Lane 3 asked on
  #473 (13:22Z) that #370 be placed in the notes.

Each line was written from the PR body and its squash message, and its
central claim read in main's code: `_entry_findings` and
`_root_symlink_findings` (adopt-project.py:4701, :5717),
`_refuse_symlinks_checked_out_as_files` (:5824) before
`_create_leg_remotes` (:7542, :7580); `Refusal`'s empty default
(scripts/repo_shape.py:166-174), `validate-pins.py`'s handler
(templates/assembly-root/scripts/validate-pins.py:1181-1184), family.py's
`gitmodules-unreadable` (:1912-1926), and the doctor rows that take
`exc.remediation or None` (shape-doctor.py:4546).

Section 6:
- Dropped #178 and #183, closed by #504.
- Added L1-J1, L1-J2, L1-J3 (#457, 2026-10-12T23:36Z) and L1-K8 (#457,
  2026-10-14T01:17Z). Round 2 left them out because they described
  #504's branch. #504's body lists all four as its known limits. J2's
  `check` half (the refusal and its false sentence), J1's `check` half
  (`plan ok`) and J3's `check` halves were run at 3121945 in an offline
  probe. J1's and J3's `execute` halves rest on lane 1's measurement and
  the code (`_has` and `CLONE_GIT_TREES` at :5427-5438;
  `adopt-symlinks-off` at :5824-5866). K8 is read from the code only
  (`CLONE_GIT`, :5201-5206), and its line says it is untested.
- Narrowed L1-K1's line. A link at `contracts/shape-pin.yaml` that leads
  nowhere is now a `plan-root-symlink` finding, since the walk follows
  `SHAPE_PIN` (shape_materialize.py's `assembly_root_paths`). The probe
  ran it, and a real file there still passes `check`. The line now says
  a file, or a link there to a declared pin's file, as K1 measured.
- Narrowed #408's line. The issue's own repro, a policy file that does
  not parse, carried `repo_shape`'s default lockstep remedy, which #494
  removed, so the `placement` row now has no NEXT. Only a policy file
  that is not UTF-8 still gets prose there (`load_yaml`'s own remedy,
  which holds no backtick). An in-process probe of `PathPolicy.load` with
  main's and 8beb3d9's repo_shape.py shows both.
- Kept L1-K2, K9 and K10, which lane 1 measured as the same on #504's
  branch. The five parents have no new comment and an unchanged body
  since round 2's read (03:11Z to 03:18Z). Their sub-issues still list
  every open, unmilestoned one: 45 (#457 10, #458 6, #459 17, #460 9,
  #461 3).

Section 7: the milestone is 45 closed and 2 open (#471, #472). #370 left
the list. Of the three issues section 7 named as outside the milestone,
only #181 is open.

No claim is made about an unlanded pull request (#511, #518), and
nothing is said about a tag or a date. 7,354 words before, 7,628 after.

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 14, 2026
…5 cites #511 and drops the Windows qualifier now that the Windows job runs the adoption suites, section 6 cites the v1.1.0 issue of each line the triage scheduled, drops the five lines closed on their parents and #408, and adds #511's line-ending limits, and section 7 lists #472 alone (#472)

Refresh round 3b, against main 5ed3e5d (#511, merged 2026-10-14T16:08:28Z,
closing #471). All 226 merge commits are on main.

Section 5. #511 is cited twice: under adopt-project.py, as a new bullet on
a source's own `.gitattributes`, and under the test suite and CI, in the
"Three operating systems" bullet. "Every source path is verified by blob
sha" gains the exception AGENTS.md step 7 now states. The claims were read
in main's code:
- `.github/workflows/tests.yml`:171-182, where `tests-windows` installs
  `git-filter-repo==2.47.0`; :209, where it runs the whole suite; and
  :118-125, where it runs on pull requests, not on a push to main;
- `scripts/shape_materialize.py`: `EOL_BLOCK_HEAD` (:701), `eol_line`
  (:712-721), `eol_block_paths` (:724-729), `with_eol_block` (:738-744), and
  `_append_eol_block` (:813-831), which writes through no symlink, as called
  from `materialize_assembly_root` (:2256-2257);
- `adopt-project.py`: `_land_the_eol_block` (:8081-8124, called at :8063)
  and the split message's `APPENDED TO` (:7953);
- `update-shape.py`: `_extend_the_eol_block` (:3061-3089).

The qualifier. Main's workflow now does what the unqualified sentences say,
so both `<!-- drop the Windows qualifier once #471 lands as proposed -->`
comments and the two sentences they marked are gone.

Section 6, re-read after lane 3's v1.1.0 triage (#473, 16:03:48Z, on
Brett Heap's approval at 14:31:28Z):
- 74 lines marked `→ #N` or "rides with #N" on their parent now end with
  that number, as in (#524). For each parent, the set of numbers cited
  equals the set its marks name. One sentence in the section's opening
  says what the number is.
- Dropped, each closed on its parent: `--project ATLAS` and a fork's
  `origin` (#457, ruled behaviour); the scaffold's three working trees
  (#459, fixed by #505 and kept documented); and the `shape currency`
  row's uncommitted-copy line (#460, obsolete since #402 and #512). Also
  #408, closed as fixed by #494.
- Added, under #457: the symlinked root `.gitattributes`, the nested one
  (#530), and a root adopted before #511 (lane 4, 2026-10-12T23:25Z);
  then #511's three posted at 16:09:31Z: a copy written through a
  symlinked directory, a CRLF `.gitattributes` under a rule other than
  `text=auto`, and a `filter=` or `working-tree-encoding=` rule. Its
  fourth, a test helper that feeds text on stdin, is left out: no person
  meets it. These rest on lane 4's measurement (V511c at 1ce4f45, whose
  tree is main's); `eol_line` writing only `text eol=lf` and the symlink
  skip were read in code.
- Reworded:
  - AT-scaffold N9 (#458), with lane 4's 16:10:03Z note. `cmd_apply`
    always stages the pin and the manifest (`update-shape.py`:3280-3281),
    and `_land` prints them all (:3224).
  - The annotated-tag line (#459). It is now ruled that `bump --to` peel
    it (#541), so "One rule is still to be chosen" is gone.
  - The Windows symlink line (#457), narrowed to its parent's own words.
    #511's end-to-end test of a source that tracks a symlinked
    `.gitattributes` under `core.symlinks=false` carries only
    `needs_filter_repo` (tests/test_adopt_ignored_shape_files.py:862-863),
    so it now runs on Windows.
- Left out, new: the three CI lines new on #461 (the merge queue and two
  CI costs). They are this repository's CI, not a limit of the shape.
- Lane 1 has posted no #504 batch, so D13 stays out.

Section 7: v1.0.0 is 46 closed and #472 open. #181 is still outside the
milestone.

No claim is made about an unlanded pull request (#518, #551), and nothing
is said about a tag or a date. 7,628 words before, 7,906 after.

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@brettheap
brettheap merged commit b8b7062 into main Oct 14, 2026
5 checks passed
@brettheap
brettheap deleted the fix/adopt-plan-quote-yaml-keywords branch October 14, 2026 18:50
brettheap pushed a commit that referenced this pull request Oct 14, 2026
…5 cites #518, section 6 drops #181's line, which #518 closed, adds the three limits lane 4's reviews posted on #458 and #460 after the triage and extends two #544 lines with what #461's body now says, and section 7 no longer says #181 is outside the milestone (#472)

Refresh round 3c, the last before the v1.0.0 tag. Main is b8b7062
(#518, merged 2026-10-14T18:50:03Z, closing #181), the only merge since
round 3b's 5ed3e5d. All 227 merge commits are ancestors of it. The notes
describe nothing on an unlanded pull request.

Section 5: one adopt-project.py bullet, "Keywords as typed", written from
#518's squash message with its body beside it. Read in main's code:
adopt-project.py `_PLAIN_RE` (:580), `loader_reading` (:583),
`_reads_back_as_text` (:603), `y` (:625-637), `MANIFEST_MISREAD_REMEDIATION`
(:2216), `_misread_findings` (:2587, `plan-manifest-misread`),
`_refuse_misread_values` (:2939, `adopt-manifest-misread`), `_entry_findings`'
return (:5354), `_refuse_what_check_finds` (:7576, called :8206);
scripts/shape_materialize.py `leg_path_problem` (:3492, a null leg path is
"not text"); templates/assembly-root/project.yaml quotes each placeholder the
gate asks about (:31, :42, :46, :47, :62, :67, :159, :163). Run offline, with
a stub `gh` that was never called, at b8b7062 and at 5ed3e5d: `--spec-path
null` mounts the leg at `null/` (at 5ed3e5d, at `spec`), `--id null` writes
`id: "null"` (`"northwind"`), `--tracking-branch true` on a source with that
branch puts both legs on `true` (refused `plan-tracking-branch-missing`),
a hand-edited `spec_path: null` is `plan-bad-leg-path` (`plan ok`), and the
default flags adopt as before. The gate's "names none today" rests on the
templates' quotes and on lane 1's fuzz (286 values, 0 rows), not on a run.

Section 6: #181's line is dropped (a source with files named `null` and
`false` now plans them quoted, asks the ordinary `plan-unresolved` question,
and adopts once it is answered; probed). #457 has no new comment and an
unchanged body since 16:09:31Z. L1-J1, J2, J3, K1 and K8 hold on main
(`check` halves probed; K8 read at adopt-project.py:5861), K3 (`id: 007`
written as `"7"`) and K7 (two findings for a tab in a pin's product) probed
again. K4 and K6 stay out: nothing a person meets. Added from lane 4's
2026-10-14T18:32Z posts, which are untriaged and so carry no number: on #458
a quoted or spaced `files:` or `shape:` key (read at update-shape.py:1378 and
:1502, and run in process for the pin; `check`'s exit and the manifest half
rest on R558's measurement) and `bump-leg.py --dry-run` over a pin its
validators refuse (read at bump-leg.py:1475 against :1388-1396; the tab-line
example rests on R561); on #460 an unparsable standard policy file
(shape-doctor.py:1723 read; the exit 2 rests on R557). Left out: #458's two
R556 lines (a wording imprecision; and a refusal that is by design on main,
bootstrap.py:275), #459's two R557 lines (each repeats a line the notes
already hold, #443's and the `UnicodeEncodeError` one), and #461's
optional hygiene-test line. #461's other two new lines extend the two #544
lines: four `git submodule update --init` runs in two test files and
test_adopt_plan_yaml_quoting.py:2036 (read), and tests/test_cli_reference.py's
`--help` children (render-cli-reference.py's `child_env` sets no
PYTHONDONTWRITEBYTECODE; the `.pyc` itself rests on #554's review).

Section 7: the sentence about #181 is gone. The v1.0.0 milestone stands at 1
open (#472) and 46 closed.

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 14, 2026
…e `execute` has just pushed the split into, which #518 added, is a `cloned_just_pushed`, so no plain-path clone of a just-pushed remote is left that main brought in (#544)

What: after `execute --local-remote-dir` the test cloned the source at the split branch by its plain path with `git -c protocol.file.allow=always clone --recurse-submodules -b`. It is now `cloned_just_pushed(source, tmp_path / "clone", SPLIT_BRANCH)`, the same options: recursive, `-b`, the legs read through the `file://` rewrite (they sit in `tmp_path / "remotes"`, beside the source). `FILE_PROTOCOL` is no longer imported there; nothing else in the file used it.

Why: the landing condition of #554, which said to move #518's clone at the pre-landing merge of main. A re-grep of every `"clone"` call in tests/ finds no other new plain-path clone in what the merge brought in (test_adopt_leg_paths.py, test_adopt_manifest_values.py, test_template_text_placeholders.py, test_adopt_plan_yaml_quoting.py).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 14, 2026
….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)

#472 asks for one file that says what v1.0.0 covers and lists its known
limits. `docs/release-notes/v1.0.0.md` is that file. Its eight sections
are:

1. what openRepoShape is, and what 1.0 promises: every procedure in
   AGENTS.md works end to end for ordinary repositories on Linux, macOS
   and Windows, with park and resume under WSL2 on Windows;
2. what 1.0 contains, one line per procedure;
3. requirements and platforms;
4. how to pin 1.0.0, naming its commit rather than its tag;
5. what changed on the way: all 227 merged pull requests, #1 to #518,
   grouped by theme under the five tracking areas (#457 to #461);
6. the known limits: 43 open issues and 124 checklist lines from those
   five parents. Each line names the input that triggers the limit and
   what the person sees, and ends with the `v1.1.0` issue that schedules
   it, where there is one;
7. "Before tagging": the `v1.0.0` milestone, 1 open (#472, which this
   closes) and 46 closed;
8. links to the merged pull requests and the milestone.

The notes describe main at `b8b70627`. The last merge that section 5
covers is #518, at 2026-10-14T18:50:03Z. The first draft was `cbe44fe`.
Six later rounds brought it up to main as pull requests landed, the last
one after #518.

The file is new and nothing else changes. README.md stays at 1422 lines
and AGENTS.md at 490. No rule in `tests/test_repo_hygiene.py` asks for a
README Layout or handbook line for a file under `docs/`. Two rules read
every tracked `.md`: the host-absolute-path scan and the "<word> naming
families" count. They find nothing in this file. The hygiene tests gave
50 passed on the branch, and main's two patterns, applied to the file,
match nothing.

Section 7 and the `<release-commit>` placeholders in section 4 are for
the tag. The GitHub release body takes this text with `<release-commit>`
replaced by this commit's full sha and section 7 removed. The file keeps
both, because it cannot name the commit that adds it.

Closes #472

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

adopt-project.py plan writes null, true and other YAML keywords unquoted, so plan --spec-path null silently mounts the leg at the default spec

1 participant