Repository navigation
scaffold-project.py: --project NUL and --project Con are taken and the three repositories and their folders are made, where family.py init refuses the same names as family-bad-holder-name #989
Description
Goal: scaffold-project.py refuses, before anything exists and under --dry-run alike, a --project that is a name no Windows disk can make a file or folder of (CON, PRN, AUX, NUL, COM1 to COM9, LPT1 to LPT9, in any case), by the question family.py init already asks of --family.
Filed by lane openRepoShape-3, from RV972's review of #972, its N5 (comment 6254722026), on #972. Lane 3 ruled that N5 predates #972 and goes to a follow-up issue; this is it.
State. Read at origin/main ae63d450. scaffold-project.py, scripts/family.py and scripts/shape_materialize.py are unchanged since c4dd19dc, so every line below is the same there.
family.py initasks the question._checked_family_identitycalls_classify_family_nameand then_refuse_an_unholdable_holder_name(family)(scripts/family.py:2232-2233; the function isscripts/family.py:733-760). That asks_unmountable_reason(scripts/shape_materialize.py:4481, which readsWINDOWS_DEVICE_REat:4420) and refuses asfamily-bad-holder-name. family.py init:--created-byandGIT_AUTHOR_NAMEare never asked whether the manifest can hold them, a whitespace-onlyGIT_AUTHOR_NAMEfails after the remote exists, and a--familya Windows disk cannot hold exactly is accepted #688 added it forinit.scaffold-project.pynever asks it of--project. The value goes throughchecked_value("--project", ...)(scaffold-project.py:1953), the three names are built (:2000) and judged by the naming policy alone (_refuse_bad_idat:2022,_check_declared_namesat:2048). The one question about a Windows-unmakeable name that the scaffold does ask is about the two leg PATHS (refuse_bad_leg_paths, called at:1967, which reaches_unmountable_reasontoo).NULandConare each one CamelCase token, which is all the naming policy asks of a project name, so nothing else stops them.
What happens. Measured at cf66102f (the NUL and Con dry runs and the init refusals again at ae63d450, with the same results) with a stub gh (exit 97) first on PATH, GH_TOKEN=bogus, an empty GH_CONFIG_DIR, GIT_SSH_COMMAND=false, and --local-remote-dir so no network is asked.
scaffold-project.py --org acme --project NUL --dry-run --local-remote-dir <dir>/lr --work-dir <dir>/out --elected-by X
assembly acme/NUL -> <dir>/lr/NUL.git
spec acme/NUL-spec -> <dir>/lr/NUL-spec.git
code acme/NUL-code -> <dir>/lr/NUL-code.git
--dry-run: nothing was created. exit 0
--project Con --dry-run exit 0 (the same plan, acme/Con and <dir>/lr/Con.git)
--project Nul, com1, Lpt9, COM9, Aux, Prn --dry-run exit 0 each
--project NUL and --project Con, without --dry-run exit 0 each
<dir>/out : NUL NUL-code NUL-spec <dir>/lr : NUL-code.git NUL-spec.git NUL.git
family.py init --org acme --family NUL --dry-run --local-remote-dir <dir>/lr --work-dir <dir>/out2 --created-by X
REFUSED family-bad-holder-name: --family is 'NUL', which names the holder and the folders `init` makes for it, and `NUL` is a Windows device name (with or without an extension), which cannot be created there. exit 2
--family Con the same refusal, exit 2
The device-name rule is a whole-name rule: NUL-spec and NUL-code are not device names (WINDOWS_DEVICE_RE.fullmatch needs the whole segment), but the assembly root's folder NUL and, under --local-remote-dir, its bare repository NUL.git (a device name with an extension) are. The root is the repository every engineer clones, into a folder of that name. I ran no Windows machine; the claim that a Windows disk cannot make NUL or NUL.git is HOLDER_NAME_REMEDIATION's (scripts/family.py:721-730), the rule #688 settled for holders.
adopt-project.py plan takes the same names for --project too, measured on a one-commit local repository: plan --source <dir>/src --project NUL --org acme --elected-by X exits 0 and writes a plan (project NUL (nul) in acme); Con the same. execute was not run.
Evidence.
- The two code paths above, and the probe output.
- RV972's N5 and lane 3's ruling on scaffold-project.py and family.py init: a flag passed empty is refused by name, a passed --id is told what an id may hold once the name passes, and an empty folder this user cannot list is told the permission (#911, #930) #972 (
issuecomment-6254722026). tests/test_family_init_asks_every_value.py:461-485holds theinitrows (bad_holder_name,CON,Nul,com1, in any case). No test intests/passes a device name as the scaffold's--project(grep).
Direction.
- Ask the scaffold's
--projectthe same question asinit's--family, before_declaration(the first place a--pincan reachgh), beside_refuse_bad_idatscaffold-project.py:2022. Keep scaffold-project.py and family.py init: a flag passed empty is refused by name, a passed --id is told what an id may hold once the name passes, and an empty folder this user cannot list is told the permission (#911, #930) #972's order for a bad name and a bad id: the naming refusal first, then this one, then a passed--id's sentence. scaffold-project.py and family.py init: a flag passed empty is refused by name, a passed --id is told what an id may hold once the name passes, and an empty folder this user cannot list is told the permission (#911, #930) #972 (open, head4e44df40) edits that region: its_refuse_bad_id(scaffold-project.py:879there, called at:2005) asks the naming refusals before a passed--id's sentence. Write this against scaffold-project.py and family.py init: a flag passed empty is refused by name, a passed --id is told what an id may hold once the name passes, and an empty folder this user cannot list is told the permission (#911, #930) #972 once it lands. - One function, not a second copy: lift
_refuse_an_unholdable_holder_name's question intoscripts/shape_materialize.py, whichfamily.pyalready imports_unmountable_reasonfrom (scripts/family.py:212), taking the refusal code, the flag and the remediation as the leg-path rule does (refuse_bad_leg_paths(refusal, ...)). The scaffold's code is its own (for examplescaffold-bad-project-name; the writer chooses it), and its remedy names--project, the root's repository, the folder it is cloned into and, under--local-remote-dir,<Project>.git.HOLDER_NAME_REMEDIATIONis the holder's: it talks aboutmake siblingsand the family folder. - Decide with this whether
adopt-project.py planandcheckask it of--project(measured above: plan takes the names). That is a second tool and may be its own pull request; the shared function makes it a few lines. scripts/shape_materialize.py,scripts/family.pyandscaffold-project.pyare not pinned copies, so no project re-pins.
Verify.
scaffold-project.py --project NUL,Con,Nul,com1,Lpt9,COM9,AuxandPrn, with and without--dry-run, exit 2 with the new code; noghcall is made, and no work folder and no bare repository exist afterwards.Nullable,ConsoleandAuxostill scaffold, as they did atcf66102f(a device name is the whole name only).- Rows for the scaffold beside
init's, in both modes, each holding that nothing was made. - One test over a list of names that BOTH tools must refuse, so the two cannot part again.
tests/test_family_init_asks_every_value.py,tests/test_scaffold_e2e.py,tests/test_dry_run_parity.py, and the docs guardstests/test_agents_md_codes.py,tests/test_cli_reference.py,tests/test_repo_hygiene.pyandtests/test_no_auto_maintenance.pypass, one file at a time under a stubgh.
Size: S.
Lane: openreposhape-3 (openRepoShape-3)
Activity
brettheap commentedon Oct 19, 2026
CLAIMED — lane openRepoShape-3, session b30e8b49-f249-4668-adb1-1dbfa93c09e1@Eagle, 2026-10-19T18:33:38Z, for #989
Logged in
lanes/log/openRepoShape-3.mdat83bfb56ef8c2463acb3679deea6ba9904049b462.The three reads (lane-collision-protocol Rule 1):