Repository navigation
scaffold-project.py and family.py: a --local-remote-dir whose clone URL is not text a README can hold is refused before any remote or folder exists, --dry-run alike, so a directory named in a legacy code page no longer ends in a UnicodeEncodeError traceback after the bare remotes are made (#433) - #640
Conversation
… URL is not text a README can hold is refused before any remote or folder exists, `--dry-run` alike, so a directory named in a legacy code page no longer ends in a `UnicodeEncodeError` traceback after the bare remotes are made (#433) Under `--local-remote-dir` the clone URL of a new repository is the bare repository's own path. The scaffold renders it as `CLONE_URL` and `ASSEMBLY_CLONE_URL` into the READMEs of the assembly root and of each leg, and `family.py init` renders the holder's into the holder's README, all through `write_lf`, which encodes strictly. A directory whose name is not UTF-8 reaches `sys.argv` as a string with a lone surrogate, so both tools ended in `UnicodeEncodeError`, exit 1, after the bare remotes existed (three for the scaffold, `<Family>.git` and the family folder for `init`), while `--dry-run` exited 0 and said nothing. A line break, a control character or a bidi override in the path was written into the README raw. `adopt-project.py` asked the question before any leg existed (#255). Both tools now ask it of the same value with the same `unwritable_reason`, and refuse `scaffold-unwritable-value` and `family-unwritable-value` in adopt's words: under --local-remote-dir the clone URL is the bare repository's own path, `<url>`, which the tool writes into the README, and why it cannot be written; nothing was created; pass a directory whose name is text, or use GitHub. The scaffold hoists `_repository_urls` to just after the name-length check and asks the assembly root's URL there, before `_declaration` runs `gh api` and before a work directory or a remote exists. Only the assembly root's URL is asked: it is the one a file holds, and the legs' sit beside it in the same directory. `family.py init` asks the holder's URL right after `_holder_remote`. `_refuse_unwritable_values` is not reused, because its remediation tells a person to retype a value and a directory is renamed or replaced. tests/test_clone_url_bytes.py: not UTF-8, a line feed, an escape and a bidi override in the directory's name, absolute and relative, each tool, real and `--dry-run`, in this process under a guard and as a child process; a UTF-8 directory still plans and still creates. Five mutants, each killed. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Reviewer's GuideThe PR prevents scaffold-project.py and family.py init from creating partial repositories or folders when a local clone URL cannot be safely represented in README text, by validating the resolved URL up front and applying equivalent behavior during --dry-run; extensive tests cover invalid bytes, unsafe controls, path resolution, exact diagnostics, side-effect absence, and valid UTF-8 paths. Sequence diagram for preflight validation of local clone URLssequenceDiagram
participant CLI
participant Resolver
participant Validator
participant Creator
alt scaffold-project.py
CLI->>Resolver: _repository_urls(args, names)
Resolver-->>CLI: urls[assembly]
CLI->>Validator: _refuse_an_unwritable_clone_url(local, urls[assembly])
alt URL is unwritable
Validator-->>CLI: Refusal scaffold-unwritable-value
CLI-->>CLI: Exit 2, no remote or work directory
else URL is writable
Validator-->>CLI: Continue
CLI->>Creator: _declaration(...)
Creator-->>CLI: Create scaffold resources
end
else family.py init
CLI->>Resolver: _holder_remote(args, family)
Resolver-->>CLI: local, repository, url
CLI->>Validator: _refuse_an_unwritable_clone_url(local, url)
alt URL is unwritable
Validator-->>CLI: Refusal family-unwritable-value
CLI-->>CLI: Exit 2, no remote or family folder
else URL is writable
Validator-->>CLI: Continue
CLI->>Creator: materialize_family_root(...)
Creator-->>CLI: Create family resources
end
end
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
…w test file holds no raw bidi character and no composite assertion, and the holder's clone-URL remediation comment says which `family-unwritable-value` it is for (#433) Review R433 measured f25917e and found SonarCloud's quality gate in ERROR, from the test file only. - `tests/test_clone_url_bytes.py:71` held a RAW U+202E RIGHT-TO-LEFT OVERRIDE in the `bidi-override` row, which is S6389 (new-code security rating C), and contradicted the file's own docstring: every invisible character in it is spelled as an escape, because a raw bidi control reorders the line it is on. It is now `"evil".encode("utf-8")`, the same bytes, the way `tests/test_scaffold_free_text_values.py` spells its own. No Cc, Cf, Zl or Zp character is left in the file; the only non-ASCII characters are the visible `é` of the issue's own `<dir>/café` control. - `directory_named` asserted `path.is_dir() and os.listdir(path) == []` in one statement, which is S9073. It is two assertions, so a failure names which half failed. - `scripts/family.py`, the `#:` comment above `CLONE_URL_REMEDIATION`, now says it is what a `family-unwritable-value` FOR THE HOLDER'S CLONE URL is told to do, as the scaffold's twin already does. A second remediation for the same code, a creator read from `git config user.name`, arrives with #641, and an unqualified comment would claim to be the only one. Comment only. No behaviour changes. The new file still gives its 30 passed. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Main gained #626 (`family.py add`, `bump` and `remove` refuse before their first write), #633, #642, #590 and #641 (`git status --porcelain` and `git config user.name` read as bytes) since this branch's base 8f453d0. One conflict, in scripts/family.py: the comment above the single shared `from shape_materialize import unwritable_reason` line. #433 and #641 each added a use of that import and each wrote its own comment above it. The resolution keeps both uses and all three issue numbers (#426 for the creator read from `git config user.name`, #255 and #433 for the holder's clone URL): # Whether a value `init` writes into the holder is one line of text a file # can hold: the creator read from `git config user.name` (#426), and the # holder's clone URL under `--local-remote-dir` (#255, #433). It is the # question the scaffold and `adopt-project.py` ask of theirs. scaffold-project.py auto-merged. No other hunk conflicted. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Sonnet 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 19 minutes by commenting |
…ies keyed by pin_product_key, the key the chain readers use (#444 x #610) #638 keys every reader of a chain by `pin_product_key` (`lower()`), and `_chain_link_findings` now finds a holder's remedy by it too, as `_link_remedies` keys it. Keyed by `casefold()`, the chain `openß, openss, openInk` wrote one entry for its two links, so the first link, `openß`, was offered the later link's words (that the scaffold reads no source for it) instead of its own `--pin-source`. New test: test_two_pin_files_one_casefold_are_each_offered_their_own. The one conflict, in `scripts/repo_shape.py`, keeps both `_chain_link_findings` docstring paragraphs and #638's key with this branch's `LinkPins` return. R444's N2: the naming gate's `LinkSources` docstring cited `validate-pins.py` by line numbers that no longer held the lines, and now names `PinSources.__init__` and `resolve_neutral_pin_source`. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main gained #563's own #590 (fe237ec) and #599 (32c22b0), whose trees this branch already held through #599's 750366b (#599's final head, eaa0cda, is 750366b plus a merge of main), and #626, #633, #642, #641, #643, #644, #632, #636, #631, #638, #635, #640, #623, #637 and #622. No conflict: the merged tree is 9be0d0d, the tree `git merge-tree --write-tree` predicted. AGENTS.md, docs/cli.md and tests/test_repo_hygiene.py merged as text; README.md and docs/handbook.html are unchanged by the merge. README.md is 1461 lines, its cap (1449 + 12, tests/readme-cap.d/563.txt), and AGENTS.md 494, its cap. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… tab shows the one-line install for Git Bash, held to the other copies by a test, and the notes are current to main d4a7fab with the fifteen pull requests since 4cf5fb0, the environments, the tag ruleset and the rehearsal (#563, #461) The handbook (#461's line "The handbook's step 3 has no Git Bash path", which lane 3 told lane 5 on #473 that #630 settles): step 3's install block was `data-platform="linux mac wsl"`, so a reader on the Windows tab never saw the one line the Windows panel says Git Bash runs. The Windows tab now has a lead, "In Git Bash, the bash that Git for Windows installs, the one-line install works as on Linux:", the Linux tab's block copied byte for byte as `data-lang="sh"`, and the panel's caveat: the preflight it ends with cannot take a typed `yes` in Git Bash. README.md is unchanged: no test asked for it, and it is at its cap. tests/test_repo_hygiene.py: the test above it finds one copy of the one line per file; `test_every_handbook_copy_of_the_one_line_install_is_the_same` holds every handbook copy (the masthead's and step 3's two) to ONE_LINE_INSTALL, so a copy edited on one tab alone fails. A drifted Windows copy fails it; the page as committed passes. The release notes: - "What changed since v1.0.1" gains one entry per pull request that landed after 4cf5fb0 and is not #563's own, from `git log --first-parent 4cf5fb0..origin/main` at d4a7fab: under update-shape.py and bump-leg.py, #644, #633 and #631; under scaffold, family and the materializer, #622, #643, #640, #641, #623, #638, #635, #626, #637 and #642; under shape-doctor.py, #636 and #632. Each from its squash commit and the issues GitHub says it closes. - The list's placeholder says it is current to main d4a7fab and stays a placeholder for what lands after it; the counts are refreshed inside their placeholder (66 pull requests, 75 of 97 milestone issues closed). - #590 and #599 are cited beside #563, and "Before tagging" says they landed (fe237ec, 32c22b0) and lists the 22 open milestone issues. - "How this release is cut": Brett Heap's word at 2026-10-16T12:06:40Z, the environments testpypi (12:07:31Z) and pypi (12:07:39Z), the tag ruleset 25237399 (12:08:14Z), and the rehearsal, run 38898735926 (12:10:53Z-12:12:00Z, success, both uploads skipped), each read back from the API. The trusted publishers' date stays a placeholder. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-text: remedies keyed by pin_product_key, the key the chain readers use (#444 x #610) The remedies keying fix is in the merge below, b7472df: `_link_remedies` keys a link's remedy by `pin_product_key`, and `_chain_link_findings` finds it by that key, as #638's chain readers do. Keyed by `casefold()`, the chain `openß, openss, openInk` wrote one entry for its two links, so the first link was offered the later link's words. This merge adds #623 and #637, and git merged them clean. One more change rides with it (#607 x #610): `_lockstep`'s remedy now ends with #637's BRANCH_LEFT sentence, as each remedy that names `_switch_line` does. Without it, #637's guard, test_every_remedy_that_names_a_switch_line_ends_with_the_sentence, and its partner test fail on the merge. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… member clone URL `.gitmodules` cannot hold as text, `--dry-run` alike, each clone-URL remedy names a link's target and a lone surrogate, and the READMEs quote the clone URL, so the line under `a b/` pastes as one command (#693) (#812) #640 (#433) asked `unwritable_reason` of the clone URL the scaffold and `family.py init` write into their READMEs. Four things were left, each measured on main 8062810: - `family.py add --local-remote-dir <dir>/caf<e9>` exited 0, dry run and real, and committed the raw byte into the holder's `.gitmodules`, where the holder's `make siblings` (`siblings.submodule_urls`, `git config --list` read with `errors="replace"`) read the URL back as `caf\ufffd`; a line break was escaped as `\n` by git and read back cut short at it, and an escape and a bidi override were committed raw. `cmd_add` now asks `_refuse_an_unwritable_member_url` of `_member_url`'s result, after the branch question and before `_probe_member`'s throwaway clone, so the real run and `--dry-run` refuse alike, `family-unwritable-value`, with nothing read, mounted or written. - `--local-remote-dir link`, a UTF-8 name linked to a `caf<e9>` directory, was refused naming the RESOLVED URL by a remedy that said to pass a path that is UTF-8 text, which it was. Every clone-URL remedy now says the URL is the path the flag resolves to, and that where the path given is text and the URL is not, a link's target (or, for a relative path, the directory it was given from) is the one to rename. - `unwritable_reason` names a lone surrogate, which a Windows name can hold, and no clone-URL remedy did; both now say "or a lone surrogate", as #282's `UNWRITABLE_REMEDIATION` does. - `git clone --recurse-submodules {{CLONE_URL}}` (holder) and `{{CLONE_URL}}`/`{{ASSEMBLY_CLONE_URL}}` (assembly root, both legs) took a local path raw, so under `a b/` or `it's/` the line split when pasted. Both tools now render the value through `shell_quoted(url, "posix")`, POSIX whatever the host as #532 rules for pasted commands: a GitHub URL and an ordinary ASCII POSIX path stay bare, so the templates and their pins are unchanged, but a path holding `é`, and every Windows path (it is spelled with `\`), is quoted too, as `shell_quoted`'s conservative set has it, and `test_clone_url_bytes.py`'s two `café` README asserts now expect the quotes. The README remedies say what that does: every character but an apostrophe is written as itself. The link sentence and the surrogate are static text in the remedies, not a branch on what was typed: it is true of every refusal (including #640's relative path under such a directory), and it leaves `cmd_init`'s and `_scaffold`'s call sites untouched. `add`'s remedy shares `init`'s words (`LOCAL_REMOTE_DIR_REMEDY`) with its own verb, GitHub URL and reasons: `.gitmodules` holds what `README.md` cannot, so its reasons say how the holder reads it back, not that it cannot be written. And it says what the directory must hold, which is the caller's (`{holding}`): `init` and the scaffold make the bare repository there, so a directory made anew is a way out, but `add` reads the member's `<Project>.git` from it, so a new, empty directory is refused again with `does not exist`; `add`'s remedy says the path must hold that repository, its directory renamed or the repository moved. Closes #693 Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…efusals and `family-operation-in-progress` name a path as `shown_source` names it, so an ESC or a line break in a path no longer reaches the terminal (#692) (#731) #622's folder refusals printed a path as it was. `family.py init --into <a file named wd-ESC[31mRED> --dry-run` gave `REFUSED family-folder-cannot-be-made: the family folder .../wd-^[[31mRED/Orbit cannot be made: ...` through `cat -v`. A name with a line break and then `WARNING forged-line: pwned` printed that line as a line of its own. R612's `family-operation-in-progress` printed the mark's path across two lines for a holder whose path holds a newline. Each path is now spelled as `shape_materialize.shown_source` spells a `--pin-source`: as it is, and as Python's `repr` spells it where a character in it is not printable. - `scripts/shape_materialize.py`: both answers of `folder_cannot_be_made` (a file or a bad stat in the way, and a folder this user cannot write in), the folder in `refuse_unmakeable_folder`, and the folder in `make_folder`. - `scaffold-project.py`: `scaffold-target-exists`. - `scripts/family.py`: `family-target-exists`, and the mark's path in `family-operation-in-progress` (`where.as_posix()`). A printable path is named byte for byte, so no existing row changes and `why.endswith(CANNOT_WRITE_IN)` in `refuse_unmakeable_folder` still holds. No help text changed, so `docs/cli.md` is untouched. No README, handbook or template sentence says how a refusal names a path (`git grep` of `shown_source`, `target-exists` and `folder-cannot-be-made` in AGENTS.md, README.md, `docs/handbook.html` and the templates found none), so no doc changes. - **`shown_source`, not `shown_path`.** Since #634, `shape_materialize.shown_source` returns `repo_shape.shown_path(source)` (`scripts/shape_materialize.py`: `return shown_path(source)`), so the seven call sites this change adds (four in `shape_materialize.py`, one in `scaffold-project.py` and two in `family.py`) spell a path as `shown_path` does. Renaming them to `shown_path` stays out of this PR: it would only move imports in `scaffold-project.py` and `family.py` and reword docstrings in `family.py`, `shape_materialize.py` and the test file, and the review's N1 left it optional. I did not take it. The predicted merge with main works with the alias: `family.py --help` and `scaffold-project.py --help` exit 0 on it, and `shown_source` and `shown_path` return the same string for an ESC name, a line-break name and a plain path. - **`shown_source` is imported by name into each file's existing `from shape_materialize import (...)` block.** The first commit put it on a line of its own after the block; lane 4's #660, #688 and #693 add an import at that same place, and `merge-tree` reported add/add conflicts in `scripts/family.py` and `scaffold-project.py`. The second commit moves the name into the block, and those conflicts are gone. - **"The holder in the other refusals of the same block" is one refusal.** The `# What the holder is in the middle of, and what its sparse checkout leaves out` block holds `family-operation-in-progress`, `family-sparse-excluded` and `member-path-unaddable`. Only the first prints a path of the holder's. `family-sparse-excluded` names `members/<Project>` and `.gitmodules`, and `member-path-unaddable` names `members/<Project>`. - **No scaffold row for `--local-remote-dir`.** A control character there is refused earlier as `scaffold-unwritable-value`, which already names the path with `repr`. The unit test covers `make_folder`, which only a race reaches by the command line. `tests/test_dry_run_parity.py`: - `SCAFFOLD_CASES` gains five rows: ESC and line break in the `--work-dir` name, ESC in a folder name above it, and ESC and line break in a work dir that holds a file named `Atlas` (`scaffold-target-exists`). - `FAMILY_CASES` gains five rows: ESC and line break in `--work-dir`, ESC in `--into` (`the family folder`), and ESC and line break in a work dir that holds a file named `Orbit` (`family-target-exists`). - Two rows join `UNWRITABLE_FOLDER_AT` (ESC in the scaffold's and in `init`'s `--work-dir`, a folder this user cannot write in). - Rows named `ESC-...` and `line-break-...` are skipped on Windows, whose file names cannot hold them (`POSIX_NAMES`, `case_params`). Every expectation spells the path with `repr`, and not with `shown_source`. - `assert_refused` now fails any stderr line that starts with `WARNING forged-line`. - Three unit tests: `test_each_folder_question_spells_a_name_that_is_not_printable` (ESC, line break; `folder_cannot_be_made`, `refuse_unmakeable_folder` and `make_folder` by themselves), `test_a_folder_this_user_cannot_write_in_is_spelled_safe_to_show`, and `test_a_printable_name_is_named_byte_for_byte` (a name with a space and an accented letter stays unquoted). `tests/test_family_refuse_up_front.py`: `test_a_holder_path_with_a_newline_still_shows_its_merge` asserted the raw newline in the refusal (`"Ink\nRouter/.git/worktrees/" in ran.stderr`). It is now `test_a_holder_path_with_a_control_character_still_shows_its_merge`, over a line break, an ESC and a forged line, and asserts the one-line `repr` form (`refusal()` fails unless the stderr is two lines). It is the one existing test this change had to edit. Results at head `ab55c29f` (Python 3.12.3, Linux): - `tests/test_dry_run_parity.py tests/test_dry_run_asks_remotes.py tests/test_family_dry_run.py tests/test_family_init_landing.py tests/test_repo_hygiene.py tests/test_pin_source_unreadable.py tests/test_scaffold_leg_path_collisions.py tests/test_scaffold_reuse_seeded_legs.py`: **494 passed**. - `tests/test_family.py tests/test_family_refuse_up_front.py tests/test_family_init_values.py tests/test_family_add_refusals.py`: **356 passed, 19 skipped** (the skips are `test_family_init_values.py:219` and `:291`, template-quoting premises). - Not run: the full suite (the coordinator's gate), and macOS and Windows (CI is Linux-only since #597). The new hostile-name rows are POSIX only by design. On the base code: the review overlaid this head's two test files on main `2fac9790` and ran `tests/test_dry_run_parity.py` and `tests/test_family_refuse_up_front.py` with `-k "dry_run_parity or control_character"`. The result is **20 failed, 87 passed** (76 deselected, 43.5 s). The 20 are exactly the new and edited tests: the 5 scaffold and 5 `init` rows, the 4 unwritable-folder rows, the 3 unit tests (`test_each_folder_question_spells_a_name_that_is_not_printable` in two cases, and `test_a_folder_this_user_cannot_write_in_is_spelled_safe_to_show`), and the 3 cases of the edited `test_a_holder_path_with_a_control_character_still_shows_its_merge`. The one new test that passes on main's code is `test_a_printable_name_is_named_byte_for_byte`, by design: a printable name is named byte for byte on both. An earlier draft of this body said 21 failed, 2 passed. That run was on `8062810d` with a wider selection, and it held a scaffold row for ESC in the local remote dir's name (`ESC-in-the-local-remote-dir-name`) that this head's tests no longer have; its second pass was the existing `test_the_refusal_quotes_gh_s_first_line_and_escapes_what_is_unprintable`, which also passes in the new run, one of the 87. `family.py init` and `scaffold-project.py --dry-run` against a file named `wd-ESC[31mRED`, a file named `wd-\nWARNING forged-line: pwned`, and a work dir with a file `Atlas` or `Orbit` in it, run with a stub `gh` and no real `gh`: - On main, through `cat -v`: `... the family folder /.../wd-^[[31mRED/Orbit cannot be made: /.../wd-^[[31mRED is a file, not a folder`, and a stderr line `WARNING forged-line: pwned cannot be made: ...` of its own. - On this head: `REFUSED family-folder-cannot-be-made: the family folder '.../wd-\x1b[31mRED/Orbit' cannot be made: '.../wd-\x1b[31mRED' is a file, not a folder`, and one line for the forged name: `--work-dir '.../wd-\nWARNING forged-line: pwned' cannot be made: '...' is a file, not a folder`. `scaffold-target-exists` and `family-target-exists` are one line each in the same form. Each spelled site reverted alone in a throwaway clone at `19d126cd` (the call sites are the same at `ab55c29f`); the new tests that fail for each: - `folder_cannot_be_made`, the `_not_a_folder` answer: 8 failed (six CLI rows and the two unit tests). - `folder_cannot_be_made`, the `CANNOT_WRITE_IN` answer: 5 failed (the four unwritable rows and the unit test). - `refuse_unmakeable_folder`: 12 failed. - `make_folder`: 2 failed (the unit test only). - `scaffold-target-exists`: 2 failed (its two rows). - `family-target-exists`: 2 failed (its two rows). - `family-operation-in-progress`: 3 failed (line break, ESC, forged line). `git merge-tree --write-tree` of this head `ab55c29f` against origin/main `20c8873b` and against every ref on origin that could meet it, fetched 2026-10-16: the 41 branches (`fix/*`, `l3-*`, `wip/*`, `docs/*` and `release/v1.0`) and the 55 PR heads from #630 up. That is 96 refs. (The same list was run for #730's head `7f2bb36b`.) - **origin/main `20c8873b`: clean** (the predicted tree `00b5b24c`, which differs from main in these five files only: 216 lines added, 25 removed). - **#630's head `6fd33a93`: clean.** It conflicted with main in `docs/handbook.html` when this body was first drafted, and no longer does, with or without this change. This change touches no README or handbook. - **The refs that edit one of this change's five files**, and so could meet it, all merge clean with this head: `fix/646`, `fix/651`, `fix/654`, `fix/660`, `fix/661`, `fix/684`, `fix/686`, `fix/688`, `fix/689` and `fix/693`, the heads of #670, #725, #728, #743 and #744, and the heads of the landed #634, #638, #639, #640, #643 and #671. #634 has landed (`30676c90`), so the `fix/660` note about a conflict with it is gone. - **Four refs conflict with origin/main on their own**, and so with this head: `docs/release-notes-v1.0.0` and `release/v1.0`, and the pre-squash heads of the landed #637 and #641. Their conflicts do not come from this change. Every other ref, 92 of the 96 in all, is clean with this head, including the refs that conflict with main in `templates/family-root/scripts/validate-family.py` (#682, #745, #747, `fix/648`, `fix/681`, `fix/704`) or in `shape-doctor.py` (#742, `l3-683`) on their own. So do the old heads of the landed #638 and #639, and lane 3's `l3-677-pins-hand-step-wip` branch, and each of them merges clean with this head too. - **Tests on the predicted merge**, run by the review on the merge with main `2fac9790`: `test_dry_run_parity`, `test_family_refuse_up_front`, `test_pinned_copy_text`, `test_pin_source_unreadable`, `test_dry_run_asks_remotes`, `test_family_dry_run` and `test_family_init_landing`: 445 passed in 215 s. `test_family`, `test_family_init_values`, `test_family_add_refusals`, `test_derived_id_refusals`, `test_repo_hygiene`, `test_naming_policy`, `test_scaffold_leg_path_collisions` and `test_scaffold_reuse_seeded_legs`: 758 passed, 19 skipped in 243 s. I did not re-run them on `20c8873b`; the merge is clean, and the two commits since `2fac9790` (#700 and #702) change only `shape-doctor.py`, `templates/assembly-root/scripts/validate-pins.py` and four test files, none of them a file of this change. - Before the second commit, `fix/660`, `fix/688` and `fix/693` each conflicted with this head in the import region of `scripts/family.py` or `scaffold-project.py` (a separate import line each side added at the same place). That is why the import joins the existing block. #634 is on main, and this change does not edit `shown_source` or its docstring in `scripts/shape_materialize.py`. The review approved with nits. Its S1, the stale base-run numbers and merge lines, is taken here, in this body: no code changed, and the head is still `ab55c29f`. Its N1, renaming the call sites to `shown_path`, is not taken (see "Decisions and their evidence"). It also re-ran 5 of the 7 mutation probes on `ab55c29f` and got the same failure counts as above (12, 2, 3, 2 and 2), and ran 144 hostile-name dry runs (16 names in 9 scenarios): on main 69 rows put a control character on stderr and 6 print a forged line of their own; on this head all 144 exit 2 with no traceback, no control character and no forged line. - `scripts/family.py` `init`'s plan prints `folder` and `landing` lines with the path raw (`print(f"landing {holder} ...")`, `print(f"folder {folder} ...")`). On a dry run or a refused run the plan is on stdout before the refusal, so `WARNING forged-line: pwned/Orbit (--work-dir: no family folder)` still appears there. It is the plan, not a refusal, and not one of the issue's lines. - Other refusals that name a holder or folder path raw, none of them in the issue's lines: `family-holder-detached` and `family-on-tracking-branch` (`{family.root}`), `family-index-locked` (`lock.as_posix()` in the detail), `family-landing-in-standard` (`{holder}`, `{checkout}`, `{beside / family / family}`), `family-holder-already-here` (`{into}`), and `family-remote-exists` and `scaffold-remote-exists` (`{target}`, a local remote path). 1. **family.py `init`'s plan prints the holder and folder paths raw.** `scripts/family.py` `landing` and `folder` rows (about :1756-1759) print `holder` and `folder` as they are, so a forged line or an ESC reaches stdout in a `--dry-run` and in a run refused after the plan. Direction: spell them with `shown_source`. 2. **The same raw spelling in the refusals listed above.** `family-holder-detached`, `family-on-tracking-branch`, `family-index-locked`, `family-landing-in-standard`, `family-holder-already-here`, `family-remote-exists` and `scaffold-remote-exists` each print a path the person supplied or git keeps. Direction: the spelling this change gives its sites. Review: rv692, lane 5's independent review, approved ab55c29 with nits, and its body fix (S1) is published. CI (Brett Heap's ruling 27 and his fast-refresh ruling of 2026-10-15T22:48:35Z): H0 ab55c29 passed tests, tests-git-bash and SonarCloud; the head 13f643d is a clean update-branch merge of main 431c8cf, with tests-git-bash and SonarCloud green; and the local gate passed on its predicted squash tree 4327947 over main 431c8cf: the dry_run_parity, family_refuse_up_front, repo_hygiene, agents_md_codes, cli_reference sets, 330 passed. Landed on Brett Heap's word of 2026-10-17T10:59:39Z, relayed by lane 3 on #473: "merge lane 5's approved v1.1.0 drafts". Closes #692 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>



Lane: openreposhape-4 (openRepoShape-4)
Claim: #433 (comment)
What changed and why
Under
--local-remote-dirthe clone URL of a new repository is the bare repository's own path.scaffold-project.pyrenders it asCLONE_URLandASSEMBLY_CLONE_URLinto the READMEs of the assembly root and of each leg, andfamily.py initrenders the holder's into the holder's README, all throughwrite_lf, which encodes strictly. A directory whose name is not UTF-8 reachessys.argvas a string with a lone surrogate, so on main (re-measured at8f453d0e,LANGunset):UnicodeEncodeErrorinwrite_lfafterAtlas.git,Atlas-spec.gitandAtlas-code.gitexisted and the work directory heldAtlas-spec;family.py initexited 1 the same way afterContoso.gitexisted and the family folder was made;--dry-runexited 0 for both and said nothing;adopt-project.pyasks the question before any leg exists (#255,_refuse_an_unwritable_clone_url). Both tools now ask it of the same value with the sameunwritable_reason, and refuse in adopt's words, before any remote or folder exists and under--dry-runalike:scaffold-project.py:scaffold-unwritable-value, the code 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 gave the free-text values._repository_urlsmoves up to just after the name-length check and_refuse_an_unwritable_clone_urlasks the assembly root's URL there, so the refusal comes before_declarationrunsgh apiand before a work directory or remote exists. Only the assembly root's URL is asked: it is the one a file holds, and the legs' sit beside it in the same directory, so what makes one unwritable makes all three so.scripts/family.py:family-unwritable-value, a new code of the same family, asked right after_holder_remote.The sentence, scaffold:
under --local-remote-dir the assembly root's clone URL is the bare repository's own path, '<url>', which the scaffold writes into theREADME.mdof the assembly root and of each leg, and it holds a byte that is not UTF-8.The family's:... the holder's clone URL is the bare repository's own path, '<url>', whichinitwrites into the holder'sREADME.md, and .... TheRemediation:says nothing was created and--dry-runrefuses the same, to pass a--local-remote-dirwhose path is one line of UTF-8 text with no control character (renamed or made anew), or to run against GitHub, where the clone URL ishttps://github.com/<org>/<name>.git, and why each kind of byte cannot be written.The issue offered reusing
_refuse_unwritable_valueswith the URL among its fields. I did not: its remediation tells a person to retype a value, and a directory is renamed or replaced, not retyped. The code, the question and the sentence's skeleton are the same.Tests
tests/test_clone_url_bytes.py, 30 tests, on the #282 file's harness (scaffoldandno_processimported fromtests/test_scaffold_free_text_values.py) andtests/test_family_init_values.py's (FAMILY,family_module):<dir>/caf<e9>, plus a line feed, an escape and a U+202E in the directory's name, each tool, real run and--dry-run, exit 2, nothing on stdout, the whole of stderr compared, the remote directory still empty and the work directory or family folder absent; the scaffold under a guard that fails if any process starts,initunder one that allows onlygit check-ref-format(scaffold-project.py holds--tracking-branchonly tochecked_value's alphabet, so a name git refuses as a branch (~,a..b,x.lock, a trailing/) passes--dry-runand, against GitHub, dies at the firstgit init -baftergh repo createhas made all three repositories #283's question, asked first);--local-remote-dir rehearsalrun from the bad directory (the URL asked is the resolved one);cafédirectory still plans and still scaffolds / creates the holder, the README holding the URL as written.Where the disk cannot hold the name (macOS, Windows) the test skips with a reason; none skipped here. New file alone: 30 passed. Targeted run on the committed bytes, 76 files (every
test_scaffold_*,test_family*,test_setup_*, and each file naming the scaffold,family.pyorsetup-project.pythree or more times, plustest_repo_hygiene.py,test_cli_reference.py,test_agents_md_codes.py): 4620 passed, 31 skipped (template-quoting, Windows-only and PowerShell skips), exit 0, 48 min. After main moved to4cf5fb07, which touches none of the three files, the new file and its two harness files on a scratch merge with main: 249 passed, 19 skipped.Mutants, each run on the new file alone:
init's callFix round 1 (
ac404626, after review R433 found SonarCloud's gate in ERROR, from the test file only; no behaviour changed):tests/test_clone_url_bytes.py:71: thebidi-overriderow held a RAW U+202E in the file (S6389, new-code security rating C), against the file's own promise that every invisible character in it is an escape. It is now"\u202eevil".encode("utf-8"), the same bytes. A scan of the file for any control, format, line-separator, private-use or unassigned character finds none; the only non-ASCII characters left are the visibleéof the issue's own<dir>/cafécontrol.tests/test_clone_url_bytes.py:149:assert path.is_dir() and os.listdir(path) == []is two assertions (S9073).scripts/family.py:1140: the comment aboveCLONE_URL_REMEDIATIONnow says it is what afamily-unwritable-valuefor the holder's clone URL is told to do, as the scaffold's twin already does, because bump-leg.py, scaffold-project.py and family.py:git status --porcelainandgit config user.nameare read as bytes, so a file name that is not UTF-8 isbump-leg-root-dirtyor the DIRTY warning and auser.namethat is not UTF-8 is refusedfamily-unwritable-valuenaming--created-by, where each ended in a UnicodeDecodeError traceback (#423, #426) #641 gives the same code a second remediation (a creator read fromgit config user.name). Comment only.tests/test_clone_url_bytes.py,tests/test_scaffold_free_text_values.py,tests/test_family_init_values.py,tests/test_family_dry_run.py,tests/test_family_init_landing.py,tests/test_repo_hygiene.py,tests/test_cli_reference.pyandtests/test_agents_md_codes.py: 453 passed, 19 skipped (16 and 3 template-quoting skips intest_family_init_values.py), exit 0, 1:29. The new file still collects 30 tests.Main merge (
541f3aec, after #641 landed as42f64111):tests/test_clone_url_bytes.py,tests/test_non_utf8_git_reads.py(#641's),tests/test_family_refuse_up_front.py(#626's),tests/test_scaffold_free_text_values.py,tests/test_family_init_values.py,tests/test_family_dry_run.py,tests/test_family_init_landing.py,tests/test_repo_hygiene.py,tests/test_cli_reference.pyandtests/test_agents_md_codes.py, on the committed merge,GIT_ALLOW_PROTOCOLunset: 589 passed, 19 skipped (the same 16 and 3 template-quoting skips intest_family_init_values.py), exit 0, 2:58.What it cannot tell
--work-dir,--intoand--sourceholding a non-UTF-8 byte are not in the issue and not touched.family.py add --local-remote-dir <bad>runs to exit 0 (git writes the URL into.gitmodules); that URL's readers are adopt-project.py:_electorand_remote_repositoryreadgit config user.nameand the origin URL throughgit_out, which decodes strictly, so a name or URL that is not UTF-8 endsplan(and, for the URL,checkandexecute) in a UnicodeDecodeError traceback, exit 1, naming neither the git setting nor the flag that avoids it #292's and scripts/shape_advisory.pyrepository_namesand shape-doctor.pyorigin_urlandorigin_namereadgit remote get-url originthroughgit_outand catchRefusalandOSErroronly, so an origin URL that is not UTF-8 makesreadanswernot-answered(a single repository loses the Triad advisory, a leg clone its pointer to the assembly root) and endsshape-doctor.py --rootin a UnicodeDecodeError traceback, exit 1, where an ASCII origin gives NOT A SHAPE ROOT, exit 2 #385's.docs/release-notes/v1.0.0.md:452still lists scaffold-project.py and family.py init with--local-remote-dirunder a directory whose name is not UTF-8 put that path into the clone URL they render into the new repositories' files, andwrite_lf(strict UTF-8) raises a UnicodeEncodeError traceback, exit 1, after the bare remotes exist (--dry-runexits 0), where adopt refuses an unwritable clone URL before any leg exists #433 as a v1.0.0 limit. It is history for that release and is left as it is.Shared files
scaffold-project.pyandscripts/family.py, and the new test filetests/test_clone_url_bytes.py; the diff against main is those three files and no others. No--helptext, sodocs/cli.mdis unchanged, and no README or handbook sentence became false. No pinned shape copy is touched.Main is merged in. Head
541f3aec, a--no-ffmerge of main42f64111(which holds #641, #626, #633, #642 and #590, among others, since this branch's base8f453d0e) into fix round 1ac404626. Exactly one hunk conflicted, inscripts/family.py: the comment above the one sharedfrom shape_materialize import unwritable_reasonline, which #433 and #641 each gave its own comment.scaffold-project.pyauto-merged. The hunk, markers included, is resolved to these five lines (reviewer R423 tested them byte for byte), as committed:py_compilepasses onscripts/family.py, which holds bothCREATOR_UNWRITABLE_REMEDIATION(#426) andCLONE_URL_REMEDIATION(#433), oneunwritable_reasonimport and no duplicate definition.git merge-tree --write-treeof this head541f3aec, each head read withls-remotejust before:42f64111592d3611c189c62e(= this head's own tree: main is already in it)fix/421-push-branch-refspec1fe37647d9308d90f0ba0caffix/609-dry-run-parity227bc3b01c08ef44f058f3f1fix/602-free-plan-hint-root10dda171dd7738e99bd5767f#626 and #641 are in main now, so they are not listed.
Closes #433
🤖 Generated with Claude Code
Summary by Sourcery
Refuse unwritable local clone URLs before creating repositories or project folders in scaffold and family initialization.
Bug Fixes:
Enhancements:
Tests: