Repository navigation
scaffold-project.py, adopt-project.py and family.py: the Free-plan secrets hint goes by what GitHub says of the root or holder and of its organisation's plan, and names the answer GitHub did not give, so init --reuse-empty-repo no longer calls a public holder private, an unread root is not taken as the run's visibility, and an unread plan is not silent - #728
Conversation
…crets hint goes by what GitHub says of the root or holder and of its organisation's plan, and names the answer GitHub did not give, so `init --reuse-empty-repo` no longer calls a public holder private, an unread root is not taken as the run's visibility, and an unread plan is not silent (#660) The hint (FREE_PLAN_HINT) holds when the repository whose `validate` workflow reads SHAPE_LEGS_APP_ID and SHAPE_LEGS_APP_PRIVATE_KEY, the assembly root or the family holder, is private or internal and its organisation is on the GitHub Free plan. Since #621 adopt and the scaffold ask GitHub the root's visibility. This finishes both questions in all three tools, in one place: scripts/shape_materialize.py, which is not a shape copy. - `repository_visibility` (and `visibility_in`, its reading of the answer) is the one visibility reader. The scaffold's `remote_visibility` is it, with a missing `gh` still `gh-not-found`; adopt's `_root_visibility` keeps its own `gh` call and reads the answer with `visibility_in`. - `organisation_plan` answers free, other, or None (unread): a failed or unstartable `gh api orgs/<org>`, or no plan name. - `free_plan_secrets_note` is the decision the three tools share: an unread root is ROOT_VISIBILITY_UNREAD (moved here from adopt, word for word); a public root nothing; a closed root on Free the hint, naming the root alone; a closed root whose plan is unread the new FREE_PLAN_UNREAD. L2-58: `family.py init --reuse-empty-repo` asks GitHub the reused holder's own visibility and gates its NOTE and the hint on it; an unread holder is said to be unread. A holder this run creates still goes by `--visibility`. family.yaml's `visibility:` keeps recording the run's value (L2-23's question, a known limit on #459). L2-57: the scaffold's reused root whose visibility GitHub does not give is said to be unread, where it took the run's `--visibility`. L2-59: a closed root whose plan GitHub does not name is said to be on a plan this run could not read, where all three tools were silent as for a paid plan. scripts/repo_shape.py, FREE_PLAN_HINT and free_plan_secret_hint are untouched, so no project re-pins. L2-71: `init`'s hint names the holder alone, and its NOTE no longer says "and its members will be too": `init` makes no member and `add` mounts one of any visibility. It names the members you add that are private or internal. Lands after #621. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
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 4 hours and 56 minutes by commenting |
Reviewer's GuideThe PR moves Free-plan secret guidance into shared materialization helpers that separately normalize repository visibility and organization-plan responses, then updates scaffold, adopt, and family flows to report confirmed, unread, or non-applicable states without guessing—especially for reused roots and holders—while retaining existing command/refusal semantics and adding broad cross-tool coverage. Sequence diagram for reused root visibility and plan handlingsequenceDiagram
participant Tool as CreatingTool
participant Materialize as shape_materialize.py
participant GitHub as GitHubCLI
Tool->>Materialize: repository_visibility(root)
Materialize->>GitHub: gh repo view root --json visibility
GitHub-->>Materialize: visibility or command failure
Materialize-->>Tool: public, private, internal, or None
Tool->>Materialize: free_plan_secrets_note(org, root, visibility)
alt visibility unread
Materialize-->>Tool: ROOT_VISIBILITY_UNREAD
else public
Materialize-->>Tool: no note
else private or internal
Materialize->>GitHub: gh api orgs/org --jq .plan.name
GitHub-->>Materialize: free, other, unread
Materialize-->>Tool: FREE_PLAN_HINT, FREE_PLAN_UNREAD, or no note
end
Flow diagram for shared Free-plan secrets decisionflowchart TD
A["Creating tool receives root visibility"] --> B{"Visibility readable?"}
B -- "No" --> C["Return ROOT_VISIBILITY_UNREAD"]
B -- "Public" --> D["No secrets note"]
B -- "Private or internal" --> E["organisation_plan(org)"]
E --> F{"Plan readable?"}
F -- "No" --> G["Return FREE_PLAN_UNREAD"]
F -- "Free" --> H["Return FREE_PLAN_HINT for root"]
F -- "Other" --> D
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
… to read, so a personal account's private root gets no Free-plan NOTE, and the tests pin init's unstartable-gh read and a 403 or 404 visibility read (#660) rv660's review of #728 (F1, F7, F8). F1. `organisation_plan` took every failure of `gh api orgs/<owner>` as an unread plan. GitHub answers that request 404 where the owner is not an organisation, so a scaffold, `init` or adoption owned by a personal account printed FREE_PLAN_UNREAD, whose remedy can never answer. A personal account has no organisation secrets, so nothing applies. - `gh_said_not_found(probe, words)` is #609's reading of gh's not-found answer (exit 1, gh's words on stderr), taken out of `refuse_unless_not_found`, which now asks it. It takes gh's words for each API: GH_NOT_FOUND_WORDS for `gh repo view` (GraphQL), and the new GH_API_NOT_FOUND_WORDS, "Not Found (HTTP 404)", for `gh api` (REST). - `organisation_plan` answers "not-an-organisation" there, and `free_plan_secrets_note` says nothing for it. A 403, a 502, a blank answer and every other failure stay unread. The command runs through `subprocess.run`, because the reading is of its own stderr, which `run` folds into stdout. F8. A test runs `_finish_holder_remote` for a reused holder whose visibility read cannot start `gh`, so the `OSError` branch is pinned as unread (rv660-R2 survived every test). Each tool's table gains a 403 visibility row, and `init`'s a 404 one. F7. tests/test_free_plan_hint.py's docstring says its silence tests are `free_plan_secret_hint`'s, which no tool calls since #660, and what the tools now do instead. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-2, 2026-10-16T18:27:13Z) Review round 1 for rv660's CHANGES REQUIRED, at 0b97f1a (was bc8c73b). Only F1, F7 and F8 change. F2-F6 are held for #459 until this lands.
Tests.
|
…import beside #728's FREE_PLAN_HINT import Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
main gained 25 pull requests since de92614: #634, #699, #671, #719, #701, #717, #700, #702, #722, #720, #725, #721, #728, #705, #727, #718, #744, #743, #749, #740, #737, #734, #742, #751 and #736. No conflict: AGENTS.md, adopt-project.py and docs/cli.md merged as text, and README.md and docs/handbook.html are unchanged by the merge. README.md is 1473 lines, its cap; AGENTS.md 494, its cap; every handbook marker is current; `render-cli-reference.py --check` is ok. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…es every reason THIS checkout cannot answer, and the notes are current to main bf1f099 with the 25 pull requests since de92614 (#563, #683) - W683 and RV-f's N4 on #742: README's exit-3 list (Keeping a project's shape current, :1208-1220) and its handbook twin (`doctor`) named only a pin whose commit this checkout does not carry. They now say, as shape-doctor.py's EXIT CODES does: a pin's commit it lacks, a partial clone missing what is compared (#683), a read that fails (`update-upstream-unreadable`), a crashed validator of its own (#278, #410), or its own naming policy not loading (#678). +1 line, with tests/readme-cap.d/683.txt; README.md is 1474, its cap (1422 + 2 + 5 + 12 + 22 + 10 + 1). The two `Keeping a project's shape current` markers move: `doctor` text and marker, `current` marker only. - The notes, "What changed since v1.0.1", one entry per pull request from `git log --first-parent de92614..bf1f099`, each from its squash commit and the issues GitHub says it closes: - adopt-project.py: #699, #719, #749, #727, #717, #728 (with #621's hint), #743 + #720 + #718, #722, #721; - update-shape.py, bump-leg.py and a project's own scripts: #702 (with the pinned validators), #634; - scaffold, setup, family and the shared readers: #725, #671, #744; - shape-doctor.py: #742, #701, #737, #751, #736, #734, #700 + #705, #740; - install, CI and the docs: the exit-3 sentence above. - Counts (99 pull requests since bfed143; 109 of 135 milestone issues closed at 10:28Z), the "current to main" sha and the open list (26) are at main bf1f099, still placeholders for tag time. Left for round B: lane 5's eight lines (#630 issuecomment-6227129879), whose pull requests #726, #747, #682, #724, #746, #745 and #738 are open drafts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main's 28 new commits since 8062810 merged with no conflict. Four of them touch a file this branch changes: #744, #728, #671 and #634 edit scaffold-project.py, and #728 and #671 edit scripts/family.py too, each in other functions than the clone URL's remedy, its refusal and the README values. Nothing this branch changes is a pinned copy, so nothing was re-pinned. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…efusals and `family-operation-in-progress` name a path as `shown_source` names it, so an ESC or a line break in a path no longer reaches the terminal (#692) (#731) #622's folder refusals printed a path as it was. `family.py init --into <a file named wd-ESC[31mRED> --dry-run` gave `REFUSED family-folder-cannot-be-made: the family folder .../wd-^[[31mRED/Orbit cannot be made: ...` through `cat -v`. A name with a line break and then `WARNING forged-line: pwned` printed that line as a line of its own. R612's `family-operation-in-progress` printed the mark's path across two lines for a holder whose path holds a newline. Each path is now spelled as `shape_materialize.shown_source` spells a `--pin-source`: as it is, and as Python's `repr` spells it where a character in it is not printable. - `scripts/shape_materialize.py`: both answers of `folder_cannot_be_made` (a file or a bad stat in the way, and a folder this user cannot write in), the folder in `refuse_unmakeable_folder`, and the folder in `make_folder`. - `scaffold-project.py`: `scaffold-target-exists`. - `scripts/family.py`: `family-target-exists`, and the mark's path in `family-operation-in-progress` (`where.as_posix()`). A printable path is named byte for byte, so no existing row changes and `why.endswith(CANNOT_WRITE_IN)` in `refuse_unmakeable_folder` still holds. No help text changed, so `docs/cli.md` is untouched. No README, handbook or template sentence says how a refusal names a path (`git grep` of `shown_source`, `target-exists` and `folder-cannot-be-made` in AGENTS.md, README.md, `docs/handbook.html` and the templates found none), so no doc changes. - **`shown_source`, not `shown_path`.** Since #634, `shape_materialize.shown_source` returns `repo_shape.shown_path(source)` (`scripts/shape_materialize.py`: `return shown_path(source)`), so the seven call sites this change adds (four in `shape_materialize.py`, one in `scaffold-project.py` and two in `family.py`) spell a path as `shown_path` does. Renaming them to `shown_path` stays out of this PR: it would only move imports in `scaffold-project.py` and `family.py` and reword docstrings in `family.py`, `shape_materialize.py` and the test file, and the review's N1 left it optional. I did not take it. The predicted merge with main works with the alias: `family.py --help` and `scaffold-project.py --help` exit 0 on it, and `shown_source` and `shown_path` return the same string for an ESC name, a line-break name and a plain path. - **`shown_source` is imported by name into each file's existing `from shape_materialize import (...)` block.** The first commit put it on a line of its own after the block; lane 4's #660, #688 and #693 add an import at that same place, and `merge-tree` reported add/add conflicts in `scripts/family.py` and `scaffold-project.py`. The second commit moves the name into the block, and those conflicts are gone. - **"The holder in the other refusals of the same block" is one refusal.** The `# What the holder is in the middle of, and what its sparse checkout leaves out` block holds `family-operation-in-progress`, `family-sparse-excluded` and `member-path-unaddable`. Only the first prints a path of the holder's. `family-sparse-excluded` names `members/<Project>` and `.gitmodules`, and `member-path-unaddable` names `members/<Project>`. - **No scaffold row for `--local-remote-dir`.** A control character there is refused earlier as `scaffold-unwritable-value`, which already names the path with `repr`. The unit test covers `make_folder`, which only a race reaches by the command line. `tests/test_dry_run_parity.py`: - `SCAFFOLD_CASES` gains five rows: ESC and line break in the `--work-dir` name, ESC in a folder name above it, and ESC and line break in a work dir that holds a file named `Atlas` (`scaffold-target-exists`). - `FAMILY_CASES` gains five rows: ESC and line break in `--work-dir`, ESC in `--into` (`the family folder`), and ESC and line break in a work dir that holds a file named `Orbit` (`family-target-exists`). - Two rows join `UNWRITABLE_FOLDER_AT` (ESC in the scaffold's and in `init`'s `--work-dir`, a folder this user cannot write in). - Rows named `ESC-...` and `line-break-...` are skipped on Windows, whose file names cannot hold them (`POSIX_NAMES`, `case_params`). Every expectation spells the path with `repr`, and not with `shown_source`. - `assert_refused` now fails any stderr line that starts with `WARNING forged-line`. - Three unit tests: `test_each_folder_question_spells_a_name_that_is_not_printable` (ESC, line break; `folder_cannot_be_made`, `refuse_unmakeable_folder` and `make_folder` by themselves), `test_a_folder_this_user_cannot_write_in_is_spelled_safe_to_show`, and `test_a_printable_name_is_named_byte_for_byte` (a name with a space and an accented letter stays unquoted). `tests/test_family_refuse_up_front.py`: `test_a_holder_path_with_a_newline_still_shows_its_merge` asserted the raw newline in the refusal (`"Ink\nRouter/.git/worktrees/" in ran.stderr`). It is now `test_a_holder_path_with_a_control_character_still_shows_its_merge`, over a line break, an ESC and a forged line, and asserts the one-line `repr` form (`refusal()` fails unless the stderr is two lines). It is the one existing test this change had to edit. Results at head `ab55c29f` (Python 3.12.3, Linux): - `tests/test_dry_run_parity.py tests/test_dry_run_asks_remotes.py tests/test_family_dry_run.py tests/test_family_init_landing.py tests/test_repo_hygiene.py tests/test_pin_source_unreadable.py tests/test_scaffold_leg_path_collisions.py tests/test_scaffold_reuse_seeded_legs.py`: **494 passed**. - `tests/test_family.py tests/test_family_refuse_up_front.py tests/test_family_init_values.py tests/test_family_add_refusals.py`: **356 passed, 19 skipped** (the skips are `test_family_init_values.py:219` and `:291`, template-quoting premises). - Not run: the full suite (the coordinator's gate), and macOS and Windows (CI is Linux-only since #597). The new hostile-name rows are POSIX only by design. On the base code: the review overlaid this head's two test files on main `2fac9790` and ran `tests/test_dry_run_parity.py` and `tests/test_family_refuse_up_front.py` with `-k "dry_run_parity or control_character"`. The result is **20 failed, 87 passed** (76 deselected, 43.5 s). The 20 are exactly the new and edited tests: the 5 scaffold and 5 `init` rows, the 4 unwritable-folder rows, the 3 unit tests (`test_each_folder_question_spells_a_name_that_is_not_printable` in two cases, and `test_a_folder_this_user_cannot_write_in_is_spelled_safe_to_show`), and the 3 cases of the edited `test_a_holder_path_with_a_control_character_still_shows_its_merge`. The one new test that passes on main's code is `test_a_printable_name_is_named_byte_for_byte`, by design: a printable name is named byte for byte on both. An earlier draft of this body said 21 failed, 2 passed. That run was on `8062810d` with a wider selection, and it held a scaffold row for ESC in the local remote dir's name (`ESC-in-the-local-remote-dir-name`) that this head's tests no longer have; its second pass was the existing `test_the_refusal_quotes_gh_s_first_line_and_escapes_what_is_unprintable`, which also passes in the new run, one of the 87. `family.py init` and `scaffold-project.py --dry-run` against a file named `wd-ESC[31mRED`, a file named `wd-\nWARNING forged-line: pwned`, and a work dir with a file `Atlas` or `Orbit` in it, run with a stub `gh` and no real `gh`: - On main, through `cat -v`: `... the family folder /.../wd-^[[31mRED/Orbit cannot be made: /.../wd-^[[31mRED is a file, not a folder`, and a stderr line `WARNING forged-line: pwned cannot be made: ...` of its own. - On this head: `REFUSED family-folder-cannot-be-made: the family folder '.../wd-\x1b[31mRED/Orbit' cannot be made: '.../wd-\x1b[31mRED' is a file, not a folder`, and one line for the forged name: `--work-dir '.../wd-\nWARNING forged-line: pwned' cannot be made: '...' is a file, not a folder`. `scaffold-target-exists` and `family-target-exists` are one line each in the same form. Each spelled site reverted alone in a throwaway clone at `19d126cd` (the call sites are the same at `ab55c29f`); the new tests that fail for each: - `folder_cannot_be_made`, the `_not_a_folder` answer: 8 failed (six CLI rows and the two unit tests). - `folder_cannot_be_made`, the `CANNOT_WRITE_IN` answer: 5 failed (the four unwritable rows and the unit test). - `refuse_unmakeable_folder`: 12 failed. - `make_folder`: 2 failed (the unit test only). - `scaffold-target-exists`: 2 failed (its two rows). - `family-target-exists`: 2 failed (its two rows). - `family-operation-in-progress`: 3 failed (line break, ESC, forged line). `git merge-tree --write-tree` of this head `ab55c29f` against origin/main `20c8873b` and against every ref on origin that could meet it, fetched 2026-10-16: the 41 branches (`fix/*`, `l3-*`, `wip/*`, `docs/*` and `release/v1.0`) and the 55 PR heads from #630 up. That is 96 refs. (The same list was run for #730's head `7f2bb36b`.) - **origin/main `20c8873b`: clean** (the predicted tree `00b5b24c`, which differs from main in these five files only: 216 lines added, 25 removed). - **#630's head `6fd33a93`: clean.** It conflicted with main in `docs/handbook.html` when this body was first drafted, and no longer does, with or without this change. This change touches no README or handbook. - **The refs that edit one of this change's five files**, and so could meet it, all merge clean with this head: `fix/646`, `fix/651`, `fix/654`, `fix/660`, `fix/661`, `fix/684`, `fix/686`, `fix/688`, `fix/689` and `fix/693`, the heads of #670, #725, #728, #743 and #744, and the heads of the landed #634, #638, #639, #640, #643 and #671. #634 has landed (`30676c90`), so the `fix/660` note about a conflict with it is gone. - **Four refs conflict with origin/main on their own**, and so with this head: `docs/release-notes-v1.0.0` and `release/v1.0`, and the pre-squash heads of the landed #637 and #641. Their conflicts do not come from this change. Every other ref, 92 of the 96 in all, is clean with this head, including the refs that conflict with main in `templates/family-root/scripts/validate-family.py` (#682, #745, #747, `fix/648`, `fix/681`, `fix/704`) or in `shape-doctor.py` (#742, `l3-683`) on their own. So do the old heads of the landed #638 and #639, and lane 3's `l3-677-pins-hand-step-wip` branch, and each of them merges clean with this head too. - **Tests on the predicted merge**, run by the review on the merge with main `2fac9790`: `test_dry_run_parity`, `test_family_refuse_up_front`, `test_pinned_copy_text`, `test_pin_source_unreadable`, `test_dry_run_asks_remotes`, `test_family_dry_run` and `test_family_init_landing`: 445 passed in 215 s. `test_family`, `test_family_init_values`, `test_family_add_refusals`, `test_derived_id_refusals`, `test_repo_hygiene`, `test_naming_policy`, `test_scaffold_leg_path_collisions` and `test_scaffold_reuse_seeded_legs`: 758 passed, 19 skipped in 243 s. I did not re-run them on `20c8873b`; the merge is clean, and the two commits since `2fac9790` (#700 and #702) change only `shape-doctor.py`, `templates/assembly-root/scripts/validate-pins.py` and four test files, none of them a file of this change. - Before the second commit, `fix/660`, `fix/688` and `fix/693` each conflicted with this head in the import region of `scripts/family.py` or `scaffold-project.py` (a separate import line each side added at the same place). That is why the import joins the existing block. #634 is on main, and this change does not edit `shown_source` or its docstring in `scripts/shape_materialize.py`. The review approved with nits. Its S1, the stale base-run numbers and merge lines, is taken here, in this body: no code changed, and the head is still `ab55c29f`. Its N1, renaming the call sites to `shown_path`, is not taken (see "Decisions and their evidence"). It also re-ran 5 of the 7 mutation probes on `ab55c29f` and got the same failure counts as above (12, 2, 3, 2 and 2), and ran 144 hostile-name dry runs (16 names in 9 scenarios): on main 69 rows put a control character on stderr and 6 print a forged line of their own; on this head all 144 exit 2 with no traceback, no control character and no forged line. - `scripts/family.py` `init`'s plan prints `folder` and `landing` lines with the path raw (`print(f"landing {holder} ...")`, `print(f"folder {folder} ...")`). On a dry run or a refused run the plan is on stdout before the refusal, so `WARNING forged-line: pwned/Orbit (--work-dir: no family folder)` still appears there. It is the plan, not a refusal, and not one of the issue's lines. - Other refusals that name a holder or folder path raw, none of them in the issue's lines: `family-holder-detached` and `family-on-tracking-branch` (`{family.root}`), `family-index-locked` (`lock.as_posix()` in the detail), `family-landing-in-standard` (`{holder}`, `{checkout}`, `{beside / family / family}`), `family-holder-already-here` (`{into}`), and `family-remote-exists` and `scaffold-remote-exists` (`{target}`, a local remote path). 1. **family.py `init`'s plan prints the holder and folder paths raw.** `scripts/family.py` `landing` and `folder` rows (about :1756-1759) print `holder` and `folder` as they are, so a forged line or an ESC reaches stdout in a `--dry-run` and in a run refused after the plan. Direction: spell them with `shown_source`. 2. **The same raw spelling in the refusals listed above.** `family-holder-detached`, `family-on-tracking-branch`, `family-index-locked`, `family-landing-in-standard`, `family-holder-already-here`, `family-remote-exists` and `scaffold-remote-exists` each print a path the person supplied or git keeps. Direction: the spelling this change gives its sites. Review: rv692, lane 5's independent review, approved ab55c29 with nits, and its body fix (S1) is published. CI (Brett Heap's ruling 27 and his fast-refresh ruling of 2026-10-15T22:48:35Z): H0 ab55c29 passed tests, tests-git-bash and SonarCloud; the head 13f643d is a clean update-branch merge of main 431c8cf, with tests-git-bash and SonarCloud green; and the local gate passed on its predicted squash tree 4327947 over main 431c8cf: the dry_run_parity, family_refuse_up_front, repo_hygiene, agents_md_codes, cli_reference sets, 330 passed. Landed on Brett Heap's word of 2026-10-17T10:59:39Z, relayed by lane 3 on #473: "merge lane 5's approved v1.1.0 drafts". Closes #692 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>



Lane: openreposhape-2 (openRepoShape-2)
Claim: #660 (comment)
Lands after #621 (on main as 4ce9045). This branch is cut from it.
The Free-plan secrets hint (
FREE_PLAN_HINT) holds when the repository whosevalidateworkflow readsSHAPE_LEGS_APP_IDandSHAPE_LEGS_APP_PRIVATE_KEY(the assembly root, or the family holder) is private or internal AND its organisation is on the GitHub Free plan. That is two questions for GitHub, and either can go unanswered. Since #621 adopt and the scaffold ask the first one of the root. This PR asks both questions in all three tools, in one place, and says which answer GitHub did not give instead of guessing.What changed
scripts/shape_materialize.py (new, beside
refuse_unless_not_found). This file is not a shape copy, so nothing re-pins.visibility_in(answer)reads agh repo view --json visibilityanswer. It returns one of GitHub's three visibilities, in any case, or None.repository_visibility(repository)is the one visibility reader for the scaffold andfamily.py. A failedgh repo viewis None. Aghthat cannot be started is left to the caller, because the scaffold still refuses that asgh-not-found(family.py init --dry-runandscaffold-project.py --dry-runexit 0 without asking whether the remotes already exist #537).organisation_plan(org)returns"free","other","not-an-organisation", or None (unread)."not-an-organisation"is gh's not-found answer togh api orgs/<owner>. GitHub answers 404 there when the owner is a personal account, which has no organisation secrets (review round 1, F1).ghthat cannot be started) and an answer that names no plan.subprocess.run, because the not-found reading is of the command's own stderr, whichrunfolds into stdout.gh_said_not_found(probe, words)is scaffold-project.py andfamily.py init: the dry runs skip the target-exists and folder checks, and anygh repo viewfailure reads as "does not exist" #609's reading of gh's not-found answer (exit 1, gh's words on stderr), taken out ofrefuse_unless_not_found, which now asks it. It takes gh's words for each API:GH_NOT_FOUND_WORDSforgh repo view(GraphQL), as before;GH_API_NOT_FOUND_WORDS,Not Found (HTTP 404), forgh api(REST).free_plan_secrets_note(org, root, visibility)is the decision all three tools now share:ROOT_VISIBILITY_UNREAD(moved here from adopt, word for word), and the plan is not asked;free:FREE_PLAN_HINT, with the root alone as its subject;FREE_PLAN_UNREAD;scripts/family.py (L2-58, L2-71).
_finish_holder_remotenow takesreuse.--reuse-empty-reporeused is asked of GitHub (repository_visibility), and the NOTE and the hint go by that answer.--visibility, as before: it was created at that visibility.ROOT_VISIBILITY_UNREADand no NOTE. Whether it is private is exactly the question that went unanswered.f"{repository} is", the holder alone, as the scaffold and adopt say it since adopt-project.py and scaffold-project.py: the Free-plan secrets hint goes by the assembly root's own visibility on GitHub and names the root as its reason, so a public source adopted with private legs is no longer told its organisation secrets resolve to EMPTY (#602) #621.scaffold-project.py (L2-57).
remote_visibilityis nowrepository_visibility, plus the samegh-not-foundrefusal, in the same words, for the same argv._create_remotesno longer falls back toor args.visibilityfor a reused root, so an unread root stands as None._private_leg_noteprintsfree_plan_secrets_note.adopt-project.py (L2-59).
_free_plan_hint_by_the_rootprintsfree_plan_secrets_note._root_visibilitykeeps its ownghcall, through adopt'srun, and reads the answer withvisibility_in.ROOT_VISIBILITY_UNREADnow lives inshape_materialize.py, andimport jsonis gone (its only user was the parse).ghcall log for four cases.scripts/repo_shape.py is untouched.
FREE_PLAN_HINT's text stays.free_plan_secret_hintstays too: the three tools no longer call it, and removing it from the pinned copy would re-pin every project.New messages, as a probe printed them (stub
gh,testorg):init's NOTE, before:and after:
init's hint: "testorg/Orbit and its members are private, so org-level ..." is now "testorg/Orbit is private, so org-level ...". The scaffold over a reused root whose visibility read fails (HTTP 502) printed the hint ("testorg/Atlas is private, so org-level ..."), whatever the root was. It now printsROOT_VISIBILITY_UNREAD, adopt's NOTE: "NOTE this run could not read testorg/Atlas's visibility from GitHub, so it cannot say whether the Free-plan secrets hint applies. ...".Decisions the issue left open
visibility:records the run's value, not the value read from GitHub.refuse_unholdable_manifestchecks that very table before the plan, and before_checked_reuseknows whether a holder exists. So either agh repo view --json visibilitywould come ahead of the existence check in both runs, orfamily.yamlwould be written from a table other than the one that was checked. The first moves the firstghquestion thattests/test_dry_run_asks_remotes.pypins forinit --reuse-empty-repowith nogh.--visibilityover a reused root that disagrees (tests/test_reuse_visibility.py), and adopt records the plan's while its root keeps its own.test_init_notes_a_reused_holder_at_its_own_visibilitypins this, so a later change to it is a deliberate one.initmounts no member, andaddmounts a member of any visibility, so nothing here decides what that sentence promised. The NOTE now names the members it is about: those you add that are private or internal. It is still printed only for a closed holder, as before, and now by the holder's own visibility.ghcall in_root_visibilityand shares only the reading (visibility_in). Its 11 reader tests (adopt-project.py and scaffold-project.py: the Free-plan secrets hint goes by the assembly root's own visibility on GitHub and names the root as its reason, so a public source adopted with private legs is no longer told its organisation secrets resolve to EMPTY (#602) #621) monkeypatchadopt.run, so a call throughshape_materialize.runwould escape them.ROOT_VISIBILITY_UNREADalready names both conditions, so it holds whatever the plan is. This keeps adopt-project.py and scaffold-project.py: the Free-plan secrets hint goes by the assembly root's own visibility on GitHub and names the root as its reason, so a public source adopted with private legs is no longer told its organisation secrets resolve to EMPTY (#602) #621's behaviour and itsghcalls in adopt.organisation_plan, the one decision function the three callers share, the three call sites, and the one test that named the old function (test_every_creating_tool_calls_the_hint). No README or template edit was needed. README.md:631 ("the creating tools check and say so") is still true.FREE_PLAN_UNREAD's last clause, "run with an owner's login", rests on GitHub's REST page forGET /orgs/{org}("the authenticated user must be an organization owner" for the full details). It is not measured: no probe here reached GitHub.Tests
New file
tests/test_free_plan_hint_reads.py, 50 tests (38, and 12 from review round 1):test_each_tool_says_what_github_said_of_the_root_and_its_plan(scaffold, adopt, family × public, private, visibility-502, visibility-403, plan-502, not-an-organisation, plan-unnamed, paid; 24)free, naming the root alone;ROOT_VISIBILITY_UNREADfor a root whose--json visibilityread fails (HTTP 502, or a SAML 403), with the plan not asked;FREE_PLAN_UNREADfor a plan read that fails or names no plan; nothing for a public root, a paid plan, or an owner gh says is no organisation (gh: Not Found (HTTP 404)); never "its members"; the root's visibility is askedtest_init_notes_a_reused_holder_at_its_own_visibility(public, private, internal, visibility-502, visibility-404)init's NOTE goes by GitHub's answer, in the new words; no NOTE for an unread holder; family.yaml keeps the run'svisibilitytest_init_asks_nothing_of_a_holder_it_createsinitcreates gets the NOTE and the hint at the run's visibility, and GitHub is not askedtest_init_takes_a_holder_gh_cannot_be_asked_about_as_unreadgh(OSError) getsROOT_VISIBILITY_UNREAD, and no NOTE or hint at the run's--visibilitytest_the_plan_is_free_other_or_unread(11)organisation_plan, through a fakedsubprocess.run: free, free with whitespace, team, enterprise, empty, blank line, a REST 404 (not an organisation), a SAML 403, a 502, a 502 whose stdout carries a 404 body, and noghtest_a_visibility_read_is_one_of_three_or_unreadrepository_visibility: lowercased, an unknown word unread, a failedghunread, a missingghraised to the callertest_the_note_goes_by_both_answers(7)free_plan_secrets_note: each answer pair, not-an-organisation included, and the plan asked only of a closed rootfake_ghvisibility_errors(fails onlyrepo view --json visibility) andplan_error(failsapi orgs/<org>)create_the_legsfake_gh's other keywords throughtest_every_creating_tool_calls_the_hint, and the module docstringfree_plan_secrets_notewith its own organisation; the docstring says the file's silence tests arefree_plan_secret_hint's, which no tool calls since #660 (F7)family.pyskips the holder read: 9 failed, among them[family-public].[family-visibility-502]andtest_init_notes_...[visibility-502].[family-private].[scaffold-visibility-502].[*-plan-unnamed].[*-plan-502].[*-public].repository_visibilityswallows a missinggh: the unit test and both scaffoldgh-not-foundtests intest_dry_run_asks_remotes.py.[unknown-word].[free-spaced].test_free_plan_hint_reads.py,test_adopt_free_plan_hint_root.py,test_dry_run_asks_remotes.py,test_free_plan_hint.pyandtest_scaffold_reuse_seeded_legs.py; files restored byte-exact).inittakes an unstartableghas the run's visibility:test_init_takes_a_holder_gh_cannot_be_asked_about_as_unread. It survived all 129 tests before this round.[scaffold|adopt|family-not-an-organisation]and the unit row.[*-plan-502],[saml-403]and 2 more.shape_materialize.py, the new tests): 4 failed, 46 passed. The 4 are the three tools'not-an-organisationrows and the unit row.test_free_plan_hint_reads.py,test_free_plan_hint.py,test_adopt_free_plan_hint_root.py,test_scaffold_reuse_seeded_legs.py),test_shape_doctor.py,test_scaffold_leg_path_collisions.py,test_dry_run_asks_remotes.py, and the 3 that namerefuse_unless_not_foundorremote-unreadable(test_adopt_github_exits.py,test_adopt_split_branch.py,test_dry_run_parity.py).tests/test_scaffold*.py,test_family*.pyandtest_adopt*.pythat namesfree_plan,FREE_PLAN,visibilityorROOT_VISIBILITY, every test file that names a function or message this changes,test_free_plan_hint.py,test_reuse_visibility.py,test_dry_run_asks_remotes.py,test_family.py,test_dry_run_parity.py,test_push_branch_refspec.py,test_ruleset_hint_tracking_branch.py,test_repo_hygiene.py,test_cli_reference.py,test_agents_md_codes.py,test_shape_doctor.pyandtest_scaffold_leg_path_collisions.py.scripts/render-cli-reference.py --check: ok, because no--helptext changed.ghthat exits 97 on anything it was not given,GH_TOKEN=bogus, an emptyGH_CONFIG_DIRand an isolatedHOMEran each tool at the base and at this change.ghcalls are byte-identical before and after for private on Free, public, internal on a paid plan, and an unread root.api orgs/someuseransweredgh: Not Found (HTTP 404)on stderr, the 404 JSON body on stdout and exit 1, these runs printedFREE_PLAN_UNREADat bc8c73b and print nothing about the plan at 0b97f1a: the scaffold (--org someuser, reused and fresh),init(reused and created) and adopt. Atestorgplan read that fails with 502 still printsFREE_PLAN_UNREAD.Review round 1 (rv660, CHANGES REQUIRED)
FREE_PLAN_UNREAD, and its remedy (gh api orgs/<owner> --jq .plan.name) can never answer for one. GitHub answersGET /orgs/<login>404 where the login is not an organisation. Now gh's not-found answer there is "not an organisation", and nothing is printed: a personal account has no organisation secrets. The answer is read by scaffold-project.py andfamily.py init: the dry runs skip the target-exists and folder checks, and anygh repo viewfailure reads as "does not exist" #609's reading (gh_said_not_found), taken out ofrefuse_unless_not_foundrather than written twice. Every other failure staysFREE_PLAN_UNREAD.except OSError: visibility = args.visibilityinscripts/family.py) survived every test. It is now killed by a new in-process test. The table gains a 403 visibility row in each tool, andinit's test a 404 row.tests/test_free_plan_hint.py's docstring now says its silence tests arefree_plan_secret_hint's, which no tool calls since the Free-plan hint's reads: a public reused holder gets the "private" NOTE and hint, an unread root is taken as the run's visibility, and an unread plan is silent #660, and what the tools do instead.Not changed
_reused_leg_visibility,return seen or args.visibility, scaffold-project.py:1348). It decides only the per-leg NOTE and whether the hint is asked at all. the Free-plan hint's reads: a public reused holder gets the "private" NOTE and hint, an unread root is taken as the run's visibility, and an unread plan is silent #660 is about the root._leg_visibility'sreturn reused_visibility or "private"(scaffold-project.py:1135) is the visibility a NEW leg is created at when the root cannot be read and no--visibilitywas given. It is a creation default, and nothing claims it of the root.visibility:for a reused holder records the run's value (scripts/family.py:1708; L2-23, a known limit).init's plan lineremotes gh repo create --privateprints under--reuse-empty-repotoo (scripts/family.py:1759). The next line says the holder is reused.init's NOTE is still gated on the holder being closed, so a public holder whose members will be private gets no credential NOTE atinit. The gate is as before.CONTRACTS_DIR, a second setup run refused at the plan) #606's (L2-66).Closes #660
🤖 Generated with Claude Code
Summary by Sourcery
Make Free-plan secrets guidance reflect GitHub’s reported repository visibility and organisation plan without guessing when either value is unavailable.
New Features:
Bug Fixes:
Enhancements:
Tests: