Repository navigation
adopt-project.py: _elector keeps a user.name byte that is not UTF-8 for #255's question, _remote_repository reads such an origin URL as no org/repo, and plan asks adopt-no-org before checked_value, so a Latin-1 name is refused as adopt-unwritable-value naming --elected-by, and plan, check and execute go on past such an origin, where both ended in a UnicodeDecodeError traceback (#292) - #353
Conversation
…er a file can hold it, in `plan`, in `check` and in `execute` before any leg exists, so a byte that is not UTF-8 in `--elected-by`, `--reference` or a local `--source` directory no longer pushes both legs before `write_lf` raises (#255) Since #247 the plan writer keeps a lone surrogate (a byte that is not UTF-8, as Python decodes argv and paths) as `\udce9`, and the reader gives it back. `plan --elected-by b'Jos\xe9'`, `--reference b'ref-\xe9'` and a source under a directory `caf\xe9` with local remotes therefore passed `plan` and `check`, and `execute` created and pushed both leg remotes before `write_lf` raised `UnicodeEncodeError` (exit 1). Before #247 `plan` raised and nothing was created. A hand-edited `\ud800` in `elected_by`, `elected_on`, `reference`, `visibility`, a pin or a follow-up ended the same way, and a NUL or a line break there went into the manifest. `shape_materialize.unwritable_reason` asks the question, beside `one_printable_line`: one line, no lone surrogate, no control character. It is `one_printable_line` less `Cf`, and it takes the empty value: a Persian name joins with U+200C, which a file holds and `execute` adopted before #247 and since, and an empty `elected_by` is what `plan` records when `git config user.name` names nobody. - `plan` refuses `--elected-by` and `--reference` as `adopt-unwritable-value` and writes no plan. - `check` reports `elected_by`, `elected_on`, `reference`, `visibility`, each pin and each follow-up as `plan-unwritable-value`, through `_entry_findings`, so `execute` refuses the same findings in `check`'s words through `_refuse_what_check_finds`, before `_create_leg_remotes`. - `execute` refuses a local clone URL (the source's own path under `--local-remote-dir`) as `adopt-unwritable-value`, right after `_repository_urls`. `check` cannot know it will be written, and against GitHub it is an https URL, so `plan` and `check` still pass such a directory, as #247 made them. - `Source.open` refuses a `local_path` that does not exist and is not text as `source-unresolvable`, where a NUL or a surrogate reached `print` or `subprocess` as a traceback. - `check`'s WHAT WILL HAPPEN shows the plan's visibility, adopt branch, leg paths and follow-ups through `_shown`, so a surrogate there no longer raises on a strict stdout before the finding is printed. tests/test_adopt_template_values.py covers each argv route, the local source directory, each hand-edited value, `local_path`, the printed values, and controls (`José`, U+200C, a `café` source directory) that still adopt. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…value `execute` writes is refused before anything exists, and the clone-URL refusal's remediation moves the source and plans again, so no README line is shown reversed and following the remediation adopts instead of raising (#255) Fix round 1 on PR #270, answering lane openRepoShape-2's review at aed9190 (F1, F2, F3, and N1's test point). F1. `unwritable_reason` was `one_printable_line` less all of `Cf`, so U+202A..U+202E and U+2066..U+2069 passed `plan`, `check` and `execute` and went raw into `project.yaml`, the README's `Elected by` line and the split commit's message; a viewer showed the rest of that line reversed. Issue #255's question, `one_printable_line`, refuses them. The question is now `one_printable_line` less `Cf` other than those nine (`BIDI_CONTROLS`, by code point), plus the empty value. The joiners U+200C and U+200D, U+00AD, U+FEFF and the marks U+200E, U+200F and U+061C stay writable: a Persian name needs U+200C, and a mark sets no span's direction. `plan` refuses such a flag as `adopt-unwritable-value`, `check` finds such a plan value as `plan-unwritable-value`, `execute` refuses it before any leg exists, and a local clone URL holding one is `adopt-unwritable-value`. F2. The test file held a raw U+202E, which SonarCloud's text:S6389 flags, and other invisible literals a reader cannot see. Each is now an escape; only `é` and `日本` stay literal. The two composite assertions SonarCloud's python:S9073 flagged are four, each with a message. F3. The clone-URL refusal said to clone the source to a UTF-8 path and pass it to `execute` with --source. Followed, that raised: the clone's `origin` is the old path, and `_remote_repository` reads it as UTF-8. The remediation now says to rename or move the source and run `plan --source <its new path>`, `check` and `execute` again, and the test follows it to `adoption verified`, for a byte that is not UTF-8 and for a bidi override in the directory's name. N1. The same-sentence test now asserts both exit codes and that no run raised. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…er.name` as `--elected-by (not given, so `git config user.name`)` when it refuses it, so the refusal no longer names a flag the person never passed (#255) Fix round 1 on PR #270, N2 of lane openRepoShape-2's review at aed9190. With no --elected-by, `plan` records `git config user.name`. A TAB there was refused as `--elected-by is 'Jo\tSmith'`, naming a flag the operator never passed. It is now named as #181's `FLAG_DEFAULTS` names a value `plan` filled in, and the remediation says to pass the flag or correct `git config user.name`. A test runs `plan` under `GIT_CONFIG_GLOBAL` holding a TAB name, and skips where a repository's own `user.name` answers first. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the manifest back through the reader instead of matching its line, so it holds on main, which since #250 writes a YAML template's value as `double_quoted` spells it (#255) Fix round 1 on PR #270. Built on the merge with main be2790e, the U+200C control failed: `execute` adopted, but main b30595c (#250) renders a value between a YAML template's double quotes through `double_quoted`, so `project.yaml` holds `"AliReza"` with the character spelled as an escape, and the test looked for it raw. The reader gives back U+200C either way, so the control now asks the reader, and also asks for the name in the shape's README, which is not YAML and holds it as it stands on this branch and on main. GitHub's checks run on the pull request's merge with main, so they would have failed here. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lvable` before anything runs, so `gh repo clone` is no longer handed it as its first argument, where gh's own parser read it as one of its options (#255) SonarCloud's S8705 on #270 asked whether a value reaches a command as an option. The lane's re-verify found one that does: `plan --source=-u/x` is no path that exists and holds a `/`, so `Source.open` took it for an `org/repo` name and ran `gh repo clone -u/x <dir> -- --quiet`. gh read `-u` as `--upstream-remote-name` with the value `/x`, and `--source=--upload-pack=x/y` as a flag it does not know. A hand-edited `local_path:` or `repository:` reaches the same call from `check` and `execute`. It cannot reach git as an option (gh hands git only what follows its `--`), and main does the same. The guard sits in the condition this PR already edits. No GitHub owner name begins with `-`, so nothing gh could clone is refused, and an existing path that begins with `-` is still taken as a path. The tests put a stub `gh` first on PATH that records each call: `plan` with either value, and `check` and `execute` on a plan whose `local_path:` holds it, each exit 2 `source-unresolvable` with no traceback and no leg, and the stub records nothing. Against 7e9cdc3's code all four fail with the stub's record of `repo clone -u/x ...` and `repo clone --upload-pack=x/y ...`. A control shows the stub records the clone of `testorg/Thing`. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s `_entry_findings` composed with #255's, and this PR's pair renamed `unwritable_findings`/`unwritable_remediation` #231 (6f45bf9) puts each entry's path first in `_entry_findings` and returns `unsafe_remediation` ahead of the rest; this branch appends each value `execute` writes into a file that no file can hold. The two hunks are its docstring's order sentence and its return. The sentence keeps main's order and appends this PR's clause; the return is `(findings + submodule_findings + unwritable_findings, unsafe_remediation + remediation + unwritable_remediation)`, so the paths still come first, as #196 needs. This PR's local pair `value_findings, value_remediation` is renamed `unwritable_findings, unwritable_remediation`. #261's branch (ca21b57) binds `value_findings, value_remediation` in the same function, from `_manifest_findings`: a resolution keeping both lines under one name would silently drop one PR's findings. With the rename it cannot. The rest of main (#254/#281, #238/#287, #203/#236, #204/#227, #249/#250, #241, #215 and the earlier merges) merges with no conflict. The resolution is the lane's re-verify one (`resolve-main.py`, 385 passed on 6f45bf9), rename aside. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s `_entry_findings` order sentence composed with #255's #313 (71b58a1, #262 and #263) makes `check` find a kept root path where the shape writes a directory or its `shape/` copy, through `_root_shape_findings` called from `_leg_findings`, and makes `shape_materialize` refuse a copy whose own path is taken. This branch appends each value `execute` writes into a file that no file can hold to `_entry_findings`. The one conflict is that function's docstring, the sentence naming the order of its findings: it keeps #313's words (each entry's `leg:`, the two mount paths and what the root keeps where the shape writes, then the source's own submodules) and appends this PR's clause, ", then each value `execute` writes into a file that no file can hold (#255)". Nothing else in the file conflicts: the return still reads `(findings + submodule_findings + unwritable_findings, unsafe_remediation + remediation + unwritable_remediation)`, and `_leg_findings` carries #313's findings inside `findings`. Main's c0ed615 (#308, family.py) comes with it and touches none of this branch's files. Main has since moved on to c7c02fa (#315, family.py); this merge stops at 71b58a1. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-8 for #255's question, `_remote_repository` reads such an origin URL as no `org/repo`, and `plan` asks `adopt-no-org` before `checked_value`, so a Latin-1 name is refused as `adopt-unwritable-value` naming `--elected-by`, and `plan`, `check` and `execute` go on past such an origin, where both ended in a UnicodeDecodeError traceback (#292) git keeps a config value and a remote URL as the bytes it was given, so a repository can hold either without anyone having done anything wrong: a name saved in a legacy code page, a clone of a directory named in one. `_elector` (`git config user.name`, asked when `--elected-by` is not given) and `_remote_repository` (`git remote get-url origin` of a local source) read both through `git_out`, which decodes strictly, and caught only `Refusal`. `UnicodeDecodeError` is a `ValueError`, so `main` did not catch it either: `plan` (and, for the URL, `check` and `execute`) ended in a traceback, exit 1, naming neither the setting nor the flag past it. `_elector` now reads the name as bytes and decodes it with `surrogateescape`, as `sys.argv` keeps such a byte. It does not carry on with it: #270's `unwritable_values` asks it, in `_checked_plan_inputs`, right after `_elector` returns, and refuses it as `adopt-unwritable-value`, named as `--elected-by (not given, so `git config user.name`)`, with the remediation to pass the flag or correct the setting, before any plan is written. `_remote_repository` returns None for a URL it cannot decode, as for a source with no origin: no `org/repo` is named by it. `plan` takes `--org` and records `repository: null`, and `check` and `execute` name the source by its path and adopt it, as does `execute --source` with a UTF-8 clone of a source whose own path is not UTF-8 (whose origin is that path). Without `--org`, such a source was to meet `adopt-no-org`. It never could: since v0.2 (#5) the `or "-"` that stood in for a missing org reached `checked_value` first, which refused `--org is '-'`, a value nobody passed. `adopt-no-org` is now asked first, for a source with no origin and for one whose origin is not UTF-8. `git_out` itself is untouched: `scripts/repo_shape.py` is a pinned copy, and a change there would move every project's shape pin. Lane: openreposhape-2 (openRepoShape-2) 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 10 hours and 18 minutes by commenting |
|
@codex review |
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. Can't wait for the next one! 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". |
#270 landed on main as the squash f795cf7, and this branch was stacked on #270, so it still carries an older copy of #270's commits. Merging #270's final head 3cee853 first brings those commits up to the head the squash holds, so the merge of origin/main that follows can take this branch's side of adopt-project.py instead of conflicting with the squash. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…270 landed as the squash f795cf7 #270 landed on main as the squash f795cf7 and this branch, stacked on #270, was retargeted to main. The previous commit merged #270's final head 3cee853, so this merge changes adopt-project.py only by what that commit already brought in: the squash's content of adopt-project.py is 3cee853's, and `git diff 3cee853 f795cf7 -- adopt-project.py` is empty. adopt-project.py conflicted, and the whole file takes this branch's side, which holds 3cee853 plus #292's own change. No other file conflicted. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-2, 2026-10-12T14:54Z) This lane reviewed its own PR #353 at dcbc37b, read-only, in its own detached worktrees and one scratch clone. It authored nothing. MERGEABLE WITH NITS. There is no sequencing constraint. dcbc37b already contains main ab623dd. The issue's promise holds. On
The two merges keep the PR's diff byte for byte. Nits for the fix round:
Follow-ups. None is in #292's promise, and the PR makes none worse:
Tests:
Merges with 21 open heads: 19 are rc 0. #314 and #358 conflict exactly as they conflict with main alone. The full report is in opensoft/brett-wip at handoffs/xFactory/attachments/lane-openRepoShape-2/review353-dcbc37b/final-report.md. |
…ource whose `origin` is not UTF-8 or ends in no `org/repo` is no longer told it has no `origin`, and a passed `--org` is pinned to its `unsafe-value` refusal (#292) Review of dcbc37b, N1 and N3. N1: #292 routes a source whose origin URL is not UTF-8 to `adopt-no-org`. The sentence said "the source has no `origin` to read an organisation from", which is false of that source: `git remote -v` shows its origin. It is false too of a source whose URL ends in no `org/repo`. The refusal, its exit (2) and its remediation (`pass --org <org>`) are unchanged; the sentence now reads "the source's `origin` names no organisation this tool can read: it has none, its URL is not UTF-8, or its URL ends in no `org/repo`". `test_plan_without_org_refuses_a_source_that_names_none` takes the new bytes and a third source, `no-pair`, an origin of `repo.git`. N3: #292 moved `args.org = checked_value("--org", args.org)` below `adopt-no-org`. Deleting that line survived this file and `tests/test_adopt_plan.py`: `plan --org=-evil` exited 0 and wrote `org: "-evil"`, `check` said `plan ok`, and only `execute` refused it, at `_repository_urls`. `test_plan_refuses_an_unsafe_org_that_is_passed` pins exit 2, `REFUSED unsafe-value: --org is '-evil'` and no plan file, for `-evil`, `-` and `my org`. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…me in git's command scope as well as in `GIT_CONFIG_GLOBAL`, so they run, and the `_elector` fix is tested, in a checkout whose own config sets `user.name` (#292) Review of dcbc37b, N2. `global_user_name` skipped whenever `git config user.name`, asked in the checkout, did not answer the name the test wrote to its `GIT_CONFIG_GLOBAL` file, and a `user.name` in the checkout's own `.git/config` outranks that file. In such a checkout (the shared one had one for a while) the file reported 7 passed, 4 skipped, and mutants of `_elector`'s decode (strict, `replace`, `ignore`, no `.strip()`), which only those four tests kill, went uncaught. On POSIX the name is now also given as `GIT_CONFIG_COUNT`, `GIT_CONFIG_KEY_<n>` and `GIT_CONFIG_VALUE_<n>`, git's command scope, which outranks every config file. The entry is APPENDED to a count the environment already holds: the Windows job sets `GIT_CONFIG_COUNT=2` (`core.autocrlf`, `core.longpaths`), and replacing it would drop both. The `GIT_CONFIG_GLOBAL` file stays, as the only route where an environment variable cannot carry the byte (Windows). The byte goes in through `os.fsdecode`. Two tests pin the route without depending on whose machine runs them: `test_the_user_name_given_outranks_a_checkouts_own` makes a checkout with a local `user.name` in `tmp_path` and reads the name given, and `test_the_user_name_given_is_appended_to_the_config_the_environment_holds` sets a count of 2 and reads both entries and the name. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-2, 2026-10-12T16:52Z) The fix round on this lane's review of dcbc37b is complete at 96ff0c7: 344028e takes N1 and N3, and 96ff0c7 takes N2. The body is now v3.
Tests at 96ff0c7
Details are in |
|
@codex review |
|
Codex Review: Didn't find any major issues. 👍 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". |
|
…plan-manifest-invalid` joins `_entry_findings` after #255's values, and this branch's `plan-root-symlink` stays after the source's own submodules origin/main moved 92e29d9 -> dd8d0ea (twelve commits), merged by sha: - f0a8bf6 (#330 via #381), scripts/family.py; - 87d2eb7 (#333 via #379), update-shape.py; - cd1fc79 (#327 via #375), shape-doctor.py; - 45e01f4 (#348 via #396), tests/test_agents_md_codes.py: every code-shaped name AGENTS.md spells is a code a tool prints; - 3ef059c (#258 and #261 via #314), adopt-project.py, scaffold-project.py and scripts/shape_materialize.py: the project's YAML reader and its `validate-manifest.py` are asked about the manifest's values before anything exists; `check` reports `plan-manifest-invalid` through `_manifest_findings` in `_entry_findings`; `_repository_urls` refuses `adopt-name-too-long`; - f7f4b14 (#366 via #399), scripts/family.py; - cb7063f (#301 and #280 via #390), scripts/repo_shape.py and the template's validate-manifest.py: a bracket or brace in a plain flow scalar is refused; - 5678f68 (#278, #297 and #298 via #357), shape-doctor.py; - 53bee5b (#289 via #352), scripts/bump-leg.py; - f5cb616 (#369 via #398), scripts/shape_materialize.py: the docstrings of `force_stage` and `stage_written`; - a92e60b (#326 via #400), scripts/family.py; - dd8d0ea (#292 via #353), adopt-project.py: `_elector` keeps a `user.name` byte that is not UTF-8, `_remote_repository` reads an origin URL that is not UTF-8 as none, and `adopt-no-org` is asked before `checked_value`. scripts/shape_materialize.py, scripts/repo_shape.py and every other file merge with no conflict. adopt-project.py conflicts in two hunks of `_entry_findings`, each resolved by keeping both sides: 1. The docstring. Main's order, with #261's manifest findings after #255's values and before #190's relative urls; this branch's symlinks stay after the source's own submodules. 2. The return. Main's `_manifest_findings(plan)` call, and the findings in main's order: each entry's path (#196), coverage, the legs and what the root keeps where the shape writes, the source's own submodules (#166), this branch's `_root_symlink_findings`, #255's values, #261's manifest findings, and #190's relative urls last. The remediation and the NOTEs are main's. How each of main's commits that reach this branch's code meets it: - 3ef059c: `_manifest_findings` asks the validator the template ships about the values `execute` writes; it reads no symlink and no path the shape writes. Its materializer half adds the validator's helpers, `refuse_unstorable_names` and `MANIFEST_VALIDATOR`, and holds `election_date` to the validator's `DATE_RE`; it leaves `place`, `_materialize`, `copy_tree`, `template_files` and `_refuse_unwritable` as they were. In `execute`, its `adopt-name-too-long` (in `_repository_urls`) comes before #255's clone-URL refusal, `_refuse_what_check_finds` and this branch's `core.symlinks` probe, all before any leg. Its new paragraph of the module docstring follows the one 6b53a6d corrected; both merge with no conflict. - f5cb616: docstrings only. - dd8d0ea: `_remote_repository`, `_checked_plan_inputs`' organisation and `_elector`, none of them this branch's. - cb7063f: the plans `check` and `execute` read go through this reader. The plans `plan` writes for this branch's tests hold their entries as block mappings, each path `double_quoted` where it needs to be, and the rehearsal below read every one of them. Checked on the merged tree: - AST (fin183g/names.py, base 92e29d9): no top-level name is bound twice in adopt-project.py, scaffold-project.py or scripts/repo_shape.py, and none is loaded but bound nowhere. scripts/shape_materialize.py binds `YamlError` and `parse_yaml` twice, as main dd8d0ea does (1f90a0f's import and 3ef059c's, the same objects); the merge adds none. - Both sides intact (fin183g/sides_intact.py): the only lines that differ from either side's own change are those of the two hunks. - A rehearsal of this merge (tree d21f8b4, the same as this one) passed tests/test_adopt_root_symlinks.py with main's test_adopt_{manifest_values,git_reads,relative_submodule_url}.py, test_scaffold_manifest_values.py, test_agents_md_codes.py and test_yaml_flow_brackets.py: `440 passed`. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… exists, a source symlink with an empty target and the source's own `project.yaml` kept in the root, and `plan` no longer offers `root` for that file (#303, #305) (#387) Two ways `check` said `plan ok` to a source `execute` could not adopt, both reproduced on c7c02fa with the issues' own scripts. **#303, a symlink whose target is empty.** A `120000` entry whose blob is the empty blob passed `check`, which never reads a link's target. - With symlinks on (git's default where the disk can hold one), `execute` created both leg repositories, then failed in its first clone of the source: exit 2, nothing on stderr, two empty legs left behind. Linux refuses a symlink to nothing. This happened whatever the plan answered: `docs/empty-link` sent to spec, and a top-level `empty-link` answered `root` or `drop`. - With `core.symlinks=false`, it said `adoption verified`. The spec leg held the empty link, a clone of that leg exited 128, and the assembly's recursive clone exited 1. The top-level link answered `root` or `code` verified the same way. Only `drop` gave an assembly that clones. **#305, the source's own `project.yaml` answered `root`.** `plan` asked about it under `ambiguous-top-level-yaml`, whose question offers `root`. Answered `root`, `check` said `plan ok`. `execute` wrote the shape's manifest beside the source's file as `shape/project.yaml`, said "nothing was overwritten" and `adoption verified`. `shape-doctor.py` then said NOT A SHAPE ROOT (exit 2) and pointed at `adopt-project.py plan`. `make bootstrap` failed `naming (exit 2), manifest (exit 1), pins (exit 2)`. Answered `code` or `spec`, the doctor said COMPLIANT and `bootstrap ok`. **What changed.** - **`plan-empty-symlink` (#303).** `_empty_symlink_findings` reports each `120000` row of size 0 in `Source.tree()`. The size `ls-tree -l` gives a symlink is the length of its target, so no target is read; a SHA-256 repository reports size 0 too. - It is listed right after #196's `plan-unsafe-path`, through `_source_path_findings`, on the line `_entry_findings` already had. So `check` prints it, and `execute` refuses it through `_refuse_what_check_finds` before any leg exists (#168). - `EMPTY_SYMLINK_REMEDIATION` is said once after the findings, in `check` and in `execute`'s refusal. The fix is in the source: give the link a target, or remove it, commit, and re-run `plan`. - `plan` notes each such link in `check`'s words, and still writes the plan. - **`ambiguous-project-yaml` (#305).** A new rule in `contracts/path-classification.yaml`, above `ambiguous-top-level-yaml`, for the file `project.yaml`. Its question asks spec, code or drop, and says `root` is not an answer for this file. `release.yaml` keeps the old question. - **`plan-root-blocks-manifest` (#305).** #313's machinery is extended, not copied: `_shape_blocks`, which `_root_shape_findings` runs over each disk, now refuses the root holding `project.yaml` outright, with no copy question after it. It fires for a kept row at that path (file or symlink), or a kept directory there, on a disk that keeps case or one that ignores it. The finding's remediation is #313's `_fix_the_kept_path`. - A submodule there stays `plan-submodule-root`'s, said once. - `_kept_there` is the at-or-under lookup the copy branch already did, factored out for both. - `execute` refuses it before any leg exists, through the same list. - **Files.** `adopt-project.py` (+205/-13), `contracts/path-classification.yaml` (+28), the two new test files (399 and 366 lines) and `tests/test_adopt_root_shape_paths.py` (+6/-2). **Decisions.** - **#303 is refused whatever the plan answers, and whatever `core.symlinks` says.** - Where symlinks are made, no answer adopts. The run fails at the first clone of the source, which checks the whole tree out before anything is taken out. Measured with the link in `docs/` (sent to spec), at the top level answered `root` and `drop`, and with every path rooted or dropped and both legs seeded: there the seeded legs were pushed and the assembly's clone exited 128. - `check` cannot know where `execute` will run. A check that passed `drop` with symlinks off would pass a plan the next Linux machine cannot adopt, and the root's own `validate` workflow runs on `ubuntu-latest`. - The remediation costs one commit in the source. - **#305 covers the manifest only.** The issue asked whether other rendered root paths need the same rule. Measured on c7c02fa, with the source's own file kept in the root at each of the 18 `assembly_root_paths()`: - `check` passes all 18. - These verify over a broken assembly: `contracts/spec-pin.yaml` and `contracts/code-pin.yaml` (DRIFTED, pins fail); `scripts/validate-pins.py`, `scripts/validate-manifest.py`, `scripts/repo_shape.py` and `contracts/repository-naming.yaml` (INVALID); `scripts/validate-repository-naming.py` (COMPLIANT, naming fails); `scripts/bootstrap.py` (COMPLIANT, and `make bootstrap` runs the source's own script). - `contracts/shape-pin.yaml` fails verification after both legs exist. - README.md, AGENTS.md, CLAUDE.md, .gitignore, .gitattributes, AGENTS-shape.md and the workflow come out COMPLIANT with the copy beside them. `Makefile` is rooted by default, and the follow-up merge is its designed exit. - `project.yaml` is the one path `plan`'s own question offered `root` for. The others are reached only by hand-editing a `contracts/` or `scripts/` answer to `root`. They are left for a follow-up issue (under Left out), not folded in here. - **A symlink at `project.yaml` is refused too.** On c7c02fa: - a link to the source's own file verified and was NOT A SHAPE ROOT; - a link to nothing had the manifest written through it, and came out COMPLIANT on Linux. A clone that makes no symlinks holds that link as a file naming its target, so it is refused anyway, and #183 refuses a link to nothing at a shape path regardless; - a directory `project.yaml/` verified and was NOT A SHAPE ROOT. - **A case twin `Project.yaml` kept in the root is refused, with the case aside.** It adopts COMPLIANT on Linux. On a disk that ignores case, `place` writes the manifest beside it. This is the trade #172 and #313 already make. **Evidence.** In this section and in the two below it, "this head" is 69c54c9, the branch's own commit and the one the review approved; main and #314 were merged into the branch afterwards (last paragraphs). - **Reproductions, on this head** (exits as printed): - #303 run 1: `check exit 1`, one `plan-empty-symlink` finding; `execute exit 2`, `REFUSED plan-empty-symlink`, `leg repositories left:` empty. - #303 run 2 (`core.symlinks=false`): the same finding and refusal; no leg, no verification. - Top-level `empty-link`, symlinks off and answered root, code or drop, and symlinks on and answered root or drop: each `check exit 1`, `execute exit 2`, no leg. - #305 root: `check exit 1`, one `plan-root-blocks-manifest` finding; `execute exit 2`, `leg repositories:` empty. - #305 code, spec and drop: `plan ok`, `adoption verified`, the root's `project.yaml` begins `schema_version: 1`, doctor COMPLIANT, `bootstrap ok`. - The plan's entry for `project.yaml` now reads `rule: ambiguous-project-yaml`, with no `(root)` in its question. - `project.yaml` as a link to a file, a link to nothing, a directory, and the twin `Project.yaml`: each `check exit 1` and `REFUSED plan-root-blocks-manifest`. - No row needs real GitHub or a cloud tool. - **New tests.** `tests/test_adopt_empty_symlinks.py` (28) and `tests/test_adopt_root_manifest.py` (27). - The symlinks are made with `git update-index --cacheinfo`, so no disk symlink is needed. The one test that checks a link out (a link given a target, end to end) is skipped on Windows by its marker, before it runs. - On c7c02fa: 37 failed, 18 passed. The 18 are the controls that must pass on both sides: a symlink with a target, the remediations adopting, `project.yaml` sent to a leg or dropped, a submodule there, other shape paths keeping their copy, and `release.yaml`'s question. - **One existing test changed.** `test_a_file_kept_at_a_path_the_shape_writes_is_not_a_finding` in `tests/test_adopt_root_shape_paths.py` held that a kept file at any shape path is no finding. It now holds that for every path but the manifest, which is `plan-root-blocks-manifest`. - **Mutation probes:** 14 of 14 hunks reverted one at a time, each killed by at least one new test. - **Targeted runs.** The adopt list and the doctor ran on a local state of this branch (7a22a21) whose files were this head's but for `tests/test_adopt_root_shape_paths.py`. That file was re-run on this head, so the adopt list is two runs, not one sum. - The 15 adopt files (`test_adopt_empty_symlinks`, `_root_manifest`, `_root_shape_paths`, `_leg_path_collisions`, `_execute_refusals`, `_plan`, `_e2e`, `_submodules`, `_verify`, `_mount_ignored`, `_ignored_shape_files`, `_entry_paths`, `_finding_spelling`, `_history_gitlinks`, `_spec_only`) on 7a22a21: 837 passed, 1 failed, 1 skipped (a MedxEHR clone the plan test needs). The one failure is the #313 test this head changes. - `tests/test_adopt_root_shape_paths.py` alone, on this head: 120 passed. - `tests/test_shape_doctor.py`, as its own stream: 242 passed, 3 skipped (PowerShell lines only a Windows host prints). - `tests/test_scaffold_leg_path_collisions.py`, `tests/test_repo_hygiene.py` and `tests/test_cli_reference.py`: 167 passed. - This head merged with origin/main at 87373a6, which brings `tests/conftest.py`'s new `git()` identity: the two new files, `tests/test_adopt_root_shape_paths.py` and the three above, 342 passed. **The review.** An independent opus review at 69c54c9 approved with nits: 0 blocker, 0 should, 8 nit. It reproduced every row of both issues on c7c02fa and on this head, and killed 6 of 6 mutants: two of the author's, re-run, and four of its own. Its targeted runs on this head merged with origin/main ab623dd (`git merge-tree` clean, with #349's session `gh` guard in place): - the new files, `tests/test_adopt_root_shape_paths.py`, `tests/test_scaffold_leg_path_collisions.py`, `tests/test_adopt_execute_refusals.py`, `tests/test_repo_hygiene.py` and `tests/test_cli_reference.py`: 379 passed; - the other adopt files, in two streams: 303 passed, 1 skipped (the MedxEHR clone), and 323 passed; - the clone-template, literal-names, template-values, plan-escapes, stdout-encoding and Windows-path tests, which cover #268 and #270: 266 passed, 2 skipped (an operator `user.name` set; Windows only); - `tests/test_shape_doctor.py`: 254 passed, 3 skipped (PowerShell lines only a Windows host prints). c7c02fa's own `tests/test_adopt_root_shape_paths.py` against this head's code gave 1 failed (the `project.yaml` row, `:191`) and 119 passed, so no other #313 behaviour changed. - **A cross-lane composition, with lane 1's #183.** On the tree merged with lane 1's `fix/adopt-root-kept-symlinks` (#183, 6c44591), `tests/test_adopt_root_symlinks.py::test_a_link_with_an_empty_target_is_fixed_in_the_source` fails: `AssertionError: assert 'plan-empty-symlink' == 'plan-root-symlink'`. An empty link at or above a shape path is reported by both findings, and `execute` refuses with the first, which is this branch's. The two remediations agree (fix it in the source). Whichever of the two lands second removes the duplicate report. The same file also fails 3 other tests on origin/main with #183 and without this branch (`[a-link-at-the-copy-itself]` and `test_the_materializer_refuses_before_it_writes`, two cases), because #313's `plan-root-blocks-shape-copy` and `materialize-copy-blocked` answer first; those are not this branch's. - **Two known nits, left for a later round.** - An empty link AT `project.yaml` (or at a #313 copy path) gets `plan-root-blocks-manifest`, whose fix offers `drop`, and `plan-empty-symlink`, which says `drop` is no fix. - `plan` notes every empty link (25 links print 25 NOTEs), where `check` names 20 and counts the rest. - The other nits are smaller. A symlink target that begins with a NUL byte passes `check` and fails like #303, and the issue's `_verify` bullet, are follow-up issues; the case twin `Project.yaml` is under Left out. The new tests' `SYMLINKS_OFF` replaces the Windows job's own `GIT_CONFIG_COUNT` settings (`core.autocrlf=false`, `core.longpaths=true`) for those child processes, which is harmless while none of them checks anything out. **Merge-tree, as of 2026-10-12T15:14Z.** `git merge-tree --write-tree --name-only` of this head (69c54c9), against heads fetched then; every sha below is the one it had then, and main has moved since (see the merge paragraph below). It is a textual merge only; the semantic meeting with #183 was tested by the review (the first item above). - origin/main: clean at 0f67f7b. That includes #316 (34ecbc2), #268 (e43fa95), #270 (f795cf7) and #245 (161c6c9), which have landed since this branch was cut. It was clean at 161c6c9 and at ab623dd, the review's merge, too. - Open PRs touching `adopt-project.py`: #184 (f416e13), #353 (dcbc37b), #356 (5a5011d), #371 (34b9500), #373 (5ac03de), #377 (c35c896), #378 (2da8691), #382 (7e55d00) and #384 (a96297e) are all clean. #356 and #382 conflict with origin/main alone (2 and 5 blocks, from #245's landing), not with this head. - #314 (4b8da3d) was NOT clean: one block, in `_entry_findings`' docstring, where each side adds a paragraph after the same one (this head's "THEN THE SOURCE'S EMPTY SYMLINKS (#303)" and #314's "A VALUE NO FILE CAN HOLD"). Keep both. It had been clean at f45edf4, and it had 3 blocks against origin/main alone since #245 landed, so it needed a merge of main in any case. - Lane 1's branches without a PR: `fix/adopt-late-failure-notice` (9f1778d), `fix/adopt-record-submodule-placement` (70f4d29) and `fix/adopt-plan-quote-yaml-keywords` (d38e143) were clean. `fix/adopt-plan-quote-yaml-keywords` was 3 blocks against this head at e337f44, and conflicts with origin/main alone in 4. - `fix/adopt-root-kept-symlinks` (6c44591, #183) conflicts in 3 blocks of `adopt-project.py`: the module docstring's `check` paragraph, `_entry_findings`' docstring and its return line. They are the same 3 that c7c02fa itself has with that branch, and this branch's one added docstring line sits inside the first. It has 5 blocks against origin/main alone. - Lane 5's `fix/adopt-pin-writes-pin-file` (d806e8f): clean. `docs/adopt-execute-exits` is #371, above. - When #183 lands, an empty symlink kept at or above a shape path is reported twice, by `plan-empty-symlink` and by #183's `plan-root-symlink` (the first review item above). A dangling link kept at `project.yaml` is reported twice too, by `plan-root-blocks-manifest` and `plan-root-symlink`, and an empty one three times. Whichever lands second should keep one. **Brought onto main past #314 (bbbb541).** Since this branch was cut from c7c02fa, main took #245 (relative `.gitmodules` urls), #270 (`plan-unwritable-value`), #371 (`execute` verifies before it exits on a refused `gh pr create`), #377 (`plan` asks about every path `execute` will not extract) and #314 (`plan-manifest-invalid`, 3ef059c), all in `adopt-project.py`. The merge of origin/main 3ef059c has ONE conflict, in `_entry_findings`' docstring, where this branch adds "THEN THE SOURCE'S EMPTY SYMLINKS (#303)" and #314 adds "A VALUE NO FILE CAN HOLD IS SAID BEFORE THE VALIDATOR'S WORD ON IT (#255 before #261)" after the same paragraph: both are kept, this branch's first, because it describes the head of the list and #314's its tail. Nothing in code conflicted. `check` and `execute` read one list, and it composes as each feature says: a source's unsafe paths (#196), its empty symlinks (#303), coverage, `_leg_findings` with `plan-root-blocks-manifest` (#305), #166's split, #270's `plan-unwritable-value`, #314's `plan-manifest-invalid`, then #245's relative urls last. So an empty symlink beside an invalid value is still refused wearing `plan-empty-symlink`, with no leg created; probed on the merged tree with an empty link plus `visibility: secret`, plus a line break in `visibility`, and with `project.yaml` answered `root` plus `visibility: secret`, each listing the findings in that order. No test expectation or order changed: the merged tree was rehearsed before #314 landed (69c54c9 + 45e01f4 + c6f441b) and is the same tree object as the pushed merge, and `git diff 3ef059c bbbb541` is this PR's five files and nothing else (+1004/-15). Targeted runs on bbbb541: the new files, `test_adopt_root_shape_paths`, `test_adopt_manifest_values`, `test_scaffold_leg_path_collisions`, `test_agents_md_codes` and `test_cli_reference` 418 passed; `test_shape_doctor` and #377's, #245's and #270's adopt tests 595 passed, 4 skipped (PowerShell lines only a Windows host prints; one operator `user.name`); the other adopt files 234 passed, 1 skipped (the MedxEHR clone) and the guards, literal-names, clone-template, plan-escapes, gitmodules, leg-paths, stdout and Windows-path tests 1101 passed, 1 skipped. **Left out.** - `_verify` and the materializer do not themselves refuse a root `project.yaml` that is not the shape's manifest. This is the issue's second bullet. Once `execute` refuses the plan before any leg, through the same list `check` prints, nothing reaches them with one. It is a follow-up issue. - A top-level file named `project.yaml` only once case is ignored (`Project.yaml`) is still asked `ambiguous-top-level-yaml`'s question, which offers `root`. `check` refuses that answer with the case aside, before any leg. - #171 (a failed clone exits 2 silently) is lane 1's PR #184. This PR only keeps the empty-symlink run from reaching that clone. - Follow-up issues proposed by this PR (two since filed): - the source's own file kept in the root at one of the other shape paths the root's tools read (the pins, the validators, `scripts/repo_shape.py`, the naming policy, `scripts/bootstrap.py`, `contracts/shape-pin.yaml`), each measured above; - a symlink whose target begins with a NUL byte (`\0spec.md`, size 8), which `plan-empty-symlink`'s size test does not see: it fails like #303 (filed as #388); - `_verify` asking whether the split's root `project.yaml` is the shape's manifest (filed as #389); - an entry git cannot check out where `execute` runs, other than an empty symlink or a NUL-led one (a target longer than the platform allows, say), which is still found only by the first clone, after both legs exist. #303 leaves this as a note. Reviewed adversarially at 69c54c9 (APPROVE WITH NITS: 0 blockers, 0 should-fixes, 8 nits). The review reproduced every row of both issues on c7c02fa and on that head, and killed 6 of 6 mutants. No fix round followed: the branch's own commit is as reviewed, and what was added after it is the merge described above. CI is green on Linux, macOS and Windows, with SonarCloud passing, at fce86fa (main 7a8b981 merged in). Its merge with main a73b4e9 passed the targeted set (the three adopt test files, `tests/test_shape_doctor.py` and `tests/test_scaffold_leg_path_collisions.py`): 597 passed, 3 skipped. Closes #303 Closes #305 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>



Lane: openreposhape-2 (openRepoShape-2)
Claim: #292 (comment)
Merged main after #270 landed
#270 landed on main as the squash f795cf7 (its final head was 3cee853), and this PR, stacked on it, now targets main. The branch carried an older copy of #270's commits, so a plain merge of main conflicts in
adopt-project.py. It took two merges: #270's final head 3cee853 (81df4e2, no conflict), then origin/main ab623dd (dcbc37b). The second conflicted inadopt-project.pyonly, and the whole file takes this branch's side: the squash's content of the file is 3cee853's (git diff 3cee853 f795cf7 -- adopt-project.py tests/test_adopt_template_values.pyis empty), which the first merge had brought in. At dcbc37b,git diff HEAD^1 HEADon those two files is empty,git diff --stat origin/main HEADlistsadopt-project.pyandtests/test_adopt_git_reads.pyonly, andgit merge-tree --write-tree origin/main HEADis rc 0. This PR's own change toadopt-project.pyis the same lines it was before the merges. Each count below says the head it was run at.Fix round (review of dcbc37b)
The review of dcbc37b found no P1 or P2, and four nits for this PR. They are fixed in two commits on top of dcbc37b (main is not merged again), head 96ff0c7:
344028e. Theadopt-no-orgsentence said "the source has nooriginto read an organisation from" of a source that has an origin whose URL is not UTF-8, which is the case this PR newly sends there (and of one whose URL ends in noorg/repo). It now reads "the source'soriginnames no organisation this tool can read: it has none, its URL is not UTF-8, or its URL ends in noorg/repo". The refusal, exit 2 andpass --org <org>remediation are the same.test_plan_without_org_refuses_a_source_that_names_nonetakes the new bytes and a third source,no-pair(an origin ofrepo.git).344028e. Deleting the movedargs.org = checked_value("--org", args.org)survived this file andtests/test_adopt_plan.py:plan --org=-evilexited 0 and wroteorg: "-evil",checksaidplan ok, and onlyexecuterefused it.test_plan_refuses_an_unsafe_org_that_is_passed[leading-dash|dash-only|space]pins exit 2,REFUSED unsafe-value: --org is '-evil'and no plan file.96ff0c7. The fouruser.nametests skipped in a checkout whose own.git/configsetsuser.name, because that outranks theGIT_CONFIG_GLOBALfile they wrote, and the_electorfix was then untested. On POSIX the name is now also given in git's command scope (GIT_CONFIG_COUNT,GIT_CONFIG_KEY_<n>,GIT_CONFIG_VALUE_<n>), which outranks every config file, appended to any count the environment already holds: the Windows job setsGIT_CONFIG_COUNT=2, and replacing it would dropcore.autocrlfandcore.longpaths. TheGIT_CONFIG_GLOBALfile stays where an environment variable cannot carry the byte (Windows); CI is the only Windows runner, so CI is what confirms that route is unchanged. Two tests pin the route on any machine: a checkout made intmp_pathwith a localuser.namestill reads the name given, and a count of 2 in the environment survives.Composes withtable, the twoLeft outitems, and the claim about which test files were run are re-measured at 96ff0c7.What
adopt-project.pyasks git two questions whose answers are bytes it did not write, and now reads both without dying on one that is not UTF-8:_electorreadsgit config user.name(asked when--elected-byis not given) withbinary=Trueand decodes it withsurrogateescape, assys.argvkeeps such a byte. The lone surrogate then meets the question adopt-project.py: a valueexecutewrites into a file is asked whether a file can hold it, inplan, incheckand inexecutebefore any leg exists, so a byte that is not UTF-8 in--elected-by,--referenceor a local--sourcedirectory no longer pushes both legs beforewrite_lfraises (#255) #270 asks of every valueexecutewrites, andplanrefuses it asadopt-unwritable-value, exit 2, before any plan is written:_remote_repositorycatchesUnicodeDecodeErrorbesideRefusaland returns None, as it does for a source with no origin: such a URL names noorg/repo.plantakes--organd recordsrepository: null;checkandexecutename the source by its path and adopt it._checked_plan_inputsasksadopt-no-orgbeforechecked_value. Since v0.2 (v0.2: adopt an existing repository in place, reuse an empty root, scaffold a declared descendant #5) theor "-"stand-in for a missing org reachedchecked_valuefirst, which refused--org is '-', a value nobody passed, soadopt-no-orgwas never reached. The issue's "What it should do" 1 assumed it was; now it is, for a source with no origin, for one whose origin is not UTF-8, and for one whose URL ends in noorg/repo. The refusal says each of the three, because it is asked of a source that has an origin as often as of one that has none (the fix round's N1)._refuse_an_unwritable_clone_url's docstring said a UTF-8 clone of a source whose path is not UTF-8, passed with--source, raised in_remote_repository; it now says that route adopts too.git_outinscripts/repo_shape.pyis untouched: it is a pinned copy, and a change there would move every project's shape pin (the issue's point 3).Why
git keeps a config value and a remote URL as the bytes it was given, so a repository can hold either without anyone having done anything wrong: a name saved in a legacy code page, a clone of a directory named in one. Both reads decoded strictly through
git_out, andUnicodeDecodeErroris aValueError, not aRefusal, somaindid not catch it: the run ended in a traceback, exit 1, naming neither the git setting nor the flag (--elected-by,--org) that gets past it. Reproduced on #270's head before it landed (edfa7c6, then 72bb869) and again on main ab623dd (local only):user.name=Jos\xe9, no--elected-byplanUnicodeDecodeError ... byte 0xe9 in position 3from_elector, no planadopt-unwritable-valueas above, no planuser.name(control)planelected_by: Joseuser.name,--elected-by 'T H'(control)plan\xe9,--orgplan_remote_repositoryrepository: null--orgplanadopt-no-org, "pass --org "--orgplanunsafe-value: --org is '-'adopt-no-org\xe9, a plan written earliercheckplan okexecute --yes --local-remote-diradoption verifiedcaf\xe9/,execute --source <clone>executeadoption verifiedThe issue said
_elector"should not decode witherrors="surrogateescape"and carry on", because the surrogate would be recorded andexecutewould die after pushing both legs. On this branch it does not carry on: #270'sunwritable_valuesrefuses it in_checked_plan_inputs, right after_electorreturns, before the plan is written, with the setting and the flag named. That is the refusal the issue asked for, and it is one sentence with every other valueplancannot write.Tests
tests/test_adopt_git_reads.py, new, 17 tests. It reusestest_adopt_template_values.py's runners (strict stdout, bytes, every run read for a traceback).user.namecomes from aGIT_CONFIG_GLOBALfile written withwrite_bytesand, on POSIX, from git's command scope as well (command_scope, the fix round's N2), so the four tests that use it run in a checkout whose own config setsuser.nametoo; the origin URL is written into the source's.git/configas bytes, so it runs on every OS; each skips where git does not give the bytes back as written.test_plan_refuses_a_user_name_that_is_not_utf8: exit 2, the refusal naminggit config user.nameand--elected-by, no traceback, no plan.test_plan_records_a_user_name_that_is_utf8[ascii|utf8]: controls, recorded as itself.test_elected_by_given_adopts_under_a_user_name_that_is_not_utf8: control,plan,checkandexecuteunder the Latin-1 config with the flag,adoption verified.test_plan_reads_an_origin_that_is_not_utf8_as_none[not-utf8|ascii]:repository: null; the ASCII URL's control readscafe/repo.test_plan_without_org_refuses_a_source_that_names_none[not-utf8|no-origin|no-pair]:adopt-no-org, with one sentence for the three, notunsafe-value.test_plan_refuses_an_unsafe_org_that_is_passed[leading-dash|dash-only|space]:--org=-evil,--org=-and--org='my org'exit 2 asunsafe-value, naming--org, with no plan written.test_plan_without_org_takes_the_org_an_ascii_origin_names: control,org: cafe.test_check_and_execute_adopt_a_source_whose_origin_is_not_utf8:plan ok,adoption verified.test_a_utf8_clone_of_a_source_whose_path_is_not_utf8_adopts: adopt-project.py: a valueexecutewrites into a file is asked whether a file can hold it, inplan, incheckand inexecutebefore any leg exists, so a byte that is not UTF-8 in--elected-by,--referenceor a local--sourcedirectory no longer pushes both legs beforewrite_lfraises (#255) #270's clone-URL refusal, thenexecute --source <UTF-8 clone>adopts (POSIX; skipped where no directory holds the byte 0xE9).test_the_user_name_given_outranks_a_checkouts_ownandtest_the_user_name_given_is_appended_to_the_config_the_environment_holds(POSIX): the N2 route, on any machine.Against main's code (this file at 96ff0c7 copied into a detached worktree of origin/main 28bdc84, and again of 92e29d9, the same 7 failing): 7 failed, 10 passed. The 7 fail for the issue's reason: 5 on the
UnicodeDecodeErrortraceback (_electoronce,_remote_repositoryfour times), and[no-origin]and[no-pair]onREFUSED unsafe-value: --org is '-'. The 10 that pass are the controls, the three--orgtests (that check exists on main) and the twouser.nameroute tests. The 11-test file was 6 failed, 5 passed against edfa7c6, 72bb869 and ab623dd; the one more failure isno-pair.The N2 case, emulated. This worktree's own config has no
user.name, and the shared checkout's[user]section is gone, so auser.namethat outranksGIT_CONFIG_GLOBALwas given from outside, in the environment (GIT_CONFIG_COUNT=1,GIT_CONFIG_KEY_0=user.name,GIT_CONFIG_VALUE_0=Outer; command scope outranks a local config, so this skips the old helper exactly as a localuser.namedoes). dcbc37b's file under it: 7 passed, 4 skipped. This file under it: 17 passed, 0 skipped; plain: 17 passed; in a scratch clone whose own config sets nouser.name: 17 passed; under the Windows job's own two entries plus the name: 17 passed.The N2 survival proof, in a detached scratch worktree of dcbc37b whose only change is
_electordecoding strictly (name.decode("utf-8")): dcbc37b's own test file under the outeruser.nameruns 7 passed, 4 skipped, 0 failed, so the mutant survives: the four tests that would kill it skip, which is the state N2 removes. The same file without the outer name fails 1 (test_plan_refuses_a_user_name_that_is_not_utf8), so the skip is the cause. This PR's file on the same mutated code under the outer name fails 4: that test, and the threeadopt-no-orgrows that fail only because dcbc37b's code still has the old sentence (N1).Mutants, at 96ff0c7 in a scratch clone, each run with that outer
user.namestanding in for a local one, one per decisive line, each caught except where it says so:_electorreturnsname.decode("utf-8")test_plan_refuses_a_user_name_that_is_not_utf8(1 without the outer name too)replacedecode("utf-8", "replace")ignoredecode("utf-8", "ignore").strip()_electordrops.strip()test_plan_records_a_user_name_that_is_utf8[ascii]and[utf8](3 plain)_remote_repositorycatchesRefusalonlyexcept Refusal:test_plan_reads_an_origin_that_is_not_utf8_as_none[not-utf8],test_plan_without_org_refuses_a_source_that_names_none[not-utf8],test_check_and_execute_adopt_a_source_whose_origin_is_not_utf8,test_a_utf8_clone_of_a_source_whose_path_is_not_utf8_adoptsor "-"stand-in backargs.org or ... .split("/")[0] or "-"adopt-no-orgrowschecked_value("--org", ...)deleted (N3)test_plan_refuses_an_unsafe_org_that_is_passedrows (run withtests/test_adopt_plan.py: 3 failed, 70 passed, 1 skipped)adopt-no-orgsentence (N1)adopt-no-orgrowsglobal_user_nameremovedtest_the_user_name_given_is_appended_to_the_config_the_environment_holds) and 5 skippedTargeted runs, git-filter-repo 2.47.0 on PATH, each row at the head its last column names (cae1bf5 before the merges, dcbc37b after them, 96ff0c7 now; the dcbc37b rows of 14 files were run by the lane's review of dcbc37b):
tests/test_adopt_git_reads.py(new)tests/test_adopt_template_values.pytests/test_adopt_plan.py:393, needs aSHAPE_MEDXEHR_CLONEcheckout)tests/test_git_output_decoding.pytests/test_scaffold_leg_path_collisions.pytests/test_cli_reference.pytests/test_repo_hygiene.pytests/test_shape_doctor.pytests/test_adopt_literal_names.pytests/test_adopt_clone_template.pytests/test_windows_paths.pytests/test_adopt_execute_refusals.pytests/test_adopt_e2e.pytests/test_adopt_verify.pytests/test_adopt_spec_only.pytests/test_adopt_entry_paths.pytests/test_adopt_history_gitlinks.pytests/test_adopt_leg_path_collisions.pytests/test_adopt_plan_escapes.pytests/test_adopt_submodules.pytests/test_adopt_finding_spelling.pytests/test_adopt_root_shape_paths.pytests/test_adopt_leg_paths.pytests/test_adopt_gitmodules_values.pytests/test_adopt_ignored_shape_files.pytests/test_adopt_mount_ignored.pyscripts/render-cli-reference.py --checkpasses at 96ff0c7; no--helptext changed. dcbc37b holds 20tests/test_adopt_*.pyfiles, where cae1bf5 held 18:test_adopt_clone_template.pyandtest_adopt_literal_names.pyarrived with the merges, and were first run against this PR's code by the review of dcbc37b. Every one of the 20 was run at cae1bf5 or dcbc37b, and the six marked cae1bf5 were not re-run since. The two commits since dcbc37b change one sentence inadopt-project.pyand one test file, and no other file undertests/names the sentence (git grep -e 'to read an organisation' -e 'names no organisation'outside those two finds onlytest_setup_project_py.pyandtest_setup_sh.py, which are aboutsetup-project.py).Composes with
git merge-tree --write-treeat 96ff0c7 against every open head at 15:38Z, in this repository's object store (origin/main 28bdc84, a983e6a since, and 92e29d9 at 16:41Z, not fetched by this writer):adopt-project.pyand four adopt test files)adopt-project.py: #394 48bdbf9, #387 69c54c9, #384 a96297e, #382 7e55d00, #378 2da8691, #377 c35c896, #373 5ac03de (lane 3), #371 34b9500 (lane 5), #356 5a5011d (lane-matefix/adopt-check-hand-edited-plan), #314 a5438a5, #184 f416e13adopt-project.py(#395, #390, #381, #380, #379, #376, #375, #374, #357, #355, #354, #352, #347, #346, #343, #267, #266)fix/doctor-manifest-kinds-unreadable-pin, based onfix/doctor-root-shape-pin-unreadable)shape-doctor.pyshape-doctor.py, so it adds none.shape-doctor.pyThe merge of 96ff0c7 and 92e29d9 (
git merge --no-commitin a scratch tree: clean, tree b2b4b7f; its diff against 92e29d9 isadopt-project.pyandtests/test_adopt_git_reads.pyonly) rantest_adopt_git_reads.py,test_adopt_plan.py,test_adopt_template_values.py,test_adopt_entry_paths.py,test_adopt_plan_escapes.py,test_adopt_literal_names.py,test_adopt_github_exits.pyandtest_scaffold_leg_path_collisions.py: 418 passed, 1 skipped (test_adopt_plan.py:393,SHAPE_MEDXEHR_CLONEunset).#356 and #382 conflict with origin/main 28bdc84 alone (main's newer commits on
adopt-project.py) and merge with this branch; that is theirs against main, not something this branch adds or lifts. At dcbc37b the review merged #356 5a5011d into this branch (ce952fb) and rantest_adopt_git_reads.pyandtest_adopt_execute_refusals.pyon the result: 92 passed. That was not re-run at 96ff0c7. None of the 11 heads onadopt-project.pypins the oldadopt-no-orgsentence in a test:git grepfor it finds it inadopt-project.pyalone at each of them.One meaning to keep in view when #314 (#261, a blank or malformed
--elected-by) and this land together: both ask about the electorplantakes fromgit config user.name. They merge textually (rc 0); on this branch_checked_plan_inputsasks #270's question of the elector right after_electorreturns.Left out
scripts/shape_advisory.pyrepository_namesreads the same origin URL and catches(Refusal, OSError)only. Under a URL that is not UTF-8 it raises,shape_advisory.read()returnsnot-answered, and_print_triad_advisoryhas nothing to print:planandcheckprint no Triad advisory for that source, and nothing raises up to the caller to say so. A leg clone namedNorthwind-specloses its leg-clone NOTE the same way.shape-doctor.pyreads the URL at two more places (origin_url,origin_name, each catching(Refusal, OSError)), andshape-doctor.py --rootends in aUnicodeDecodeErrortraceback on such a source, as on main. These are other functions in other tools, not in this issue; a follow-up.scaffold-project.py_elector(:605, the read at:618) readsgit config user.namethe same strict way: with no--elected-byand noGIT_AUTHOR_NAME, a Latin-1 name endsscaffoldin the sameUnicodeDecodeErrortraceback, exit 1, nothing created. That is another tool, and the open PR scaffold-project.py:--elected-by,--reference,--nameand the elector's fallback are refused before anything exists when no file can hold them, so a byte that is not UTF-8 no longer creates repositories before a traceback (#282) #351 (closes scaffold-project.py writes--elected-by,--referenceand--namebetween literal double quotes with no escaping and asks none of them whether a file can hold it, so a line break leaves a project.yaml its own validators refuse, a backslash is recorded as another character (CORP\nickas CORP, a line feed, ick), and a byte that is not UTF-8 dies inwrite_lfafter the legs are pushed #282) already takes it.scripts/family.py_creator_name(:695) readsgit config user.namethe same way:family.py initwith a Latin-1 name and no--created-byends in the traceback, nothing created. Another tool; a follow-up._default_branchand the othergit_outcallers ofadopt-project.py(a branch name that is not UTF-8:plan,checkandexecutestill end in a traceback fromSource.openfor a source whoseHEADis oncaf\xe9) are the issue's "separate question", and are not asked here. The same on main.checked_valuerefuses (café,my repos,caf%C3%A9,-evil) is refused as--org is 'café', a flag nobody passed, and the remediation never says to pass--org. The input is UTF-8, not this issue's, and it is the same on main; a follow-up.git_outitself, in the pinnedscripts/repo_shape.py.Closes #292
🤖 Generated with Claude Code
Summary by Sourcery
Make project adoption robust when Git configuration or origin data contains non-UTF-8 bytes.
Bug Fixes:
Enhancements:
Tests: