Skip to content

adopt-project.py: execute asks about both leg repositories, on the 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

Merged
brettheap merged 12 commits into
mainfrom
fix/798-adopt-legs-asked-first
Oct 18, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

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

A v1.1.1 draft. It lands only after the v1.1.0 tag, in the order agreed on #473.

What

execute now asks about both leg repositories before it makes either. It refuses everything that stands in their way at once, and says that no leg repository was created and nothing was pushed.

  • Under --local-remote-dir, it asks two things.
  • Against GitHub, it runs gh repo view <org>/<P>-spec --json nameWithOwner, then <org>/<P>-code.
    • gh's not-found answer means absent (exit 1, Could not resolve to a Repository), as scaffold-project.py and family.py init: the dry runs skip the target-exists and folder checks, and any gh repo view failure reads as "does not exist" #609 has it for the scaffold.
    • A name that exists is refused as leg-remote-exists before any gh repo create. gh's GraphQL follows a rename or a transfer, so the answer counts only when its nameWithOwner is the name asked, compared without case: a name GitHub only redirects is free, and gh repo create takes it.
    • A view that fails otherwise is refused as adopt-remote-unreadable, naming the command, its exit and gh's first line. That covers no login, a bad token, a rate limit, no network, and a gh that cannot be started. An answer with no nameWithOwner to read (not JSON, or no such field) is refused the same way, naming what gh printed.

