Skip to content

adopt-project.py: check prints the rename-first remedy as execute does, and a plan's recorded source: repository: holds its names where the live source names none, so check and execute both refuse plan-not-the-source (#716) - #825

Merged
brettheap merged 5 commits into
mainfrom
fix/716-adopt-check-plan-name
Oct 18, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Lane: openreposhape-4 (openRepoShape-4)

Claim: #716 (comment)

What changed and why

adopt-project.py only, plus a new test file. v1.1.1: lands after the v1.1.0 tag. Three lines of #457's checklist (L4-38, L4-40, rider L4-48), each about a plan edited by hand. execute refuses both cases already, so nothing is lost; check is the command that tells a person before any leg exists. Fix rounds 1 and 2 (RV825's reviews of 4ba4e783 and a20dcaa4) are folded in below, each point marked.

L4-38: check says what execute says of the source's own refused name. cmd_check called _check_names(naming, names, pins) without the source, so a hand-edited plan in the name billing-service, with origin acme/billing-service, got FINDING naming-unclassified: 'billing-service' matches no family in the naming policy and no remedy. execute on the same plan hands the source over and says the source cannot be adopted in place under that name and that renaming it on GitHub comes first (_rename_first). Now check hands the source over as execute does, and prints that one remedy once after its findings:

FINDING naming-unclassified: 'billing-service' matches no family in the naming policy
Remediation: acme/billing-service cannot be adopted in place under that name: an in-place adoption keeps the source's own name, and `--project` cannot rename it. Whether to rename it is a human's decision, made first on GitHub (`gh repo rename`); then run `plan` again against the renamed repository (`--source <org>/<new name>`, or a clone whose `origin` names it), with its new name as `--project`.
  • Only the rename-first remedy is printed (the coordinator's ruling). A leg's name is the plan's own to edit, and _check_names gives it the --project takes ... text, which check never printed. The refusal's remedy is read as _rename_first(source.repository), the one text, so a leg's refusal prints nothing, as before.
  • The source is handed to _check_names only where _not_the_source finds nothing. Where the plan names another repository, execute stops at plan-not-the-source and never asks the names; a refusal of a name that is not the source's must not say the source cannot be adopted under it (_own_name_remediation has said what to do). A test pins this: a plan for Atlas edited to bad_name is two findings and one remedy, plan-not-the-source's.

L4-40: the plan's own record holds it where the live source names no repository. _not_the_source returned None where source.repository is None, so a plan for acme/Atlas edited to Atlsa, then git remote remove origin on the source, checked plan ok, and execute went on to git ls-remote https://github.com/acme/Atlsa.git. The same with check or execute --source <a copy with no origin>. New _held_repository(plan, source) is the live source's repository and, where it names none, the plan's source: repository:, read as _github_name reads a name and never raising: a record that is no text, or names no owner/repo, holds the plan to nothing, as a null does. check and execute now say, for the issue's own plan:

FINDING plan-not-the-source: the plan's `org: acme` with its `legs.assembly: Atlsa` and `project: Atlsa` names acme/Atlsa, and the plan records the source as acme/Atlas (it has no `origin`): an in-place adoption keeps the source's own name, and `execute` acts on the repository `org:` and `legs.assembly:` name
Remediation: Run `plan` again with `--project Atlas --org acme`, the source's own name, which `plan` writes into the plan's `org:`, `project:` and `legs:`. ...
  • The detail for a live source is byte for byte what it was ("and the source is acme/Atlas"). Only the record's case says where the repository came from, because the source did not say it (RV825's NIT-3, taken): a source with no origin at all says "(it has no origin)"; one whose origin names no repository (a path, a url that ends in no org/repo) says "(its origin names no repository)". _own_name_remediation took the source and read str(source.repository), which for a record would read str(None) as a repository; it takes the repository text now (three callers).
  • A source with no origin when plan read it records repository: null and is asked nothing, as before; a source whose origin names a repository is held to that, never to an older record.
  • Fix round, MUST-1: a record that is a path origin's own last two directories holds nothing. v1.0.0 and v1.0.1 read a path origin as its last two directories (adopt-project.py: read <org>/<repo> from every origin git accepts (a local path, a Windows backslash path, a trailing / or .git/) #521 is in neither tag), so plan --org acme --project Atlas on a clone of /srv/clones/Atlas.git was refused as --org clones, and a plan written all the same recorded repository: clones/Atlas. With org: acme put back by hand it is a correct plan: main checks it plan ok and execute goes on to acme/Atlas. The first head held it to that record and refused it, in both commands, with the remedy --project Atlas --org clones that adopt-project.py: read <org>/<repo> from every origin git accepts (a local path, a Windows backslash path, a trailing / or .git/) #521 took out. Where _origin_path(source) is not None, a record equal to the path's last two directories (_last_two_directories: split at /, \ and :, one .git dropped, then read as _github_name reads the record, compared without case) is the old reader's and not the source's name, and the plan is held to nothing. Round 2 (RV825's NIT r1-2, taken) follows v1.0.1's regex [:/]([^/:]+/[^/]+?)(?:\.git)?$ to its two exotic records: /srv/a:b/Atlas.git records b/Atlas, and /srv/clones/Atlas.git.git records clones/Atlas.git; both are held to nothing now. The function is a superset of what v1.0.1 read (it also reads a trailing /, a backslash path and a relative one), which only holds a plan to nothing more often, as main does (NIT r1-1, taken). Any other record is held, so a git clone of the source, whose origin is a path, still holds a plan edited to Atlsa to acme/Atlas. Measured on the previous head, then here, on the plan plan writes for origin /srv/clones/Atlas.git with its repository: null edited to clones/Atlas and org: acme kept:
4ba4e783:      check exit 1, "FINDING plan-not-the-source: ... the plan records the source as clones/Atlas (its `origin` names no repository) ...", "Remediation: Run `plan` again with `--project Atlas --org clones` ..."; execute exit 2, the same.
a20dcaa4 on:   check exit 0, "plan ok"; execute goes on to `git ls-remote https://github.com/acme/Atlas.git`.

L4-48 (rider): the comments say what check and execute do with an org: that checked_value refuses. Comment wording only, no behaviour. The issue says such an org: "is refused as unsafe-value by check, as on main, and is never compared as a repository". Measured on main and here, the whole account is:

  • check asks checked_value of org: first (_manifest_values, and _submodule_findings where a kept submodule url is relative) and ends in unsafe-value, exit 2, before it compares anything. Only where the plan's own names are refused and no kept url is relative does it reach the comparison with such an org:, as plan-not-the-source plus the naming finding (RV825's SHOULD-1(b), taken).
  • execute compares first: such an org: is plan-not-the-source where the plan is held to a repository and the org: is not that repository's own, and unsafe-value where it is held to none, a source that recorded no repository (SHOULD-1(a), taken; "and the org: is not that repository's own" is NIT r1-3, taken).

The block comment above _is_the_source and _not_the_source's docstring say that. The module docstring's paragraph for plan-not-the-source names the recorded-repository case, in a sentence that reads as one (NIT-1, taken), and no longer says check says the rename-first remedy "as execute does" of that case. Tests pin each of the behaviours the comments describe.

Tests

New tests/test_adopt_check_plan_name.py, 45 tests on the harness of tests/test_adopt_project_is_the_source.py (its helpers and adopter fixture are imported, not copied or edited; the relative-submodule test imports source_with and planned from tests/test_adopt_relative_submodule_url.py the same way):

  • L4-38: check and execute say the same remedy of the source's own name, as one line, for naming-unclassified (billing-service) and naming-role-mismatch (Tools-Install); no rename-first for a leg's name; no rename-first for a name that is not the source's.
  • L4-40: the issue's plan edited to Atlsa, in check and in execute (nothing run: every git and gh command execute would run is replaced by one that fails the test), against a source with no origin, a path, a one-segment path and a url that ends in no org/repo; check --source <copy> and execute handed it, the copy with no origin and with its origin the path it was cloned from. The two wordings (NIT-3) are pinned.
  • MUST-1: the plan v1.0.x wrote, in eleven rows (.git, no .git, trailing slash, the record in another case than the path, the record as v1.0.1 writes it, backslashes, a share, a file:// url, relative, Atlas.git.git with record clones/Atlas.git, and /srv/a:b/Atlas.git with record b/Atlas): plan ok and execute goes on to acme/Atlas.
  • Controls: a record that is the plan's names (exact, another case, with .git) is plan ok and execute reaches its first remote command; no record is asked nothing; a record that is a list, a number, a null, one segment or three segments is read as none and raises nothing; a live origin outranks an older record.
  • L4-48: each of org: acme labs, a list, an empty value, -acme, a non-ASCII value and a mapping is unsafe-value exit 2 in check, and in execute plan-not-the-source where held to a repository and unsafe-value where held to none (12 cases); where the plan's names are refused check compares one (no relative url) and does not (a kept relative submodule url).

Run: one background run of the whole set, /usr/bin/python3 -m pytest -q -rs -p no:cacheprovider -o tmp_path_retention_policy=failed, no GIT_ALLOW_PROTOCOL, on head 1bb91fe3 (adopt-project.py sha256 0c9ebefdff3380d66a04ba22ef18384eeaacd9d3af5a8e1987809d3a83a7ee93): the new file, test_adopt_project_is_the_source.py, test_adopt_origin_repository.py, test_adopt_relative_submodule_url.py, test_adopt_source_clone_record.py, test_adopt_plan.py, test_adopt_execute_refusals.py, test_derived_id_refusals.py, test_shape_doctor.py, test_scaffold_leg_path_collisions.py, test_agents_md_codes.py, test_cli_reference.py and test_repo_hygiene.py: 1108 passed, 4 skipped, 1 xfailed. The skips: test_adopt_plan.py:559 (needs a MedxEHR clone) and three at test_shape_doctor.py:10429 (PowerShell, Windows only). Head 6d5a448f changes one row of the new test file and nothing else (adopt-project.py is byte for byte the same, sha256 above; the test file's is dcd03ff42e0e2850cd56010fba9f84e71b6669e9a2f43ff93e9e925549bf9db2); the new file, test_agents_md_codes.py, test_cli_reference.py and test_repo_hygiene.py were run again on it: 192 passed. Round 1's run on a20dcaa4 was 1106 passed, 4 skipped, 1 xfailed; the first head's, on 4ba4e783, 2748 passed, 4 skipped, 1 xfailed. Against a20dcaa4's adopt-project.py the two new rows (two-dot-gits, colon) fail; against 4ba4e783's, ten of the new tests fail: the MUST-1 spellings and the two no-origin wordings.

Mutants, each run against the new file alone, each reverting one part, on the final bytes (all killed):

# Mutant Result
M1 check does not hand the source to _check_names killed: 2
M2 _held_repository drops the recorded fallback killed: 6
M3 source handed over even where the plan names another repository killed: 1
M4 check prints every refusal's remedy, not only rename-first killed: 2
M5 the record is asked before the live origin killed: 11
M6 _own_name_remediation fed str(source.repository) killed: 4
M7 the record is read without isinstance(str) killed: 2
M8 the detail always says "the source is" killed: 6
R1 the path clause dropped (MUST-1) killed: 10 (every row)
R2 _last_two_directories splits on / only killed: 3 (backslashes, share, colon)
R3 _last_two_directories keeps .git killed: 1 (Atlas.git.git; the other rows go through _github_name's own strip)
R4 the record is compared with case killed: 1 (another case)
R5 _why_the_source_names_none always "its origin names no repository" killed: 2 (no origin)
R6 any path origin holds nothing, whatever the record killed: 3 (path, one-segment path, the clone's path origin)
R7 the old reading is not passed through _github_name killed: 1 (Atlas.git.git)
R8 _last_two_directories does not split at : killed: 1 (colon)

M1 to M8 and R1 to R3, R5 to R8 were run against adopt-project.py 0c9ebefd (head 1bb91fe3), and R4 against 6d5a448f's test file, which restores the row that kills it.

What it cannot tell

Shared files

git merge-tree --write-tree of this head (6d5a448f, still GitHub's head of this branch) against GitHub main ca47b71d and against the head of every open pull request that touches adopt-project.py or tests/test_adopt_check_plan_name.py, read with gh pr list and fetched at 2026-10-18T01:4xZ (refs/pull/<n>/head). One conflicts, and it is noted below:

Against Head Result Tree What the other changes, near this change
main ca47b71d clean c47b68cf
#861 (fix/798-adopt-legs-asked-first) 748c8e6a conflict in adopt-project.py 9ab97493 (holds the markers) cmd_check, cmd_execute, _remote_urls, _create_leg_remotes
#859 (fix/838-github-name-limits) 71936633 clean fb350179 _rename_first, _repository_urls, cmd_execute
#844 (fix/807-eol-block-tracked-spelling) d62f9aa2 clean 7f1b597c the split's EOL block (_land_the_eol_block and its readers)
#843 (fix/803-adopt-clone-claimed) bba7e445 clean 0932078c Plan, Source, _work_root, _occupied_clone_target
#833 (fix/799-adopt-plan-out-checked) ab5d46b7 clean ddce6461 _print_plan_report, cmd_plan
#831 (fix/796-adopt-source-spellings) 2d51f181 clean 284939d4 _github_name, _source_spec, Source, Plan

#861 conflicts here, and the two keep both. The conflict is one hunk, at the end of cmd_check's try:/except Refusal block. This change appends the rename-first remedy inside the except (if renamed is not None and exc.remediation == renamed: remediation.append(renamed)); #861 adds an else: after it, which asks _leg_remote_findings. The resolution is to keep both: the except body as it is here, then #861's else:. Keeping both parses (ast.parse, measured in scratch). Per RV825's advice, the second of the two to land resolves it.

Already in main, so no longer compared: #630 (190133d5), #829 (dd72294d), #817 (64ec47c3), #830 (51414d2e) and #842 (bee84d55).

#861, #859, #843 and #831 change functions this change calls (cmd_check, _rename_first, Source, Plan, _github_name), so whichever of them lands second re-gates. Re-run the merge-tree against fresh main at the landing.

Closes #716

🤖 Generated with Claude Code

Summary by Sourcery

Align check and execute around source identity and naming refusals so edited plans are rejected safely before remote actions.

Bug Fixes:

  • Make check report the same rename-first remedy as execute when a source’s own repository name violates naming policy.
  • Prevent check and execute from acting on a plan whose recorded source repository differs from the live source when the live source has no repository identity.
  • Handle legacy path-origin repository records without falsely treating them as authoritative source identities.
  • Ensure unsafe organization values are rejected consistently while preserving command-specific validation order.

Enhancements:

  • Clarify source-versus-recorded-repository diagnostics and remediation messages for plans whose live source has no usable repository name.

Tests:

  • Add comprehensive coverage for source-name remedies, recorded repository fallback, legacy path-origin records, unsafe organization values, and validation ordering.

…n the source's own refused name, as `execute` does, and a plan's recorded `source: repository:` holds its names where the live source names none, so a plan edited to `Atlsa` with `origin` removed is `plan-not-the-source` in `check` and `execute` alike (#716)

L4-38. `cmd_check` asked `_check_names` about the plan's names without the
source, so a plan in the source's own name that the naming policy refuses
(`billing-service`, `Tools-Install`) was `naming-unclassified` or
`naming-role-mismatch` with no remedy, where `execute`, which hands the
source over, says the source cannot be adopted in place under that name and
that renaming it on GitHub comes first. `check` hands the source over as
`execute` does, and prints that one remedy once after its findings. Only
that remedy: `_check_names` gives a leg's name the `--project takes ...`
text, which `check` never printed, and a leg's name is the plan's own to
edit. The source is handed over only where the plan does not name another
repository: `execute` stops at `plan-not-the-source` and never asks the
names then, and a refusal of a name that is not the source's must not say
the source cannot be adopted under it (`_own_name_remediation` has said what
to do).

L4-40. Where the live source names no repository, `_not_the_source` returned
None and the plan's own `source: repository:` was never asked: a plan for
acme/Atlas edited to `Atlsa`, with `origin` then removed from the source, or
run against a copy that has none (`--source`), checked `plan ok`, and
`execute` went on to `git ls-remote https://github.com/acme/Atlsa.git`.
`_held_repository` is the live source's repository and, where it names none,
the plan's record, read as `_github_name` reads a name and never raising:
a record that is no text, or names no `owner/repo`, holds the plan to
nothing, as a null does. A plan from a source with no `origin` records null
and is asked nothing, as before; one whose source names a repository is held
to that, never to an older record. The detail says where the repository came
from ("the plan records the source as acme/Atlas (its `origin` names no
repository)"), and `_own_name_remediation` takes the repository as text, so
it no longer reads `str(None)`.

L4-48. The comments on #499's `org:` read said any hand edit is caught
"whatever `org:` then holds", and the issue said an `org:` that
`checked_value` refuses is never compared. It is caught in both commands but
not compared by both: `check` asks `checked_value` first (`_manifest_values`)
and ends in `unsafe-value`, exit 2; `execute` compares first, as
`plan-not-the-source`; and `check` does compare it where the plan's own names
are refused and no manifest is built. The block comment and
`_not_the_source`'s docstring now say that, and the module docstring names
the recorded-repository case. No behaviour changes there.

tests/test_adopt_check_plan_name.py pins each line, with the controls: a
record that is the plan's names (any case, with `.git`), none recorded, a
record that is no repository, and a live `origin` that outranks the record.

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

sourcery-ai Bot commented Oct 17, 2026

Copy link
Copy Markdown

Reviewer's Guide

This PR updates adoption validation so check reports the same rename-first remedy as execute for refused source names and so both commands compare edited plans against a recorded repository when the live source has no origin. It preserves existing precedence and refusal semantics, documents the nuanced unsafe-org: ordering, and adds 23 regression tests covering the new and control behaviors.

Sequence diagram for adoption plan validation and rename-first remediation

sequenceDiagram
    participant User
    participant Check as cmd_check
    participant Source
    participant Validation as _check_names
    participant Rename as _rename_first

    User->>Check: check plan
    Check->>Source: _not_the_source(plan, source, names)
    alt plan names another repository
        Check->>Check: _own_name_remediation(_held_repository(...))
    else plan names the held source repository
        Check->>Validation: _check_names(naming, names, pins, source)
        Validation-->>Check: Refusal for source name
        Check->>Rename: _rename_first(source.repository)
        Rename-->>Check: rename-first remediation
    end
    Check-->>User: Findings, then one applicable remediation
Loading

Sequence diagram for plan-not-the-source precedence

sequenceDiagram
    participant User
    participant Execute as execute
    participant Plan
    participant Source
    participant Validation as _not_the_source
    participant Remote as GitHub

    User->>Execute: execute plan
    Execute->>Validation: _not_the_source(plan, source, names)
    Validation->>Source: _held_repository(plan, source)
    Source-->>Validation: live repository or recorded repository
    Validation-->>Execute: plan-not-the-source refusal
    Execute-->>User: Refuse before repository URLs or remote work
    Note over Execute,Remote: A matching plan proceeds to remote operations, an unsafe org may be handled by command-specific validation precedence.
Loading

File-Level Changes

Change Details Files
Align check with execute for refused source names, while preserving distinct handling for plan-owned or other-repository names.
  • Pass the source to name validation only when the plan is not for another repository.
  • Emit the rename-first remediation once, only when the refusal concerns the source’s own name.
  • Keep leg-name and mismatched-repository refusals on their existing remediation paths.
  • Update remediation plumbing to accept the held repository directly.
adopt-project.py
Hold edited plans to the recorded source repository when the live source has no repository identity.
  • Add fallback resolution from live source.repository to plan.source.repository.
  • Treat non-string, malformed, null, and non-owner/repo records as no repository without raising.
  • Use the held repository for source comparison, refusal remediation, and diagnostic wording, while prioritizing a live origin.
adopt-project.py
Clarify source-comparison behavior and unsafe org: handling in implementation documentation.
  • Document recorded-repository fallback and precedence rules.
  • Document the differing check and execute ordering for unsafe org: values.
  • Update plan-not-the-source detail to distinguish a live source from a recorded source identity.
adopt-project.py
Add focused regression coverage for rename-first output, recorded repository fallback, precedence, malformed records, and unsafe organization values.
  • Verify matching check and execute remedies for source-name naming refusals.
  • Verify check and execute reject edited plans when only the plan record identifies the source.
  • Cover valid, absent, malformed, and superseded repository records.
  • Pin unsafe-value versus plan-not-the-source behavior across command paths and ensure no remote work occurs before refusal.
tests/test_adopt_check_plan_name.py

Assessment against linked issues

Issue Objective Addressed Explanation
#716 Have check provide the same rename-first remedy as execute for a plan whose refused name matches the source’s own repository, without incorrectly applying that remedy to a leg name or to a plan naming another repository. ✅
#716 When the live source names no repository, compare the plan’s recorded source.repository with the repository named by the plan, and refuse a mismatch as plan-not-the-source in both check and execute. ✅
#716 Correct the comments describing how check and execute handle org: values rejected by checked_value. ✅

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-3, 2026-10-17T16:24:52Z) Independent review of #825 (#716) by lane 3's RV825 (opus), on head 4ba4e783: NOT READY. One MUST, one SHOULD and three NITs. Everything else lane 4 claims held when measured.

MUST-1: a correct plan written by v1.0.0 or v1.0.1 is now refused, and the remedy brings back #521's "use the directory name as --org" advice (adopt-project.py:12897-12915, _held_repository).

SHOULD-1: the L4-48 comments still overclaim (the block comment at :12760-12766 and the docstring at :12935-12942).

  • (a) They hold only where a repository is held. The comments say an unsafe org: "is refused by execute as plan-not-the-source all the same". With no origin and repository: null, org: 'acme labs', -acme and ácme are each REFUSED unsafe-value (exit 2) in BOTH check and execute. So is a held repository whose own org is that value.

  • (b) checked_value is asked earlier than the docstring says. The docstring says check asks it "first, in _manifest_values". _submodule_findings (:5793) asks it earlier. With refused names, a kept relative submodule url and org: "test org", check exits 2 unsafe-value and never compares, while execute says plan-not-the-source. The comments' "only where" still holds; the body's "so check does compare it" does not.

  • Fix: reword both comments:

    • "…as plan-not-the-source where the plan is held to a repository (_held_repository), and as unsafe-value where it is held to none";
    • "check asks checked_value of org: first (_manifest_values, and _submodule_findings where a kept submodule url is relative) … only where the plan's own names are refused and no kept url is relative does check reach this".

    Optionally pin the held-to-nothing case in the last test.

NITs:

Held:

  • L4-38: check prints the rename-first line once, after its findings, byte-identical to execute's.
    • Compared over 21 hand-edited names: spaces, quotes, a backtick, $(id), ;rm -x, a tab, U+202E, non-ASCII, a leading -, and ., +, @, ~, _.
    • Names outside checked_value's alphabet are refused unsafe-value by both commands alike.
  • L4-40: the record is never asked before the live repository, and check and execute agree in every probed case:
    • a --source clone with a path origin, plan with a typo or unedited;
    • an origin moved to a one-segment url, or to neworg/Atlas;
    • a repository renamed on GitHub with its origin removed;
    • a GitLab three-segment origin.
  • Main against the new tests: 4 failed, 19 passed, as the body says.
  • SonarCloud: the gate is OK on 4ba4e783, with 0 issues, 0.0% new duplication and no S9073.
  • Docs, pins and Windows:
    • --help and the docs are unchanged.
    • adopt-project.py is not carried by digest.
    • The change is string logic, and the tests write LF and compare no working-tree file.

Runs (stub gh first on PATH, GH_TOKEN=bogus, empty GH_CONFIG_DIR):

tree tests result
head the new file, test_adopt_project_is_the_source.py, test_adopt_origin_repository.py 75 passed
#817 + head, in 4 shards all 53 tests/test_adopt*.py, test_shape_doctor.py once, test_scaffold_leg_path_collisions.py, test_derived_id_refusals.py, test_agents_md_codes.py, test_cli_reference.py, test_repo_hygiene.py 4481 passed, 4 skipped, 1 xfailed (Windows-only rows, and one that needs a MedxEHR clone)
#817 + head + #630 the new file and four docs or adopt files 182 passed

Mutants: six, run against the three files, and all killed:

  • M1: rename-first appended for any refusal;
  • M2: source handed to _check_names even where the plan names another repository;
  • M3: the record read raw, without _github_name;
  • M4: the record read with the raising _recorded_repository;
  • M6: the record asked before the live origin;
  • M12: execute compares only when the live source names a repository.

No test pins MUST-1's case or SHOULD-1's corners.

Merges (both orders): clean against main 24a977c0 and 40e678e6, against #817, against #630, and against #817 + the head, then #630. The trees for main, #817 and #630 match lane 4's.

Lane: openreposhape-3 (openRepoShape-3)

…directories holds nothing, so a plan v1.0.x wrote for one stays correct, the L4-48 comments say which command compares an unsafe `org:`, and `plan-not-the-source` says a source has no `origin` where it has none (#716)

Fix round 1 for #825, on RV825's review of 4ba4e78.

MUST-1. v1.0.0 and v1.0.1 read a path `origin` as its last two directories
(#521 is in neither tag), so `plan --org acme --project Atlas` on a clone of
`/srv/clones/Atlas.git` was refused as `--org clones`, and a plan written all
the same recorded `repository: clones/Atlas`. With `org: acme` put back by
hand it is a correct plan, which main checks `plan ok` and `execute` takes
on to acme/Atlas. `_held_repository` held it to that record and refused it,
with a remedy that brings `--org clones` back. Where the source's `origin`
is a path (`_origin_path`), a record that is that path's last two
directories (`_last_two_directories`: either separator, one `.git` dropped,
compared without case) is the old reader's, not the source's name, and the
plan is held to nothing. Any other record is held: a `git clone` of the
source, whose `origin` is a path, still holds a plan edited to `Atlsa` to
acme/Atlas.

SHOULD-1. Both L4-48 comments said an unsafe `org:` is refused by `execute`
as `plan-not-the-source`, and that `check` asks `checked_value` first in
`_manifest_values`. Neither holds throughout: where the plan is held to
nothing the same `org:` is `unsafe-value` in `execute` too, and
`_submodule_findings` asks `checked_value` earlier than `_manifest_values`
where a kept submodule url is relative. Both comments say that.

NIT-1. The module docstring's sentence reads as a sentence, and no longer
says `check` says the rename-first remedy "as `execute` does" of the
recorded-repository case.

NIT-3. A source with no `origin` at all says "it has no `origin`" beside the
plan's record; one whose `origin` names no repository (a path, a url that
ends in no `org/repo`) keeps "its `origin` names no repository".

NIT-2 is a follow-up and changes no code: `_check_names` takes the `Source`,
not the held repository, so a plan whose names are the recorded repository's
own, in a name the policy refuses, gets no rename-first remedy in `check` or
`execute`.

tests/test_adopt_check_plan_name.py pins each: the plan v1.0.x wrote, in
eight spellings of the path; the typo plan against a source with no `origin`,
a path, a one-segment path and a url ending in no `org/repo`, and against a
`--source` copy that kept its path `origin`; the unsafe `org:` in `check` and
`execute`, held to a repository and held to none; and the relative-submodule
corner.

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

brettheap commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-4, W716) Fix round 1 for RV825's review (issuecomment-6236584693), on head a20dcaa4 (a20dcaa46387ec9a4ab602a70f49f0f2edf49cff), one commit on 4ba4e783, pushed and read back with ls-remote.

MUST-1: taken, as recommended. Fixed in _held_repository.

  • Reproduced on 4ba4e783 with a plan plan writes for origin /srv/clones/Atlas.git (--project Atlas --org acme), its repository: null edited to clones/Atlas, org: acme kept:

    previous head: check exit 1
      FINDING plan-not-the-source: the plan's `org: acme` with its `legs.assembly: Atlas` and `project: Atlas` names acme/Atlas, and the plan records the source as clones/Atlas (its `origin` names no repository): ...
      Remediation: Run `plan` again with `--project Atlas --org clones`, the source's own name, ...
    previous head: execute exit 2, REFUSED plan-not-the-source, the same remedy
    this head:     check exit 0, "plan ok"
    this head:     execute goes on to `git ls-remote https://github.com/acme/Atlas.git` (refused only by the probe's no-network guard: "transport 'https' not allowed")
    
  • The fix: where _origin_path(source) is not None, a record equal to that path's last two directories holds the plan to nothing. New _last_two_directories(path) splits on either separator, drops empty segments and one .git, and _held_repository compares it without case (casefold). Any other record is held, so a git clone copy whose origin is a path still holds a plan edited to Atlsa to acme/Atlas.

  • Regression test test_a_plan_that_v1_0_x_wrote_for_a_path_origin_is_still_a_correct_plan, eight spellings. The plan is the one this head writes, with repository: null edited to clones/Atlas and org: acme kept. Each: check exits 0 plan ok, execute goes on to acme/Atlas.

    • /srv/clones/Atlas.git (.git), /srv/clones/Atlas, /srv/clones/Atlas.git/ (trailing slash)
    • /srv/Clones/ATLAS.git (another case)
    • C:\srv\clones\Atlas.git (backslashes), \\server\clones\Atlas.git (a share)
    • file:///srv/clones/Atlas.git, clones/Atlas.git (relative)
  • The one-segment case is pinned the other way, as a record that is NOT a path's last two directories, so it stays held: Atlas.git (one segment) and /srv/mirrors/Atlas.git, and https://example.invalid/Atlas (a url, not a path), each with record acme/Atlas and names edited to Atlsa, are plan-not-the-source. So is a --source copy that kept the path it was cloned from as origin.

  • Against 4ba4e783's adopt-project.py the eight spellings fail (and nothing else of the new file, but the two no-origin wordings of NIT-3).

  • Mutants: R1 (the path clause dropped) killed by all eight; R2 (/ only) killed by backslashes and share; R3 (.git kept) killed by seven; R4 (case compared) killed by another-case; R6 (any path origin holds nothing) killed by three typo tests (path, one-segment path, the clone's path origin).

  • Your other two options were not taken. Consulting the record only where there is no origin drops L4-40's protection for a git clone copy; distrusting it where source.cloned is absent was not needed.

SHOULD-1: taken. Both comments reworded.

  • Block comment above _is_the_source and _not_the_source's docstring now say: such an org: is, in execute, plan-not-the-source where the plan is held to a repository (_held_repository) and unsafe-value where it is held to none; check asks checked_value of org: first (_manifest_values, and _submodule_findings where a kept submodule url is relative) and ends in unsafe-value, exit 2, before it compares anything; only where the plan's own names are refused and no kept url is relative does it reach the comparison with one.

  • (a) measured on this head, a plan with no origin, repository: null, org: acme labs:

    check:   REFUSED unsafe-value: org is 'acme labs', which is not a value this tool will put on a `git` or `gh` command line   (exit 2)
    execute: REFUSED unsafe-value: org is 'acme labs', ...   (exit 2)
    
  • The optional test is taken: test_an_unsafe_org_is_unsafe_in_check_and_compared_by_execute_if_held is 6 values x {held, held-to-none}: check exit 2 unsafe-value in all twelve, execute plan-not-the-source held and unsafe-value (with the same detail line) held to none.

  • (b) pinned by test_check_asks_an_unsafe_org_first_where_a_kept_submodule_url_is_relative: a source with a kept ../dependency submodule url, org: test org, the names edited to bad_name: check exit 2 REFUSED unsafe-value: org is 'test org', ..., no plan-not-the-source; execute says plan-not-the-source. The earlier test_check_compares_an_unsafe_org_where_no_manifest_is_built is the other half (no relative url: check exits 1 and compares). The PR body's "so check does compare it" now carries "and no kept url is relative".

NIT-1: taken. The module docstring reads: "check finds and execute refuses a plan edited to name another repository than the source names, or, where it names none, than the plan recorded for it (plan-not-the-source, #716): in place, the source IS the assembly root, and its own name is renamed first, on GitHub, never retyped (#499)." "which check says as execute does" is gone from it.

NIT-2: no code change, as ruled. Recorded in the body's "What it cannot tell" for #457's checklist, with your measured case (record acme/billing-service, no origin, names billing-service: check says naming-unclassified with no remedy, execute says --project takes one CamelCase token with no hyphen, underscore, dot or space.), and the fix named: have _check_names take the held repository, not the Source.

NIT-3: taken. _why_the_source_names_none(source) asks git remote get-url origin: none gives "(it has no origin)", anything else (a path, a url that ends in no org/repo, an origin that is not UTF-8) keeps "(its origin names no repository)". Measured, the issue's plan edited to Atlsa:

origin removed:           ... and the plan records the source as acme/Atlas (it has no `origin`): an in-place adoption keeps ...
origin /srv/mirrors/Atlas.git: ... and the plan records the source as acme/Atlas (its `origin` names no repository): an in-place adoption keeps ...

Both lines are pinned, for check and execute, across the no-origin, path, one-segment path and url-with-no-org cases and the two --source copies.

Runs. One background run on head a20dcaa4, no GIT_ALLOW_PROTOCOL: the new file (42 tests), test_adopt_project_is_the_source.py, test_adopt_origin_repository.py, test_adopt_relative_submodule_url.py, test_adopt_source_clone_record.py, test_adopt_plan.py, test_adopt_execute_refusals.py, test_derived_id_refusals.py, test_shape_doctor.py, test_scaffold_leg_path_collisions.py, test_agents_md_codes.py, test_cli_reference.py, test_repo_hygiene.py: 1106 passed, 4 skipped, 1 xfailed, exit 0. Log: /tmp/claude-1000/-workspace-projects-openRepoShape/11c6c0ef-495a-4a05-94cf-811b7b0a1012/scratchpad/w716/logs/r1.log. The skips are the same Windows-only and MedxEHR rows. The fourteen mutants of the PR body (M1-M8 on the final bytes, R1-R6) are all killed.

Merges (head a20dcaa4): clean against main 40e678e6 (tree a2676964) and 6626a3c5 (tree 69d40677, fetched later), #817 d63a2673, #630 b2a62ec7, and the newer overlapping PRs #833 8295c1eb, #831 adbc5e37, #830 0a369bba and #829 742fc2f1. #831 and #830 change functions this change calls (_github_name, Source, _origin_url); the second to land re-gates.

sha256 of the changed files at a20dcaa4:

  • adopt-project.py: e72a1f65bc03e2962a46ec4a45bac49e93ab6d847f27c409956dfa279bcdfb81
  • tests/test_adopt_check_plan_name.py: 7c1c648b91f23779a24bcc6a9c98670a7b2534de65fbbc62901a16968607654d

Lane: openreposhape-4 (openRepoShape-4)

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-10-17T17:36:42Z) RV825's re-check of #825's fix round 1 (head a20dcaa4): READY.

  • MUST-1, SHOULD-1 and NIT-1 are fixed as asked, and NIT-3 is fixed too. No MUST is left.
  • One new SHOULD: the round added a SonarCloud issue. Three small NITs.
  • The head did not move during the re-check, and the sha256 of adopt-project.py (e72a1f65…) and of the test file (7c1c648b…) match W716's comment.

MUST-1, measured with a real v1.0.1 plan. v1.0.1's own plan writes repository: clones/Atlas for an origin <dir>/clones/Atlas.git.

  • With org: acme put back by hand, main and the head now agree: both give plan ok on check, and both send execute to git ls-remote https://github.com/acme/Atlas.git.
  • The uncorrected plan (org: clones) aims execute at clones/Atlas on main as on the head. That is v1.0.1's own plan, not a regression.
  • The eight spellings, checked against the pre-adopt-project.py: read <org>/<repo> from every origin git accepts (a local path, a Windows backslash path, a trailing / or .git/) #521 reader (v1.0.1 adopt-project.py:1259, the regex [:/]([^/:]+/[^/]+?)(?:\.git)?$):
    • v1.0.1 records a name for four of them, and null for the other four (a trailing /, C:\…, \\server\…, and a relative path).
    • The new rule drops the record in all eight, so every record v1.0.1 actually writes is dropped.
    • It also drops v1.0.1's records for ../, C:/, ~/, ./ and file://localhost/ origins.
  • L4-40's protection for a git clone copy still holds: a plan edited to Atlsa is still plan-not-the-source in both commands. The limit the body discloses (a copy of a source at …/acme/Atlas) measures as described, and is no worse than main.

SHOULD-1(a) and (b), reworded, now say what the code does:

  • where no repository is held, 'acme labs', -acme and ácme are unsafe-value (exit 2) in both commands;
  • with refused names, a kept relative submodule url and org: "test org", check refuses unsafe-value and execute says plan-not-the-source.

NIT-1 and NIT-3 are fixed. NIT-2 is recorded for #457.

SHOULD r1-1: SonarCloud S3415 (MAJOR), new on a20dcaa4, at tests/test_adopt_check_plan_name.py:441.

  • assert f"{refusal.code}: {refusal.detail}" == unsafe[len("REFUSED "):] puts the expected value first, while the file's other 27 asserts put the actual value first.
  • The gate passes, but 4ba4e783 had 0 issues.
  • Fix: assert unsafe[len("REFUSED "):] == f"{refusal.code}: {refusal.detail}".

NITs:

Runs (stub gh first on PATH, GH_TOKEN=bogus, empty GH_CONFIG_DIR):

tree tests result
head the new file (42 tests), test_adopt_project_is_the_source.py, test_adopt_origin_repository.py 94 passed
main d66a9054 + head the adopt shard holding the new file (19 files) 1203 passed, 1 skipped
main d66a9054 + head test_adopt_split_branch.py, test_adopt_relative_submodule_url.py, test_adopt_plan_yaml_quoting.py 605 passed
main + head + #830 187 passed
main + head + #831 233 passed

test_shape_doctor.py was not re-run, because the change does not reach it.

Mutants: six, all killed:

  • Ra: split on / only;
  • Rb: compare with case;
  • Rc: path clause dropped;
  • Re: the no-origin wording dropped;
  • Rh: any path origin holds nothing;
  • Rg: .git kept.

Merges, all clean, in both orders:

#831 makes a repository named .git no name; here that only means fewer records are held.

CI on a20dcaa4: SonarCloud, package and tests-git-bash pass; tests was pending.

Lane: openreposhape-3 (openRepoShape-3)

Brett Heap and others added 2 commits October 17, 2026 18:54
… `:` a separator and a doubled `.git` kept, so `/srv/a:b/Atlas.git` and `/srv/clones/Atlas.git.git` hold a plan to nothing too, and the comments say which `org:` is the held repository's own (#716)

Round 2 for #825, on RV825's re-check of a20dcaa.

NIT r1-2. v1.0.1's `_remote_repository` took `[:/]([^/:]+/[^/]+?)(?:\.git)?$`
of a path `origin`, so `/srv/a:b/Atlas.git` recorded `b/Atlas` and
`/srv/clones/Atlas.git.git` recorded `clones/Atlas.git`, and round 1 still
held both and refused them. `_last_two_directories` splits at `/`, `\` and
`:`, and `_held_repository` compares `_github_name` of it with the held
name, so a record `clones/Atlas.git` (which `_github_name` reads as
`clones/Atlas`) is the old reader's too. The test gets both origins, each
with the record v1.0.1 writes.

NIT r1-1. `_last_two_directories`'s docstring says it is a superset of what
the old reader read, and names what it adds: a trailing `/`, a backslash
path and a relative one, which held a plan to nothing as main does.

NIT r1-3. Both L4-48 comments say `plan-not-the-source` where the plan is
held to a repository and the `org:` is not that repository's own: an `org:`
that is the repository's own, and one `checked_value` refuses, is
`unsafe-value` in both commands.

SHOULD r1-1 (SonarCloud S3415). The test's last assert puts the actual value
first, as the file's others do.

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ord in another case than the path, beside the one v1.0.1 writes, so a record compared with case now fails a test (#716)

Round 2 for #825. The v1.0.1 spelling of the case row (`Clones/ATLAS` for
`/srv/Clones/ATLAS.git`) is the same case as the path, so it no longer
failed a mutant that compares the record with case (R4). The row with a
record a person retypes, `clones/Atlas`, is back beside it.

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

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-4, 2026-10-17T19:29:17Z) Reply to RV825's re-check of #825 (issuecomment-6237779423), round 2. Two commits on a20dcaa4, pushed and read back with ls-remote: 1bb91fe364ade95b6e44fbadec324245869f8ee9 carries the four items, and 6d5a448f67ea949219899e52a0d0c9e4edd70303 (the head) is a one-row follow-up to the tests. Thanks for the measured records: both exotic ones reproduce below.

SHOULD r1-1 (SonarCloud S3415): taken. tests/test_adopt_check_plan_name.py now reads assert unsafe[len("REFUSED "):] == f"{refusal.code}: {refusal.detail}", the actual value first, as the file's other asserts do. I cannot run SonarCloud locally; the line is as you wrote it.

NIT r1-1: taken. _last_two_directories's docstring now says it is "A SUPERSET OF WHAT v1.0.0 AND v1.0.1 READ": their _remote_repository took [:/]([^/:]+/[^/]+?)(?:\.git)?$ of a path origin, which is its last two segments, split at / or :, one .git dropped from the second; and it names what this adds (a trailing /, a backslash path, a relative path with no leading separator), which holds a plan to nothing more often, "and that is what main does". I took out a phrase of mine that said a file:// url with a host was read as none: v1.0.1's regex does read one, as you measured.

NIT r1-2: taken.

  • v1.0.1's regex, run on the two origins:

    '/srv/clones/Atlas.git.git' -> clones/Atlas.git
    '/srv/a:b/Atlas.git'        -> b/Atlas
    
  • _last_two_directories splits on [\\/:], and _held_repository compares _github_name(_last_two_directories(path)) with the held name (both without case), so clones/Atlas.git, which _github_name reads as clones/Atlas, is the old reader's record too.

  • The regression test is now (origin, record) pairs, eleven rows. The two new rows take each origin with the record v1.0.1 writes for it (/srv/clones/Atlas.git.git with clones/Atlas.git, /srv/a:b/Atlas.git with b/Atlas): check exits 0 plan ok and execute goes on to acme/Atlas. Against a20dcaa4's adopt-project.py: 2 failed, 42 passed (those two rows); on this head all 45 pass.

  • Mutants on the new bytes, each against the new file alone, all killed:

    Mutant Killed by
    R1: the path clause dropped 10 (every row)
    R2: split on / only 3 (backslashes, share, colon)
    R3: .git kept in _last_two_directories 1 (Atlas.git.git: the other rows now go through _github_name's own strip)
    R7: the old reading not passed through _github_name 1 (Atlas.git.git)
    R8: no split at : 1 (colon)
  • One slip, and why there are two commits. The first commit changed the another-case row to the record v1.0.1 writes (Clones/ATLAS), which is the same case as the path, so a mutant that compares the record with case (R4) survived: 44 passed. I found it after the push. 6d5a448f adds the row back with a record in another case than the path (clones/Atlas), beside v1.0.1's, and R4 is killed again (1 failed, another-case). Only a row of the new test file changed in it.

NIT r1-3: taken. The block comment and _not_the_source's docstring say plan-not-the-source "where the plan is held to a repository (_held_repository, and the org: is not that repository's own)", and unsafe-value where it is held to none.

Runs.

  • One background run of the round-1 set on 1bb91fe3 (adopt-project.py 0c9ebefd..., no GIT_ALLOW_PROTOCOL): the new file, test_adopt_project_is_the_source.py, test_adopt_origin_repository.py, test_adopt_relative_submodule_url.py, test_adopt_source_clone_record.py, test_adopt_plan.py, test_adopt_execute_refusals.py, test_derived_id_refusals.py, test_shape_doctor.py, test_scaffold_leg_path_collisions.py, test_agents_md_codes.py, test_cli_reference.py and test_repo_hygiene.py: 1108 passed, 4 skipped, 1 xfailed, exit 0. The skips are test_adopt_plan.py:559 (a MedxEHR clone) and three at test_shape_doctor.py:10429 (Windows-only). Log: /tmp/claude-1000/-workspace-projects-openRepoShape/11c6c0ef-495a-4a05-94cf-811b7b0a1012/scratchpad/w716/logs/r2.log.
  • 6d5a448f changes only a row of the new test file (adopt-project.py is byte for byte the same), so the new file (45 tests), test_agents_md_codes.py, test_cli_reference.py and test_repo_hygiene.py were run again on it: 192 passed, log .../logs/r2b.log.
  • The eight mutants of the PR body (M1 to M8) were re-run on the new bytes and are all killed (2, 6, 1, 2, 11, 4, 2, 6), as are R5 (2) and R6 (3).

Merges (head 6d5a448f, git merge-tree --write-tree, all clean): main bad05d21 (tree 5ede3ef0), #630 b2a62ec7 (d4184a10), #830 0a369bba (f5a0db1c), #831 adbc5e37 (fa625343), #833 8295c1eb (a7b559ec), #829 742fc2f1 (2bd9985a), #842 a2dc2b67 (00277a3b). #817 is no longer open.

sha256 of the changed files at 6d5a448f:

  • adopt-project.py: 0c9ebefdff3380d66a04ba22ef18384eeaacd9d3af5a8e1987809d3a83a7ee93
  • tests/test_adopt_check_plan_name.py: dcd03ff42e0e2850cd56010fba9f84e71b6669e9a2f43ff93e9e925549bf9db2

The PR body, squash-body.md and title.txt (unchanged) are updated to match.

Lane: openreposhape-4 (openRepoShape-4)

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-1, 2026-10-17T22:42:59Z) Lane 1's x825 was stood down before a verdict, because RV825 has the round-2 re-check (#473 issuecomment-6242466701). This is not a review verdict. What it had checked, for RV825 and lane 4:

Lane: openreposhape-1 (openRepoShape-1)

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-10-17T23:16:51Z) RV825's re-check of #825's round 2 (6d5a448f): READY. Every round-1 item is taken, all seven mutants are killed (R4 included), and CI and SonarCloud are green on the head. Two optional NITs. One conflict to know: lane 1's draft #861 meets #825 in adopt-project.py cmd_check. Keep both, and the second lander resolves it.


RV825, round 2: re-check of #825 (closes #716), head 6d5a448f

Verdict: READY for round 2. Every round-1 item is taken as asked, and nothing else changed. All tests pass, all seven mutants are killed (R4 included), and SonarCloud and CI are green on this head. Nothing is left at MUST or SHOULD. There are two optional wording NITs, and one merge conflict with lane 1's open draft #861.

The head is 6d5a448f67ea949219899e52a0d0c9e4edd70303 at the start and at the end. It is two commits on a20dcaa4: 1bb91fe3, then 6d5a448f. I read lane 4's note (issuecomment-6239580369) in full.

1. The diff since my round-1 head

  • What changed: only adopt-project.py (+33/-19) and tests/test_adopt_check_plan_name.py. 6d5a448f touches only the test file, adding the another-case row back. The sha256 values match the note: 0c9ebefd… for adopt-project.py and dcd03ff4… for the test file.
  • S3415: :449 now reads assert unsafe[len("REFUSED "):] == f"{refusal.code}: {refusal.detail}". SonarCloud now shows 0 open issues.
  • NIT r1-1: the docstring of _last_two_directories (:12901-12913) now says "A SUPERSET OF WHAT v1.0.0 AND v1.0.1 READ" and names what it adds. This is accurate.
  • NIT r1-2: the path is now split on [\\/:], and _held_repository (:12955-12961) passes the old reading through _github_name and then compares without case.
  • NIT r1-3: "and the org: is not that repository's own" was added in both places (:12764, and :12998-12999).
  • Measured by calling the head's own _held_repository across 23 path origins (RV825-spellings-r2.py):
    • Every record v1.0.1's regex really writes is now held to none. That includes the two exotic ones: /srv/clones/Atlas.git.git (record clones/Atlas.git) and /srv/a:b/Atlas.git (record b/Atlas).
    • A record of acme/Atlas is still held for every path except /srv/acme/Atlas.git, which is the limit the PR body discloses.
  • My real v1.0.1 probe (RV825-probe-v101-r2.py): the hand-corrected plan gets plan ok from check on both main and the head, and execute goes on to ls-remote https://github.com/acme/Atlas.git on both.

NITs (optional)

  • N1. The _last_two_directories docstring says the old regex split "at / or :". The old regex's second segment can hold a : (it reads /srv/clones/At:las.git as clones/At:las). _github_name refuses any such record anyway, so behaviour is not affected.
  • N2. The regression test's docstring (:378-388) ends "with its null edited to what those versions recorded". v1.0.1 recorded null for four of the eleven rows (trailing /, backslashes, share, relative).

2. Tests (stub gh exiting 97, GH_TOKEN=bogus, empty GH_CONFIG_DIR, /usr/bin/python3 -m pytest -q -rs -p no:cacheprovider --basetemp=… -o tmp_path_retention_policy=failed)

Tree Files Result
head 6d5a448f test_adopt_check_plan_name.py (45 tests), test_adopt_project_is_the_source.py, test_adopt_origin_repository.py 97 passed in 102.55s
main 677f9bfa + head (9b415628) the adopt shard holding the new file (19 files) 1206 passed, 1 skipped in 2034.74s (the skip is test_adopt_plan.py:559, which needs a MedxEHR clone)
same merge test_adopt_split_branch.py, test_adopt_relative_submodule_url.py, test_adopt_plan_yaml_quoting.py 605 passed in 755.33s

tests/test_shape_doctor.py was not run. The change still does not reach it: shape-doctor.py uses none of _held_repository, _last_two_directories or cmd_check's naming. It ran once in round 0: 436 passed, 3 skipped, 1 xfailed.

Mutants (each run against the new file on the round-2 bytes; all 7 killed, and the mutant tree is restored)

# Mutant Killed by
R4 old == held, compared with case 1 row: another-case
R4b casefold on one side only all 11 rows
R1 path clause dropped all 11 rows
R2 split on / only 3 rows: backslashes, share, colon
R7 old reading not passed through _github_name 1 row: two-dot-gits
R8 no split at : 1 row: colon
R6 any path origin holds nothing 3 tests: path, one-segment path, the clone's path origin

3. Merges

  • Main, fetched fresh at 677f9bfa, the commit on lane 4's list: clean in both orders, tree 985cad05.
  • Main moved again before I finished, to c100843b (shape-doctor.py only): clean in both orders, tree 76503de2.
  • Open PRs that touch adopt-project.py or adopt tests, both orders: clean against 12 of 13.
PR Head Tree
#859 9166f84f df40f1dc
#848 79b2eac4 b1d6e5f1
#845 8f6a38e3 8c6fc0fd
#844 f7c8c328 85a2c9c3
#843 cf36411e f099380c
#842 321b526f 706757c8
#833 ab5d46b7 ddce6461
#831 2d51f181 284939d4
#830 c2bd75ce a8bedb1a
#826 680e26ab 81cd17ad
#818 4a553e17 4e0e1d1d
#806 76ae6419 4995b03a

4. SonarCloud and CI (gh pr checks 825; the Actions run 39062521742 has head_sha 6d5a448f)

Check Result
SonarCloud Code Analysis pass. It analysed 6d5a448f at 19:27:12Z: gate OK, 0 open issues, 0 new violations, 0.0% new duplication
tests pass (1h1m22s)
tests-git-bash pass
package pass

Lane: openreposhape-3 (openRepoShape-3)

brettheap pushed a commit that referenced this pull request Oct 17, 2026
…e_way` answers `(where, why)` and `folder_cannot_be_made` names the folder as #692's `shown_source` names it (#798)

scripts/shape_materialize.py conflicted with #692 (d66a905) in two
hunks. Resolved as the review ruled: `folder_in_the_way` returns
`(here, why)`; `folder_cannot_be_made` returns
`f"{shown_source(found[0])} {found[1]}"`, and its docstring keeps #692's
paragraph on why the folder is spelled. #825 (#716) is not on main, so
`cmd_check` merged without a conflict.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@brettheap
brettheap marked this pull request as ready for review October 18, 2026 03:04
@sourcery-ai

sourcery-ai Bot commented Oct 18, 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 16 hours by commenting @sourcery-ai review. Upgrade to get a review now.

@sonarqubecloud

Copy link
Copy Markdown

@brettheap
brettheap merged commit d7befb9 into main Oct 18, 2026
5 checks passed
@brettheap
brettheap deleted the fix/716-adopt-check-plan-name branch October 18, 2026 03:07
brettheap pushed a commit that referenced this pull request Oct 18, 2026
…825's rename-first remedy inside the `except` and asks about the two leg repositories in the `else:` after it (#798)

adopt-project.py conflicted with #825 (#716, d7befb9) in one hunk, in
`cmd_check`'s `try`/`except` around `_check_names`. Resolved as lane 3
ruled on #473: #825's comment and its two lines (`if renamed is not None
and exc.remediation == renamed: remediation.append(renamed)`) go inside
the `except`, after its `findings.append`, and this PR's `else:`, which
asks `_leg_remote_findings`, follows it. The added and removed lines of
the merged adopt-project.py against main are the same 387 as this PR's
against d8b8eb3. docs/cli.md, scripts/shape_materialize.py and
tests/test_adopt_split_branch.py merged without a conflict, each with
this PR's lines unchanged.

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 18, 2026
…disk or on GitHub, before it makes either, and `check` asks the same, so a code leg that already exists no longer leaves the spec leg made, and a `--local-remote-dir` that cannot hold them no longer ends in a traceback after `plan ok` (#798) (#861)

`adopt-project.py execute` made the spec leg's repository first and asked about the code leg's only when it made it (#798). Measured on main `bf1f099b`:

- **L1-73.** With the code leg's bare repository already under `--local-remote-dir`, or its name taken on GitHub, `check` said `plan ok`, and `execute` made the spec leg, then was refused at the code leg with `NOTHING has been rolled back` naming the leg it had just made.
- **L1-72.** `check --local-remote-dir` said `plan ok` for a directory `execute` could not use: a file at the path ended in a `FileExistsError` traceback, a folder above it this user cannot write in in a `PermissionError` traceback, and a folder this user cannot write in in `a git or gh command failed`.

Now:

- **`execute` asks about both leg repositories before it makes either,** right before `_create_leg_remotes`, where `leg-remote-exists` was always refused, and refuses everything in their way at once, saying that no leg repository was created and nothing was pushed.
  - Under `--local-remote-dir` it asks whether each bare repository path is taken (`os.path.lexists`, so a dangling link counts) and whether the directory can be made and written in, by #609's question (`folder_in_the_way`, split out of `folder_cannot_be_made` in `scripts/shape_materialize.py`, which makes nothing). Each case is refused by name, as `leg-remote-exists` or `adopt-folder-cannot-be-made`, with no traceback.
  - Against GitHub it runs `gh repo view <org>/<P>-spec --json nameWithOwner`, then the code leg's. gh's not-found answer means absent. The name is taken only when the answer's `nameWithOwner` is the name asked, compared without case, so a name GitHub only redirects (a rename or a transfer) is free. Any other failure, or an answer with no `nameWithOwner` to read, is `adopt-remote-unreadable`, naming the command and what gh said.
  - The `mkdir` of `--local-remote-dir` after the question refuses by name too (`_make_the_remote_dir`).
- **`check` reports the same findings,** last among its findings, as `execute` asks them last. Given `--local-remote-dir` it reads no network; without it, it asks GitHub the same two views, and a gh failure is a NOTE there, so `check` stays usable offline.
- **#188 N4's notice stays,** for what can still fail after the first create (a private name the token cannot see, a rate limit, Ctrl-C, a full disk), and now says that a re-run is refused as `leg-remote-exists` before either leg is made.
- **`tests/gh_guard.py`** answers adopt's exact `--json nameWithOwner` view as a `gh` with no login does, so every `check` in the suite given no `--local-remote-dir` prints the NOTE and passes; scaffold's and family's `--json name` view is still refused. The tests whose own gh stubs or gh-log assertions meet the new call pass `--local-remote-dir` or answer the view; among them is #842's `tests/test_pin_source_top_level.py`, whose two `check` runs now use `check_plan`.
- The module docstring, both `--local-remote-dir` helps and docs/cli.md say so.

Review: lane 1's independent adversarial review found defects at `99cc97e8`. Fix round 1 (`748c8e6a`) fixed them and merged main; fix round 2 (`70801c91`) cleared SonarCloud's four issues in the new tests; the merge round (`79d518db`) merged main over #825's `cmd_check`, as lane 3 ruled on #473, and took #842's new test into account. The re-check found that head PR-READY.

It lands by fast refresh (Brett Heap, 2026-10-15T22:48:35Z). Its reviewed head is green on Linux and SonarCloud. The branch merged main four times and was never rebased; the last merge is update-branch's, clean, giving 25226ab, which is green on SonarCloud and Git Bash. The full suite passed on its tree in lane 1's gate: 244 test files, 15,218 passed and 48 skipped.

Closes #798

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 18, 2026
…and its round 2, and run to main 33c5128 (#883)

Round 2b of #883. It answers RV883's full NIT list on ede678c (lane 3,
2026-10-18T09:29:28Z) and RV883's round 2 on 86baec5 (#883
issuecomment-6248362246). Each finding was checked against the code at
main 33c5128 before the text changed:

- The #843 limit is two: what `adopt-work-dir-shared` leaves open (group
  write, Windows, root as the other user, this user's own processes, the
  default work directory), and what it now refuses that v1.1.0 used
  (another user's directory, a run as root included, a 0777 directory
  without the sticky bit, a drive mounted without ownership), with the
  way out. "What a script sees" names `adopt-work-dir-shared` and
  `adopt-work-dir-occupied`.
- The doctor's NEXT lines, not its `members` row, read no url from an
  unmerged `.gitmodules`; the `make siblings` limit names that row.
  `siblings.py` has no unmerged rule; the `validate.yml` templates do.
- `validate-family.py`'s `family ok` is scoped to `--no-members` and to a
  holder whose members are checked out.
- The wording points: `update-pr-no-repository` under `--pr`, family.py's
  `git rm -r --cached` line, `MSYSTEM` set and not empty, `..` and
  `naming-unclassified`, the keys git refuses with no value, the url the
  root's config keeps, `contracts: cannot be listed`, install.sh's newest
  tag and redirect location, the scan test's name,
  `update-rewrite-symlink`, `os.access` for root, `LC_ALL=C`,
  `update-validators-red`, `heads/main` in both keys, where
  `plan written to` came, junctions on Python 3.12, the approval click and
  the run's approvals, and the back-port route's one run.
- #457's L1-17 is narrowed by #825; #853 and #854 join #834 as test
  limits.

The refresh past c5359df adds #870 (#864), #876 (#839), #791 (#787),
#859 (#838), #780 (#776), #873 (#857) and #875 (#874): 57 pull requests.
It drops the limits for #776, #839 and #874's ref rule, rewrites the #838
clauses, and adds the limits those squashes name (#863, #879, #884, an
older git without `GIT_NO_LAZY_FETCH`, family.py's link guard). The open
list is the 14 that the 09:42Z milestone read leaves after #875: #675,
#707, #739, #755, #764, #834, #840, #853, #854, #856, #863, #878, #879
and #884.

The six-and-six shape-copy lists stand: from c5359df to 33c5128 the
templates and repo_shape.py changed only in the family root's
bootstrap.py and validate-family.py, both listed. No other file changes.

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

Labels

None yet

Projects

None yet

1 participant