Repository navigation
adopt-project.py: plan --project NUL (and Con, com1, Lpt9) is refused as adopt-bad-project-name before anything exists, and check and execute find it in the plan's legs.assembly, by the one question the scaffold and family.py init ask (#1014) - #1041
Conversation
… refused as `adopt-bad-project-name` before anything exists, and `check` and `execute` find it in the plan's `legs.assembly`, by the one question the scaffold and `family.py init` ask (#1014) `plan` asks `shape_materialize.refuse_an_unholdable_name` of `--project` with its flags, before the work directory, the source, `gh` and the plan file: after the naming policy (asked only of names it holds), before a passed `--id`, the order #972 set. `check` and `execute` ask it of `legs.assembly` first in `_what_cannot_be_held`, under the same code, which joins `RENAMES_THE_LEGS`, so `check` asks `gh` nothing about the legs of a name that has to change. `Nullable`, `Console` and `Auxo` plan and check as before. RV1010's NITs on #1010: the `"nul\n"` row holds the scaffold's "the name speaks before a passed id's alphabet" (it kills mutant C), and "whatever the id" reads "whatever id is passed" in `scaffold-project.py` and the docstring of `tests/test_scaffold_device_names.py`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewer's GuideThis PR consistently rejects Windows device-name project roots as Sequence diagram for project-name validation across adopt commandssequenceDiagram
actor User
participant Adopt as adopt-project.py
participant Policy as Naming policy
participant Shape as shape_materialize
participant GitHub as gh
participant FS as Filesystem
User->>Adopt: plan --project NUL
Adopt->>Policy: _names_held(project, pins)
Policy-->>Adopt: true
Adopt->>Shape: refuse_an_unholdable_name(adopt-bad-project-name)
Shape-->>Adopt: refusal
Adopt-->>User: REFUSED adopt-bad-project-name
Note over Adopt,FS: No source read, gh call, work directory, or plan file
User->>Adopt: check plan
Adopt->>Adopt: _what_cannot_be_held(legs.assembly)
Adopt->>Shape: refuse_an_unholdable_name(adopt-bad-project-name)
Shape-->>Adopt: refusal
Adopt-->>User: FINDING adopt-bad-project-name
Note over Adopt,GitHub: No gh repo view calls
User->>Adopt: execute plan
Adopt->>Adopt: _what_cannot_be_held(legs.assembly)
Adopt->>Shape: refuse_an_unholdable_name(adopt-bad-project-name)
Shape-->>Adopt: refusal
Adopt-->>User: REFUSED adopt-bad-project-name
Note over Adopt,FS: No repository is created
Flow diagram for shared assembly-root refusal handlingflowchart TD
A[Project root enters command] --> B{Command}
B -->|plan --project| C[_names_held]
C --> D[refuse_an_unholdable_name]
D --> E{Windows device name?}
E -->|yes| F[adopt-bad-project-name]
F --> G[No plan written]
E -->|no| H[Continue plan validation]
B -->|check or execute| I[Read legs.assembly]
I --> J[_what_cannot_be_held]
J --> D
F --> K{Command result}
K -->|check| L[Finding reported; skip GitHub leg queries]
K -->|execute| M[Refuse before repository creation]
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
(lane openRepoShape-3, 2026-10-20T05:49:06Z) RV1041 (independent opus review, ruling 8) of #1041 at ce53b0b: READY. No MUST, no SHOULD, one NIT. Probes (stub gh exit 97, GH_TOKEN=bogus, empty GH_CONFIG_DIR, GIT_SSH_COMMAND=false), main 5b41d44 vs head:
Decisions: all agreed. legs.assembly over project: is right: project: is read only by _not_the_source (adopt-project.py:15817). A hand edit with project: NUL and legs.assembly: Zqxj passes check, but nothing named NUL is ever made. The _names_held gate keeps naming first (M3 killed: 4 rows). Tests at head, one file at a time: test_adopt_device_names 44, test_derived_id_refusals 63, test_adopt_plan_flag_refusals_first 89, test_scaffold_device_names 48, test_scaffold_leg_path_collisions 104, test_agents_md_codes 47, test_cli_reference 23, test_update_add_codes_documented 6, test_repo_hygiene 100, test_no_auto_maintenance 60, all passed. The docs guards need no doc: neither requires a new adopt-* code to be documented. Mutants: M3 killed (4), M6 killed (12). Mine, R1 (the device question moved last in _what_cannot_be_held), survives: see NIT-1. NIT-1 (tests/test_adopt_device_names.py:236): decision 4, "first in the list", is held by no test. With org "a"*40 and legs.assembly NUL the list is [adopt-bad-project-name, adopt-org-too-long] at head and the reverse under R1. Fix: one row with a 40-character org asserting that order. Merges (git merge-tree --write-tree): origin/main 5b41d44 clean (tree 535279bb), #1038 3d52f86 clean, #1031 abd1b98 clean, #1030 18cc659 clean; test_adopt_device_names gives 44 passed on each predicted merge. Lane: openreposhape-3 (openRepoShape-3) |
…t in `_what_cannot_be_held`, and a row holds it, a 40-character `org:` with `legs.assembly` NUL listing `adopt-bad-project-name` before `adopt-org-too-long` (RV1041 NIT-1, #1014) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-3, 2026-10-20T05:52:22Z) F1041 (fix round 1 for RV1041, READY with one NIT) of #1041: NIT-1 is closed at c0ccf75. NIT-1 (decision 4, "first in the list" was held by no test): tests/test_adopt_device_names.py gains test_the_device_question_comes_before_the_organisations. It calls _what_cannot_be_held with org "a"*40, not local, and legs.assembly NUL, and asserts the codes are exactly [adopt-bad-project-name, adopt-org-too-long], in that order. Only that test file changed (10 lines added); adopt-project.py is untouched. RV1041's mutant R1 (the device question moved to the end of _what_cannot_be_held) now fails: 1 failed (the new row, ['adopt-org-too-long', 'adopt-bad-project-name'] != [adopt-bad-project-name, adopt-org-too-long]), 44 passed. adopt-project.py was restored and its sha256 matched (e6cf9639...). Tests at c0ccf75, each file alone under the stub gh (exit 97), GH_TOKEN=bogus, an empty GH_CONFIG_DIR, GIT_SSH_COMMAND=false and EMAIL set: test_adopt_device_names.py 45 passed, test_adopt_plan_flag_refusals_first.py 89 passed, test_no_auto_maintenance.py 60 passed. The PR body gains a "Fix round 1 (RV1041)" section; the rest is kept. The PR is still a DRAFT. Lane 3's coordinator checked the 10-line diff against RV1041's suggested row, so no second review round is needed. Lane: openreposhape-3 (openRepoShape-3) |
|
… holding U+202E or another character the launcher refuses, by code point and in the launcher's words, before it prints the value or runs `git clone` (#953) (#1043) `setup-project.py`'s self-bootstrap refuses an `$OPENREPOSHAPE_REPO` holding a character the launcher refuses (U+202E and the rest of `MISDRAWN_CHARACTERS`). It refuses by code point, in the launcher's words, before it prints the value or hands it to git. Head `c42afa30`, one commit on origin/main `5b41d44f`. Only `setup-project.py` and a new `tests/test_setup_project_repo_misdrawn.py` change. No flag, no `--help` line and no doc changes. **`setup-project.py`** (section 0, just above `self_bootstrap`): - `MISDRAWN_CHARACTERS` is the launcher's list (`openRepoShape:502` at `5b41d44f`), entry for entry, in the package's tuple form. It is a copy, for `checked_value`'s reason: self-bootstrap mode runs before there is a checkout to read the launcher from. - `misdrawn_characters(value)` names the characters a value holds as `U+XXXX`, in code point order. It is the package's `standard.misdrawn_characters`, and it looks for characters, as `CONTROL_RE` does. - `checked_repo(value)` asks that first. It refuses with the launcher's `checked_repo` sentence, which names the code points and nothing else of the value. Then it calls `checked_value(..., PATH_RE)`, unchanged, for a leading `-` and a control character. - `self_bootstrap` reads a non-empty `$OPENREPOSHAPE_REPO` through `checked_repo`. Unset or empty still means the default. The check stays where `--org` is asked, so the order is the same: `--org` first, then the repository, then `openRepoShape setup` is printed, then `mkdtemp` and `git clone`. - The value's raw bytes are never printed. The refusal goes through `die`, so it is `REFUSED: ...` on stderr with exit 2. **`tests/test_setup_project_repo_misdrawn.py`**, 22 tests: - **Parity:** `test_the_misdrawn_table_is_the_launchers_list` reads `MISDRAWN_CHARACTERS=(...)` out of `openRepoShape` and compares the tuple. It also checks that `misdrawn_characters` names all of the launcher tests' `MISDRAWN`, each once and in order. `test_misdrawn_characters_names_the_launchers_and_nothing_else` runs the launcher's `SHARING_ITS_BYTES`. These follow #947's two tests for the package. - **Direct path, 12 tests:** two runs (`Atlas --org acme`, which is the issue's run, and `--org acme` alone) times six values: U+202E, U+00AD, U+200B, U+E0001 (two, three and four UTF-8 bytes), two characters at once, and a tab before U+202E (the launcher asks for the code point before the control character). - Each test runs a lone copy of the file under `call_loggers`' logging git, gh and curl. - Each checks the launcher tests' `assert_refused_saying`: exit 2, the launcher's sentence, an empty stdout, and no listed character, escape or `forged` in either stream. So no `E2 80 AE` gets out. - The git log must hold exactly one call: the checkout probe. - **Values that still go on, 7 tests:** the launcher's `NOT_CONTROLS` after `acme/`: e-acute, a no-break space, U+200C and U+200D where a script or an emoji needs them, U+2026, U+FF21 and a subdivision flag. Each is printed in the clone line and handed to `git clone` as before. - **Package path, 1 test:** a tree with no `.git` (the package's modules under `src/openreposhape/`, plus `setup-project.py`, `scaffold-project.py`, `shape-doctor.py` and `contracts/repository-naming.yaml`). `openRepoShape Atlas --org acme` is run from that tree's `src` with the issue's value. The package prints its one line (`the standard is the checkout this package sits in, <tree>.`) and hands over. The tree's `setup-project.py` refuses: exit 2, the launcher's sentence, no U+202E and no `forged` in either stream, and only the checkout probe in the git log. - Tests that use the logging stub are skipped on Windows. There `subprocess` starts `git.exe` and never the stub's `.cmd`, so the stub would log nothing. CI is Linux only (#597). - `setup.sh` has no twin of these tests. Its guard covers control characters and the two separators and no other listed character, by #940's design: the launcher refuses the rest first where it is the way in. **"No git call", stated exactly.** The refused run makes one git call, `git -C <dir> rev-parse --show-toplevel`. That is `is_shape_checkout`, which runs before `self_bootstrap` on every run past `--help` and `--preflight`. It asks whether the file stands in a checkout, which is how the run knows it is self-bootstrapping at all, and it names nothing of the value. No git call names the value, and there is no clone. Moving the check above the probe would also refuse the value on the developer path, which never reads it. It would also leave `setup-project.py` ordered differently from `setup.sh`, which probes first and guards only on its self-bootstrap path. These run `$Q/v118/w953/probe.py`. Each case uses a fresh scratch folder, a logging git stub that then runs the real git, a gh that fails, `GH_TOKEN=bogus`, an empty `GH_CONFIG_DIR`, `GIT_SSH_COMMAND=false` and `GIT_CONFIG_GLOBAL=/dev/null`. "Before" is `git archive 5b41d44`; "after" is the worktree. Results are the same under git 2.43.0 and 2.47.1. | Case | Before (`5b41d44f`) | After | |---|---|---| | package, ZIP tree, `Atlas --org acme`, `acme/r[U+202E]forged` | exit 128; `E2 80 AE` on stdout and stderr; git: probe, then `clone --quiet -- acme/r[U+202E]forged /tmp/openreposhape-shape-...` | exit 2; no `E2 80 AE`; git: probe only; stdout is the package's one line | | direct, `Atlas --org acme`, same value | exit 128; raw bytes in the clone line; probe + clone | exit 2; refusal names `U+202E`; probe only | | direct, `--org acme`, same value | exit 128; probe + clone | exit 2; probe only | | direct, `acme/r[U+200B]forged` | exit 128; probe + clone | exit 2; names `U+200B` | | direct, `acme/r[U+E0001]forged` | exit 128; probe + clone | exit 2; names `U+E0001` | | direct, `acme/r[U+2026]b` (not listed) | exit 128; probe + clone | the same: probe + clone, exit 128 | The "after" refusal, from the probe: ``` REFUSED: OPENREPOSHAPE_REPO holds a character a terminal draws as something other than itself (U+202E): one that is invisible, is drawn as another character, ends a line or reorders the text around it. This tool will not put it on a `git clone` command line. Set it to a URL or a path. ``` The new file was run on main's `setup-project.py` (`git show 5b41d44:setup-project.py`, put back byte for byte afterwards): **15 failed, 7 passed**. - Failed: both parity tests (`AttributeError`: no `MISDRAWN_CHARACTERS`, no `misdrawn_characters`), all 12 direct tests, and the package test. - Passed: the 7 `NOT_CONTROLS` tests. They pass at main by design, because they guard against a check that is too broad. Each mutant was applied alone (`$Q/v118/w953/mutate.py`), the new file was run (22 tests), and both files were put back. **All 18 are killed.** | Mutant | Failed | |---|---| | m01 check removed (main's read) | 13 | | m02a check moved after the `(0) self-bootstrap` line | 13 | | m02b check moved after the clone line | 13 | | m03a only the value's first character checked | 15 | | m03b only the table's first entry checked | 13 | | m04 only U+202E checked | 8 (U+00AD, U+200B, U+E0001, two-at-once) | | m05 refusal exits 1 | 13 | | m06a table drops `E0001` | 4 | | m06b parity compares the first five entries + table drops `E0001` | 4 (parity's second assert, `SHARING_ITS_BYTES`, both U+E0001 tests) | | m06c parity compares the first five + table adds `200C` | 2 (`SHARING_ITS_BYTES`, the Persian/U+200C test) | | m06d parity compares the first five + `2060-206F` narrowed to `206E` | 2 | | m06e parity uses set inclusion + table drops `E0001` | 4 | | m07 a range read without its last code point | 15 | | m08 control check asked before the code points | 2 (both tab-then-U+202E tests) | | m09 characters named in the value's order | 3 | | m10 every character past ASCII refused | 7 (every `NOT_CONTROLS` test) | | m11 refusal quotes the value back | 13 | | m12 check skipped on a hand-over (`OPENREPOSHAPE_SELF_BOOTSTRAP=1`) | 1 (the package test alone) | All at `c42afa30`, one pytest process at a time: `/usr/bin/python3 -m pytest -q -p no:cacheprovider -o tmp_path_retention_policy=failed`, one reused basetemp, `EMAIL=service@opensoft.dev`, a stub gh first on PATH, `GH_TOKEN=bogus`, an empty `GH_CONFIG_DIR`, `GIT_SSH_COMMAND=false`, `GIT_CONFIG_GLOBAL=/dev/null`. | Run | git | Files | Result | |---|---|---|---| | h1 | 2.43.0 | `test_setup_project_repo_misdrawn.py` (22), `test_setup_project_ref_parts.py` (27), `test_openreposhape_package.py` (213), `test_readme_windows_quoting_sentence.py` (44), `test_release_install_check_children.py` (30), `test_repo_hygiene.py` (100), `test_agents_md_codes.py` (47), `test_cli_reference.py` (23) | 506 passed in 94.06s | | h2 | 2.43.0 | `test_setup_project_py.py` | 340 passed in 122.68s | | h3 | 2.43.0 | `test_setup_sh.py` | 191 passed in 96.62s | | h4 | 2.47.1 | `test_setup_project_repo_misdrawn.py`, `test_setup_project_ref_parts.py`, `test_setup_project_py.py` | 389 passed in 128.13s | | h5 | 2.43.0 | `test_no_auto_maintenance.py`, `test_setup_project_dry_run_offers.py`, `test_validator_child_encoding.py`, `test_setup_project_name_edges.py`, `test_setup_project_typed_name.py` | 353 passed, 2 skipped in 73.92s (the two skips: `GIT_CONFIG_GLOBAL` was set) | No `shell_quoted` call was added (count 0), so the README's and the handbook's Windows-quoting sentences and `NAMED` are unchanged. `setup-project.py` is not a pinned copy. The wheel takes it at build time (`pyproject.toml` `force-include`), and the tests that read its source (`test_setup_project_py.py`, `test_release_install_check_children.py`, `test_repo_hygiene.py`) pass. - `git merge-tree --write-tree` of `c42afa30` with origin/main `5b41d44f` (fetched with `-c gc.auto=0 -c maintenance.auto=false`) is clean, tree `59de4c261c228274a5cff18aff953211de7002e3`. - No open PR touches `setup-project.py` or the new test file. Every open PR's head was still merged in, and all ten are clean: #1041 `4a23c6e2`, #1039 `5f924500`, #1038 `fd1de5ab`, #1031 `eea70be1`, #1030 `9b4845bb`, #1028 `0c9eac6c`, #1027 `052f1e24`, #1025 `f83ca618`, #1023 `4d0aa1cf`, #954 `dc7fd7ea`. - **RV947's companion is left out.** That companion is the package's scaffold run asking the repository even beside a checkout. - Once this lands, no package run prints the value or hands it to git. Beside a git checkout, the hand-over takes `setup-project.py`'s developer path, which never reads `$OPENREPOSHAPE_REPO`. `self_bootstrap` and `standard.py` are the only shipped Python that read it. Beside a tree with no `.git`, this check refuses it. - The companion would change `standard.prepare`, which `--doctor` and the four tools share. They would then refuse a value they never use. That is the launcher's "checked once, before every run but `--help`" policy, and it is a decision about the package of its own. - `src/openreposhape/standard.py` is also open in #1038. - **Found while measuring, not changed here:** `CONTROL_RE` and `PATH_RE` hold `\x00-\x1f\x7f` only, so a C1 control passes `checked_value`. The launcher and the package refuse C1 (U+0080 to U+009F) in a repository. With `acme/r[U+009B]31mforged`, the clone line prints the raw `C2 9B`, before and after this change. `--into` and `--local-remote-dir` go through the same `PATH_RE` (read in the code, not measured). That would be a follow-up for `checked_value`, which every flag shares. Review: rv953, lane 5's independent reviewer, not the writer (w953): READY at `c42afa30`, the head that lands (issuecomment-6284626889), with no MUST. It rules #953's Verify met in substance: the refused run's only git call is the checkout probe every run makes, which names nothing of the value, and there is no clone. Its SHOULD is #459's documented C1 known limit (U031, L4-24), left with its test NIT for a follow-up on a ruling. CI (Brett Heap's ruling 27): H0 is the head c42afa3 itself, which passed tests, tests-git-bash and SonarCloud; main has touched none of its files since its base 5b41d44; and the local gate passed on its predicted squash tree e146bb5 over main e656199: the setup_project_repo_misdrawn, repo_hygiene, agents_md_codes, cli_reference sets, 192 passed. Landed on Brett Heap's word of 2026-10-18T14:45:28Z, relayed to every lane by lane 3 (#473 issuecomment-6251255596): "merge v1.2.0 PRs once reviewed and green". Closes #953. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Sorry @brettheap, you've used your own review budget of 250,000 diff characters for the last 7 days. You can request another review in 1 day and 8 hours by commenting |
…`--link-source` reading at the old first-`=` split's corners (a keyed path that begins with `=`, a keyed blank path, a key in capitals) (#1054) (#1058) Three test rows hold what #980 (#1044) left unchanged in how `--link-source` reads a value (a keyed path that begins with `=`, a keyed blank path, a key typed in capitals), so each of rv980's four surviving mutants of `scripts/validate-repository-naming.py` fails a test. Fix round 1 (rv1058's NIT 1) adds a fifth mutant to what the rows kill. #1044 said a chain whose links hold no `=` is read exactly as before. That is true (rv980 compared 800 runs on the base and the head), but it rested on that probe: `tests/test_chain_remedy_callers.py` held the #980 rows for the new reading and none for the corners of the old first-`=` split. Of rv980's seven mutants, R1, R5, R10 and R14 survived the five closest test files (404 passed under each). Tests only. The one file changed is `tests/test_chain_remedy_callers.py` (72 lines added, none removed, over two commits), in the "#980" section after `test_a_key_is_read_whole`, reusing the file's `THREE`, `EQUALS`, `naming`, `tree`, `gate_warnings`, `UNREACHED` and `clear_ambient_pin_sources`. No assert joins conditions with `and` or `or`. No script, template or doc changes, so no pinned copy moves and a root's `update-shape.py check` gains no row. Each pins bytes the gate already prints on main (the new parameter of row 3 passes on `8a057215`'s script, which is byte-identical to the one here). 1. `test_a_keyed_path_may_begin_with_an_equals`, two parameters: `openXdox==x` with `THREE` (`a-plain-link`), and `open=y==` with `EQUALS` (`a-link-holding-an-equals`, the issue's parenthetical). Exit 0; the WARNING `link-source-unreadable: =x (--link-source openXdox=...) could not answer for openXdox: no such file or directory` (and `= (--link-source open=y=...) ... for open=y`); no "read as a PATH" line. Kills R10. 2. `test_a_keyed_blank_path_is_one_blank_and_is_not_refused`: `'openXdox= '` with `THREE`. Exit 0; no `REFUSED`; the WARNING `link-source-unreadable: (--link-source openXdox=...) could not answer for openXdox: no such file or directory` (the path is one blank); no "read as a PATH" line. Kills R1 and R14. 3. `test_a_key_typed_in_capitals_is_noted_as_the_chain_spells_it`, two parameters, with the name `brett-wip`, which reads no declaration. Exit 0, and the one `NOTE ` line is compared as a whole line: `NOTE --link-source <typed>: not used - no name classified here reads <link>'s own declaration.` (the em dash of the gate's text, as the file's other NOTE rows spell it). - `a-link-holding-an-equals`: `OPEN=Y=<tree>` with `EQUALS`, the NOTE ends `reads open=y's own declaration.`. Kills R5. - `a-capital-inside-a-link` (new in fix round 1, rv1058's NIT 1): `OPENXDOX=<tree>` with `THREE`, the NOTE ends `reads openXdox's own declaration.`. The first parameter's chain, `open=y`, is all lowercase, so it could not tell "the link as the chain spells it" from "the link lowercased"; `openXdox` has a capital X. Kills R5 as well, and M2 (`return link.lower(), ...`), which survived the first head. Bytes against `5b41d44f`, the base of #1044 (checked with `git archive 5b41d44 contracts scripts` and the same values: stdout, stderr and exit code compared): - Identical to main: `openXdox==x` (row 1), `'openXdox= '` (row 2) and `OPENXDOX=<tree>` (row 3, the `a-capital-inside-a-link` parameter). - Different, because they are the new reading of a link holding `=`: `open=y==` with `EQUALS` (row 1's second parameter; at `5b41d44f` it is a bare PATH, `link-source-unreadable: open=y== (--link-source) ... for openXdox`, with "It was read as a PATH: `open` is not a link ...") and `OPEN=Y=<tree>` with `EQUALS` (row 3's first parameter; at `5b41d44f` its NOTE says `reads a chain link's own declaration`, with the same "read as a PATH" sentence). Left alone, as the issue's "Optional" part says it may be: the "It was read as a PATH" line (`LinkSources.read_as_path`, which names only the text before the first `=`). It is a NIT for a follow-up and #1053's neighbour (`validate-pins.py` has the twin), and changing it touches the pinned `scripts/validate-repository-naming.py`. Each applied alone to `scripts/validate-repository-naming.py` at the line the issue (or rv1058, for M2) names, in the worktree, restored with the saved copy and read back (sha256 `ec167fb0e259f90705db895a1a37f4adebd4cbf03ca2d5ae18f6d9a8df896548` after each; `git diff` of the script empty). R10's text is also at `:479` in `_read_remedy`, which is not mutated. The whole file (68 tests) is run with each, on git 2.43, at the fix round's head. | Mutant | The one-line edit | What it prints for the row's value | Fails | Count | |---|---|---|---|---| | R10 `:439` | `typed.rfind("=", 0, at)` to `typed.rfind("=", 0, at - 1)` | `openXdox==x` is a bare PATH: `link-source-unreadable: openXdox==x (--link-source) ...` and "It was read as a PATH: `openXdox` is not a link ..." | row 1, both parameters, on the WARNING assert | 2 failed, 66 passed | | R1 `:437` | `typed[at + 1:]` to `typed[at + 1:].strip()` in `if link is not None and ...` | `'openXdox= '` is a bare PATH: `openXdox= (--link-source)`, with the same false line | row 2, on the WARNING assert | 1 failed, 67 passed | | R14 `:385` | `self._read_source(typed)[1]` to `self._read_source(typed)[1].strip()` in `no_path` | `'openXdox= '` is `REFUSED link-source-no-path`, exit 2 (main: 0) | row 2, on `returncode == 0` | 1 failed, 67 passed | | R5 `:438` | `return link, typed[at + 1:]` to `return typed[:at], typed[at + 1:]` | the NOTE says `reads OPEN=Y's own declaration` (and `OPENXDOX's`) | row 3, both parameters, on the NOTE assert | 2 failed, 66 passed | | M2 `:438` (rv1058) | `return link, typed[at + 1:]` to `return link.lower(), typed[at + 1:]` | the NOTE says `reads openxdox's own declaration` | row 3, `a-capital-inside-a-link` only, on the NOTE assert | 1 failed, 67 passed | Each mutant fails only the row it is named for, and the file passes (68) with the script restored. At the first head (`b775c739`, 67 tests) R10 was 2 failed, R1 1, R14 1 and R5 1, and M2 survived with 67 passed. `EMAIL=service@opensoft.dev`, a stub `gh` first on PATH (it refuses every call), `GH_TOKEN=bogus`, an empty `GH_CONFIG_DIR`, `GIT_SSH_COMMAND=false`, `GIT_CONFIG_GLOBAL=/dev/null`; through `$Q/v110/run9.sh` (`-q -rs -p no:cacheprovider -o tmp_path_retention_policy=failed`, one reused basetemp each). Never the full suite. - git 2.43.0 (`/usr/bin/git`), `tests/test_chain_remedy_callers.py` alone: 68 passed (63 before this PR, plus 5: row 1 has two parameters, row 3 has two, row 2 is one). - git 2.43.0, that file with `tests/test_link_source_chain_defaults.py` and the trio (`tests/test_repo_hygiene.py`, `tests/test_agents_md_codes.py`, `tests/test_cli_reference.py`): 261 passed. - git 2.47.1 (exactly `$Q/v115/w764/git247/bin/git`, first on PATH), `tests/test_chain_remedy_callers.py`: 68 passed. - The five new test ids on `8a057215`'s tree (a scratch copy of its `git archive`, with this file copied in): 5 passed. The whole file there: 66 passed, 2 skipped (the two older-library rows need history the scratch copy lacks). - `git diff --check` clean; no line over 79 characters. `git merge-tree --write-tree origin/main HEAD`, origin/main `833af144` (fetched with `-c gc.auto=0 -c maintenance.auto=false`, and `git ls-remote` agrees; main moved from `8a057215` by #1041, which touches `adopt-project.py`, `scaffold-project.py` and device-name tests, none of this PR's files): clean, exit 0, tree `bd706fdacef2`. The branch is two commits ahead of the old base `8a057215`. Review: rv1058, lane 5's independent reviewer, not the writer (w1054): READY at `b775c739` (issuecomment-6289571980) with no MUST or SHOULD; its two NITs were taken in one new commit, and the re-check is READY at `af390352` (issuecomment-6289945831), with no MUST or SHOULD and one optional docstring NIT. CI (Brett Heap's ruling 27): H0 is the head af39035 itself, which passed tests, tests-git-bash and SonarCloud; main has touched none of its files since its base 8a05721; and the local gate passed on its predicted squash tree 60843b9 over main 9e154d8: the chain_remedy_callers, repo_hygiene, agents_md_codes, cli_reference sets, 238 passed. Landed on Brett Heap's word of 2026-10-18T14:45:28Z, relayed to every lane by lane 3 (#473 issuecomment-6251255596): "merge v1.2.0 PRs once reviewed and green". Closes #1054. Closes #1054. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…round a loop is refused in words that name the link and the loop, not "(File exists)", and a symlink to a file is named as one (#978) (#1065) ## What `plan`, `check` and `execute` refused a `--work-dir` that is a symlink round a loop as "making it a directory failed (File exists)". `os.mkdir` of such a link fails with EEXIST, the error a file in the way gets, so the person was told a file is there and found a link. The way out, "Pass a --work-dir this user can write in", was the one for a folder this user cannot write in, and said nothing of a link. #963's review (r903) left it tracked as #978. `_work_root` now asks a new `_unmade_work_dir` why the directory could not be made. The code stays `adopt-work-dir-unwritable`, exit 2, and nothing is made. - The `--work-dir` is named as given, made absolute and not resolved. On base the reason named `os.path.realpath`'s reading of it, which for a link into a loop is another link, a path the person never typed. - A loop is named only where `os.path.realpath` left a symlink in the path (it follows every other one) and `os.stat` of that link fails with ELOOP (`_the_loop_in`). So a link to a file, or a long chain of links, is never called a loop. - The way out names the link: the `--work-dir` itself where it is a symlink, else the link in the loop. - A file at the `--work-dir`, or above it, keeps the system's words and #524's way out, as the issue asks. - A dangling link is no loop: `realpath` reads it as the path it leads to, which is made, as on base (the controls below). - `_work_dir_refusal` takes the way out as `fix=`, #524's by default. Names are printed with `_spelled`. No `shell_quoted` call is added. Measured with `plan` of a local source, `<A>` the directory that holds the links, before on `1a47ed07` and after on this head (exit 2 in every row): | `--work-dir` | Before | After | | --- | --- | --- | | `self`, a link to itself | `` --work-dir `<A>/self` cannot be made: making it a directory failed (File exists) `` | `` --work-dir `<A>/self` cannot be made: it is a symbolic link that leads round a loop `` | | `a`, with `a -> b` and `b -> a` | `` --work-dir `<A>/a` cannot be made: making it a directory failed (File exists) `` | `` --work-dir `<A>/a` cannot be made: it is a symbolic link that leads round a loop `` | | `into -> a`, into that pair | `` --work-dir `<A>/a` cannot be made: making it a directory failed (File exists) `` | `` --work-dir `<A>/into` cannot be made: it is a symbolic link that leads round a loop, at the symbolic link `<A>/a` `` | | `through -> loop/sub`, with `loop -> loop` | `` --work-dir `<A>/loop/sub` cannot be made: making it a directory failed (Too many levels of symbolic links) `` | `` --work-dir `<A>/through` cannot be made: it is a symbolic link that leads round a loop, at the symbolic link `<A>/loop` `` | | `loop/sub`, no link itself | `` --work-dir `<A>/loop/sub` cannot be made: making it a directory failed (Too many levels of symbolic links) `` | `` --work-dir `<A>/loop/sub` cannot be made: the way to it leads round a loop, at the symbolic link `<A>/loop` `` | | `tofile -> a-file` | `` --work-dir `<A>/a-file` cannot be made: making it a directory failed (File exists) `` | `` --work-dir `<A>/tofile` cannot be made: it is a symbolic link that leads to `<A>/a-file`, which is not a directory `` | | `a-file`, a file | `(File exists)` | unchanged | | `a-file/under` | `(Not a directory)` | unchanged | The way out: - Before, in every row: "Remediation: Pass a --work-dir this user can write in, or none: without one, each run makes a new one." - After, in the six symlink rows: "Remediation: Remove the symbolic link `<link>`, or make it lead to a directory, or pass another --work-dir, or none: without one, each run makes a new one." `<link>` is `<A>/self`, `<A>/a`, `<A>/into`, `<A>/through`, `<A>/loop` and `<A>/tofile`. - `execute` still says "no leg repository was created and nothing was pushed." first. The two file rows keep the way out they had. **Wider than the issue.** The issue asks for the EEXIST case, where the `--work-dir` is itself the link. A path that runs through a loop fails `os.mkdir` with ELOOP instead, and said only the system's words, with the same way out for a folder this user cannot write in. It is named the same way now: `--work-dir through` and `--work-dir loop/sub` reach the same place. So the words `tests/test_adopt_clone_claimed.py` held for a loop above the work directory change. That row's point, #650's code `adopt-work-dir-unwritable` and not `adopt-work-dir-shared`, is unchanged. ### The comment wording in #963's code r903's round-2 NITs a-c (#963 issuecomment-6253937080). Comments and docstrings only. - **(a)** `_ask_git_branch` passes an empty temporary directory as `--git-dir`, which is no repository. - `adopt-project.py`, `_checked_flag_inputs`: "That question is asked of a throwaway repository in the system temp folder, which it removes" is now "That question is asked with an empty temporary directory as its `--git-dir`, which `plan` removes". - `adopt-project.py`, `cmd_plan`: "runs `git check-ref-format` in a throwaway repository first" is now "runs `git check-ref-format` first, with an empty temporary directory as its `--git-dir`, which `plan` removes". - `tests/test_adopt_plan_flag_refusals_first.py`, the module docstring: "in a throwaway repository" is now "with an empty temporary directory as its `--git-dir`, which `plan` removes". - The same file's `test_the_branch_a_tracking_branch_names_is_asked_before_the_source`: "is asked of a throwaway repository in the temp folder ... and it removed what it made" is now "is asked of git with an empty temporary directory as its `--git-dir` ... and the directory was removed". - "which it removes" in r903's words is "which `plan` removes" here, so that it does not read as git removing it. - **(b)** The refusals still made after the source are listed "among them", in `_checked_flag_inputs`' docstring and the test's module docstring: a `--project` of 251 to 255 characters is refused `unsafe-value` after git's reads. - **(c)** The naming refusals come "with a remedy about the source where it names a repository". ## Tests - `tests/test_adopt_work_dir_refusal.py`, 53 tests: - `test_a_work_dir_that_is_a_symlink_loop_is_refused` held "(File exists)" through `mkdir_says`. It now holds the new words, for a link to itself and for a pair, through `plan`, `check` and `execute`: 6. - New: a link into a pair and a link through a loop (6), a path under a loop (3), and a link to a file (3), each through the three commands; each asserts exit 2, the whole of stderr, no plan written, no leg, and nothing made or changed under the test's directory. - New: `_the_loop_in` asked directly, which calls a link to a file or to a directory no loop (1). - New controls: a dangling link to `target-a` and to `missing-dir/target-b`, through `plan` and `check`, exit 0 and the target made (4). - `tests/test_adopt_clone_claimed.py`, 42 tests: `test_a_loop_or_a_file_above_the_work_dir_keeps_its_650_refusal`'s three `a-symlink-loop` rows hold the new words; its file rows keep the system's. - `tests/test_adopt_plan_flag_refusals_first.py`: docstrings only. - **Fail on base.** With `1a47ed07`'s `adopt-project.py` in this head's tree, the two changed files: 22 failed, 73 passed. The 22 are exactly the #978 rows: the 19 in `test_adopt_work_dir_refusal.py` and the 3 `a-symlink-loop` rows. The 4 dangling-link controls pass on both. On this head: 95 passed. - **The gate**, on this head, one basetemp, `EMAIL` exported, `LANES_LANE` unset, a stub `gh` first on PATH, `GH_TOKEN=bogus-w978`, an empty `GH_CONFIG_DIR`, `GIT_CONFIG_GLOBAL=/dev/null` and `GIT_SSH_COMMAND=false`, in 17 chunks and 4 shards, each under 9 minutes: - 46 files: the 34 that name `adopt-work-dir-unwritable`, `_work_root` or `work_dir`; `test_adopt_plan_flag_refusals_first`, `test_repo_hygiene`, `test_agents_md_codes`, `test_cli_reference`, `test_readme_windows_quoting_sentence` and `test_scaffold_leg_path_collisions`; and the 7 that read `adopt-project.py`'s source (`test_git_output_decoding`, `test_no_auto_maintenance`, `test_non_utf8_git_failures`, `test_shape_advisory`, `test_pinned_copy_text`, `test_openreposhape_package`, `test_template_values_single_pass`): 3444 passed, 2 skipped, in 17 chunks. - `test_shape_doctor.py`, all 448 tests in 4 shards: 445 passed and 3 skipped. **Mutants**, each applied alone to `adopt-project.py`, then `tests/test_adopt_work_dir_refusal.py` run, then restored by sha256. All 12 killed. They ran on a file that differs from this head's only in two comment lines ("which `plan` removes"). | Mutant | Failed | Killed by | | --- | --- | --- | | M1 ELOOP dropped from the errno check | 6 | through-a-loop, under-a-loop | | M2 the same-link branch always taken | 6 | into-a-pair, through-a-loop | | M3 the "is a link" test dropped from the link-to-a-file branch | 3 | the file AT the `--work-dir` (keeps `File exists`) | | M4 the way out under a loop names no link | 3 | under-a-loop | | M5 the `--work-dir` named as `realpath` reads it | 9 | into-a-pair, through-a-loop, link-to-a-file | | M6 only the last part of the path asked for a link | 6 | through-a-loop, under-a-loop | | M7 no loop ever found | 15 | every loop row | | M8 the link named where its target goes | 3 | link-to-a-file | | M9 the old way out kept | 18 | every symlink row | | M10 `_same_entry` always false | 6 | to-itself, a-pair | | M11 `realpath`'s reading passed for the path as typed | 9 | into-a-pair, through-a-loop, link-to-a-file | | M12 any link left in the path called a loop, with no ELOOP | 1 | `test_only_a_symlink_that_loops_is_called_a_loop` | M12 survives every command-level row: on Linux `realpath` leaves no link in the path but one that loops. The direct test of `_the_loop_in` holds the guard. ## Merge with main `git merge-tree --write-tree` of this head with `main` at `eee089ca` is clean (tree `02c1717d`). Of this branch's files, only `adopt-project.py` moved on main. #1046 changed other functions. #1041 added the `adopt-bad-project-name` refusal inside `_checked_flag_inputs`, below the docstring and the comment this branch rewords. A `--project` the naming policy refuses as well (`NUL.git`) is still refused only after the source, and the docstring's list of what comes after the source, read "among them", stays true. The branch was not merged, and the gate ran on this head. ## Not in this PR - A link to a path under a file (`w -> a-file/sub`) still names the path `realpath` reads, `<A>/a-file/sub`, with "(Not a directory)". True, but it does not name the link. - Not measured on Windows, where a loop is named only if `os.stat` of the link reports ELOOP. It lands by fast refresh (Brett Heap, 2026-10-15T22:48:35Z). Its reviewed head `a4e179dd` is green on Linux and SonarCloud. The branch merged main once and was never rebased; the last merge is update-branch's, clean, giving 228531a, which is green on SonarCloud and Git Bash. Lane 1's gate passed on its tree: the 47 test files that run `--work-dir` refusals and #650's claimed clones, read `adopt-project.py`'s source, and the doctor's, 3,898 passed and 3 skipped. Closes #978 Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>



Closes #1014.
adopt-project.py plannow refuses a--projectthat no Windows disk can make a folder of (NUL,Con,com1,Lpt9, in any case).checkfinds the same name in a plan, andexecuterefuses it. All three use the one function the scaffold andfamily.py initalready ask:shape_materialize.refuse_an_unholdable_name, from #1010 (e076e3e8). The two NITs from RV1010's review of #1010 are closed here too.What changed
SHOULD-1,
plan._checked_flag_inputsnow asks the question of--projectasadopt-bad-project-name. It runs with the flags, so nothing exists yet: no work directory, no git in the source, nogh repo clone, noghcall and no plan file. On main,plan --project NULwrote a plan (project NUL (nul) in acme, exit 0). Now:SHOULD-1,
checkandexecute. The question is now the first one in_what_cannot_be_held. That is the one listexecuterefuses the first entry of (_repository_urls) andcheckreports every entry of (_held_findings, #863). It is asked of the plan'slegs.assembly, under the same code, both on GitHub and under--local-remote-dir. The code joinsRENAMES_THE_LEGS, socheckasksghnothing about the legs of a name that has to change. On main,checkof a plan forNULprintedplan ok, exit 0, aftergh repo view acme/NUL-specandacme/NUL-code. Now it gives one finding,FINDING adopt-bad-project-name: <plan>:legs.assemblyis 'NUL', ..., exit 1, and the stubghlogs nothing.NIT-1.
"nul\n"is added to@pytest.mark.parametrize("passed", ...)oftest_a_name_no_root_can_have_on_windows_speaks_before_a_passed_idintests/test_derived_id_refusals.py. The row's asserts are unchanged; its docstring says what the row holds. It kills RV1010's mutant C (see Mutants).NIT-2. "whatever the id" now reads "whatever id is passed" in
_refuse_an_unholdable_project_name's docstring (scaffold-project.py) and in the docstring oftest_a_name_the_policy_refuses_too_is_refused_by_the_policy(tests/test_scaffold_device_names.py). These are comments only.Measured with a stub
gh(logs, exit 97),GH_TOKEN=bogus, an emptyGH_CONFIG_DIRandGIT_SSH_COMMAND=false, on a one-commit local source:--projectplanat mainplanat headcheck(main's plan) at maincheck(main's plan) at headNUL,Con,com1,Lpt9adopt-bad-project-name, no plan, 0 gh callsplan ok, 2repo viewcallsFINDING adopt-bad-project-name, 0 gh calls,--work-diremptyNullable,Console,Auxoplan okplan ok(2repo viewcalls, as at main)Decisions the Direction left open
adopt-bad-project-name. It is one code forplan,checkandexecute, asadopt-name-too-longandadopt-bad-orgare.planasks it. It is asked in_checked_flag_inputs, right before a passed--id's question, and only where the naming policy holds the three names--projectmakes (_names_held, the gate the passed-id question already used).--id,NUL.gitandNul.arenaming-unclassified. With no--id, they areadopt-bad-idfor the derived id, as the scaffold saysscaffold-bad-id.--id's sentence (foo_bar,'', andnulwith a line break).--pinand collision refusals of_checked_flag_inputsstill come before it, as they do in the scaffold.checkandexecuteask it oflegs.assembly, notproject:.executeclones the root into<work-dir>/<legs.assembly>._what_cannot_be_heldcomes fromlegs:.project:names no folder; only_not_the_sourcereads it.planwrote holds the same value in both, so "a plan whoseproject:isNUL" is found.--local-remote-dirtoo, because a root is cloned there as well.planstarts it with "no plan was written." (PLAN_NOT_WRITTEN);checkandexecutestart it with "no repository was created.", asORG_REMEDIATIONdoes.planasks before it reads the source, so it cannot yet tell whether--projectis the source's own name. The remedy therefore gives both ways out: another--project, or, in place, the rename first, in_rename_first's words.adopt-project.py's--helpis unchanged, as the scaffold's--projecthelp was in scaffold-project.py, family.py and shape_materialize.py: the scaffold refuses a --project no Windows disk can make a folder of, by the one question init asks of --family, and the five folder flags' --help say what an empty value and . mean (#989, part of #999) #1010, sodocs/cli.mdis unchanged.AGENTS.md, the README and the handbook name noadopt-*orscaffold-bad-project-namecode, so they are unchanged too. The module docstring gains one sentence.Left out of scope
scaffold-project.pyasks_refuse_a_name_no_root_can_havetwice) is not part of adopt-project.py:planandchecktake a Windows device name (--project NUL,Con) that the scaffold andfamily.py initnow refuse #1014 and is unchanged.executerun. The issue's Verify does not ask for one, andexecute's refusal is held in process through_repository_urls, whichcmd_executecalls right after_check_names.Tests
Each test file was run alone under the stub
gh(exit 97), withGH_TOKEN=bogus, an emptyGH_CONFIG_DIR,GIT_SSH_COMMAND=falseandEMAIL=tests@openreposhape.invalid. The command was/usr/bin/python3 -m pytest -q -p no:cacheprovider --basetemp=<scratch>/bt-<name> -o tmp_path_retention_policy=failed tests/<file>. They ran at13fd52e93(one2f9fc424). It was then rebased onto5b41d44fc, which changes onlytemplates/family-root/scripts/validate-family.pyand its test; the rows marked "after the rebase" ran at the head.tests/test_adopt_device_names.py(new)tests/test_derived_id_refusals.pytests/test_scaffold_device_names.pytests/test_adopt_plan_flag_refusals_first.pytests/test_github_limits_init_check_org.pytests/test_github_name_limits.pytests/test_adopt_leg_paths.pytests/test_scaffold_leg_path_collisions.pytests/test_agents_md_codes.pytests/test_cli_reference.pytests/test_update_add_codes_documented.pytests/test_repo_hygiene.pytests/test_no_auto_maintenance.pytests/test_shape_doctor.py(after the rebase)tests/test_adopt_device_names.py(after the rebase)tests/test_repo_hygiene.py(after the rebase)pyflakesgives the same count on the four changed files as on main (1, which predates this change). No added line is over 79 characters.Mutants
Each mutant changed one hunk in a separate worktree of the commit. The file was then restored with
git checkoutand checked by sha256 (OK every time).plandoes not ask the questiontest_plan_refuses_a_device_name_before_anything_existsrows and 3test_plan_says_the_name_before_a_passed_idrowsplanasks it after a passed--id's questiontest_plan_says_the_name_before_a_passed_id[foo_bar]and[]planasks it without the_names_heldgatetest_plan_leaves_a_name_the_policy_refuses_to_the_policy(NUL.git,Nul.with--id fooandfoo_bar)planasks it after the source is read (after_check_namesin_checked_plan_inputs)_what_cannot_be_helddoes not ask ittest_check_finds_a_device_name_and_asks_gh_nothingand 8test_execute_refuses_it_first_of_the_list_check_readsRENAMES_THE_LEGScheckrows (assert ['repo view t...ameWithOwner'] == []) and the 8 list rows--local-remote-dirtest_execute_refuses_it_first_of_the_list_check_reads[*-local]checked_value("--id"))test_a_name_no_root_can_have_on_windows_speaks_before_a_passed_id[nul\n](REFUSED unsafe-value: --id is 'nul\n')Merges
git merge-tree --write-treeof the headce53b0b4f3ecc22f5686b438b523614c8e9243a8:5b41d44fc535279bb(the head's own tree)abd1b9844(fix/910-check-throwaway-repos, touchesadopt-project.py)9e4988f418cc65939(fix/900-spelled-names, touchesadopt-project.py)89b24784No other open PR (7 open) touches
adopt-project.py,scaffold-project.pyor the three test files.Fix round 1 (RV1041)
RV1041 (independent review at
ce53b0b4f): READY, no MUST, no SHOULD, one NIT._what_cannot_be_held, survived both test files, because the test named "first of the list" only asserted[CODE].c0ccf755faddstest_the_device_question_comes_before_the_organisationsintests/test_adopt_device_names.py: a 40-characterorg:, not local, andlegs.assemblyNUL give the codes[adopt-bad-project-name, adopt-org-too-long], in that order. Only that test file changed;adopt-project.pyis untouched.['adopt-org-too-long', 'adopt-bad-project-name'] != [CODE, 'adopt-org-too-long']), 44 passed.adopt-project.pywas restored and its sha256 matched (e6cf9639...).c0ccf755f, each file alone, same stub setup:test_adopt_device_names.py45 passed,test_adopt_plan_flag_refusals_first.py89 passed,test_no_auto_maintenance.py60 passed.Lane: openreposhape-3 (openRepoShape-3)
🤖 Generated with Claude Code
Summary by Sourcery
Refuse Windows device names as adoption project roots before any artifacts or repositories are created, and report the same validation consistently during plan, check, and execute.
New Features:
Bug Fixes:
Enhancements:
Tests: