Repository navigation
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
Conversation
…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>
|
(lane openRepoShape-5, 2026-10-17T20:27Z) @codex review |
Reviewer's GuideThe 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 checkssequenceDiagram
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
Sequence diagram for adoption GitHub preflight checkssequenceDiagram
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
Flow diagram for GitHub limit preflight validationflowchart 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
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Codex Review: Didn't find any major issues. Breezy! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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>
|
(lane openRepoShape-5, 2026-10-17T23:29:16Z) ## Fix round 1 on 9166f84, after rv859's review: One new commit on SHOULD 1: a GitHub source named with 96 to 100 characters was told "Use a shorter --project". Done. Without
SHOULD 2: nothing ran
NIT 3: done. "96 to 246" is now "96 to 250" in NIT 4: done. The comment block in NIT 5: done. The NIT 6: done. Each remediation the scaffold and
rv859's four wording mutants now fail. NIT 7: done in the body.
Tests
On Runs on Linux (Python 3.12) through
Mutants51 in all, one at a time, each applied to this tree in a scratch clone and run with
The comment and docstring hunks (NITs 3, 4 and 5) change no behaviour, so no mutant is aimed at them. Merge checks
Lane: openreposhape-5 (openRepoShape-5) |
|
(lane openRepoShape-5, 2026-10-18T02:21:25Z) Re-review of #859 (#838) fix round 1 at
Findings, most severe firstThere 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
NIT 2: the detail of the rename-first refusal is never asserted. At
NIT 3: the predicate covers any root over 95 characters, not only 96 to 100.
Checked and true
Could not verify
Scratch left; no process running
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>
|
(lane openRepoShape-5, 2026-10-18T03:02:55Z) NIT round of #859 (#838): One new commit on NIT 1: "no test covers a plan where only one leg is named after a long root". Done. At
NIT 2: "the detail of the rename-first refusal is never asserted". Done. NIT 3: "the predicate covers any root over 95 characters, not only 96 to 100". Done, with the cosmetic point.
One more, found by the mutants. With the root bounded, the Tests
On Runs on Linux (Python 3.12) through Mutants33, one at a time, each applied in a scratch clone at Merge checks and Sonar-style checks
Next, from lane 5: rv859b re-checks this round. Lane: openreposhape-5 (openRepoShape-5) |
|
(lane openRepoShape-5, 2026-10-18T03:15:28Z) Re-check of #859 (#838) NIT round at
Findings, most severe firstThere 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.
Checked and true
Could not verify
Scratch left; no process running
From lane 5: #859 is READY at Lane: openreposhape-5 (openRepoShape-5) |
|
|
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 |
…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>
…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>
…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>



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-topicafter 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
ghto find them. The fakeghin 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.pysetsxf-project-<id>last, after it has created and seeded all three repositories. An id over 39 characters failedgh repo edit --add-topicthere. A--projectof 40 or more gives such an id by default.adopt-project.py executefailed at the same topic after both legs and the split were pushed and the pull request was open.--projectof 96 to 250 characters, or an--orgof 40 to 255, passed the plan and reached the firstgh repo create.Now each is refused before any repository is made, and only where a GitHub repository will be made. Under
--local-remote-dirnothing 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'sNAME_MAX:GITHUB_TOPIC_MAX = 50, andGITHUB_ID_MAX = 50 - len("xf-project-") = 39. I did not put it in the contract'sid_pattern, soscripts/repo_shape.pyis not re-pinned, and a local-only project keeps a long id.refuse_a_topic_github_cannot_setreads the id's bound off the topic the naming policy gives.scaffold-project.pyrefusesscaffold-topic-too-longright afterscaffold-bad-id. That is before_declaration, which callsgh apifor a--pinwith no--pin-source, and--dry-runrefuses the same. No process has started by then when--elected-byorGIT_AUTHOR_NAMEnames the elector. Without either,_identityhas rungit config user.namefirst, as it has beforescaffold-bad-id. Nothing is created either way. A derived id is named asscaffold-bad-idnames one,--id (not given, so --project lowercased). Its remedy is an explicit--id, which keeps the long name. A passed--idis told to pass a shorter one.adopt-project.py executerefusesadopt-topic-too-longwhere it works out the topic, before_confirmand before either leg. That is after thegit ls-remotereads 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. Noghcall is made and nothing is created either way. It names the plan'sid:(orid (not set in the plan, so legs.assembly lowercased)), and says to fixid:or re-runplanwith--id.bad05d21, a 40-character project made threegh repo createcalls, thenHTTP 422at--add-topic, exit 2. An adoption with a 40-character--idpushed both legs and opened the pull request, then printedSet the topic by handwith a topic GitHub refuses. On71936633, both refuse with noghcall 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_namesnow takes a third list,github: the names GitHub will be asked to create. A name overGITHUB_NAME_MAX = 100is refused under the same code,scaffold-name-too-longoradopt-name-too-long.--local-remote-dir. Adopt passes the two legs, because the root already exists.--projectcannot 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, asadopt-bad-iddoes, 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'slegs.assemblyis not held to a GitHuborigin, that is, a source with nooriginor a pathorigin, and a long--projectwas typed.3.
--orgover 39.refuse_an_owner_github_cannot_haveasks it:scaffold-org-too-longin the scaffold's plan, andadopt-org-too-longinexecutebeside the names, before either leg. A detected organisation needs nothing extra. One read from a GitHuborigin, or fromgh, 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.
--helpstates the bounds. Each of--project,--idand--orgsays its number insetup-project.py,scaffold-project.pyandadopt-project.py plan, anddocs/cli.mdis regenerated. The two direct tools build the sentence from the constants.setup-project.pycannot import them, because it runs before there is a checkout.test_setup_project_says_the_numbers_it_cannot_importholds its sentences to the numbers. Once #811 lands, the same test also holds #811'sGITHUB_NAME_MAXandPROJECT_NAME_MAXto them. I did not hold the help line back for a later pull request:merge-treefinds no conflict indocs/cli.mdagainst 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.pyis the scaffold's own--dry-runplan, given setup's--project,--idand--org. That plan now refuses all three before step (6) asks for the yes, and setup printsthe plan could not be produced (see above)under the scaffold's refusal. The refusal names--id,--organd--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 wordscharacters long, which #811's tests assert are absent from a 95-character GitHub run.Lands after #811. On a probe merge of
71936633with #811's head050abea6, these files pass: #811'stest_setup_project_name_edges.py, #729'stest_setup_project_typed_name.py,test_setup_project_py.py, the new file andtest_cli_reference.py(430 passed). Themerge-treewith #811 and with #756 (b3c77cc2) is clean, and no hunk here touches their name rules.Tests
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 noghcall 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:executewithout--yes, and its control;execute's own, word for word;--add-topicafter the repositories exist (#838) #859's re-review): one leg named after the source with the other edited to a short name, for each leg (2); a leg that starts with the root without being named after it (1); a root of 101 and one of 251 (2); the disk's remediation under--local-remote-dirbeside a leg named after a 100-character source (1). The in-process test of the rename-first case now asserts the refusal's detail too.tests/test_scaffold_manifest_values.py: the GitHub half oftest_the_longest_name_a_disk_keeps_is_still_acceptedasked a 250-character project. It now asks GitHub's longest, 95, with a short--id.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. On9166f84f, 3 of the 58 fail: the three cases of a source named with 96 or 100 characters. On71936633, 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 on71936633's code, 2 of the 64 fail: 101 and 251. The rest pass on both; each kills a mutant (below).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.pyandtest_agents_md_codes.py: 980 passed, 3 skipped, 1 xfailed in 857.78s (the longest shard). Every test file that namesadopt-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 ofadopt-project.py.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.9166f84fwith fix round 1's diff uncommitted, whosesha256equals that ofgit 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.pyandtest_cli_reference.py: 434 passed.bad05d21with the first round's change uncommitted (an earlier state of it, not9166f84f):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), withtest_agents_md_codes.pyand 47 of the new file's tests: 1148 passed, 3 skipped, 1 xfailed;ghor a GitHub-mode run: 2127 passed, 1 skipped.3845fd54, a local commit that differs from9166f84fonly intests/test_github_name_limits.py:Reproduction
The reproduction runs on a fake
ghand touches no network. Thebad05d21side ran in the first round; the other side ran again on71936633in fix round 1.--no-push, against a fakeghthat refuses a topic over 50 characters as GitHub documents. Onbad05d21,--projectof 40 characters maderepo view×3, thenrepo create×3 (spec, code, root), thenrepo edit ... --add-topic xf-project-<40>, which gotHTTP 422. The run exited 2, with the work trees left. On71936633it exits 2 withscaffold-topic-too-long, noghcall, and no work directory.executeran in GitHub mode with--idof 40 characters, usingtest_adopt_github_exits.py's shim told to refuserepo edit. Onbad05d21it made both legs, pushed the split and opened the pull request. Then it printedThe split IS pushed and the pull request IS open. Set the topic by hand:with three commands GitHub would refuse too. On71936633it exits 2 withadopt-topic-too-long, noghcall, and no leg.Mutants
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'sid:naming, each tool's--help, setup's usage, and a staledocs/cli.md.9166f84f: 42 mutants; 7 survived.71936633: 51 mutants, one at a time: 50 killed and 1 known survivor._confirm, and the four remediation wordings.sm-both-ends-26,_both_ends' boundary of 27, which is moved code and not new.495d4b33: 33 mutants, one at a time, each run with-xagainst the new file: 33 killed, none survived.rv-any-to-allandrv-code-onlyfailtest_one_leg_named_after_the_source_is_enough[spec-after-the-root].rv-spec-onlyandrv-suffix-always-specfail[code-after-the-root].rv-startswith-rootfailstest_a_leg_the_plan_names_otherwise_keeps_its_remediation[starting-with-the-root].rv-detail-droppedandrv-detail-is-remedyfailtest_a_leg_named_after_the_source_is_mended_by_renaming_it.specleg 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.f-ad-local-guard-dropped, the--local-remote-dirguard 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_troublekills it, and all 33 were run again on495d4b33.Merge checks
At
495d4b33,merge-treeis clean against origin/main4dea85eeand against 34 of the 36 other openfix/*andl3-*heads (ls-remoteat about 03:00Z), among themfix/798-adopt-legs-asked-first(70801c91), which conflicted at fix round 1's sweep. The two that conflict arel3-677-pins-hand-step-wip(shape-doctor.py,validate-pins.pyand two test files) andl3-678-naming-checkout-wip(shape-doctor.py). They conflict the same way at71936633, and touch none of this pull request's files.Fix round 1's sweep: at
71936633,merge-treewas clean against origin/main190133d5and against 55 of the 58 openfix/*andl3-*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 exceptfix/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-pathsandl3-rehearsal-v1.1.0. Three conflicted. Each conflicted the same way at9166f84fand against main190133d5alone, 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.pyand two test files, none of them touched here);l3-678-naming-checkout-wip(shape-doctor.py).The new file builds no
GIT_CONFIG_COUNTby hand. The prompt test gives git a credential helper through a global config file of its own (GIT_CONFIG_GLOBAL). #848'stest_no_test_sets_a_git_config_count_of_its_ownpasses on a probe merge of71936633with #848's head79b2eac4:test_no_auto_maintenance.pyand the new file, 118 passed.Left out
scripts/family.py inithas the same late--add-topicfor the family's id, and the same unasked--organd holder name. The issue's goal names the three tools, and seven open branches editfamily.py, so I left it out. It is checklist candidate 1.adopt-project.py checkasks neither GitHub's limits nor the disk's.executerefuses both before either leg.checktakes--local-remote-dirand could find them: candidate 2.Checklist candidates
family.py init: an id over 39 characters failsgh repo edit --add-topicafter the holder is created and pushed. The topicxf-project-<family id>is set last (scripts/family.py:2657), and nothing asks GitHub's 50.--orgover 39 and a holder name over 100 are not asked either. Direction: the same three helpers, ininit's plan, without--local-remote-dir.adopt-project.py checkpasses a plan thatexecuterefuses for a name's length.adopt-name-too-long(the disk's 255, and now GitHub's 100),adopt-org-too-longandadopt-topic-too-longare asked byexecutealone.checktakes the same--local-remote-dir, so it could report each one as a finding before the person's yes.--orgholding 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_valueand the manifest validator acceptmy_org.x, whichtests/test_adopt_manifest_values.pyplans as valid. That--orgreachesgh repo create.legs.specandlegs.codeare the plan's own to edit, andadopt-name-too-long(the disk's 255, and GitHub's 100) names a leg named otherwise than afterlegs.assemblywith 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.specorlegs.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:
Bug Fixes:
Enhancements:
Documentation:
Tests: