Skip to content

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

Merged
brettheap merged 4 commits into
mainfrom
fix/838-github-name-limits
Oct 18, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Title

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)

Body

GitHub keeps a repository's name to 100 characters, a topic to 50, and a user's or an organisation's name to 39. The 100 and the 50 are documented on docs.github.com ("Creating a new repository"; "Classifying your repository with topics"). For 39, the docs give the limit on a user's name (the Enterprise Cloud "Username considerations for external authentication"), and nothing for an organisation's. None of the three is measured here. No run of this repository creates a real repository, and I did not run the real gh to find them. The fake gh in the new tests refuses only a topic over 50 characters; a name over 100 or an organisation over 39 never reaches it. A probe against a real organisation is lane 1's.

Nothing asked these limits before:

  • scaffold-project.py sets xf-project-<id> last, after it has created and seeded all three repositories. An id over 39 characters failed gh repo edit --add-topic there. A --project of 40 or more gives such an id by default.
  • adopt-project.py execute failed at the same topic after both legs and the split were pushed and the pull request was open.
  • A --project of 96 to 250 characters, or an --org of 40 to 255, passed the plan and reached the first gh repo create.

Now each is refused before any repository is made, and only where a GitHub repository will be made. Under --local-remote-dir nothing goes to GitHub: a name's limit is still the disk's, and no topic is set.

Each line of the issue

1. A topic over 50 characters. The direction was to ask in the plan. The number lives in scripts/shape_materialize.py, beside the disk's NAME_MAX: GITHUB_TOPIC_MAX = 50, and GITHUB_ID_MAX = 50 - len("xf-project-") = 39. I did not put it in the contract's id_pattern, so scripts/repo_shape.py is not re-pinned, and a local-only project keeps a long id. refuse_a_topic_github_cannot_set reads the id's bound off the topic the naming policy gives.

  • scaffold-project.py refuses scaffold-topic-too-long right after scaffold-bad-id. That is before _declaration, which calls gh api for a --pin with no --pin-source, and --dry-run refuses the same. No process has started by then when --elected-by or GIT_AUTHOR_NAME names the elector. Without either, _identity has run git config user.name first, as it has before scaffold-bad-id. Nothing is created either way. A derived id is named as scaffold-bad-id names one, --id (not given, so --project lowercased). Its remedy is an explicit --id, which keeps the long name. A passed --id is told to pass a shorter one.
  • adopt-project.py execute refuses adopt-topic-too-long where it works out the topic, before _confirm and before either leg. That is after the git ls-remote reads of the repository the split is pushed to (its tracking branch, a split branch left behind, a branch in the split branch's way), so one of those refusals can be shown first. No gh call is made and nothing is created either way. It names the plan's id: (or id (not set in the plan, so legs.assembly lowercased)), and says to fix id: or re-run plan with --id.
  • Evidence, by the reproduction below. On bad05d21, a 40-character project made three gh repo create calls, then HTTP 422 at --add-topic, exit 2. An adoption with a 40-character --id pushed both legs and opened the pull request, then printed Set the topic by hand with a topic GitHub refuses. On 71936633, both refuse with no gh call and nothing made.

2. Names over 100 in the direct tools. The direction was the same check, once, in the helper that asks the disk's limit. refuse_unstorable_names now takes a third list, github: the names GitHub will be asked to create. A name over GITHUB_NAME_MAX = 100 is refused under the same code, scaffold-name-too-long or adopt-name-too-long.

  • The scaffold passes all three names without --local-remote-dir. Adopt passes the two legs, because the root already exists.
  • In adopt, that root is the source's own repository on GitHub, which GitHub lets be named with up to 100 characters. A source named with 96 to 100 characters gives legs of 101 to 105. Every adoption keeps the source's own name, so --project cannot shorten it. Where an over-long leg is named after a root of at most 100 characters, the remediation says to rename the source on GitHub first, to at most 95 characters, and plan again. It says so in _rename_first's words, as adopt-bad-id does, and says GitHub's 95 once. A leg the plan was edited to name otherwise keeps the remediation it had. So does any leg of a root over 100 characters, which no repository on GitHub can be named, so renaming the source shortens nothing: "Use a shorter --project". That comes only where the plan's legs.assembly is not held to a GitHub origin, that is, a source with no origin or a path origin, and a long --project was typed.
  • A name the disk cannot hold is not named twice.
  • The remediation names each limit the names broke. On GitHub it also names the id's 39, which the topic line bounds: whoever picks a shorter name learns both numbers at once.
  • The disk-only remediation is word for word as before.

3. --org over 39. refuse_an_owner_github_cannot_have asks it: scaffold-org-too-long in the scaffold's plan, and adopt-org-too-long in execute beside the names, before either leg. A detected organisation needs nothing extra. One read from a GitHub origin, or from gh, is never over 39. One read from another forge's URL is held to GitHub's limit, because GitHub is where the repositories go.

4. --help states the bounds. Each of --project, --id and --org says its number in setup-project.py, scaffold-project.py and adopt-project.py plan, and docs/cli.md is regenerated. The two direct tools build the sentence from the constants. setup-project.py cannot import them, because it runs before there is a checkout. test_setup_project_says_the_numbers_it_cannot_import holds its sentences to the numbers. Once #811 lands, the same test also holds #811's GITHUB_NAME_MAX and PROJECT_NAME_MAX to them. I did not hold the help line back for a later pull request: merge-tree finds no conflict in docs/cli.md against any open branch that edits it (below), and any later conflict there is solved by running the renderer again.

setup-project.py

Step (5) of setup-project.py is the scaffold's own --dry-run plan, given setup's --project, --id and --org. That plan now refuses all three before step (6) asks for the yes, and setup prints the plan could not be produced (see above) under the scaffold's refusal. The refusal names --id, --org and --project, which setup takes too. Tests cover each value through the flags and a name typed at the prompt (40 refused, 39 goes on).

I added no check of my own to setup's parsing. #811's tests pin that a 95-character name with a derived id goes on to step (4) (test_a_name_at_the_bound_goes_on_to_step_four). A topic check at parse time would refuse that name before step (4). #811's own 95 stays setup's early check of --project. The new refusals also avoid the words characters long, which #811's tests assert are absent from a 95-character GitHub run.

Lands after #811. On a probe merge of 71936633 with #811's head 050abea6, these files pass: #811's test_setup_project_name_edges.py, #729's test_setup_project_typed_name.py, test_setup_project_py.py, the new file and test_cli_reference.py (430 passed). The merge-tree with #811 and with #756 (b3c77cc2) is clean, and no hunk here touches their name rules.

Tests

  • New, tests/test_github_name_limits.py, 64 tests (58 after fix round 1, 50 before it). Each bound is tested at N and N+1 through the flags, a derived id, --id '', setup's prompt, both direct tools, and with and without --local-remote-dir. Refusals are asserted with no process started (in process, no_process) or with no gh call and nothing on disk (end to end). One case covers a name of 40 to 95 characters with a short --id, which still passes. Fix round 1 added:
  • Changed, tests/test_scaffold_manifest_values.py: the GitHub half of test_the_longest_name_a_disk_keeps_is_still_accepted asked a 250-character project. It now asks GitHub's longest, 95, with a short --id.
  • On base bad05d21, 33 of the first round's 50 new tests fail. The other 17 are controls that pass on both: the value at the bound, local runs, and a short --id. The changed test passes on both. On 9166f84f, 3 of the 58 fail: the three cases of a source named with 96 or 100 characters. On 71936633, 7 of the 64 fail: the roots of 101 and of 251, which were told to rename the source, and five that compare the rename-first remediation word for word, whose sentence now says 95 once (the 96- and 100-character end-to-end cases, the in-process test, and the two one-leg cases). With only that sentence changed on 71936633's code, 2 of the 64 fail: 101 and 251. The rest pass on both; each kills a mutant (below).
  • Runs, all on Linux, each with the commit it ran on:
    • On 495d4b33 (the NIT round, dirty=0), in one run of three shards: test_github_name_limits.py, test_scaffold_manifest_values.py, test_adopt_manifest_values.py, test_adopt_github_exits.py, test_adopt_project_is_the_source.py, test_cli_reference.py, test_shape_doctor.py, test_scaffold_leg_path_collisions.py, test_repo_hygiene.py and test_agents_md_codes.py: 980 passed, 3 skipped, 1 xfailed in 857.78s (the longest shard). Every test file that names adopt-name-too-long, the remediation or the predicate is among them. The 179-file run below was not repeated for this round: it changes one predicate and one sentence of adopt-project.py.
    • On 71936633 (fix round 1, dirty=0): the 179 test files that import or drive a changed file, in two runs: 5364 passed, 13 skipped, 1 xfailed, and 5573 passed, 24 skipped.
    • On 9166f84f with fix round 1's diff uncommitted, whose sha256 equals that of git diff 9166f84f 71936633: the new file, test_scaffold_manifest_values.py, test_adopt_manifest_values.py, test_adopt_github_exits.py, test_adopt_project_is_the_source.py, test_repo_hygiene.py, test_agents_md_codes.py and test_cli_reference.py: 434 passed.
    • On bad05d21 with the first round's change uncommitted (an earlier state of it, not 9166f84f):
      • the issue's Verify files (test_setup_project_py.py, test_scaffold_manifest_values.py, test_adopt_manifest_values.py, test_scaffold_leg_path_collisions.py, test_shape_doctor.py, test_cli_reference.py, test_repo_hygiene.py), with test_agents_md_codes.py and 47 of the new file's tests: 1148 passed, 3 skipped, 1 xfailed;
      • the 37 other files that drive gh or a GitHub-mode run: 2127 passed, 1 skipped.
    • On 3845fd54, a local commit that differs from 9166f84f only in tests/test_github_name_limits.py:
      • the other 98 test files that import or run a changed file: 3523 passed, 2 skipped, and 2908 passed, 28 skipped;
      • the setup cases again with git's system config emptied, as on a machine with no credential helper (CI's runner): 10 passed.

Reproduction

The reproduction runs on a fake gh and touches no network. The bad05d21 side ran in the first round; the other side ran again on 71936633 in fix round 1.

  • Scaffold. It ran in GitHub mode with --no-push, against a fake gh that refuses a topic over 50 characters as GitHub documents. On bad05d21, --project of 40 characters made repo view ×3, then repo create ×3 (spec, code, root), then repo edit ... --add-topic xf-project-<40>, which got HTTP 422. The run exited 2, with the work trees left. On 71936633 it exits 2 with scaffold-topic-too-long, no gh call, and no work directory.
  • Adopt. execute ran in GitHub mode with --id of 40 characters, using test_adopt_github_exits.py's shim told to refuse repo edit. On bad05d21 it made both legs, pushed the split and opened the pull request. Then it printed The split IS pushed and the pull request IS open. Set the topic by hand: with three commands GitHub would refuse too. On 71936633 it exits 2 with adopt-topic-too-long, no gh call, and no leg.

Mutants

  • First round: 20 mutants, one per new hunk, and each was caught. They covered the GitHub names ignored, the de-duplication, the GitHub remediation, the topic and the organisation off by one, GITHUB_PROJECT_MAX, each call site in the scaffold and in adopt, the local guards both ways, the derived, passed and empty id's naming, adopt's id: naming, each tool's --help, setup's usage, and a stale docs/cli.md.
  • The review at 9166f84f: 42 mutants; 7 survived.
  • Fix round 1, at 71936633: 51 mutants, one at a time: 50 killed and 1 known survivor.
    • rv859's 42 are among them, and six of its seven survivors are now killed: the two refusals moved below _confirm, and the four remediation wordings.
    • The survivor is sm-both-ends-26, _both_ends' boundary of 27, which is moved code and not new.
    • 9 are new, for the rename-first hunk, and each is caught.
  • The NIT round, at 495d4b33: 33 mutants, one at a time, each run with -x against the new file: 33 killed, none survived.
    • rv859b's seven survivors are killed. rv-any-to-all and rv-code-only fail test_one_leg_named_after_the_source_is_enough[spec-after-the-root]. rv-spec-only and rv-suffix-always-spec fail [code-after-the-root]. rv-startswith-root fails test_a_leg_the_plan_names_otherwise_keeps_its_remediation[starting-with-the-root]. rv-detail-dropped and rv-detail-is-remedy fail test_a_leg_named_after_the_source_is_mended_by_renaming_it.
    • Its other five mutants of the hunk, f859's eight (re-aimed at the new predicate) and six of rv859's earlier survivors are rerun, and killed.
    • 7 are new: the assembly bound dropped, strict, one over, 95, read off the spec leg and inverted, and the sentence that said 95 twice. Two are killed by the root of 101, four by the root of 100, and the sentence that said 95 twice by the remediation's words.
    • One survived a first pass, made on an earlier local form of this commit: f-ad-local-guard-dropped, the --local-remote-dir guard in the handler. Its only test used a root of 247, which the new bound refuses with the disk's remediation anyway, so the guard was no longer reached. test_a_leg_named_after_the_source_is_not_the_disks_trouble kills it, and all 33 were run again on 495d4b33.

Merge checks

At 495d4b33, merge-tree is clean against origin/main 4dea85ee and against 34 of the 36 other open fix/* and l3-* heads (ls-remote at about 03:00Z), among them fix/798-adopt-legs-asked-first (70801c91), which conflicted at fix round 1's sweep. The two that conflict are l3-677-pins-hand-step-wip (shape-doctor.py, validate-pins.py and two test files) and l3-678-naming-checkout-wip (shape-doctor.py). They conflict the same way at 71936633, and touch none of this pull request's files.

Fix round 1's sweep: at 71936633, merge-tree was clean against origin/main 190133d5 and against 55 of the 58 open fix/* and l3-* heads other than this one. Among the 55 are #811 (fix/777-typed-name-edges, 050abea6) and #848 (fix/835-config-count-appends, 79b2eac4). They also include every head that touches these files except fix/798: fix/710-non-utf8-branch-names, fix/711-git-bash-dialect, fix/715-yaml-reader-spaces, fix/716-adopt-check-plan-name, fix/729-typed-project-name (#756), fix/764-bootstrap-notice-exit-texts, fix/766-spelled-paths, fix/796-adopt-source-spellings, fix/797-adopt-default-branch-refs, fix/799-adopt-plan-out-checked, fix/800-pin-source-top-level, fix/801-answered-paths-per-leg, fix/803-adopt-clone-claimed, fix/807-eol-block-tracked-spelling, fix/836-child-environment, l3-785-non-ascii-paths and l3-rehearsal-v1.1.0. Three conflicted. Each conflicted the same way at 9166f84f and against main 190133d5 alone, so they are theirs and not this change's:

  • fix/798-adopt-legs-asked-first (scripts/shape_materialize.py);
  • l3-677-pins-hand-step-wip (shape-doctor.py, validate-pins.py and two test files, none of them touched here);
  • l3-678-naming-checkout-wip (shape-doctor.py).

The new file builds no GIT_CONFIG_COUNT by hand. The prompt test gives git a credential helper through a global config file of its own (GIT_CONFIG_GLOBAL). #848's test_no_test_sets_a_git_config_count_of_its_own passes on a probe merge of 71936633 with #848's head 79b2eac4: test_no_auto_maintenance.py and the new file, 118 passed.

Left out

  • scripts/family.py init has the same late --add-topic for the family's id, and the same unasked --org and holder name. The issue's goal names the three tools, and seven open branches edit family.py, so I left it out. It is checklist candidate 1.
  • adopt-project.py check asks neither GitHub's limits nor the disk's. execute refuses both before either leg. check takes --local-remote-dir and could find them: candidate 2.

Checklist candidates

  1. family.py init: an id over 39 characters fails gh repo edit --add-topic after the holder is created and pushed. The topic xf-project-<family id> is set last (scripts/family.py:2657), and nothing asks GitHub's 50. --org over 39 and a holder name over 100 are not asked either. Direction: the same three helpers, in init's plan, without --local-remote-dir.
  2. adopt-project.py check passes a plan that execute refuses for a name's length. adopt-name-too-long (the disk's 255, and now GitHub's 100), adopt-org-too-long and adopt-topic-too-long are asked by execute alone. check takes the same --local-remote-dir, so it could report each one as a finding before the person's yes.
  3. An --org holding a character GitHub refuses in an organisation's name passes every plan. GitHub keeps an account's name to ASCII letters, digits and single hyphens, with no hyphen at either end; that rule is not checked against docs.github.com here. checked_value and the manifest validator accept my_org.x, which tests/test_adopt_manifest_values.py plans as valid. That --org reaches gh repo create.
  4. A leg the plan was edited to name, over a limit, is told "Use a shorter --project". legs.spec and legs.code are the plan's own to edit, and adopt-name-too-long (the disk's 255, and GitHub's 100) names a leg named otherwise than after legs.assembly with the remediation of a name made from --project, which does not shorten it. Fix round 1 keeps that remediation word for word for such a leg; only a leg named after the source is told to rename the source. The mixed case goes with it: a source of 96 to 100 characters, one leg named after it and the other edited past 100, is told only to rename the source, which does not shorten the edited leg (rv859b's re-check). Direction: name the plan's key, legs.spec or legs.code, as the thing to shorten.

Closes #838

Summary by Sourcery

Reject GitHub-incompatible project inputs during planning so scaffold, adopt, and setup workflows fail safely before creating remote resources.

New Features:

  • Validate GitHub repository, topic, and organisation name limits before creating remote repositories in scaffold and adopt workflows.
  • Propagate the new validation through setup-project planning and document the applicable limits in command help and CLI documentation.

Bug Fixes:

  • Prevent late failures after repositories, seeds, pushes, or pull requests have been created when GitHub would reject a name, organisation, or project topic.
  • Preserve appropriate local-only and adoption-specific remediation guidance for overlong names.

Enhancements:

  • Centralize GitHub limits and refusal handling while avoiding duplicate diagnostics for names that exceed both disk and GitHub limits.

Documentation:

  • Update generated CLI reference and help text with GitHub's repository, topic, organisation, project, and id bounds.

Tests:

  • Add comprehensive coverage for boundary values, local versus GitHub workflows, setup prompts, scaffold dry runs, adoption execution, remediation messages, and prevention of repository creation.

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

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-5, 2026-10-17T20:27Z) @codex review

@sourcery-ai

sourcery-ai Bot commented Oct 17, 2026

Copy link
Copy Markdown

Reviewer's Guide

The PR introduces documented GitHub limits as shared preflight validations, applies them before any GitHub repository or adoption side effects while exempting local-only runs, updates help and CLI documentation, and adds extensive boundary and integration tests.

Sequence diagram for scaffold GitHub preflight checks

sequenceDiagram
    actor User
    participant Scaffold as scaffold-project.py
    participant Validate as shape_materialize
    participant GitHub as gh

    User->>Scaffold: Run scaffold
    Scaffold->>Validate: refuse_unstorable_names
    Validate-->>Scaffold: Repository names valid
    Scaffold->>Validate: refuse_an_owner_github_cannot_have
    Validate-->>Scaffold: Organisation valid
    Scaffold->>Validate: refuse_a_topic_github_cannot_set
    alt Limit exceeded
        Validate-->>Scaffold: Refusal
        Scaffold-->>User: No process or repository created
    else Limits valid
        Validate-->>Scaffold: Preflight passed
        Scaffold->>GitHub: Create repositories
        Scaffold->>GitHub: Set xf-project-id topic
    end
Loading

Sequence diagram for adoption GitHub preflight checks

sequenceDiagram
    actor User
    participant Adopt as adopt-project.py execute
    participant Validate as shape_materialize
    participant GitHub as gh

    User->>Adopt: Execute adoption plan
    Adopt->>Validate: refuse_unstorable_names for spec and code
    Adopt->>Validate: refuse_an_owner_github_cannot_have
    Adopt->>Validate: refuse_a_topic_github_cannot_set
    alt Any GitHub limit exceeded
        Validate-->>Adopt: Refusal
        Adopt-->>User: No leg, push, or pull request created
    else Limits valid
        Validate-->>Adopt: Preflight passed
        Adopt->>GitHub: Create or update adoption legs
        Adopt->>GitHub: Push split and open pull request
        Adopt->>GitHub: Set project topic
    end
Loading

Flow diagram for GitHub limit preflight validation

flowchart TD
    A[Project plan or adoption execute] --> B{--local-remote-dir?}
    B -->|Yes| C[Apply disk name limit only]
    B -->|No| D[Validate repository names <= 100]
    D --> E[Validate organisation name <= 39]
    E --> F[Validate xf-project-id topic <= 50]
    F --> G[Create or adopt repositories]
    C --> H[Create local bare repositories]
    D -->|Invalid| X[Refuse before side effects]
    E -->|Invalid| X
    F -->|Invalid| X
Loading

File-Level Changes

Change Details Files
Add shared GitHub length limits and preflight refusal helpers, while preserving disk-only validation behavior.
  • Define documented limits for repository names, topics, owners, project names, and derived IDs.
  • Extend repository-name validation to distinguish disk and GitHub targets and avoid duplicate diagnostics.
  • Add topic and organisation validation with limit-specific remediation messages.
scripts/shape_materialize.py
Validate GitHub constraints before scaffold or adoption side effects, only in GitHub-backed mode.
  • Make scaffolding reject oversized project names, IDs/topics, and organisations before process or repository creation, including dry runs.
  • Make adoption reject oversized leg names, IDs/topics, and organisations before confirmation or either leg is created.
  • Add dynamic help text describing the applicable bounds and local-mode exception.
scaffold-project.py
adopt-project.py
Document the new command-line bounds and setup behavior.
  • Update setup help and generated CLI documentation for project, ID, and organisation limits.
  • Route setup's plan step through the scaffold preflight checks without adding separate parsing logic.
setup-project.py
docs/cli.md
Add boundary, mode, integration, and regression coverage for GitHub limits.
  • Test values at and above each limit across scaffold, adopt, setup prompts, dry runs, derived and explicit IDs, and local remote mode.
  • Use fake GitHub implementations to verify no GitHub call or repository is made before refusal and that boundary values succeed.
  • Update the existing maximum-name acceptance test to use GitHub's project-name bound with a short ID.
tests/test_github_name_limits.py
tests/test_scaffold_manifest_values.py

Assessment against linked issues

Issue Objective Addressed Explanation
#838 Refuse GitHub repository names over 100 characters, organisation names over 39 characters, and topic IDs that produce a topic over 50 characters before creating repositories, in GitHub mode for setup, scaffold, and adopt. ✅
#838 Apply GitHub-specific name limits only when repositories are being created on GitHub, preserving disk-based limits for local-remote runs and allowing a long project name when an explicit shorter ID keeps its topic within the limit. ✅
#838 State the applicable project-name, ID, and organisation bounds in the three tools’ help and regenerate the CLI reference. ✅

Possibly linked issues


Tips and commands

Interacting with Sourcery

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

Customizing Your Experience

Access your dashboard to:

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

Getting Help

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-17T20:29:06.922915Z 9166f84 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Breezy!

Reviewed commit: 9166f84f8a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

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

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-5, 2026-10-17T23:29:16Z) ## Fix round 1 on 9166f84, after rv859's review: 71936633

One new commit on 9166f84f, in four files: adopt-project.py, scaffold-project.py, scripts/shape_materialize.py and tests/test_github_name_limits.py. Main is not merged: merge-tree with main 190133d5 is clean. docs/cli.md is unchanged, because no --help text changed. Each point of the review, and what was done:

SHOULD 1: a GitHub source named with 96 to 100 characters was told "Use a shorter --project". Done. Without --local-remote-dir, the assembly root is the source's own repository on GitHub. GitHub lets a repository be named with up to 100 characters, so the legs of a 96- to 100-character source are 101 to 105 characters, which adopt-name-too-long refuses. Every adoption keeps the source's own name, so --project cannot shorten it.

  • _repository_urls now catches that refusal. Where a leg over 100 is named after the root (<legs.assembly>-spec or -code, as plan names both), the remediation is _rename_first's own text. adopt-bad-id says its way out with the same helper. Before that text comes one sentence: "nothing was created. The legs are named <Project>-spec and <Project>-code, and --project is the source's own name, which must be at most 95 characters: " followed by GitHub's numbers, as before. So the person is told to rename the source on GitHub to 95 characters or fewer, and then run plan again.
  • The repository named is the one execute acts on, <org:>/<legs.assembly>. For a source whose origin names it, plan-not-the-source has already held that to the source, without case.
  • Kept word for word: the disk's remediation under --local-remote-dir, and the remediation of a leg the plan was edited to name otherwise. That second one is the plan's own name to shorten, not the source's. scripts/shape_materialize.py is not changed for this.
  • Evidence. rv859's probe (adopt_probe.py), run on this tree, now prints "testorg/Abbb...b cannot be adopted in place under that name: an in-place adoption keeps the source's own name, and --project cannot rename it. Whether to rename it is a human's decision, made first on GitHub (gh repo rename); then run plan again ..." for both the 96- and the 100-character root, with no gh call. Three new or changed tests fail on 9166f84f and pass here: test_execute_refuses_what_github_will_not_take_before_a_leg[project] (96) and [project-100] end to end, and test_a_leg_named_after_the_source_is_mended_by_renaming_it in process. On 9166f84f each shows Remediation: nothing was created. Use a shorter --project: ....
  • Two controls pass on both commits. test_a_leg_the_plan_names_otherwise_keeps_its_remediation holds the edited leg's remediation. test_the_disk_alone_keeps_its_remediation_under_a_local_remote_dir holds a 247-character project's bare -spec.git (256 bytes) to the disk's remediation, word for word.

SHOULD 2: nothing ran execute without --yes. Done. test_execute_refuses_what_github_will_not_take_before_a_leg has two more cases, [id-unconfirmed] and [org-unconfirmed]. They run execute without --yes, on the stdin github_run gives it (DEVNULL, not a terminal). The test drops --yes from github_run's execute command through a stand-in for that module's subprocess (_WithoutYes), so test_adopt_github_exits.py is not edited. Each expects exactly one REFUSED line, adopt-topic-too-long or adopt-org-too-long, with its remediation in full, no gh call and no leg. The control test_without_yes_a_plan_github_takes_is_unconfirmed runs a plan GitHub takes the same way and gets adopt-unconfirmed, so the stand-in really reaches _confirm. rv859's mutants now fail:

  • ad-topic-after-confirm fails [id-unconfirmed];
  • ad-org-after-confirm fails [org-unconfirmed], with ['REFUSED adopt-unconfirmed'] == ['REFUSED adopt-org-too-long'].

NIT 3: done. "96 to 246" is now "96 to 250" in scaffold-project.py (the _scaffold comment), scripts/shape_materialize.py (the #838 block comment, which now says 250 makes the longest -spec a directory can be named), tests/test_github_name_limits.py (its docstring) and the body. 9166f84f's own commit message still says 246; this commit's message corrects it.

NIT 4: done. The comment block in scripts/shape_materialize.py, the test module's docstring and the body now say: 100 for a repository's name and 50 for a topic 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 test test_the_limits_are_githubs_documented_numbers is renamed test_the_limits_are_githubs_numbers, and the comment over the test's constants no longer says "documented". The remediations still say "GitHub gives no user or organisation a name of more than 39 characters". That states the limit; it does not claim a source.

NIT 5: done. The _refuse_what_github_cannot_hold docstring now says the refusal comes before any repository or work directory exists, under --dry-run alike. No process has started yet where --elected-by or GIT_AUTHOR_NAME names the elector. Otherwise _identity has run git config user.name first, as it has before scaffold-bad-id. The test module's docstring and the body say the same.

NIT 6: done. Each remediation the scaffold and execute write themselves is now held word for word:

  • the scaffold's derived-id and passed-id remediations, now from their first word, SCAFFOLD_NOTHING_MADE ("Remediation: nothing was created, and --dry-run refuses the same.");
  • the scaffold's organisation remediation, in the new test_an_organisation_is_mended_as_github_spells_it;
  • execute's topic remediation (TOPIC_REMEDY), in the in-process test and the end-to-end [id] and [derived-id] cases;
  • execute's organisation remediation (ORG_REMEDY), in the [org] case.

rv859's four wording mutants now fail. sm-both-ends-26 is left alone, as asked: that code is moved, not new.

NIT 7: done in the body.

  • Each run now says which commit it was on.
  • The fake gh refuses only a topic over 50.
  • Adopt's topic refusal comes after the git ls-remote reads of the repository the split is pushed to.

Tests

tests/test_github_name_limits.py now has 58 tests (it had 50):

  • the organisation's scaffold remediation (1);
  • the three in-process _repository_urls cases (3);
  • [project-100] (1);
  • the two unconfirmed cases (2);
  • the unconfirmed control (1).

On 9166f84f, with only this file put in: 3 failed, 55 passed in 28.03s. The 3 are SHOULD 1's. The other new tests kill mutants, and they pass on code that was already right.

Runs on Linux (Python 3.12) through run9.sh:

Mutants

51 in all, one at a time, each applied to this tree in a scratch clone and run with -x against the new file (and, for sm-both-ends-26, the two manifest-values files too): 50 killed, 1 known survivor. rv859's 42 are rerun. 40 have the same patterns; ad-names-local-swap and ad-names-github-never were re-aimed at the call's new indentation inside the try. 9 are new, for the new hunk.

Mutant Was (rv859, 9166f84) Now Test that fails
sc-org-remedy-words survived killed test_an_organisation_is_mended_as_github_spells_it
sc-nothing-made-words survived killed test_a_derived_id_is_named_as_derived_and_mended_with_an_id
ad-topic-remedy-words survived killed test_executes_topic_refusal_names_the_id_by_where_it_came_from[id]
ad-org-remedy-words survived killed test_execute_refuses_what_github_will_not_take_before_a_leg[org]
ad-topic-after-confirm survived killed ...before_a_leg[id-unconfirmed]
ad-org-after-confirm survived killed ...before_a_leg[org-unconfirmed]
sm-both-ends-26 survived survived (known: moved code; 242 passed) none
the other 35 of rv859's killed killed as before
f-ad-local-guard-dropped: rename-first under --local-remote-dir too killed test_the_disk_alone_keeps_its_remediation_under_a_local_remote_dir
f-ad-rename-never: always re-raise killed test_a_leg_named_after_the_source_is_mended_by_renaming_it
f-ad-old-remedy: a bare raise in the handler killed the same
f-ad-named-after-any: any leg over 100, named after the root or not killed test_a_leg_the_plan_names_otherwise_keeps_its_remediation
f-ad-len-dropped: any leg named after the root, of any length killed the same
f-ad-repo-unowned: the repository named without its org killed test_a_leg_named_after_the_source_is_mended_by_renaming_it
f-ad-said-dropped: no sentence before _rename_first killed the same
f-ad-said-95: "at most 100 characters" killed the same
f-ad-limits-dropped: GitHub's numbers dropped killed the same

The comment and docstring hunks (NITs 3, 4 and 5) change no behaviour, so no mutant is aimed at them.

Merge checks

git merge-tree --write-tree of 71936633:

Lane: openreposhape-5 (openRepoShape-5)

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-5, 2026-10-18T02:21:25Z) Re-review of #859 (#838) fix round 1 at 71936633, for lane openRepoShape-5: READY

$Q = /tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad. My logs are under $Q/v118/rv859b/. I changed no branch, pushed nothing and posted nothing.

Findings, most severe first

There is no MUST and no SHOULD. All seven of rv859's points are closed. Three NITs remain; none blocks landing.

NIT 1: no test covers a plan where only one leg is named after a long root. At adopt-project.py:10784-10786 (_a_leg_named_after_a_long_root).

  • Scenario: the source is 96 characters long. legs.spec is <root>-spec and legs.code was edited to a short name, or the other way round. At the head this gets the rename-first remedy, which is right. My probe shows it ($Q/v118/rv859b/probes/root_lengths-head.log): github root=96 spec-after-root code-edited-short: adopt-name-too-long RENAME-FIRST, and the mirror case gives the same.

  • Mutants that survive, each with 58 passed ($Q/v118/rv859b/mutants.out):

    • rv-any-to-all (return any( changed to return all();
    • rv-spec-only and rv-code-only (the loop over roles cut to one role);
    • rv-suffix-always-spec (both legs compared with <root>-spec);
    • rv-startswith-root, a weaker mutant.

    Each of these sends the mixed plan back to "Use a shorter --project".

  • I applied rv-any-to-all and rv-detail-dropped together and ran five files (test_adopt_manifest_values.py, test_adopt_github_exits.py, test_adopt_project_is_the_source.py, test_scaffold_manifest_values.py and the new file): 287 passed (survivors-wide.log). No other file catches them.

  • Fix: one in-process case for each side, with names_of(a_name(NAME_MAX), code="Other-code") and its mirror, each expecting renamed_first(...).

NIT 2: the detail of the rename-first refusal is never asserted. At adopt-project.py:10746, raise Refusal(exc.code, exc.detail, _rename_first(.

  • rv-detail-dropped (the detail replaced with "") survives, and so does rv-detail-is-remedy, each with 58 passed.
  • The in-process test checks only the remediation. The end-to-end [project] and [project-100] cases check only the code (through refusals()) and the remediation.
  • refuse_unstorable_names' own detail is tested directly. What is untested is the detail argument of this new line.
  • Fix: assert caught.detail in test_a_leg_named_after_the_source_is_mended_by_renaming_it.

NIT 3: the predicate covers any root over 95 characters, not only 96 to 100.

  • Roots of 101 to 250 characters, which no GitHub repository can have, are also told to rename the source on GitHub. A root of 251 gets the disk's detail but a remediation that gives only GitHub's numbers. Probe lines root=101, 150, 250 and 251 in root_lengths-head.log all show RENAME-FIRST.
  • Where it can happen: only where the plan's legs.assembly is not checked against a GitHub origin, that is, a source with no origin or a path origin, plus a long --project someone typed. plan-not-the-source holds a URL origin's plan to the source's own name, which is at most 100. In this narrow case, the old "Use a shorter --project" was the better advice.
  • Related: a 96-character root with legs.code edited to 120 characters also gets rename-first, though renaming the source does not shorten the edited leg.
  • Direction: add len(names["assembly"]) <= GITHUB_NAME_MAX to the predicate. It is narrow enough to be a follow-up next to the body's checklist candidate 4.
  • Cosmetic: SOURCE_NAME_TOO_LONG says 95 twice ("must be at most 95 characters: … so a project name is at most 95").

Checked and true

  1. Scope. git diff --stat 9166f84f 71936633 touches 4 files: adopt-project.py (+43), scaffold-project.py (7), scripts/shape_materialize.py (33) and tests/test_github_name_limits.py (239). docs/cli.md and setup-project.py are untouched.

    • Every hunk serves a point in the account.
    • The account does not mention these, all within scope:
      • the _rename_first docstring edit;
      • the GITHUB_LIMITS import;
      • the GITHUB_LIMITS_SAID and DISK_REMEDY constants pulled out of existing asserts;
      • removing the now-unused NOTHING;
      • the reflowed comment paragraph in shape_materialize.py;
      • the existing end-to-end asserts, tightened (refused in became refusals(...) ==, and \n{made} became \n{made}\n).
    • f859's diff hash is true. git diff --abbrev=8 9166f84f 71936633 | sha256sum gives 0eed04a2d421f8e9…. My clone's default 9-character abbreviation gives 7248fa94…; the content is the same.
  2. Each of rv859's points is done.

    • SHOULD 1. The ask: "rename-first remedy" for a 96- to 100-character GitHub source, with the disk-only and edited-leg cases keeping their text.
      • The answer is adopt-project.py:10733-10747: except Refusal as exc: if local or not _a_leg_named_after_a_long_root(names): raise … raise Refusal(exc.code, exc.detail, _rename_first(repositories["assembly"], SOURCE_NAME_TOO_LONG)) from None. Also :10772-10786.
      • Tests: ADOPT_OVER["project"] (96) and ["project-100"] at tests/test_github_name_limits.py:679-684, compared word for word with renamed_first() (:658).
      • The kept cases are at :756 (an edited leg keeps "Use a shorter --project: ") and :766 (== DISK_REMEDY under --local-remote-dir, 247 characters).
    • SHOULD 2. The ask: refuse on the topic and org limits before _confirm.
      • The answer is [id-unconfirmed] and [org-unconfirmed] (:799-800): _WithoutYes drops --yes and the stdin is DEVNULL, and each case asserts refusals(result.stderr) == [refused.rstrip(": ")].
      • The control (:825-833) gets ["REFUSED adopt-unconfirmed"].
      • I re-ran both of rv859's mutants and both are now killed: ad-topic-after-confirm by [id-unconfirmed], ad-org-after-confirm by [org-unconfirmed].
    • NIT 3. "96 to 250" is at scaffold-project.py:1871, shape_materialize.py:4996 and test :18. A grep finds no "96 to 246" left in the touched files.
    • NIT 4. shape_materialize.py:4990-4993 reads "both documented on docs.github.com. For 39, the docs give the limit on a user's name … and nothing for an organisation's". The test docstring at :5-8 says the same. The test is renamed test_the_limits_are_githubs_numbers (:111). No "documented" claim about the limits remains in the five tools.
    • NIT 5. The docstring at scaffold-project.py:875-878 reads "No process has started yet where --elected-by or GIT_AUTHOR_NAME names the elector; otherwise _identity has asked git config user.name first". That matches the code: _elector (:758) reads the environment first, then git_out(["config","user.name"]), and --org is required (:284).
    • NIT 6. SCAFFOLD_NOTHING_MADE (:358), TOPIC_REMEDY and ORG_REMEDY (:646-655) and the new test_an_organisation_is_mended_as_github_spells_it (:391). I re-ran rv859's four wording mutants and all four are now killed.
    • NIT 7. Body line 7 says the fake gh "refuses only a topic over 50". Line 21 says "after the git ls-remote reads". Line 52 says "each with the commit it ran on". body-guard.py gives rc=0 on the body, on the fix-round text and on the test file. The added lines in the three code files hold no non-ASCII character.
  3. The new tests.

    • On 9166f84f with the new test file copied in: 3 failed, 55 passed in 32.56s (base.log). The three are test_a_leg_named_after_the_source_is_mended_by_renaming_it, [project] and [project-100]. f859's claim holds.

    • I ran 18 mutants, one at a time, with -x against the new file: 11 killed and 7 survived. Twelve were my own on the round's new code:

      • 5 killed: rv-code-renamed, rv-names-spec-repo, rv-local-and, rv-said-nothing-word, rv-spec-code-swapped;
      • 7 survived: rv-any-to-all, rv-spec-only, rv-code-only, rv-suffix-always-spec, rv-startswith-root, rv-detail-dropped, rv-detail-is-remedy (NITs 1 and 2).

      The other six were rv859's former survivors, and all six are now killed. The clone was left clean (STATUS clean).

  4. Runs at 71936633 (dirty=0, one -q):

    • the 5 named files plus the trio: 434 passed in 308.20s (headA1.log);
    • test_shape_doctor.py and test_scaffold_leg_path_collisions.py: 540 passed, 3 skipped, 1 xfailed in 650.34s (headA2.log).

    Merged with main ca47b71d: the merge is clean (merge-tree tree 96f1c60b; scratch merge commit 6cdab51e).

    • The same 8 files: 435 passed in 300.91s (mergeA1.log).
    • The doctor pair: 543 passed, 3 skipped in 677.01s (mergeA2.log).

    Main moved to c15a49e5 during the review. That also merges clean (tree 135ae8e9, scratch commit b4dcdb2a). There, the new file, test_no_auto_maintenance.py and the trio give 266 passed in 77.03s (mergeC15.log).

  5. docs/cli.md. test_cli_reference.py passes on both merged trees, inside the 435 and the 266.

  6. Sonar-style checks.

    • Duplication: no run of 6 or more identical lines inside the new test file or against any tests/*.py. The only 5-line runs are import blocks.
    • The AST finds no assert on a BoolOp anywhere in the test file.
    • Cognitive complexity, by hand:
      • _repository_urls: 3 → 7 (two ternaries, the except, a nested if worth 2, the or, if not local);
      • _a_leg_named_after_a_long_root: 1 (the and);
      • every new test helper or test: at most 1;
      • cmd_execute is not touched by the round and stays at 15 by hand (main 14). The estimator gives 16 on both commits because it double-counts, as rv859 noted.

Could not verify

  • The 179-file importer set at 71936633: I did not re-run it. I read f859's logs: impA.log shows 5364 passed, 13 skipped, 1 xfailed and impB.log shows 5573 passed, 24 skipped, both with head=71936633… dirty=0 and rc=0.
  • Merges with the other open branches: not re-checked, because the brief did not ask for it.
  • GitHub's real behaviour for 100, 50 and 39 (lane 1's), and macOS and Windows.

Scratch left; no process running

  • Clones under /workspace/projects/openRepoShape-worktrees/_lane5-scratch/:
    • rv859b-head: 71936633, clean;
    • rv859b-base: 9166f84f, with the new test file staged;
    • rv859b-mut: 71936633, clean;
    • rv859b-merge: at b4dcdb2a (the head merged with c15a49e5); 6cdab51e (the head merged with ca47b71d) is in its reflog.
  • Basetemps: $Q/v110/bt/rv859b-*, 25 directories.
  • Logs and scripts in $Q/v118/rv859b/:
    • logs: base.log, headA1.log, headA2.log, mergeA1.log, mergeA2.log, mergeC15.log, survivors-wide.log, mutants.out and mut/*.log;
    • scripts: mutants.py, laneX.sh, laneY.sh and probes/root_lengths.py (its output is probes/root_lengths-head.log);
    • copies made for the complexity count: adopt-9166.py and adopt-main.py.
  • Processes: none of mine is running. The only run9 processes alive belong to w865f.

Next, from lane 5: a NIT round for NITs 1 to 3, then rv859b re-checks it. #859 lands at a later lane-5 turn, after an update-branch.

Lane: openreposhape-5 (openRepoShape-5)

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

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-5, 2026-10-18T03:02:55Z) NIT round of #859 (#838): 71936633..495d4b33.

One new commit on 71936633, in two files: adopt-project.py and tests/test_github_name_limits.py. Main is not merged: merge-tree with main 4dea85ee is clean. docs/cli.md is unchanged, because no --help text changed. rv859b's re-review had no MUST and no SHOULD; these are its three NITs, and what was done.

NIT 1: "no test covers a plan where only one leg is named after a long root". Done. At 71936633 a plan with legs.spec named <root>-spec and legs.code edited to a short name already got the rename-first remedy, which is right, but nothing held it. test_one_leg_named_after_the_source_is_enough now runs names_of(a_name(NAME_MAX), code="Other-code") and its mirror, spec="Other-spec", each expecting renamed_first(...) ([spec-after-the-root] and [code-after-the-root]). It uses the file's own helpers and repeats no block.

  • rv859b's four survivors are killed, each with one failing test: rv-any-to-all and rv-code-only fail [spec-after-the-root]; rv-spec-only and rv-suffix-always-spec fail [code-after-the-root].
  • rv-startswith-root, the weaker one, would have survived those two cases: a leg named after the root also starts with it. It is killed by a third case, added to the existing test_a_leg_the_plan_names_otherwise_keeps_its_remediation as [starting-with-the-root]: a leg of 110 characters that starts with Northwind- without being Northwind-spec keeps "Use a shorter --project".

NIT 2: "the detail of the rename-first refusal is never asserted". Done. test_a_leg_named_after_the_source_is_mended_by_renaming_it now asserts caught.detail, word for word: both legs, 105 characters each, named by their two ends. rv-detail-dropped and rv-detail-is-remedy both fail it.

NIT 3: "the predicate covers any root over 95 characters, not only 96 to 100". Done, with the cosmetic point.

  • _a_leg_named_after_a_long_root now reads len(names["assembly"]) <= GITHUB_NAME_MAX and any(...). A root of 101 or more is no name GitHub gives a repository, so there is nothing on GitHub to rename, and the old "Use a shorter --project" was the better advice. Its docstring says so, as does the comment at the handler in _repository_urls; both still describe the predicate as it is.
  • rv859b's probe (root_lengths.py), run on the new head, now says SHORTER-PROJECT for roots of 101, 150, 250 and 251, where 71936633 said RENAME-FIRST for all four. Roots of 96 and 100, the one-leg cases and a root of 96 with legs.code edited to 120 are as before. The probe's disk case, a root of 247 under --local-remote-dir, is as before too.
  • Two tests pin it, in test_a_root_github_cannot_have_is_not_the_sources_to_rename. A root of 101 gets the remediation that starts "Use a shorter --project" and ends with GitHub's numbers. A root of 251, whose legs are 256 bytes, gets the disk's remediation word for word, with no word of GitHub's. Both fail on 71936633.
  • SOURCE_NAME_TOO_LONG said 95 twice. It now reads "... and --project is the source's own name: " and then GITHUB_LIMITS, which says "a project name is at most 95" once. The test helper renamed_first has the same sentence.
  • Left alone: rv859b's related point, a root of 96 with legs.code edited to 120. One leg is still named after the source, so the remedy is still rename-first, which does not shorten the edited leg. It was not in this round's ask. It belongs to checklist candidate 4, where the plan's own legs are the thing to shorten.

One more, found by the mutants. With the root bounded, the --local-remote-dir guard in the handler (if local or not ...) is reached only where the disk refuses an edited leg beside a leg named after a source of 96 to 100 characters. The guard's one test used a root of 247, which the new bound refuses with the disk's remediation anyway, so the mutant that dropped the guard (f-ad-local-guard-dropped) survived a first pass of this round. test_a_leg_named_after_the_source_is_not_the_disks_trouble holds it: a spec leg of 105 characters named after a 100-character source, and legs.code edited to 300, under --local-remote-dir, get the disk's remediation. The mutant fails it.

Tests

tests/test_github_name_limits.py now has 64 tests (it had 58). Six are new: the two one-leg cases, the [starting-with-the-root] case, the roots of 101 and 251, and the disk test above. The detail assertion is added to an existing test. Two helper changes hold no test of their own: 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.

On 71936633, with only the new file put in: 7 failed, 57 passed in 40.29s. The 7 are the roots of 101 and 251, which fail by behaviour, and five that compare the rename-first remediation word for word, whose sentence now says 95 once: [project] and [project-100] end to end, test_a_leg_named_after_the_source_is_mended_by_renaming_it, and the two one-leg cases. With only that sentence changed on 71936633's code, 2 failed and 62 passed in 39.17s: the roots of 101 and 251. The other new tests pass on both commits and kill mutants.

Runs on Linux (Python 3.12) through run9s.sh, 3 shards, on 495d4b33 (dirty=0): test_github_name_limits.py, test_scaffold_manifest_values.py, test_adopt_manifest_values.py, test_adopt_github_exits.py, test_adopt_project_is_the_source.py, test_shape_doctor.py, test_scaffold_leg_path_collisions.py and the trio, test_repo_hygiene.py, test_agents_md_codes.py and test_cli_reference.py: 980 passed, 3 skipped, 1 xfailed in 857.78s. By shard: test_shape_doctor.py alone 436 passed, 3 skipped, 1 xfailed; the new file with test_scaffold_manifest_values.py, test_adopt_manifest_values.py, test_adopt_github_exits.py, test_adopt_project_is_the_source.py and test_cli_reference.py 316 passed; test_scaffold_leg_path_collisions.py, test_repo_hygiene.py and test_agents_md_codes.py 228 passed. Every test file that names adopt-name-too-long, the remediation or the predicate is among them. The 179-file importer run of fix round 1 was not repeated: this commit changes one predicate and one sentence.

Mutants

33, one at a time, each applied in a scratch clone at 495d4b33 and run with -x against the new file: 33 killed, none survived. They are rv859b's seven survivors, its other five of the hunk, f859's eight (re-aimed at the new predicate; its f-ad-said-95 mutated a sentence that is gone), six of rv859's earlier survivors, and seven new. The seven new are the assembly bound dropped, strict, one over, 95, read off the spec leg and inverted, and the sentence that said 95 twice. Two are killed by the root of 101, four by the root of 100, and the last by the remediation's words. The first pass, on an earlier local form of this commit, had one survivor, f-ad-local-guard-dropped, as above; all 33 were run again on 495d4b33.

Merge checks and Sonar-style checks

  • git merge-tree --write-tree of 495d4b33 is clean against origin/main 4dea85ee and against 34 of the 36 other open fix/* and l3-* heads (ls-remote at about 03:00Z). The two that conflict, l3-677-pins-hand-step-wip and l3-678-naming-checkout-wip, conflict the same way at 71936633 and touch none of this pull request's files.
  • No run of 4, 5, 6 or 10 identical lines in the changed lines, inside the changed files, or against the other Python files under tests/, scripts/ and the repository root. No assert joins conditions with and (the AST finds no assert on a BoolOp). _a_leg_named_after_a_long_root has two sequences of and, so its cognitive complexity is 2 by hand, 3 if the generator's for counts, against a limit of 15. _repository_urls is unchanged but for a comment.
  • body-guard.py is clean on both of this round's files, on the commit message and on the changed code.

Next, from lane 5: rv859b re-checks this round.

Lane: openreposhape-5 (openRepoShape-5)

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-5, 2026-10-18T03:15:28Z) Re-check of #859 (#838) NIT round at 495d4b33, for lane openRepoShape-5: READY

$Q = /tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad. This is a read-only re-check. I changed no branch, pushed nothing and posted nothing.

Findings, most severe first

There is no MUST and no SHOULD. All three of my NITs are closed. One small body point is left:

NIT (body only, optional): checklist candidate 4 does not mention the mixed case that was set aside.

  • The case: a source of 96 to 100 characters, with one leg named after it and the other edited past 100. It still gets only the rename-first remedy, which does not shorten the edited leg.
  • Where it is said: fixround.md says "It belongs to checklist candidate 4". But candidate 4 in the body ($Q/v118/f859n/body-live3.md:105) speaks only of a leg "named otherwise than after legs.assembly".
  • Suggestion: one sentence there, so whoever files candidate 4 gets this case too. Nothing in the code needs to change.

Checked and true

  1. Scope. git diff 71936633 495d4b33 is one commit (parent 71936633) in 2 files: adopt-project.py (+21/−10) and tests/test_github_name_limits.py (+76/−16).

    • Every hunk is in fixround.md's account: the predicate and its docstring, the handler comment, SOURCE_NAME_TOO_LONG, the test docstring, SHORTER_PROJECT, DISK_REMEDY rebuilt from it, the renamed_first sentence, the detail assert and the four new or parametrized tests.
    • DISK_REMEDY evaluates to the same bytes at both commits (checked with Python: True).
    • GITHUB_PROJECT_MAX is still used in adopt-project.py (:13349), so no import is left unused.
  2. Each NIT is closed.

    • NIT 1. test_one_leg_named_after_the_source_is_enough has two cases, [spec-after-the-root] and [code-after-the-root], each expecting renamed_first(...). A third case, [starting-with-the-root], was added to the edited-leg test.
    • NIT 2. test_a_leg_named_after_the_source_is_mended_by_renaming_it now asserts caught.detail == "Abbbbbbbbbbb...bbbbbbb-spec is 105 characters, … -code is 105 characters, … at most 100."
    • NIT 3. The predicate now reads return len(names["assembly"]) <= GITHUB_NAME_MAX and any(...). test_a_root_github_cannot_have_is_not_the_sources_to_rename checks [101] (SHORTER_PROJECT + GITHUB_LIMITS_SAID) and [251] (== DISK_REMEDY).
      • I re-ran my probe on the head ($Q/v118/rv859b/probes/root_lengths-495d4b33.log). Roots of 101, 150, 250 and 251 now give SHORTER-PROJECT, where 71936633 gave RENAME-FIRST. Every other line is unchanged: 95 is accepted; 96, 100, the one-leg cases and the 96-plus-edited-120 case give RENAME-FIRST; the edited leg on Northwind and the disk case at 247 give SHORTER-PROJECT.
      • The full remediation for a 96-character root now contains "95" once ("--project is the source's own name: GitHub keeps … so a project name is at most 95 …"). The one for 251 is exactly the disk's text, with no GitHub numbers in it.
  3. Mutants. I wrote a new driver ($Q/v118/rv859b/mutants2.py) with my patterns re-aimed at the new predicate. It runs each mutant one at a time against the new file with -x. All 11 were killed (mutants2.out), and the clone was left clean:

    Mutant Test that fails
    rv-any-to-all [spec-after-the-root]
    rv-spec-only [code-after-the-root]
    rv-code-only [spec-after-the-root]
    rv-suffix-always-spec [code-after-the-root]
    rv-startswith-root [starting-with-the-root]
    rv-detail-dropped …mended_by_renaming_it
    rv-detail-is-remedy …mended_by_renaming_it
    f-ad-local-guard-dropped (f859's pattern) test_a_leg_named_after_the_source_is_not_the_disks_trouble
    my rv2-bound-dropped [101]
    my rv2-bound-strict (< for <=) …mended_by_renaming_it
    my rv2-bound-or [named-otherwise]
  4. Runs at 495d4b33 (dirty=0), through run9s.sh with SHARDS=3, on the new file, the four related files and the trio: 440 passed in 252.78s, END rc=0 ($Q/v118/rv859b/nit-run9s.log). That is 434 plus the 6 new tests.

    • I skipped the doctor pair. A grep finds no use of adopt-name-too-long, SOURCE_NAME_TOO_LONG, _a_leg_named_after_a_long_root, _rename_first or GITHUB_LIMITS in test_shape_doctor.py or test_scaffold_leg_path_collisions.py. The only test files that name the changed code are the new file and test_adopt_manifest_values.py, and both were in the run.
  5. Merge. Main is 4dea85ee (read from ls-remote at about 03:10Z). merge-tree --write-tree 495d4b33 4dea85ee is clean (tree 2eae56fe).

    • Main has moved since the last round. 4dea85ee changes setup-project.py's typed name, and 3fb30a8f changes adopt-project.py and docs/cli.md. So I also made a scratch merge (296ba947, the same tree) and ran the new file, test_cli_reference.py and test_setup_project_typed_name.py on it: 107 passed in 80.24s (merge4dea.log).
  6. Sonar-style checks.

    • Duplication: no run of 6 or more identical lines; the only 5-line runs are import blocks.
    • No assert on a BoolOp.
    • _a_leg_named_after_a_long_root has cognitive complexity 2 by hand (two and sequences), 3 if the generator counts. _repository_urls is unchanged at 7, apart from a comment.
    • body-guard.py gives rc=0 on the test file, and the added lines of adopt-project.py hold no non-ASCII character.
  7. f859n's base claim, read from its logs and not re-run: base.log (START head=71936633 dirty=1) shows 7 failed, 57 passed in 40.29s, and base-wording-only.log shows 2 failed, 62 passed.

Could not verify

  • The doctor pair and the 179-file importer set at 495d4b33, both skipped as you asked. This round changes one predicate, one sentence and tests.
  • A merge-tree against the other open fix/* and l3-* heads, which you did not ask for.

Scratch left; no process running

  • Clones under /workspace/projects/openRepoShape-worktrees/_lane5-scratch/:
    • rv859b-head: 495d4b33, clean;
    • rv859b-mut: 495d4b33, clean;
    • rv859b-base: 9166f84f, with the round-1 test file staged;
    • rv859b-merge: 296ba947 (495d4b33 merged with 4dea85ee).
  • Shard logs: /workspace/projects/openRepoShape-worktrees/_lane5-scratch/rv859b-head-s1.log, -s2.log and -s3.log. run9s.sh removed its worktrees -s2 and -s3 on rc 0; git worktree list shows no rv859b entry.
  • Basetemps: $Q/v110/bt/rv859b-*, 40 directories.
  • New in $Q/v118/rv859b/: mutants2.py, mutants2.out, mut2/*.log, nit-run9s.log, merge4dea.log and probes/root_lengths-495d4b33.log.
  • Processes: none of mine is running.

From lane 5: #859 is READY at 495d4b33, and lands at a later lane-5 turn, after an update-branch. The body takes the optional sentence on checklist candidate 4.

Lane: openreposhape-5 (openRepoShape-5)

@sonarqubecloud

Copy link
Copy Markdown

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

@brettheap
brettheap merged commit 281df26 into main Oct 18, 2026
5 checks passed
@brettheap
brettheap deleted the fix/838-github-name-limits branch October 18, 2026 08:56
brettheap pushed a commit that referenced this pull request Oct 18, 2026
…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>
brettheap pushed a commit that referenced this pull request Oct 18, 2026
… 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 added a commit that referenced this pull request Oct 18, 2026
…light's offers and `--shape-ref` is held to git's rule part by part, so a rehearsal no longer writes git's global config and `a.lock/b` is refused before the clone (#857) (#873)

Stacked on #811 (`fix/777-typed-name-edges`); retarget to main once it lands.

#756 has landed as `4dea85ee`, and #811's head is now `b6f56cad`: #756's squash merged in, and a test-only change to `tests/test_setup_project_name_edges.py`. This branch stays on `050abea6`, the merge base of the two, so its diff against #811's branch is its two commits: the fix (`79a33f11`) and fix round 1 (`ac570069`), which changes two test files and nothing else. Lane 5 restacks it after #811 lands.

**Direction: the silent reading.** A run passed `-- --dry-run` reports what is missing and asks nothing that would change the machine. Where a terminal would have been offered a command, it is told `not offered: --dry-run was passed through. A run without it offers this here, and so does --preflight.`, and asked nothing. The plan's last line now ends: ``The preflight made none of its offers; a run without --dry-run makes each one marked `not offered` above.`` `--preflight` still makes every offer, whatever follows `--`: it is the run that exists for them (#59), and it returns before anything reads the passthrough.

**Why.** Measured at `050abea6`, what each offer does on a typed `yes`, and all three change the machine:

- an install (`offer_missing`): git or gh, through the package manager;
- `gh auth login` (`_offer_login`): gh's own sign-in;
- `gh auth setup-git` (`_check_credential_helper`): gh writes a credential helper into the person's global git config.

A pty run of `setup-project.py Sample --org demoorg --elected-by Tester --into <dir> -- --dry-run`, under a signed-in stub `gh` and a git with no helper, asked ``Run `gh auth setup-git` now? Type yes to continue:``. The typed `yes` ran it, and the stub, which writes what the real one writes, added `[credential "https://github.com"]` to the file `GIT_CONFIG_GLOBAL` names. The run then ended at the plan saying `nothing was created and nothing was cloned`. With gh signed out, the same run asked ``Run `gh auth login` now?``, and the `yes` signed the stub in. The help said a `--dry-run` there "ends this run at the plan, creating nothing". And #811's CI run 39027235147 showed five rehearsals that typed only the project name waiting at that question until they were killed. Nothing argued for the offering reading: the offers stay one flag away (`--preflight`), and the real run makes them.

**What changed.** `preflight` passes `offering = not passes_a_dry_run(opts.passthrough)` to `preflight_checks`, and it goes down through `_check_git`, `_check_gh`, `offer_missing`, `offer_install`, `_offer_login` and `_check_credential_helper` to `offer`. `offer` tests it after `can_ask()`, so a run with no terminal prints what it printed before, and it is a bool, never `Options`, so `test_the_offer_helpers_cannot_see_the_yes_flag` still holds. The help says it, and `docs/cli.md` is regenerated. `AGENTS.md`, `README.md` and the handbook were grepped for `dry-run`, `creating nothing` and the offers: no sentence there is made false, so none is touched.

The git install offer cannot be reached by a dry run end to end: `_main` refuses a machine with no git (`GIT_MISSING`) before the preflight runs. It is held in process, through `preflight`, as the other two are.

**Tests** (`tests/test_setup_project_dry_run_offers.py`):

- `test_a_dry_run_makes_none_of_the_offers_preflight_makes[install|login|helper]`, once for `--dry-run` and once for its short spelling `--dry` (`passes_a_dry_run` reads both), in process on every platform. A fake terminal types `yes` to anything and a recorder runs nothing. `preflight` on `Sample --org demoorg -- <flag>` makes no prompt and runs no offered command, says `not offered` once per offer (2, 1, 1), and refuses or passes as before. `preflight_only` on `--preflight -- <flag>` then makes the same 2, 1, 1 offers.
- `test_a_dry_run_with_no_terminal_says_what_a_run_without_it_says[install|login|helper]`, in process: with no terminal (stdin is not one and there is none to open), `preflight` on `-- --dry-run` prints no `not offered` line, and what it prints is what the same run prints without `--dry-run`. It holds the order in `offer`, `can_ask()` before `offering`.
- `test_a_dry_run_leaves_git_with_no_helper_as_it_found_it[py|piped|py with --dry]`, a pty run (`PTY_SKIP`). A signed-in stub `gh`, an empty system config and a global config of the run's own, with no `GIT_CONFIG_COUNT` and run from a folder that is no repository. The run asks nothing, exits 0 at the plan, prints the `[??]` warning with the `not offered` line under it and the new last line, never calls `gh auth setup-git`, and leaves the global config the bytes it was.
- `test_a_dry_run_does_not_sign_gh_in`: the same with gh signed out (git has a helper, so the login is the one offer at stake). It asks nothing, the `[!!]` line has the `not offered` line under it, it refuses `one or more prerequisites are missing`, and gh is still signed out.

**Direction: finish the rule,** in a new `why_not_a_ref` beside `why_refused`, which `checked_ref` asks after `REF_RE`. Git's rule is per part: no part is empty (`//`, a trailing `/`), none starts with `.`, none ends in `.lock`, and the whole holds no `..` and does not end in `.`. Each failure has its own sentence (`WHY_EMPTY_PART`, `WHY_DOT_PART`, `WHY_LOCK_PART`, `WHY_DOT_END`). `WHY_RANGE` and `WHY_LOCK` keep #811's words, so its `ref-range` and `ref-lock` cases are unchanged. `REF_RE`'s comment and `checked_ref`'s docstring say what is asked.

**Evidence.** At `050abea6`, `checked_ref` beside `git check-ref-format --branch` disagreed on seven of the issue's ten names (`a.lock/b`, `x/y.lock/z`, `release.lock/v1`, `a/.b`, `a//b`, `a/b/`, `a.lock/`), and on `a.` and `a/b.`, under git 2.43.0 and 2.47.1 alike. Over all 3110 names of up to five parts from {`a`, `0`, `.`, `/`, `-`, `.lock`} that `REF_RE` takes, they disagreed on 1122 before and on none now, under both versions. In self-bootstrap mode `--shape-ref a.lock/b` cloned the standard and ended in `error: pathspec 'a.lock/b' did not match any file(s) known to git`, exit 1. Now it is refused before `(0) self-bootstrap` prints.

**Tests** (`tests/test_setup_project_ref_parts.py`):

- `test_the_tool_and_git_give_one_answer[<name>]`: the issue's ten names and fourteen around them (`a.`, `a/b.`, `a./b`, `a.lockb`, `a/b.lock/`, `a/.lock`, `a..b`, `refs/tags/v1.0`, `v1.0.0`, and five from fix round 1). Each is fed to `checked_ref` and to `git check-ref-format --branch`, which must give one answer, and the tool's reason is pinned. No network. Fix round 1's five are `a/.b/c` and `a/b/.c/d`, a part that starts with `.` that is not the last part; and three names that break two rules, which hold the order `why_not_a_ref` asks them in (its docstring says the sentence names the first one the value failed): `a//..b` is a range, not an empty part; `a//.b` an empty part, not a dot part; `a.lock/b.` a lock part, not a `.` at the end. With `a/.lock` and `a/b.lock`, that holds each of the five pairs of neighbours in the order.
- `test_a_ref_git_refuses_is_refused_before_the_clone[a.lock/b|a/.b|a//b]`: a self-bootstrap run exits 2 with the reason, prints nothing on stdout and clones nothing.

Through `run9s.sh`, `SHARDS=3`, at `79a33f11`: the two setup files, every other test file that names `setup-project` (`git grep -l 'setup-project' -- tests`), the two new files and the trio, 25 files.

- git 2.43.0: 1606 passed, 3 skipped, 1 xfailed in 746.00s, rc 0 (shards 428 passed, 3 skipped, 1 xfailed; 546 passed; 632 passed)
- git 2.47.1: 1606 passed, 3 skipped, 1 xfailed in 677.22s, rc 0 (shards 428 passed, 3 skipped, 1 xfailed; 546 passed; 632 passed)

Before the commit, unsharded on git 2.43.0: `test_setup_project_py.py`, `test_setup_project_name_edges.py`, `test_setup_project_typed_name.py` and the two new files, 377 passed. `test_setup_sh.py` and the trio, 235 passed.

Fix round 1, at `ac570069` (`setup-project.py` is unchanged, so every other file's result stands):

- git 2.43.0, `SHARDS=3`: `test_setup_project_py.py`, `test_setup_project_name_edges.py`, `test_setup_project_typed_name.py`, the two new files and the trio, 535 passed in 174.38s, rc 0.
- the two new files, 40 tests: 40 passed on git 2.43.0 (3.74s) and 40 passed on git 2.47.1 (2.64s).

The two new files, run against `050abea6`'s code: at `ac570069`, 40 tests, 28 failed and 12 passed (at `79a33f11`, 28 tests: 20 failed, 8 passed). All ten dry-run cases that make an offer fail: the three machines in both spellings, the three terminal runs and the signed-out run. Line 2's fifteen table rows that `050abea6` gets wrong fail: ten names git refuses that it passed, `a/.lock`, which it refused for the `.lock` and not for the part that starts with `.`, and fix round 1's `a/.b/c`, `a/b/.c/d`, `a//.b` and `a.lock/b.`. So do the three self-bootstrap cases. The 12 that pass are the three no-terminal cases (`050abea6` made no offer there either), and nine table rows: eight where `050abea6` already agreed with git (`a.lock`, `a/b.lock`, `topic.locked`, `a./b`, `a.lockb`, `a..b`, `refs/tags/v1.0`, `v1.0.0`) and `a//..b`, which it refused for its `..`, with the sentence the order gives. They hold the table to both directions, `topic.locked` passing among them.

23 mutants, each run against its tests, and all 23 caught:

- the gate in `offer` removed;
- `offering` not passed on in each of `offer_install`, `offer_missing`, `_check_git`, `_check_gh` (install, login), `_offer_login`, `_check_credential_helper`, and each of `preflight_checks`'s three calls;
- `preflight` always offering;
- `preflight_checks` silencing `--preflight -- --dry-run` too;
- the plan's new sentence removed;
- the `not offered` words changed;
- the help reverted (`tests/test_cli_reference.py`);
- `checked_ref`'s old two checks put back (`text.endswith(".lock")`);
- each of the four new parts of the rule dropped;
- `why_not_a_ref`'s answer ignored;
- the `.lock` part sentence changed.

The comment and docstring hunks have no mutant: they are prose. None survived.

An independent review then made 14 mutants of its own at `79a33f11`. Nine were caught. Five survived the tests as first pushed (the two new files and the setup tests): R1, the dry-run check put before `can_ask()` in `offer`; R3, the `..` and empty-part checks swapped; R4, `parts[-1].startswith(".")` only; R7, a literal `"--dry-run" not in opts.passthrough` in `preflight`; and R13, `parts[:2]` only. Fix round 1 closes each, rebuilt from the review's descriptions and run at `ac570069` against the two new files on git 2.43.0:

- R1 fails the no-terminal test on all three machines (3);
- R3 fails `a//..b` (1);
- R4 fails `a/.b/c` and `a/b/.c/d` (2);
- R7 fails the three `--dry` cases in process and the `--dry` terminal run (4);
- R13 fails `a/b/.c/d` (1).

Run against the two files as first pushed (`79a33f11`), the same five all survived, which is what the review found. Two more mutants for the new order rows are caught: `a//.b` fails with an empty part checked after a dot part, and `a.lock/b.` fails with a `.` at the end checked before a lock part. The other nine of the review's mutants were caught before and were not rerun.

`git merge-tree --write-tree` of `79a33f11`, and of `ac570069` for the first line:

- **#811's head `b6f56cad`: clean**, for both (`git merge-tree --write-tree b6f56ca ac57006`, tree `3e369754`). Fix round 1 changes only the two test files this PR added, so the other lines below stand for it: they were checked at `79a33f11` and not run again.
- **origin/main `d7befb91`: conflicts in `setup-project.py` and `tests/test_setup_project_typed_name.py`.** The conflict is inherited: `050abea6` alone conflicts with `d7befb91` in the same two files (#756 landed as a squash, and #811's branch held its history). A commit-tree prediction of `79a33f11` merged onto `b6f56cad` is clean against `d7befb91`.
- #859 `495d4b33` (`fix/838-github-name-limits`): clean. `setup-project.py` and `docs/cli.md` merge by themselves, with this head and with the predicted restack.
- #821 `e15c237b` (`fix/711-git-bash-dialect`): clean, `setup-project.py` merging by itself. The predicted restack conflicts with it only in `README.md`, `docs/handbook.html` and `shape-doctor.py`, where `d7befb91` itself conflicts with `e15c237b`. None of the three is this branch's.
- `fix/764` `05a1ebb3`, `fix/796` `2d51f181`, `fix/798` `70801c91` and `fix/803` `08f7a283` (`docs/cli.md`): clean.
- No other `fix/*` or `l3-*` branch touches these four files.
- #848's scan on main (`counts_set_to_a_number`) finds no count set in either new test file.

- `setup.sh`'s shim keeps its own copy of the old rule (`*..*|*.lock`), and its comment says it mirrors `setup-project.py`. Under `cat setup.sh | bash -s -- --shape-ref a.lock/b` the shim clones the standard and ends in git's `pathspec 'a.lock/b' did not match`, exit 1, before this file's check runs. Measured at this head, it is outside #857's lines and is reported to lane 5 as a checklist candidate.
- The plan's last line prints on every dry run, including where the preflight marked no offer `not offered`, where "each one marked `not offered` above" points at none. It is true as written. Printing it only where an offer was marked would take a count that `offer` writes and the plan reads, held in the module: `offer` is three to five calls below `preflight`, and each caller between returns a bool that the next one reads. That is state in a file whose `offer` takes its inputs as arguments and nothing else, for a sentence most dry runs would then not print, so it is left.
- No Windows or macOS run: CI is Linux only (#597). The in-process test runs on every platform; the pty tests are POSIX only.

Review: rv873, lane 5's independent review, READY at `79a33f11` with S1 (issuecomment-6245576863); f873's fix round 1 `ac570069` re-checked READY with no findings (issuecomment-6245715704). S2 is filed as #874. `0774ad60` restacks it onto #811's squash `bc362333` with three merges and the same `-U0` lines. CI (Brett Heap's ruling 27 and his fast-refresh ruling of 2026-10-15T22:48:35Z): H0 0774ad6 (the restack of the branch onto main after #811 landed) passed tests, tests-git-bash and SonarCloud; the head e41a38a is a clean update-branch merge of main 85aa8a1, with tests-git-bash and SonarCloud green; and the local gate passed on its predicted squash tree fa52066 over main 85aa8a1: the doctor_next_is_a_command, doctor_validator_rows, setup_project_dry_run_offers, setup_project_ref_parts, repo_hygiene, agents_md_codes, cli_reference sets, 320 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 #857

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
…and its round 2, and run to main 33c5128 (#883)

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

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

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

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

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

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

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

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

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

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