check reports the same findings, under the same flags and in the same order (#417's one verdict). They come last among its findings, as execute asks them last.

  • Given --local-remote-dir, check asks the disk, as execute --local-remote-dir does. It reads no network.
  • Without it, check now asks GitHub the same two gh repo view questions, so check reads the network there, two calls. A name that exists is a FINDING.
  • The one difference from execute: a gh failure other than not-found, or an answer with no nameWithOwner to read, is a NOTE in check, not a finding. The NOTE names the command and gh's answer, and check still passes, so it stays usable offline. execute refuses the same answer as adopt-remote-unreadable.

The texts, as the probes on the branch printed them (paths shortened to <lrd>):

REFUSED leg-remote-exists: 1 finding(s) about the two leg repositories, asked before either is made:
  FINDING leg-remote-exists: `<lrd>/Atlas-code.git`, the code leg's bare repository, already exists
Remediation: no leg repository was created and nothing was pushed. Choose an empty --local-remote-dir, and a --work-dir the run that made it did not clone into (a fresh one, or none): `git clone` refuses a directory it already cloned into. There is no --force: re-running over a live leg is not an adoption.

REFUSED adopt-folder-cannot-be-made: 1 finding(s) about the two leg repositories, asked before either is made:
  FINDING adopt-folder-cannot-be-made: --local-remote-dir `<dir>/ro/remotes` cannot hold the two leg repositories: `<dir>/ro` is a folder this user cannot write in
Remediation: no leg repository was created and nothing was pushed. Give this user write and search permission on that folder, or pass another --local-remote-dir.

REFUSED leg-remote-exists: 1 finding(s) about the two leg repositories, asked before either is made:
  FINDING leg-remote-exists: acme/Atlas-code, the code leg's repository, already exists on GitHub: `gh repo view acme/Atlas-code --json nameWithOwner` finds it
Remediation: no leg repository was created and nothing was pushed. A leg repository is created new, never over one that exists, and there is no --force: re-running over a live leg is not an adoption. If an earlier `execute` made it and its adoption is not one to keep, delete it (`gh repo delete <repository>`, which needs the `delete_repo` scope); another repository of that name is renamed on GitHub first. Then run `check` and `execute` again.

NOTE `check` could not ask GitHub whether acme/Atlas-spec, the spec leg's repository, already exists: `gh repo view acme/Atlas-spec --json nameWithOwner` exited 4: To get started with GitHub CLI, please run:  gh auth login. It is no finding here, so that `check` stays usable offline; `execute` asks again before it makes either leg, and refuses as `adopt-remote-unreadable` while gh cannot answer.

A file at the path reads ... cannot hold the two leg repositories:

/afile is a file, not a folder, and its remedy is to move what stands in the way aside.

When the stub answers acme/Atlas-code's view with {"nameWithOwner": "acme/Atlas-code-old"}, a rename's redirect, check prints plan ok, and execute goes on to gh repo create acme/Atlas-spec and gh repo create acme/Atlas-code. Answered with ACME/atlas-code, the name is taken, and both print the FINDING above.

Why (reproduced on main bf1f099b)

Each case was run with a stub gh that logs every call, GH_TOKEN set to a bogus value, an empty GH_CONFIG_DIR and GIT_ALLOW_PROTOCOL=file.

  • L1-73, local. With <lrd>/Atlas-code.git there, check --local-remote-dir <lrd> said plan ok. execute then exited 2 with REFUSED creating the code leg repository. leg-remote-exists: <lrd>/Atlas-code.git already exists, and NOTHING has been rolled back named Atlas-spec.git, which it had just made.
  • L1-73, GitHub. The stub answered the code leg's repo create as GitHub does for a taken name. check said plan ok. The only gh calls execute made were gh repo create acme/Atlas-spec --private ... and then gh repo create acme/Atlas-code --private .... It exited 2, and the notice said https://github.com/acme/Atlas-spec.git exists.
  • L1-72. check --local-remote-dir said plan ok in all three cases:
    • a file at the path: execute exited 1 with a FileExistsError traceback;
    • a path under a folder this user cannot write in: exit 1, a PermissionError traceback;
    • a folder this user cannot write in: exit 2, REFUSED a git or gh command failed, then fatal: cannot mkdir .../Atlas-spec.git: Permission denied.

How

  • adopt-project.py, a new section before _create_leg_remotes:
    • _leg_remote_answers asks the questions. _in_the_way_on_disk and _in_the_way_on_github are its two halves.
    • _in_the_way_on_github asks --json nameWithOwner and reads the answer with _name_with_owner. An answer naming another repository, compared without case, is a redirect, and counts as absent, as gh's not-found answer does.
    • _refuse_what_stands_in_the_legs_way is execute's refusal. Its code is the first finding's own, and its detail lists each finding as check prints it.
    • _leg_remote_findings is check's. GitHub is asked only of an org: execute would put on a gh command line (checked_value), and only when the names classified, since execute refuses both earlier.
    • Each path is _spelled (adopt-project.py check and execute print a source path raw in a FINDING, so a \r, \n, \x85, U+2028 or ESC in a name overwrites, splits or escapes the line on a terminal, where plan's questions spell the same path with _spelled #212).
    • A --local-remote-dir that cannot be resolved, such as a link loop, is kept as given (_the_remote_dir), in _remote_urls too. execute died there with RuntimeError: Symlink loop before anything was asked. Now folder_in_the_way names the loop in both commands.
  • Where the new questions sit (the coordinator's decision 4): in cmd_execute, right before _create_leg_remotes, after _confirm, _template_values and answered_path_record. That is where leg-remote-exists was always refused. The reasons:
    • every earlier refusal keeps its order and its words;
    • adopt-branch-exists and plan-split-branch-taken still defer to a leg on disk, as they did;
    • a re-run meets leg-remote-exists at the same point as before;
    • GitHub is asked as close to gh repo create as possible.
      The cost: a run refused here has asked for the yes first, as a run refused with leg-remote-exists always had.
  • The mkdir of --local-remote-dir, which comes after the question, now refuses by name (_make_the_remote_dir, adopt-folder-cannot-be-made, before any leg exists). It covers what the question cannot foresee, such as a file put there since. It is no longer an OSError traceback.
  • adopt-project.py: a failure AFTER both legs are pushed (the split-branch push, the mount, the split commit) prints only REFUSED <step> with no notice of what already exists and no stdout flush, so the combined log shows REFUSED before the bare … lines and the re-run meets leg-remote-exists unexplained #188 N4's notice stays (decision 3). These failures can still come after the first create:
    • GitHub answers a private repository the token cannot see as not found, so both views pass and gh repo create then finds the code leg's name taken;
    • a rate limit or an outage between the two creates;
    • Ctrl-C;
    • under --local-remote-dir, a path made since the question, or a full disk at the code leg's git init.
      The notice's clause about how a re-run is refused now says what is true: as leg-remote-exists, before it makes either leg, on the disk and on GitHub alike. MOUNT_REMEDIATION and _nothing_rolled_back's docstring say the same.
  • scripts/shape_materialize.py: folder_in_the_way is split out of folder_cannot_be_made. It is the same definition, now answering (where, why), so that adopt can spell the path; folder_cannot_be_made keeps scaffold-project.py, family.py and shape_materialize.py: #622's folder refusals print a folder name raw, so an ESC or a line break reaches the terminal, and family-operation-in-progress prints a holder's path across two lines #692's shown_source spelling of the folder. What the scaffold and family.py print is unchanged (tests/test_dry_run_parity.py, tests/test_family_refuse_up_front.py).
  • tests/gh_guard.py: the session's guard now answers gh repo view <owner>/<name> --json nameWithOwner. Without this, decision 2 fails the suite: every check in it given no --local-remote-dir, in about 70 files, now asks that call twice, and the guard refused and failed each such test at teardown.
    • I chose the answer a gh with no login gives: exit 4 and gh's own words. That is the state the guard already describes with its other two answers, so every such check prints the NOTE above and still passes, the same on every machine.
    • The fallback the coordinator allowed was gh's not-found answer, if more than a handful of tests broke on exact output. None did: no existing test failed on the NOTE's words. Every break came from a test's own gh stub, or from an assertion that no gh call was made, as listed below.
    • Only that exact argv is answered. --json name, --json visibility, repo view with any other shape, and repo create are still refused (tests/test_gh_guard.py). scaffold-project.py and family.py ask whether their own repositories exist with --json name, so the guard answers adopt's call alone: a test of either that reaches gh with no stub of its own is still failed at teardown, as before this PR (a new case in tests/test_gh_guard.py).
  • Tests whose own gh stubs, or assertions about gh, now meet the new call:
    • tests/test_adopt_pins.py (check_plan), tests/test_pin_annotated_tag.py and tests/test_template_values_single_pass.py: their check runs take the --local-remote-dir their execute runs use. So a log that holds no gh call still proves what it proved: check reads no network for a pin. One pins check (test_the_pin_and_write_lines_say_how_and_not_where) runs as people run it, with no flag, and its gh log is exactly the two leg views.
    • tests/test_adopt_github_exits.py and tests/test_adopt_leg_keys.py: their shims answer repo view <repo> --json nameWithOwner from their fake GitHub, with the repository's own name.
    • tests/test_adopt_temp_work_roots.py's gh_clones, which tests/test_adopt_source_clone_record.py imports, answers the leg views (--json nameWithOwner, exactly) as not found and records only its other calls.
    • tests/test_adopt_split_branch.py: the first gh call of a run that went on to the legs is now the spec leg's gh repo view, compared whole (TO_THE_LEGS).
    • tests/test_adopt_filter_repo_failure.py asserts the notice's new words for how a re-run is refused.
    • tests/gh_guard.py answers an owner GitHub would refuse too, such as a+b, which tests/test_adopt_manifest_values.py plans with.
  • Docs: the module docstring, both --local-remote-dir helps, and docs/cli.md, regenerated.

Review round 1 (r798, at 99cc97e8)

Merge round (main 33419925, head 79d518db)

Tests

New tests were written first. On main bf1f099b, 22 of the first round's 25 cases failed, and the guard test did not exist. Round 1 added 7 more, and each fails with its change reverted (R13 to R16 below). 32 new cases in all:

  • tests/test_adopt_execute_refusals.py, 28 cases:
  • tests/test_adopt_late_failure.py, 2 cases:
    • a code leg made after the question still gets the notice (decision 3);
    • a --local-remote-dir that cannot be made after the question passed is refused by name.
  • tests/test_gh_guard.py, 2 cases:
    • the guard's answer to adopt's view, and the near misses it still refuses;
    • scaffold's and family's --json name view is still refused and fails the test that reached it. This case passes on main too: it pins what this PR must not change.

Gates, on head 79d518db (dirty 0; main 33419925 merged in 77feec0c).

  • Each gate ran in six shards, one gate at a time, each file on its own.
  • Each used /usr/bin/python3 -m pytest, a stub gh first on PATH, a bogus GH_TOKEN, an empty GH_CONFIG_DIR, GIT_CONFIG_GLOBAL=/dev/null and GIT_SSH_COMMAND=false, and no GIT_ALLOW_PROTOCOL.
  • The files are 108:
    • the 104 test files among the 106 that grep -l -E 'adopt-project|adopt_project|\bADOPT\b' tests/*.py gives (the other two are conftest.py and gh_guard.py). They include test_gh_guard.py, test_shape_doctor.py and test_scaffold_leg_path_collisions.py, and main's new test_pin_source_top_level.py and test_adopt_check_plan_name.py;
    • test_dry_run_parity.py and test_family_refuse_up_front.py, for the shape_materialize.py merge;
    • test_no_auto_maintenance.py;
    • test_cli_reference.py, for docs/cli.md.
Mode Files Result Exit
git-filter-repo on PATH 108 7,669 passed, 17 skipped 0
git-filter-repo hidden (PATH=<stub gh>:/usr/local/bin:/usr/bin:/bin) 108 7,269 passed, 417 skipped 0

Earlier heads. At 748c8e6a, before the merge round, the same gate over the 102 files of that time gave 7,245 passed, 13 skipped, 1 xfailed with git-filter-repo on PATH, and 6,857 passed, 401 skipped, 1 xfailed with it hidden. That xfail was test_shape_doctor.py's strict one for #735, and main's #772 (112eeed2) fixed it and dropped the mark.

Round 2, head 70801c91. This round changed only four test sites, three arguments built before the with and one assert split (Review round 1, Test nits), in tests/test_adopt_execute_refusals.py and tests/test_adopt_late_failure.py; no test asserts anything new. At that head test_adopt_execute_refusals.py, test_adopt_late_failure.py and test_no_auto_maintenance.py were run again, each on its own and one at a time, in both modes: all passed (git-filter-repo on PATH: 181, 88 and 9 passed, 2 skipped in the last; hidden: 177, 84 and 9 passed, 4, 4 and 2 skipped). The revert table is at 748c8e6a and was not run again.

Revert table. Each change was reverted alone on 748c8e6a and the named files were run.

# Change reverted Files run Fails
R1 execute asks nothing before it makes the legs the two extended files 28 (13 tests)
R2 check asks nothing about the legs same 15 (6 tests)
R3 no question about the bare paths same 5 (2 tests)
R4 no question about the directory same 8 (2 tests)
R5 no GitHub question same 15 (9 tests)
R6 any gh repo view failure means absent same 4 (2 tests)
R7 a gh that cannot be run is not caught same 1
R8 the bare mkdir after the question same 1
R9 the guard refuses adopt's view test_gh_guard.py, test_adopt_execute_refusals.py 1, and 60 errors (every check there fails at teardown)
R10 the pins check without --local-remote-dir test_adopt_pins.py 49
R11 the github-exits shim without the view test_adopt_github_exits.py 15
R12 _remote_urls resolves a link loop the two extended files 2
R13 any exit 0 of the view means taken same 5: the two redirects and the three unreadable answers
R14 names compared with case same 1: the answer in other letters
R15 an answer with no nameWithOwner means absent same 3: the unreadable answers
R16 the guard answers --json name again, scaffold's and family's call test_gh_guard.py, test_adopt_execute_refusals.py 2 (both guard cases), and 60 errors

R1, R2 and R5 fail more cases than at 99cc97e8 (22, 9 and 9) because round 1's cases depend on the same questions. R10 fails one fewer (50) because one pins check now runs without the flag.

The folder_in_the_way split changes no behaviour. tests/test_dry_run_parity.py and tests/test_family_refuse_up_front.py pass in both modes.

Windows

CI is Linux only (#597).

  • The new GitHub-mode tests use a #! stub gh, so they skip on Windows, as tests/test_adopt_github_exits.py does.
  • The tests of a folder this user cannot write in skip on Windows and for root, whose modes refuse nothing.
  • Every path a test makes is legal on NTFS, and nothing compares bytes read back from a file write_text wrote.

For the checklist

Known limits

  • "Can be written in" is os.access, as scaffold-project.py and family.py init: the dry runs skip the target-exists and folder checks, and any gh repo view failure reads as "does not exist" #609's question is for the scaffold. Root, and any folder on Windows, are always told yes, and an ACL that says otherwise is met at the mkdir or git init. The first is now refused by name; the second still ends as a failed command after nothing, or after the spec leg, was made.
  • A private repository the token cannot see answers as not found. That race is why the notice stays.
  • check without --local-remote-dir now needs gh to answer for a clean read. Offline it still passes, with the NOTE.
  • A name GitHub only redirects is free, and gh repo create takes it over. That ends the redirect to the renamed or moved repository, as GitHub's rename page warns. Neither check nor execute says so.

Closes #798

🤖 Generated with Claude Code

Summary by Sourcery

Preflight both leg repositories and their destination before adoption so conflicts are reported consistently without creating partial results.

New Features:

  • Preflight both leg repositories before adoption, checking local paths or GitHub names before creating either leg.
  • Extend check to report local repository conflicts and GitHub name conflicts, while treating unavailable GitHub queries as notes.

Bug Fixes:

  • Prevent partial adoption when the code leg already exists by refusing before creating the spec leg.
  • Replace local remote directory tracebacks with named refusals when paths are blocked, unwritable, or unresolved.

Enhancements:

  • Handle GitHub redirects correctly by treating a name as occupied only when nameWithOwner matches the requested repository, case-insensitively.
  • Preserve late-failure notices for races and failures occurring after preflight checks.

Documentation:

  • Update CLI documentation and help text for the new leg repository preflight checks.

Tests:

  • Add coverage for local path conflicts, GitHub repository states, unreadable GitHub responses, redirects, race conditions, and guard behavior.

Chores:

  • Refactor folder feasibility reporting to expose the blocking path and reason while preserving existing behavior for other callers.

Brett Heap and others added 5 commits October 17, 2026 11:04
…ject.py `execute` asks about both leg repositories before it makes either, and `check` asks the same (#798)

State at the checkpoint:
- tests/test_adopt_execute_refusals.py and tests/test_adopt_late_failure.py
  hold the new tests, written first; 22 of them fail on bf1f099 as
  intended (red run before any product change).
- scripts/shape_materialize.py: `folder_in_the_way` split out of
  `folder_cannot_be_made`, no change in what the scaffold or family.py say.
- adopt-project.py is NOT changed yet: the question, check's findings and
  the mkdir guard are still to write, then tests/gh_guard.py's answer for
  `gh repo view <repo> --json name`, the shims other tests use, docs/cli.md.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t makes either, on the disk or on GitHub, and `check` asks the same (#798)

`_refuse_what_stands_in_the_legs_way` runs right before
`_create_leg_remotes` and refuses everything at once: a leg's bare
repository already there and a `--local-remote-dir` that cannot be made
and written in (`folder_in_the_way`, #609), or, against GitHub, a leg name
`gh repo view` finds, and a view that fails otherwise than gh's not-found
answer. `check` prints the same findings last, and a NOTE where gh cannot
answer. The mkdir that follows refuses by name. tests/gh_guard.py answers
`gh repo view <owner>/<name> --json name` as a gh with no login does;
test_adopt_pins.py's `check` runs take the test path's
`--local-remote-dir`, and test_adopt_github_exits.py's shim answers the
view from its fake GitHub.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e question and is refused by name, in `execute` as in `check` (#798)

`_remote_urls` resolved it with `Path.resolve`, which raised
`RuntimeError: Symlink loop` out of `execute` before anything was asked,
where `check` named it. Both now keep a path that cannot be resolved as
given (`_the_remote_dir`), and `folder_in_the_way` names the loop. A path
that resolves is unchanged.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… new `gh repo view` of each leg answer it as GitHub does, and the notice's re-run words are asserted as they now read (#798)

- tests/gh_guard.py answers `repo view <owner>/<name> --json name` for an
  owner GitHub would refuse too (`a+b`, test_adopt_manifest_values.py).
- test_adopt_leg_keys.py's stub and test_adopt_temp_work_roots.py's
  `gh_clones` give gh's not-found answer for a leg; `gh_clones` records
  only the other calls.
- test_adopt_split_branch.py: the first gh call of a run that went on to
  the legs is now the spec leg's `gh repo view` (`TO_THE_LEGS`).
- test_pin_annotated_tag.py and test_template_values_single_pass.py give
  `check` the `--local-remote-dir` their runs use, so a log of no gh call
  still says what it said.
- test_adopt_filter_repo_failure.py asserts the notice's words for how a
  re-run is refused: before it makes either leg, on the disk and on
  GitHub alike.

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

sourcery-ai Bot commented Oct 17, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds a shared, ordered preflight for both adoption leg repositories: local runs validate paths and directory writeability, GitHub runs use gh repo view, and check reports matching findings while downgrading unavailable GitHub queries to notes; late races remain protected by existing creation-time checks and notices.

Sequence diagram for adoption leg repository preflight

sequenceDiagram
    participant User
    participant Execute as execute
    participant Preflight as leg_remote_answers
    participant Disk as Local filesystem
    participant GitHub as gh
    participant Creator as create_leg_remotes

    User->>Execute: cmd_execute
    Execute->>Preflight: _refuse_what_stands_in_the_legs_way
    alt --local-remote-dir
        Preflight->>Disk: Check spec and code paths with os.path.lexists
        Preflight->>Disk: folder_in_the_way
    else GitHub mode
        Preflight->>GitHub: gh repo view org/P-spec --json name
        Preflight->>GitHub: gh repo view org/P-code --json name
    end
    alt findings exist
        Preflight-->>Execute: Refusal before either leg is made
        Execute-->>User: No repository created or pushed
    else no blocking findings
        Execute->>Creator: _create_leg_remotes
        Creator-->>User: Create spec and code repositories
    end
Loading

Sequence diagram for check repository findings

sequenceDiagram
    participant User
    participant Check as check
    participant Findings as leg_remote_findings
    participant GitHub as gh
    participant Disk as Local filesystem

    User->>Check: cmd_check
    Check->>Findings: _leg_remote_findings
    alt --local-remote-dir
        Findings->>Disk: Check both bare paths and folder_in_the_way
        Disk-->>Findings: Findings in spec-then-code order
    else GitHub mode
        Findings->>GitHub: gh repo view org/P-spec --json name
        Findings->>GitHub: gh repo view org/P-code --json name
        alt gh cannot answer
            GitHub-->>Findings: CommandFailed or OSError
            Findings-->>Check: NOTE, not a finding
        else repository exists
            GitHub-->>Findings: Existing repository
            Findings-->>Check: FINDING leg-remote-exists
        end
    end
    Findings-->>Check: Findings, remediation, and notes
    Check-->>User: Final check verdict
Loading

File-Level Changes

Change Details Files
Preflight both leg repositories and the local remote directory before creation, consolidating all blocking findings into one refusal.
  • Check local bare paths with lexists, including dangling links, and validate directory creation/writeability.
  • Check GitHub names with gh repo view, treating only the documented not-found response as absence.
  • Preserve unresolved paths and convert late directory-creation errors into named refusals.
  • Keep repository creation as a second-line race guard for changes occurring after preflight.
adopt-project.py
Make check perform the same leg-repository preflight as execute, while remaining usable when GitHub cannot be queried.
  • Report local and existing GitHub repositories as findings in execute order.
  • Report non-not-found GitHub failures as notes for check but refusals for execute.
  • Limit GitHub checks to valid repository names that execution would use.
adopt-project.py
Expose the directory obstruction location and preserve existing scaffold behavior.
  • Split directory validation into folder_in_the_way, returning both the blocking path and reason.
  • Retain folder_cannot_be_made as a compatibility wrapper.
scripts/shape_materialize.py
Update CLI documentation and test infrastructure for the new GitHub probes and refusal behavior.
  • Document preflight checks and offline notes in help and CLI documentation.
  • Extend the GitHub guard and stubs to answer exact repo view ... --json name calls.
  • Add coverage for local/GitHub collisions, unreadable GitHub responses, path failures, races, notices, and matching check output.
docs/cli.md
tests/gh_guard.py
tests/test_gh_guard.py
tests/test_adopt_execute_refusals.py
tests/test_adopt_late_failure.py
tests/test_adopt_filter_repo_failure.py
tests/test_adopt_github_exits.py
tests/test_adopt_leg_keys.py
tests/test_adopt_pins.py
tests/test_adopt_split_branch.py
tests/test_adopt_temp_work_roots.py
tests/test_pin_annotated_tag.py
tests/test_template_values_single_pass.py

Assessment against linked issues

Issue Objective Addressed Explanation
#798 Before creating either leg repository, have execute check both leg names and the viability of --local-remote-dir, refusing all detected conflicts before creating anything. ✅
#798 Make check report the same disk or GitHub leg-repository conflicts and local-directory problems under the corresponding flags, with GitHub lookup failures reported as notes rather than findings. ✅
#798 Handle an unusable local-remote directory without an uncaught traceback, including failures when creating it after the initial check. ✅

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-1, 2026-10-17T23:27:26Z) Lane 1's independent review of #861 at 99cc97e: CHANGES NEEDED

All three of the issue's lines are fixed on a local merge with current main. All four revert rows I re-ran match the writer's table, and the gate is green. One new defect blocks: a leg name that GitHub only redirects is now refused, and nothing gets the run past it. Separately, no CI has ever run on this head, because the PR has conflicted with main since it was opened.

The issue's lines (main 677f9bfa against the local merge)

Each line was measured with a stub gh first on PATH, GH_TOKEN=bogus-r798, an empty GH_CONFIG_DIR, GIT_CONFIG_GLOBAL=/dev/null, GIT_SSH_COMMAND=false and GIT_ALLOW_PROTOCOL=file. The source is acme/Atlas.

Line main merge
L1-73 local (<lrd>/Atlas-code.git there) check: plan ok. execute: exit 2, REFUSED creating the code leg repository. leg-remote-exists, then NOTHING has been rolled back: …Atlas-spec.git exists check: exit 1, FINDING leg-remote-exists: …Atlas-code.git, the code leg's bare repository, already exists. execute: exit 2, the same finding, and no Atlas-spec.git is made
L1-73 GitHub (code name taken) gh calls: repo create acme/Atlas-spec, then repo create acme/Atlas-code; the notice names https://github.com/acme/Atlas-spec.git gh calls: repo view acme/Atlas-spec, then repo view acme/Atlas-code; exit 2 REFUSED leg-remote-exists, and no create
L1-72 file exit 1, FileExistsError traceback exit 2, adopt-folder-cannot-be-made, and check finds it
L1-72 unwritable parent exit 1, PermissionError traceback the same as the file case
L1-72 unwritable dir exit 2, REFUSED a git or gh command failed … Permission denied the same as the file case

Fixed: all three lines.

Ordinary output against main:

  • check --local-remote-dir on an ordinary plan: stdout and stderr are byte-identical to main's.
  • check without the flag, offline: identical, plus the two NOTE lines.

Findings, ranked

1. A leg name GitHub only redirects is refused, and nothing gets the run past it (blocking)

GitHub's GraphQL repository(owner, name) follows renames by default.

  • The schema's followRenames argument reads: "Follow repository renames. If disabled, a repository referenced by its old name will return an error."
  • gh's FetchRepository sends only owner and name.

So gh repo view acme/Atlas-code --json name, for a repository renamed to Atlas-code-legacy, exits 0 and prints {"name":"Atlas-code-legacy"}. Yet the old name is free. GitHub's rename page warns: "do not reuse the original name of the renamed repository. If you do, redirects to the renamed repository will no longer work."

_in_the_way_on_github ignores stdout, so any exit 0 means "taken". A transfer leaves the same kind of redirect, and the stub below models it the same way. It answers {"name":"Atlas-code"}: the same name under another owner, so comparing name alone cannot catch it.

The PR's own remediation walks into this. ON_GITHUB_FIX says "another repository of that name is renamed on GitHub first". Renaming it creates exactly this redirect, and the re-run is refused again.

Repro. A gh stub models GitHub:

  • repo view of an existing name, or of a redirected one (following it), exits 0 and prints the JSON;
  • any other repo view gives the not-found words and exit 1;
  • repo create fails only for a name that really exists.

The setup: acme/Atlas-code-legacy exists, and acme/Atlas-code redirects to it. https://github.com/ is rewritten to a local root with GIT_CONFIG_COUNT/insteadOf.

adopt-project.py plan --source ./Atlas --project Atlas --org acme --elected-by 'T H' --elected-on 2026-09-02 --out plan.yaml
adopt-project.py check --plan plan.yaml
adopt-project.py execute --plan plan.yaml --yes --work-dir W

On main:

  • check: plan ok, exit 0.
  • execute: exit 0. The calls are repo create acme/Atlas-spec, repo create acme/Atlas-code, … pr create, and the adoption completes.

On the merge:

  • check: exit 1, FINDING leg-remote-exists: acme/Atlas-code, the code leg's repository, already exists on GitHub: \gh repo view acme/Atlas-code --json name` finds it`.
  • execute: exit 2, REFUSED leg-remote-exists: 1 finding(s) …. The only calls are the two views.

The transferred variant (acme/Atlas-code → other/Atlas-code) gives the same check FINDING on the merge and plan ok on main.

Fix.

  • Ask gh repo view <repository> --json nameWithOwner.
  • Count the name as taken only when nameWithOwner equals repository ignoring case. GitHub names are case-insensitive, so ACME/atlas-code is still taken.
  • Any other answer is a redirect, so the name is absent.
  • Answer JSON that cannot be read as adopt-remote-unreadable.
  • Move the guard's answered argv, the four stubs that answer the view (test_adopt_github_exits.py, test_adopt_leg_keys.py, GH_STUB and gh_clones) and the texts to nameWithOwner.
  • Add a row: a view that answers testorg/Northwind-code-old leads to both repo create calls.

2. The head has never had CI, because the PR conflicts with main (note for the landing)

3. The test guard now answers scaffold-project.py's and family.py's identical call (minor)

scaffold-project.py (remote_is_empty) and family.py run the same argv, gh repo view <repo> --json name. ASKS_WHETHER_IT_EXISTS cannot tell the callers apart. Using the per-test teardown decision itself:

failure([record(['repo','view','acme/Atlas','--json','name'], 'tests/test_scaffold_x.py::test_y (call)')])
  • main: returns a message, so the test fails at teardown.
  • merge: returns None, so the test passes.

Does it let another test pass today? No. Main is green with the guard refusing these calls, so no current test reaches one. No test asserts that a view is refused (grep GUARD_EXIT finds only test_gh_guard.py, which uses api user).

What it costs: a future scaffold or family test that reaches gh with no stub of its own gets exit 4 and that tool's remote-unreadable, not a teardown failure. I see no narrowing that tells adopt's call from the other two, so the cost needs saying (the text below).

4. Test honesty (nits)

  • The pins tests. test_adopt_pins.py (check_plan) and test_pin_annotated_tag.py now run every check under --local-remote-dir. So assert gh_calls(gh_env) == "" no longer covers the GitHub-mode check, which is the one users run without the flag.
    • Pin logic does not read local_remote_dir today; only _submodule_findings does. So nothing is false yet.
    • Keep at least one GitHub-mode check, asserting the log equals the two leg views exactly.
  • test_adopt_split_branch.py. TO_THE_LEGS = ["gh", "repo", "view"] compares only three words. Compare the full spec-leg argv (["gh", "repo", "view", "testorg/<P>-spec", "--json", "name"]), so that another repo view added earlier cannot pass for it.
  • The other changes are honest. The github_exits/leg_keys shims, gh_clones, template_values_single_pass (its check now takes the same --local-remote-dir as its execute) and filter_repo_failure's new notice words each change only what the new calls need.
  • Possible S1192, unverified because Sonar has not run. Counted with ast, leaving out f-string parts:
    • in tests/test_adopt_late_failure.py, "leg-remote-exists", "--bare" and "execute" each go from 2 to 3 uses;
    • in tests/test_adopt_execute_refusals.py, "check" goes from 2 to 3.
  • Two added lines are over 79 characters:
    • the @pytest.mark.parametrize("case", …) line in test_adopt_execute_refusals.py (82);
    • a docstring line in test_adopt_filter_repo_failure.py (113).

The attack points, answered

  1. check's network read.
  2. The guard: finding 3.
  3. The eight files: finding 4.
  4. Order.
    • _refuse_what_stands_in_the_legs_way sits immediately before _create_leg_remotes, after answered_path_record, which is where _create_leg_remotes stood on main.
    • _refuse_a_split_branch_left_behind and _refuse_a_branch_in_the_split_branchs_way are untouched, so their deferral to a leg on disk is unchanged.
    • check asks last, and only when the names classify and org: passes checked_value, which matches when execute gets that far.
  5. The notice.
    • What still reaches it after the first create:
      • a repository the token cannot see;
      • a rate limit or outage between the two creates;
      • Ctrl-C;
      • a path made after the question (tested);
      • a full disk, or an ACL that os.access cannot see, at the code leg's git init.
    • Its new clause holds: a re-run is refused before it makes either leg. A GitHub re-run after the split push is an exception, but main's wording already had it (see the checklist).
  6. Links.
    • Link-loop --local-remote-dir: main exits 1 with RuntimeError: Symlink loop. The merge refuses it by name in both commands: `…/loop` cannot be used as a folder: Too many levels of symbolic links. Python 3.13+ realpath does not raise on a loop in non-strict mode and returns the unresolved path, which is what absolute() returns, so CI's 3.x gets the same answer.
    • A dangling link at <lrd>/Atlas-code.git:
      • main: makes Atlas-spec.git, then fails with fatal: cannot mkdir …Atlas-code.git: File exists and gives the notice;
      • merge: refuses it as leg-remote-exists, with nothing made.
  7. Windows. Nothing new beyond the body's known limits.
    • os.access says yes; a mkdir that then fails is refused by name.
    • A link loop's OSError is caught in _the_remote_dir.
    • The new tests skip where they must: the #! stub, the unwritable-folder fixture, and the symlink test.
    • Their paths are legal on NTFS, and none compares bytes it read back.

My runs

The merge. A LOCAL-ONLY merge of 99cc97e8 with main 677f9bfa is not clean: two hunks conflict in scripts/shape_materialize.py, resolved as in finding 2. The result is ffb2371b.

The gate, with git-filter-repo on PATH, /usr/bin/python3 -m pytest, 17 files:

  • the 11 test files the diff touches;
  • test_adopt_source_clone_record.py, which imports gh_clones;
  • test_dry_run_parity.py and test_family_refuse_up_front.py, for the conflict;
  • test_shape_doctor.py, test_scaffold_leg_path_collisions.py and test_cli_reference.py.

Result: 1,551 passed, 3 skipped, 1 xfailed, EXIT=0.

Revert rows, each reverted alone on ffb2371b, running the two extended files. All match the writer's table.

Row Reverted Writer Mine
R1 execute asks nothing first 22 failed (10 tests) 22 failed (10 tests)
R2 check asks nothing 9 failed (3 tests) 9 failed (3 tests)
R6 any gh failure means absent 4 failed (2 tests) 4 failed (2 tests)
R12 _remote_urls resolves a link loop 2 failed 2 failed

#825 (#716) and #861 together, as the coordinator asked.

Exact text replacements

  1. If finding 1 is fixed, in What, under "Against GitHub", replace:

    A name that exists is refused as leg-remote-exists before any gh repo create.

    with:

    A name that exists is refused as leg-remote-exists before any gh repo create. gh's GraphQL follows a rename or a transfer, so the answer counts only when its nameWithOwner is the name asked, compared without case: a name GitHub only redirects is free, and gh repo create takes it.

    Then change every --json name in the body, the code's texts, docs/cli.md and the guard's docstring to --json nameWithOwner.

  2. In How, tests/gh_guard.py, replace:

    Only that exact argv is answered. --json visibility, repo view with any other shape, and repo create are still refused (tests/test_gh_guard.py).

    with:

    Only that exact argv is answered. --json visibility, repo view with any other shape, and repo create are still refused (tests/test_gh_guard.py). scaffold-project.py and family.py ask the same argv of their own repositories, so the guard answers theirs too: a test of either that reaches gh with no stub of its own now meets that tool's remote-unreadable refusal, where the guard failed it at teardown.

  3. In How, scripts/shape_materialize.py, after the merge round, replace:

    It is the same definition, now answering (where, why), so that adopt can spell the path. What the scaffold and family.py print is unchanged (tests/test_dry_run_parity.py).

    with:

    It is the same definition, now answering (where, why), so that adopt can spell the path; folder_cannot_be_made keeps scaffold-project.py, family.py and shape_materialize.py: #622's folder refusals print a folder name raw, so an ESC or a line break reaches the terminal, and family-operation-in-progress prints a holder's path across two lines #692's shown_source spelling of the folder. What the scaffold and family.py print is unchanged (tests/test_dry_run_parity.py, tests/test_family_refuse_up_front.py).

  4. In Tests, Gates: name the merged main and the new head after the merge round.

The other texts are true at the head: the title, the Lane and Claim lines, Closes #798, the robot line, and 15,797 characters by len().

Notes for the landing

For the checklist

Lane 1 takes a fix round on the same branch now. It merges main (resolving #692 as above, and #825 if that has landed, as lane 3 ruled) and fixes finding 1 with nameWithOwner, findings 3 and 4, and the texts. The same reviewer re-verifies.

Lane: openreposhape-1 (openRepoShape-1)

Brett Heap and others added 3 commits October 17, 2026 23:31
…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>
…ew --json nameWithOwner` answers with that very name, compared without case, so a name GitHub only redirects after a rename or a transfer is free, and an answer with no `nameWithOwner` to read is `adopt-remote-unreadable` (#798)

gh's GraphQL follows renames and transfers, so `gh repo view <old name>`
exits 0 for a name that is free; any exit 0 was "taken", and the PR's own
remedy (rename the other repository first) walked into it. The view now
asks `nameWithOwner`; an answer naming another repository is a redirect,
and the name is free. The test guard answers the new argv only, so it no
longer answers scaffold-project.py's and family.py's `--json name` view
(a new guard test shows theirs still refused); the four stubs answer
`nameWithOwner`; new tests cover a rename, a transfer, another case and
three unreadable answers. `"check"` in test_adopt_execute_refusals.py is
a constant now, and its long parametrize line is wrapped.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e-dir`, and its gh log is the two leg views exactly; the split-branch tests compare the spec leg's whole view; repeated literals are constants and a long docstring line is wrapped (#798)

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

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-1, 2026-10-18T01:33:32Z) Lane 1's re-verification of #861 at 748c8e6: CHANGES NEEDED

The four findings are fixed and measured. On a local merge with current main, the redirect repros now reach both repo create calls, the guard answers adopt's call alone, and #692's spelling is main's. The gate is green, and CI is green on the first run this PR has ever had.

What blocks: SonarCloud reports 4 new issues in the PR's own tests, three S5778 and one S9073. Those are smells lane 1's rules forbid. Each is a mechanical edit, given below.

The four findings from round 1, measured

The merge. A LOCAL-ONLY merge of 748c8e6a with main b9b70692 is clean (2fa257fa).

Every probe below ran on the merge with a gh stub that models GitHub's documented behaviour: repo view follows a rename or transfer redirect, and repo create takes a redirected name. The usual probe environment applied: a bogus GH_TOKEN, an empty GH_CONFIG_DIR, GIT_CONFIG_GLOBAL=/dev/null, GIT_SSH_COMMAND=false and GIT_ALLOW_PROTOCOL=file.

1. A redirected name is free now (fixed).

Case main b9b70692 merge
renamed (acme/Atlas-code redirects to acme/Atlas-code-legacy) execute exit 0 check: plan ok. execute: exit 0, calls repo view spec, repo view code, repo create acme/Atlas-spec, repo create acme/Atlas-code
transferred (redirects to other/Atlas-code) execute exit 0 the same as renamed
case-insensitive taken (acme/atlas-CODE exists) n/a check exit 1, FINDING leg-remote-exists: acme/Atlas-code, … \gh repo view acme/Atlas-code --json nameWithOwner` finds it. execute` exit 2, same finding, no create

Unreadable answers. For each exit-0 answer below, check printed a NOTE and passed, and execute exited 2 with adopt-remote-unreadable and made no create:

  • Atlas-code (not JSON);
  • {"name": …} (no field);
  • empty output;
  • [];
  • {"nameWithOwner": ""};
  • {"nameWithOwner": null};
  • an ESC sequence, which the message spells as \x1b[31mred\x1b[0m.

The texts read … exited 0, and what it printed holds no \nameWithOwner`: …and… exited 0 and printed nothing. Only adopt asks nameWithOwner`: no other tool in the repository does.

2. The merge with main (fixed). CI is green. The #692 resolution holds:

  • test_dry_run_parity.py and test_family_refuse_up_front.py pass in my gate.
  • folder_cannot_be_made prints byte for byte what main prints:
    • '<D>/a\x1bb' is a file, not a folder;
    • <D>/ro is a folder this user cannot write in.

3. The guard (fixed). The guard's per-test teardown decision, called with a record of each argv:

argv main merge
repo view acme/Atlas --json name (scaffold's and family's) refused, teardown fails refused, teardown fails
repo view acme/Atlas --json nameWithOwner (adopt's) refused, teardown fails answered, teardown passes

The new test_gh_guard.py case pins this, and it reads both sources for that argv.

4. The test nits (fixed).

  • One pins check runs with no flag, and its log equals exactly the two --json nameWithOwner views.
  • TO_THE_LEGS is compared whole.
  • The constants LEG_REMOTE_EXISTS, BARE_FLAG, EXECUTE and CHECK replace the repeated literals.
  • Both long lines are wrapped.

Finding (blocking): 4 new SonarCloud issues in the new tests

From SonarCloud's API (issues/search?pullRequest=861), total 4. The Quality Gate passed, but lane 1's rules forbid S5778 and S9073.

python:S5778 MAJOR tests/test_adopt_execute_refusals.py 1805  Refactor this exception test to have only one invocation possibly throwing an exception.
python:S9073 MAJOR tests/test_adopt_execute_refusals.py 1854  Split this composite assertion into separate assertions.
python:S5778 MAJOR tests/test_adopt_execute_refusals.py 1868  (S5778, as above)
python:S5778 MAJOR tests/test_adopt_late_failure.py     1337  (S5778, as above)

Each S5778 site calls an argument builder inside pytest.raises. Two of them (1868, and 1337) date from round 0, when Sonar never ran.

Exact replacements:

  1. tests/test_adopt_execute_refusals.py, in test_a_name_github_only_redirects_is_free:
        with pytest.raises(LegsMade):
            adopter.cmd_execute(against_github(world))
    becomes
        github_args = against_github(world)
        with pytest.raises(LegsMade):
            adopter.cmd_execute(github_args)
  2. The same file, in test_an_answer_with_no_name_to_read_is_unreadable:
        assert len(notes) == 1 and told(CODE, what) in notes[0], notes
    becomes
        assert len(notes) == 1, notes
        assert told(CODE, what) in notes[0], notes
  3. The same file, in test_a_name_github_hides_still_gets_the_notice_once_the_spec_leg_exists:
        with pytest.raises(adopter.CommandFailed):
            adopter.cmd_execute(against_github(world))
    becomes
        github_args = against_github(world)
        with pytest.raises(adopter.CommandFailed):
            adopter.cmd_execute(github_args)
  4. tests/test_adopt_late_failure.py, in test_a_code_leg_made_after_the_question_still_gets_the_notice only. The same two lines appear four more times in that file, in pre-existing tests.
        with pytest.raises(adopter.Refusal) as caught:
            adopter.cmd_execute(execute_args(tmp_path, plan))
    becomes
        run_args = execute_args(tmp_path, plan)
        with pytest.raises(adopter.Refusal) as caught:
            adopter.cmd_execute(run_args)

None of these changes what a test asserts. A re-run of the two files and a fresh SonarCloud read are enough to verify them.

My runs

The gate, with git-filter-repo on PATH, on 2fa257fa (main b9b70692 + 748c8e6a), 18 files:

  • the 11 test files the diff touches;
  • test_adopt_source_clone_record.py;
  • test_dry_run_parity.py and test_family_refuse_up_front.py;
  • test_no_auto_maintenance.py, main's new scan over every test file;
  • test_shape_doctor.py, test_scaffold_leg_path_collisions.py and test_cli_reference.py.

Result: 1,597 passed, 3 skipped, EXIT=0.

Revert rows, each reverted alone on 2fa257fa. All match the writer's table.

Row Reverted Files Result
R13 any exit 0 means taken the two extended files 5 failed: both redirects and the 3 unreadable answers
R14 names compared with case same 1 failed: the answer in other letters
R15 an unreadable answer means absent same 3 failed
R16 the guard answers --json name again test_gh_guard.py, test_adopt_execute_refusals.py 2 failed (both guard cases), 60 errors at teardown

From round 1, on ffb2371b, rows R1, R2, R6 and R12 matched too.

CI at 748c8e6a, the first CI this PR has had:

Check Result
tests pass (53m28s)
tests-git-bash pass
package pass
SonarCloud Quality Gate passed: 0.0% duplication on new code, 0 hotspots, 4 new issues (above)

The texts

The posted body is identical to the writer's file: 20,848 characters by len(). It has the Lane and Claim lines, Closes #798 and the robot line. The title, What, How, "Review round 1", Tests, the revert table and "For the checklist" are true at the head. One replacement:

  • The new known limit. check says nothing about a redirect either (plan ok in the probe). Replace:

    execute does not say so.

    with:

    Neither check nor execute says so.

After the Sonar fix, add one sub-bullet under "Review round 1" > "Test nits":

  • SonarCloud's first run found three S5778 (an argument built inside pytest.raises) and one S9073 (a composite assert) in the new tests; each argument is built before the with, and the assert is split.

Notes for the landing

For the checklist

Next (lane 1's coordinator): fix round 2 applies r798's four replacements as given, each argument built before the with and the composite assert split, plus the two body texts. It starts after lane 1's usage reset at 03:30Z. The same reviewer re-verifies with a re-run of the two files and a fresh SonarCloud read, and #861 then joins lane 1's v1.1.1 queue on #473, after #831. r798's checklist item, a NOTE when the name only redirects, stays in "For the checklist".

Lane: openreposhape-1 (openRepoShape-1)

….raises` in three tests, and a composite assert is split in two, as SonarCloud's first run on #861 asked (S5778, S9073) (#798)

SonarCloud's first analysis of this PR found four issues in its tests:
three S5778 (a call that can raise, `against_github(world)` or
`execute_args(tmp_path, plan)`, inside `pytest.raises`) and one S9073
(`assert len(notes) == 1 and told(CODE, what) in notes[0]`). Each
argument is now built on the line before the `with`, and the assert is
two.

Four tests change: `test_a_name_github_only_redirects_is_free`,
`test_an_answer_with_no_name_to_read_is_unreadable` and
`test_a_name_github_hides_still_gets_the_notice_once_the_spec_leg_exists`
in test_adopt_execute_refusals.py, and
`test_a_code_leg_made_after_the_question_still_gets_the_notice` in
test_adopt_late_failure.py. No test asserts anything new, and the four
older copies of those two lines in test_adopt_late_failure.py are left as
they were.

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

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-1, 2026-10-18T03:43:37Z) Lane 1's re-verification of #861 at 70801c9: CHANGES NEEDED

Fix round 2 is right: four test sites, no assertion changed, and SonarCloud now shows all four round-1 issues FIXED. But CI tests failed at 70801c91, and SonarCloud still counts one new issue. Both come from main moving after the branch's last merge (d8b8eb33), and the branch cannot fix either without merging main. So nothing changes on the branch now. The fixes below go into the deferred merge round, which then needs green CI and a SonarCloud read of 0.

1. The diff 748c8e6a..70801c91

One commit, 70801c91: two files, 8 insertions, 4 deletions. It touches exactly the four sites I gave:

  • test_a_name_github_only_redirects_is_free: github_args = against_github(world) is built before with pytest.raises(LegsMade):.
  • test_an_answer_with_no_name_to_read_is_unreadable: the composite assert is split into assert len(notes) == 1, notes and assert told(CODE, what) in notes[0], notes.
  • test_a_name_github_hides_still_gets_the_notice_once_the_spec_leg_exists: github_args is built before with pytest.raises(adopter.CommandFailed):.
  • tests/test_adopt_late_failure.py, test_a_code_leg_made_after_the_question_still_gets_the_notice only: run_args = execute_args(tmp_path, plan) is built before the with. The four older copies of those two lines are untouched.

No assertion changed. Each argument is still built after every monkeypatch/setenv above it, so the order things happen in is the same.

2. Tests on a LOCAL-ONLY merge with main 3fb30a8f

The merge is clean: commit 13260a6e, tree 5e510086, which matches the coordinator's merge-tree tree. #833 is 3fb30a8f, and its new test file is tests/test_adopt_plan_out_checked.py. #830 is 51414d2e.

Mode Files Result
1, git-filter-repo on PATH test_adopt_execute_refusals.py, test_adopt_late_failure.py, test_adopt_plan_out_checked.py, test_gh_guard.py, test_cli_reference.py, test_no_auto_maintenance.py, test_shape_doctor.py, test_scaffold_leg_path_collisions.py 931 passed, 3 skipped, EXIT=0 (17m47s)
2, git-filter-repo hidden (PATH=<stub gh>:/usr/local/bin:/usr/bin:/bin; command -v git-filter-repo returns 127, git filter-repo returns 1) the first six of those 379 passed, 8 skipped, EXIT=0

Both runs used /usr/bin/python3 -m pytest, a stub gh, a bogus GH_TOKEN, an empty GH_CONFIG_DIR, no GIT_ALLOW_PROTOCOL, and basetemps under my scratch. #833's plan changes and #861's check and execute changes hold together.

CI at 70801c91: tests FAILED

From gh pr checks 861:

Check Result
tests fail, 1h7m46s, completed 03:38:03Z
tests-git-bash pass
package pass
SonarCloud Code Analysis pass (Quality Gate)

The job log (a read-only GET) ends with 11 failed, 13980 passed, 181 skipped, 1 xfailed. All 11 failures are in tests/test_pin_source_top_level.py:

  • test_check_finds_a_recorded_path_that_is_no_clone_s_top_level: 7 cases;
  • test_check_asks_a_recorded_top_level_as_it_always_did: 4 cases.

Each fails with AssertionError: assert 'repo view te...meWithOwner\n' == ''.

Cause: a semantic conflict with main.

Reproduced with tests/test_pin_source_top_level.py:

  • on main 3fb30a8f: 48 passed;
  • on my merge 13260a6e: 11 failed, 37 passed.

The fix, for the merge round. It uses check_plan, which #861's tests/test_adopt_pins.py already provides. check_plan passes the same --local-remote-dir, so the empty log still proves "check reads no network for a pin". With this edit, the file on 13260a6e gives 48 passed. ADOPT and run_script are still used elsewhere in the file.

 from test_adopt_pins import (  # noqa: F401
-    ENTRY, PIN_FILE, PLANNED, adopter, gh_calls, gh_env, read_from,
-    with_pins,
+    ENTRY, PIN_FILE, PLANNED, adopter, check_plan, gh_calls, gh_env,
+    read_from, with_pins,
 )
@@ test_check_finds_a_recorded_path_that_is_no_clone_s_top_level
-    checked = run_script(ADOPT, "check", "--plan", str(plan), env=gh_env)
+    checked = check_plan(plan, env=gh_env)
@@ test_check_asks_a_recorded_top_level_as_it_always_did
-    checked = run_script(ADOPT, "check", "--plan", str(plan), env=gh_env)
+    checked = check_plan(plan, env=gh_env)

In the same CI run, no other file of the suite failed.

3. SonarCloud on 70801c91

The PR analysis is dated 02:30:23Z, at commit 70801c91, with the Quality Gate OK.

  • Resolved: the four round-1 issues are FIXED: three S5778 and one S9073.
  • Open: 1.
    python:S1192 CRITICAL adopt-project.py 3649 | Define a constant instead of duplicating this literal "refs/heads/" 4 times.
    

It comes from main's code, not the PR's.

New issues therefore read 1 at 70801c91, and the merge round's head must read 0.

4. The body

The posted body has 21,739 characters by len(), not counting the newline gh adds; that newline is the difference from the coordinator's 21,740.

  • Under "Test nits", the sub-bullet appears verbatim: "SonarCloud's first run found three S5778 (an argument built inside pytest.raises) and one S9073 (a composite assert) in the new tests; each argument is built before the with, and the assert is split." The bullet before it ends in ";".
  • The known limit reads "Neither check nor execute says so.", and "execute does not say so." no longer appears.
  • The Lane and Claim lines, Closes #798 and the robot line are present. The robot emoji is the only non-ASCII character, and there is no /tmp path or address.
  • The new "Round 2, head 70801c91" paragraph is true.

Add at the merge round, under "Review round 1", or in a "Merge round" note:

Notes for the landing

For the checklist

  • A gate of the files a diff touches misses main's new tests. A gate built from the files the diff touches cannot see a test file main adds later that runs check and asserts an empty gh log. At each merge round, look in main's new test files for check runs without --local-remote-dir that assert on a gh log.
  • Still open from round 1 (all already in the body): scaffold's and family's redirect problem with --json name; the notice's wording on GitHub after the split push; no timeout on check's two views; Path.exists() in the split-branch deferrals; bare.exists() in _create_leg_remote; README and the handbook not mentioning check's two reads; a NOTE when a name only redirects.

Next (lane 1's coordinator): the branch stays at 70801c91, because both remaining failures come from main moving after the branch's last merge, and only a merge fixes them. #861's merge round comes just before its turn, after lane 1's #831 lands, because #831 also touches adopt-project.py. It carries lane 3's cmd_check resolution against #825, r798's check_plan fix for tests/test_pin_source_top_level.py, and a gate that adds that file and tests/test_adopt_check_plan_name.py. The merged head then needs green CI, 0 new SonarCloud issues and r798's re-check before it lands.

Lane: openreposhape-1 (openRepoShape-1)

brettheap pushed a commit that referenced this pull request Oct 18, 2026
…estion beside a topic finding and skips it only after a name or an `org:` finding, whose fix renames both legs, and a misspelled `org:` past GitHub's 39 is shown cut (#863, #871's review)

#871's first head skipped #861's leg question after any of #863's
findings, a topic-only one too: a plan whose id was 40 characters and
whose code leg was already taken on GitHub listed `adopt-topic-too-long`
alone, and `leg-remote-exists` only once the id was mended, though an id
names no repository (RV871 S1, lane 3's fix (a)). `_held_findings` now
gives the codes too, and `check` skips the leg question only after
`adopt-name-too-long`, `adopt-org-too-long` or `adopt-bad-org`
(`RENAMES_THE_LEGS`): a shorter --project or another `org:` changes
`<org>/<Project>-spec` and `-code`, so the question would be asked of
names the mended plan does not use.

`refuse_an_owner_github_cannot_spell` shows a value past GitHub's 39 as
`refuse_an_owner_github_cannot_have` shows it, cut with `_both_ends` and
its length, so `check` no longer prints all of a 255-character `org:`
(N3).

Tests: `[id]` and `[derived-id]` expect the two leg views; a topic and a
taken leg are found in one round (S1); one `org:` both too long and
misspelled is listed length first by `check` and refused length first by
`execute`, `init`, the scaffold and setup's step (5) (N2); a cut value
(N3); and a plan whose names do not classify gets no held finding (N5).
The arguments are built before each `pytest.raises` and the composite
assert is split (S2: S5778, S9073).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brett Heap and others added 2 commits October 18, 2026 04:56
…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>
…take the `--local-remote-dir` that `check_plan` gives, as `test_adopt_pins.py`'s do, so the empty gh log still proves that `check` reads no network for a pin (#798)

Main's #842 (bee84d5) added tests/test_pin_source_top_level.py after
this branch's last merge. Its two `check` runs passed no
`--local-remote-dir` and asserted `gh_calls(gh_env) == ""`; since this
PR, `check` without that flag asks GitHub whether each leg repository
exists, so the stub logged the two views and CI's `tests` failed 11
cases (7 of `test_check_finds_a_recorded_path_that_is_no_clone_s_top_level`,
4 of `test_check_asks_a_recorded_top_level_as_it_always_did`). Both now
run `check_plan(plan, env=gh_env)`, imported from `test_adopt_pins`, as
r798's re-verification gave it. No assertion changed. The file gives
48 passed.

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

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-1, 2026-10-18T05:58:51Z) Verdict: PR-READY at 79d518db. No change is required. CI tests was still running when I stopped, so the verdict holds only if it goes green. One optional docstring fix is below, and it needs a push, which restarts that run.

1. The merge 77feec0c (parents 70801c91 and main 33419925)

2. 79d518db

  • It changes one file, tests/test_pin_source_top_level.py, with 4 insertions and 4 deletions.
  • The diff is exactly the one in r798-reverify2.md:
    • the import gains check_plan;
    • the two run_script(ADOPT, "check", "--plan", str(plan), env=gh_env) calls become check_plan(plan, env=gh_env).
  • No assertion changed. ADOPT and run_script are still used at lines 200, 246 and 250.

3. The writer's scan

  • Scope. I did the scan on the merged tree (HEAD^{tree} d14aa410, from 79d518db merged with 3bf2d3c9).
  • Method.
    • I listed every test function with a "check" run, 97 of them. 74 carry no --local-remote-dir marker.
    • I took every gh-log reader that touches adopt: gh_calls in test_adopt_pins.py, test_adopt_github_exits.py and test_adopt_template_values.py; gh_stub in test_template_values_single_pass.py; and gh_clones.
    • I read each check run in those files.
  • Result.
    • test_adopt_pins.py, test_pin_annotated_tag.py and test_template_values_single_pass.py use check_plan or pass the flag.
    • test_adopt_github_exits.py:817 and test_adopt_late_failure.py:1718 assert only the return code.
    • test_adopt_template_values.py's empty-log test is refused at the source before any name is asked.
    • Main's landings since 3fb30a8f that run check assert no gh log.
    • I found no other check run that asserts on a gh log.
  • Gate. I ran the files beyond your list too, and the gate below passed them in both modes.

4. The docstring in tests/test_adopt_check_plan_name.py

  • Measured. I ran the file with --basetemp and read the guard's own log (gh-guard0/gh-guard.log).

    gh calls in the file's 45 cases
    79d518db merged with 3bf2d3c9 62, all repo view <owner/name> --json nameWithOwner, all logged as probe, across 27 of the 45 cases
    plain main 3bf2d3c9 0
  • Ruling. The docstring is half false. "Runs gh" is wrong, because check runs it twice. "Reaches GitHub" stays true: the guard answers exit 4 and nothing leaves the machine.

  • Optional fix. Replace lines 37-40 of the module docstring. The new text is 7 lines, ASCII, every line at most 79 columns, and the file still parses.

Old:

Nothing here reaches GitHub or runs `gh`: each source's `origin` is an
`https://github.com/` URL that nothing fetches, and `execute` is run
in-process with every `git` and `gh` command it would run replaced by one
that fails the test (`refused_before_any_remote`).

New:

Nothing here reaches GitHub, though `check` does run `gh`: given no
`--local-remote-dir`, it asks `gh repo view` of each leg repository (#798),
twice, and the session's guard answers it as a `gh` with no login does
(`tests/gh_guard.py`). Each source's `origin` is an `https://github.com/` URL
that nothing fetches, and `execute` is run in-process with every `git` and
`gh` command it would run replaced by one that fails the test
(`refused_before_any_remote`).
  • Cost. A push restarts the tests run, about 1h08m. I suggest pushing it just before the landing's update-branch, so one CI run covers both. It is not a blocker.
  • Where the diff is. I applied it in my scratch tree only, saved it as /workspace/projects/openRepoShape-worktrees/_lane1-scratch/r798m/docstring-candidate.diff, and reverted it before the gate. The gate ran on the unedited merge.

5. Targeted gate (LOCAL-ONLY merge of 79d518db with origin/main 3bf2d3c9)

  • Merge. It is clean: commit 2bb79501, tree d14aa410, which equals git merge-tree --write-tree 3bf2d3c9 79d518db. The worktree was /workspace/projects/openRepoShape-worktrees/_lane1-scratch/r798m/tree, now removed.
  • Environment. /usr/bin/python3 -m pytest -p no:cacheprovider, stub gh first on PATH, GH_TOKEN=bogus-r798m, empty GH_CONFIG_DIR, GIT_CONFIG_GLOBAL=/dev/null, GIT_SSH_COMMAND=false, and no GIT_ALLOW_PROTOCOL. Each file ran on its own, with tmp_path_retention_policy=failed.
  • Hidden mode. PATH was <stub>:/usr/local/bin:/usr/bin:/bin. command -v git-filter-repo returned 1 and git filter-repo --version returned 1. In mode 1, command -v git-filter-repo gave /home/brett/.local/bin/git-filter-repo.
Mode Your 13 files My 15 extras Exit
git-filter-repo on PATH 1,373 passed, 5 skipped 784 passed, 3 skipped every file 0
git-filter-repo hidden 1,330 passed, 48 skipped 715 passed, 72 skipped every file 0

Per-file lines for the files you named, with git-filter-repo on PATH (hidden mode in brackets where it differs):

  • test_adopt_execute_refusals 181 passed (177, 4 skipped)

  • test_adopt_late_failure 88 (84, 4 skipped)

  • test_pin_source_top_level 48 (48)

  • test_adopt_check_plan_name 45 (45)

  • test_adopt_pins 146 (122, 24 skipped)

  • test_gh_guard 8 (8)

  • test_adopt_split_branch 47 (40, 7 skipped)

  • test_dry_run_parity 104 (104)

  • test_family_refuse_up_front 79 (79)

  • test_no_auto_maintenance 58, 2 skipped (58, 2 skipped)

  • test_shape_doctor 442, 3 skipped (438, 7 skipped)

  • test_scaffold_leg_path_collisions 104 (104)

  • test_cli_reference 23 (23)

  • The 15 extras. They are the other test files that run check and that main changed since 3fb30a8f or adopt-project.py: execute asks about both leg repositories, on the 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 touched. They are test_adopt_template_values, test_adopt_github_exits, test_template_values_single_pass, test_pin_annotated_tag, test_adopt_eol_block_through_links, test_adopt_git_reads, test_adopt_plan_escapes, test_adopt_source_clone_record, test_adopt_work_dir_refusal, test_non_utf8_name_callers, test_adopt_project_is_the_source, test_adopt_attributes_eol_block, test_stdout_encoding, test_adopt_entry_paths and test_adopt_temp_work_roots.

  • Against the writer's gate. All 28 files give counts identical to the writer's per-file logs in both modes. So the writer's 108-file results hold on the later main as well.

  • Main moved again. It is now fc289dd7, three landings past 3bf2d3c9: acc5a7bf, 3d81259b and fc289dd7.

    • git merge-tree --write-tree fc289dd7 79d518db is clean, tree ac139651.
    • The landings touch none of the PR's 16 files.
    • Their new tests run no adopt check; their gh_calls assertions are update_shape's.
    • The tests/conftest.py change only adds git helpers.

6. CI at 79d518db

gh pr checks 861 read at 05:56:20Z. The run is 39100766048, created 05:30:13Z on head 79d518db.

Check State
SonarCloud Code Analysis pass (quality gate OK)
package pass, 25s
tests-git-bash pass, 2m47s
tests pending, started 05:30:15Z; the last run took 1h07m, so it should finish near 06:38Z
  • The mergeable state read UNKNOWN at 05:56, because main had just moved.
  • GitHub merges the PR into main at the time the run starts. The tests run began at 05:30, before the three landings above, so it covers main 3bf2d3c9. That is the merge I gated, and its 28 files, including the 11 that failed at 70801c91, passed.

SonarCloud, PR 861:

  • The analysis is dated 05:30:18Z at commit 79d518db.
  • Open issues: 0. Bugs, vulnerabilities and code smells: 0. Hotspots: 0.
  • Quality gate conditions: all OK (new duplicated lines 0.0).
  • All five issues the PR has had are CLOSED with resolution FIXED, including python:S1192 "refs/heads/" on adopt-project.py.
  • The literal appears 4 times at 70801c91 and once at 79d518db, as on main. The PR's diff touches 0 lines holding it.

7. The body

The live PR body equals pr798-body.md, apart from the trailing newline: 23,873 characters in the file against 23,874 live. The only non-ASCII character is the robot emoji. There is no /tmp path.

Non-blocking note

This is the writer's point 4, and I measured it. check asks the two leg questions in the else: of the names' try, so a plan that already got plan-not-the-source still gets them. execute refuses at plan-not-the-source before it asks (refused_before_any_remote).

The guard log shows 4 gh calls in each test_a_plan_edited_to_a_typo... case (two check runs, two views each), and 2 in the --source case, whose one check ends in plan-not-the-source.

It was true at 70801c91 and is not a regression. It is not in the body. A one-line addition under "For the checklist" would fit:

  • check asks about the legs after plan-not-the-source. Its two leg questions are in the else: of the names' try, so a plan that names another repository still gets them, where execute refuses at plan-not-the-source before it asks.

Housekeeping

  • I pushed and posted nothing.
  • My scratch worktrees (r798m/tree and r798m/main-tree) are removed with git worktree remove --force. The shared checkout shows only ?? handoffs.
  • Logs and basetemps remain under /workspace/projects/openRepoShape-worktrees/_lane1-scratch/r798m/ (logs/, bt-*, docstring-candidate.diff). I did not use rm -rf.

Next (lane 1's coordinator): #861 is PR-READY at 79d518db, on condition that CI tests (run 39100766048) finishes green. The docstring fix is NOT pushed now: a push would restart the 68-minute tests run and cost #861 its turn. Both of r798m's notes are in the body's "For the checklist" instead, and the docstring goes to a follow-up. #861 takes lane 1's next turn in lane 3's round-robin, gated over the main of that moment. #843's merge round is running.

Lane: openreposhape-1 (openRepoShape-1)

@sonarqubecloud

Copy link
Copy Markdown

@brettheap
brettheap marked this pull request as ready for review October 18, 2026 06:46
@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 5 days and 18 hours by commenting @sourcery-ai review. Upgrade to get a review now.

@brettheap
brettheap merged commit fe99b8b into main Oct 18, 2026
5 checks passed
@brettheap
brettheap deleted the fix/798-adopt-legs-asked-first branch October 18, 2026 06:46
brettheap pushed a commit that referenced this pull request Oct 18, 2026
…org (restack after #861 landed as fe99b8b): step 1 of 3 merges, clean (#863)

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
… step 2 of 3 merges, adopt-project.py taken as ours (#861's own file, the squash equals its final head 25226ab there) (#863)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 18, 2026
…GitHub's limits on the topic, the `--org` and the holder's name before any repository exists, `check` finds what `execute` refuses from one list, and an `--org` GitHub gives no organisation (`.`, a trailing hyphen, `--`) is refused where GitHub is asked (#863) (#871)

`family.py init` set the holder's topic last, after `gh repo create` and the
push, and asked none of GitHub's limits: 50 for the topic, 39 for `--org`
and 100 for the holder's name. So a family of 40 characters made and pushed
its holder, then failed at the topic. `init` now asks all three where GitHub
is asked, before any `gh` call, so `--dry-run` refuses the same. There are
four new codes: `family-name-too-long`, `family-org-too-long`,
`family-bad-org` and `family-topic-too-long`.

`adopt-project.py check` asked none of what `execute` asks of the names, the
organisation and the topic, so it said `plan ok` over plans `execute`
refused. `_what_cannot_be_held` is now the one list, in `execute`'s order.
`execute` refuses its first refusal before either leg exists. `check` finds
every one as a FINDING under the same code, with `execute`'s remediation.

An `--org` GitHub gives no organisation is refused where GitHub is asked.
This is option (a) of the issue: the rule is in the tools' plans, and no
pinned file changes. `refuse_an_owner_github_cannot_spell` in
`scripts/shape_materialize.py` refuses a character outside ASCII letters,
digits, `_` and `-`, a trailing hyphen, and two hyphens in a row. Its codes
are `scaffold-bad-org`, `adopt-bad-org` and `family-bad-org`. `_` is let by,
for GitHub Enterprise Managed Users. None is asked under
`--local-remote-dir`. GitHub's numbers and characters are from its
documentation; no run here creates a real repository.

Lane 3's independent reviewer RV871 (opus) found it READY (#871
issuecomment-6245358225), and RV871b (opus) found the fix round after it
READY (#871 issuecomment-6245718752). After #861 and #859 landed, it was
restacked onto both squashes by five merges, and its changed lines equal
the reviewed delta (#871 issuecomment-6248107580). It lands on Brett Heap's
word "merge lane 3's v1.1.1 drafts after the v1.1.0 tag"
(2026-10-17T13:00:57Z).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 18, 2026
…say that `check` asks `gh repo view` once of each leg once the plan's names and `org:` pass, which the session's guard answers, and what `execute`'s in-process runs replace (#880) (#881)

Since #861 (#798), `check` asks `gh repo view <owner>/<name> --json nameWithOwner` once of the spec leg's repository and once of the code leg's, when it is given no `--local-remote-dir` and the plan's names and `org:` pass. Off Windows, the session's guard (`tests/gh_guard.py`) answers it as a `gh` with no login does, and nothing reaches GitHub. Six module docstrings said no `gh` ran, or said when it ran wrongly (#880):

| File | Calls (cases that call / cases) |
|---|---|
| `tests/test_adopt_check_plan_name.py` | 62 (27/45) |
| `tests/test_adopt_project_is_the_source.py` | 28 (10/22) |
| `tests/test_adopt_tracking_branch.py` | 18 (8/17) |
| `tests/test_adopt_origin_repository.py` | 4 (2/30) |
| `tests/test_template_text_placeholders.py` | 20 (10/142) |
| `tests/gh_guard.py` | "every `check` ... asks it twice", which 18 refused cases do not |

Each now says when `check` asks, and that the guard answers off Windows. The first two also had two more false sentences:
- each source's `origin` was "an `https://github.com/` URL". Now: where a source has an `origin`, it is a string `plan` and `check` read from the source's own config, and nothing fetches it.
- `execute`'s every `git` and `gh` command was "replaced". Now: every command it runs through `run`.

Module docstrings only: in each file, the AST outside the module docstring is unchanged.

Review: lane 1's reviewer found it PR-READY at `8345de60`. Lane 5's cross-lane review found it READY there with two SHOULDs, two NITs and four more files, all taken at `1d21a850`, and lane 5's re-check found `1d21a850` READY. Its last NITs and one more file go to #938.

It lands by fast refresh (Brett Heap, 2026-10-15T22:48:35Z). Its reviewed head `1d21a850` is green on Linux and SonarCloud. The branch merged main once and was never rebased; the last merge is update-branch's, clean, giving 7cd1b7d, which is green on SonarCloud and Git Bash. Lane 1's gate passed on its tree: the 8 test files it changes or that read the guard (`test_adopt_check_plan_name.py`, `test_adopt_project_is_the_source.py`, `test_adopt_origin_repository.py`, `test_adopt_tracking_branch.py`, `test_template_text_placeholders.py`, `test_gh_guard.py`, `test_no_auto_maintenance.py` and `test_repo_hygiene.py`), 405 passed and 0 skipped.

Closes #880

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