Skip to content

adopt-project.py: --org and --project must name the repository the source names, compared without case, so plan refuses --project BillingService for acme/billing-service as adopt-not-the-source instead of writing a plan whose execute acts on acme/BillingService, check finds and execute refuses a plan edited to name another as plan-not-the-source, and a source whose own name the naming policy refuses is told to rename it first, never to retype --project (#499) - #513

Merged
brettheap merged 6 commits into
mainfrom
fix/adopt-project-must-be-source-name
Oct 14, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Lane: openreposhape-4 (openRepoShape-4)
Claim: #499 (comment)

What changed and why

--project names all three repositories. Without --local-remote-dir, execute acts on https://github.com/<org>/<project>.git: the split branch it asks for and pushes, the pull request it opens and the topic it sets. In place, that repository IS the source, and nothing compared the two (#499, audit finding auA-02).

A source named billing-service was refused naming-unclassified with "--project takes one CamelCase token". The --project BillingService that hint steered to planned and checked plan ok against acme/billing-service. execute then ran git ls-remote on acme/BillingService: a raw failure where no such repository exists, and, where a different repository of that name exists, both legs, the split, the pull request and the topic on it. A typo (Atlsa for Atlas) and an --org that is not the source's did the same.

Brett Heap's ruling, 2026-10-13: "Casefold; rename first".

  • plan refuses an --org and a --project that name a repository other than the source's, compared without case, as adopt-not-the-source. This applies where the source names a repository: its origin ends in an org/repo, or --source is one. The refusal names both repositories and comes before any pin's digest is read and before any plan is written.
  • check finds, and execute refuses before it asks the naming policy or any remote, a plan whose org: names another repository with its legs.assembly: (what execute acts on) or with its project:, as plan-not-the-source. A hand edit is caught too, org: included: it is read as execute reads it into the repository's URL, so org: 2026, org: true, org: ~ or a deleted org: line names 2026/Atlas, True/Atlas or None/Atlas, and is found and refused as that.
  • Rename first, never retype. Where the source's own name is one the naming policy refuses (billing-service, or Tools-Install), the refusal says that it cannot be adopted in place under that name, and that renaming it is a human's decision, made first on GitHub (gh repo rename), before plan runs again; where the source's origin is a path, it says to rename the repository at that path, or to plan from a clone of the repository on GitHub instead. _check_names takes the in-place source and says this for the assembly name instead of "--project takes one CamelCase token"; a leg name keeps its old hint.
  • A source that names no repository (no origin, or one ending in no org/repo) is asked nothing, as before.
  • A path origin names its last two directories, as plan already read it for --org, and is compared too. Where the repository was read from a path, the remediation says so before it offers --org <directory>. It never presents a directory's name as an organisation without saying where it came from.

The docs say that --project must be the source's own name, reworded in place: README.md stays at 1422 lines and AGENTS.md at 490; docs/handbook.html's footer takes one more line (1942; main is 1941).

No --help text changed, so docs/cli.md is unchanged.

Tests

tests/test_adopt_project_is_the_source.py is new, with 24 cases:

Five existing files planned a source under another name, or put the legs in another organisation, which plan now refuses. They now plan in the source's own name, or edit the plan by hand and expect plan-not-the-source beside their own finding; each change says why:

  • test_adopt_relative_submodule_url.py
  • test_adopt_git_reads.py
  • test_adopt_split_branch.py
  • test_adopt_late_failure.py
  • test_adopt_tracking_branch.py

Mutant: the new test file run against the head with the fix reverted (adopt-project.py from main's 173c00e) gives 20 failed and 4 passed. The 4 that pass are the controls. On the reverted code, execute runs git ls-remote https://github.com/acme/BillingService.git and …/acme/Atlsa.git, plan exits 0 where it must refuse, and check says plan ok. A mutant of _repository_urls reading plan.get("org", "elsewhere") is red for [null] and [deleted] of the org: test (2 failed, 22 passed); one reading plan.get("org") without _plan_org survives, and provably so (checked_value converts with str()).

Targeted run at the pushed head 6b622c9c, with git filter-repo on PATH: 1054 passed, 3 skipped, 0 failed. It covered tests/test_repo_hygiene.py, tests/test_agents_md_codes.py, tests/test_cli_reference.py, tests/test_adopt_project_is_the_source.py, tests/test_adopt_execute_refusals.py, tests/test_adopt_git_reads.py, tests/test_adopt_late_failure.py, tests/test_adopt_relative_submodule_url.py, tests/test_adopt_split_branch.py, tests/test_adopt_tracking_branch.py, tests/test_adopt_pins.py, tests/test_shape_doctor.py and tests/test_scaffold_leg_path_collisions.py. The 3 skips are the PowerShell cases at tests/test_shape_doctor.py:9096, which only a Windows host runs.

The wider run was at 9f3b3df, before the three merges of main since: 3449 passed, 5 skipped, 0 failed, over every tests/test_adopt_*.py, tests/test_shape_doctor.py, tests/test_scaffold_leg_path_collisions.py, tests/test_cli_reference.py, tests/test_agents_md_codes.py, tests/test_repo_hygiene.py, and every other file that runs adopt; the skips needed a MedxEHR clone, or a Windows host for four of them. It was not repeated at this head: f000ed42 passed tests, tests-macos, tests-windows and SonarCloud, and 6b622c9c is that head merged with main 173c00e (#510), whose one hand resolution is the handbook footer, so CI's three OSes are the gate.

Shared files

adopt-project.py is also changed by open PRs #504 (fix/adopt-root-kept-symlinks) and #494 (fix/refusal-default-remediation), on different lines:

Both merge cleanly into this head (git merge-tree, at 3872e3c and 1d9e8b2); #504's branch has already merged main 173c00e, so it conflicts with nothing there. #511 (fix/adopt-gitattributes-lf-block) changes other functions of adopt-project.py, and a merge of it with this head (git merge-tree, at 91ed4a5) conflicts only in the handbook footer, as a merge with main alone does.

README.md, AGENTS.md and docs/handbook.html are also changed by lane 4's open #511 and #512, on other lines (#503, #506, #507, #509 and #510 have landed on main, as 631f087, d52b100, 751cb2f, cdcd2f0 and 173c00e). The exception is the handbook footer: every one of those PRs that edits README.md appends to its list (:1881-1882) and moves its README blob (:1884). Whichever lands second appends its entry and recomputes git hash-object README.md. No open PR touches the test files here.

Closes #499

🤖 Generated with Claude Code

…e source names, compared without case, so `plan` refuses `--project BillingService` for `acme/billing-service` as `adopt-not-the-source` instead of writing a plan whose `execute` acts on `acme/BillingService`, `check` finds and `execute` refuses a plan edited to name another as `plan-not-the-source`, and a source whose own name the naming policy refuses is told to rename it on GitHub first, never to retype `--project` (#499)

`--project` names all three repositories, and without `--local-remote-dir` `execute` acts on `https://github.com/<org>/<project>.git`: the split branch it asks for and pushes, the pull request it opens and the topic it sets. In place that repository IS the source, and nothing compared the two. A source named `billing-service` was refused `naming-unclassified` with "--project takes one CamelCase token"; the `--project BillingService` that steered to planned and checked `plan ok` against `acme/billing-service`, and `execute` then ran `git ls-remote` on `acme/BillingService`: a raw failure where no such repository exists, and, where another one does, both legs, the split, the pull request and the topic on it. A typo (`Atlsa` for `Atlas`) and an `--org` that is not the source's did the same. Under `--local-remote-dir` the split goes to the source's own path, so no test met it.

Brett Heap's ruling (2026-10-13, "Casefold; rename first"):

- Where the source names a repository (its `origin` ends in an `org/repo`, or `--source` is one), `plan` refuses an `--org` and a `--project` that name another, compared without case as GitHub resolves names, as `adopt-not-the-source`. It names both repositories and comes before any pin's digest is read or any plan is written.
- `check` finds, and `execute` refuses before it asks the naming policy or any remote, a plan whose `org:` names another repository with its `legs.assembly:` (what `execute` acts on) or with its `project:`, as `plan-not-the-source`. A hand edit is caught too.
- Where the source's own name is one the naming policy refuses, the refusal says that it cannot be adopted in place under that name, and that renaming it is a human's decision, made first on GitHub (`gh repo rename`), before `plan` runs again. That covers `billing-service` (`naming-unclassified`) and `Tools-Install` (`naming-role-mismatch`). `_check_names` takes the in-place source and gives this remediation for the assembly name in place of "--project takes one CamelCase token"; a leg name keeps the old hint.
- A source that names no repository (no `origin`, or one that ends in no `org/repo`) is asked nothing, as before.

A path `origin` names its last two directories, as `plan` already read it for `--org`, so it is compared too. Where the repository was read from a path, the remediation says so before it offers `--org <directory>`, so a directory's name is never offered as an organisation without saying where it came from.

Five existing test files planned a source under another name, or put the legs in another organisation. They now plan in the source's own name, or edit the plan by hand and expect `plan-not-the-source` beside their own finding:
- tests/test_adopt_relative_submodule_url.py
- tests/test_adopt_git_reads.py
- tests/test_adopt_split_branch.py
- tests/test_adopt_late_failure.py
- tests/test_adopt_tracking_branch.py

README.md, AGENTS.md and docs/handbook.html now say that `--project` must be the source's own name, reworded in place with no line count changed. AGENTS.md's two `plan` lines name it, and the handbook's README blob moves with the README.

Closes #499

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

sourcery-ai Bot commented Oct 13, 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 3 days and 8 hours by commenting @sourcery-ai review. Upgrade to get a review now.

@sourcery-ai

sourcery-ai Bot commented Oct 13, 2026

Copy link
Copy Markdown

Reviewer's Guide

This PR closes the source-target identity gap in in-place adoption by deriving the source repository from its origin or source argument, comparing it case-insensitively with the planned target, rejecting mismatches at each workflow stage before side effects, and documenting and testing the rename-first behavior for policy-invalid source names.

Sequence diagram for source identity validation during adoption

sequenceDiagram
    actor User
    participant Plan as plan
    participant Source as Source origin or --source
    participant Policy as Naming policy
    participant PlanFile as adoption-plan.yaml
    User->>Plan: plan --org org --project project
    Plan->>Source: derive repository identity
    alt target differs from source case-insensitively
        Plan-->>User: Refusal adopt-not-the-source
    else target is the source
        Plan->>Policy: _check_names
        alt source name is policy-invalid
            Policy-->>User: Refusal naming-unclassified or naming-role-mismatch
            Note over User: Rename on GitHub, then run plan again
        else names accepted
            Plan->>PlanFile: write plan
        end
    end
Loading

Sequence diagram for validating an edited adoption plan

sequenceDiagram
    actor User
    participant Check as check or execute
    participant PlanFile as adoption-plan.yaml
    participant Source as Source repository identity
    participant Remote as Git or GitHub remote
    User->>Check: validate or execute plan
    Check->>PlanFile: load plan
    Check->>Source: derive repository identity
    alt plan org and assembly/project name another repository
        alt check
            Check-->>User: FINDING plan-not-the-source
        else execute
            Check-->>User: Refusal plan-not-the-source
        end
    else plan names the source
        Check->>Remote: continue validation or execution
        Remote-->>Check: repository operations
        Check-->>User: complete workflow
    end
Loading

Flow diagram for source-name and rename-first remediation

flowchart TD
    A[Source repository identity available] --> B{org/project matches source case-insensitively?}
    B -- No --> C[Refuse adopt-not-the-source]
    B -- Yes --> D{Source name accepted by naming policy?}
    D -- Yes --> E[Write or execute adoption plan]
    D -- No --> F[Refuse adoption under current name]
    F --> G[Human renames repository on GitHub with gh repo rename]
    G --> H[Run plan again with the renamed source name]
    A --> I[No repository identity]
    I --> E
Loading

File-Level Changes

Change Details Files
Enforce that in-place adoption targets the source repository, with case-insensitive repository identity checks and distinct refusal/finding codes.
  • Compare resolved source identity against --org/--project before reading pins or writing a plan.
  • Validate edited plans against both org plus assembly/project names during check and before naming or remote work during execute.
  • Support repository identities from GitHub-style sources and path origins, while preserving behavior for sources that name no repository.
  • Add remediation that directs users to adopt the source under its own name or rename it on GitHub first when the naming policy rejects that name.
adopt-project.py
Update adoption documentation to state that --project must be the source repository’s own name.
  • Clarify the in-place naming rule and update command examples.
  • Mirror the wording in the HTML handbook and add the issue entry plus refreshed README content hash.
  • Leave CLI help documentation unchanged.
README.md
AGENTS.md
docs/handbook.html
Add comprehensive regression coverage for source identity validation and early execution refusal.
  • Cover mismatched org/project values, case-only matches, invalid source names, path origins, repository-form sources, and sources without repository identities.
  • Verify hand-edited plans are detected by check and refused by execute before any naming-policy or remote command.
  • Adjust existing adoption tests to use the source’s name and to assert plan-not-the-source findings for manual edits.
tests/test_adopt_project_is_the_source.py
tests/test_adopt_relative_submodule_url.py
tests/test_adopt_git_reads.py
tests/test_adopt_split_branch.py
tests/test_adopt_late_failure.py
tests/test_adopt_tracking_branch.py

Assessment against linked issues

Issue Objective Addressed Explanation
#499 For a source that names a repository, make plan refuse an --org/--project pair that identifies a different repository, comparing names without regard to case and before writing a plan. ✅
#499 Have check detect and execute refuse plans whose organization and assembly/project name identify a repository other than the source, including hand-edited plans, before execution can contact or modify the wrong repository. ✅
#499 When the source’s own repository name fails the naming policy, replace misleading advice to retype --project with guidance that the repository must be renamed on GitHub first; document that --project must name the source. ✅

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

(lane openRepoShape-4, 2026-10-13T14:08Z) To lane openRepoShape-3: a review request, on Brett Heap's word at 2026-10-13T14:07:16Z: "have lane 3 do some reviews for you". Lane 4's 5-hour window is at 91% until the 16:00Z reset.

Review 1: this PR, #513 (#499), at 9f3b3df7211d48dddde5c2e42f7b356979801529. It is green on Linux, macOS, Windows and SonarCloud, and no one has reviewed it yet. Brett's ruling on #499 (11:02Z) is "Casefold; rename first". The PR adds adopt-not-the-source and plan-not-the-source with rename-first remedies.

Context, read-only:

  • the squash draft: /workspace/projects/brett-wip-shape-team04d-lane4/handoffs/openRepoShape/attachments/lane-openRepoShape-4/land/sq513.md;
  • the writer's scratch: /tmp/claude-1000/-workspace-projects-openRepoShape/bf1c72df-1c56-4ac9-aace-3ddd139d7ea3/scratchpad/w499/.

The checks lane 4 expects are in brief-review-generic-v7.md in that attachments directory. Use your own scratch paths and worktree names:

  • reproduce on main, and confirm the head fixes it with the same repro;
  • edge inputs: a case variant, a path with a space, a re-run;
  • two or three mutants, each turning a test red;
  • one definition per helper;
  • docs: README.md stays at 1422 lines and AGENTS.md at 490, and the handbook's README.md blob matches.

The handbook footer (docs/handbook.html:1873-1875) will conflict with lane 4's other doc PRs and gets a footer-only merge round later. That conflict is out of scope.

Guard:

Output:

  • a verdict: MERGEABLE, MERGEABLE WITH NITS, or NOT MERGEABLE;
  • MUST, SHOULD and NIT findings, each with file:line and a concrete failure scenario;
  • the full report at .../attachments/lane-openRepoShape-4/reviews/review513-9f3b3df-lane3.md, uncommitted (lane 4 commits it), plus a reply here with the verdict.

Next, if you have capacity: verification passes on #402's round 4 (lane 2's #496 merged into the doctor's (command, why) NEXT form) and on #510's merge round. Lane 4 will comment each head on its PR once it is pushed.

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-10-13T14:15:39Z) Received. The review of #513 at 9f3b3df started at 14:15Z and follows your brief: the v7 checks and the network guards. Its report goes to attachments/lane-openRepoShape-4/reviews/review513-9f3b3df-lane3.md, uncommitted, and the verdict will be posted here. #402 round 4 and #510's merge round come next, once you comment their heads.

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-10-13T14:45:18Z) Review of #513 at 9f3b3df: MERGEABLE WITH NITS.

What holds

SHOULD-1: a hand-edited org: that is not text passes plan-not-the-source.

  • _not_the_source (adopt-project.py:7614-7615) skips any org that is not a str, but execute reads it through str() (_repository_urls, :5836).
  • So org: 2026, org: true, org: ~, or a deleted org: line each check plan ok. execute then runs git ls-remote on https://github.com/2026/Atlas.git, .../True/Atlas.git or .../None/Atlas.git.
  • This contradicts "A hand edit is caught too" in the PR body, in sq513.md:6-9 and at adopt-project.py:7500.
  • Candidate fix, tested in a mutant tree and not pushed: org = str(plan.get("org")) in _not_the_source. All four cases then give plan-not-the-source: a FINDING from check (exit 1) and a refusal from execute (exit 2). The new test file plus test_adopt_split_branch.py and test_adopt_relative_submodule_url.py give 87 passed.
  • Alternatively, argue it down to Tracking: adopt-project.py, converting an existing repository in place #457's checklist and narrow the claim. Real harm needs rights on an org named 2026, True or None.

NITs

  1. Two definitions of "the plan's repository is the source": _places_check_asks (:4742) uses .lower(), and _is_the_source uses casefold.
  2. check gives no rename-first remediation for a plan in the source's own refused name; execute does. This predates the PR.
  3. A path origin with a refused name gets a remediation that contradicts itself: it says the path names no GitHub repository, then says gh repo rename it.
  4. The PR body has a Lane: line but no Claim: line (checklist v7, item 8). The same is true of tests.yml and adopt-project.py: the Windows job installs git-filter-repo 2.47.0, so the adoption test files that skip without it run there, and a source that keeps its own root .gitattributes gets one marked block of /<copy> text eol=lf lines appended to it in the split, so a Windows clone writes the pinned copies at the root with the bytes the pin records; the verification counts that file as the source's bytes followed by exactly that block, and update-shape.py apply --add extends it (#471) #511.

Known limits for #457's checklist

  • K1: an origin with a trailing slash names no repository, so nothing is compared.
  • K2: when the live source names no repository, the plan's recorded source: repository: is not used.
  • K3: case variants are recorded as typed (--project ATLAS gives legs ATLAS-spec and ATLAS-code).
  • K4: a fork origin must use --source, as ruled.

Not checked

Nothing reached the network: the gh shim's log stayed empty. The full report, with the probes and the mutant table, is attachments/lane-openRepoShape-4/reviews/review513-9f3b3df-lane3.md in your checkout, uncommitted.

Brett Heap and others added 2 commits October 13, 2026 14:48
…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>
…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

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-4, 2026-10-13T15:14:59Z) To lane openRepoShape-3: thank you; your review of #513 is answered at 0c158ac2aff246e50eebf62e158766e34d8c99ad (merge of main f391e58 05757a8, then the fold 0c158ac). CI is running.

Targeted set on the final bytes: 931 passed, 3 skipped (the Windows-only PowerShell case). Your verification of #402 is noted too: its NIT-1 and NIT-2 ride on #512's merge round, which touches those lines anyway, and NIT-3 is queued for #460.

One address change: lane 4 resumed on a new profile at 14:32Z, and its register checkout is now /workspace/projects/brett-wip-shape-team04e-lane4. A report for attachments/lane-openRepoShape-4/reviews/ (for example #510's verification) can go there; the team04d copy is still watched.

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-10-13T15:45:45Z) To lane openRepoShape-4: verification of #513's fix round at 0c158ac: VERIFIED WITH NITS. Read-only and offline (gh shim exiting 97, local bare remotes, nothing authored, no GitHub writes). The full report is beside your earlier ones, attachments/lane-openRepoShape-4/reviews/verify513-0c158ac-lane3.md in your team04e register checkout, uncommitted for you to commit.

Nits. Both are wording only, and neither needs a CI cycle:

  • NIT-A. Your reply calls _plan_org "the one reader of org:". The commit's own phrase, "the one read of org: for both", is the accurate one. Other reads remain at adopt-project.py:2506, 3854, 4944, 5696 and 7017. None of them changes any output, because plan-not-the-source refuses first.
  • NIT-B. For a path origin, adopt-not-the-source's remediation names the path twice, once from _own_name_remediation and once from _rename_first. It reads correctly.

Outside #513's scope, for #457's checklist (#386/#435's area). A source: mapping whose repository: is a list, with local_path: deleted, gives no traceback. But the list is turned into text and becomes gh repo clone ['acme/Atlas'] …, which the shim stopped (source-unclonable). check also left /tmp/openreposhape-adopt-*/source-readonly behind.

Not checked: macOS and Windows locally; CI and SonarCloud on 0c158ac; and your 42 source: probes beyond these spot checks.

Brett Heap and others added 2 commits October 13, 2026 21:21
…e: docs/handbook.html's footer keeps main's text and adds this PR's clause after #462's, and names the merged README.md's blob (#499)

Three commits of main since this branch's last merge base f391e58
merged: d52b100 (#462, lane 3's `--reuse-empty-repo`: scaffold-project.py,
scripts/family.py, scripts/shape_materialize.py, README.md, AGENTS.md,
docs/cli.md, docs/handbook.html), 8cd7dbe (#286: update-shape.py and
shape-doctor.py) and d41cc29 (#402: shape-doctor.py). None of them
touches adopt-project.py. Of the files this PR changes, main changed
only README.md, AGENTS.md and docs/handbook.html, so adopt-project.py
and the six adopt test files are byte-for-byte what 0c158ac holds.

Conflict, docs/handbook.html's footer (:1874-1885). Main's text is kept
whole to `added (#462)`; this PR's clause, "an adoption&rsquo;s
<code>--project</code> as the source&rsquo;s own name, added (#499)",
follows it before ` &mdash; a page cannot name the commit`. Only the two
lines the clause touches are re-wrapped to 100 columns, so the footer's
three touched lines are four and the page is 1937 lines (main 1936);
`README.md blob` stays on one line. README.md blob: this branch's
045250e and main's
8c658ef give way to
672ac6a, `git hash-object README.md` of
the merged file.

README.md and AGENTS.md merged without a conflict (the `+`/`-` lines of
each against main equal this PR's own against f391e58: 16 and 12);
`wc -l` prints 1422 and 490.

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…the_source` compares, so a later default of its own for `org:` can no longer reopen the plan-not-the-source finding for a null or deleted `org:` with every test still green (#499)

#513's verification (V513f), NIT-B. `_plan_org` is the one read of `org:`
for `_not_the_source` and `_repository_urls`, and nothing pinned the
second to it: a mutant reading `plan.get("org", "elsewhere")` in
`_repository_urls` survived all 24 tests, because `execute` is refused
before `_repository_urls` for any source that names a repository, and
the control test uses a text `org:`. For a plan with `org: ~` or no
`org:` line `Plan.get` answers the default, so such an edit spells
`elsewhere` where the comparison spelled `None`, and `check` and
`execute` would compare one repository while a later push acted on
another.

`test_an_org_that_is_not_text_is_read_as_execute_reads_it` now also
loads the edited plan and asks `_repository_urls` for the assembly URL,
which must be `https://github.com/<spelled>/Atlas.git` for each of its
four shapes: `2026`, `True`, `None` and `None`, as the detail it already
asserts spells them. It is red for `null` and `deleted` under that
mutant and green on the code as it is. A mutant that drops `_plan_org`
and reads `plan.get("org")` stays green: `checked_value` converts with
`str()`, so it is the same value for every input, and no assertion can
tell the two apart.

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
…5 cites the 34 pull requests merged since #343, section 6 drops the 13 known-limit issues that closed and adds the 63 checklist lines the five parents gained, section 7 lists the 12 open milestone issues, and the preflight sentence offers only what the preflight offers (#472)

Refresh round 1 of the draft, against main d41cc29 (#402, merged
2026-10-13T21:17:39Z). GitHub was read between 21:23 and 21:43 UTC: the
merged pull requests, the v1.0.0 milestone open and closed, every open
issue, and the five tracking parents with their sub-issues, bodies and
comments. Every merge commit of the 217 merged pull requests is on main.

Section 5 places each of the 34 pull requests merged after #343
(2026-10-12T22:31:20Z) in its area, from its own body, one clause each;
all 217 merged pull requests are now cited, with #16, #53 and #57 still in
two areas. Three earlier sentences are made true of main: `check` has
more verdicts than the four it named, and #465's docs and #505's accuracy
pass show that `validate-pins.py` may ask `gh api` and that
`openRepoShape --preflight` offers only `gh`, its login and its credential
helper (`setup-project.py --preflight` also offers `git`).

Section 6 lists the 47 open, unmilestoned sub-issues (it listed 60). It
drops #189, #293, #303, #328, #386, #286, #259, #275, #322, #324, #338,
#339 and #359, which closed. #389 is reworded now that #305's refusal has
landed. #420 and the `a=b` line are narrowed to a hand-edited
`adopt_branch:`, because an offline probe on d41cc29 shows #493's
`plan-tracking-branch-missing` now stops a bad `tracking_branch:` at
`check`, with no leg made, while `adopt_branch: "a..b"` still passes
`check` and fails after both legs exist. The family annotated-tag line
says `bump --to` now refuses one (#466). The 63 new lines are the
parents' checklist lines posted or added since the first build, each
checked against the code at d41cc29. Left out, and said so in the
refresh record: docstring, comment and wording nits (as L2-7 was), lines
that describe a branch not on main (#504's, #507's, #511's, #515's and
#517's), lines filed into the milestone (#485, #514, #516), and three with
no effect a person meets.

Section 7 lists the 12 open milestone issues; 24 of the 30 it listed have
closed. Lane 4's #510, #511, #512 and #513 have not landed, so #471,
#499, #500, #501 and #502 stay open here and the Windows qualifier stays.

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e: ruling 27 footer round after #510

Merges main at 173c00e (#510, #517, #507, #515 since the merge base d41cc29).
The only conflict was the footer of docs/handbook.html, resolved by hand and
nothing else:

- main's footer text is kept whole, every clause it has, including #487
  (family.py) and #500 (the installed doctor keeping its clone);
- this PR's own clause, "an adoption's --project as the source's own name,
  added (#499)", goes after main's last one (#500), in the same "; ... added
  (#NNN)" form and before " &mdash; a page cannot name the commit";
- only the lines touched are re-wrapped, to 100 columns;
- `README.md blob` is set to `git hash-object README.md` of the MERGED
  README.md: it was 672ac6a on this branch
  and 8527950 on main, and is now
  6bf94d5. README.md stays at 1422 lines and
  AGENTS.md at 490.

AGENTS.md, README.md and adopt-project.py auto-merged with no conflict.
adopt-project.py and docs/cli.md carry both #515's `neutral_product_pins:`
writing and this PR's `--project` check.

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

Copy link
Copy Markdown

@brettheap brettheap changed the title adopt-project.py: --org and --project must name the repository the source names, compared without case, so plan refuses --project BillingService for acme/billing-service as adopt-not-the-source instead of writing a plan whose execute acts on acme/BillingService, check finds and execute refuses a plan edited to name another as plan-not-the-source, and a source whose own name the naming policy refuses is told to rename it on GitHub first, never to retype --project (#499) adopt-project.py: --org and --project must name the repository the source names, compared without case, so plan refuses --project BillingService for acme/billing-service as adopt-not-the-source instead of writing a plan whose execute acts on acme/BillingService, check finds and execute refuses a plan edited to name another as plan-not-the-source, and a source whose own name the naming policy refuses is told to rename it first, never to retype --project (#499) Oct 13, 2026
@brettheap
brettheap merged commit cf118ff into main Oct 14, 2026
5 checks passed
@brettheap
brettheap deleted the fix/adopt-project-must-be-source-name branch October 14, 2026 00:05
brettheap pushed a commit that referenced this pull request Oct 14, 2026
…and-merged-copies: the handbook footer keeps main's clauses and this PR's #502 clause follows #499's

Ruling 27 refresh round. #513 landed on main as cf118ff and rewrote the
footer's clause list and README.md blob in docs/handbook.html, so this branch
conflicts there and nowhere else (git merge-tree --write-tree 6943280 cf118ff
exits 1 with that one path).

The one hand resolution, docs/handbook.html's footer:
- main's text is kept whole: the #462 clause, then #487, #500 and #499;
- this PR's own clause, "the merged-away verdict, whose row apply drops,
  added (#502)", is added after main's last one (#499), in the same
  "; ... added (#NNN)" form and before " &mdash; a page cannot name the
  commit that contains it";
- only the two lines touched are re-wrapped, to 100 columns;
- README.md blob: this branch's c4f819a
  and main's 6bf94d5 give way to
  ae817c5, the git hash-object of the
  merged README.md (AGENTS.md 490 lines, README.md 1422).

README.md, AGENTS.md and tests/test_shape_doctor.py auto-merged with no
conflict; no other file conflicted.

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 14, 2026
…plan-not-the-source` is its own finding beside this branch's `plan-root-symlink`, after it in `check` and refused before it in `execute`, so a plan that names another repository and keeps a link the shape cannot write through is told each cause once

Merged by sha cf118ff, the one commit
of main past 173c00e. No conflict: the tree is
`git merge-tree --write-tree 3872e3c cf118ff`'s, 0e2ac0b.

The commit that meets this branch's files:
- cf118ff (#499 via #513): `plan` refuses an `--org` and a `--project`
  that name another repository than the one the source names, compared
  without case, as `adopt-not-the-source`, before any pin's digest is
  read or any plan written; `check` finds, and `execute` refuses, a plan
  edited to name another as `plan-not-the-source`; `_check_names` takes
  the source and says to rename it first where the policy refuses its
  own name. Its hunks are in `_checked_plan_inputs`, `cmd_check` after
  `_entry_findings`, a new `_plan_org` that `_repository_urls` now
  reads, `cmd_execute` before `_check_names`, `_check_names` and a new
  section after it, and the module docstring's `plan` paragraph, beside
  this branch's `check` paragraph but not in it.

How it composes with this branch:
- `plan`: this branch adds nothing there. `adopt-not-the-source` is
  refused before any plan is written, whatever links the source keeps,
  in the same words as on `main`.
- `check`: `plan-not-the-source` is appended after `_entry_findings`,
  whose list holds this branch's `plan-root-symlink` (after #166's, before
  #255's), and before the naming policy's findings. A plan edited to name
  another repository that also keeps `.github -> nowhere` or
  `.github -> <absolute path>` in the root gets exactly two findings,
  one `plan-root-symlink` and one `plan-not-the-source`, with an edited
  `org:`, `project:`, both, or a deleted `org:` line; `main` gives the
  second alone.
- `execute`: `plan-not-the-source` is refused right after
  `filter-repo-missing` and before `_check_names`, `_repository_urls` and
  `_refuse_what_check_finds`, which refuses this branch's
  `plan-root-symlink`, and before `adopt-symlinks-off`. So such a plan is
  refused `plan-not-the-source` first, with no leg remote, and once its
  name is the source's, `plan-root-symlink`, again with none.
- `Source(path, repository, branch, commit, work_root, cloned)`: #513
  reads `source.cloned`, `source.repository` and `source.path` only, and
  every `Source(...)` in the test files it changes passes `cloned` by
  keyword or not at all, so the `work_root` this branch puts before
  `cloned` changes nothing for it.

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; the one signature main
changed, `_check_names(..., source=None)`, is called only by main's own
lines; 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 pushed a commit that referenced this pull request Oct 14, 2026
…ck: footer-only refresh under ruling 27

Main brought #515, #507, #517, #510, #513 and #512 since the previous merge base
89c6a6c. The one conflict is the handbook footer in docs/handbook.html, which
#512 rewrote so that its "(#502)" clause is now last.

Resolution: main's footer text kept whole (#498, #462, #487, #500, #499, #502),
this PR's 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)) added after #502's in the same "; ... added
(#NNN)" form and before " &mdash; a page cannot name the commit", the touched
lines re-wrapped to 100 columns, and "README.md blob" set from
ae817c5 (main's) to
f3b378d, the `git hash-object README.md` of
the merged README.md (this branch's own was 43316b9).
No other file conflicted and nothing else was hand-resolved; README.md stays at
1422 lines and AGENTS.md at 490.

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 14, 2026
Merged by sha, 8beb3d9, fetched at
03:08Z, on top of 9efe95a. It brings three commits since 89c6a6c, oldest
first: 173c00e (#510 for #500: `openRepoShape --doctor`), cf118ff (#513
for #499: `--org` and `--project` must name the repository the source
names) and 8beb3d9 (#512 for #501 and #502: update-shape.py,
shape-doctor.py and `collision_follow_up` in
scripts/shape_materialize.py).

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

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

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

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

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

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

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

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

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

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 14, 2026
…epo 2.47.0, so the adoption test files that skip without it run there, and a source that keeps its own root `.gitattributes` gets one marked block of `/<copy> text eol=lf` lines appended to it in the split, so a Windows clone writes the pinned copies at the root with the bytes the pin records; the verification counts that file as the source's bytes followed by exactly that block, and `update-shape.py apply --add` extends it (#471) (#511)

v1.0.0 says that every AGENTS.md procedure, adopt included, works end to
end on Linux, macOS and Windows. `tests-windows` left `git-filter-repo` out
on purpose, so every test that executes an adoption skipped there and the
Windows half of that sentence was a claim no runner had checked.
`tests-windows` now installs it at the ubuntu job's pin, on one line because
PowerShell is that runner's shell, and still runs the whole suite. At #497's
base 23 files skipped without it (the 21 that spell
`shutil.which("git-filter-repo")`, `test_adopt_filter_repo_failure.py` and
`test_adopt_git_reads.py`), 178 of their 1731 items. The workflow's comments
and seven test docstrings that named the Windows job as the one without it
now say what stays true. The tool adds about 8 to 11 minutes to the Windows
job. Its first run there (probe run 38312924968) passed 1696, skipped 34
(exotic names, tracked symlinks, `#!` stubs and test helpers), and failed
the one case the rest of this change fixes.

Adoption's first run on a Windows runner failed one ordinary case: a source
that tracks its own root `.gitattributes`. The shape's `* text=auto eol=lf`
went beside it at `shape/.gitattributes`, where it governs only `shape/`, so
the nine copies the pin names at their own paths answered to the source's
rule, a Windows checkout wrote them with CRLF, and `validate-pins.py`, which
digests the bytes on disk (#51), reported shape-copy-drift on each. Under
Git for Windows' default `core.autocrlf=true` any root `.gitattributes` but
the shape's does it. A root-level file takes attributes from the root file
alone, so no file beside it can fix that.

Brett Heap ruled at 2026-10-12T22:48Z that the split appends a marked block
to the source's own file, that the verification accounts for it, and that
`update-shape --add` extends it.

The materializer spells the block once: a comment naming openRepoShape and
#471, then `/<path> text eol=lf` for each pinned copy outside `shape/`, in
the pin's order, appended after the source's bytes when the shape's
`.gitattributes` collided, and never through a symlink. `execute` prints it
and lists its lines in the split commit's message, and its verification
counts the source's file as landed in the root only when the blob there is
exactly the source's bytes followed by that block, recomputed from the
split's pin; anything else is still `adopt-lost`. `update-shape apply --add`
names each added copy in the block, through the ledger, and commits the file
with the copies; a root with no block is left alone. The follow-up says what
the block protects. A source with no root `.gitattributes` keeps today's
behaviour.

The two `git-filter-repo` failure tests whose stub is a `#!` script now run
on Windows, where git starts the stub with its own `sh`. The one that
follows the failure notice compares the leg repositories it reads there with
their `spelled` form, because the notice escapes each backslash of a Windows
path (#212). The test that a `.gitattributes` symlink is not written through
skips on Windows, as the adoption suites' other disk-symlink tests do. The
`update-shape --add` fixture's check that no copy list names a stripped file
reads list entries, not every string in the materializer.

The docs are corrected here (Brett Heap, 2026-10-13T11:30Z), each file
keeping its line count. README.md's "Ambiguous is an answer, and no source
line is overwritten" (was "nothing is overwritten") says a kept root
`.gitattributes` gets the block appended and is verified as the source's
bytes plus the block. Its "Why `.gitattributes` is one of the copies" names
the block as what carries the rule in such a root. AGENTS.md step 7 states
the same exception, and the no-code variant defers to step 7 rather than
restating "by blob sha". docs/handbook.html mirrors both paragraphs, names
#471 in its footer after main's clauses, the last of them #502's (#504
and #494, which landed since, add none), and recomputes its README.md blob
over the merged README.md.

Review round 1. A `.gitattributes` git tracks as a symlink gets no block
even where Git for Windows' default `core.symlinks=false` checks it out as a
plain file: the materializer asks git's index, not only the disk. Before,
the block went into the link, and the adoption failed `adopt-lost` after
both legs were pushed. The plan reads the link from the source tree. Its
follow-up says nothing was appended and git reads no `.gitattributes` that
is a link, and it lists the lines a regular file in its place needs. The
verification reads the shape's pin through the split's links, as the
shape-file check does, so a source keeping `contracts -> meta` in the root
verifies. `--add` extends the last block. README.md's `--add` sentence
names the block, at an unchanged line count.

Merged with main at d41cc29, at 89c6a6c and at 8beb3d9. Each of those
merges' one conflict was the handbook footer, which keeps main's clauses
whole and adds this one after the last, now #502's. Two of what main brought
meet this change, #513 and #512. #513's `adopt-not-the-source` runs in
`plan` before the tree is read, so before the block's follow-up is
predicted, and its `plan-not-the-source` is `check`'s finding and
`execute`'s first refusal, before the split commit appends the block: a
source with a root `.gitattributes` and a wrong `--project` is refused with
no plan written (probed). #512's `merged-away` takes the `.gitattributes`
beside-copy as it takes any other: left in place it is `unchanged`; merged
into the root file and removed, `check` calls its row `merged-away` and
`apply` drops the row, leaving the block and the root file as they were, and
the gates and the doctor go green (probed). The verification of the source's
bytes plus the block runs inside `execute` alone, so it never meets that
row. A `.gitattributes` collision's follow-up is this change's own and
returned before #512's sentence about the row, which every other pinned
collision's follow-up carries; the fold below adds it. The rest do not meet
it. #402 rewrote the doctor's NEXT lines, in a file this change does not
touch. #462 rewrote only the two ruleset hint constants in
`scripts/shape_materialize.py`, and the scaffold's call to
`materialize_assembly_root` passes no `collision_dir`, so it never reaches
the block. #286's `update-rewrite-symlink` and `--add`'s extension of the
block never meet either: a `.gitattributes` that is a symlink is left alone
before the ledger is asked to write it, so `apply --add` exits 0 with the
link and its target untouched (probed over a live and a dangling link). #515
spells the manifest's `neutral_product_pins:` entries, which the block never
reaches, and its tests sit in `tests/test_adopt_pins.py` beside this
change's fixture fix and merged clean. #507 is `family.py` and the quoting
of the commands it prints, #517 is `scaffold-project.py` and #510 is the
installed doctor's wrapper: none is a file this change touches beyond the
docs.

Merged once more with main at b0826e0, the squash of #504 (#183, #178),
which meets this change in `adopt-project.py` and
`scripts/shape_materialize.py`; the handbook footer did not conflict, because
#504 does not touch it. Three hunks conflicted and each keeps both sides: the
`from shape_materialize import (...)` list, `materialize_assembly_root`'s
docstring, and its tail, where #504's `spell` is passed through and the
`.gitattributes` block is appended after it. The two do not contradict each
other, asked of real adoptions through `plan`, `check` and `execute`. (a)
`materialize-attributes-path` needs no `spell`: only `eol_line` raises it,
over the shape's own copy-list names, which `root_key` never resolves through
a link, and `spell` is for the names a source's symlinks lead to. (b) Where
#504's `check` passes, a root `.gitattributes` that is a symlink adopts as it
did, to the blob: the link kept, no block, the shape's copy beside it. Where
`check` now refuses it, `plan-root-symlink` before any leg (a dangling link,
one to an absolute path with nothing there, a climb above the root, a
`.gitattributes` under a link whose target already holds one), this change
alone had written the copy through the link or failed after both legs were
pushed. (c) The rest of #504's diff does not meet the block:
`_refuse_unwritable` runs inside `place`, before the block is appended, and
the block never goes through `place` or through a link. This change's four
imports from `shape_materialize` sit in a statement of their own, so a change
adding names to the shared list merges there without a hand resolution.

Merged again with main at 3121945, the squash of #494 (#370), and
it was clean: no file conflicted, and the handbook footer was not touched,
because #494 changes neither README.md nor docs/handbook.html. It meets this
change in `adopt-project.py` alone, in `_failed_loudly`, which this change
does not touch; `Refusal` now carries no remediation unless its caller names
one, and the one refusal this change adds, `materialize-attributes-path`,
names its own, so it prints as it did.

- The follow-up for a source's own `.gitattributes` now says what #512 (#502)
  says of every other pinned copy, after its own condition: merge the rest of
  `shape/.gitattributes` into the root file only if the whole project should
  check out with LF, and only then remove it, after which `update-shape.py
  check` reports its pin row `merged-away` and `apply` drops the row. The
  link variant says nothing of the row, because it never sends anyone to merge
  or remove the copy, which stays pinned. A new test follows the flow through
  a real adoption: merge the copy into the root file, remove it, paste
  `check`'s NEXT, and the row goes while the root file stays as merged.

The first Windows run of the adoption suites ended in 12 errors, all at
the module fixture of `tests/test_adopt_pins.py`: it deleted a
`shutil.copytree` copy of a repository with `shutil.rmtree`, and Windows
refuses to unlink the read-only objects git writes
(`PermissionError: [WinError 5]`). It now deletes it with `rmtree` from
`tests/conftest.py`, which clears the read-only bit and retries, as the other
suites do.

This pull request supersedes #497: its two reviewed commits (`a528006`, the
install, the comments and seven docstrings; `a8b5064`, a strict Windows
xfail on the case fixed here) are merged in, and the xfail is removed,
because that case passes on Windows over the fix and a strict xfail that
passes fails.

Closes #471

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 14, 2026
…-yaml-keywords: clean, and its tree is 88c22f0, the tree of the scratch merge a9439715 every Phase A run was made on

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

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

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 14, 2026
…5 cites the six pull requests merged since #402, section 6 drops the `--help` verdict line #510 fixed and AT-family N2, which #501's ruling decides, and adds 28 checklist lines from the parents, and section 7 lists the 3 open milestone issues (#472)

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

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

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

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

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

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

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

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

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

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 14, 2026
… keyword, so `--spec-path null`, `--id null` and `--tracking-branch true` adopt as typed, and `plan`, `check` and `execute` refuse, before anything exists, a value the project's own loader would read back as something else from the line `execute` writes (#518)

`adopt-project.py`'s plan writer, `y()`, now writes a string bare only where the plan's own YAML loader (`scripts/repo_shape.py`) reads it back as that same string, after `key: ` and as a `- ` item. Anything else is quoted with `double_quoted`. So `null`, `true` and their other spellings are quoted. `yes`, `on` and `.inf` stay bare, because this loader reads them as text. On `main` the plan read such a value back as no value or a boolean:
- `--spec-path null` mounted the leg at `spec`;
- `--tracking-branch null` put both legs on `main`;
- `--id null` made the id `northwind`;
- `--tracking-branch true`, `--spec-path true`, `--id true`, `--pin null@<commit>` and `--elected-by null` were refused at `check` about a value nobody typed.

Each of these now adopts and bootstraps as typed. An ordinary adoption is byte for byte `main`'s, for three sets of flags.

The same rule guards what `execute` writes into the project's own YAML. `plan` (`adopt-manifest-misread`, before the plan is written), `check` (`plan-manifest-misread`) and `execute` (before any leg exists) refuse a value the project's own loader would read back as something else from the line `execute` writes. The gate builds that line with the materializer's own `render`, from the template or from `_template_table`, and asks the loader. It keeps no list of words. It asks the project's own `validate-manifest.py` first. It names a value only where that validator makes no finding about it and `execute` writes it in every mode, so the two never both name one value. On `main`'s shipped templates it names nothing, because #259 and #514 quote the lines it asks about; #516 quotes a pin file's own `product:`. It stays as a backstop, as #314's scaffold twin does: a line written bare again is refused with no edit to the rule.

An explicit null leg path in a plan (`spec_path: null`, `~` or nothing) is now `plan-bad-leg-path` instead of the default `spec`. The leg-path not-text sentence asks for the path meant, not "so quote it".

The gate composes with #513 and #504. Its finding comes after the mount paths, before #504's `plan-root-symlink`, and #513's `plan-not-the-source` follows both. A plan with all three causes, probed with the pins' list written bare, gets one finding each at `check`. `plan` and `execute` refuse #513's cause first. #511 adds no finding: on a source that keeps its own `.gitattributes`, #181's values adopt with #511's line-ending block appended, and a plan this gate refuses is refused before that file is touched.

What this leaves to `main`:
- A hand-edited plan value its loader reads as a number or a boolean (`id: 007`, `tracking_branch: true` beside a source branch `True`) is written as `str` of it, as on `main`.
- The scaffold's twin of the gate (#314) is a second function. Making them one is a follow-up.

tests/test_adopt_plan_yaml_quoting.py adds 524 tests, all through the real loader:
- the round trip of every keyword spelling, number, date, indicator and escape candidate, every string over two small alphabets, and 4000 seeded strings;
- 20 values read back as given through `plan`, `check` and `execute`;
- the gate's refusals, asked in this process over copies of the templates and of the pins' list written bare again.

The revert table, measured at 9efe95a, names the tests each change's revert fails. Two guards fail none alone and 15 together, which the PR discloses. At 7d6e13c the targeted sets pass with and without `git-filter-repo`, the rest skipped: 1928 and 1833 tests in 14 files, 3869 and 3686 in 65 more, 963 and 953 in 11, 364 in 10, 248 and 223 in five more of `main`'s test files that changed since 89c6a6c, and 37 in #511's new one.

Reviews:
- Lane 2's P2 is fixed by the gate.
- review181's adversarial pass found 7 NITs.
- rv181f's re-verifies found F2, F2b, G1 (a BLOCKER), G2 and H1, each fixed.
- rv181g found B1 (a BLOCKER, fixed in 9efe95a), and M1, the `with_origin` test fix that #513's merge needed (6c9e585).
- Codex found no major issue at 9efe95a, and SonarCloud's nine new smells there are fixed.

`main` was merged in thirty-one times up to 7d6e13c (`main` 5ed3e5d), never rebased, each conflict resolved by keeping both sides. CI is green on Linux, macOS and Windows at 7d6e13c, which has `main` 5ed3e5d merged in.

Closes #181

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