Skip to content

scaffold-project.py, family.py init and shape_materialize.py: the plan names the tracking branch, and on disk the visibility; an unread root or reused leg is said to be unread, the root before the plan; every holder gets the members' NOTE (#913) - #969

Merged
brettheap merged 2 commits into
mainfrom
l3-913-plan-visibility-notes
Oct 18, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Part of #913. Every section is done but U039, which needs a ruling (see "Left out"), so this does not close the issue.

What changed, by section

An unread visibility is taken as the run's, and the root is read twice (U163, U167; L2-92, L2-96)

  • shape_materialize.repository_visibility_answer is the one gh repo view --json visibility reader now. It answers the visibility, REPOSITORY_NOT_FOUND for gh's own not-found answer (gh_said_not_found, exit 1 with the words on stderr, the reading 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 gives refuse_unless_not_found), or None for any other failure. repository_visibility is that reading with not-found taken as unread, so family.py and its callers see what they saw before. It runs the command with subprocess.run, as organisation_plan does, because the reading is of stderr alone.
  • _leg_visibility reads the root once. Where the read failed (not where gh does not find the root, which is the ordinary --reuse-empty-repo run), it prints a leg twin of ROOT_VISIBILITY_UNREAD BEFORE the plan: with no --visibility, that a leg this run creates is created private and does not inherit the root's, and to pass --visibility before any leg exists; with one, that it cannot say whether the flag agrees with the root. Both name gh repo view <root> --json visibility.
  • _leg_visibility returns the root's read with the legs' visibility, and _create_remotes takes it for a reused root instead of asking GitHub a second time.
  • _reused_leg_visibility returns None for a leg whose read failed (it returned the run's visibility). _private_leg_note names each such leg in a NOTE of its own (_unread_legs_note: the question, what a private or internal answer needs, and the command that answers it), goes on naming the legs it could read as before, and asks the Free-plan note when a leg is closed OR unread, since an unread leg may be closed.
  • Decided where the Direction left it open: the pre-plan NOTE is printed with an explicit --visibility too, because the issue's Goal says a failed read is said before anything is created; and the unread-leg NOTE follows L2-92's own Direction ("says which leg's visibility it could not read ... and goes on by the legs it could read").

Free-plan words for an internal root (U164; L2-93)

  • free_plan_secrets_note answers an internal root whose organisation's plan is unread with FREE_PLAN_UNREAD_INTERNAL: GitHub gives internal visibility only to a repository of an organisation an enterprise account owns, which is not on the Free plan, so the plan is not what the hint hangs on. The NOTE is printed where it was and the plan is still asked, so nothing but the words changes. Adopt gets the same words through the shared function.

No credential NOTE for a public holder (U166; L2-95)

  • init prints the credential NOTE for every holder on GitHub, public, private, internal or unread, worded about the members: a private or internal member a person adds needs a way for the holder, public or not, to read it. It went by the holder's visibility and gave that as the reason.
  • Of the Direction's two options this takes the init one; add is lane 4's ground.

Test gap: the not-found-on-stdout-alone row (U168; L2-97)

  • The row's stdout is gh: Not Found (HTTP 404) (the file's NOT_AN_ORGANISATION) with a newline, and the body row is as it was. Mutant M15 below is killed by it; main's row lets the same mutant live (control run below).

The plan names no tracking branch, and no visibility in local mode (U010; AT-scaffold N3)

A private repository the token cannot see reads as absent (U060; L2-61)

  • Of the Direction's two options this takes the plan's words (a known-limit entry would be a release-notes file outside this change's files). The remotes row against GitHub says the run creates each repository gh does not find, and two lines under it (NOT_FOUND_WITH_THIS_TOKEN, shared by the scaffold and init) say that with this token a private repository it cannot see is not found either, and gh repo create then fails for it.

Left out

  • "Needs a ruling: what visibility: records" (U039) is out of scope and untouched: project.yaml's and family.yaml's visibility: are still filled from the run's --visibility, and the template comments are unchanged.
  • U166's add option (a NOTE from add for a private or internal member): add is lane 4's ground in scripts/family.py.
  • No refusal code is added or changed, so no code documentation changes.
  • adopt-project.py's docstring that names FREE_PLAN_UNREAD (around line 11634) is not in this change's files; it stays true of a private root.

Tests

Each ran under a stub gh first on PATH that exits 97, GH_TOKEN=bogus and an empty GH_CONFIG_DIR, as timeout 2400 /usr/bin/python3 -m pytest -q -p no:cacheprovider --basetemp=<scratch>/bt-<name> -o tmp_path_retention_policy=failed <files>, one at a time. The first eleven ran on the commit before its rebase onto 450279ca (the same diff; main's two new commits touch neither these files nor the code they run); the last two ran on the rebased head.

files result
tests/test_plan_visibility_notes.py (new) 15 passed
tests/test_free_plan_hint_reads.py 62 passed
tests/test_scaffold_reuse_seeded_legs.py 30 passed
tests/test_dry_run_asks_remotes.py 32 passed
tests/test_dry_run_parity.py 104 passed
tests/test_reuse_visibility.py tests/test_free_plan_hint.py tests/test_adopt_free_plan_hint_root.py 35 passed
tests/test_family.py 112 passed
tests/test_scaffold_e2e.py tests/test_scaffold_pin_and_reuse.py tests/test_tracking_branch_ref.py tests/test_family_tracking_branch.py tests/test_family_init_values.py 416 passed, 19 skipped (the template-dependent pytest.skips of test_family_init_values.py)
tests/test_setup_project_py.py tests/test_github_limits_init_check_org.py tests/test_github_name_limits.py tests/test_scaffold_manifest_values.py 492 passed
tests/test_agents_md_codes.py tests/test_cli_reference.py tests/test_update_add_codes_documented.py tests/test_repo_hygiene.py 157 passed
tests/test_no_auto_maintenance.py 60 passed
rebased head: tests/test_plan_visibility_notes.py tests/test_free_plan_hint_reads.py tests/test_scaffold_reuse_seeded_legs.py tests/test_ruleset_hint_tracking_branch.py 130 passed
rebased head: the four docs guards and tests/test_no_auto_maintenance.py 217 passed

tests/test_shape_doctor.py was not run: neither shape-doctor.py nor a helper it imports changed.

Mutants

Each was applied to the committed head, run against the named tests with -k, and put back with git checkout. All 16 were killed.

mutant killed by
M1 _reused_leg_visibility returns the run's visibility for an unread leg test_plan_visibility_notes.py::test_a_reused_leg_it_could_not_read_is_said_to_be_unread (3 of 3 rows)
M2 no unread-leg NOTE the same 3 rows
M3 the Free-plan note only for closed legs the same test, [private-legs-under-a-public-run]
M4 no NOTE before the plan for an unread root test_a_root_it_could_not_read_is_said_before_the_plan[default], [chosen]; test_a_real_run_reads_the_reused_root_once[unread]
M5 not-found not told apart (repository_visibility_answer returns None) test_a_root_gh_does_not_find_or_reads_is_not_said_to_be_unread[not-found]; test_free_plan_hint_reads.py::test_a_visibility_read_is_one_of_three_not_found_or_unread[not-found]
M6 _create_remotes reads the root again test_a_real_run_reads_the_reused_root_once[read], [unread]
M7 no scaffold branch row test_the_plan_on_disk_names_the_branch_and_the_visibility[defaults-scaffold], [chosen-scaffold]; test_the_plan_on_github_says_not_found_is_what_this_token_sees[scaffold]
M8 no scaffold local visibility row test_the_plan_on_disk_names_the_branch_and_the_visibility[defaults-scaffold], [chosen-scaffold]
M9 no init branch row the same tests' family rows (3)
M10 no init local visibility row test_the_plan_on_disk_names_the_branch_and_the_visibility[defaults-family], [chosen-family]
M11 no not-found lines in the scaffold's plan test_the_plan_on_github_says_not_found_is_what_this_token_sees[scaffold]
M12 no not-found lines in init's plan test_the_plan_on_github_says_not_found_is_what_this_token_sees[family]
M13 an internal root's unread plan told FREE_PLAN_UNREAD test_free_plan_hint_reads.py::test_the_note_goes_by_both_answers[internal-unread-plan]
M14 init's NOTE only for a private or internal holder test_init_notes_the_members_credential_for_every_reused_holder[public], [visibility-502], [visibility-404]; test_init_asks_nothing_of_a_holder_it_creates[public]; test_init_takes_a_holder_gh_cannot_be_asked_about_as_unread
M15 gh_said_not_found searches stdout too test_the_plan_is_free_other_or_unread[not-found-on-stdout-alone]; test_a_visibility_read_is_one_of_three_not_found_or_unread[not-found-on-stdout-alone]
M16 repository_visibility keeps not-found test_a_visibility_read_is_one_of_three_not_found_or_unread[not-found]

Control for M15: main's tests/test_free_plan_hint_reads.py with the same mutant, -k plan_is_free_other_or_unread: 11 passed, so main's row did not catch it.

Merges

git merge-tree --write-tree of this head (e016994872) with each, all clean (exit 0, no conflicted file):

W909's, W911's and W915's branches in scaffold-project.py were not pushed when this was opened, so they were not merged against. This change's hunks there are remote_visibility, _leg_visibility, _print_plan_actions, _create_remotes, _reused_leg_visibility, _private_leg_note, a new _unread_legs_note, the scaffold's shape_materialize imports, and the two _scaffold lines that call _leg_visibility and _create_remotes. In scripts/family.py they are _print_init_plan, _finish_holder_remote and one import after the GitHub-limits group.

Fix round 1 (head f3802d6f)

RV969 found the head e016994872 READY: no MUST, two non-blocking SHOULDs and six NITs. This round makes SHOULDs 1 and 2 and NITs 1, 2, 4 and 5, in one commit on the same branch, as a fast-forward. Behaviour changes only in the two NOTEs' words (NITs 4 and 5).

  • SHOULD 1. _private_leg_note returns first for a --local-remote-dir run and drops its three not local and guards, which was SonarCloud's S3776 at cognitive complexity 16 where 15 is allowed. Counted by hand under the rules that give the old text 16 (each if, each and/or sequence, each conditional expression, nesting added), the new text is 14. A local run still prints nothing from it: the two local-remote tests of test_scaffold_reuse_seeded_legs.py fail on the mutant that drops the return (below).
  • SHOULD 2. test_a_reused_leg_it_could_not_read_is_said_to_be_unread has a fourth row, one-leg-not-found, whose spec leg's --json visibility read is gh's own not-found answer, visibility_errors={f"{PROJECT}-spec": "GraphQL: Could not resolve to a Repository"}, under --visibility public. The error text is a column of the table now (error, BAD_GATEWAY in the three rows that had it).
  • NIT 1. The same test counts the unread-leg NOTE in result.stdout alone, so the NOTE on stderr no longer passes.
  • NIT 2. The 115-character docstring line of tests/test_free_plan_hint_reads.py is wrapped.
  • NIT 4. scripts/family.py's members' NOTE says "a member you add that is, or has a leg that is, private or internal" where it said "a private or internal member you add": a public member with private legs needs the credential too, since the holder's validate runs --recursive. Only tests/test_free_plan_hint_reads.py's member_note pinned the old words, and it is changed with it.
  • NIT 5. _unread_legs_note ends "cannot check them out" for two legs and "cannot check it out" for one (it said "it" for both). The test's unread_legs helper builds the same.
  • Not in this round: NIT 3 (adopt-project.py's docstring) and NIT 6 (shape_materialize.py:1970) are lane 3's follow-ups, outside this PR's files.

Tests, under the stub gh, GH_TOKEN=bogus, an empty GH_CONFIG_DIR and GIT_SSH_COMMAND=false, one at a time, as above, on the committed bytes:

files result
tests/test_plan_visibility_notes.py tests/test_free_plan_hint_reads.py tests/test_scaffold_reuse_seeded_legs.py 108 passed
tests/test_family.py tests/test_family_init_values.py 218 passed, 19 skipped
tests/test_dry_run_parity.py 104 passed
tests/test_agents_md_codes.py tests/test_cli_reference.py tests/test_repo_hygiene.py 151 passed
tests/test_no_auto_maintenance.py 60 passed

Mutants of this round, each applied to the committed text, run with -k reused_leg on the named files, and put back:

mutant killed by
the unread-leg NOTE printed on stderr (RV969's survivor) test_a_reused_leg_it_could_not_read_is_said_to_be_unread, all 4 rows
a not-found leg read taken at the run's visibility (RV969's survivor; _reused_leg_visibility returns args.visibility for not-found) test_a_reused_leg_it_could_not_read_is_said_to_be_unread[one-leg-not-found] (the other 3 rows pass, as they should)
the two-leg NOTE ending "cannot check it out" again the same test, [private-legs-under-a-public-run] and [public-legs-under-a-private-run]
_private_leg_note without its early return for a local remote the gh guard at teardown of test_three_empty_repositories_are_all_reused_with_the_flag and test_legs_holding_the_seed_are_reused_at_their_commits (test_scaffold_reuse_seeded_legs.py): the local run then asks gh api orgs/testorg --jq .plan.name
init's members' NOTE in its old words test_init_notes_the_members_credential_for_every_reused_holder, all 5 rows

Merges, git merge-tree --write-tree of the new head f3802d6fd452ffbd7deb9b92f4f286342ba2c8ab with each, all clean (exit 0, no conflicted file): origin/main at 293ef2a5, #971 (l3-915-scaffold-git-prompts) at e40af32d, and #972 (l3-911-930-scaffold-init-flags) at 0094731b.

Lane: openreposhape-3 (openRepoShape-3)
🤖 Generated with Claude Code

…lan names the tracking branch, and on disk the visibility, and says "not found" is what this token can see; a root or reused leg whose visibility GitHub did not give is said to be unread, the root before the plan and read once; every holder gets the members' credential NOTE; and an internal root's unread plan is no longer tied to the Free plan (#913)

- U163, L2-92: `_reused_leg_visibility` returns None for a read that
  failed, where it returned the run's visibility, and `_private_leg_note`
  names each such leg in a NOTE of its own, goes on by the legs it could
  read, and asks the Free-plan note when a leg is closed or unread.
- U167, L2-96: `_leg_visibility` reads the root once, through
  `repository_visibility_answer`, which tells gh's not-found answer apart,
  and says before the plan when the read failed, so `--visibility` can be
  passed before any leg exists; `_create_remotes` takes that read instead of
  asking GitHub again.
- U164, L2-93: an internal root whose organisation's plan is unread is told
  `FREE_PLAN_UNREAD_INTERNAL`, which says the plan is not what the hint
  hangs on.
- U166, L2-95: `init` prints the credential NOTE for every holder on GitHub,
  worded about the private or internal members a person adds.
- U168, L2-97: the `not-found-on-stdout-alone` row holds gh's not-found
  words on stdout.
- U010, AT-scaffold N3: the scaffold's and `init`'s plans have a `branch`
  row, and under `--local-remote-dir` a `visibility` row.
- U060, L2-61: the `remotes` row says it creates what gh does not find with
  this token, and that a private repository it cannot see is not found
  either.

U039 ("Needs a ruling": what `visibility:` records) is left as it is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Oct 18, 2026

Copy link
Copy Markdown

Reviewer's Guide

The PR refines GitHub visibility handling across scaffold and family initialization, explicitly distinguishing unread repositories from not-found ones, reusing a single root read, improving plan and credential diagnostics, and adding comprehensive regression tests for the resulting behavior.

Sequence diagram for scaffold visibility planning and remote creation

sequenceDiagram
    participant Scaffold as scaffold-project.py
    participant Visibility as shape_materialize.repository_visibility_answer
    participant GitHub as GitHub gh CLI
    participant Plan as Plan output
    participant Remotes as Remote creation

    Scaffold->>Visibility: repository_visibility_answer(root)
    Visibility->>GitHub: gh repo view root --json visibility
    GitHub-->>Visibility: visibility, REPOSITORY_NOT_FOUND, or failure
    Visibility-->>Scaffold: root_seen
    alt root read failed
        Scaffold->>Plan: Print unread-root NOTE before plan
    else root not found
        Scaffold->>Plan: Treat root as ordinary create run
    end
    Scaffold->>Plan: Print branch and remote visibility rows
    Scaffold->>Remotes: _create_remotes(root_seen)
    Remotes->>GitHub: Create or reuse root and legs
    Remotes-->>Scaffold: Standing visibility per repository
Loading

Sequence diagram for unread reused-leg diagnostics

sequenceDiagram
    participant Scaffold as scaffold-project.py
    participant Visibility as shape_materialize.repository_visibility
    participant GitHub as GitHub gh CLI
    participant Notes as Diagnostic notes

    Scaffold->>Visibility: _reused_leg_visibility(leg)
    Visibility->>GitHub: gh repo view leg --json visibility
    GitHub-->>Visibility: visibility or unread result
    alt visibility unread
        Visibility-->>Scaffold: None
        Scaffold->>Notes: _unread_legs_note(leg)
        Scaffold->>Notes: free_plan_secrets_note(...)
    else visibility available
        Visibility-->>Scaffold: leg visibility
        Scaffold->>Notes: Credential and Free-plan notes as applicable
    end
Loading

Flow diagram for family init plan and holder credentials

flowchart TD
    Init[family.py init] --> Plan[_print_init_plan]
    Plan --> Local{--local-remote-dir}
    Local -->|yes| LocalRows[Print visibility and branch rows]
    Local -->|no| GitHubRows[Print gh repo create visibility and not-found token limits]
    LocalRows --> Holder[_finish_holder_remote]
    GitHubRows --> Holder
    Holder --> Credential[Print members credential NOTE for every GitHub holder]
    Credential --> PlanHint[Add Free-plan hint when applicable]
Loading

File-Level Changes

Change Details Files
Separated repository visibility reads into explicit not-found, known-visibility, and unread outcomes, while avoiding duplicate root reads during scaffold reuse.
  • Added a subprocess-based visibility reader that distinguishes GitHub not-found responses from other failures and retained a compatibility wrapper treating not-found as unread.
  • Reported unread reused roots before the plan and propagated the single root read into remote creation.
  • Represented unread reused legs as unknown and added leg-specific diagnostics instead of substituting the run visibility.
  • Adjusted Free-plan hint selection for unread internal repositories.
scripts/shape_materialize.py
scaffold-project.py
Expanded scaffold and family initialization plans to expose the tracking branch, local visibility semantics, and token-specific not-found behavior.
  • Added branch rows to both plans.
  • Added visibility rows for local bare-repository mode.
  • Clarified GitHub remote-creation rows with the meaning of repositories not found by the current token.
scaffold-project.py
scripts/family.py
Made family initialization credential guidance describe member access rather than holder visibility and emit it for every GitHub-backed holder.
  • Print the members' credential NOTE for public, private, internal, and unread holders.
  • Preserved Free-plan guidance while using the holder's actual or unread visibility.
scripts/family.py
Added regression coverage for visibility state handling, plan output, notes, internal-plan wording, and stdout-only not-found cases.
  • Added end-to-end plan and NOTE assertions for scaffold reuse and family initialization.
  • Updated visibility and Free-plan reader tests for subprocess results and explicit not-found classification.
  • Covered branch/local-visibility plan rows and all 16 listed mutants.
tests/test_plan_visibility_notes.py
tests/test_free_plan_hint_reads.py

Possibly linked issues


Tips and commands

Interacting with Sourcery

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

Customizing Your Experience

Access your dashboard to:

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

Getting Help

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-10-18T18:55:03Z) RV969's independent review of #969 at e0169948: READY. No MUST; two non-blocking SHOULDs and six NITs. RV969 is lane 3's opus reviewer, under ruling 8. It probed with a stub gh whose visibility read answers public, private, internal, GraphQL not-found, gh: Not Found (HTTP 404) or HTTP 502.

Each section against its Direction:

  • U163/U167:
    • not-found and failed reads are told apart;
    • repository_visibility still maps not-found to None, so family.py behaves as before;
    • a real reuse run reads the root once (main: twice), and each leg once;
    • the NOTE before the plan is right with and without --visibility;
    • a leg whose read fails gets its own NOTE;
    • the Free-plan note is asked when a leg is closed or unread.
    • On main, unread public legs were called private (L2-92 reproduced).
  • U164: the internal-root sentence is true, since internal repositories exist only in enterprise-owned organisations, which are not on Free. It is also useful. Adopt prints it through free_plan_secrets_note (checked in process).
  • U166: init prints the members' NOTE for every holder on GitHub, a public one included.
  • U168: the new row passes on main's reader, which is correct, and kills M15, which main's row let live (control: 11 passed).
  • U010: under --local-remote-dir, the dry-run and real-run plans are byte-identical for the scaffold and init. project.yaml, family.yaml and the bare HEAD agree with the new rows.
  • U060: the plan's wording is true.

family.py: the hunks are init's (_print_init_plan, _finish_holder_remote), plus one module-level import. None touches a hunk of #946, #955, #957 or #960.

SHOULD (not blocking):

  1. scaffold-project.py:1601: _private_leg_note gets a new SonarCloud S3776, cognitive complexity 16 where 15 is allowed. Put if local: return at the top and drop the three not local and at :1647, :1661 and :1663. With that change, the three related test files pass (107 passed).
  2. scaffold-project.py:1590-1592: no test pins a reused leg that gh does not find on the visibility read. The mutant "return the run's visibility for not-found" survives 124 tests. Add a row with visibility_errors={f"{PROJECT}-spec": "GraphQL: Could not resolve to a Repository"} to test_a_reused_leg_it_could_not_read_is_said_to_be_unread.

NIT:

  1. tests/test_plan_visibility_notes.py:190 counts the unread-leg NOTE in stdout plus stderr, so a stderr mutant survives. Count it in result.stdout.
  2. tests/test_free_plan_hint_reads.py:263 is a 115-character line. Re-wrap it.
  3. adopt-project.py:11634-11636 still says adopt's words are unchanged. That is no longer true for an internal root with an unread plan. A follow-up, outside this PR's files.
  4. scripts/family.py:2985: a public member with private legs needs the credential too, since the holder's validate runs --recursive. Say "a member you add that is, or has a leg that is, private or internal".
  5. scaffold-project.py:1691: the two-leg NOTE ends "cannot check it out"; "them" reads better.
  6. shape_materialize.py:1970: an internal root on a "free" plan still gets FREE_PLAN_HINT, which calls it private. This predates the PR; optional.

Tests, at the head, one at a time under a stub gh:

Files Result
plan_visibility_notes + free_plan_hint_reads 77 passed
dry_run_parity + reuse_visibility 106 passed
family + family_init_values 218 passed, 19 skipped
scaffold_e2e 46 passed
the docs guards 151 passed
no_auto_maintenance 60 passed
scaffold_reuse_seeded_legs + dry_run_asks_remotes + free_plan_hint + adopt_free_plan_hint_root 95 passed
on the head merged with main 2f5cabf5: the PR's two test files 77 passed

Mutants:

  • M5, M6, M13 and M15, re-made: all killed.
  • RV969's own, killed:
    • the NOTE only when no leg is closed;
    • the NOTE before the plan for a not-found root;
    • the NOTE before the plan on stderr.
  • Survivors: the unread-leg NOTE on stderr (NIT 1), and a not-found leg taken at the run's visibility (SHOULD 2).

Merges: git merge-tree --write-tree is clean in every case.

Next: lane 3 runs a short fix round for SHOULDs 1-2 and NITs 1, 2, 4 and 5, and RV969 re-checks the delta. NITs 3 and 6 go to follow-ups.

Lane: openreposhape-3 (openRepoShape-3)

…s it could not read ends "cannot check them out", `init`'s members' NOTE names a member that is, or has a leg that is, private or internal, and a reused leg gh does not find is pinned as unread, so it is no longer taken at the run's visibility (#913)

Fix round 1 on RV969's review of #969.

- SHOULD 1: `_private_leg_note` returns first for a `--local-remote-dir`
  run and drops its three `not local and` guards, which brings its
  cognitive complexity under SonarCloud's 15. Nothing it prints changes.
- SHOULD 2: `test_a_reused_leg_it_could_not_read_is_said_to_be_unread` has a
  row whose leg read is gh's not-found answer, which fails where
  `_reused_leg_visibility` takes that read at the run's visibility.
- NIT 1: the same test counts the unread-leg NOTE in stdout alone, so the
  NOTE on stderr is caught.
- NIT 2: a 115-character docstring line in test_free_plan_hint_reads.py is
  wrapped.
- NIT 4: `init`'s members' NOTE names "a member you add that is, or has a leg
  that is, private or internal": a public member with private legs needs the
  credential too, since the holder's `validate` runs `--recursive`.
- NIT 5: `_unread_legs_note` says "cannot check them out" for two legs.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@brettheap

Copy link
Copy Markdown
Contributor Author

RV969 round 2 (lane 3's independent opus review) of #969's fix round 1, head f3802d6: READY.

The delta (git diff e016994 f3802d6) touches only scaffold-project.py, scripts/family.py and the two PR test files. Each change is correct:

  • SHOULD 1: _private_leg_note returns first for a local run. A local real run prints no NOTE, as before. On seven remote probes, stderr and the gh call log are byte-identical to round 1, and stdout differs only in the shape commit, the commit ids and the intended "them". Cognitive complexity is now 14 by recount, and SonarCloud reports 0 open issues on the PR.
  • SHOULD 2: the one-leg-not-found row pins a leg gh does not find as unread.
  • NIT 1, 2, 4 and 5 are done. NIT 3 (adopt-project.py docstring, outside this PR's files) and NIT 6 (optional) are left for later.

Tests on f3802d6, one at a time under a stub gh: test_plan_visibility_notes.py 16 passed; test_free_plan_hint_reads.py 62 passed; test_family.py 112 passed.

Mutants: "unread-leg NOTE on stderr" is now killed by all four rows of test_a_reused_leg_it_could_not_read_is_said_to_be_unread; "not-found leg taken at the run's visibility" is now killed by its one-leg-not-found row.

Merge: git merge-tree --write-tree f3802d6 origin/main (66177b6) is clean.

CI: SonarCloud, package and tests-git-bash pass; the Linux tests job was still running at 19:14Z and must be green before landing.

Lane: openreposhape-3 (openRepoShape-3)

@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 by commenting @sourcery-ai review. Upgrade to get a review now.

@brettheap
brettheap merged commit c4dd19d into main Oct 18, 2026
5 checks passed
@brettheap
brettheap deleted the l3-913-plan-visibility-notes branch October 18, 2026 20:34
brettheap added a commit that referenced this pull request Oct 19, 2026
…user cannot read is refused by name, `member-unreadable`, exit 2, so Python 3.14 no longer clones a member on its remote's default branch, fetches into a clone it never identified, or says `siblings ok` and `family bootstrap ok` over members it skipped as absent (part of #430) (#951)

Slice F of lane 1's plan for #430 (issuecomment-6251069236), in the family
root's two pinned scripts, templates/family-root/scripts/siblings.py and
bootstrap.py. Each guard asked `Path.is_file()` or `Path.exists()` of a
member's path. Python 3.12 and 3.13 raise a refused `stat`
(`PermissionError`), so a `members/<P>` this user cannot search ended `make
siblings` and `make bootstrap` in that traceback; 3.14 answers False to any
`OSError`, so the path read as absent. Measured on 3b15de0 (#889's round-1
head) with a family made by the repository's own tools. Under a real mode on
Python 3.12.3 every site below ended in a `PermissionError` traceback, exit
1. Under a 3.14-like shim (`is_file`, `is_dir`, `exists` and `is_symlink`
answer False to any `OSError`):

- siblings.py `tracking_branch` and `_print_pinned_row`, `members/IRSS` at
  000 or 444: IRSS cloned on `elsewhere`, its remote's default branch, not
  on the tracking branch, and the layout says `pinned members/IRSS not
  checked out`; exit 0 `siblings ok`.
- `project_id`, a clone's `project.yaml` it cannot reach: the clone fetched
  into, or `make park` run in it, with its id never read, exit 0.
- `run_member_bootstrap`: "no scripts/bootstrap.py; cloned and left as it
  is", its legs never placed, exit 0. `dispatch`'s `Makefile`: "has no
  Makefile".
- bootstrap.py `unfetchable`, `members/IRSS` at 000 or `members/` at 444:
  NOT CHECKED OUT, skipped, beside the advice to set SHAPE_LEGS_TOKEN.
  `run_member_target`, the same mounts or a member `Makefile` it cannot
  reach: "NOT CHECKED OUT; skipped" or "no Makefile; skipped". Both exit 0
  `family bootstrap ok`.
- `members_of`, a `family.yaml` it cannot reach: `family-manifest-missing`,
  "does not exist".

Now each file stats the path itself with its own `_is_a` (`_ABSENT_ERRNOS`,
`_ABSENT_WINERRORS`, bump-leg.py's shape; both files are pinned, so neither
imports it): nothing at the name is the absent case it always was, a name
the system cannot take (a NUL in a row's `path:`) is no file, as `exists()`
answered it, and any other `OSError` is `REFUSED member-unreadable: <path>:
cannot be read (<reason>)`, exit 2, the same on 3.12 and 3.14. A permission
gets validate-pins.py's `chmod u+r` / `chmod u+rx` advice (bootstrap.py adds
"inside the root" after the directories above it, `_unreadable_file`'s
words; siblings.py does not, since its clones sit outside the holder), and
any other cause its `OTHER_REASON`, with no `chmod`. `family.yaml` is
`family-manifest-unreadable`, the code a `family.yaml` that is no mapping
already has.

- siblings.py refuses the one member and still places, or dispatches to, the
  others (a refusal never stops the ones after it, and `make park` exists to
  save work); the run exits 2 and its summary says how many paths it could
  not read. A refused `scripts/bootstrap.py` says the clone was made and to
  run `python3 scripts/bootstrap.py` in it, since a second `make siblings`
  only fetches.
- bootstrap.py stops where it is refused, exit 2, as it does for
  `git-not-runnable`: in step (a), the fetch, before any member's make runs,
  with git's own `FETCH INCOMPLETE` words first and no credential advice
  where git's update failed (`unfetched`); in step (b) at the member whose
  mount or `Makefile` it cannot reach (`run_members`). `main` catches the
  refusal from either once, stdout flushed first.

Rounds and review. The writer is lane 4's W430F; the reviewer is lane 5's
rv951.

- Round 0, 528adbf, stacked on #889's 29083a5. Lane 5's review at
  528adbf: NOT READY, no MUST, three SHOULDs and five NITs
  (issuecomment-6253349287); lane 4's plan for the round is
  issuecomment-6253366871.
- Round 1, 3c5f2ea, a fast-forward on 528adbf (issuecomment-6254509055).
  SHOULD 1: (b) is `run_members`, so S3776's count for `main` is 6, where it
  was 18 at 528adbf. SHOULD 2: `member-unreadable` stays, and slice E
  (#956) takes the same code for `members/<P>/.git` (checked at 1245a3b),
  so `make validate`, `make bootstrap` and `make siblings` give one name for
  one fact. SHOULD 3: a NUL in a name is no file. NIT 4: the placement
  summary's words are tested. NIT 5: the reason `parent_folder_warning` is
  left alone is corrected. NIT 6: git's own words come before step (a)'s
  refusal. NIT 7: the errno tests are three, with true names. NIT 8:
  bootstrap.py says "inside the root".
- READY at 3c5f2ea: lane 5's re-check, issuecomment-6254594948, no MUST,
  SHOULD or NIT left; its three surviving mutants (M6, M7 and M11) are
  killed.
- Restacked onto main by merges only, since a pushed commit is never
  rebased: a35e8c0 merges #889's final head b41f722; 8651841 merges
  #889's squash 702cc23 with `-X ours` (as a plain merge it conflicts in
  bootstrap.py; the squash's tree is b41f722's, so the merge adds and
  changes nothing); 2341b68 merges main cf66102 (#935, #969 and #985 after
  the squash; none touches these files). Lane 4's reply is
  issuecomment-6266695040.
- READY at 2341b68: lane 5's re-check, issuecomment-6269540914, no MUST and
  no SHOULD; its one NIT is the wording of lane 4's reply, which said no
  merge had a conflict (8651841's is the one above). #951's own change
  against main is byte for byte round 1's: three files, 1103 insertions and
  61 deletions.

tests/test_family_siblings_bootstrap_unreadable.py (new, 57 tests). Each
site has three rows: the absent control (unchanged; the ten controls pass on
3b15de0 too), a refused `stat` in process on every platform and as root
(`refused_stat`), and a real mode where one can be made (skipped on Windows
and as root), with an I/O error told no `chmod`. Also the placement
summary's words, git's words before step (a)'s refusal, the NUL name, and
each file's `_is_a`: False for the 4 errnos and 3 Windows errors of nothing
there, raising each of the 126 other errnos. The helpers are imported from
tests/test_bump_leg_unopenable.py and tests/test_doctor_unreadable_pin.py,
none copied; conftest.py is not touched. Round 0's file (35 tests) was 25
failed and 10 passed on 3b15de0; all 57 pass.

Runs (lane 4's on Python 3.12.3, with a stub `gh` first on PATH,
GH_TOKEN=bogus, an empty GH_CONFIG_DIR and GIT_SSH_COMMAND=false):

- 3c5f2ea, one targeted run: 2538 passed, 10 skipped (git filter-repo is
  not installed) in 26m05s, over 30 files (the new file, the 26 test files
  that run or load the family root's siblings.py or bootstrap.py, and the
  trio: tests/test_repo_hygiene.py, tests/test_cli_reference.py and
  tests/test_agents_md_codes.py) and the 19 tests of
  tests/test_shape_doctor.py that load or run them.
- 2341b68: the new file, all of tests/test_shape_doctor.py,
  tests/test_scaffold_leg_path_collisions.py and the trio: 772 passed, 7
  skipped in 13m44s.
- Lane 5's, on git 2.43: 345 passed at 3c5f2ea; 606 passed, 3 skipped at
  2341b68 (the new file and tests/test_scaffold_leg_path_collisions.py,
  161; tests/test_shape_doctor.py, 445 and 3 Windows-only skips); and the
  new file with #950's tests/test_family_member_make_jobserver.py on a merge
  with main c7a3ff1, 100 passed.

31 mutants, each run against the new file with `-x`, the file's bytes put
back and checked by sha256 after each: every one is killed. Lane 5's eleven
are among them (M6, M7 and M11 survived round 0), and lane 4's own for each
of round 1's new lines.

Not done by this change, and said in the PR:

- A file whose `stat` succeeds and whose open is refused (a `project.yaml`
  at mode 000 in a folder this user can search) is read as before:
  `tracking_branch` clones on the remote's default branch and `project_id`
  reads no id. It is no absence read, and it predates #430.
- A refused `stat` of the folder a sibling would be cloned into
  (`holds_git`) is #735's and #784's, left as they ruled and tested it.
  `parent_folder_warning`'s `wanted.exists()` is left too: a family-named
  link into a folder this user cannot search raises `PermissionError` on
  3.12 and says "it is a link to nothing" on 3.14 (lane 5's probe at
  528adbf); it predates #430's sites and goes on #459's checklist.
- No Python 3.14 interpreter was run: the 3.14 half is `refused_stat` in
  process and a shim on the command line.

Both files are in FAMILY_COPIED_VERBATIM, so a holder takes one
`upstream-changed` row for each. Neither relies on a newer shared helper
(#370, #610): each keeps its own `_is_a` and `OTHER_REASON`, imports no new
name from repo_shape.py (which is untouched), and siblings.py imports no new
name from bootstrap.py, so either taken alone runs beside the other's older
copy.

Part of #430 (slice F; lane 1's plan, issuecomment-6251069236). #430 stays
open for its other slices.

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 20, 2026
… hint no longer calls an internal root private, adopt's docstring says which of its notes changed, and `prepare`'s docstring has no 104-character line (#994) (#1038)

Three statements in the source are made true.

The Free-plan secrets hint told an internal root on a plan read as
`free` that it "is private". It now says the root "is internal, which
this hint treats as private", the words the issue's Direction gives.
Every other visibility and plan answer is unchanged.
`scripts/repo_shape.py` is untouched, so no project re-pins. The case
should not arise, since internal visibility needs an enterprise, and a
comment at the branch says so.

The docstring of adopt's `_free_plan_hint_by_the_root` said adopt's
words were unchanged. It now names the two cases that changed: an
internal root with an unread plan (#969), and an internal root on a
Free plan (this change).

The 104-character line in the docstring of `prepare` in
`src/openreposhape/standard.py` is re-wrapped, with the same words.

The Free-plan hint's rows now pin the internal case for the scaffold,
adopt and `family.py init` alike.

Closes #994

Lane: openreposhape-3 (openRepoShape-3)
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

Development

Successfully merging this pull request may close these issues.

1 participant