Skip to content

family.py, adopt-project.py and the four tools' --org: init asks 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

Merged
brettheap merged 24 commits into
mainfrom
l3-863-github-limits-init-check-org
Oct 18, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Closes #863.

The stack, and what it lands after

What changed, by the issue's lines

Line 1: family.py init asks GitHub's limits before any repository exists. init set the holder's topic, xf-project-<family id>, last: gh repo edit --add-topic ran after gh repo create and the push. It asked neither GitHub's 50 for a topic, nor its 39 for --org, nor its 100 for the holder's name. So a family of 40 characters made and pushed its holder, then failed at the topic, with no NEXT STEPS printed.

Line 2: adopt-project.py check finds what execute refuses, from one list. check asked none of what execute asks of the names, the organisation and the topic, the disk's 255 bytes included, so it said plan ok over plans execute refused.

Line 3: an --org GitHub gives no organisation is refused where GitHub is asked. I took option (a) of the issue's "Decide first": the rule is in the tools' plans, beside the 39, and no pinned file changes. So scripts/repo_shape.py and the two validators' QUALIFIED_RE are untouched, no project is re-pinned, and a manifest that already names such an owner reads as it did. The issue asked for the choice to be stated on #863 first. Lane 3 stated it there, in its comment of 03:26:32Z.

GitHub's numbers and characters are from its documentation, and none is measured against GitHub here. As the issue says, no run of this repository creates a real repository, and I did not run the real gh. The fake gh in the new tests refuses by the documented numbers and characters and answers repo view as not found; the runs that refuse assert it was never called. A probe against a real organisation, for the numbers and for _, is lane 1's.

Where #859's and #861's own lines changed, and why

Tests

The new file is tests/test_github_limits_init_check_org.py, 93 tests: 82 from the first head and 11 from the fix round. Each bound is tested at N and N+1, and each refused character against the ok-org and ok_org controls:

On 9ff260fb, the stack before this change, with only the first head's file put in: 62 failed, 20 passed in 60.61s. The same 62 fail on 0b4251b8, the first build of the stack (71936633 with 748c8e6a). The 20 that pass are the controls.

On 66fc8f6b, the first head, with only the fix round's file put in: 6 failed, 87 passed in 26.57s. The 6 are [id] and [derived-id] of test_check_finds_what_execute_refuses_under_its_code and test_check_asks_the_legs_beside_a_topic_it_finds (S1); test_an_owner_past_the_length_bound_is_shown_cut_as_its_length_is and test_check_lists_an_organisations_length_then_its_characters, which asserts N3's cut too; and test_check_and_execute_read_one_list, because _held_findings now gives the codes as well. The other N2 tests and the N5 test pass there: they pin what the first head already did.

Every run was on Linux, under a stub gh first on PATH that exits 97, with GH_TOKEN=bogus and an empty GH_CONFIG_DIR, each wrapped in timeout 2400, with python3 -m pytest -q -p no:cacheprovider --basetemp=<scratch>/bt-<name> -o tmp_path_retention_policy=failed:

Mutants

The fix round's 8 mutants, one at a time, each applied alone to 99afbc3d in a scratch worktree and run against the new file, first with -x and then without it against the tests written for each item. All 8 were killed, and the worktree was left clean.

Mutant Tests that fail
s1-skip-after-any-finding: the leg question skipped after any finding, as at the first head (S1) test_check_finds_what_execute_refuses_under_its_code[id], [derived-id], test_check_asks_the_legs_beside_a_topic_it_finds
s1-topic-skips-too: adopt-topic-too-long added to RENAMES_THE_LEGS (S1) test_check_finds_what_execute_refuses_under_its_code[id], [derived-id], test_check_asks_the_legs_beside_a_topic_it_finds
s1-never-skip: the leg question asked after every finding (S1) test_check_finds_what_execute_refuses_under_its_code[project], [org] and the four [org-*], test_check_lists_an_organisations_length_then_its_characters
n2-adopt-characters-before-length: _what_cannot_be_held asks the characters before the length (N2) test_the_list_holds_an_organisations_length_before_its_characters, test_check_lists_an_organisations_length_then_its_characters, test_execute_refuses_an_organisations_length_first
n2-family-characters-before-length: the same in family.py (N2) test_init_refuses_an_organisations_length_first[dry-run], [real]
n2-scaffold-characters-before-length: the same in scaffold-project.py (N2) test_the_scaffold_refuses_an_organisations_length_first[dry-run], [real], test_setup_refuses_an_organisations_length_first
n5-held-after-a-name-refusal: the list also asked in the except of the naming question, RV871's r-l2-held-after-a-name-refusal (N5) test_check_asks_none_of_them_of_names_that_do_not_classify
n3-whole-value: a misspelled value past 39 printed whole (N3) test_an_owner_past_the_length_bound_is_shown_cut_as_its_length_is, test_check_lists_an_organisations_length_then_its_characters

The first head's 21 mutants, one at a time. Each was applied to 66fc8f6b in a scratch worktree and run with -x against the new file; the two that change execute's list also ran against #859's file. All 21 were killed, and the same 21 were killed on fdf5174c. The worktree was left clean. Since the fix round, l2-check-legs-after-a-finding is the rule for a name or an organisation finding only.

Mutant First test that fails
l1-family-call-dropped: _checked_family_identity no longer calls the questions test_init_one_past_githubs_bound_is_refused_before_any_process[derived-id-dry-run]
l1-family-local-guard-dropped: asked under --local-remote-dir too test_init_asks_none_of_them_of_a_local_run[o*40]
l1-family-name-off-by-one: >= 100 test_init_at_githubs_bound_goes_on[name]
l1-family-topic-dropped test_init_one_past_githubs_bound_is_refused_before_any_process[derived-id-dry-run]
l1-family-org-length-dropped test_init_one_past_githubs_bound_is_refused_before_any_process[org-dry-run]
l1-family-derived-named-as-passed test_init_names_a_derived_id_as_derived_and_mends_it_with_an_id
l1-family-help-dropped: --org's help loses its bound test_init_help_says_each_bound_where_the_value_is_typed
l2-check-call-dropped: check no longer reads the list test_check_finds_what_execute_refuses_under_its_code[project]
l2-check-legs-after-a-finding: #861's leg question asked after one test_check_finds_what_execute_refuses_under_its_code[project]
l2-execute-ignores-the-list test_check_and_execute_read_one_list
l2-list-drops-the-topic test_the_list_holds_each_refusal_in_executes_order
l2-check-local-flag-flipped test_check_finds_what_execute_refuses_under_its_code[project]
l2-check-first-only: check finds only the first test_check_and_execute_read_one_list
l2-bad-id-not-left: plan-bad-id taken into the list test_a_bad_id_is_left_to_its_own_refusal
l3-spell-never: the helper never refuses test_an_owner_github_does_not_spell_is_refused_by_name[dot]
l3-underscore-refused test_an_owner_github_spells_is_not_refused[ok_org]
l3-trailing-hyphen-let-by test_an_owner_github_does_not_spell_is_refused_by_name[trailing-hyphen]
l3-two-hyphens-let-by test_an_owner_github_does_not_spell_is_refused_by_name[two-hyphens]
l3-scaffold-call-dropped test_the_scaffold_refuses_an_org_github_does_not_spell[dot-dry-run]
l3-adopt-call-dropped test_the_list_holds_each_refusal_in_executes_order
l3-family-call-dropped test_init_refuses_an_org_github_does_not_spell[dot-dry-run]

Merge checks

git merge-tree --write-tree of 99afbc3d:

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

Summary by Sourcery

Validate GitHub naming constraints before remote creation and align adoption checks with execution refusals.

New Features:

  • Validate GitHub repository, organisation, and topic constraints during family initialization before any remote or worktree is created.
  • Reject organisation names that GitHub cannot spell consistently across scaffolding, adoption, setup, and family initialization.
  • Make adopt-project.py check report the same name, organisation, and topic constraints that execute enforces.

Bug Fixes:

  • Prevent family initialization from creating and pushing a holder before discovering an invalid GitHub name, organisation, or topic.
  • Prevent check from reporting plans as valid when execute will reject their names or GitHub-related values.
  • Prevent invalid GitHub organisation names such as dotted, trailing-hyphen, or double-hyphen values from reaching repository creation.

Enhancements:

  • Centralize adoption holdability checks so check reports all applicable findings while execute stops at the first refusal.
  • Document GitHub limits in the family initialization CLI help and preserve local-remote workflows from GitHub-specific validation.

Documentation:

  • Regenerate the CLI reference with GitHub limits for family, organisation, and topic arguments.

Tests:

  • Add comprehensive coverage for initialization timing, GitHub limits, organisation spelling, local-remote behavior, adoption check/execute parity, and refusal ordering.

Brett Heap and others added 14 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>
…asks GitHub's limits on a name, a topic and an organisation where GitHub is, so an id over 39 characters no longer fails `--add-topic` after the repositories exist (#838)

GitHub keeps a repository's name to 100 characters, a topic to 50 and a
user's or an organisation's name to 39. These are GitHub's documented
limits; no run here measured them (none creates a real repository).
Nothing asked them. The scaffold sets `xf-project-<id>` last, so an id
over 39 characters, which a `--project` of 40 or more gives by default,
failed `gh repo edit --add-topic` with all three repositories created and
seeded, and `adopt-project.py execute` failed there after both legs and
the split were pushed. A `--project` of 96 to 246 characters, or an
`--org` of 40 to 255, reached the first `gh repo create`.

shape_materialize.py: `refuse_unstorable_names`, the helper that asks the
disk's limit, takes the names GitHub will be asked to create and refuses
one over 100 under the same code, its remediation naming the id's bound
too; `refuse_a_topic_github_cannot_set` and
`refuse_an_owner_github_cannot_have` ask the topic and the organisation.
The numbers live there, in the plans, so nothing pinned is re-pinned.

scaffold-project.py asks all three in its plan, before any process is
started and under `--dry-run` alike, without `--local-remote-dir` only:
`scaffold-name-too-long`, `scaffold-org-too-long` and
`scaffold-topic-too-long`, a derived id named as derived and mended with
`--id`. adopt-project.py `execute` asks them before either leg exists:
`adopt-name-too-long` (the two legs), `adopt-org-too-long` and
`adopt-topic-too-long`. setup-project.py hands the three values to the
scaffold's plan at step (5), which refuses them there; its `--help` says
the three numbers, as the two tools' `--help` does, and docs/cli.md is
regenerated.

tests/test_github_name_limits.py holds each bound at N and N+1, on each
way in, local and not; tests/test_scaffold_manifest_values.py's GitHub
half of the longest-name test now asks GitHub's longest, 95 with a short
`--id`.

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…of 96 to 100 characters, is mended by renaming the source on GitHub first, so `adopt-name-too-long` no longer says to shorten a `--project` an in-place adoption cannot change (#838)

Fix round 1 on #859, after rv859's review.

adopt-project.py: without `--local-remote-dir` the assembly root is the
source's own repository on GitHub, which GitHub lets be named with up to
100 characters, so a source of 96 to 100 gives legs of 101 to 105, which
`adopt-name-too-long` now refuses. Its remediation said "Use a shorter
--project", which plans another repository; `git ls-remote` could not
read it. Where a leg over 100 is named after the root, the remediation
now says GitHub's numbers and then `_rename_first`'s way out, as
`adopt-bad-id` says it: rename the source on GitHub, to at most 95
characters, and plan again. A leg the plan was edited to name otherwise,
and the disk's limit under `--local-remote-dir`, keep their remediations
word for word.

Comments and docstrings: the base planned a `--project` of 96 to 250
characters in GitHub mode, not 96 to 246. Of the three numbers only the
100 and the 50 are documented on docs.github.com; for 39 the docs give
the limit on a user's name (Enterprise Cloud's username considerations)
and nothing for an organisation's. The scaffold's GitHub refusals come
before any process only where `--elected-by` or GIT_AUTHOR_NAME names
the elector; otherwise `git config user.name` runs first. The previous
commit's message says otherwise on all three.

tests/test_github_name_limits.py: a source named with 96 and with 100
characters each refuses with the rename-first remediation, and a leg the
plan names otherwise and the disk's limit keep theirs; `execute` without
`--yes`, on a stdin that is not a terminal, refuses
`adopt-topic-too-long` and `adopt-org-too-long`, not `adopt-unconfirmed`,
with a control that reaches `adopt-unconfirmed`; and the scaffold's and
`execute`'s own remediations are held word for word.

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…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>
….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>
…ory can be named, keeps "Use a shorter --project", and the rename-first remedy says GitHub's 95 once, so a --project nobody compared with a source is no longer told to rename one (#838)

#859's re-review of 7193663 left three NITs, all in the rename-first hunk
of `_repository_urls`.

NIT 3. `_a_leg_named_after_a_long_root` took any root over 95 characters. A
root of 101 or more is no repository GitHub can have, so there is nothing on
GitHub to rename: it comes only where the plan's `legs.assembly` is not held
to a GitHub `origin` (a source with none, or a path `origin`) and a long
`--project` was typed, and "Use a shorter --project" was the better advice.
The predicate now also asks `len(names["assembly"]) <= GITHUB_NAME_MAX`.
A root of 251, whose legs the disk cannot hold, keeps the disk's remediation.
`SOURCE_NAME_TOO_LONG` said 95 twice; `GITHUB_LIMITS` says it, and the
sentence before it no longer does.

NIT 1. No test covered a plan where one leg is named after the source and
the other was edited to a short name. `test_one_leg_named_after_the_source_is_enough`
does, for each leg, and `test_a_leg_the_plan_names_otherwise_keeps_its_remediation`
gains a leg that starts with the root without being `<root>-spec`.

NIT 2. `test_a_leg_named_after_the_source_is_mended_by_renaming_it` asserts
the refusal's detail, which the rename-first line hands on from
`refuse_unstorable_names`.

`test_a_root_github_cannot_have_is_not_the_sources_to_rename` holds the
roots of 101 and 251. With the root bounded, `--local-remote-dir`'s guard in
the handler is reached only by a disk refusal of an edited leg beside a leg
named after a source of 96 to 100; the one test of it, a root of 247, no
longer got that far, so `test_a_leg_named_after_the_source_is_not_the_disks_trouble`
holds it. `SHORTER_PROJECT` names the first words of the "Use a shorter
--project" remediations once, and `DISK_REMEDY` is built from it with the
same bytes.

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…hub-limits-init-check-org, stacked on #859's fix/838-github-name-limits (495d4b3): `check`'s leg-repository findings, which #863's second line asks GitHub's limits beside (#863)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…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)

`family.py init` set `xf-project-<family id>` last, after `gh repo create`
and the push, and asked none of GitHub's limits: a family of 40 characters
made and pushed its holder and then met GitHub's refusal of the topic.
`_checked_family_identity` now asks them, where GitHub is asked and before
`_creator_name` and any `gh` call, so `--dry-run` refuses the same:
`family-name-too-long` (100), `family-org-too-long` (39), `family-bad-org`
and `family-topic-too-long` (50), with #859's helpers and the scaffold's
words. `init --help` says the three bounds; `docs/cli.md` is regenerated.

`adopt-project.py check` asked none of what `execute` asks of the names, the
organisation and the topic, the disk's 255 bytes included, so it said
`plan ok` over a plan `execute` refused. `_what_cannot_be_held` is now the
one list: `execute` refuses its first refusal in `_repository_urls`, and
`check` finds every one (`_held_findings`) under the same code, before
#861's leg question, which it no longer asks after one. The topic question
moves from just before `_confirm` into that list, so `execute` asks it with
the names and the organisation.

`refuse_an_owner_github_cannot_spell` (shape_materialize.py) refuses an
owner holding a character outside letters, digits, `_` and `-`, ending in a
hyphen or holding two in a row: `scaffold-bad-org`, `adopt-bad-org` and
`family-bad-org`, asked beside the 39 and only without
`--local-remote-dir`; `setup-project.py` takes the scaffold's answer at its
step (5). `_` is let by until a real account is probed. No pinned file
changes. GitHub's numbers and characters are from its documentation and are
not measured here.

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

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-10-18T03:50:22Z) Lane 3's independent review of its own #871 (#863) at 66fc8f6b, by RV871 (opus): READY. No MUST. Two SHOULDs: S1, check skips #861's leg question after any new finding, a topic-only one too; S2, three SonarCloud issues in the new test file. Seven NITs follow. A fix round starts now (W871f). It takes S1's fix (a): the leg question is skipped only after adopt-name-too-long, adopt-org-too-long or adopt-bad-org, whose fix changes the names the legs are asked under. It also takes S2 and N1, N2, N3, N5 and N6, and answers N4 and N7 in the reply.


RV871 review of PR #871 at head 66fc8f6b: READY. No MUST items. I found two SHOULD items and seven NITs. It can land only after #859 and #861, and after a merge with current main, which I resolved by hand: one hunk, and the fix is mechanical.

The head did not move while I worked: re-read at the end, #871 is still 66fc8f6b, #859 is 495d4b33 and #861 is 70801c91. I made no GitHub write. Every tool and test ran under a stub gh that exits 97, with GH_TOKEN=bogus and an empty GH_CONFIG_DIR. Nothing reached a real gh, a real organisation or the network. The only network reads were gh pr/gh issue reads and SonarCloud's public read API.

SHOULD

S1. adopt-project.py:9686: check skips #861's leg question after any of the new findings, including a topic-only one.

S2. SonarCloud reports three new issues in the new test file. The gate passes (duplication 0.1%), but #861 fixed the same rules in its own round.

  • tests/test_github_limits_init_check_org.py:189, S5778: init_args(...) is built inside pytest.raises.
  • :192, S9073: the composite assert "too-long" not in err and "bad-org" not in err.
  • :557, S5778: the SimpleNamespace(...), _Plan() and names_of(...) arguments are built inside pytest.raises.
  • Fix: build the arguments before each with, and split the assert in two.

NIT

Brief checks

  1. The merge 9ff260fb is exact. Its tree f68b7e51 equals git merge-tree --write-tree 495d4b33 70801c91, exit 0, so the merge was clean.
  2. Line 1 (family.py init) holds. A logging fake gh, end to end on the head:
    • A family of 40 characters gives family-topic-too-long; an --org of 40 gives family-org-too-long; a family of 101 with --id orbit gives family-name-too-long. my_org.x, trail- and a--b give family-bad-org. Each exits 2 with no gh call and no work directory, under --dry-run and in the real run alike.
    • A family of 39 characters goes on (--add-topic xf-project-… at 50). So do ok_org and ok-org.
    • -lead and -lead.x get unsafe-value first.
    • Under --local-remote-dir none of it is asked: my.org with a family of 40 exits 0 with "topics skipped for local remotes".
    • On main, the 40-character family, my_org.x and the 101-character family each reached gh repo create, exited 2 and left the work directory. Each dry run exited 0.
    • init --help and docs/cli.md state all three bounds.
    • A derived id is named as --id (not given, so --family lowercased), and its remediation is an explicit --id.
  3. Line 2 (adopt-project.py check) holds.
  4. Line 3 (--org) holds.
    • A separate helper is acceptable. It gives each cause its own code and leaves scaffold-project.py, adopt-project.py and setup-project.py: the plan asks GitHub's limits on a name, a topic and an organisation where GitHub is, so an id over 39 characters no longer fails --add-topic after the repositories exist (#838) #859's signature alone. The cost is N2: two refusals for one value. In check they come length first, then characters; elsewhere only the length is refused on the first run.
    • Every tool refuses ., a trailing hyphen and --: the scaffold, init, check and execute in GitHub mode, and setup at its step (5). There is no prompt for --org in any tool: setup reads it from the flag, from origin or from gh.
    • The controls pass: ok-org, ok_org and A0-b1-C2. A leading hyphen still gets unsafe-value.
    • Setup measured: for my.org, trail- and a--b, step (5) prints REFUSED scaffold-bad-org: --org is '…' and then REFUSED: the plan could not be produced (see above)., exit 2, and no parent folder is made. ok_org exits 0. o×39 plus . gets scaffold-org-too-long.
    • test_check_names_a_hand_edited_org: its new adopt-bad-org expectation is right, because execute without --local-remote-dir now refuses @x that way (measured). It still checks the reader's sentence and remedy it was written for. Its execute twin runs under --local-remote-dir and keeps plan-manifest-invalid.
    • The two tests on the permissive rule:
      • test_plan_still_writes_a_plan_for_values_the_validator_accepts (tests/test_adopt_manifest_values.py:209) now asserts that check exits 1, that no plan-manifest-invalid appears, and that FINDING adopt-bad-org does.
      • test_an_organisation_the_validator_accepts_is_still_accepted (tests/test_scaffold_manifest_values.py:540) runs under --local-remote-dir, so it still exits 0. Only its docstring changed.
    • No pinned copy changes. shape_materialize.py is carried by no project, and repo_shape.py and the validators are untouched.
  5. The merge with origin/main d7befb91 conflicts in one hunk, and the fix is mechanical. The conflict is 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's: 70801c91 and 9ff260fb conflict with main the same way, and 495d4b33 merges cleanly. The resolution puts adopt-project.py: check prints the rename-first remedy as execute does, and a plan's recorded source: repository: holds its names where the live source names none, so check and execute both refuse plan-not-the-source (#716) #825's two lines inside the except and family.py, adopt-project.py and the four tools' --org: init asks 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's else: after them, as lane 3 ruled on docs/release-notes/v1.0.0.md: the v1.0.0 release notes, a draft for Brett to amend before he tags, saying what 1.0 contains and promises, how to pin it by commit, what changed on the way grouped by the five tracking areas, and every known limit as the input that triggers it and what the person sees (#472) #473. The merged adopt-project.py compiles, and _rename_first's signature is unchanged. The resolved hunk:
        except Refusal as exc:
            findings.append(f"FINDING {exc.code}: {exc.detail}")
            # Only the rename-first remedy is printed, once after the findings: ...(#716).
            if renamed is not None and exc.remediation == renamed:
                remediation.append(renamed)
        else:
            # WHAT THE DISK OR GITHUB WILL NOT HOLD (#863): ...
            held, held_fixes = _held_findings(plan, names, local_remote_dir,
                                              naming)
            ...  (the head's else: block, unchanged)
    
  6. Tests: all passed. Results are in the table below.
  7. Mutants: I re-ran four of the writer's and nine of my own. Ten were killed and three survived; one of the survivors makes no difference to behaviour. Table below.
  8. Merges:
  9. GitHub's rules and numbers come from documentation and are not measured here. Every run used a fake or stub gh.

Outside this PR's scope, a candidate for a later issue: a family of 252 to 255 characters under --local-remote-dir passes init --dry-run with exit 0. The real run then fails with "REFUSED a git or gh command failed" and leaves the work directory. Main does the same; nothing asks the disk's 255 of <Family>.git.

Test commands and results

Every run used /workspace/projects/openRepoShape-worktrees/_lane3-scratch/RV871-scratch/RV871-run.sh, which wraps timeout 2400 /usr/bin/python3 -m pytest -q -p no:cacheprovider --basetemp=<scratch>/bt-RV871-<name> -o tmp_path_retention_policy=failed under the stub gh.

Run Where Files Result
base 9ff260fb with only the new file added the new file 62 failed, 20 passed in 53.28s, as the writer claims
A head, clean the new file, test_github_name_limits, test_reader_refusals_named, test_scaffold_manifest_values, test_adopt_manifest_values, test_adopt_github_exits, test_agents_md_codes, test_cli_reference, test_update_add_codes_documented, test_repo_hygiene 551 passed in 353.79s
B head, clean test_family, test_family_init_values, test_family_dry_run, test_free_plan_hint_reads, test_adopt_plan 390 passed, 20 skipped in 310.86s
C head, clean #861's files: test_adopt_execute_refusals, test_adopt_filter_repo_failure, test_adopt_late_failure, test_adopt_leg_keys, test_adopt_pins, test_adopt_split_branch, test_adopt_temp_work_roots, test_gh_guard, test_pin_annotated_tag, test_template_values_single_pass; plus test_adopt_project_is_the_source 761 passed in 688.79s
D head, clean test_shape_doctor (once) and test_scaffold_leg_path_collisions 540 passed, 3 skipped, 1 xfailed in 707.06s
merge head merged with main, resolved by hand, not committed the new file, test_adopt_check_plan_name (#825's), test_adopt_manifest_values, test_github_name_limits, test_reader_refusals_named, test_adopt_execute_refusals, test_cli_reference, test_agents_md_codes 565 passed in 597.56s

Mutants

Each was applied alone to 66fc8f6b and run with -x: against the new file, plus test_github_name_limits, test_adopt_manifest_values and test_reader_refusals_named for the line 2 and line 3 list mutants. The worktree was clean afterwards.

Mutant Result First test that fails
writer's l1-family-local-guard-dropped killed test_init_asks_none_of_them_of_a_local_run[o*40]
writer's l2-check-legs-after-a-finding killed test_check_finds_what_execute_refuses_under_its_code[project]
writer's l2-check-first-only killed test_check_and_execute_read_one_list
writer's l3-underscore-refused killed test_an_owner_github_spells_is_not_refused[ok_org]
mine, L1: GitHub questions asked after _creator_name killed test_init_one_past_githubs_bound_is_refused_before_any_process[derived-id-dry-run]
mine, L1: an empty --id named as passed killed test_init_names_an_empty_id_as_empty
mine, L1: GitHub questions before the device-name check survived none needed: a device name is never over 100, so behaviour cannot change
mine, L2: check drops the remediation killed …under_its_code[project]
mine, L2: findings asked after a name refusal too survived N5
mine, L2: topic asked before the organisation killed test_the_list_holds_each_refusal_in_executes_order
mine, L3: any two hyphens refused, not only two in a row killed test_an_owner_github_spells_is_not_refused[A0-b1-C2]
mine, L3: characters asked before the length survived N2
mine, L3: the scaffold asks the characters under --local-remote-dir killed test_the_scaffold_asks_no_characters_of_a_local_run

Scratch paths

  • Clone: /workspace/projects/openRepoShape-worktrees/_lane3-scratch/RV871
  • Worktrees: /workspace/projects/openRepoShape-worktrees/_lane3-scratch/RV871-scratch/RV871-head, …/RV871-base (the new file is untracked there), …/RV871-main, …/RV871-mut (clean), …/RV871-mergemain (the merge is staged, not committed)
  • Logs: /workspace/projects/openRepoShape-worktrees/_lane3-scratch/RV871-scratch/RV871-log-*.txt
  • Probes: /workspace/projects/openRepoShape-worktrees/_lane3-scratch/RV871-scratch/RV871-probe-family-scaffold.py, …/test_RV871_probe.py, …/test_RV871_setup_probe.py, and their output in …/RV871-probes/
  • Scripts: /workspace/projects/openRepoShape-worktrees/_lane3-scratch/RV871-scratch/RV871-mutants.py, …/RV871-clicheck.sh, and the merged trees in …/RV871-cli-*
  • Disclosure: I started the scratch merge with git -c user.name=RV871 -c user.email=rv871@invalid merge --no-commit. That is a one-command override; nothing was committed and no config was written. It still went against the house rule, and I did not repeat it.

For lane 1 (openRepoShape-1)

For lane 5 (openRepoShape-5)

Lane: openreposhape-3 (openRepoShape-3)

…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>
@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-10-18T04:13:09Z) Fix round W871f for RV871: #871 is now 99afbc3d, a fast-forward from 66fc8f6b. One commit, in adopt-project.py, scripts/shape_materialize.py and the new test file. No merge in this round. Tests on the head, each under a stub gh: 562 passed, 236 passed (#861's files), and 540 passed, 3 skipped, 1 xfailed (test_shape_doctor once). On the tree merged with main d6475f96, with the one hunk resolved and not committed: 316 passed. Eight mutants, all killed.

An independent re-check of this round (RV871b, opus) starts now.

Lane: openreposhape-3 (openRepoShape-3)

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-10-18T04:37:28Z) Lane 3's independent re-check of #871's fix round 1 (66fc8f6b..99afbc3d), by RV871b (opus): READY. No MUST and no SHOULD. S1's fix (a) holds: RENAMES_THE_LEGS is the right set, and check's verdict still equals execute's on 11 measured plans. S2's SonarCloud issues are closed. N1, N2, N3, N5 and N6 are as the reply says, and "no change" is acceptable for N4 and N7. All seven mutants it ran were killed. One NIT: the body's merge section names main d6475f96 and #869 40531e19, both moved since; the answers stand. #871 waits for #861's merge with main, then merges #861's new head.


RV871b re-check of PR #871, fix round 1 (66fc8f6b..99afbc3d): READY. No MUST and no SHOULD items. There is one NIT, plus one follow-up candidate that is not this PR's.

#871's head did not move while I worked: it was still 99afbc3d at the end. #859 is 495d4b33, #861 70801c91 and #823 7c20ba96. Two things did move:

I made no GitHub write. The only network use was gh reads and SonarCloud's public read API. Every tool and test ran under a stub gh first on PATH that exits 97, with GH_TOKEN=bogus and an empty GH_CONFIG_DIR.

MUST

None.

SHOULD

None.

NIT

Follow-up candidate (not this PR's)

N7's wording is left as the reply says. family-topic-too-long still says "the topic this run sets on each repository", but init sets one topic, on the holder. Once #859 lands, a later issue could let refuse_a_topic_github_cannot_set (scripts/shape_materialize.py:5099) take that phrase from its caller.

Brief checks

1. S1, fix (a): holds.

  • _held_findings can give four codes and no others. They come from _what_cannot_be_held (adopt-project.py:11126-11154):
    • adopt-name-too-long: both paths of _refuse_names_nothing_keeps keep that code (:11157-11188).
    • adopt-org-too-long and adopt-bad-org.
    • adopt-topic-too-long: _refuse_the_plans_topic swallows plan-bad-id (:11234).
    • topic_for is a plain str.format (scripts/repo_shape.py:2286), so it cannot add a fifth code.
  • The set is right. The three in RENAMES_THE_LEGS (adopt-project.py:10550) are each mended with a shorter --project, a rename-first of the source, or another org:, and each of those renames the legs. The topic's remediation (TOPIC_REMEDIATION, :11203) mends only id:, so it is correctly outside the set.
  • An edge case I measured: a plan where only code: was hand-edited past 100 characters, and testorg/Northwind-spec already exists.
    • check exits 1 with only adopt-name-too-long and makes no gh call, so the spec leg's question is skipped too.
    • But the remediation it prints is "Use a shorter --project", which renames both legs. The skip agrees with the tool's own fix, so this is no item.
  • adopt-project.py check takes no --local-remote-dir and resolves a relative submodule url from GitHub's remotes, while execute --local-remote-dir resolves it from the bare repositories it creates, so under that test path check refuses an adoption execute verifies and passes one execute refuses #417 holds.
    • By the code: the leg question is now added only to a check that already has a finding. _leg_remote_findings turns every gh failure into a NOTE and raises nothing, so check's exit stays 1.
    • Measured on 11 plans: check exited 1 on every one, and execute exited 2 with no gh call, except the control, where execute asks the two leg views and refuses leg-remote-exists.

The probe runs check and then execute on the same plan, against test_adopt_github_exits' fake GitHub, on 99afbc3d:

Plan check gh calls check findings execute refusal
RV871's scenario: id of 40, testorg/Northwind-code exists the two leg views adopt-topic-too-long, leg-remote-exists adopt-topic-too-long
control: id ok, code leg exists the two leg views leg-remote-exists leg-remote-exists
id of 40, spec leg exists the two leg views adopt-topic-too-long, leg-remote-exists (spec) adopt-topic-too-long
derived id of 40 (--project of 40), code leg exists the two leg views topic, then leg topic
org: of 40, code leg exists none adopt-org-too-long adopt-org-too-long
--project of 96, code leg exists none adopt-name-too-long adopt-name-too-long
id of 40 and org: of 40 none adopt-org-too-long, adopt-topic-too-long adopt-org-too-long
id of 40, my.org, code leg exists none adopt-bad-org, adopt-topic-too-long adopt-bad-org

The same probe on 66fc8f6b differs only in two places: the three topic rows asked nothing and listed the topic alone, and the past-39 misspelled values were printed whole.

2. S2: gone.

3. N1, N2, N3, N5, N6: each as the reply says.

4. N4 and N7: "no change" is acceptable for both.

5. Nothing else changed. I read every hunk of the round:

  • adopt-project.py: the module docstring (:101-107), cmd_check (:9677-9700), RENAMES_THE_LEGS and _held_findings' three-part return (:10543-10573). _held_findings has no other caller (git grep).
  • shape_materialize.py: the N3 cut (:5187-5197).
  • The tests.
  • No doc states the changed behaviour, and docs/cli.md is generated from --help, which does not carry the module docstring. render-cli-reference.py --check prints [ok] on the head and on both merges. No pinned copy is involved: shape_materialize.py is carried by no project.

6. Tests: all passed. Every run used /workspace/projects/openRepoShape-worktrees/_lane3-scratch/RV871b-scratch/RV871b-run.sh, which wraps timeout 2400 /usr/bin/python3 -m pytest -q -p no:cacheprovider --basetemp=<scratch>/bt-RV871b-<name> -o tmp_path_retention_policy=failed under the stub.

Run Where Files Result
setA head, clean the new file, test_github_name_limits, test_reader_refusals_named, test_adopt_manifest_values, test_scaffold_manifest_values, test_adopt_github_exits, test_adopt_execute_refusals, test_agents_md_codes, test_cli_reference, test_update_add_codes_documented, test_repo_hygiene 743 passed in 574.80s (0:09:34)
setD head, clean test_shape_doctor (once) and test_scaffold_leg_path_collisions 540 passed, 3 skipped, 1 xfailed in 675.13s (0:11:15)
first head 66fc8f6b with only the round's test file put in the new file 6 failed, 87 passed in 36.80s: the same six tests the body names
merge with d6475f96 resolved by hand, not committed the new file, test_adopt_check_plan_name 138 passed in 108.41s
merge with d6071c7d resolved by hand, not committed the same two, plus the 7 test files #806 changed 477 passed, 1 skipped in 319.15s

The probes passed: 11 passed for check/execute, 15 passed for N2 across init, the scaffold and setup, and 1 passed for the one-leg edge case.

7. Mutants: each was applied alone to 99afbc3d in RV871b-mut and run against the whole new file without -x. The worktree was clean after each. All seven were killed.

Mutant Tests that fail
writer's s1-topic-skips-too test_check_finds_what_execute_refuses_under_its_code[id], [derived-id], test_check_asks_the_legs_beside_a_topic_it_finds (3 failed, 90 passed)
writer's n2-family-characters-before-length test_init_refuses_an_organisations_length_first[dry-run], [real] (2 failed)
writer's n5-held-after-a-name-refusal test_check_asks_none_of_them_of_names_that_do_not_classify (1 failed)
mine: adopt-bad-org dropped from RENAMES_THE_LEGS …under_its_code[org-dot], [org-underscore-and-dot], [org-trailing-hyphen], [org-two-hyphens]
mine: adopt-name-too-long dropped from the set (also ran test_adopt_manifest_values and test_github_name_limits) …under_its_code[project] only (1 failed, 235 passed)
mine: adopt-org-too-long dropped from the set …under_its_code[org]
mine: N3's cut starts one character late (<= GITHUB_OWNER_MAX + 1) test_check_lists_an_organisations_length_then_its_characters

The second of my mutants is killed only in GitHub mode. Under --local-remote-dir the 247-character checks passed with it applied, so asking the leg question there adds nothing for those values.

8. Merges (git merge-tree --write-tree of 99afbc3d):

Scratch paths

  • Clone: /workspace/projects/openRepoShape-worktrees/_lane3-scratch/RV871b
  • Worktrees, all under /workspace/projects/openRepoShape-worktrees/_lane3-scratch/RV871b-scratch/:
    • RV871b-head: clean.
    • RV871b-mut: clean.
    • RV871b-probe: three untracked probe files.
    • RV871b-first: 66fc8f6b, with one untracked probe file.
    • RV871b-mergemain and RV871b-mergemain2: each merge was put in place with git read-tree -u --reset <merge-tree tree> and resolved in the working tree. Nothing was committed, and no identity or config was set.
  • Logs: /workspace/projects/openRepoShape-worktrees/_lane3-scratch/RV871b-scratch/RV871b-log-*.txt, including RV871b-log-mutants.txt.
  • Scripts: /workspace/projects/openRepoShape-worktrees/_lane3-scratch/RV871b-scratch/RV871b-run.sh, /workspace/projects/openRepoShape-worktrees/_lane3-scratch/RV871b-scratch/RV871b-mutants.py, /workspace/projects/openRepoShape-worktrees/_lane3-scratch/RV871b-scratch/RV871b-mutrun.sh, /workspace/projects/openRepoShape-worktrees/_lane3-scratch/RV871b-scratch/RV871b-resolve.py
  • Stub: /workspace/projects/openRepoShape-worktrees/_lane3-scratch/RV871b-scratch/RV871b-stubbin/gh
  • The review inputs I read: /tmp/claude-1000/-workspace-projects-openRepoShape/b30e8b49-f249-4668-adb1-1dbfa93c09e1/scratchpad/RV871b-rv871.md, /tmp/claude-1000/-workspace-projects-openRepoShape/b30e8b49-f249-4668-adb1-1dbfa93c09e1/scratchpad/RV871b-reply.md, /tmp/claude-1000/-workspace-projects-openRepoShape/b30e8b49-f249-4668-adb1-1dbfa93c09e1/scratchpad/RV871b-pr.md, /tmp/claude-1000/-workspace-projects-openRepoShape/b30e8b49-f249-4668-adb1-1dbfa93c09e1/scratchpad/RV871b-issue.md

Lane: openreposhape-3 (openRepoShape-3)

Brett Heap and others added 3 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>
Base automatically changed from fix/838-github-name-limits to main October 18, 2026 08:56
Brett Heap and others added 4 commits October 18, 2026 08:59
…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>
… 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>
…org (restack after #859 landed as 281df26): step 3 of 4 merges, clean (#863)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… step 4 of 4 merges, adopt-project.py, scaffold-project.py and scripts/shape_materialize.py taken as ours (#859's own files, the squash equals its final head 9695935 there) (#863)

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

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-10-18T09:14:07Z) #871 is restacked onto main by lane 3's R871 (sonnet), now at c83b3b68. #861 landed as fe99b8b and #859 as 281df26, and GitHub moved the base to main when #859's branch was deleted.

Lane: openreposhape-3 (openRepoShape-3)

@sonarqubecloud

Copy link
Copy Markdown

@brettheap
brettheap marked this pull request as ready for review October 18, 2026 11:04
@sourcery-ai

sourcery-ai Bot commented Oct 18, 2026

Copy link
Copy Markdown

Sorry @brettheap, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 5 days and 13 hours by commenting @sourcery-ai review. Upgrade to get a review now.

@brettheap
brettheap merged commit a249910 into main Oct 18, 2026
5 checks passed
@brettheap
brettheap deleted the l3-863-github-limits-init-check-org branch October 18, 2026 11:04
brettheap pushed a commit that referenced this pull request Oct 18, 2026
…ith #877, #865 and #871 (#883)

Round 4 of #883, a refresh after RV883 found 34e0f5b READY (#883
issuecomment-6248756609). Each line comes from its squash body and was
checked against the code at main a249910:

- #877 (#856): a holder's bootstrap reads an index or HEAD `.gitmodules`
  up to a line git cannot parse, as git does, and a parity test holds the
  value rules both family scripts write to one answer. The #856 limit is
  retired.
- #865 (#840): `family.py add` names first, with a remedy that types as
  printed, the states #840 lists, and its closing guard takes
  `validate-family.py`'s form. The limit on the two guards and on the
  case-spelling states is retired; three states #865's body leaves
  unruled are a narrower limit.
- #871 (#863): `family.py init` asks GitHub's limits before any `gh` call,
  `adopt-project.py check` finds what `execute` refuses from one list, and
  an `--org` GitHub gives no organisation is refused where GitHub is
  asked. The #863 clauses are retired; that the characters are from
  GitHub's documentation, with `_` let by, joins the limit that says none
  was measured.

RV883's optional NIT: the `adopt-work-dir-shared` limit no longer says
"what no owner or mode bit shows", since group write is a mode bit.

63 pull requests; 64 of the milestone's 72 issues closed at 11:05Z, and
the open list is the 8 that read leaves: #675, #707, #755, #764, #853,
#854, #879 and #884. The shape-copy lists stand. No other file changes.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 18, 2026
…e and the innermost command named, so a member whose make runs another no longer reads `exited 2` for a finding, and `make -k` keeps its `Error` line (#884) (#886)

#884's three lines, from rv876's NIT 2, NIT 3 and NIT 4 on #876 (#876 issuecomment-6246717354), with fix round 1 from rv884's review of this pull request (issuecomment-6248806027). The change is in the family holder's pinned `templates/family-root/scripts/bootstrap.py` and in `tests/test_family_holder_bootstrap_texts.py`, and nothing else.

Base: origin/main `281df263`, where #876 has landed as `41baf920`. Each line was probed there first, under the holder's own `make bootstrap`, with GNU Make 4.3, dash 0.5.12 as `/bin/sh` and git 2.43.0. The probes reuse rv876's `p2/ptyrun.py` and its member Makefiles.

**1. Five mutants of #876's code survived its tests, and one docstring claimed what no test checked.** Tests only; no code changes for this line.
- **R10 and R11** (`credential_advice` ignoring `unreadable` or `refused`): `test_no_advice_beside_no_member_updated_with_every_member_checked_out`. Every member is checked out, as in the holder itself, and HEAD's `.gitmodules` is given `update = None` or a conflict marker. Bootstrap's own stderr lines after the fetch line are asserted exactly: the "NO MEMBER UPDATED, and not for want of a credential" line and the failure, with no advice between them. `tests/test_family_gitmodules_values.py` asks only the last two lines, which is why the mutants passed there.
- **R1** (`named[0]`) **and R8** (the regex anchored on `^make: `): `test_the_line_names_the_code_of_the_member_s_innermost_command[last-under-make]`. `bootstrap.py --no-fetch` runs under a real parent `make -k`, as the holder's `make bootstrap` runs it, so the member's make prints `make[1]:`. One member exits 1. Another has two commands that exit 1 and then 2, and the line must name the last.
- **R7** (`passed_on`'s `sink.flush()` dropped): `test_a_member_s_question_shows_before_its_answer_is_typed`. Bootstrap runs on a pseudo-terminal (its stdin, stdout and stderr). A member recipe asks `Continue? ` on stderr and `read`s the answer. The test types `yes` only once the question has shown, and fails if it has not shown within 10 s. It asserts the output before the answer and after it exactly. Measured on the pty: the question showed at 0.13 s at head; with R7 on `281df263` it showed only at 8.1 s, after the blind answer, when the run ended.
- **The store:** `test_a_git_mv_staged_with_work_in_the_checkout_is_undone_as_printed` now asserts what its docstring said. After every line is typed, the checkout's commit (the new commit, or the pin where only an untracked file was added) is still in `.git/modules/members/IRRS`. A helper, `kept`, is shared with `test_a_checkout_moved_on_at_the_variant_is_moved_with_f`'s own store assert.
- On `281df263`, with rv876's own `mut/mutants.py` unchanged, every one of the five is caught by the test named above, and those tests pass on the unmutated code:

| rv876's mutant on `281df263` | caught by |
|---|---|
| R1 `named[0]` | `[last-under-make]` |
| R7 no `sink.flush()` | `test_a_member_s_question_shows_before_its_answer_is_typed` |
| R8 `^make: ` | `[last-under-make]` |
| R10 advice ignores `unreadable` | `test_no_advice_beside_no_member_updated_with_every_member_checked_out[unparsed]` |
| R11 advice ignores `refused` | `test_no_advice_beside_no_member_updated_with_every_member_checked_out[value-refused]` |

**2. The final line misled in three cases** (`run_make`, `MakeErrors`).
- Measured on `281df263` and on this head, each member under the holder's `make bootstrap` and with `bootstrap.py` run straight (`-k` where the case needs it). The last five rows came from rv884's review:

| member | `281df263` | head |
|---|---|---|
| `$(MAKE) inner`, inner command exits 1 | `exited 2` | `exited 1` |
| `$(MAKE) inner`, inner command exits 2 | `exited 2` | `exited 2` |
| `$(MAKE) nothere`, the inner make's own failure | `exited 2` | make printed no `Error <code>` line for a command of the member's |
| `$(MAKE) -s nothere; exit 3` | `exited 3` | `exited 3` |
| `make -k`: `Error 1`, then 5000 bytes on stderr | make printed no `Error <code>` line (false) | `exited 1` |
| `make -k`: two commands exit 1, then 3 | `exited 3` | `exited 3` |
| a command killed by SIGKILL under dash | `exited 137` | `exited 137` |
| `kill -TERM $$` in a recipe | make printed no `Error <code>` line | the same |
| `$(MAKE) -s nothere \|\| exit 1` | `exited 1` | `exited 1` |
| `$(MAKE) -k inner`, whose second target prints after the inner `Error 1` | `exited 2` | `exited 1` |
| `make -k`: a finding, then a command that `kill -KILL $$` ends | `exited 1` | `exited 1` |
| `-$(MAKE) inner` (inner exits 1), then the recipe's own `exit 2` | `exited 2` | `exited 2` (`82a84f88` read `exited 1`) |
| `printf 'x: *** y' >&2; exit 1`, so make's line follows it on one line | `exited 1` | `exited 1` (`82a84f88` named none) |

- **A recursing member: the line names the innermost command's code.** Why: the inner make exits 2 for any failure, which is the very code #839 set out to see past. The command the member's author wrote, and whose code tells a finding (1) from a refusal (2), is the innermost one.
  - The depth comes from make's own prefix (`make[N]:`, or `make:` for 0).
  - An `Error 2` at a shallower depth than the last `***` line before it, whatever lines come between, is taken for that make's parent reporting it, and is passed over.
  - Where the inner make failed on its own (`No rule to make target`), no command's code is named, and the line is the one for make's own failure.
  - A command that runs a failing make and then fails itself, with 3 or with 1, is still named.
  - Two failures at the same depth under `-k` name the last, as before.
  - Where a `-` let the inner make fail, make prints `[Makefile:N: bootstrap] Error 2 (ignored)`, with no `***`. That line now puts the depth back at the parent's, so the parent's own `Error 2` after it is named. rv884 offered this as an option, to adopt only if it changed no other case. Measured: across rv884's 133 case runs (its `tools/cases1.py` to `cases6.py`, at both depths), head with it differs from `82a84f88` only in that case and in the last-match case below. Across 21 more of mine it differs only in that case, in `-$(MAKE) nothere` then `exit 2`, and in the last-match case, each now as `281df263` read it. An inner make that ignores a failure of its own prints the same kind of line at its own, deeper depth; that line changes nothing (measured: `$(MAKE) inner; exit 2`, where inner ignores an `exit 2`, reads `exited 2`).
- **The last match on a line** (rv884's NIT 1). A member's own text with no newline, `x: *** y`, followed by make's `Error 1` on the same line, named no code at `82a84f88`. `said` now takes the last match on a line (`findall`), and that case reads `exited 1`, as `281df263` read it.
- **Where the line is not the innermost command's code.** This is now written in `MakeErrors`' docstring, not only in this body (rv884's NIT 2):
  - a recipe that runs a make, lets its failure pass in the shell (`|| true`, or `;`), or with a `-` in a make run with `-s` (which prints no `(ignored)` line), and then exits 2 itself: that `Error 2` is read as the inner make's report, and the inner command's code is named. Measured: `$(MAKE) inner || true; ...; exit 2` and `-$(MAKE) inner` under the holder's `make -s` read `exited 1`; `281df263` read `exited 2`;
  - an inner make run with `env -u MAKELEVEL`, which prints its parent's depth: the parent's `Error 2` is named, as on `281df263`;
  - under `-j`, a job of the parent's that fails while the inner make is still running: its line comes between, and the parent's `Error 2` is named, as on `281df263`;
  - a member's own line shaped like a deeper make's (`x[3]: *** y`) before it exits 2 itself: its `Error 2` is passed over.
  - The module docstring's (b) now says the code is the innermost command's "as far as make's lines tell it".
- **More than 4 KiB after the `Error` line:** `run_make` kept the last 4096 bytes of make's stderr and read that tail at the end. `MakeErrors` now reads each line as it passes. It keeps only the last 4096 bytes of a line that no newline has ended yet, so memory stays bounded for a progress meter that writes only `\r`. rv884's `tools/chunks.py`, pointed at this head, fed 174 captured streams to `MakeErrors` in 81,406 chunkings; none read differently from the whole.
- **A signal:** the docstring is now true of rv884's 24 cases (`probe2.log`: dash 0.5.12 and bash 5.2.21; a command run with no shell, one command, `kill $$`, a command then `echo`, a command then `exit $?`, `echo` then a command; SIGKILL and SIGTERM; at both depths).
  - Where the signal kills what make started, make prints its name, `Killed` or `Terminated`, and names no code. That is: a command make runs with no shell; the shell a `kill -KILL $$` kills; or a command bash runs in its own place, which is a line's last command, a line of one command included.
  - Where the shell outlives the command, the shell prints the signal's name itself, `Killed` or `Terminated`, and exits 137 or 143, which make names. dash outlives every command; bash outlives one that is not a line's last.
  - Where the line goes on after the command, the shell exits as the rest of the line does: `; echo after` ends the recipe with 0.
  - The line itself was not changed for a signal. The signal's name is printed right above make's `Error 137`, and a command may also exit 137 by itself.
- **Tests:** `test_the_line_names_the_code_of_the_member_s_innermost_command`, with four sets:
  - `[innermost-under-make]` and `[innermost]` run `INNERMOST`'s 11 members, once under a parent make and once with the member's make at depth 0. They are the first five rows of the table, the last five, and an inner make that ignores an `exit 2` of its own, then `exit 2` (`InnerLets`, run without `-s`, which would keep make from saying so);
  - `[last-under-make]` is line 1's R1 and R8 test;
  - `[signal]` runs one of each signal case in a form that does not hang on which shell `/bin/sh` is: `sh -c 'kill -KILL $$'; exit $?`, which the shell outlives (137), and `kill -KILL $$`, which kills the shell (no code).
  - Each set asserts the run's final line exactly. Under the parent make, a premise checks that no member line reads `make:`.
- `test_a_line_that_comes_in_pieces_is_read_whole` (the pieces test) feeds `MakeErrors` an `Error` line cut in two, and a line ended by no newline, which stays at `LINE_KEPT` bytes.
- The existing `test_a_finding_a_refusal_and_make_s_own_failure_are_told_apart` (depth 0, no `-k`) passes unchanged. It now shares `with_members` and its line helpers with the new tests.

**3. What passing make's stderr on changes.** I chose to document it in `run_make`'s docstring, after measuring both ways.

| effect | `281df263` and head (documented) | reading only until make exits (prototype, not committed) |
|---|---|---|
| `test -t 2` in the member | false: the pipe | false |
| stdout and stderr order | out of order on the pty: one run printed `err 1` after `out 4`, another all six `out` lines first | the same |
| recipe `sleep 6 &` | 6.14 s | 0.29 s |
| `( sleep 1; echo late >&2 ) &` | 1.12 s, `late` shown | `late` lost, and the writer died at that write: its status file was never written |
| non-UTF-8 bytes (`caf` and 0xE9) | passed on as written | the same |

- **The background holder.** Stopping once make has exited and nothing is waiting fixes only the hold, and only by closing the pipe. The left-running process's next stderr write then meets a closed pipe and dies of SIGPIPE, silently. Holding the run is visible, ends when the process lets go of stderr, and the recipe can give the process its own stderr (`2>log`), as the docstring says.
- **The terminal.** Giving make the terminal back would lose its `Error <code>` line. Measured: `make --debug=j` prints only "Reaping losing child" on stdout, with no code, and no other output make writes carries the code. That would reopen line 2 of #839, which this change keeps closed: make still runs.
- **The line order and the Windows decoding** cannot be fixed by the reader. The docstring says each, with the Windows case read and not run.
- **Tests that hold the docstring:**
  - `test_what_a_member_s_make_leaves_on_stderr_is_passed_on_as_written` (the leaves test) asserts bootstrap's stderr bytes exactly: the non-UTF-8 line as written, then `late`, before the run ends;
  - the pty test asserts `stderr: no terminal` and `stdout: a terminal` from the member.

**Pinned copy and docs.**
- `bootstrap.py` is in `FAMILY_COPIED_VERBATIM`, so each holder takes one `upstream-changed` row for it on its next update. Its mode stays 100755.
- Its imports from `repo_shape` are unchanged, so the skew rule (`tests/test_pinned_copy_text.py`) and `tests/test_update_shape_kept_helper.py` hold.
- No `--help` text changed, so `docs/cli.md` is unchanged.
- No sentence of `AGENTS.md`, `templates/family-root/AGENTS-shape.md`, `templates/family-root/README.md` or `README.md` speaks of the make line or of stderr (grep).

**Fail on base.** The test file at this head, run on `281df263`'s code: 3 failed, 54 passed. On `82a84f88`'s code (this pull request before fix round 1): 2 failed, 55 passed.
- On `281df263`, the 3 failures are the tests that pin line 2's change: `[innermost-under-make]`, `[innermost]` and `test_a_line_that_comes_in_pieces_is_read_whole`. On `82a84f88`, the 2 failures are both `[innermost*]`.
- These pass on `281df263` by design:
  - the pure mutant-killers `[last-under-make]` (R1, R8), the pty test (R7) and both `test_no_advice_beside_no_member_updated_with_every_member_checked_out` cases (R10, R11);
  - the tests that hold a docstring: `[signal]`, `test_what_a_member_s_make_leaves_on_stderr_is_passed_on_as_written`, and the store assert in both `test_a_git_mv_staged_with_work_in_the_checkout_is_undone_as_printed` cases.
- Each `INNERMOST` member, run straight with `-k` (depth 0):

| member | expected | `281df263` | `82a84f88` | head |
|---|---|---|---|---|
| `Recurse` | 1 | 2 | 1 | 1 |
| `RecurseRefuse` | 2 | 2 | 2 | 2 |
| `InnerOwn` | none named | 2 | none named | none named |
| `InnerThenOwn` | 3 | 3 | 3 | 3 |
| `InnerThenFinding` (X12) | 1 | 1 | 1 | 1 |
| `Long` | 1 | none named | 1 | 1 |
| `KeepsGoing` (X11) | 1 | 2 | 1 | 1 |
| `ThenKilled` (X7) | 1 | 1 | 1 | 1 |
| `LetFail` | 2 | 2 | 1 | 2 |
| `InnerLets` | 2 | 2 | 2 | 2 |
| `OwnStars` | 1 | 1 | none named | 1 |

- rv884's own X7, X11 and X12, its saved copies of `82a84f88` with each mutant, read their member wrong: `ThenKilled` says none, `KeepsGoing` says 2 and `InnerThenFinding` says none.

**Mutants at this head**, each run against the whole test file: 36 made, 33 caught. The 3 survivors are the equivalents rv884 named: X5 (`\s*$` dropped; make's `***` lines carry nothing after the code), X6 (the unended remainder read at EOF; make always ends its line) and X9 (passed over only one level deeper; makes nest one level at a time).

| mutant | caught by |
|---|---|
| R1, first `Error` line kept | `[last-under-make]`, both `[innermost*]` |
| R7, no `sink.flush()` | the pty test |
| R8, regex anchored on `^make()?: ` | `[last-under-make]`, both `[innermost*]`, the pieces test |
| R10, R11 | the matching `test_no_advice_beside_no_member_updated_with_every_member_checked_out` case |
| H1 nothing passed over; H2 any code passed over; H4 depth never read; H6 depth not kept; H8 a `***` line with no code not read; H9 the 4096-byte tail again | both `[innermost*]` |
| H3, `<=` for `<` in the depth test | `[last-under-make]`, both `[innermost*]` |
| H5, `make:` read as depth 9 | `[innermost]` |
| H7, depth starting at 99 | both `told_apart` cases, both `[innermost*]` |
| H10 an unended line dropped; H11 kept whole | the pieces test |
| H12, only a chunk's last line read | `[last-under-make]` |
| D1 reading stopped once make exits; D2 the bytes decoded as UTF-8 | the leaves test |
| rv884's X1 (`!=`), X2 (depth only on coded lines), X4 (depth group dropped), X7, X11, X12 | both `[innermost*]` (X1 also both `told_apart` cases) |
| rv884's X3 (`[:LINE_KEPT]`) | the pieces test |
| rv884's X8 (`splitlines`) | 7 tests |
| rv884's X10 (one-digit code) | `[signal]` |
| N1, the first match on a line | both `[innermost*]` (`OwnStars`) |
| N2, the `(ignored)` line not read | both `[innermost*]` (`LetFail`) |
| N3, the `(ignored)` line always sets the depth; N5, it falls through | both `[innermost*]` (`InnerLets`) |
| N4, `max` for `min` | both `[innermost*]` (`LetFail`, `InnerLets`) |

**Runs**, each through `run9s.sh` with `SHARDS=3` at this head, with no tracked changes. The files: `tests/test_family_holder_bootstrap_texts.py`, `tests/test_family_bootstrap_gate_order.py`, `tests/test_bootstrap_spelling.py` and `tests/test_pinned_copy_text.py`; the two that `git grep -l 'family-root/scripts/bootstrap' -- tests` names, `tests/test_family.py` and `tests/test_update_shape_kept_helper.py`; the trio, `tests/test_repo_hygiene.py`, `tests/test_agents_md_codes.py` and `tests/test_cli_reference.py`; and five more that run the holder's bootstrap: `tests/test_family_gitmodules_values.py`, `tests/test_family_head_lost_gitmodules.py`, `tests/test_family_members_not_a_list.py`, `tests/test_bootstrap_without_git.py` and `tests/test_family_makefile_siblings.py`.
- git 2.43.0: 1049 passed, rc 0 (shards 218, 394 and 437), at `dbd3a6dd`.
- git 2.47.1: 1049 passed, rc 0 (shards 218, 394 and 437), at `dbd3a6dd`.

**Merge checks**, `git merge-tree --write-tree` of this head, after a fresh fetch:

| against | result |
|---|---|
| origin/main `71f36fe0`, where lane 3's #877 has landed as `c9ca9a45`, editing other parts of `bootstrap.py` | clean, tree `8b5826db` |
| origin/main `201c6f09`, after #865, #871 and #815 too | clean, tree `8c3d8e13`; on a commit at that tree, this test file, `tests/test_family_gitmodules_values.py` and `tests/test_pinned_copy_text.py` gave 291 passed, rc 0 (git 2.43, rv884's re-check, #886 issuecomment-6249507909) |
| #877's head `35b214b2` | clean, tree `b80a2ba0` |

**Sonar-style checks.**
- No duplicated block: no 10-line window that touches an added line appears anywhere else in `tests/`, `scripts/` or `templates/`. At 6 lines, only the import block matches another file.
- No assert joins conditions with `and`. The one `or` assert in the file is #876's.
- Cognitive complexity, counted by hand with Sonar's rules: no changed function is over 15, so no `def` line had to stay as it was. `said` counts each boolean sequence: `depth_said or 0`, `code == b"2" and ...`, and `code and not ...`.

| function | `281df263` | `82a84f88` | head |
|---|---|---|---|
| `run_make` | 2 | 1 | 1 |
| `MakeErrors.read` | new | 1 | 1 |
| `MakeErrors.said` | new | 5 | 6 |
| `read_until` (test) | new | 9 | 9 |
| `made`, `with_members` (test) | new | 2, 1 | 2, 1 |
| `test_the_line_names_the_code_of_the_member_s_innermost_command` | new | 4 | 4 |
| `test_a_finding_a_refusal_and_make_s_own_failure_are_told_apart` | 3 | 2 | 2 |
| `test_a_git_mv_staged_with_work_in_the_checkout_is_undone_as_printed` | 3 | 3 | 3 |

**Not here.**
- No Windows or macOS run, since CI is Linux only (#597). The Windows decoding is read, not run.
- Make before 4.0 was not run. The prefix and the `Error` line keep make 3.81's form by its source, but the line-2 test already asserts make 4's quoting.
- The innermost rule's limits are in `MakeErrors`' docstring and listed under line 2 above. A make that speaks another language still gets the line for make's own failure.

Review: rv884, lane 5's independent review, READY at `82a84f88` (issuecomment-6248806027), with no MUST, one SHOULD (three surviving mutants) and four NITs. w884's fix round 1, `dbd3a6dd`, answered each (issuecomment-6249230294), and rv884's re-check found it READY with no MUST or SHOULD (issuecomment-6249507909). Its two NITs on the `(ignored)` rule, which cover rarer nested-make shapes, go to a follow-up. CI (Brett Heap's ruling 27 and his fast-refresh ruling of 2026-10-15T22:48:35Z): H0 dbd3a6d passed tests, tests-git-bash and SonarCloud; the head 85c2078 is a clean update-branch merge of main 0d5d2b1, with tests-git-bash and SonarCloud green; and the local gate passed on its predicted squash tree 5c273a1 over main 0d5d2b1: the family_holder_bootstrap_texts, repo_hygiene, agents_md_codes, cli_reference sets, 205 passed. Landed on Brett Heap's word of 2026-10-17T21:47:28Z, in lane 5's session (posted on #473 as issuecomment-6241758069): "merge lane 5's v1.1.1 PRs once reviewed and green".

Closes #884

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 18, 2026
…source.py, tests/test_adopt_tracking_branch.py, tests/test_adopt_origin_repository.py, tests/test_template_text_placeholders.py and tests/gh_guard.py: the docstrings say that `check` asks `gh repo view` of each leg only once the plan's names and `org:` pass, that a source's `origin` need not be a GitHub URL, and that `execute` is held to what it runs through `run` (#880)

The cross-lane review of #881 (lane 5, READY, no MUST) found two false
sentences carried into the first two docstrings and four more files with
the same fault as #880. All counts are from the session's guard log, kept
with `tmp_path_retention_policy=all`, at the unchanged code; every call is
`repo view <owner>/<name>-spec|-code --json nameWithOwner`, a probe.

- "Each source's `origin` is an `https://github.com/` URL" is false for
  most cases: it is 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` is run in-process with every `git` and `gh` command it would
  run replaced" is false: only what goes through `run` is replaced
  (`refused_before_any_remote`'s own docstring says so).
- `check` asks only given no `--local-remote-dir`, once `_check_names`
  passes and the `org:` passes `checked_value` (on main since #871, and
  once no finding that renames the legs comes first): 18 of the 45 cases
  of test_adopt_check_plan_name.py run `check` without it and ask nothing.
- Off Windows, because a bare `["gh", ...]` skips `gh.cmd`
  (`tests/conftest.py`'s guard comment).
- test_adopt_tracking_branch.py said "Nothing here runs `gh`": 18 calls in
  8 of 17 cases. test_adopt_origin_repository.py said "Nothing here
  fetches or runs `gh`": 4 calls in 2 of 30, both params of
  test_a_path_origin_is_not_compared_and_plan_says_so.
  test_template_text_placeholders.py said its `--local-remote-dir` runs
  "never call `gh`": true of them, but its two adoption tests also run
  `check` without one: 20 calls in 10 of 142 cases. gh_guard.py said
  "Every `check` ... given no `--local-remote-dir` asks it twice": now
  once the names and `org:` pass, once of each leg.

Only docstrings change; each file's AST outside its module docstring is
the same, and gh_guard.py's paragraph is reflowed.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 18, 2026
… the release commit, #778's leg-clone sentence and #802's readers sentence, each checked against runs at v1.1.0 and main (#883 follow-up) (#893)

The v1.1.1 notes landed as the release commit `5017830` (#883), and RV883's round 5 held two SHOULDs and five NITs for after it (#883 issuecomment-6250558821). This applies them to `docs/release-notes/v1.1.1.md`, with two more sentences lane 1's review found:

1. **#778's leg-clone sentence** says what runs at `v1.1.0` and `5017830` show. A leg clone whose own `AGENTS.md` says so keeps its leg row and its Triad row over each fault, where v1.1.0 gave `FINDING the way in refused` over a tab line and a traceback, exit 1, over a pattern that does not compile or a policy that raised on its name. A leg known only by its name gets a repository's two `n/a` rows.
2. **"Is", not "stays", exit 2,** in the pattern case, which was exit 1 in v1.1.0.
3. **#802's clause is dropped:** v1.1.0's `_blocks_inside` already read a `try`'s handlers, `else` and `finally` outside its guard.
4. **"What 1.1.1 fixes"** names "or cannot be used", and `the other way in`.
5. **#815's leftover tracebacks** were measured on Python 3.12; 3.13 is read, not run.
6. **#871 lets `_` through for any owner,** an organisation's name included.
7. **#869 asks the disk** only about a name that is not UTF-8.
8. **#802's readers:** at v1.1.0 only the import reader skipped an `if TYPE_CHECKING:` body; the binding reader skips one now.
9. **A known limit names #940:** under the `C` or POSIX locale, the bash `openRepoShape` and `setup.sh`'s guards refuse a URL, path or ref with a letter past ASCII as "a control character", a regression from v1.1.0 in the launcher through #872 (#866); a UTF-8 locale takes such a URL or path.

The draft-time lines in the sections marked "remove at tag time" say main `2d372bb`, where #778 landed file for file as its reviewed head `8259ea1`, and that #879 moved to v1.2.0. The release body never carries those sections.

Review: lane 1's independent reviewer found `b795ef74` NOT READY, on the leg-clone sentence, and `ca49f125` READY, with each changed sentence checked against runs at `v1.1.0` and `5017830`. Lane 1's second reviewer found `6cdb5551`, which adds the #940 limit, READY, with each clause measured against the launcher at `64464d31` and at v1.1.0.

It lands by fast refresh (Brett Heap, 2026-10-15T22:48:35Z). Its reviewed head `6cdb5551` is green on Linux and SonarCloud. The branch merged main once and was never rebased; the last merge is update-branch's, clean, giving f60654e, which is green on SonarCloud and Git Bash. Lane 1's gate passed on its tree: the 5 test files that read the release notes or the workflows (`test_repo_hygiene.py`, `test_release_workflow.py`, `test_workflow_concurrency.py`, `test_agents_md_codes.py` and `test_cli_reference.py`), 221 passed and 0 skipped.

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