Skip to content

adopt-project.py: check names, and execute refuses before the work directory is made, a plan whose source: or legs: is not a mapping or whose allow_empty_legs: or paths: is not a list of what plan writes, and asks the same first of a pins: that is not a list, so a number or a boolean in any of the four no longer ends both in a traceback and allow_empty_legs: code is no longer read as four legs (#386) - #435

Merged
brettheap merged 31 commits into
mainfrom
fix/adopt-plan-field-types
Oct 13, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Lane: openreposhape-2 (openRepoShape-2)
Claim: #386 (comment)
#356 landed as 965420b; this branch merged its final head 2fdfbed and main (cc8f2da). The PR targets main. Since #273 landed (aa4e1f5), pins: is a list of mappings that #273's reader judges, and this PR asks it only whether it is a list (below).

What

Plan read four keys of a plan file without asking their type, and, until #273, a fifth, pins:. The constructor iterated paths:, allowed_empty_legs iterated allow_empty_legs:, and names, source_commit and open_source called .get on legs: and source:. A plan is edited between plan and execute on purpose, and x or [] rescues only the falsy values. Each key is now read through a reader that takes only what plan writes, and check and execute name whatever that reader leaves out before they read anything else.

Why

On main cc8f2da, with local remotes and a stub gh that was never called (tools/s435-repro-main-cc8f2da.log):

The issue was first reproduced on #356's 5a5011d (tools/repro-base-5a5011d.log), where pins: openGlass also passed check, adopted, and wrote eight one-letter neutral-product pins. #273 fixed that row on main.

On this head, every one of those rows is a named finding from check, exit 1, and a refusal under the same code from execute, exit 2, with no leg and no branch created. For each of the four keys, and for a pins: that is not a list, that is before the work directory is made; an entry of a pins: list is refused where #273 refuses it. pins: 5 beside legs: 5 names both in one run. The controls still adopt (tools/s435-repro-head-42253cb.log).

Tests

All tests are in tests/test_adopt_execute_refusals.py, beside #356's follow_ups: and elected_on: tests and using its helpers: 59 tests.

Head 2795063 is 42253cb with main 47b1e06 merged in by GitHub's update-branch (#395, templates and tests only); git diff 42253cb 2795063 -- adopt-project.py tests/test_adopt_execute_refusals.py is empty. The lane's review ran these files at 2795063, plus tests/test_template_text_placeholders.py: 38 files, 3035 passed, 4 skipped, 0 failed (review435-27950638/tools/per-file-2795063.txt).

Runs at 42253cb:

  • 37 files, 2906 passed and 4 skipped, 0 failed (every tests/test_adopt_*.py, tests/test_shape_doctor.py, tests/test_scaffold_leg_path_collisions.py, tests/test_cli_reference.py and tests/test_repo_hygiene.py, with git filter-repo on PATH so no adoption test skips for want of it). The skips: test_adopt_plan.py:403 (SHAPE_MEDXEHR_CLONE unset, 1) and test_shape_doctor.py:8373 (the PowerShell line exists only on a Windows host, 3).
    • test_adopt_execute_refusals.py 150: main's 91 and this PR's 59. test_adopt_pins.py (adopt-project.py --pin <name> is accepted by plan, check and execute ("adoption verified"), but writes only the name into neutral_product_pins: and no contracts/<name>-pin.yaml, so the adopted root's own validate-pins.py (and its validate.yml) refuses with neutral-pin-missing from the first run #273's) 70.
    • test_shape_doctor.py 352, test_adopt_leg_paths.py 349, test_adopt_gitmodules_values.py 187, test_adopt_submodules.py 167, test_adopt_spelled_unambiguous.py 145, test_adopt_finding_spelling.py 128, test_adopt_root_shape_paths.py 120, test_adopt_question_and_submodule_spelling.py 106, test_scaffold_leg_path_collisions.py 104, test_adopt_leg_path_collisions.py 95.
    • test_adopt_late_failure.py 86, test_adopt_entry_paths.py 79, test_adopt_manifest_values.py 79, test_adopt_template_values.py 78, test_adopt_plan_escapes.py 59, test_adopt_plan.py 57, test_repo_hygiene.py 53, test_adopt_history_gitlinks.py 45, test_adopt_plan_leg_path_help.py 45, test_adopt_split_branch.py 37.
    • test_adopt_relative_submodule_url.py 32, test_adopt_clone_template.py 31, test_adopt_ignored_shape_files.py 30, test_adopt_empty_symlinks.py 28, test_adopt_root_manifest.py 27, test_adopt_e2e.py 26, test_adopt_mount_ignored.py 26, test_cli_reference.py 22, test_adopt_github_exits.py 19, test_adopt_git_reads.py 17.
    • test_adopt_verify.py 16, test_adopt_literal_names.py 14, test_adopt_spec_only.py 14, test_adopt_filter_repo_failure.py 11, test_adopt_tracking_branch.py 6.
  • scripts/render-cli-reference.py --check: ok.

Earlier runs, before #273 landed: the first 57 tests (d53602b), run against #356's code at 5a5011d, gave 44 failures, each for the issue's reason, and 13 controls passed (tools/new-tests-on-base-5a5011d-reasons.txt). 11 mutants of the fix at 5f177c9 were each killed (tools/mutants-5f177c9.log); three of them were of code #273 has since replaced.

Merged #356 and main (after #356 landed)

#356 (issues #293, #294) squash-landed on main as 965420b. This branch was stacked on it, so three merges bring it up to main, with no force-push and no rebase:

  1. 2fdfbed, adopt-project.py: check finds, and execute refuses before any leg exists, a plan follow_ups: that is not a list of text and an elected_on: that election_date does not read, with or without a reference:, so follow_ups: 5 is no longer a traceback and elected_on: someday is no longer refused under a flag neither command takes (#293, #294) #356's final head, as 1e6e9b8. It was clean, and its tree is the one git merge-tree 2fdfbed 3efcc7a gave (468826c).

  2. 965420b, adopt-project.py: check finds, and execute refuses before any leg exists, a plan follow_ups: that is not a list of text and an elected_on: that election_date does not read, with or without a reference:, so follow_ups: 5 is no longer a traceback and elected_on: someday is no longer refused under a flag neither command takes (#293, #294) #356's squash, as ed5d54a. git diff 2fdfbed 965420b over adopt-project.py: check finds, and execute refuses before any leg exists, a plan follow_ups: that is not a list of text and an elected_on: that election_date does not read, with or without a reference:, so follow_ups: 5 is no longer a traceback and elected_on: someday is no longer refused under a flag neither command takes (#293, #294) #356's three files (adopt-project.py, tests/test_adopt_execute_refusals.py, tests/test_adopt_manifest_values.py) is empty, so git checkout --ours for them is adopt-project.py: check finds, and execute refuses before any leg exists, a plan follow_ups: that is not a list of text and an elected_on: that election_date does not read, with or without a reference:, so follow_ups: 5 is no longer a traceback and elected_on: someday is no longer refused under a flag neither command takes (#293, #294) #356's final version plus this PR's change. The result differs from 965420b by exactly this PR's change: its patch-id is the same as git diff dbfad70 3efcc7a's.

  3. Main cc8f2da, as 42253cb, a real merge. Since 965420b, main changed adopt-project.py in 70d0d1b (adopt-project.py: check and execute spell a plan's hand-edited question: in plan-unresolved, its source: commit: in plan-stale and a submodule name in missing value for '<key>' with _spelled, so a line feed in any of them no longer forges a FINDING line and an ESC no longer reaches the terminal (#328) #384), 56713e2 (adopt-project.py: the tracking branch defaults to the source's default branch, topics are set before the pull request, and NEXT names scripts/bootstrap.py when the shape's Makefile went aside (#479, #480, #481) #493) and aa4e1f5 (adopt-project.py: plan --pin takes product@<40 hex> and execute writes its pin file as the scaffold does, so an adopted root passes validate-pins.py (#273); a --pin-source that cannot answer is pin-source-unreadable (#339) #508, lane 5's adopt-project.py --pin <name> is accepted by plan, check and execute ("adoption verified"), but writes only the name into neutral_product_pins: and no contracts/<name>-pin.yaml, so the adopted root's own validate-pins.py (and its validate.yml) refuses with neutral-pin-missing from the first run #273 and scaffold-project.py: pin_digest_from_source catches only Refusal, so a --pin-source that does not exist, is a file, or sits under a parent this user cannot enter ends --dry-run and the real run alike in a FileNotFoundError, NotADirectoryError or PermissionError traceback (exit 1), where a directory that is not a clone is the pin-source-unreadable refusal (exit 2) with its remediation #339). Two hunks conflicted, both in adopt-project.py and both the ones aa4e1f5's description names:

    • render_plan's pins: line takes main's _pin_lines, which writes the product through y and the commit and digest quoted. This PR's y(pin) change is superseded and gone.
    • Plan.pins takes main's _pin_products.

    The rest is the reconciliation that aa4e1f5 and lane 5's comment on this PR asked of whichever landed second. As this branch stood, its item check took only text, so every plan plan --pin now writes was plan-bad-pins in its own check. Now _plan_key_findings leaves pins: entries to _read_pins. A pins: that is not a list is still asked first, in _read_pins' own plan-pin-malformed finding, so plan-bad-pins is gone. PINS_REMEDIATION names the mapping, and PLAN_KEY_REFUSAL says "no leg repository was created", which conftest.refused_before_any_leg asserts of adopt-project.py --pin <name> is accepted by plan, check and execute ("adoption verified"), but writes only the name into neutral_product_pins: and no contracts/<name>-pin.yaml, so the adopted root's own validate-pins.py (and its validate.yml) refuses with neutral-pin-missing from the first run #273's not-a-list row, now refused here. The tests above changed with it.

git diff --stat cc8f2da 42253cb lists only adopt-project.py and tests/test_adopt_execute_refusals.py, and git merge-tree --write-tree cc8f2da 42253cb is rc 0. flake8 reports the same findings for both files as on main, with the same messages, and scripts/render-cli-reference.py --check is ok; no --help text changed.

Mutants of the reconciliation, each killed (tools/mutants-s435-42253cb.log, run over tests/test_adopt_execute_refusals.py and tests/test_adopt_pins.py):

mutant tests failed
R1: the gate judges pins: entries again and takes only text, as at 3efcc7a 8, among them #273's test_a_plan_with_a_whole_pin_checks_ok and the four plan --pin round trips
R2: the gate leaves pins: wholly to _read_pins, so it is not asked first 9
R3: a pins: that is no list named under a code of the gate's own 3, among them #273's plan-pin-malformed-not-a-list row
R4: the refusal no longer says "no leg repository was created" 3, among them #273's plan-pin-malformed-not-a-list row
R5: Plan.pins reads text items only, as at 3efcc7a 5

Composes with

git merge-tree --write-tree 42253cb <X> (tools/s435-merge-tree-42253cb.txt), against the shared store's main and the open pull requests' heads at 11:21Z:

Left out

Closes #386

🤖 Generated with Claude Code

brettheap and others added 19 commits October 11, 2026 22:38
…er a file can hold it, in `plan`, in `check` and in `execute` before any leg exists, so a byte that is not UTF-8 in `--elected-by`, `--reference` or a local `--source` directory no longer pushes both legs before `write_lf` raises (#255)

Since #247 the plan writer keeps a lone surrogate (a byte that is not
UTF-8, as Python decodes argv and paths) as `\udce9`, and the reader gives
it back. `plan --elected-by b'Jos\xe9'`, `--reference b'ref-\xe9'` and a
source under a directory `caf\xe9` with local remotes therefore passed
`plan` and `check`, and `execute` created and pushed both leg remotes
before `write_lf` raised `UnicodeEncodeError` (exit 1). Before #247 `plan`
raised and nothing was created. A hand-edited `\ud800` in `elected_by`,
`elected_on`, `reference`, `visibility`, a pin or a follow-up ended the
same way, and a NUL or a line break there went into the manifest.

`shape_materialize.unwritable_reason` asks the question, beside
`one_printable_line`: one line, no lone surrogate, no control character.
It is `one_printable_line` less `Cf`, and it takes the empty value: a
Persian name joins with U+200C, which a file holds and `execute` adopted
before #247 and since, and an empty `elected_by` is what `plan` records
when `git config user.name` names nobody.

- `plan` refuses `--elected-by` and `--reference` as
  `adopt-unwritable-value` and writes no plan.
- `check` reports `elected_by`, `elected_on`, `reference`, `visibility`,
  each pin and each follow-up as `plan-unwritable-value`, through
  `_entry_findings`, so `execute` refuses the same findings in `check`'s
  words through `_refuse_what_check_finds`, before `_create_leg_remotes`.
- `execute` refuses a local clone URL (the source's own path under
  `--local-remote-dir`) as `adopt-unwritable-value`, right after
  `_repository_urls`. `check` cannot know it will be written, and against
  GitHub it is an https URL, so `plan` and `check` still pass such a
  directory, as #247 made them.
- `Source.open` refuses a `local_path` that does not exist and is not text
  as `source-unresolvable`, where a NUL or a surrogate reached `print` or
  `subprocess` as a traceback.
- `check`'s WHAT WILL HAPPEN shows the plan's visibility, adopt branch,
  leg paths and follow-ups through `_shown`, so a surrogate there no
  longer raises on a strict stdout before the finding is printed.

tests/test_adopt_template_values.py covers each argv route, the local
source directory, each hand-edited value, `local_path`, the printed
values, and controls (`José`, U+200C, a `café` source directory) that
still adopt.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…value `execute` writes is refused before anything exists, and the clone-URL refusal's remediation moves the source and plans again, so no README line is shown reversed and following the remediation adopts instead of raising (#255)

Fix round 1 on PR #270, answering lane openRepoShape-2's review at
aed9190 (F1, F2, F3, and N1's test point).

F1. `unwritable_reason` was `one_printable_line` less all of `Cf`, so
U+202A..U+202E and U+2066..U+2069 passed `plan`, `check` and `execute`
and went raw into `project.yaml`, the README's `Elected by` line and the
split commit's message; a viewer showed the rest of that line reversed.
Issue #255's question, `one_printable_line`, refuses them. The question
is now `one_printable_line` less `Cf` other than those nine
(`BIDI_CONTROLS`, by code point), plus the empty value. The joiners
U+200C and U+200D, U+00AD, U+FEFF and the marks U+200E, U+200F and
U+061C stay writable: a Persian name needs U+200C, and a mark sets no
span's direction. `plan` refuses such a flag as
`adopt-unwritable-value`, `check` finds such a plan value as
`plan-unwritable-value`, `execute` refuses it before any leg exists, and
a local clone URL holding one is `adopt-unwritable-value`.

F2. The test file held a raw U+202E, which SonarCloud's text:S6389
flags, and other invisible literals a reader cannot see. Each is now an
escape; only `é` and `日本` stay literal. The two composite
assertions SonarCloud's python:S9073 flagged are four, each with a
message.

F3. The clone-URL refusal said to clone the source to a UTF-8 path and
pass it to `execute` with --source. Followed, that raised: the clone's
`origin` is the old path, and `_remote_repository` reads it as UTF-8.
The remediation now says to rename or move the source and run `plan
--source <its new path>`, `check` and `execute` again, and the test
follows it to `adoption verified`, for a byte that is not UTF-8 and for
a bidi override in the directory's name.

N1. The same-sentence test now asserts both exit codes and that no run
raised.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…er.name` as `--elected-by (not given, so `git config user.name`)` when it refuses it, so the refusal no longer names a flag the person never passed (#255)

Fix round 1 on PR #270, N2 of lane openRepoShape-2's review at aed9190.

With no --elected-by, `plan` records `git config user.name`. A TAB
there was refused as `--elected-by is 'Jo\tSmith'`, naming a flag the
operator never passed. It is now named as #181's `FLAG_DEFAULTS` names a
value `plan` filled in, and the remediation says to pass the flag or
correct `git config user.name`. A test runs `plan` under
`GIT_CONFIG_GLOBAL` holding a TAB name, and skips where a repository's
own `user.name` answers first.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the manifest back through the reader instead of matching its line, so it holds on main, which since #250 writes a YAML template's value as `double_quoted` spells it (#255)

Fix round 1 on PR #270. Built on the merge with main be2790e, the
U+200C control failed: `execute` adopted, but main b30595c (#250)
renders a value between a YAML template's double quotes through
`double_quoted`, so `project.yaml` holds `"Ali‌Reza"` with the
character spelled as an escape, and the test looked for it raw. The
reader gives back U+200C either way, so the control now asks the reader,
and also asks for the name in the shape's README, which is not YAML and
holds it as it stands on this branch and on main. GitHub's checks run
on the pull request's merge with main, so they would have failed here.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lvable` before anything runs, so `gh repo clone` is no longer handed it as its first argument, where gh's own parser read it as one of its options (#255)

SonarCloud's S8705 on #270 asked whether a value reaches a command as
an option. The lane's re-verify found one that does: `plan
--source=-u/x` is no path that exists and holds a `/`, so
`Source.open` took it for an `org/repo` name and ran `gh repo clone
-u/x <dir> -- --quiet`. gh read `-u` as `--upstream-remote-name` with
the value `/x`, and `--source=--upload-pack=x/y` as a flag it does not
know. A hand-edited `local_path:` or `repository:` reaches the same
call from `check` and `execute`. It cannot reach git as an option
(gh hands git only what follows its `--`), and main does the same.

The guard sits in the condition this PR already edits. No GitHub owner
name begins with `-`, so nothing gh could clone is refused, and an
existing path that begins with `-` is still taken as a path.

The tests put a stub `gh` first on PATH that records each call: `plan`
with either value, and `check` and `execute` on a plan whose
`local_path:` holds it, each exit 2 `source-unresolvable` with no
traceback and no leg, and the stub records nothing. Against 7e9cdc3's
code all four fail with the stub's record of `repo clone -u/x ...` and
`repo clone --upload-pack=x/y ...`. A control shows the stub records
the clone of `testorg/Thing`.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s `_entry_findings` composed with #255's, and this PR's pair renamed `unwritable_findings`/`unwritable_remediation`

#231 (6f45bf9) puts each entry's path first in `_entry_findings` and
returns `unsafe_remediation` ahead of the rest; this branch appends each
value `execute` writes into a file that no file can hold. The two hunks
are its docstring's order sentence and its return. The sentence keeps
main's order and appends this PR's clause; the return is
`(findings + submodule_findings + unwritable_findings,
  unsafe_remediation + remediation + unwritable_remediation)`, so the
paths still come first, as #196 needs.

This PR's local pair `value_findings, value_remediation` is renamed
`unwritable_findings, unwritable_remediation`. #261's branch (ca21b57)
binds `value_findings, value_remediation` in the same function, from
`_manifest_findings`: a resolution keeping both lines under one name
would silently drop one PR's findings. With the rename it cannot.

The rest of main (#254/#281, #238/#287, #203/#236, #204/#227,
#249/#250, #241, #215 and the earlier merges) merges with no conflict.
The resolution is the lane's re-verify one (`resolve-main.py`, 385
passed on 6f45bf9), rename aside.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s `_entry_findings` order sentence composed with #255's

#313 (71b58a1, #262 and #263) makes `check` find a kept root path where
the shape writes a directory or its `shape/` copy, through
`_root_shape_findings` called from `_leg_findings`, and makes
`shape_materialize` refuse a copy whose own path is taken. This branch
appends each value `execute` writes into a file that no file can hold
to `_entry_findings`. The one conflict is that function's docstring, the
sentence naming the order of its findings: it keeps #313's words (each
entry's `leg:`, the two mount paths and what the root keeps where the
shape writes, then the source's own submodules) and appends this PR's
clause, ", then each value `execute` writes into a file that no file can
hold (#255)". Nothing else in the file conflicts: the return still reads
`(findings + submodule_findings + unwritable_findings,
  unsafe_remediation + remediation + unwritable_remediation)`, and
`_leg_findings` carries #313's findings inside `findings`.

Main's c0ed615 (#308, family.py) comes with it and touches none of this
branch's files. Main has since moved on to c7c02fa (#315, family.py);
this merge stops at 71b58a1.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… exists, a plan `follow_ups:` that is not a list of text, so `follow_ups: 5` is no longer a `TypeError` traceback and `abc` is no longer written as three follow-ups (#293)

`plan` writes `follow_ups:` as a block list, and its three readers
(`_plan_values`, `_print_what_will_happen`, `_commit_the_split`) iterated
the value with no test that it was one. A hand-edited `follow_ups: 5`,
`true`, `false` or `1.5` ended `check` in `TypeError: ... is not
iterable`, exit 1, with no FINDING naming the key. On main `execute` died
the same way in `_commit_the_split`, after both legs were pushed; since
#270 it dies in `_plan_values`, before any leg, but still as a traceback.
`abc` went into the split commit's message as `- a`, `- b`, `- c`, and an
item `- true` or `- null` as `True` or `None`.

The plan is untrusted input (`cmd_execute`'s own comment), so the value
is asked before it is used:

- `_plan_follow_ups` is the one read for the three readers: the items of
  text of a `follow_ups:` list, and nothing for anything else.
- `_follow_ups_findings` is a `plan-bad-follow-ups` FINDING for a
  `follow_ups:` that is present and not a list, naming what it holds
  (`the number 5`, `the text 'abc'`, `a mapping of 1 key(s)`), and one for
  each item that is not text. An empty list and a bare key stay valid.
- They join `_entry_findings` through `_plan_field_findings`, last, so
  `check` reports them and `execute` refuses them through
  `_refuse_what_check_finds`, before `_create_leg_remotes`, under the
  finding's own code, with a remediation that says how `plan` writes the
  list.

Tests in tests/test_adopt_execute_refusals.py: `check` (a finding, exit
1, no traceback) and `execute` (the refusal, no leg, no split branch) for
nine edits, three controls, and one run through `main` with
`git filter-repo` that is refused with exit 2 and then adopts from the
corrected plan.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ted_on:` whether or not the plan has a `reference:`, and `execute` refuses what it finds before any leg exists, so `elected_on: someday` is no longer refused after both legs are created, under `--elected-on`, a flag neither command takes (#294)

`check` never read `elected_on:`. `execute` read it in `_template_values`
only for a plan with no `reference:`, where `default_reference` asks
`election_date`. That runs after `_create_leg_remotes`, so a hand-edited
`elected_on: someday` passed `check` (`plan ok`). `execute` then created
both leg repositories and only afterwards refused
`election-date-malformed`. The refusal named `--elected-on` and said to
re-run with it, but only `plan` takes that flag. A second `execute` met
`leg-remote-exists`. With `reference:` kept, the date was never read, and
`execute` wrote `someday` into the manifest.

`_elected_on_findings` asks `election_date` about `str` of what the plan
holds. That is the value `_template_values` takes, and the rule `plan`
asks of `--elected-on`. It asks whether or not the plan has a
`reference:`. A date it refuses is a FINDING `election-date-malformed`
that names the plan's `elected_on:` and no flag, with a remediation that
says to edit the key in the plan file. It joins `_plan_field_findings`
ahead of the follow-ups, in the order the plan file holds the two keys.
`execute` refuses it through `_refuse_what_check_finds`, before any leg
exists.

The rule stays `election_date`'s, unchanged. It reads `2026-W41-6` and
`20261011` from Python 3.11, and the project's validator refuses both.
That is #261's to rule on (PR #314 holds `election_date` to the
validator's `DATE_RE`). Asking the same function keeps `plan`, `check`
and `execute` in agreement now, and asks the tighter rule with no edit
once it lands. Only the words are this caller's: `election_date`'s
refusal names `--elected-on`, which is right for `plan` and wrong here.
Only that refusal is a finding about the date. Anything else
`election_date` raises is raised as it is, so a date it never judged is
not reported as malformed. An example is #314's
`manifest-validator-unusable`, when the validator it consults is missing.

Tests in tests/test_adopt_execute_refusals.py:
- `check` and `execute` for five dates (four with no `reference:`, one
  with it kept), and four controls;
- `check`'s verdict equals `election_date`'s on the running interpreter,
  for `2026-W41-6`, `20261011`, a date and a word;
- a stricter `election_date`, stood in by monkeypatch, refuses
  `2026-W41-6` before any leg;
- another refusal from `election_date` is raised as it is;
- both findings in one plan;
- one run through `main` that is refused with exit 2 and no leg, then
  adopts from a corrected plan with no `reference:` and records the
  reference its date chooses.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#270 (fix/adopt-template-values-utf8) landed on main as the squash
f795cf7. Its final head was 3cee853, which had merged main e43fa95. This
branch was stacked on #270 and carries an older copy of #270's commits,
so a plain merge of main would conflict in adopt-project.py, where the
squash and those commits touch the same lines. Merging #270's final head
comes first so the next merge, of origin/main, can take this branch's
side of the files #270 changed.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ter #270 landed as the squash f795cf7

#270 landed on main as the squash f795cf7, and this branch was stacked
on it. The previous merge, ead7716, brought #270's final head 3cee853
into this branch. This merge brings in main.

adopt-project.py conflicted, and this merge takes this branch's own
version of the whole file. That is right because the squash's content of
adopt-project.py (and of tests/test_adopt_template_values.py) is
3cee853's, which the previous merge already brought in: `git diff 3cee853
f795cf7` over those files is empty. Of the main commits not in 3cee853,
only f795cf7 touches adopt-project.py, tests/test_adopt_template_values.py
or scripts/shape_materialize.py. So main adds nothing to those files
beyond what this branch already carries, and no other file conflicted.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…g is made, a plan whose `source:` or `legs:` is not a mapping or whose `pins:`, `allow_empty_legs:` or `paths:` is not a list of what `plan` writes, so a scalar no longer ends both in a traceback and `pins: openGlass` no longer adopts with eight one-letter pins (#386)

`Plan` read five keys of a plan file without asking their type: the
constructor iterated `paths:` and `pins:`, `allowed_empty_legs` iterated
`allow_empty_legs:`, `names` called `.get` on `legs:`, and `source_commit`
and `open_source` on `source:`. The file is edited between `plan` and
`execute` on purpose, and `x or []` rescues only the falsy values. So a
number or a boolean ended `check` and `execute` in a `TypeError` or an
`AttributeError`, exit 1, naming Python's error and not the key. Text, which
is iterable, was read one character at a time: `pins: openGlass` passed
`check`, adopted, and wrote eight pins, `G a e l n o p s`, that the root's
own `validate-pins.py` refused. An item of `pins:` that is not text was
written as Python spells it (`True`, `{'a': 1}`), and `allow_empty_legs:
code` consented for four legs, `c`, `o`, `d` and `e`.

Each key is now read through one reader that takes only what `plan` writes
(`_plan_list`, `Plan.source`, `Plan.names`), and `_plan_key_findings` names
what the reader left out in #293's words: a code per key (`plan-bad-source`,
`plan-bad-legs`, `plan-bad-pins`, `plan-bad-allow-empty-legs`,
`plan-bad-paths`), `_read_as` for what the file holds, one finding per item
of `pins:` that is not text and of `allow_empty_legs:` that is not `spec` or
`code`, and each key's remediation said once. An absent or bare key is not a
finding.

They are asked FIRST, right after `Plan.load`, and not in `_entry_findings`
beside #293's and #294's. These keys are what the rest of the run is built
from: the source is opened from `source:`, the names `execute` refuses on
first come from `legs:`, and the constructor builds the entries and pins.
Every later finding would be a statement about what the reader made of them
(a `paths:` that is not a list reads as every source path uncovered). So
`check` prints them, with #293's and #294's beside them so that every bad
key of the plan is named in one run, and exits 1; `execute` refuses the same
list in `_refuse_what_check_finds`' shape (the first finding's code, each
finding as `check` prints it, capped), exit 2, before `_work_root` makes the
work directory and before the source is opened or a leg exists.

`legs:` is still kept by the constructor as it stands: the doctor's
placement plan carries it as a list, and `tests/test_shape_doctor.py` hands
that to `Plan` and `_leg_findings`. Only `check` and `execute`, reading a
plan file, name it.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ter #245 and #371 landed

Main took #245 (161c6c9) and #371 (a983e6a) since this branch last merged
it, and both changed `_entry_findings` where this PR does, so GitHub could
not merge the branch. `adopt-project.py` was the only file that conflicted,
in 2 hunks, both in `_entry_findings`; every other file came from main as
it is.

- The docstring: main's text, with its ending "and last each relative url
  of a kept submodule that names another repository from its leg (#190)"
  changed to "then each relative url ... (#190), and last each plan value
  `execute` reads late and `_plan_field_findings` asks about (#293, #294)",
  so this PR's findings still come last.
- The return: main's three-tuple, with `notes` kept. The findings are
  `findings + split + unwritable_findings + moved + field_findings` and the
  remediations are the same order, ending in `field_remediation`.

`cmd_check` and `_refuse_what_check_finds` already unpack three values on
main, and both keep their `local_remote_dir=None` defaults, so this PR's
tests that call `_refuse_what_check_finds(plan, source)` need no edit.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…merged main a983e6a

#386's branch is stacked on #356's fix/adopt-check-hand-edited-plan and
was built at 5a5011d. #356 has since merged main a983e6a, which brings
#245 (a relative `.gitmodules` url resolved from the source's remote and
the leg's) and #371 (`execute` verifies before it exits on a refused
`gh pr create`) into adopt-project.py, with #351, #350, #345 and #372 in
other files.

The merge is clean. `cmd_check` and `cmd_execute` still ask
`_plan_type_findings` and `_refuse_a_mistyped_plan` first, right after
`Plan.load` and before `_work_root`, so nothing #245 or #371 added runs on
a mistyped `source:`, `legs:`, `pins:`, `allow_empty_legs:` or `paths:`.
#245's new reader, `_kept_relative_urls`, takes `plan.entries`, which
`_plan_list` already reads only from a list, and every reader of
`plan.legs` asks its type. #245 gives `_refuse_what_check_finds` a
`local_remote_dir` and rewrites its first line; `_refuse_a_mistyped_plan`
shares only its shape and `REFUSED_FINDINGS_SHOWN`, not its text, so
neither needs a change.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ted_on:` is "no date", so an empty text, `false`, `0`, `[]` and `{}` stay findings in `check` and refusals before any leg, and `if not elected_on:` no longer passes the suite (#294)

Review of 5a5011d (F1): `_elected_on_findings` returns nothing for `elected_on is None` only, and `""` and the other falsy values reach `election_date`, so `check` exits 1 with `FINDING election-date-malformed` and `execute` exits 2 with no leg. No test pinned it: changing `is None` to `not elected_on` passed all 81 tests, and under that change `check` says `plan ok` and `execute` creates both legs before it refuses.

`empty-text` is a row of BAD_DATES, so the `check` test and the `execute` test both take it. A `check` test of its own takes `false`, `0`, `[]` and `{}`, because BAD_DATES' assertion says "the text" and these are not text. Under the mutant the six new cases fail, `check` exiting 0 and `execute` having made the leg remotes; at this commit they pass. Tests only: adopt-project.py is unchanged.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…` writes ("the form `execute` writes into the project's `project.yaml`"), and `_plan_field_findings` says the order `plan` writes the two keys, so neither overstates what the code does (#294)

Review of 5a5011d (N1, N2), both wording in this PR's own text.

N1: the docstring said the findings come "in the order the plan file holds the two keys". The order is fixed, date then follow-ups, and it is the order `plan` writes them (`render_plan` emits `elected_on` before `follow_ups`). It now says so, and so does the both-findings test's docstring.

N2: the finding called the date "the one form `execute` reads". That does not hold on Python 3.11 and later until #314 lands: `20261011` and `2026-W41-6` pass `election_date`, and `execute` writes them into `project.yaml`. It now says "the form `execute` writes into the project's `project.yaml`", which is true on every Python. The `check` test pins the new words and that the old ones are gone: at the reviewed adopt-project.py its six rows fail, and they pass here. The two `startswith` assertions match only up to "YYYY-MM-DD" and did not change.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…writes every other value, so `plan --pin 2026` no longer writes a plan its own `check` refuses as `plan-bad-pins`, and `--pin 007` is no longer declared as the pin `7` (#386)

#386's item check names an item of `pins:` that the reader does not give
back as text. `render_plan` wrote the line as `"pins: [" + ", ".join(pins)
+ "]"`, the one value in the plan not written through `y`, and
`PIN_NAME_RE` takes names that are digits: `repo_shape.parse_yaml` reads a
bare `2026` as the number 2026, `007` as the number 7 and `1.5` as a
float. So at 4d8a149, `plan --pin 2026` (or `007`, or `1.5`) exited 0 and
the plan it wrote failed `check` with
`FINDING plan-bad-pins: item 1 of pins: is the number 2026, not text`.
Before #386 `2026` was declared as `str(2026)`, and `007` silently as `7`.

`y` double-quotes a name the reader would take for a number, so each comes
back as the text `--pin` was given, which is the invariant the writer's own
comment states: everything it emits is read back by `check`. A bare YAML
keyword (`--pin true`, `--pin null`) is still written bare, as `y` writes
one everywhere (#181); `check` now names it where the pin `True` used to be
declared, or the pin dropped.

The new `test_a_pin_plan_writes_reads_back_as_the_name_it_was_given`
(`2026`, `007`, `1.5`, and `openGlass` as the control) runs `plan --pin`,
answers the plan's questions and asks `check` and `Plan.load`: at 4d8a149
the three numeric names fail (`check` exit 1, `plan-bad-pins`); on #356's
0aff5ae `007` fails (`['7'] == ['007']`); all four pass here.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s fix round

#386's branch is stacked on #356's fix/adopt-check-hand-edited-plan, last
merged at 0aff5ae. #356's fix round added f8e6de1 (tests only: the
empty-text BAD_DATES row and a `check` test for an `elected_on:` of
false, 0, [] and {}) and dbfad70 (the `elected_on:` finding says "the form
`execute` writes into the project's `project.yaml`", and
`_plan_field_findings`' docstring says the order `plan` writes the keys).

The merge is clean and adds exactly 0aff5ae..dbfad70. No #386 test asserts
the `elected_on:` finding's text: `test_every_key_that_is_wrong_is_named_in_one_run`
puts #293's `follow_ups:` beside #386's keys and asserts its code and its
remediation's first words, which dbfad70 does not change.

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

sourcery-ai Bot commented Oct 12, 2026

Copy link
Copy Markdown

Sorry @brettheap, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 19 hours and 10 minutes by commenting @sourcery-ai review. Upgrade to get a review now.

@sourcery-ai

sourcery-ai Bot commented Oct 12, 2026

Copy link
Copy Markdown

Reviewer's Guide

The PR hardens plan parsing by validating the five structurally significant keys through type-aware readers, reporting all malformed values in check, and making execute refuse them before any side effects; it also quotes generated pin names and adds extensive regression coverage.

Sequence diagram for early plan validation in check and execute

sequenceDiagram
    participant CLI
    participant Plan
    participant Validation as _plan_type_findings
    participant WorkRoot as _work_root
    participant Source as open_source

    CLI->>Plan: load(plan_path)
    CLI->>Validation: _plan_type_findings(plan)
    alt malformed plan keys or items
        alt check
            Validation-->>CLI: findings and remediation
            CLI-->>CLI: exit 1
        else execute
            Validation-->>CLI: findings and remediation
            CLI-->>CLI: Refusal(first finding code)
            CLI-->>CLI: exit 2
        end
    else valid plan structure
        CLI->>WorkRoot: _work_root(args)
        CLI->>Plan: open_source(override, work_root)
        Source-->>CLI: source
    end
Loading

Flow diagram for type-aware plan readers and findings

flowchart TD
    Load["Plan.load"] --> Validate["_plan_type_findings"]
    Validate --> Source["source: mapping"]
    Validate --> Legs["legs: mapping"]
    Validate --> Lists["paths:, pins:, allow_empty_legs: lists"]
    Lists --> PinItems["pins: text items"]
    Lists --> EmptyLegItems["allow_empty_legs: spec or code"]
    Source --> Findings["Named plan-bad findings"]
    Legs --> Findings
    Lists --> Findings
    Findings --> Check["check: print findings, exit 1"]
    Findings --> Execute["execute: refuse before side effects, exit 2"]
    Validate -->|no malformed values| Continue["Continue normal source and work-root processing"]
Loading

Flow diagram for quoting generated pin names

flowchart LR
    Pin["--pin name"] --> Render["render_plan"]
    Render --> Y["y(pin)"]
    Y --> YAML["pins: [quoted YAML values]"]
    YAML --> Reader["Plan.pins"]
    Reader --> Check["check accepts numeric-looking names as text"]
Loading

File-Level Changes

Change Details Files
Add type-safe readers and early validation for plan keys before adoption work begins.
  • Centralize list handling for paths:, pins:, and allow_empty_legs:.
  • Treat source: and legs: as mappings only when they actually contain mappings, while preserving placement-plan list compatibility.
  • Filter invalid pin and empty-leg items instead of coercing or iterating malformed values.
  • Add per-key findings, remediation text, and stable refusal codes for malformed plan data.
  • Run check validation immediately after loading and make execute refuse before creating work directories, opening sources, prompting, or creating legs.
adopt-project.py
Ensure generated plans round-trip pin names without YAML type coercion.
  • Quote every rendered pin through the existing YAML value writer.
  • Preserve numeric-looking pin names such as 2026, 007, and 1.5 when read back.
adopt-project.py
Add comprehensive regression coverage for malformed plans, early refusal behavior, and plan round trips.
  • Cover malformed scalar, mapping, list, and item values for all newly validated keys.
  • Verify multiple findings, remediation de-duplication, refusal ordering, no side effects, and behavior without --yes.
  • Retain controls for absent, bare, empty, and valid keys, including shape-doctor placement plans.
  • Test generated pin plans and an end-to-end refusal/corrected adoption flow.
tests/test_adopt_execute_refusals.py

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@brettheap

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 12, 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-12T17:47:10.545106Z 3efcc7a 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: 3efcc7a72f

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

…landed, with one guard in _manifest_values and #314's 5 elected_on: rows retargeted to election-date-malformed

#314 (scaffold-project.py and adopt-project.py ask validate-manifest.py
about the manifest's values) landed on main as 3ef059c. Its adopt-project.py
is a5438a5's merged with 92e29d9, the composition pr-body-v3.md planned.

adopt-project.py conflicted in 2 hunks of _entry_findings, both kept:
- the docstring lists #261's validator findings, then #190's relative urls,
  and last this PR's plan values (#293, #294);
- the return is findings + split + unwritable_findings + value_findings
  + moved + field_findings, the remediations in that order, and notes.
Main's auto-merged paragraph said the relative urls of #190 come "last";
it now says the plan values come after them.

The guard: _manifest_values returns None when _elected_on_findings(plan)
has a finding, right after its _check_names refusal. Without it, a plan
with no reference: had check exit 2 on election_date's own refusal, raised
from _template_table through default_reference and naming --elected-on,
before this PR's finding was listed; with reference: kept, the date was two
findings, the validator's and this PR's. In plan the guard is a no-op:
_checked_plan_inputs asks election_date of --elected-on first.

#314's 5 rows for an elected_on: that election_date now refuses get this
PR's one election-date-malformed finding, so they are retargeted to it in
tests/test_adopt_manifest_values.py (said_of gives each row's finding,
REFUSED prefix and remediation), and none is dropped:
- elected-on-iso-week in test_check_finds_a_value_the_validator_refuses,
  test_execute_refuses_it_before_any_leg_exists and
  test_check_finds_it_from_a_tree_with_no_git_as_in_a_checkout;
- elected-on-basic-format in
  test_execute_refuses_a_text_value_the_reader_misreads_before_any_leg and
  test_a_text_value_the_reader_misreads_is_one_finding_on_this_rule_alone,
  where _manifest_findings is now ([], []) and _plan_field_findings holds
  the one finding.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap and others added 2 commits October 12, 2026 15:11
…landed: `_commit_the_split` keeps #445's reported steps and reads `follow_ups:` through `_plan_follow_ups`

Main moved from dd8d0ea to 580d7df: decff87 (#267), dba5fb0 (#403),
245d775 (#446), 75052cb (#445), ec8e09a (#406) and 580d7df (#416).
Only #445 touches adopt-project.py, and it conflicted in one hunk of
`_commit_the_split`.

#445 wraps that function's body in a `try` that names each step
(cloning the source for the split, mounting the legs, committing the
split, pushing the split branch), so a failure once both legs are pushed
flushes stdout, prints `REFUSED <step>.`, what failed and one notice of
what the run left. This branch reads `follow_ups:` there through
`_plan_follow_ups`, the one read of it (#293). The resolution keeps
#445's `try`, steps and reports whole, and carries this branch's read
into its "committing the split" step: git had kept main's
`[str(f) for f in plan.get("follow_ups", [])]` inside the `try`.

The two contracts meet without overlap. `plan-bad-follow-ups` and
`election-date-malformed` are findings of `_entry_findings`, which
`execute` refuses through `_refuse_what_check_finds`. That runs before
#445's `_refuse_an_occupied_work_dir`, `_refuse_a_split_branch_left_behind`
and `_create_leg_remotes`, so these are refusals before any leg exists:
exit 2, `REFUSED <code>:` and the remediation that says no leg
repository was created and nothing was pushed, as #445's own refusals
before the legs say, and no notice. #445's report covers the failures
once a leg exists, which these values no longer reach. The guard in
`_manifest_values` is unchanged; #445 does not touch it.

#445's comment over `_template_values` in `cmd_execute` named "an
`elected_on` that is not a date" as a refusal the table can raise. With
#294's finding, `_refuse_what_check_finds` refuses such a date first, so
the comment now says the date no longer reaches the table. #445's test
of a table refusal stubs `_template_values` and is unchanged.

tests/test_adopt_execute_refusals.py: the two end-to-end runs through
`main` (#293, #294) now also assert that the refusal says no leg was
created and that #445's notice ("NOTHING has been rolled back") does not
follow it.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… before its date guard, so `check` and `execute` refuse a plan with a bad `elected_on:` and a bad organisation or id for the same first problem (#294)

The guard this PR added to `_manifest_values` (return None when
`_elected_on_findings` has a finding, so #314's validator is not asked
about a date this PR already reports) returned before the function's
`checked_value("org", ...)` and before `_template_table` read
`plan.project_id`. Those two are where `check` meets a bad organisation
and a bad id; `execute` meets the organisation earlier, in
`_repository_urls`, and the id here, through `_refuse_what_check_finds`.
So with `elected_on: someday` beside them:

- `org: "-evil"`: `check` exited 1 listing only the date, and `execute`
  exited 2 `REFUSED unsafe-value`;
- `id: "North Wind"`: `check` exited 2 `REFUSED plan-bad-id` (from the
  topic line it prints later), and `execute` exited 2 `REFUSED
  election-date-malformed`.

On main 580d7df both commands name `unsafe-value`, and `plan-bad-id`,
for those plans. The organisation is now refused, and the id read,
before the guard, so both commands name the same first problem again,
as on main, and a date-only plan is still one finding in the plan key's
own words, not `plan-manifest-invalid` beside it. The docstrings of
`_manifest_values` and `_manifest_findings` now also say they return
nothing for such a date.

tests/test_adopt_execute_refusals.py:
`test_a_bad_date_does_not_change_what_check_and_execute_refuse_first`,
for the organisation and the id, with `reference:` kept and removed:
`check` exits 2 on the code `execute` refuses with, and no leg is
created. All 4 fail on 88c430c's code and pass here.

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

Copy link
Copy Markdown
Contributor Author

Lane: openreposhape-5 (openRepoShape-5)

T (#508, for #273 and #339) landed at 04:37Z as aa4e1f5. Against main, this PR now has 6 conflict hunks in adopt-project.py (4 before T) and 6 in tests/test_adopt_execute_refusals.py (T added none of those).

Brett Heap and others added 3 commits October 13, 2026 11:05
#435 was stacked on #356, which squash-landed on main as 965420b. Its
final head 2fdfbed is a server-side merge of main into #356's 5982bbe, so
it brings #356's last fix (the `org:` refusal and `id:` read ahead of the
election-date guard), #445's REFUSED contract and #401's help text. This
is step 1 of bringing a stacked branch onto main after its parent's
squash: `git merge-tree 2fdfbed 3efcc7a` was rc 0, and this merge is
clean.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…this branch's side of #356's three files (#386)

#356 landed on main as 965420b. `git diff 2fdfbed 965420b` over its
three files, `adopt-project.py`, `tests/test_adopt_execute_refusals.py`
and `tests/test_adopt_manifest_values.py`, is empty, so main holds those
files exactly as #356's final head 2fdfbed left them, and this branch
already merged 2fdfbed. The conflicts in `adopt-project.py` and the
refusal tests are #356's own hunks meeting themselves, and
`git checkout --ours` for the three files is #356's final version plus
#435's change and nothing else.

Proof: `git diff 965420b` of the result over the three files has the same
patch-id (76b3015) as #435's own change, `git diff dbfad70 3efcc7a`, and
no other file differs from 965420b.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… list of mappings, so #386's gate asks it only whether it is a list, in #273's own finding (#386)

Main since #356's squash 965420b changed `adopt-project.py` three times:
70d0d1b (#384, `_spelled` in three findings), 56713e2 (#493, the
tracking branch from the source's default branch) and aa4e1f5 (#508,
lane 5's #273 and #339). aa4e1f5 makes `plan --pin` take
`product@<40 hex>` and write each pin as a MAPPING of `product:`,
`repository:`, `commit:`, `tree_sha256:` and `digest_source:`, and its
`_read_pins` judges each `pins:` entry beside the entries:
`plan-pin-unrecorded` for a name alone, `plan-pin-malformed` for anything
else `plan` never writes there, and for a `pins:` that is no list.

Two hunks conflicted, both in `adopt-project.py`, both the ones
aa4e1f5's message names: `render_plan`'s `pins:` line takes main's
`_pin_lines`, which writes the product through `y` and the commit and
digest quoted, so this branch's `y(pin)` is superseded; and `Plan.pins`
takes main's `_pin_products`, which reads a list only.

The rest is the reconciliation aa4e1f5 and lane 5's comment on #435
asked of whichever landed second. As this branch stood, its item check
took only text, so every plan `plan --pin` now writes was `plan-bad-pins`
in its own `check`:
- `_plan_key_findings` no longer judges `pins:` entries: they are
  `_read_pins`' alone, in its words and codes.
- A `pins:` that is no list is still asked first, beside the other four
  keys and before the work directory, because the naming policy reads the
  root's products from it. Its finding is `_read_pins`' own
  `plan-pin-malformed`, so it is one finding under one code, and
  `plan-bad-pins` is gone.
- `PINS_REMEDIATION` names the mapping `plan` writes.
- `PLAN_KEY_REFUSAL` begins "no leg repository was created and nothing was
  pushed", the words every refusal before a leg says (#445; conftest's
  `refused_before_any_leg` asserts them, and #273's not-a-list row now
  meets this refusal).

`tests/test_adopt_execute_refusals.py` says the same: the `pins:` key rows
expect `plan-pin-malformed` and gain a single mapping with no list around
it; the entry rows move to a test that the gate is silent and
`_read_pins` names them; the controls and the corrected end-to-end plan
are whole pins; and the round trip runs `plan --pin <name>@<commit>
--pin-source` for 2026, 007, 1.5 and openGlass, and holds `check` to
`plan ok`.

`git diff --stat cc8f2da` of the result lists only `adopt-project.py`
and `tests/test_adopt_execute_refusals.py`. Before this commit:
`tests/test_adopt_execute_refusals.py` 150 passed and
`tests/test_adopt_pins.py` 70 passed.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@brettheap brettheap changed the title adopt-project.py: check names, and execute refuses before anything is made, a plan whose source: or legs: is not a mapping or whose pins:, allow_empty_legs: or paths: is not a list of what plan writes, so a scalar no longer ends both in a traceback and pins: openGlass no longer adopts with eight one-letter pins (#386) adopt-project.py: check names, and execute refuses before the work directory is made, a plan whose source: or legs: is not a mapping or whose allow_empty_legs: or paths: is not a list of what plan writes, and asks the same first of a pins: that is not a list, so a number or a boolean in any of the four no longer ends both in a traceback and allow_empty_legs: code is no longer read as four legs (#386) Oct 13, 2026
@sonarqubecloud

Copy link
Copy Markdown

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-2, 2026-10-13T12:24Z)

This lane reviewed its own PR at 2795063, read-only in its own detached worktrees, and authored nothing.

MERGEABLE WITH NITS, once the three-OS CI is green.

The gate does what #386 asks. In 56 hand-edited plans (nested lists, a mapping for a list and the reverse, empty containers, null, a pin missing commit:, CRLF, and today's plan output edited), there was no traceback. Each key that is not what plan writes was named by check (exit 1) and refused by execute (exit 2) under the same code, before the work directory, with no leg made and no late notice. Main 47b1e06 gave tracebacks in 6 of those plans. plan --pin for 2026, 007, 1.5 and openGlass passes check and adopts end to end.

Nits:

Not this PR's, and the same on main: - 007 is written bare into neutral_product_pins: (adopt-project.py:7015), so the adopted root's validate-pins.py refuses entry 7.

Tests at 2795063: 38 files, 3035 passed, 4 skipped, 0 failed.

Merge results: rc 0 with main and with 14 open PR heads. #504 conflicts only where it conflicts with main alone.

The report is handoffs/xFactory/attachments/lane-openRepoShape-2/review435-27950638/final-report.md in opensoft/brett-wip.

@brettheap
brettheap merged commit f391e58 into main Oct 13, 2026
5 checks passed
@brettheap
brettheap deleted the fix/adopt-plan-field-types branch October 13, 2026 13:58
brettheap pushed a commit that referenced this pull request Oct 13, 2026
…471's PR carries main's #509 and #435 and its footer reads right (#471)

One conflict, docs/handbook.html's footer, which every PR in the doc train
rewrites. Resolved by the doc-train rule: main's text is kept whole, including
#509's clause "a taken clone path refused up front, added (#498)", and this
PR's own clause, "the line-ending block an adoption appends to a root
.gitattributes the source keeps, and its verification as the source's bytes
plus that block, added (#471)", follows it in the same "; ... added (#NNN)"
form before " &mdash; a page cannot name the commit". Only the touched lines
were re-wrapped, to the footer's 100-column width. The footer's
`README.md blob` moves from c61ac37 (this
branch before the merge; main's own was 43df8f4)
to e6c41c9, `git hash-object README.md` of the
merged README.md. README.md stays at 1422 lines and AGENTS.md at 490.

Every other file merged without a conflict, including adopt-project.py, which
both sides changed (main's #435 shape checks on the plan, this PR's block).

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 13, 2026
…itory with conftest's `rmtree`, which clears the read-only bit git gives its objects, so the Windows job, which refuses to unlink them, no longer ends 12 adoption-pin tests in an error at setup (#471)

#471 folds in #497's `git-filter-repo` install, so the adoption suites ran on
a Windows runner for the first time, and the run was green but for 12 errors,
all at the module fixture `adopted` of tests/test_adopt_pins.py. The fixture
copies a repository with `shutil.copytree` to give the scaffold a pin source,
and deletes the copy with `shutil.rmtree` before `check` so the digest is
proved to have been read from it. Git writes its loose objects and packs
READ-ONLY, and Windows refuses to unlink a read-only file, so the delete
raised `PermissionError: [WinError 5]` there and succeeded everywhere else.

`tests/conftest.py`'s `rmtree` clears the read-only bit and retries the one
call that failed, and is what every other test that deletes a tree holding a
`.git` already uses. The fixture now calls it. Test-only: no tool changes.

The other `shutil.rmtree` calls under tests/ were each read and none needs
the same: test_cli_reference.py:391 and test_setup_sh.py:496 pass
`ignore_errors=True` over a scratch directory of generated text and over a
checkout the Windows job never runs, test_family.py:1580 removes a
submodule's working directory, which holds a `.git` FILE and no objects, and
test_update_shape_landing.py:1284 removes a plain directory the test made.

Checked against main's f391e58 (#435), which asks the keys of a plan file
their types before anything is read: nothing this PR adds reads `source:`,
`legs:`, `paths:`, `allow_empty_legs:` or `pins:` of a plan, and its one
plan-writing change, the `.gitattributes` follow-up, is a string in the same
`follow_ups:` list, which the new checks accept (`plan ok`). A plan holding
`source: 5`, `legs: []`, `paths: {}` and `pins: true` is named by key by
`check` (exit 1) and refused by `execute` (exit 2) before a work directory
is made. Nothing changed for that.

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 13, 2026
…e: docs/handbook.html's footer keeps main's text and adds this PR's clause after #498's, and names the merged README.md's blob (#499)

Five commits of main since this branch's base 47b1e06 merged, among them
#509 (cdcd2f0, #498), which rewrote the handbook footer, and #435
(f391e58, #386), which gates a plan's `source:`, `legs:`,
`allow_empty_legs:`, `paths:` and `pins:` by type at the top of
`adopt-project.py`'s `check` and `execute`. AGENTS.md, README.md and
adopt-project.py merged without a conflict.

docs/handbook.html conflicted in its footer only. The resolution keeps
main's text, "a taken clone path refused up front, added (#498)" included,
adds this PR's clause after it in the same form, "an adoption&rsquo;s
<code>--project</code> as the source&rsquo;s own name, added (#499)",
and re-wraps only the two lines that clause touches, to the footer's
100 columns, now three. The PR's own body edit at "It KEEPS ITS NAME"
merged as it was.

`README.md blob` moves from main's 43df8f4
and this branch's d19633d to
045250e, `git hash-object README.md` of
the merged README.md, which `tests/test_repo_hygiene.py` checks. README.md
stays at 1422 lines and AGENTS.md at 490.

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 13, 2026
…ute` puts it into the repository's URL, so `org: 2026`, `org: true`, `org: ~` or a deleted `org:` line is `plan-not-the-source` instead of `plan ok` and a `git ls-remote` of `https://github.com/2026/Atlas.git`, and a source whose `origin` is a path is told to rename the repository at that path, not to run `gh repo rename` (#499)

Lane openRepoShape-3's review of 9f3b3df, SHOULD-1 and NIT-3, folded.
NIT-1 is left as it was; why is below.

SHOULD-1. `_not_the_source` compared the plan's `org:` only when it was
text and returned None otherwise, while `_repository_urls` reads it
through `checked_value`, which converts with `str()`. A plan edited to
`org: 2026`, `org: true` or `org: ~`, or with its `org:` line deleted,
checked `plan ok`, and `execute` ran `git ls-remote` on
`https://github.com/2026/Atlas.git`, `.../True/...` or `.../None/...`, so
"a hand edit is caught too" was false for exactly these. `_plan_org` is
now the one read of `org:` for both, `str()` of what the file holds, and
the repository `check` and `execute` compare with the source is the one
`execute` acts on. Each of the four is `FINDING plan-not-the-source` from
`check`, exit 1, and `REFUSED plan-not-the-source` from `execute`, exit 2,
before any `git ls-remote`, push or `gh` command. Where `org:` is not
text, the detail says what the file holds beside how `execute` spells
it: "the plan's `org:` (the number 2026, which `execute` spells 2026)".
#435 asks the type of five other keys and not `org:`; nothing is added
to its checker here.

NIT-3. For a source whose `origin` is a path, `adopt-not-the-source`
said the repository read from it "names no repository on GitHub by
itself" and then, where the policy refused that name, to rename it "on
GitHub (`gh repo rename`)"; `_check_names` said the same of `--project`
given as that name. `_rename_first` stays the one text of rename-first
and takes the path (`_origin_path`): it then says to rename the
repository at that path, or to plan instead from a clone of the
repository on GitHub, and never `gh repo rename`. `_check_names` takes
the `Source` rather than its repository name so that it can say so too.

NIT-1, left. `_places_check_asks` compares `source.repository` with the
plan's `<org>/<assembly>` by `.lower()`, and `_is_the_source` by
`casefold()`. They agree on every name `checked_value` lets through, but
`source.repository` is read from an `origin` and never goes through it:
a path origin `.../straße/Atlas.git` planned with `--org strasse` plans
(casefold), and `_places_check_asks` does not take `strasse/Atlas` for
the source's origin (`.lower()`). Calling `_is_the_source` there would
change what `check` asks of that source.

tests/test_adopt_project_is_the_source.py:
`test_an_org_that_is_not_text_is_read_as_execute_reads_it`, over the
four shapes, and `test_a_name_read_from_a_path_is_renamed_at_that_path`,
over `adopt-not-the-source` and `naming-unclassified`; all six fail on
the code before this commit. `edited` can delete a line.

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 13, 2026
…s/<product>-pin.yaml`, through the symlinks the plan keeps in the root, as `place()` writes it after the copies, so a link only a pin's file runs through is refused before any leg and no longer stops `execute` after both legs are pushed (#183, #178)

rv183g's P1 on d92b747 (SHOULD-FIX). Since #273 (aa4e1f5, merged in
d92b747), `_commit_the_split` hands the materializer
`neutral_pins=_recorded_pins(plan)`, and `place()` writes each pin's
file after the copies and before the shape's own pin. `_PlannedRoot`
walked `assembly_root_paths()` with no pins. So with
`--pin openGlass@<commit>`, a source that keeps `contracts -> governance`
and a `governance/openglass-pin.yaml` that leads to an absolute path where
nothing is, to `../../outside-pin.yaml`, or to itself got `plan ok` from
`check`, and `execute` stopped after both legs with `materialize-outside`
or `materialize-symlink-loop` (rv183g_pinprobe.py). `main` wrote the pin
outside the assembly, or raised `OSError` after the legs.

- `_PlannedRoot` reads the plan's pins as `main`'s own
  `_leg_collision_findings` reads them, `_declared_pins(plan)`, each
  product once, in the plan's order: the products of `_recorded_pins`,
  the mapping `execute` hands the materializer. `walks()` follows
  `assembly_root_paths(self.pins)`.
- A symlink AT a declared pin's file is #273's `plan-root-holds-pin`.
  `_links_at_a_pin` puts it in `seen`, as `_links_at_a_copy` does for
  #262, asked as `_kept_pin_findings` asks it: one link, one finding.
- A `pins:` that is no list, or an entry that is no pin, gives no
  product, so it adds no path and raises nothing in the walk. `check`
  names it (`plan-pin-malformed`, `plan-pin-unrecorded`); `execute`
  refuses it before any leg and writes no file for it. #435 (main
  f391e58, merged next) asks a `pins:` that is no list first, before the
  source is opened, so `check` and `execute` stop there before any walk.
- rv183g's side effect: with a pin, `contracts/shape-pin.yaml ->
  openglass-pin.yaml` was refused as leading to what "is not in the
  source tree". `place()` writes the pin's file just before the shape's
  pin, so it finds a file there and writes the shape's pin beside it,
  under `shape/`, as it does wherever the root holds that path. The walk
  now says the same, so `check` prints no link finding for it. The run
  then fails verification after both legs, as on `main`
  (`adopt-shape-pin-unreadable`, exit 1; bootstrap 1): that is what the
  root keeps at the shape's pin, which #305 refuses at `project.yaml` and
  nothing refuses yet at the shape's pin (the PR's D13 through a link
  above), not where this link leads. For #457's checklist, not widened
  here.
- A link only a pin's file runs through that leads inside to where
  nothing is (`governance/openglass-pin.yaml -> nowhere.yaml`) is now
  refused, as AT-dangle-dir-present is: `main` writes the pin through it.

Tests (tests/test_adopt_root_symlinks.py, 149 tests, was 130):
- rv183g's three cases and the one that dangles inside: one
  `plan-root-symlink`, naming `contracts` and the pin's file, and
  `execute` refuses with that line before any leg (4);
- the same sources with no pin: `check` and `_refuse_what_check_finds`
  pass (4), and end to end the absolute one adopts, the link kept (1);
- a link AT the pin's file in a real `contracts/`, and its case twin:
  exactly one `plan-root-holds-pin`, no `plan-root-symlink`, and
  `execute` refuses with #273's line (2);
- `pins: 7`, `pins: openGlass`, an entry `7`, a name alone and an entry
  with no commit: `check` names the `pins:` alone with no traceback,
  `execute` refuses with it, no leg, and the walk adds no path and no
  finding (5);
- the walk's pins are `_recorded_pins`' products, once each, and its
  paths are what the materializer writes when handed them, in order (1);
- the side effect, in the walk (1) and on the disk (1).

Revert rows, each in a clone of d92b747 with this commit's two files, the
whole file run with git filter-repo on PATH:
- M0, no change: `149 passed`.
- M75, the walk handed no pins (`assembly_root_paths()` as at
  d92b747): `6 failed`, the four refused cases, the order test and the
  side effect's walk.
- M76, `seen` without `_links_at_a_pin`: `2 failed`, the two AT cases.
- M77, `_links_at_a_pin` without the disk that ignores case: `1 failed`,
  the case twin.
- M78, the walk reading `plan.pins` (the products of entries that are no
  pin included): `2 failed`, the name alone and the entry with no commit.
- M79, the walk reading the raw `pins:`: `5 failed`, the five blocks.
- M80, the pins not deduplicated: `1 failed`, the order test.
- The old table's M11 (every link a walk meets refused) and M21 (every
  kept link walked), on this round's line: `60 failed, 4 errors` and
  `23 failed`, the four no-pin controls and the end-to-end one among
  them; M46 (`place` writes through a link AT a path): `9 failed`, the
  side effect on the disk among them.
Every new test fails under at least one row.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 13, 2026
…ype gate on the plan's keys comes first in `check` and `execute`, before the source is opened, and the walk that now follows #273's pin files reads only the pins `_read_pins` takes, so a `pins:` that is no list is named once and never walked

Merged by sha d41cc29, the nine commits
of main past af01068 (47b1e06 to d41cc29). The brief named 8cd7dbe; main
had moved to d41cc29 (#402, shape-doctor.py) by the fetch at 21:19:04Z,
so the newest was merged. No conflict: the tree is
`git merge-tree --write-tree a79936f d41cc29`'s, d0b610c.

The commits that meet this branch's files:
- f391e58 (#386 via #435): `Plan` reads `paths:` and `allow_empty_legs:`
  through `_plan_list`, `source:` through `Plan.source`, and `legs:` only
  as a mapping in `names()`; `_plan_key_findings` names a `source:` or
  `legs:` that is no mapping and a `pins:`, `allow_empty_legs:` or
  `paths:` that is no list (`plan-bad-source`, `plan-bad-legs`,
  `plan-pin-malformed`, `plan-bad-allow-empty-legs`, `plan-bad-paths`).
  `cmd_check` prints them, with #293/#294's field findings, before it
  makes the work directory or opens the source, and exits 1;
  `cmd_execute` refuses them first (`_refuse_a_mistyped_plan`).
- 191105a (#225 via #266) and d52b100 (#462 via #506) change
  scripts/shape_materialize.py: the leg-path rule refuses a leg path the
  YAML reader reads back as another type, and `RULESET_HINT` offers one
  exit. `leg_path_problems` is what `_PlannedRoot._mount` and
  `_leg_findings` ask, so a plan it refuses has no mounts in the walk, as
  before; no line this branch changed in the materializer is touched.
- 47b1e06 (#259 via #395): the templates quote their text placeholders.
- 8cd7dbe (#286 via #347): update-shape.py refuses a pinned copy, the pin
  or the manifest kept as a symlink as `update-rewrite-symlink`, and
  shape-doctor.py calls such a copy `copy-is-link`. Neither is this
  branch's file, and the lines it adds name neither the materializer nor
  adopt-project.py.
- cdcd2f0, fc48854, 631f087 and d41cc29: setup-project.py,
  tests/conftest.py, shape-doctor.py, the family Makefile and
  update-shape.py, none of this branch's files.

How they compose with this branch:
- The walk reads the plan's pins through `_declared_pins`, as
  `_leg_collision_findings` does: only the entries `_read_pins` takes as
  pins. A `pins:` that is no list is #435's first finding, so neither
  command reaches `_entry_findings`, and so the walk, with one; were it
  reached (the tests ask the walk directly), it adds no path and raises
  nothing. A list whose entry is no pin passes #435's gate and is
  `_read_pins`' finding in `_leg_findings`, and the walk adds no path for
  it.
- A `paths:` that is no list reads as no entries (`_plan_list`), and a
  `legs:` that is no mapping as the default mount paths
  (`_plan_leg_path`): both are #435's findings first, so neither reaches
  the walk from `check` or `execute`.

The order, as read in the merged code:
- `check`: #435's key findings, with #293/#294's beside them, alone and
  first; else `plan-stale`, #320's split-branch findings, then
  `_entry_findings`: #196's unsafe paths, #303's empty symlinks, coverage,
  `_leg_findings` (#273's pin findings last), #166's submodules,
  `plan-root-symlink`, #255, #261, #479, #190, #293/#294.
- `execute`: `_refuse_a_mistyped_plan` (#435) before the work directory
  is made; then `_check_names`, `_refuse_an_unrunnable_plan`,
  `adopt-name-too-long`, #255's clone URL, `_refuse_what_check_finds`,
  `adopt-symlinks-off`, seeding, `adopt-work-dir-occupied`, #479's remote
  check, `adopt-branch-exists`, #320. All before any leg.

Checked on the merged tree: no top-level name bound twice in
adopt-project.py, scaffold-project.py, scripts/repo_shape.py,
scripts/shape_materialize.py, update-shape.py or shape-doctor.py but
main's own (`YamlError`/`parse_yaml` in the materializer, `sys` in the
doctor); no name loaded and bound nowhere; each side's change reappears
line for line in the other side's diff to the merge, in every file.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 13, 2026
…s a runnable `scripts/family.py`, printed commands quote paths, and `add`, `bump` and `remove` refuse the tracking branch and print their branch's push (#488, #486, #487) (#507)

Three defects from lane 3's v1.0.0 acceptance pass of AGENTS.md's "Creating a family, and adding a member", one commit each, in this order, then one review commit for #486 and one for #487. All three change `scripts/family.py` and `add`'s NEXT line, so they are one pull request.

**What was wrong.**
- AGENTS.md spelled the command `python3 scripts/family.py init --org <org> --family <Name>`, a path that runs from the standard's root. `--into` defaults to where the person stands.
- So the command, followed exactly, printed `landing <standard>/Orbit/Orbit` with no word, and `make siblings` would then clone every member into the standard's checkout too.
- `init`'s NEXT STEPS printed `python3 family.py add …`, built from `Path(__file__).name`. From the root that fails `can't open file`, exit 2.

**What changed** (commit 1).
- With neither `--into` nor `--work-dir`, `init` refuses a landing inside a checkout of openRepoShape: `family-landing-in-standard`. A checkout is a directory holding `scripts/family.py` beside `templates/family-root/`, found in the landing's ancestry, so another clone or an install counts too.
  - The refusal comes before anything is created, under `--dry-run` alike.
  - The remedy names `--into ..` from the root. From deeper inside the checkout it names the relative path that reaches the checkout's parent.
  - An explicit `--into` or `--work-dir` is not second-guessed. `family-holder-already-here` is still asked first.
- NEXT STEPS, and the `family-holder-already-here` remedy, name this file relative to the directory `init` ran in. From the root that is `scripts/family.py`, as setup-project.py, validate-family.py, AGENTS.md and README spell it. It is absolute where no relative path exists.
- AGENTS.md's init command passes `--into ..`, as README's already does, and step 1 says why. The edit is line-neutral: step 1 is one line shorter.
- `init --into --help` says it too, and docs/cli.md is regenerated.

**How tested.** `tests/test_family_init_landing.py`, 9 tests:
- the old command from the root is refused, as a dry run, so nothing can land in the checkout;
- a real run inside a fake checkout creates nothing;
- the remedy from a subdirectory names the right parent;
- an explicit `--into .` or `--work-dir .` passes;
- outside a checkout the default is unchanged;
- AGENTS.md's line, as written, lands beside the checkout;
- the printed NEXT runs, both from the root and from elsewhere.

On origin/main code 6 of them fail. Removing the refusal turns 3 red, and putting `Path(__file__).name` back turns 2 red.

**What was wrong.** `family.py` and the holder's `siblings.py` printed every path in their pasteable commands bare. Under a holder at `…/my work/Sonata/Sonata`:
- the `member-git-dir-cached` remedy `rm -rf <store>` split into `rm -rf …/my`, an absolute directory that is NOT the store, and a relative `work/…`;
- `init`'s NEXT STEPS and `add`'s NEXT split `--family-root` and `git -C` (`fatal: cannot change to '…/my'`, exit 128);
- the `mv` lines for a misnamed family folder pasted as four arguments.

**What changed** (commit 2, and the review commit after #487's).
- `scripts/repo_shape.py` gains `shell_quoted(value, platform=None)` and `shell_line(words, platform=None)`, in a new section at its end, the library `family.py` already imports.
  - It is the doctor's `quote_arg` rule (#101/#102): the same two alphabets, and the same two escapes, `sh`'s `'\''` and PowerShell's doubled apostrophe. The dialect comes from `os.name` or a `platform` argument. An ordinary word is left bare.
  - It does not flatten to ASCII, as the doctor's report does by contract. A command that deletes or moves a directory has to name that directory. So PowerShell's typographic single quotes are doubled too.
  - A word that begins with `=` is quoted, in both dialects. zsh, macOS's login shell, expands a bare `=word` to a command's path (`=ls` reaches the command as `/bin/ls`). This is the one place it differs from the doctor's rule on ASCII.
- `family.py`:
  - `init`'s NEXT STEPS print the `add` on ONE line. It used to paste only into a POSIX shell, because of its `\` continuation. `cd <holder>` and `make siblings` are two lines, with no `&&`, which Windows PowerShell 5.1 rejects between two statements.
  - Those lines and the `family-holder-already-here` remedy quote every path.
  - `add`'s NEXT quotes `git -C <holder>`, and `member-path-taken` quotes each spelling it names.
  - `member-git-dir-cached`, and `remove`'s note about the same store, print the directory alone on its own line. Then comes `rm -rf` with its one argument quoted for the POSIX shell `rm -rf` belongs to, and the sentence names that shell: Git Bash on Windows. PowerShell refuses `-rf` and deletes nothing.
- `siblings.py`: the parent-folder warning's `mv` and `mkdir -p` lines are quoted for a POSIX shell with the standard library's `shlex.join`, and the warning says which shell. It does NOT import the new quoter: a holder that kept its own `repo_shape.py` through `update-shape.py apply --accept-local` takes the new `siblings.py` without it, and an import of a name its copy lacks stopped `make siblings`, `park` and `resume` with an ImportError that `check` and the validator never reported.
- `setup-project.py`'s family hand-over quotes the holder in its `add` line (and names the branch first, see #487). It runs before any checkout of the standard exists, so it carries its own copy of the rule, and a test holds that copy to `shell_quoted`.

**How tested.** `tests/test_family_printed_quoting.py`, 69 tests:
- The quoter round-trips through `shlex.split`, and through a PowerShell reader, for a space and an apostrophe, `é` and a typographic apostrophe, shell syntax, the empty string, and every printable ASCII character alone and before, inside and after a word.
- The host's own `sh`, `bash` and `zsh` hand each quoted word back byte for byte through `printf`, a leading `=` among them (skipped where a shell is absent, and on Windows).
- It agrees with the doctor's `quote_arg` on every one of those ASCII values but a leading `=`, and its two alphabets are the doctor's two patterns.
- Under a holder in `my work` and in `Dana's work`, each of these splits into the argv it means: `init`'s NEXT (with `cd` and `make siblings` on their own lines), `add`'s NEXT, the cached-store remedy and note, the `mv` lines, and setup-project's `switch` and `add`.
  - The `rm -rf`, run as split, deletes the store, and the decoy `<tmp>/my` survives.
  - The `mv` lines, run as split, move the holder.
- The holder's own `siblings.py` runs `--help`, the misnamed-folder warning and `--make park` beside a `repo_shape.py` that lacks every name this change added.
- Three existing tests' expected lines follow the new wording.

Mutations, each turning tests red:
- a bare quoter: 35 red;
- bare `mv` lines in `siblings.py`: 3 red;
- a bare `rm -rf`, a bare `add` NEXT, a bare `init` `add` (round 1, on the 85-test file): 2 red each;
- `siblings.py` importing `shell_line` again: 1 red (the older-`repo_shape.py` test);
- `%` dropped from the POSIX alphabet: 2 red (the every-ASCII agreement and the alphabet test). The round-1 sample test passed it, 36 of 36;
- a leading `=` left bare: 10 red, among them the real-`zsh` test;
- `cd … && make siblings` back on one line: 2 red;
- setup-project's holder unquoted: 2 red.

**What was wrong.**
- AGENTS.md says each act's ONE commit lands as a pull request, and no step made the branch that pull request needs.
- After `init` the holder stands on `main`, and all three acts committed straight onto it.
- `add` printed `NEXT  git -C <holder> push -u origin <branch> && open a pull request`. No branch is called `<branch>`, so the push failed: `src refspec … does not match any`.
- `bump` and `remove` printed no NEXT at all.
- `bump-leg.py` refuses exactly this for a leg (`bump-leg-on-tracking-branch`).

**What changed** (commit 3, and the review commit after it).
- The real run of each act refuses the holder's tracking branch, the one `family.yaml` declares (`family-on-tracking-branch`), and a HEAD on no branch (`family-holder-detached`, which is what `bump-leg.py`'s `bump-leg-root-detached` refuses too).
  - The check comes after the refusals about the arguments and the holder, and before the member is read, fetched or cloned, and before anything is written.
  - The remedy is a quoted `git -C <holder> switch -c <branch>`. The branch is `family/add-<project>`, `family/bump-<id>-<sha12>` or `family/remove-<id>`, and a name that is not a plain branch word becomes `member`.
- `--dry-run` runs on any branch. On a work branch it prints where the commit would land. On the tracking branch it prints that the real run refuses there, with the refusal's code, and the `switch` command on a line of its own. The module's EXIT CODES paragraph names this as the one refusal a dry run does not make.
- Each act that commits ends with ONE pasteable line: `NEXT  git -C <holder> push -u origin <the branch it committed on>`, quoted, then `then open a pull request against <tracking>; never push to it directly`. `bump` and `remove` gain that NEXT.
- `init`'s NEXT STEPS open with the `switch` before the `add`.
- `add`, `bump` and `remove` `--help` say the branch rule, and that each ends by PRINTING the push; docs/cli.md is regenerated.
- AGENTS.md's family block gains the `switch` step before the `add`, and step 3 says the tracking branch is refused, as `family-on-tracking-branch`, and that the branch is pushed as its NEXT line says. README's InkRouter block gains the same step, and its paragraph says it. Both edits are line-neutral: AGENTS.md tightens steps 2 and 5, and README tightens the `make bootstrap` paragraph. `docs/handbook.html` mirrors the block and the sentence, and its footer names #487 and the new README blob.
- The holder's own docs, `templates/family-root/AGENTS-shape.md` and `templates/family-root/README.md`, open their add/bump/remove block with the `switch` and say the act refuses the tracking branch. `setup-project.py`'s family hand-over prints the `switch` before its `add`.
- Test fixtures that committed on `main` now commit on a branch:
  - `tests/test_family.py`'s module fixture grows the family on `family/members` and pushes that branch to the remote's `main`, as the merged pull request would leave it;
  - `test_shape_doctor.py`'s holder fixture and `test_family_add_excludes.py`'s `empty_holder` switch to a branch;
  - `test_family_add_refusals.py` changes in four places: the `NEXT` expectation, `unborn_holder`, the empty-family test, and the corrupt-HEAD test. In that last test, the tag state's branch check is answered in-process, because the test is about the rollback's reading of HEAD.

**How tested.** `tests/test_family_tracking_branch.py`, 17 tests:
- For each act, on `main`: refused, with the holder byte for byte as it was, and for `add` nothing cloned. Then the printed `switch`, run as split, makes the branch; the act goes through on it; and its printed NEXT, run as split, pushes the branch, which the remote then holds at the act's commit.
- A detached HEAD is refused.
- Dry runs on `main` and on a branch say what they should.
- A family made with `--tracking-branch trunk` refuses `add` on `trunk`; on a branch named `main` the dry run says the commit lands there for a pull request against `trunk`, the real run commits, and its NEXT pushes `main`.
- The suggested names pass `git check-ref-format --branch`.
- `init`'s NEXT STEPS open with the `switch`.
- AGENTS.md, README.md and the holder's two docs show the `switch` in the same block as, and before, the first `add`.

Mutations, each turning tests red: the branch check letting `main` through, 4 red; the NEXT line naming `<branch>` again, 3 red; `init`'s NEXT STEPS without the `switch`, 1 red; the tracking branch hard-coded to `"main"`, 1 red (the `trunk` test; every family test passed it before); the holder docs without the `switch`, 2 red; setup-project without the `switch`, 2 red.

An independent review at 10c93de gave CHANGES NEEDED (0 blockers, 1 should-fix, 6 nits). The should-fix was a holder that kept an older `scripts/repo_shape.py` through `apply --accept-local`: its new `siblings.py` could not import the shared quoter, so `make siblings`, `park` and `resume` died. The round at bb11b63 fixes it and all six nits, as described above. Re-verified at bb11b63: APPROVE WITH NITS (0 blockers, 0 should-fixes, 3 nits), with SonarCloud's quality gate OK and 0.0% new duplication. The nits were the holder README's pointer to the push (now "as its `NEXT` line says"), two Sonar smells (S1192: `refs/heads/` is now `HEADS_PREFIX`; S3415: the two expected-first assertions now put the actual value first, as the file's other 32 do), and this description's list of other pull requests. All three are fixed at 3451a56: commit 11ae0a8, then a clean merge of main 994be39. The family set gave 566 passed, 8 skipped there. GitHub's update-branch then merged main cac6bb2 as 8e1471c, and main aa4e1f5 (#508, after #490, #474 and #468) was merged as 708f3bb. There only the `docs/handbook.html` footer conflicted: it takes main's clauses, #468's (#424) included, with the README blob recomputed, and `docs/cli.md` auto-merged and passes `--check`. Main 47b1e06 (#505 and #395, after #376, #374, #384, #495, #493 and #491) was then merged as fcbc7a2, again with only the footer in conflict: main's clauses stay in main's order, this branch's (#487) moves to the end, after (#483), and the README blob is recomputed.

Every run used `/usr/bin/python3 -m pytest -q -p no:cacheprovider`, with a stub `gh` first on PATH, a bogus `GH_TOKEN`, an empty `GH_CONFIG_DIR` and git's identity blanked. Run on Linux. At e42ef74 (main d41cc29, #402, merged in by GitHub's update-branch), on its tree 655c927: the first set listed below for fcbc7a2, plus every `tests/test_update_shape*.py`, gave **1242 passed, 22 skipped**; the second set, plus `tests/test_leg_path_yaml_scalars.py`, `tests/test_ruleset_hint_tracking_branch.py`, `tests/test_scaffold_reuse_seeded_legs.py`, `tests/test_scaffold_pin_and_reuse.py` and `tests/test_doctor_validator_rows.py`, gave **1260 passed, 12 skipped**. The skips are the same as at 5a2776d, below.

At 654df49 (main 8cd7dbe merged in, with #506 and #347): the first set listed below for fcbc7a2, plus every `tests/test_update_shape*.py`, gave **1242 passed, 22 skipped**; the second set, plus `tests/test_leg_path_yaml_scalars.py`, `tests/test_ruleset_hint_tracking_branch.py`, `tests/test_scaffold_reuse_seeded_legs.py` and `tests/test_scaffold_pin_and_reuse.py`, gave **1231 passed, 12 skipped**. The skips are the same as at 5a2776d, below.

At 5a2776d (main f391e58 merged in, with #266, #509, #496 and #435): the first set listed below for fcbc7a2, plus every `tests/test_update_shape*.py`, gave **1213 passed, 22 skipped** (the template-quoting skips below, and three NTFS-junction cases, Windows only); the second set, plus `tests/test_leg_path_yaml_scalars.py`, gave **1171 passed, 12 skipped** (the four Windows-only skips below, and eight in that file, main's new one, whose cases the quoted templates (#259) leave with nothing to refuse).

At 22733ce (main 631f087, #503, merged in by GitHub's update-branch): the first set listed below for fcbc7a2, plus every `tests/test_update_shape*.py`, gave **1213 passed, 22 skipped** (the template-quoting skips below, and three NTFS-junction cases, Windows only); the second set gave **944 passed, 4 skipped**.

At fcbc7a2 (origin/main 47b1e06 merged in, with #505 and #395):

- every `tests/test_family*.py`, `tests/test_validate_family*.py`, `tests/test_park_resume_targets.py`, `tests/test_free_plan_hint.py` and `tests/test_template_text_placeholders.py`: **844 passed, 19 skipped** (`tests/test_family_init_values.py:219` and `:291`, which skip because the template now quotes the placeholder and `id:`).
- `tests/test_repo_hygiene.py tests/test_agents_md_codes.py tests/test_cli_reference.py tests/test_shape_doctor.py tests/test_setup_project_py.py tests/test_setup_sh.py tests/test_template_quoted_values.py tests/test_stdout_encoding.py tests/test_windows_paths.py tests/test_commit_trailers.py`: **944 passed, 4 skipped** (the doctor's PowerShell line and `tests/test_stdout_encoding.py:286`, Windows only).

At 708f3bb (origin/main aa4e1f5 merged in):

- every `tests/test_family*.py`, `tests/test_validate_family*.py`, `tests/test_park_resume_targets.py` and `tests/test_free_plan_hint.py`: **718 passed, 8 skipped** (`tests/test_family_init_values.py:242`).
- `tests/test_repo_hygiene.py tests/test_agents_md_codes.py tests/test_cli_reference.py tests/test_shape_doctor.py tests/test_update_shape_landing.py tests/test_doctor_unfinished_merge.py tests/test_template_values_single_pass.py tests/test_stdout_encoding.py tests/test_windows_paths.py`: **813 passed, 4 skipped** (the doctor's PowerShell line and `tests/test_stdout_encoding.py:286`, Windows only).

Earlier, with origin/main 48585e6 merged in:

- `tests/test_family*.py tests/test_validate_family*.py tests/test_stdout_encoding.py tests/test_windows_paths.py tests/test_repo_hygiene.py tests/test_agents_md_codes.py tests/test_cli_reference.py tests/test_park_resume_targets.py tests/test_bootstrap_spelling.py tests/test_free_plan_hint.py tests/test_template_values_single_pass.py`: **1303 passed, 9 skipped**.
- `tests/test_shape_doctor.py tests/test_update_shape*.py tests/test_commit_trailers.py tests/test_tracking_branch_ref.py tests/test_scaffold_ignored_shape_files.py tests/test_ruleset_hint_tracking_branch.py tests/test_bump_leg_dry_run_tracking.py tests/test_setup_project_py.py`: **999 passed, 6 skipped**.
- The new files alone: `test_family_init_landing.py` 9, `test_family_printed_quoting.py` 69, `test_family_tracking_branch.py` 17, all passed.

The full suite was not run locally. CI's three jobs are the check.

#505 and #395 have since landed, and both are merged here at fcbc7a2. Each of #505's family sentences was read against this branch's code, and none is false: AGENTS.md 3 and 233; README 569, 822, 1152, 1269 and 1348; the handbook 1126 and 1506. #395 landed with its test switching to `family/members` before its real `add`, as the last bullet below says, and the first run above includes it. #503 then landed as 631f087, and GitHub's update-branch merged it here as 22733ce with no conflict. Main f391e58 (#266, #509, #496 and #435) was then merged by hand as 5a2776d, again with only the footer in conflict: main's clauses stay in main's order, this branch's (#487) moves after #509's (#498), and the README blob is recomputed; `git show --remerge-diff` shows nothing else. #509's reworded sentences in AGENTS.md, README.md and the handbook were read beside this branch's, and none contradicts or repeats another. Its `setup-project.py` changes (the clone-path check, the plan line, the step-(7) backstop and `_main`) and this branch's `shell_word` lines in the hand-over do not meet, and `docs/cli.md` passes `--check`. Main 8cd7dbe (#506 and #347) was then merged by hand as 654df49, again with only the footer in conflict: this branch's (#487) moves after #506's (#462), the README blob is recomputed, and `docs/cli.md`, which both sides changed, still passes `--check`. #506's two `scripts/family.py` hunks (the `FAMILY_RULESET_HINT` import, and its print when the holder's push is refused) sit unchanged inside this branch's code, and that hint prints no path. Under `--reuse-empty-repo`, `init` still refuses its default `--into` inside a checkout of this standard (`family-landing-in-standard`) before it reads the remote, as a dry run with a stub `gh` showed. #506's sentences in AGENTS.md, README.md and the handbook are still true beside this branch's. Main d41cc29 (#402, for #277), which rewrote the doctor's NEXT lines and `tests/test_shape_doctor.py`, was then merged by GitHub's update-branch as e42ef74, with no conflict; the `holder` fixture still switches to `family/members` before its `add`. AGENTS.md stays at 490 lines and README.md at 1422.

`git merge-tree --write-tree` of 708f3bb, at 04:47Z on 2026-10-13:

- **Clean:** #503 (`fix/family-makefile-without-siblings`) and #395. Each merged tree keeps AGENTS.md at 490 lines and README.md at 1422.
- **`docs/handbook.html` footer only:** #505 (`docs/v1-accuracy`) and #491. #468 has landed and is merged here. #506 conflicts there too, and in `scaffold-project.py`, as it does with main. README.md and AGENTS.md merge clean with #505, and `setup-project.py` merges clean with #491. Whichever lands second keeps every footer clause and recomputes the README blob.
- **#395 merges clean but one of its tests fails with this branch.** `tests/test_template_text_placeholders.py::test_a_member_whose_id_yaml_would_misread_is_pinned_under_that_id` inits its own holder and runs a real `add` on `main`, which #487 refuses with `family-on-tracking-branch`. Whichever of the two lands second adds `git("switch", "-q", "-c", "family/members", cwd=root)` after that test's `init`; lane 5 is adding it on #395's own branch now, so this branch needs no change for it. (#395's head d526f76 also conflicts with this head in `tests/test_adopt_manifest_values.py`, a file this branch does not touch: that is #395 against main, which the same round resolves.)

- On Windows `shell_quoted` speaks PowerShell, the native shell this standard documents (`setup-project.py`), as the doctor's NEXT lines do. `git -C …`, `python scripts\family.py …` and `cd …` are shell-agnostic, so they get the platform's quoting, and no printed line joins two commands with `&&`, which Windows PowerShell 5.1 rejects.
- `rm -rf`, `mv` and `mkdir -p` are POSIX commands. They are quoted for a POSIX shell on every host, and the text says "in a POSIX shell (Git Bash on Windows)". PowerShell refuses `rm -rf`'s `-rf` and deletes nothing.
- The tests split a host-quoted line with a PowerShell reader on Windows and with `shlex` elsewhere, and split a POSIX-only line with `shlex` everywhere. The real-shell test is skipped on Windows.
- On Windows the tests do not execute `rm` or the `mv` lines. They delete the split path with `shutil.rmtree`, and stop after checking the `mv` argv.
- Paths: the cached store keeps git's POSIX spelling (`as_posix`), as before. Test paths are `resolve()`d, which expands an 8.3 short name such as `RUNNER~1`.
- Nothing here was run on Windows or macOS locally. The `tests-windows` and `tests-macos` CI jobs are the check; on macOS the real-shell test runs the shipped `zsh`.

- **A shipped template that imports a newer shared helper can break a holder that kept an older copy.** This change no longer does (`siblings.py` uses `shlex.join`), but nothing stops the next one: `update-shape.py apply --accept-local scripts/repo_shape.py` keeps a `repo_shape.py` that lacks a name a newly copied file imports, and `check` and the validator then read the holder as fine. The general guard is for `apply` to refuse that, as rv456 found for `bootstrap.py` and `spelled` (#456).
- **The doctor's `quote_arg` leaves a word that begins with `=` bare**, which zsh expands. `shell_quoted` quotes it; the doctor is left as it is here, and the agreement test excludes that one case.
- **The doctor and `update-shape.py` keep their own quoters.** The doctor's `quote_arg` flattens to ASCII by its report's contract, so moving it onto `repo_shape.shell_quoted` is more than an import change. `update-shape.py`'s POSIX `shlex` lines are lane 3's open #374. A test holds `shell_quoted` and `quote_arg` to one answer on every printable ASCII value but a leading `=`.
- **`family.py`'s by-hand rollback commands (#365) and its `git config -f .gitmodules` removals (#317, #360) keep `shlex.quote`.** They were already quoted, they are POSIX-only (`printf … | git update-index`), and they are worded for a POSIX shell.
- **The holder's `validate-family.py` remedies that name `family.py add`** (`member-gitlink-absent`, `member-gitlink-unnamed`) print no branch step: they are a holder copy, and the `add` they name refuses the tracking branch with its own `switch` remedy.
- **`remove`'s store note still computes `.git/modules/<path>`** rather than asking `git rev-parse --git-path`, as `add` does. In a linked worktree it therefore prints no note. That predates this change.
- **PowerShell verbs** (`Remove-Item`, `Move-Item`) are not printed for the POSIX-only commands.
- **README's `--into` sentence gives the default but not the refusal.** README.md (861) and the handbook (1246) say `--into <dir>` is the PARENT directory, "the directory you are standing in, by default". They do not say that `init` refuses that default inside a checkout of this standard (`family-landing-in-standard`). AGENTS.md item 1 says so, and the README's own example passes `--into ..`. README.md is at its 1422-line cap, so the sentence is on #459's checklist.
- **`setup-project.py`'s own `cd <clone>` line and its step-(7) backstop lines print their paths unquoted.** This change quotes only the family lines it prints. Those two predate it and #509, and they are on #459's checklist.

Reviewed independently at 10c93de: CHANGES NEEDED (0 blockers, 1 should-fix, 6 nits). The round at bb11b63 fixed the should-fix and all six nits, and its re-verification gave APPROVE WITH NITS (0/0/3), with SonarCloud's quality gate OK; those three are fixed at 3451a56. Main was then merged by GitHub's update-branch (8e1471c), by hand at 708f3bb and at fcbc7a2, where each time only the handbook footer conflicted, and by update-branch again at 22733ce (main 631f087, #503), with no conflict. CI was green there on Linux, macOS and Windows, with SonarCloud passing, but main had moved on: main f391e58 (#266, #509, #496 and #435) was merged by hand at 5a2776d, where again only the footer conflicted. CI was green there too, but main moved on again: main 8cd7dbe (#506 and #347) was merged by hand at 654df49, where once more only the footer conflicted. CI was green there as well, and main d41cc29 (#402) then came in by update-branch at e42ef74, with no conflict. CI is green on Linux and macOS, with SonarCloud passing, at e42ef74, a clean merge of main. All three OSes and SonarCloud passed at 654df49, and under Brett Heap's landing rule of 22:08:06Z (ruling 27) macOS and Windows carry over from that head. Main e6fbd74 (#515, for #514), which landed after it, touches none of its files, and the combined tree passed `tests/test_adopt_pins.py`, `tests/test_shape_doctor.py`, `tests/test_scaffold_leg_path_collisions.py`, the hygiene, AGENTS-code and CLI-reference tests, `tests/test_family_printed_quoting.py` and `tests/test_family_tracking_branch.py` (777 passed, 3 skipped).

Closes #488
Closes #486
Closes #487

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 14, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment