Repository navigation
adopt-project.py: plan's follow-up git grep takes a spec directory as its pattern and each code entry as a literal path, so a spec directory named -notes/ or a code entry named :cache/ no longer loses its follow-up, a code entry named */ no longer names a spec-leg file, a name outside ASCII is spelled instead of C-quoted, and a spec directory named -O<cmd>/ no longer runs a command (#216) - #228
Conversation
…y as its pattern and each code entry as a literal path, so a spec directory named `-notes/` or a code entry named `:cache/` no longer loses its follow-up, a code entry named `*/` no longer names a spec-leg file, a name outside ASCII is spelled instead of C-quoted, and a spec directory named `-O<cmd>/` no longer runs a command (#216) `cross_leg_references` ran `git grep -l -I -F <spec dir> <commit> -- <code entries>` once per directory the plan sends to the spec leg, and wrote each file it listed into the plan's `follow_ups:`. Both inputs are names the source chose, and git read each as something else. It now runs `git --literal-pathspecs grep --no-color -z -l -I -F -e <spec dir> <commit> -- <code entries>`: - `-e`: the spec directory is the pattern wherever it sits. `-notes/` was a usage error (exit 129) whose empty stdout read as "no file names it", and `-O<cmd>/` was `--open-files-in-pager`, which git runs through a shell: measured on main a5b09c3, a local source whose index holds a file carrying the planned commit's hash got a `PWNED` file from `plan`. Now it gets the follow-up and nothing runs. - `--literal-pathspecs`: each code entry is the path it spells. `:cache` was the pathspec `cache`, so `:cache/a.py` got no follow-up, and `*` matched every path, so the spec leg's own `notes/two.md` was written up as `code/notes/two.md`. - `-z`: each record is split at the first `:` after the hex commit and decoded as `Source.tree` decodes a name, and the hit is spelled as a question spells a path (`\n`, `\xe9`), one line of UTF-8. `-l` had written `code/"src/\303\234bersicht.py"`. - `--no-color`: `color.ui=always` in the source's own config put `\x1b[m` into the plan. - A grep that exits past 1 is said, as a follow-up naming the exit, git's words on one line and what to find by hand, instead of being read as no hits. `_stage_the_split`'s forced add is `git --literal-pathspecs add -f`. A shape file written through a symlink the plan kept in the root lands at the link's target, a source name: `:ci/...` was magic and died with `fatal: pathspec ... did not match any files` after both legs were pushed, `:!ci/...` was an exclude that left an ignored workflow out of the split (`adopt-shape-file-missing`), and `ga[0]/...` was a glob that also forced an ignored `ga0/...`. On main a5b09c3 `execute` refuses those plan paths as `unsafe-value` before anything is made, so the hunk is reached once #196 (fix/adopt-check-unsafe-path) admits them; #196 changes `git rm` and filter-repo's path list but not this add. Measured end to end: #196 alone exits 2 after both legs are pushed, #196 with this change says `adoption verified`. Unchanged: `plan` over conftest's SYNTHETIC_TREE plus four plain files naming `contracts/` and `docs/` is byte-identical to main a5b09c3 in exit, stdout, stderr and plan file. The `git add -A -- .` before the forced add is as it was. No `--help` text changes. tests/test_adopt_literal_names.py: 13 cases, 11 failing on main a5b09c3 and the two controls (`notes/`, `s?c/`) passing there. The cases whose names NTFS cannot hold, and the symlink cases, skip on Windows. tests/test_adopt_ignored_shape_files.py's recorded argv gains `--literal-pathspecs`. Left out: the spec directory's own name in a follow-up is written raw, so one holding a line break or a byte outside UTF-8 is not spelled there. Lane: openreposhape-3 (openRepoShape-3) 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 5 hours by commenting |
|
(lane openRepoShape-3, 2026-10-11T19:03Z) @codex review |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Reviewer's GuideThe PR hardens Sequence diagram for literal follow-up discoverysequenceDiagram
participant Plan
participant Git
participant Source
Plan->>Git: grep --no-color -z -l -I -F -e spec_dir commit -- code_paths
Git-->>Plan: NUL-delimited commit:path hits
Plan->>Source: _git_path(record)
Source-->>Plan: decoded path
Plan->>Plan: _spelled_character(hit)
Plan-->>Plan: report follow-up or grep failure
Flow diagram for literal split stagingflowchart LR
A[Materialized shape files] --> B[_where_it_landed]
B --> C[git --literal-pathspecs add -f]
C --> D[Staged split preserves source-chosen paths]
C --> E[Ignored files are force-added]
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
…ine feed to a human, and spells the spec directory in its follow-ups, so `a<LF>b/` is no longer searched as `a` OR `b/` and written up as named by a file that never mentions it (#216) Fix round 1 on #228 (independent review rv-adopt-a, finding 2). - `git grep -F -e <pattern>` reads a pattern holding a line feed as one pattern per line. Measured on main a5b09c3 and on 115f012 alike: a spec directory `a<LF>b/` beside `src/other.py`, whose only text is `a = 1`, got the follow-up `code/src/other.py names ...`, a false follow-up like the one `*` gave. `cross_leg_references` now asks no grep for such a name and writes the by-hand follow-up it already writes for a failed grep, saying why. The two share `_search_by_hand`. - The spec directory's own name in every follow-up is spelled the way the hit beside it is (`_spelled_character`), so a line feed in it no longer splits the line. An ordinary name is unchanged. tests/test_adopt_literal_names.py gains the line-feed case (posix-only, built in the tree and never on the disk), failing on main a5b09c3 and on 115f012 with the false follow-up. Lane: openreposhape-3 (openRepoShape-3) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-3, 2026-10-11T19:35Z) fix round 1 (independent review, codex is out of quota): |
|
(lane openRepoShape-2, 2026-10-11T19:41:55Z) Lane openRepoShape-2 reviewed PR #228 at 115f012, read-only, in its own detached worktrees. Nothing was authored and this lane holds no claim. MERGEABLE WITH NITS: it must take main 54aa69a (#173) first. The merge is clean and its tests pass (59 passed). No finding is above nit. Update at 2026-10-11T19:41:55Z: the branch has since moved to f0d4334, one commit: Findings:
Tests:
Merges (
The full report is |
|
…rences` rows, which exempt nothing now that #196 and #216 are on main The branch merged main at 346079d (#228, issue #216; #196 landed earlier as 6f45bf9). The two rows were kept "until" those landed: - `scripts/shape_materialize.py` `env_commit`: its `subprocess.run` names `text=True, encoding="utf-8", errors="replace"`, so the default rule is met and the row exempted nothing (since #231). - `adopt-project.py` `cross_leg_references`: #216 made its `git grep` read BYTES (`capture_output=True`, no `text=`, the record split on NUL and each path decoded by hand), so it is no longer a text-mode call at all. Measured on c7c02fa the row was needed (`adopt-project.py:1080 asks for text and does not name ['encoding', 'errors']` without it); on 346079d the guard finds no text-mode call in that function. EXEMPT keeps the two `gh api` digest rows (`pin_digest_from_gh`, `tree_digest_from_gh`: UTF-8 yes, `replace` no), which still exempt `errors`. With both rows gone the guard flags 0 offenders over the shipped Python at 346079d: 51 spawner calls, 32 text-mode (33 before #216). Lane: openreposhape-3 (openRepoShape-3) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Main brings two commits since 71b58a1: - c7c02fa (#299 via #315): family.py's `bump` and `remove` refuse `member-path`; - 346079d (#216 via #228): `plan`'s follow-up `git grep` takes each name literally, and `_stage_the_split` adds with `git --literal-pathspecs`. It merged with no conflict. adopt-project.py and tests/test_adopt_ignored_shape_files.py changed on both sides. In each, the merge's diff against this branch is main's own diff (72 and 4 lines), and its diff against main is this branch's own diff (784 and 16 lines). Every other file is one side's. Main's hunks in adopt-project.py, by where they land: 1-7. `cross_leg_references`: names are literal (#216); the spec directory and each hit are spelled; `git --literal-pathspecs grep`; 8. after it, the new `_search_by_hand`; 9-10. `_stage_the_split`: its docstring, and `git --literal-pathspecs add -f`. Main's hunk in tests/test_adopt_ignored_shape_files.py is in `test_the_forced_paths_are_the_written_ones_in_posix_spelling`, which this branch does not change: the forced add is `git --literal-pathspecs add -f`. This branch changes none of these definitions. `_stage_the_split` runs inside `_commit_the_split`, after both legs are pushed, where this branch already reports any failure with the notice. No top-level name is bound twice in adopt-project.py, scaffold-project.py, scripts/repo_shape.py, scripts/shape_materialize.py, scripts/family.py or tests/conftest.py. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, #253 via #264, #302 via #318, #288 via #309, #279 via #310, #306 via #311, #334 via #336, #239 via #269, #243 via #268, #321 via #335) into fix/adopt-record-submodule-placement Brings in the twelve commits from c7c02fa to a1b3a4b on top of 71b58a1, merged by sha. No rebase. Among the files this branch changes: 346079d (#216) and e43fa95 (#243) change adopt-project.py; 34ecbc2 (#260) changes scripts/shape_materialize.py; 87373a6 (#306) changes shape-doctor.py and tests/test_shape_doctor.py; 7491530 (#288) changes AGENTS.md and tests/test_repo_hygiene.py. Two conflicts, each resolved keeping both sides: - adopt-project.py, the module docstring's `execute` paragraph. #243 ends "the root's only submodules are the two leg mounts, each at the commit pushed to its leg."; this branch follows "two leg mounts." with "The split's `project.yaml` records the leg each source submodule kept in a leg was answered into, under `submodules:`, for `shape-doctor.py` to check it against." The merge has #243's sentence, then this branch's, re-wrapped to the paragraph's width. The module docstring is not in `--help` (docs/cli.md holds neither sentence). - tests/test_repo_hygiene.py, `test_agents_md_is_short_enough_to_be_read`. Each side moved the AGENTS.md cap from 464 and wrote a paragraph saying why: #288 to 477, for thirteen lines, and this branch to 472, for eight. AGENTS.md itself merged cleanly and holds both: 485 lines, main's 477 plus this branch's +9 -1. The merge keeps #288's paragraph word for word, then this branch's, renumbered "2026-10-12: 477 -> 485" because it now follows #288's, and asserts `<= 485`. `git merge-tree --write-tree` of the two parents gives the same tree in every file but these two. Checked: - adopt-project.py: `_commit_the_split` was changed by both sides, in hunks that do not touch. #243 clones the assembly with `--template=`; this branch builds `append` with the `submodules:` record. The merged function holds both, and each side's text is unchanged. Every other definition one side changed is that side's text. - shape-doctor.py: #306 changes `validator_row` and `run_validator`. This branch's placement functions, F1's `gitmodules_the_record_answers` among them, are this branch's text. - No top-level name is bound twice in adopt-project.py, scaffold-project.py, scripts/repo_shape.py, scripts/shape_materialize.py, shape-doctor.py, validate-manifest.py, update-shape.py, scripts/family.py or scripts/bump-leg.py. Each merged module's names are the base's plus what either side added. - AGENTS.md is main's plus exactly this branch's changed lines. - The branch's diff against main is #189 alone: 12 files, +1694 -157. That is +1670 at 27d039b, plus F1's fix and test (+23), plus the one line the docstring re-wrap adds. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…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) **What #282 still gets wrong on main (c7c02fa).** #284 (b30595c) spells each of the three quoted placeholders in `project.yaml` with `repo_shape.double_quoted`. So the manifest now reads back whatever was typed. The README, each `AGENTS.md` and each first commit's message still take the value as it stands. Every row of the issue was re-run on c7c02fa with local bare remotes only: 76 cases across `--elected-by`, `--reference`, `--name`, `GIT_AUTHOR_NAME` and `git config user.name` (given as `GIT_CONFIG_COUNT` config). In every case that scaffolded, the project's reader and PyYAML 6.0.3 read the manifest back as typed (0 misreads, 0 raises). What was still wrong: - **A byte that is not UTF-8.** The scaffold exits 1 with a `UnicodeEncodeError` traceback from `write_lf`. Through `--elected-by`, `--reference` or `GIT_AUTHOR_NAME`, both legs are pushed by then and the assembly root is created empty. Through `--name`, all three repositories are created first. Through `git config user.name` it raises `UnicodeDecodeError` from `git_out`'s strict decode in `_elector`. That one happens under `--dry-run` too, before anything is created. The issue does not list this case. - **A line break: LF, CR LF, U+0085, U+2028, U+2029.** The scaffold exits 0. The value goes raw into the README, which gains a line. Through `--name` it also goes raw into the root `AGENTS.md` (2 extra lines) and each leg's `AGENTS.md` (1). With LF or CR LF, each commit message's first line becomes `Seed the spec leg of Ada`; U+0085, U+2028 and U+2029 sit in that line raw. The `manifest ok:` line that the project's own `validate-manifest.py` prints is split as well. - **A control character (U+0001, ESC, DEL, TAB), or a bidi embedding, override or isolate (U+202E, U+2066/U+2069).** The scaffold exits 0, and the value is raw in the README (and, for `--name`, in each `AGENTS.md` and each commit message). **The change.** - `scaffold-project.py` `_identity` asks `unwritable_reason` about the three values, beside `election_date`. Any value that fails is refused as `scaffold-unwritable-value`. The refusal is one sentence per value: the flag, the value as Python's `repr` spells it, the files it is written into, and why. All failing values go into one refusal. It comes before a work directory or a remote exists, and `--dry-run` refuses the same. The only process that can run first is the read-only `git config user.name` that finds the elector when neither `--elected-by` nor `GIT_AUTHOR_NAME` gives one: ``` REFUSED scaffold-unwritable-value: --name is 'Jos\udce9', which the scaffold writes into the assembly root's `project.yaml` and `README.md`, each repository's `AGENTS.md` and each first commit's message, and it holds a byte that is not UTF-8. ``` An elector the flag did not supply is named by its source: `--elected-by (not given, so GIT_AUTHOR_NAME)` or ``--elected-by (not given, so `git config user.name`)`` (`_elector_named`, in `_elector`'s order). The second is the wording #270's `plan` uses. - `_elector` decodes `git config user.name` with `surrogateescape`, the way argv and the environment are decoded. A byte that is not UTF-8 there is then refused by name, not raised. `adopt-project.py` already reads git with this pattern (`git_out(..., binary=True).decode("utf-8", "surrogateescape")`). - `scripts/shape_materialize.py` gains `BIDI_CONTROLS` and `unwritable_reason`: see "Composition with PR #270" below. **Each row of the issue.** | row | `project.yaml` | README / `AGENTS.md` / commit messages | where it stands | |---|---|---|---| | a backslash (`CORP\nick`, also through `GIT_AUTHOR_NAME` and `git config user.name`; `C:\docs\notes\new.md`) | fixed on main by b30595c (#284): `"CORP\\nick"`, read back exactly | raw, as typed, on its own line (Markdown keeps a backslash before a letter literal, and the path is inside backticks) | fixed on main by b30595c | | a double quote (`Joe "JJ" Smith`) | fixed on main by b30595c: `"Joe \"JJ\" Smith"`, read back exactly | raw, as typed | fixed on main by b30595c | | a line break: LF, CR LF, U+0085, U+2028, U+2029 | parses on main since b30595c (escaped, read back exactly) | split on main | **fixed by this branch**: refused | | a byte that is not UTF-8 (all five ways in) | escaped on main since b30595c; not where it failed | traceback on main: `write_lf` raised on the root README or, for `--name`, the spec leg's `AGENTS.md`; `git config user.name` raised in `git_out` | **fixed by this branch**: refused | | a control character: U+0001, ESC, DEL (TAB too) | PyYAML's `ReaderError` fixed on main by b30595c (escaped) | raw on main | **fixed by this branch**: refused | | a bidi control: U+202E, U+2066, U+2069 | escaped on main since b30595c | raw on main (`README.md:9`) | **fixed by this branch**: refused | No row is left open. On this head, the 60 cases with an unwritable value exit 2 under both `--dry-run` and a real run. Each prints one `scaffold-unwritable-value` refusal and makes no bare remote, with 0 tracebacks. The other 16 cases scaffold, exit 0: the backslash, the Windows path, the double quote, and a Persian name joined with U+200C ZERO WIDTH NON-JOINER. Each reads back exactly, and the README gains no line. **Files outside YAML.** `README.md` (`:1` name, `:9` elector, `:10` reference in backticks), the root `AGENTS.md` (`:3`, `:13`), each leg's `AGENTS.md` (`:1`), and the three first commits' messages take the value raw. A backslash and a double quote break none of them: the file holds the typed text, and the line count is unchanged. Every other row is now refused before any of them is written. `--name` also goes into the GitHub description via `gh repo create`, which is not run here. **Composition with #270 (lane 2, #255), now landed as f795cf7.** #270 added `unwritable_reason` and `BIDI_CONTROLS` to `scripts/shape_materialize.py`, and this branch adds the same hunk byte for byte at the same place (after `is_trailer`). The merge of this branch with main ab623dd (e5804e0, made by `gh pr update-branch`) is clean, and its `scripts/shape_materialize.py` holds one `def unwritable_reason` and one `BIDI_CONTROLS`. The two tools refuse the same values in the same words, and the two comments in `scaffold-project.py` that say `adopt-project.py` asks the same question are now true. **Composition with PR #314 (lane 5, `fix/scaffold-manifest-values`).** No duplication. #314 asks the project's reader and `validate-manifest.py` about the rendered manifest, after the values table is built. This branch asks whether a value is text a file can hold, earlier, in `_identity`. Each refuses values the other passes: a blank `--elected-by ' '` is #314's, and `Ada\nLovelace` is this branch's. The merge with ab66bc0 is clean. On the merged tree, #314's own tests fail in 5 places, all on a value both PRs refuse: `--elected-by` of a bare TAB, `\x0b`, `\x1c` or U+2028, which is blank to the validator and unwritable here. This branch refuses those first, as `scaffold-unwritable-value`, where #314's tests expect `scaffold-manifest-invalid` or `scaffold-manifest-`. The failing tests are `test_a_value_the_validator_refuses_is_refused_before_anything_is_made[elected-by-tab]`, `test_a_dry_run_refuses_what_the_validator_refuses[elected-by-tab]` and `test_a_blank_that_is_also_a_line_break_is_refused_too[\x0b|\x1c|\u2028]`. Whichever of the two lands second applies a four-line patch to `tests/test_scaffold_manifest_values.py` (lane 5 holds it), which expects `scaffold-unwritable-value` for those four values. With it applied, the merged tree passes: see Evidence. **Merge-tree** (`git merge-tree --write-tree HEAD <ref>`, fetched 2026-10-12): | ref | tip | result | |---|---|---| | origin/main | c7c02fa, and 346079d (#228 landed since: adopt files only) | clean | | PR #270 | 72bb869 | clean, one `unwritable_reason` | | PR #314 | ab66bc0 | clean (5 of #314's tests need the patch above) | | PR #309 (lane 4) | 2ea1717 | clean | | PR #316 (lane 5, `render` single pass) | 9469148 | clean | | PR #266 (lane 2) | b2b42f8 | clean | | lane 1 `fix/adopt-record-submodule-placement` | 3e86200 | clean | | lane 1 `fix/adopt-late-failure-notice` | 918e8f2 | clean | | lane 1 `fix/adopt-root-kept-symlinks` | 6c44591 | CONFLICT in `adopt-project.py`, which this branch does not touch; the branch conflicts with origin/main alone the same way | | lane 1 `fix/adopt-plan-quote-yaml-keywords` | e337f44 | the same: CONFLICT in `adopt-project.py`, also against origin/main alone | | lane 5 siblings `fix/pin-source-env-collisions` c32a228, `fix/family-init-values` 14e2d68, `fix/yaml-flow-brackets-surrogates` 38738cf, `docs/adopt-execute-exits` d3f71d6 | | clean | | lane 2 local `fix/scaffold-tracking-branch-ref-format` (#283) | a03abcf | clean | **Evidence.** - New `tests/test_scaffold_free_text_values.py`, 113 cases: - each of the 12 unwritable values through each flag, dry and real (72), under a guard that fails the test if any process starts; - all three bad values named in one refusal; - each value through `GIT_AUTHOR_NAME` (12), with no process started; - `git config user.name` with LF, U+202E, ESC and a byte that is not UTF-8, dry and real (8). The last is skipped on Windows, whose environment is UTF-16; - the flag winning over the fallbacks; - 15 dry runs that must not refuse; - one real scaffold of a backslash, a quote and U+200C, through `--name`, `--reference` and `GIT_AUTHOR_NAME` (the test does not use the `--elected-by` flag). It reads back exactly, and the README lines, each leg's `AGENTS.md` and the commit subjects stay single (the root `AGENTS.md` is not checked by this test); - a byte that is not UTF-8 through each flag as a child process's argv (3). - That file on head: 113 passed. On c7c02fa: 97 failed, 16 passed. The 16 are the 15 dry runs and the real scaffold of text, which hold on main since #284 by design. - Mutation probes (each reverts one part, in a throwaway worktree; every one makes at least two tests fail): - no refusal call: 97 failed; - strict decode of `git config user.name`: 2; - `_elector_named` always `--elected-by`: 20; - elector field dropped: 47; - `--reference` dropped: 26; - `--name` dropped: 26; - no line-break check: 39; - no surrogate branch: 13; - no bidi branch: 17; - no control branch: 30; - refusing every `Cf` (over-refusal): 4, the U+200C cases. - Targeted on head 8f43c96 (`git-filter-repo` 2.47.0 on PATH): `tests/test_scaffold_free_text_values.py tests/test_scaffold_e2e.py tests/test_scaffold_pin_and_reuse.py tests/test_scaffold_leg_paths.py tests/test_scaffold_leg_path_collisions.py tests/test_scaffold_pin_collisions.py tests/test_scaffold_referent_chain_links.py tests/test_scaffold_mount_ignored.py tests/test_scaffold_ignored_shape_files.py tests/test_template_quoted_values.py tests/test_naming_policy.py tests/test_reference_default.py tests/test_reuse_visibility.py tests/test_stdout_encoding.py tests/test_repo_hygiene.py tests/test_cli_reference.py`: 801 passed, 1 skipped (`test_stdout_encoding.py:286`, Windows' own default). - Merged trees: - with #270, this file plus `test_adopt_template_values.py`: 191 passed; - with #314 plus the compose patch, this file plus `test_scaffold_manifest_values.py`, `test_scaffold_e2e.py`, `test_template_quoted_values.py` and `test_cli_reference.py`: 343 passed. Without the patch (on a5ef3f5, which differs from 8f43c96 only in an import's place and a docstring): 5 failed, 338 passed, the five above; - with #316, this file plus `test_template_quoted_values.py` and `test_template_values_single_pass.py`: 344 passed. **Left out, with follow-up candidates.** - **`adopt-project.py plan` dies the same way on a `git config user.name` that is not UTF-8.** With no `--elected-by`, it exits 1 with `UnicodeDecodeError` from `git_out` (c7c02fa, and with #270 merged). `_elector` in `adopt-project.py` reads it strictly. It is adopt's half and #270's ground, so it is not touched here. - **A `--local-remote-dir` path that is not UTF-8.** The scaffold creates the three bare remotes, then raises in `write_lf` on `{{ASSEMBLY_CLONE_URL}}` in a leg's README (reproduced on this head). It is the test path only. #270 refuses the same for adopt's clone URL (`_refuse_an_unwritable_clone_url`). Here it is a separate, small issue. - **A `GIT_AUTHOR_NAME` of whitespace alone.** The scaffold falls past it to `git config user.name` for the elector. `env_commit` then hands it to git unchanged, and git refuses to commit (`fatal: name consists only of disallowed characters`) after the remotes are created. Seen on c7c02fa while testing; not this issue's input. - **Markdown syntax in the values** (`*`, `` ` ``, `[`, `\*`) renders as Markdown in the README and `AGENTS.md`. The files hold the typed text, and escaping it would print backslashes in a plain read, so it is not changed. - No `--help` text changed, so `docs/cli.md` is not regenerated. The module docstring's first line, the help description, is unchanged. **Review.** An adversarial review of 8f43c96 found 0 blockers, 0 should-fix and 5 nits (APPROVE WITH NITS): - **#270 copy:** it re-checked the copied hunk byte for byte (same sha256) and found one definition in every merge, in either landing order. - **Probes:** 63 values went through all five sources, dry and real; 320 cases were refused with no work directory, remote or traceback, and nothing was found landing broken. - **Order with #314:** the #314 patch is complete (219 passed with it), and the order of the two refusals is sensible. - **Nits handled in this body:** the "before any process" claim (N1) and wording (N5). - **Nits left for a fix round:** - three mutants of this branch's copy of `unwritable_reason` that only #270's unit table catches (N2); - the argv child-process test with no Windows skip (N3), which Windows CI then passed; - naming the elector's fallback the same way as #314 on the merged tree (N4). Reviewed adversarially: no blocker or should-fix, five nits (two corrected in this message's text, one settled by Windows CI, two left as small follow-ups). CI is green on Linux, macOS and Windows, with SonarCloud passing, at e5804e0 (8f43c96 merged with main ab623dd); main's later #245 (161c6c9) touches none of this branch's files. Closes #282 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… of main's commits merge with no conflict, among them #243's template-less clone of the assembly and #216's literal `git grep` in adopt-project.py, and #260's one-pass `render` in shape_materialize.py origin/main moved 71b58a1 -> 8cdca5e. It was merged by sha, as the coordinator asked for e43fa95 or later: - c7c02fa (#299 via #315), scripts/family.py; - 346079d (#216 via #228), adopt-project.py: `plan`'s follow-up `git grep`; - 34ecbc2 (#260 via #316), scripts/shape_materialize.py: `render` substitutes in one pass; - 86da808 (#253 via #264), scripts/bump-leg.py; - 6f4ce47 (#302 via #318), scripts/family.py; - 7491530 (#288 via #309), AGENTS.md, README, docs and scaffold-project.py's --help; - 7591455 (#279 via #310), tests/conftest.py: `git()` takes its default identity as `-c` config; - 87373a6 (#306 via #311), shape-doctor.py; - 8a14ac9 (#334 via #336), tests/test_update_shape_landing.py; - c1b6732 (#239 via #269), update-shape.py; - e43fa95 (#243 via #268), adopt-project.py: the assembly root is cloned with `--template=`, and the verification refuses a mount whose gitlink is not the commit pushed to its leg (`adopt-mount-commit`); - a1b3a4b (#321 via #335), tests/test_git_output_decoding.py; - 8cdca5e (#295 via #312), the template's scripts/validate-pins.py. `git merge-tree` and this merge give the same tree, 07e6b49. adopt-project.py, scripts/shape_materialize.py and tests/test_adopt_ignored_shape_files.py auto-merge. In the last of these main's 346079d and this branch's 86a5e30 change different cases. Checked on the merged tree: - AST (fin183g/names.py, base 71b58a1): no top-level name is bound twice in adopt-project.py, scaffold-project.py, scripts/repo_shape.py or scripts/shape_materialize.py, and none is loaded but bound nowhere. Main changed the signature of no function this branch calls. - Every sentence 86a5e30 changed is still there: in `_through`, in `_link_parts`, in `_verify`'s `_resolve` (which e43fa95's `adopt-mount-commit` leaves as it was), and in the three test files. - `_commit_the_split` now clones the assembly with `--template=`. The next commit, F3, reads what that clone holds in its own `.git`, which is why F3 comes after this merge. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…not extract as it is named, with the reason and `root` or `drop` as its only answers, so `-notes.md` is no longer planned `leg: spec` for `check` to refuse, and #166's split question says the same of such an entry (#319) (#377) Since #196 `check` and `execute` ask one test of an entry's path, `entry_path_problem`, and `plan` asked it nothing. A source holding `-notes.md`, `notes\t.md`, `a\rb.md`, `a\nb.md`, `a==>b.md` or a name that is not UTF-8 was planned `leg: spec`, `review_required: false`, with no question; `check` then refused that answer as `plan-unsafe-path`, and `execute` refused it before any leg. `Icon\r` and ` `, which no rule places, were asked the policy's question, which offers `spec` and `code`. #166's split question named such a path as an entry to write, with any `leg:`. - `ask_about_refused_paths` runs after `ask_about_submodules`. Each entry the test refuses that is not answered `root` is written `leg: null`, `review_required: true`, confidence `review`, the classifier's rule and reason kept. Its `question:` says where the rules put it, or keeps the policy's question first; then that `execute` will not extract it into a leg as it is named, with `entry_path_problem`'s reason; that `root` or `drop` is the only answer `execute` takes; and that renaming it in the source and re-running `plan` is the way into a leg. - An entry that also holds a submodule is told `drop` is its only answer, because `root` is `plan-submodule-root`. - `_asked_about`'s split sentence names each replacement entry the test refuses, with its reason, and says `root` or `drop` (`drop` for a submodule). Past SPLIT_ENTRIES_NAMED of them it counts the rest. - What the rules imply (follow-ups, predicted collisions, the `seeding:` record) is read with such an entry asked, because no answer puts it in a leg. A leg only that path fed is recorded as seeded, with the `--allow-empty-leg` warning, which matches what `check` says of the plan as written and once it is answered. - `_asked_about` sets its question as written. `Verdict` collapsed each run of spaces, so `src/a b.md` was named `src/a b.md`, an entry that covers nothing. Unchanged: `check` and `execute` (no line of either is touched); `shape-doctor.py`'s placement plan (it calls `walk` only); `--help`. The plan file and `plan`'s report are byte-identical on main and here for the synthetic tree, a source with a registered submodule, and one holding admitted names (`My Notes.md`, `#notes.md`, `[ID].tsx`, ` lead.md`), diffed. Changed with it: #228's two tests of a spec directory that begins with `-` (`-notes/`, and `-Otouch pager-ran;/`, named like `git grep`'s pager option) read `leg: spec` and the follow-up from the plan. `plan` now asks about such a directory and writes no follow-up for it, because no answer `execute` takes moves it to the spec leg. Each test now asserts the plan's question and that the plan names no file for it, and still asks #228's literal `git grep`, of the rules' own answer `spec` through `cross_leg_references`: the same follow-up, and nothing run. Every other assertion is kept, the `leg: spec` one as the rules' answer. Left out: an entry the rules answer `root` whose path the test refuses stays `root` with no note, because `check` and `execute` accept it. Lane: openreposhape-3 (openRepoShape-3) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…\udcNN` and a character as `\xNN`, as the plan's own `path:` does, so the byte 0x85 and the character U+0085 no longer both read `\x85`, and 34 pairs of different names no longer read as one in a question or a finding (#329) (#378) `_spelled_character` wrote a lone surrogate U+DC80 to U+DCFF, which is how `Source.tree` keeps a byte that is not UTF-8, as `\xNN`, and any other character that is not printable and lies below U+0100 as `\xNN` too. The 32 C1 controls U+0080 to U+009F, U+00A0 and U+00AD therefore read exactly as the bytes 0x80 to 0x9F, 0xA0 and 0xAD: 34 pairs of different names. In YAML `\x85` is the character, so a person who copied the byte's spelling out of a finding into the plan wrote the character's entry; the plan's own writer spells the byte `\udc85` (#199, #247). Now a character that is not printable is spelled by `repo_shape.double_quoted`, the one table of escapes the plan's writer uses and its reader resolves, and no table of this file's own: a finding and the written plan spell one name the same way, and a byte and a character never share an escape because each escape names a code point. - The byte 0x85 is `\udc85`; the character U+0085 is `\x85`. Every byte 0x80 to 0xFF is `\udcNN`, the 94 whose character prints as itself included, because `\xe9` for the byte 0xE9 was the character U+00E9 in the plan. - A backslash and a backtick are escaped with a backslash, as before: the backticks delimit the name. A line feed, carriage return and tab are `\n`, `\r` and `\t`; any other character below U+0100 is `\xNN`, below U+10000 `\uNNNN`, above it `\UNNNNNNNN`, as the plan writes them. - The messages #255 added name a value, not a path, with `repr`, which escapes by code point already: a byte is `\udcNN` and a character `\xNN` there too. `repr` and `_spelled` agree on every character but the backtick, which only `_spelled` escapes, so no message of either spells a byte as `\xNN` and no table is added for them. - Tests that pinned a byte as `\xNN` (the policy refusal, `plan-unsafe-path`, the mount refusal, a `.gitmodules` value, a submodule question, the source directory in `plan`'s report, a hand-edited `code_path` that `check` prints under WHAT WILL HAPPEN (#255), and the follow-up `git grep` names #228 spells through the same function) pin `\udcNN` now. - `tests/test_adopt_spelled_unambiguous.py` is new: the 34 pairs one by one, every byte and every code point of the BMP spelled apart, the spelling equal to `double_quoted`'s and read back by the plan's reader as the same name, a finding and a question naming a byte and its character differently, the messages of #255 doing the same, and `repr` and `_spelled` agreeing, and `plan` and `check` run on a source holding both. Unchanged, diffed over all 1,114,112 code points: only the 128 characters U+DC80 to U+DCFF are spelled differently; every other character, a backslash, a backtick and the whole of `_spelled`'s framing are byte for byte what they were. A spelling is only printed: in a finding, a refusal or the report, and in a question the plan carries as prose that nothing parses. Of the 28 functions that name `_spelled`, `_spelled_character` or `_shown`, one runs git, `cross_leg_references`'s `git grep`, and it is given the source's own directory name, never the spelling, which only the follow-up's text holds; none opens a file or builds a command line from one. Nothing here changes what git is asked. Left out: a `\` + backtick in a finding is still the finding's own delimiter escape and is not a YAML escape, so a name holding a backtick is not copy-and-paste into the plan as it stands. Lane: openreposhape-3 (openRepoShape-3) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…d `open_glass` share one `SHAPE_PIN_SOURCE_<PRODUCT>`, so `--pin` refuses the pair and `validate-pins.py` notes a declared one and reads the variable for neither (#275) (#409) `pin_source_env_name` turns every character outside `[A-Z0-9_]` into `_`, so `open-glass`, `open_glass` and `open.glass` are three legal product names, with three pin files (`contracts/<product lowercased>-pin.yaml`), and one variable, `SHAPE_PIN_SOURCE_OPEN_GLASS`. On ab623dd (this branch's main parent; the same rows reproduce on c7c02fa), with the issue's own probe against local bare remotes and a `gh` that exits 97: | step | ab623dd | this head (6494714) | |---|---|---| | `scaffold-project.py --pin open-glass@<A> --pin open_glass@<B>` | exit 0, three repositories | exit 2 `scaffold-pin-collides`, nothing created | | `validate-pins.py`, no source, on a project that already declares the pair | exit 0, both skips offer `SHAPE_PIN_SOURCE_OPEN_GLASS` | exit 0, one `note neutral-pin-source-collides`; the skips offer `--pin-source <product>=<path>` and a sibling checkout only | | `SHAPE_PIN_SOURCE_OPEN_GLASS=<open-glass>` | `neutral-pin-source-unreadable` for `open_glass`, exit 1 | exit 0, the note; the variable is read for neither pin, so each pin is read from its own source or skipped | | `SHAPE_PIN_SOURCE_OPEN_GLASS=<open_glass>` | `neutral-pin-source-unreadable` for `open-glass`, exit 1 | the same | | `--pin-source open-glass=... --pin-source open_glass=...` | exit 0 | both pins recompute, exit 0, the note | **What changed.** Every variable name is kept: an operator may have set one and the docs name them. What is done instead: - `scripts/repo_shape.py`: `pin_source_env_collisions(products)` is the one pure answer, `{variable: [products]}` for each variable two or more distinct products share. Distinct means distinct PIN FILES (`pin_product_key`, `lower()`, which is what `contracts/<product lowercased>-pin.yaml` is named by), so a product typed twice or in two cases is #241's file collision and never this one. `pin_source_env_shared` is the set of those keys. - `scaffold-project.py`: `_refuse_colliding_pins` (left where it is) names each variable's `--pin` arguments as typed after #241's file sentences, in the same `scaffold-pin-collides` refusal. Dry run and real run say the same no, before any process starts and ahead of the leg-path refusal. #241's wording and remediation are unchanged when only a file collides. A NEW pair is still refused, so the situation is not created anew. - `templates/assembly-root/scripts/validate-pins.py`: one NOTE per variable, `neutral-pin-source-collides`, naming every product and the variable, the keyed `--pin-source <product>=<path>` that reads each pin, and that which declaration stays is the human's decision. It is never a finding and never moves the exit code. A product whose variable is shared does not read it (rule 2 of `resolve_neutral_pin_source`), and its skip hint offers the keyed `--pin-source <product>=<path>` and not the variable or the bare form. A variable one product has to itself reads and is offered exactly as before. - `scripts/repo_shape.py`, the two chain readers: `link_pins_from_trees` does not read a variable two links of one chain share, and the `declared-unverified` hint stops naming it. Only a hand-written chain can have two such links, since scaffold takes every link but the first from the naming policy's alphanumeric `open<Product>` form. Each reader of `pin_source_env_name`, decided: | site | decision | |---|---| | `scaffold-project.py --pin` | refuse (above) | | `validate-pins.py` rule 2 and the skip hint | a note, and the variable is neither read nor offered for a shared product | | `repo_shape.py` `resolve_link_source` (via `link_pins_from_trees`) | skip the variable when another link of the chain shares it | | `repo_shape.py` `_chain_link_findings` hint | omit the variable for a shared link | | `adopt-project.py --pin`, `plan`, `check`, `execute` | not in this PR, see below | **Fix round 1 (from the review of 34dbd86).** **The design change: a declared pair is a NOTE and not a finding. This is the lane coordinator's decision, and Brett may reverse it.** The first round made `neutral-pin-source-collides` a FINDING. Main through ab623dd scaffolds such a project (`--pin open-glass@A --pin open_glass@B` exits 0), and `update-shape.py apply` runs the NEW `validate-pins.py` over the rewritten root, so for a project already declaring the pair the finding ended every apply as `REFUSED update-validators-red`, rolled back, for this update and every later one, until a person deleted a true declaration of descent. AGENTS.md tells an agent never to edit "a pinned file, a digest or a manifest to make a row go green", and the finding's remediation was exactly that, so the person was locked out. The coordinator chose the review's option (b): it is a NOTE now. The guard stays (the variable is read for neither pin), each pin still verifies from its own source, scaffold still refuses a new pair, and `validate-pins.py` says the rest in one line. To put the finding back, restore 34dbd86's `report.finding(...)` call in `_report_pin_source_collisions` (it is not a word swap for `report.advise`: `advise` takes one string, `finding` a code and a detail), give the finding the remediation 34dbd86 had, and turn the tests that expect the note (and `test_a_project_declaring_a_colliding_pair_takes_a_shape_update`) back into exit 1; the first round's versions of all of them are on 34dbd86. - **Proved on a real old project.** Main (ab623dd) scaffolded the pair into local bare remotes; `update-shape.py apply` to the first round's head 34dbd86 exited 2 `REFUSED update-validators-red` and rolled back (`git branch` shows only `main`); to this head it exits 0, the project's validator passes and prints the note, and the doctor's `pins` row is `ok` before and after. The doctor prints `passes` for a validator that exits 0 and does not echo its stdout, so the note itself is visible in `update-shape.py apply`'s output and in `validate-pins.py`'s, and the verdict moves for no reason but the update (`COMPLIANT, SHAPE BEHIND` to `COMPLIANT`). It is now a test: `test_a_project_declaring_a_colliding_pair_takes_a_shape_update`, over local stand-ins beside the project, so no `gh` is asked. - **Collisions by the variable.** Pairs were decided by `casefold()`, but the pin file is `lower()`. `openß`/`openss`, `openſ`/`opens`, `openfi`/`openfi` and `openς`/`openσ` (the letters are U+00DF, U+017F, U+FB01 and U+03C2 beside U+03C3) share a variable and have two pin files, and were never found. They are now, from the pin-file fold (`pin_product_key`). A pool of 31 names is checked pairwise against `neutral_pin_path` and `pin_source_env_name` themselves, and no single product is ever a collision. They are reachable only from a hand-written `project.yaml`. - **Windows.** The guard's only test needed the `#!` `gh` stub and was skipped on Windows. `test_a_shared_variable_answers_for_neither_pin_with_no_gh` puts a checkout of each pin beside the project, names one in the variable, and needs no `gh`: on ab623dd it is `neutral-pin-source-unreadable`, here each pin reads its own sibling. The hint and the non-ASCII rows are tested in-process, with no skip and no filesystem. - **The hint.** For a product two pins share, the skip hint offers the keyed `--pin-source <product>=<path>`: the bare form answers for every pin with no more specific source, so with two pins it gives the other the finding this issue is about. - **Trailers and title.** The trailers of the earlier commits (a30091f and 34dbd86 name Sonnet 5.5) are settled in this message, which carries Opus 5.5. The PR title now names `repo_shape.py`. - `scaffold-project.py` is not touched this round: `fix/adopt-pin-writes-pin-file` moves that code, and the change would only conflict with it. **Left out.** - `adopt-project.py` is untouched. Its half arrives with #273's PR, which moves scaffold's pin machinery into a shared module and routes adopt through `pin_source_env_collisions`. Once it lands, the one place that still keys by `casefold()` (`pin_variable_collisions` in `scripts/shape_materialize.py`) should read `pin_product_key`; it cannot differ for an ASCII `--pin`, which is all `PIN_ARG_RE` takes. - `scaffold-project.py`'s own `_colliding_pin_variables` also keys its argument lists by `casefold()` and one docstring there still says the project's validator reports the pair "as a finding"; both go with the code when #273 moves it. #312's `PinSources` keys `--pin-source` by `casefold()` too, so `openß=` and `openss=` are one key there; that is #312's. - A product in `neutral_product_pins:` and a chain link in `naming.referent_chain` that share a variable are read by two validators in one process, and neither sees both sets. Not detected here. - The doctor does not print a passing validator's stdout, so its `pins` row does not carry the note. Not changed: it would add a file to this PR. - No `--help` text changed, so `docs/cli.md` is untouched. `README.md` is at its line cap and its blob is pinned by `docs/handbook.html`, so the one README sentence this wants needs a regenerated handbook and waits for its own change. - `validate-pins.py` and `repo_shape.py` ship into projects, so every project's `shape-pin.yaml` reads `upstream-changed` for both until `update-shape.py apply`, which now succeeds for a project that declares such a pair. **Evidence.** All on the head (6494714), with git-filter-repo 2.47.0 on PATH, a `gh` that cannot be reached (a stub first on PATH, `GH_TOKEN` bogus, an empty `GH_CONFIG_DIR`), and the repository-local `user.name` blanked at command-line scope, as the standard's checkout sets one that two scaffold tests read. - `tests/test_pin_source_env_collisions.py` is 137 tests and passes on this head. Laid over 34dbd86, 50 fail and 87 pass: the note and update tests, the non-ASCII rows, the hint, the sibling guard, and `pin_product_key` itself (which 34dbd86 does not have); the scaffold refusals, the chain readers and the controls hold on both. - Targeted, run on ad6f569, whose tree differs from this head in `tests/test_pin_source_env_collisions.py` only (the last edits made one comparison read the committed blob and wrapped one line): the new file, `test_pin_validator`, `test_neutral_pin_validator`, `test_cli_reference`, `test_manifest_validator`, `test_naming_policy` and `test_repo_hygiene` **445 passed**; the scaffold pin, collision, referent-chain, leg-path and e2e files **245 passed**; `tests/test_update_shape*.py` **218 passed, 3 skipped** (junctions, NTFS only); `tests/test_shape_doctor.py` **254 passed, 3 skipped** (the PowerShell line, a Windows host only). - On this head itself: the new file, `test_repo_hygiene`, `test_cli_reference`, `test_pin_validator` and `test_neutral_pin_validator` **244 passed**. - This head merged with origin/main a983e6a, in a throwaway worktree: the new file, `test_pin_validator`, `test_neutral_pin_validator`, the scaffold pin, collision, referent-chain and e2e files, the whole doctor file and `test_cli_reference` **590 passed, 3 skipped** (the PowerShell line); the new file alone, in its final form, **137 passed**. origin/main had moved twice since; the merge-tree below is against it. - Twelve mutation probes, each hunk reverted alone: all caught. The note-to-finding one is caught by the update test (a committed mutant, since the test clones the repository's HEAD) and the guards by the sibling test, run with every test that needs a `#!` stub deselected as a Windows runner would; the hint one by an in-process test. - Nothing ran on macOS or Windows before this PR's CI. **Merge-tree, as computed during the fix round on 2026-10-12** (every sha and head is the one it had then; main and the other branches have moved since). `git merge-tree --write-tree` of this head (6494714) against origin/main 1f90a0f is clean, and so is the merge of origin/main a983e6a, which the merged-tree runs above used. #312, #309, #270, #268, #228, #245 and #351 have landed. The rest were computed on ad6f569 (the same tree but for one test file) against a983e6a. Open PRs touching this branch's files, all clean against the head: #314, #355 and #373 (`scaffold-project.py`), #343 (`validate-pins.py`), #390 (`repo_shape.py`) and #394 (`repo_shape.py`, which conflicts with origin/main by itself). The other 25 open PRs and the lane-5 branches `fix/adopt-check-link-and-manifest`, `fix/scaffold-free-text-values`, `fix/family-init-values`, `docs/adopt-execute-exits`, `fix/yaml-flow-brackets-surrogates` and `fix/adopt-pin-writes-pin-file` (#273's, 7a94b6b, also clean for this head) are clean too. #356, #382 and `fix/adopt-record-submodule-placement` conflict with origin/main and not with this head; #358, `fix/adopt-late-failure-notice` and `fix/adopt-plan-quote-yaml-keywords` conflict with both, with origin/main by themselves, so they are not this branch's. Neither are the five PRs below #200 that origin/main has not landed (#94, #96, #98, #100 and #161), which conflict with it too. The PR set is read from the refs under `refs/pull/` that origin/main's history has no `(#n)` subject for, not from GitHub, so a closed one may be in it. **One conflict, with `fix/link-source-values` (#338, 40762ca) in `scripts/repo_shape.py`, four hunks.** Not resolved in either branch; take both sides: (1) `resolve_link_source`'s signature, `default_source=None, env_shared: bool = False, *, tried: list | None = None`; (2) its docstring, both paragraphs; (3) `link_pins_from_trees`'s docstring, both paragraphs; (4) its call, `default_source, pin_product_key(name) in shared, tried=asked)` followed by #338's `if tried is not None: tried[name] = asked`. The auto-merged `resolve_link_source` body already composes: a shared variable is never an `env` candidate and so never appears in #338's `tried`. **Known limits after the re-verification (APPROVE WITH NITS, 0 blockers, 0 should-fixes, 3 nits, at 6494714).** For the four non-ASCII pairs this change now detects (`ß`/`ss`, `ſ`/`s`, `fi`/`fi`, `ς`/`σ`), the keyed `--pin-source <product>=<path>` form does NOT separate the two pins: #312's source lookup folds both keys into one, so one pin can get a false `unreadable` finding. A sibling checkout does separate them. Only a hand-written `project.yaml` can declare such a pair. The doctor's `pins` row still prints only "passes" for a validator that passes, so it does not carry the collision note; that needs `shape-doctor.py`, and is left for a follow-up. The wording for restoring the finding is under "Fix round 1" above: it is 34dbd86's `report.finding(...)` call, not a word swap. Reviewed adversarially at 34dbd86 (CHANGES NEEDED: 0 blockers, 2 should-fixes, 5 nits). The first should-fix was the finding itself, which locked a project that already declares the pair out of every `update-shape.py apply`; the second was a description that had gone stale. Fix round 1 (6494714) made the pair a NOTE, which is the coordinator's decision and one Brett may reverse (the way back is above), keyed collisions by the pin file's own fold, gave the guard a test that runs on Windows, and made a shared product's skip hint offer the keyed `--pin-source`. Its re-verification at 6494714 found every finding handled: APPROVE WITH NITS (0 blockers, 0 should-fixes, 3 nits: the keyed form and the non-ASCII pairs, the doctor's silence about the note, and the wording for restoring the finding, each stated above). CI is green on Linux, macOS and Windows, with SonarCloud passing, at b70a07d (main a73b4e9 merged in). Its merge with main 5a25387 passed the targeted set (the six pin and scaffold test files, `tests/test_validate_pins_unreadable_workflow.py`, `tests/test_ruleset_hint_tracking_branch.py` and `tests/test_shape_doctor.py`): 850 passed, 3 skipped. Closes #275 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>



Lane: openreposhape-3 (openRepoShape-3)
Claim: #216 (comment)
What
plan's follow-ups come from onegit grepper directory the plan sends to the spec leg, restricted to the entries it sends to the code leg. Both inputs are names the source chose, and git read each as something else. The call is nowgit --literal-pathspecs grep --no-color -z -l -I -F -e <spec dir> <commit> -- <code entries>:-e: the spec directory is always the pattern.-notes/was a usage error (exit 129), and its empty stdout was read as "no file names it".-O<cmd>/was--open-files-in-pager, which git runs through a shell.--literal-pathspecs: each code entry is the path it spells.:cachewas searched as the pathspeccache.*matched every path, so it named a file that moves to the spec leg.-z: each hit is decoded the waySource.treedecodes a name, then spelled the way a question spells a path (\n,\xe9), on one line of UTF-8.-lused to print git's C-quoted form.--no-color: before this,color.ui=alwaysin the source's own config wrote\x1b[minto the plan.A failed grep is reported. When
git grepexits above 1, the follow-up gives the exit code, git's message on one line, and what to look for by hand. It is no longer read as "no hits".A spec directory whose name holds a line feed is left to a human (fix round 1).
-Freads a pattern holding a line feed as one pattern per line, soa<LF>b/was searched asaORb/and named files that never mention it, the way*did. No grep is asked for such a name, and the by-hand follow-up says why.The spec directory's own name is spelled in every follow-up, the way the hit beside it is (fix round 1). An ordinary name is unchanged.
The split's forced add is
git --literal-pathspecs add -f. A shape file written through a symlink the plan kept in the root lands at the link's target, which is a name the source chose. Without the flag::ci/...was pathspec magic;:!ci/...was an exclude;ga[0]/...was a glob.On main,
executerefuses those plan paths asunsafe-valuebefore anything is made. This hunk is reached once adopt-project.pycheckaccepts plan pathsexecuterefuses asunsafe-value(a space or a non-ASCII character), soplan okis followed by a refusal and #166's split instructions can name entriesexecutewill refuse #196 admits them. adopt-project.pycheckaccepts plan pathsexecuterefuses asunsafe-value(a space or a non-ASCII character), soplan okis followed by a refusal and #166's split instructions can name entriesexecutewill refuse #196 changesgit rmand filter-repo's path list but leaves this add alone.Unchanged:
planover conftest'sSYNTHETIC_TREEplus four plain files that namecontracts/anddocs/is byte-identical to main a5b09c3 in exit code, stdout, stderr and the plan file (measured by diffing both runs, on 115f012 and again on f0d4334). Thegit add -A -- .before the forced add is untouched, and no--helptext changes.Why
The issue's repro, re-measured on this machine.
planruns over local sources, and the follow-ups are whatplanwrote. Main is a5b09c3. The head column was measured on 115f012 and again on f0d4334, with identical output.-Otouch PWNED;/,src/scratch.txtin the index holding the planned commit's hashplan exit 0, new files in the source dir: ['PWNED'], so git ran a commandplan exit 0, new files in the source dir: [], plus the follow-up forsrc/reader.pyscratch.txt[]/[][]/[]-notes/error: unknown switch `t'code/src/reader.py names `-notes/` ...-Otouch/(no hash in the tree)code/src/reader.py names `-Otouch/` ...:cache/holding the file that namesnotes/code/:cache/a.py names `notes/` ...*/, spec dirnotes/whose ownnotes/two.mdsays "see notes/"code/notes/two.md, a spec-leg filecode/src/reader.pyonlysrc/Übersicht.pynamingnotes/code/"src/\303\234bersicht.py" names ...code/src/Übersicht.py names ...s?c/beside spec dirsec/(control)code/src/reader.pyonlynotes/(control)code/src/reader.pya<LF>b/besidesrc/other.pywhose only text isa = 1(cross_leg_references, since #199 writes that plan)code/src/other.py names `a<LF>b/` ..., a false follow-up with a raw line feedgit could not search ... for `a\nb/` (`git grep` reads each line of a pattern as a pattern of its own ...); find the ones that name it by hand ...The forced add, end to end (
plan, then answer.githuband<target>/as root, thencheck, thenexecuteinto local bare remotes). The source's.gitignorehidesvalidate.ymlandga0/. "#196 + this head" is a trial merge of this head withfix/adopt-check-unsafe-path5a8df76, whose tree is identical to its current head f9656bd.:ciREFUSED unsafe-value, nothing madefatal: pathspec ':ci/workflows/validate.yml' did not match any filesadoption verified; the split holds:ci/workflows/validate.yml:!ciunsafe-valueFINDING adopt-shape-file-missing: the split lacks the workflowadoption verifiedga[0]unsafe-valueFor
ga[0], the glob only goes wrong when a file it also matches is ignored and already present in the clone (ga0/...).execute's fresh clone never holds one, so that case is covered by the unit test, which writes it.Tests
tests/test_adopt_literal_names.py, 14 cases, one per name class above.notes/ands?c/. Each failure is the defect its docstring names.fatal:rather than git's exact wording, which differs between git versions.tests/test_adopt_ignored_shape_files.py: its recorded argv for the forced add gains--literal-pathspecs.tests/test_adopt_*.py(10 files),tests/test_shape_doctor.py,tests/test_repo_hygiene.py,tests/test_cli_reference.py, with git-filter-repo 2.47.0 on PATH.1015 passed, 1 skipped in 538.50s(exit 0).1014 passed, 1 skipped in 526.49s.test_adopt_plan.py:393, which needsSHAPE_MEDXEHR_CLONE, unset here.a-code-entry-starting-with-a-colon,a-code-entry-named-star,control-a-code-entry-with-a-question-mark, the line-break / non-UTF-8 case, the line-feed spec directory, and the three forced-add cases. NTFS cannot hold:,*,?or a line break in a name, and git for Windows checks a symlink out as a plain file. The remaining six run there.Übersicht.pycase relies on git'score.precomposeunicodereturning the name in NFC there.Depends on
#214 (
fix/plan-report-stdout-encoding, local 3006303, no PR yet) should land first. This was found by the independent review.planprints each follow-up witherrors=strict. Main follows-land prints git's octal-quoted ASCII form of a code file's name. This head prints the real name. So a name the codepage cannot hold now raises after the plan is written.planraisesUnicodeEncodeErrorin its terminal report, after the plan file is written, for a printable name stdout cannot encode (日本.mdunder cp1252, as on a redirected Windows stdout), soplanexits 1 beforeplan written to#214's subject. This PR adds a second route: any file under a code entry that names a spec directory.planraisesUnicodeEncodeErrorin its terminal report, after the plan file is written, for a printable name stdout cannot encode (日本.mdunder cp1252, as on a redirected Windows stdout), soplanexits 1 beforeplan written to#214 is the mechanism. A local guard would duplicate it in_print_plan_report, which adopt-project.pyplanraisesUnicodeEncodeErrorin its terminal report, after the plan file is written, for a printable name stdout cannot encode (日本.mdunder cp1252, as on a redirected Windows stdout), soplanexits 1 beforeplan written to#214 changes.Measured with
PYTHONIOENCODING=cp1252overnotes/one.md,notes/two.md,src/日本.py(which namesnotes/) andsrc/other.py:planplan written toprinted; follow-upcode/"src/\346\227\245\346\234\254.py" names ...UnicodeEncodeError: 'charmap' codec can't encode characters in position 13-14. The plan file is written, butplan written toand the questions are not printed.code/src/\u65e5\u672c.py names ...; the plan file holdscode/src/日本.py names ...On that merged tree,
test_adopt_literal_names.py+ #214'stest_stdout_encoding.pygive 25 passed.Composes with
Trial merges (
git merge-tree --write-tree), in both orders, against every branch that touches a file this one touches:fix/plan-report-stdout-encoding(#214)fix/adopt-check-unsafe-path(#196)test_adopt_literal_names.py+ #196'stest_adopt_entry_paths.py: 68 passed. The forced-add end-to-end table above is this merge, and fix round 1 does not touch the forced add.fix/decode-git-output-utf8(#217, PR #230)cross_leg_referenceshunk and lists the call in its guard'sEXEMPTtable. On the merged tree with f0d4334 (2b83baf),test_git_output_decoding.py+test_adopt_literal_names.py: 17 passed. The call is bytes, so the exemption is unused, and the guard does not fail on an unused one.fix/adopt-history-gitlinks(#195)fix/adopt-spell-finding-paths(#212)tests/test_adopt_ignored_shape_files.py.fix/scaffold-shape-path-collisions(#197)fix/adopt-report-filter-repo-failure(#171)fix/adopt-late-failure-notice_stage_the_splitout, so they do not depend on the argv.fix/adopt-plan-quote-yaml-keywordsfix/adopt-record-submodule-placementfix/adopt-root-kept-symlinksorigin/fix/adopt-mount-past-gitignore(merged as #200)fix/adopt-plan-linebreak-names(#199)repo_shapeimport list, conflicts with main a5b09c3 alone. This head adds no conflict hunk.fix/adopt-relative-submodule-urlThe rows were re-checked against f0d4334.
Base. Tested on a5b09c3. Main has since moved to 54aa69a (#173), and
git merge-tree --write-tree origin/main f0d4334is CLEAN.None of these siblings' tests assert on the
git grepargv, oncross_leg_references' output or on the forced add's argv. Checked by scanning every sibling's added test lines.Left out
planraisesUnicodeEncodeErrorin its terminal report, after the plan file is written, for a printable name stdout cannot encode (日本.mdunder cp1252, as on a redirected Windows stdout), soplanexits 1 beforeplan written to#214's to close; see Depends on.Closes #216
🤖 Generated with Claude Code
Summary by Sourcery
Treat all source-selected paths as literal names when generating adoption follow-ups and staging split files.
Bug Fixes:
Enhancements:
Tests: