Repository navigation
family Makefile: siblings, park and resume refuse by name when scripts/siblings.py is absent, and update-shape.py says the Makefile it copies calls it, so a holder updated without --add no longer fails on can't open file (#489) - #503
Conversation
…refuse by name when `scripts/siblings.py` is not there, and `update-shape.py` says its Makefile calls it, so a holder updated without `--add scripts/siblings.py` no longer dies on `can't open file` (#489) A family holder cut before 2026-09-09 (#76) has no `scripts/siblings.py`. `update-shape.py apply` without `--add` (AGENTS.md's default: an added file only on the human's word, per file) still copies the changed Makefile, whose `siblings`, `park` and `resume` targets run that script, so all three died on `python3: can't open file` after an update that exited 0 and printed only `note ... not added`. The three targets now ask make itself (`$(wildcard)`) whether the script is there and refuse by name (`$(error)`, exit 2, no shell involved) with one shared message: what is missing, that `update-shape.py` adds a file only on the human's word, `update-shape.py check --root <holder>` to read the verdict, then the apply command it prints with `--add scripts/siblings.py`. With the file there the recipes are what they were. `check` and `apply` say, on the line that already reports the file, that the Makefile they copy calls it, so those three targets cannot run until it is added. `apply` still copies the Makefile, adds nothing unasked and exits 0. AGENTS.md says so in place (item 6 of the family section, same line count). Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-5, 2026-10-13T00:15Z) @codex review |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
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 19 hours by commenting |
Reviewer's GuideThe PR prevents legacy family holders from reaching Python's opaque missing-file error by adding a shared Make-level guard to the three affected targets, while keeping Sequence diagram for guarded family Make targetssequenceDiagram
participant User
participant Make
participant Siblings as scripts/siblings.py
User->>Make: make siblings, park, or resume
Make->>Make: wildcard scripts/siblings.py
alt script missing
Make-->>User: error: target needs scripts/siblings.py
else script present
Make->>Siblings: python3 scripts/siblings.py [target arguments]
Siblings-->>User: target completes
end
Sequence diagram for consent-based family holder updatesequenceDiagram
participant User
participant Update as update-shape.py
participant Holder
User->>Update: check --root HOLDER
Update-->>User: Makefile upstream-changed, script upstream-added
User->>Update: apply --root HOLDER
Update->>Holder: copy changed Makefile
Update-->>User: note scripts/siblings.py not added
User->>Update: apply --root HOLDER --add scripts/siblings.py
Update->>Holder: add scripts/siblings.py
Update-->>User: family update complete
Flow diagram for missing siblings script remediationflowchart TD
A[Run make siblings, park, or resume] --> B{scripts/siblings.py exists?}
B -->|Yes| C[Run scripts/siblings.py]
B -->|No| D[Make refuses by name]
D --> E[Run update-shape.py check --root HOLDER]
E --> F[Review apply command]
F --> G[Run apply with --add scripts/siblings.py]
G --> C
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
…gs.py` is also said for a holder already at the target, the Makefile's refusal names the `copy-missing` exit for a deleted pinned copy, AGENTS.md keeps "per file", and the test's docstrings say it runs on Windows (#489) Review nits N1-N4 of #489's independent review. - N2: `left_dead_by` also speaks when the calling copy is `unchanged`, the state a holder is in after its first `apply` without `--add`. Every later `check` and `apply` found the Makefile `unchanged` and the script `upstream-added` and said nothing. A `locally-modified` or `both` copy still says nothing. The doctor's `shape currency` row is unchanged. - N3: the Makefile's message ends with the case where `check` says `copy-missing` (the pin has a row and the file was deleted, so `--add` refuses it): restore it from the holder's history. Still `$(error)`, still no shell. - N4: AGENTS.md item 6 keeps "per file", in the same five lines. - N1: the test's docstrings no longer claim a skip or a pass they cannot know: it has no `WINDOWS_SKIP`, and a fixture that checked out the real commit could not run on a shallow checkout. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… writes its pin file as the scaffold does, so an adopted root passes `validate-pins.py` (#273); a `--pin-source` that cannot answer is `pin-source-unreadable` (#339) (#508) **Brought up to date after #409 squash-landed** by merging origin/main (9736424, #409's squash) as f9ed46f. Its `scaffold-project.py` conflict was resolved to this branch's side once `git diff b70a07d 9736424 -- scaffold-project.py` (#409's final head against its squash) came out empty. So the file is the one tested on this branch plus main's own later changes to it, and holds nothing of #409's; details below. This branch carries #275's adopt half. It was built on `fix/pin-source-env-collisions`, PR #409 (#275's scaffold half and its fix round), merged at 6494714 as 583f647, over the earlier 5690235 (34dbd86). After 6494714, #409 only merged main, five times, up to its final head b70a07d, and none of those merges has a hand edit (`git show --remerge-diff` is empty for each). Adopt's refusal of two products that share a source variable goes through #409's function, so this branch lands after #409. It was never rebased. `git diff b70a07d 9736424` is empty for each file #409 touched: `scaffold-project.py`, `scripts/repo_shape.py`, `templates/assembly-root/scripts/validate-pins.py` and `tests/test_pin_source_env_collisions.py`. What f9ed46f resolves, four files with one hunk each: - **`scaffold-project.py`.** - The conflict hunk is main's block from `PIN_COLLISION_REMEDIATION` through `_pin_collision_remediation`, which #409's squash edits: it adds `PIN_VARIABLE_REMEDIATION`, `_colliding_pin_variables` and `_pin_collision_remediation` and changes `_refuse_colliding_pins`. This branch moved all of that to `scripts/shape_materialize.py`. The hunk takes this branch's side. (The merge commit's message says "re-adds"; this is the exact account.) - #409's auto-merged import of `PIN_SOURCE_ENV_PREFIX, pin_source_env_collisions` is dropped, because nothing in the file uses either name any more. - It is not `git checkout --ours` of the whole file. Main changed the file beyond #409 since this branch last merged it: #355's `--tracking-branch` check (6c2a371) and a73b4e9's `RULESET_HINT`. Those lines stay. - The result is the file `git merge-tree` gives for this branch with main before the squash (518010b). Its diff from the file tested here has the same patch-id as main's own 3ef059c..518010b change to it. - **`templates/assembly-root/scripts/validate-pins.py`.** Main's side, which is #409's final blob, with main's #359 workflow reader in it. This branch had no change of its own to the file beyond #409. - **`adopt-project.py`, `_commit_the_split`.** - Main: #445 wraps the function in its late-failure `try`, and #394 builds an `append` with the plan's submodule record. - This branch: its only edits there were a comment and `neutral_pins=_recorded_pins(plan)` on the materializer call. - Both now sit on main's call inside the `try`: `append=append, neutral_pins=_recorded_pins(plan)`. - **`docs/cli.md`.** It is regenerated with `scripts/render-cli-reference.py`, and `--check` is ok. It differs from main's only in the `--pin`, `--pin-owner` and `--pin-source` help of `adopt-project.py plan`. `scripts/repo_shape.py`, `templates/assembly-root/scripts/validate-pins.py` and `tests/test_pin_source_env_collisions.py` are main's blobs, which are #409's: this branch adds nothing to them. Nothing of #409 is lost. Its scaffold behaviour is `shape_materialize.refuse_colliding_pins` and `pin_variable_collisions`, keyed by #409's `pin_product_key`. **73d65c6, after the merge.** - **The change.** `_read_pins` reads `plan.get("pins")` where it read `plan.data.get("pins")`. - **Why.** #445's `tests/test_adopt_late_failure.py` hands `_commit_the_split` a `SplitPlan` as its plan, a `dict` that adds `entries`. On f9ed46f, three of its tests stopped at "committing the split" with `AttributeError: 'SplitPlan' object has no attribute 'data'` (3 failed, 83 passed in that file). - **Nothing else changes.** `Plan.get` with no default returns `plan.data.get(key)` exactly, so a loaded plan reads as before. `adopt-project.py plan --pin` took a NAME only (`--pin openGlass`) and refused a trailing `@<commit>`. `plan`, `check` and `execute` each exited 0, and `execute` printed `adoption verified`. The manifest then listed the name in `neutral_product_pins:` with no `contracts/<product>-pin.yaml` beside it, and on its first run the adopted root's own `validate-pins.py` refused it as `neutral-pin-missing`. #273's table, reproduced on c7c02fa with the issue's own commands (local bare remotes, and a stub `gh` that exits 97): | `--pin` | c7c02fa | this branch | |---|---|---| | `openGlass` | `execute` exit 0, `- openGlass`; clone's `validate-pins.py` exit 2 `neutral-pin-missing` | `plan` exit 2 `pin-malformed`, with the scaffold's remediation (`--pin openGlass@<40 hex>`); no plan is written | | `opensoft/openGlass` | exit 0, `- opensoft/openGlass`; exit 2 `neutral-pin-missing` for `contracts/opensoft/openglass-pin.yaml` | `plan` exit 2 `pin-malformed` | | `spec` | exit 0; `validate-pins.py` exit 0, reading the leg's `contracts/spec-pin.yaml` as the neutral pin | `spec@<sha>` is `adopt-pin-collides` at `plan` | | `shape` | exit 0; `contracts/shape-pin.yaml` read as the neutral pin | `shape@<sha>` is `adopt-pin-collides` at `plan` | | `openGlass@<sha>`, `opensoft/openGlass@<sha>` | `plan` exit 2 `adopt-pin-malformed` | `plan`, `check` and `execute` exit 0, and `gh` is called 0 times. `contracts/openglass-pin.yaml` is written and the manifest lists `- openGlass`. In the clone, `validate-manifest.py` exits 0, and `validate-pins.py` exits 0 with `openGlass: pin commit … digest recomputes` | Brett ruled for decision 1's first option ("go with your recommendation on #273"): adopt takes the scaffold's pin and writes the scaffold's pin file. That reverses the NAME-only `--pin` from #6, and the comment that described it is replaced with the new contract. - **One implementation.** The scaffold's pin code moved into `scripts/shape_materialize.py`, next to `neutral_pin_path` and the materializer that writes the file. What moved: - `PIN_ARG_RE`, `parse_pin`, `pin_digest_from_source` and `pin_digest_from_gh`; - `declared_pin_values`, `declared_pin_products` and `PIN_COLLISION_REMEDIATION` (in #309's wording); - from #275's branch, `PIN_VARIABLE_REMEDIATION` and the variable check; - `_refuse_colliding_pins`, split into the pure `pin_collisions` and `pin_variable_collisions`, and `refuse_colliding_pins(refusal=...)`, which raises one refusal for both. The code did not move verbatim. `_refuse_colliding_pins` became the three functions above; `pin_digest_from_source` was rewritten for #339 (below); and fix round 1 changed three of them (below): `pin_variable_collisions` keys each pin by #275's `pin_product_key`, `pin_digest_from_source` spells a path that is not printable, and `pin_digest_from_gh` refuses a missing `gh`. The scaffold's behaviour is otherwise unchanged. `scaffold-project.py` imports what it calls under its old names. Lane 5's review ran 34 scaffold cases (every pin refusal, the `gh` stub's four modes, dry and real successes) on 34dbd86 and on this branch at 7a94b6b. rc, stdout, stderr, the `gh` log and the files made were identical, apart from the shape's own commit and digest and #339's row. Its pin tests and #275's pass as they are. `tests/test_git_output_decoding.py`'s exemption for `pin_digest_from_gh` follows the function to its new file. - **`plan`.** `--pin`, `--pin-owner` and `--pin-source` mean what they mean for the scaffold. `plan` reads the digest from `--pin-source` or `gh api`, and records product, repository, commit, digest and `digest_source` in the plan, so `check` and `execute` read no network for a pin. `plan` refuses: - a name with no `@<commit>`, as `pin-malformed`. That includes an empty `--pin ''`, which adopt used to drop and now refuses, as the scaffold always has; - `spec`, `code` and `shape`, in any case, as `adopt-pin-collides` (#273 decision 4), in the scaffold's sentence and remediation (#241, #309); - one product given twice, the same way, whether it is a repeat or two names that share one file (decision 5); - two products that one `SHAPE_PIN_SOURCE_<PRODUCT>` names, such as `open-glass` and `open_glass`, the same way, through #275's check; - a leg path over a pin's file, as `adopt-leg-path-collides`; - a `--pin-source` path that `check` would refuse in `digest_source:` (fix round 1), as `adopt-unwritable-value`. It is asked only when a `--pin` records the path; - with no `gh` on PATH and no `--pin-source`, `gh-not-found` (fix round 1). - **`check` and `execute`.** These are new `check` findings, and `execute` refuses each one before any leg exists. They come from `_leg_findings`, the list `check` and `_refuse_what_check_finds` share: - `plan-pin-unrecorded`: a name alone (an old plan's `pins: [openGlass]`, or a hand edit), or an entry missing its commit or digest; - `plan-pin-malformed`; - `plan-pin-collides`, which covers two pins at one file, a pin at a file the shape writes, and two products that share one source variable; - `plan-root-holds-pin`: a source path kept in the root at the pin's file. The materializer would write the shape's pin beside it under `shape/`, and `validate-pins.py` would read the source's file instead. How each finding ends: - `plan-pin-unrecorded` and `plan-pin-malformed` end with the `plan --pin <product>@<40 hex commit>` that records the pin whole. - `plan-pin-collides` ends with re-running `plan` with each product once, or, for two products that share a source variable, with one product of each such group. - `plan-root-holds-pin` ends with the kept path's own remedy: send that entry to a leg, `drop` it, or rename it in the source and re-run `plan`. A leg moved over a pin's file in the plan is `plan-leg-path-collides`. - **Also part of #386: its `pins:` row.** A hand-edited `pins:` that is not a list (a name such as `pins: openGlass`, a number, a mapping, a quoted string) is now `plan-pin-malformed` in `check` (`` `pins:` is 'openGlass', not a list of pins ``), and `execute` refuses it before any leg exists. - Where: `_read_pins` in `adopt-project.py`; the test row is `plan-pin-malformed-not-a-list` in `tests/test_adopt_pins.py`. - Before: on main, `check` said `plan ok` for the name, the mapping and the string, and gave a traceback for the number (reproduced by lane 5's review on cb7063f). #386 records that `execute` then wrote the name as eight one-letter pins. - Not fixed here: #386's other keys, `allow_empty_legs:`, `paths:`, `legs:` and `source:`, so #386 stays open. Lane 2's #435 takes all of #386, `pins:` included, and its `pins:` rule keeps only text items where this branch writes mappings; whichever lands second reconciles them (#435 (comment), and "Where the meaning meets" below). - **`execute`.** It writes each pin through `materialize_assembly_root(neutral_pins=...)`. The manifest lists the product with the owner stripped (decision 3), so the entry and the file agree. - **Docs.** `--help` is updated and `docs/cli.md` regenerated (`render-cli-reference.py --check` ok). `pin_digest_from_source` caught only `Refusal`. A directory that is no clone is git's own failure, so it was always `pin-source-unreadable`, exit 2. A path `subprocess.run` cannot enter never reaches git: it is a `FileNotFoundError`, `NotADirectoryError` or `PermissionError` out of `cwd=`, and nothing caught it. `scaffold-project.py --pin ... --pin-source <that>` ended `--dry-run` and the real run alike in a traceback, exit 1. Since the move above, `adopt-project.py plan --pin` reads its digest through the same function and ended the same way. Both reach it through `declared_pin_values`, and `check` and `execute` read no source, so that call is the only reader. The issue's table, run on ab623dd for the scaffold and on d806e8f for `adopt-project.py plan` (adopt has no `--pin-source` on ab623dd), then on this head. `gh` is a stub that logs and exits 97, with `GH_TOKEN` bogus and an empty `GH_CONFIG_DIR`; it was called 0 times in every run: | `--pin-source` | before | this branch | |---|---|---| | a directory that is not a clone | `pin-source-unreadable`, exit 2 | unchanged | | a path that does not exist | `FileNotFoundError` traceback, exit 1 | `pin-source-unreadable`, exit 2: `<path> could not answer for opensoft/openGlass @ <40 hex>: no such directory` | | a file | `NotADirectoryError`, exit 1 | exit 2, `... it is a file, not a directory` | | under a parent this user cannot enter | `PermissionError`, exit 1 | exit 2, `... Permission denied` | | a symlink loop (not in the issue) | `RuntimeError` out of `Path.resolve()` on 3.12, exit 1 | exit 2, `... Too many levels of symbolic links` | The real run and `--dry-run` give the same stdout and stderr, and nothing exists afterwards (no remote directory, no work directory; for adopt, no plan file). What changed, in `scripts/shape_materialize.py` only: - `pin_digest_from_source` catches `OSError` as well as `Refusal` and raises the existing `pin-source-unreadable` with its remediation unchanged. The message names the path and the reason. - `stat()` judges the source inside the guard, as #312 does in `validate-pins.py`, and the sentences are that file's: a missing path, or one beneath a file, is "no such directory"; a file is "it is a file, not a directory"; any other `OSError`, from `stat()` or from git's `cwd=`, is the system's own reason (`strerror`). It is not `exists()`/`is_dir()`, which before 3.14 swallow only a missing path. A source `stat()` can read but `cwd=` cannot enter, a directory with no search bit, fails in `tree_digest` and the same guard answers it. - `declared_pin_values` resolves the path through a guarded `_resolved_pin_source`. `Path.resolve()` raises a `RuntimeError` on a symlink loop before 3.13 (an `OSError` since), before the digest was asked for. As typed, `stat()` meets the loop and says so. - A source that answers is read exactly as before: a relative path resolves against the working directory, a symlink to a clone is followed, `digest_source` reads `local-clone (<path as typed>)`, and the digest is `tree_digest`'s. `tests/test_pin_source_unreadable.py` (46 tests in the #339 commit; fix round 1 adds 17 for S1 and moves one, below) runs each case through `scaffold-project.py --dry-run`, its real run and `adopt-project.py plan`: missing, a file, beneath a file, relative and missing, a parent and a source with no search bit (skipped on Windows and as root, by a mark, so before any fixture builds a directory), a 300-character name, a symlink loop (skipped on Windows) and a directory that is no clone. Each asserts exit 2, the refusal naming the path and the reason, its remediation, no traceback and nothing created. It also holds `--dry-run` and the real run to the same stdout and stderr, checks the path is printed escaped under an ASCII stderr and as a surrogate when it is not UTF-8, and checks that a relative source and a symlink to a clone read as before. The session `gh` guard fails any test whose run called `gh`. origin/main ab623dd reached this branch through implQ's branch: 34dbd86 is a30091f merged with ab623dd. One file conflicted there, `templates/assembly-root/scripts/validate-pins.py`, in seven hunks, because #312 (8cdca5e) replaced `keyed_sources` and `default_source` with `PinSources` and #275 added `env_shared` and `shared`. #312's blob is the same at its PR head, at the squash and at ab623dd, so it landed the file implQ resolved against. The resolution keeps #312's side in all seven and adds #275's on top: | # | hunk | resolution | |---|---|---| | 1 | `resolve_neutral_pin_source` signature | #312's `sources: PinSources` and 3-tuple return, plus #275's `env_shared: bool = False` | | 2 | its docstring tail | #312's `Returns (path, how, typed)` text, plus #275's "Rule 2 is skipped when `env_shared`" paragraph | | 3 | `_recompute_neutral_pin_digest` signature | #312's `sources`, plus #275's `shared: frozenset[str]` after `report` | | 4 | the `resolve_neutral_pin_source(...)` call inside it | #312's 3-tuple unpack, plus `env_shared = product.casefold() in shared` and the extra argument | | 5 | `_check_one_neutral_product_pin` signature | #312's `sources`, plus #275's `shared` | | 6 | its call of `_recompute_neutral_pin_digest` | `sources, report, shared` | | 7 | `_check_neutral_product_pins` docstring and body | #312's `PinSources(...)` and `_note_unused_pin_sources`, plus #275's paragraph, `_report_pin_source_collisions(products, report)` and `shared = frozenset(pin_source_env_shared(products))` | This branch's own merge of 34dbd86 (5690235) had no conflict, and neither had its merge of #409's fix round, 6494714 (583f647). That fix round made `neutral-pin-source-collides` a note and keyed "one product" by `repo_shape.pin_product_key`. At 421e4e5, `templates/assembly-root/scripts/validate-pins.py`, `scripts/repo_shape.py` and `tests/test_pin_source_env_collisions.py` were byte-identical to the tree `git merge-tree` gives for 6494714 with origin/main 3ef059c (blobs 5604007, 091966b, 5220787), so this branch added nothing to those three files beyond #409. The 34dbd86 merge brought in #335, #312, #317, #270 (f795cf7), #344 and #349 with no conflict. Earlier merges of origin/main (87373a6, for #309's reworded constant, and e43fa95) are unchanged. Fix round 1 merged origin/main three more times: - **cd1fc79 (2bcfa69).** It brought #245, #351, #350, #345, #372, #371, #354, #380, #377 and the rest since ab623dd, with no conflict. - **45e01f4 (4098b61).** It brought #396's `tests/test_agents_md_codes.py`. That test finds `scaffold-pin-collides` as a string constant of `scripts/shape_materialize.py`, which `SHIPPED` lists. - **3ef059c (421e4e5), #314.** One conflict, one hunk in `adopt-project.py`. This branch's `_refuse_an_unrecordable_pin_source` and #314's section "The project must be able to hold what `execute` writes into it (#261)" were added at the same place; both are kept. Where the two meet, lane 5's earlier recipe applies: - `_flag_plan` builds each pin as the `PIN_FIELDS` mapping `plan` writes, not `list(pins)` of products. So the plan `plan` asks the validator about is the plan it writes. - #314's `test_plan_asks_what_check_asks_of_the_same_keys` plans `--pin openGlass@<commit> --pin-source <stand-in>` where it planned the name alone, which `plan` now refuses. Then f9ed46f, after #409 squash-landed, and 73d65c6 after it (top of this description). On the merged tree exactly one test failed: `tests/test_adopt_template_values.py::test_check_finds_and_execute_refuses_a_hand_edited_value[pins-surrogate]`. It plans, then hand-edits the plan to `pins: ["openX\ud800"]`, a name-only pin with a lone surrogate. Before this branch the refusal's headline was `REFUSED plan-unwritable-value`. A name-only pin in a plan is now `plan-pin-unrecorded` (#273), so `check` has a second, independent finding for that very plan, and the headline is the first finding's: ``` REFUSED plan-pin-unrecorded: 2 finding(s) ... FINDING plan-pin-unrecorded: `pins:` entry 1, 'openX\ud800', is a name with no commit and no digest ... FINDING plan-unwritable-value: a `pins:` item is 'openX\ud800', which `execute` writes into the assembly root's `project.yaml`, and it holds a lone surrogate ... ``` Nothing about #270's contract changed. The lone surrogate is still found as `plan-unwritable-value`, listed under the refusal, and `execute` still refuses before any leg exists. `check` builds `_leg_findings`, where `_pin_findings` sits, before it calls `_value_findings`, so the pin finding comes first. d806e8f changes one assertion for that row, `pins` only: the headline is `REFUSED plan-pin-unrecorded: ` where it was `PLAN_REFUSED`. The other 15 rows and the finding assertions, including the `plan-unwritable-value` line, are untouched. Lane 5's review of 7a94b6b found 3 should-fix and 5 nits. Each is answered here: - **#409's `pin_product_key` (review N1).** The merge of #409's fix round could not carry it, because this branch had moved the function that used it out of `scaffold-project.py`. `pin_variable_collisions` now keys each pin by that function, where it casefolded. `openß` and `openss` are two pin files and one `SHAPE_PIN_SOURCE_OPENSS`, and a casefold named each of the two pins twice. No `--pin` or recorded `pins:` entry `PIN_ARG_RE` admits can spell either, so the test asks the function directly. `refuse_colliding_pins`' docstring now says, in #409's words, that the root's `validate-pins.py` only notes such a pair. - **S1, a `--pin-source` path that is not printable.** `pin_digest_from_source` names the path as typed unless it is not printable (`str.isprintable`), and then as Python's `repr` spells it (`shown_source`). That covers the refusal's head and git's own reason, which names the directory it ran in. Printed raw, an ESC reached the terminal, and a line break or a carriage return began a line of the path's choosing, a forged `REFUSED` line among them, from the scaffold and from adopt. The traceback #339 replaced had shown a `repr`. A printable path is named byte for byte as before. - **S2, no `gh` on PATH.** `pin_digest_from_gh` refuses as `gh-not-found`, exit 2, as `repo_shape.tree_digest_from_gh` names it, and offers `--pin-source`. It was a `FileNotFoundError` traceback, exit 1, from the scaffold and, new with this branch, from `adopt-project.py plan --pin`. - **S3, a hand-edited `commit:` or `tree_sha256:`.** The lane's decision is to accept it and document it (option (c)); no behaviour changed. A pair that disagrees passes `check` and `execute` (`adoption verified`), because neither reads a network for a pin, by design. The adopted root's own `validate-pins.py` then exits 1 with `neutral-pin-digest-mismatch` wherever it can read the product. `check` now says so once, after the pins it will write: "each pin as `plan` read it, not read again here: the root's own `validate-pins.py` recomputes its digest wherever it can read the product". Options (a), `execute` recomputes before it creates the legs, and (b), `check` recomputes from a clone still on the machine, are filed as #449. - **N2, a `--pin-source` that `plan` cannot record.** `plan` asks `check`'s question (`one_printable_line`) of the path it records in each pin's `digest_source:`, before any digest is read. It refuses one that fails as `adopt-unwritable-value`, in #270's words (`unwritable_reason`), or as "an invisible formatting character" where those name nothing. Before, it was planned, exit 0, and printed raw in the report. `check` then refused it as `plan-pin-malformed` and said to re-run the same `plan`. The report's `pin` line and `check`'s `write` line now spell the digest's source with `_shown`. The scaffold half is pre-existing (a line break in the path is written into the pin file; a byte that is not UTF-8 dies after all three repositories exist) and is filed as #450. - **N3, six hunks no test guarded.** Each now has a test that fails when it alone is reverted: - the two name guards (a `pins:` name holding an ESC and a forged line); - the lower-casing of the commit and the digest (an upper-cased hand edit); - the `digest_source` printability check; - the not-text check (`commit: 1234567` unquoted); - the gitlink filter (a submodule kept where the pin is written is `plan-submodule-root` alone). - **N4.** This body. - **N5, #339's third decision.** A `--pin-source` that nothing reads is still silent, now in adopt too. This PR does not decide it. #451, filed, lays out refuse, note and leave, so closing #339 loses nothing. Lane 5's second review, of 421e4e5, found no blocking item, 2 should-fix items and 3 nits. The two should-fix items are this body: the landing after #409 (above) and #386 (above). The code needed no change for them. f63d1ed adds two tests and no code: - **n1. `plan` asks nothing of a `--pin-source` no `--pin` records.** With the `args.pin and` guard dropped, such a path holding an ESC or a zero-width joiner was refused in a sentence naming no file ("`execute` writes into , and ..."), and every test still passed. `test_a_pin_source_no_pin_records_is_not_asked_about` holds the plan to exit 0 and `pins: []`, with the path named nowhere in the plan or the report. The ESC row is POSIX-only by the file's `POSIX_NAMES` mark; the zero-width joiner row runs everywhere. It holds #451's answer today, silence, so a decision there changes this test on purpose. - **n2. A printable `--pin-source` is printed as it is.** With `_spelled` in place of `_shown` in `plan`'s `pin` line, every test still passed. `test_a_printable_pin_source_is_printed_as_it_is` holds `plan`'s `pin` line, and `check`'s `write` line for the plan `plan` wrote, to their exact text, ending `(local-clone (<path>))` with no backtick. - **n3** needs a code change and is left out (below). **Fix round 2, on f63d1ed.** Each run had a `gh` stub first on PATH (it logs and exits 97), `GH_TOKEN` bogus, an empty `GH_CONFIG_DIR`, `git-filter-repo` 2.47.0 next on PATH, and the identity blanked with `GIT_CONFIG_COUNT`; the session `gh` guard came first inside each run. - **The pin set: 812 passed, 2 skipped.** It covers adopt pins, adopt plan, #339's file, #275's tests, the pin and neutral-pin validators, git output decoding, adopt template values, CLI reference, scaffold pin and reuse, scaffold pin collisions, #314's two files and #396's file. The skips are `test_adopt_plan.py:403` (no MedxEHR clone) and `test_adopt_template_values.py:223` (this host reads `git config user.name` from the repository, not from `GIT_CONFIG_GLOBAL`). That is fix round 1's 809 passed, plus the three new rows. - **Shape doctor and scaffold leg-path collisions: 369 passed, 3 skipped** (the PowerShell line exists only on a Windows host). - **Mutation probes**, each one line changed in a throwaway worktree, over `tests/test_adopt_pins.py` and `tests/test_pin_source_unreadable.py` less the five end-to-end adoptions: - the `args.pin and` guard dropped: 2 failed, both n1 rows; - `plan`'s `pin` line `_spelled`: 1 failed, the n2 test; - `check`'s `write` line `_spelled`: 2 failed, the n2 test and `test_an_upper_cased_hand_edit_is_written_lower_cased`; - `plan`'s `pin` line raw: 1 failed, the no-break-space test that already guarded that hunk. **On the merged tree, 73d65c6** (the merge f9ed46f and the `_read_pins` commit), with the same harness: - **The pin set** above, with #445's `test_adopt_late_failure.py`, `test_adopt_git_reads.py` and `test_adopt_filter_repo_failure.py` and #387's `test_adopt_empty_symlinks.py` and `test_adopt_root_manifest.py`: **988 passed, 2 skipped**. The skips are the two above. - **Shape doctor, scaffold leg-path collisions and the adopt suites:** **967 passed, 3 skipped** (the PowerShell line). The adopt suites are e2e, execute refusals, verify, root shape paths (#387's too), leg-path collisions, clone template, ignored shape files and submodules. - **On the merge alone, f9ed46f** (the same tree as before the `_read_pins` commit): `tests/test_adopt_late_failure.py` gave 3 failed, 83 passed, the three `AttributeError` rows above. - `render-cli-reference.py --check` is ok on the merged tree. pyflakes reports the same four warnings on `adopt-project.py`, `scaffold-project.py` and `scripts/shape_materialize.py` as before the merge. **Fix round 1, on 421e4e5.** Each run had `git-filter-repo` 2.47.0 on PATH and the identity blanked with `GIT_CONFIG_COUNT`. A `gh` stub that logs and exits 97 was first on PATH, with `GH_TOKEN` bogus and an empty `GH_CONFIG_DIR`, and the session `gh` guard came first inside each run. - **The pin set:** 809 passed, 2 skipped (the same files and skips as above). - **Doctor and scaffold:** 782 passed, 3 skipped (the PowerShell line exists only on a Windows host). It covers shape doctor, scaffold leg-path collisions, scaffold e2e, referent chain links, naming policy, repo hygiene and #351's free-text values. - **The adopt suites:** 882 passed. It covers e2e, execute refusals, verify, spec-only, leg paths, root shape paths, plan escapes, leg-path collisions, clone template and submodules. - **Before the #314 merge,** on 74ce4d9's tree: the first two sets gave 577 passed, 2 skipped and 782 passed, 3 skipped. On 2bcfa69's tree (the merges alone, before the fix) they gave 542 passed, 2 skipped and 782 passed, 3 skipped. After the 45e01f4 merge, `tests/test_agents_md_codes.py`, repo hygiene and CLI reference: 110 passed. - **Lane 5's second review** also ran 421e4e5 merged with main cb7063f and with 53bee5b: the pin set, the manifest-value set, and the doctor set with #357's `tests/test_doctor_validator_rows.py`. All passed, with only the skips above. Proof that fix round 1's tests test something: - **On 2bcfa69's tree,** with this head's two test files laid over it, 31 of the new rows fail: - S1: 17; - N2 and its report spelling: 7; - S2: 3; - the `pin_product_key` port: 2; - `check`'s S3 line: 1; - the not-UTF-8 `adopt plan` row: 1. The N3 rows pass there, as they must: they guard hunks this branch already had. - **Mutation probes.** Each hunk was reverted alone, in a throwaway worktree, and both test files were run: 18 of 18 were caught. - **The port:** the casefold keying (2 fail); the casefold lookup alone (2). - **S1:** the path named raw (13); git's reason named raw (2); every path `repr`'d, printable or not (27). - **S2:** no `except` (3). - **N2:** no `plan` check (11); only #270's question asked (1); the formatting-character reason said otherwise (1); the report's `pin` line raw (1); `check`'s `write` line raw (1). - **S3:** the line dropped (1). - **N3:** the `_pin_fix` name guard (1); the `_read_pins` path guard (1); the lower-casing (1); the `digest_source` check (1); the not-text check (1); the gitlink filter (1). - **The issue rows reproduced.** S1, S2 and N2 were reproduced on 2bcfa69's tree and shown gone on the fix (a probe driving both tools; `gh` was called 0 times). **Before fix round 1.** Runs were on 7a94b6b, with `git-filter-repo` 2.47.0 first on PATH and the repository-local `user.name` blanked with `GIT_CONFIG_COUNT` (a workstation whose standard checkout sets one fails `test_the_scaffold_refuses_with_no_elector` on main itself; CI sets none). 7a94b6b differs from ffd1297 in `tests/test_pin_source_unreadable.py` only, so the three suites that do not read that file ran on ffd1297: - the new file: 46 passed; - adopt pins, adopt plan, git output decoding: 172 passed, 1 skipped (the MedxEHR clone is absent); - scaffold pin and reuse, pin collisions, e2e, referent chain links, neutral pin and pin validators, #275's tests, naming policy, repo hygiene, CLI reference, scaffold leg-path collisions: 571 passed; - shape doctor: 254 passed, 3 skipped (the PowerShell line exists only on a Windows host); - the new file with repo hygiene, git output decoding and CLI reference, once more after the last edit: 176 passed. Earlier on 5690235 and d806e8f, before the #339 commit: the adopt suites (e2e, execute refusals, verify, spec-only, leg paths, root shape paths, plan escapes, leg-path collisions, clone template) 740 passed, and `tests/test_adopt_template_values.py` with #273's other adopt tests 489 passed, 2 skipped. They were not re-run on 7a94b6b: the #339 diff touches `pin_digest_from_source` and the call in `declared_pin_values`, which those suites reach through `test_adopt_pins.py` only. Nothing was run on macOS or Windows. Proof the new tests test something: - **On d806e8f.** 38 of the 46 fail, each on the traceback and its exit 1. The 8 that pass are the unchanged behaviour: the not-a-clone refusal (4 rows) and the valid-source rows (4). - **Mutation probes.** Ten hunks reverted one at a time, each failing at least one new test: the `OSError` catch (16 fail), the catch narrowed to `OSError` only (21), the missing-path clause (9), the clause narrowed to `FileNotFoundError` (3), the `S_ISDIR` judgement (3), the loop guard (4), no resolve at all (3), the old `.resolve()` call (4), the reason as `str(exc)` (12), and the file sentence as the OS's (3). - #273's own 52 tests still fail on c7c02fa (51 fail or error; the one that passes checks the test's own table), and its 18 mutation probes were each caught. **Re-verified after the merge.** An independent re-verification of f9ed46f and 73d65c6 found the code correct: - #409 is carried whole. A 30,000-case fuzz of `--pin` lists through #409's final refusal and this branch's port differed in 0 cases. - `scaffold-project.py` equals the tested file plus main's own changes that are not #409's (same patch-id). - `_commit_the_split` keeps #445's `try` and #394's `append`. - `_read_pins` reads a loaded plan as before. Its findings were all in this description (the #506 bullet and three stale facts), now fixed. Runs on the head and on its merge with main 48585e6: 988 passed, 2 skipped; and 967 passed, 3 skipped. `git merge-tree --write-tree` was run at 01:48Z on 2026-10-13, comparing hunks per file. Each PR head, fetched from `refs/pull/<n>/head` with no `gh`, was merged with 73d65c6 and with origin/main 9736424 alone. Two sets were asked about: - every PR this description listed before that main does not now contain; - every PR numbered above #468 whose head main does not contain: #473, #474, #490, #491, #493, #494, #495, #496, #497, #503, #504 and #505. **Open state, read with `gh` by the coordinator at 02:00Z and refreshed at 02:25Z after a re-verification on main 48585e6.** Every pull request named below is open. #184, #94, #96, #98, #100 and #161 are closed, and #465 landed as 48585e6. `fix/adopt-plan-quote-yaml-keywords` has no pull request. #506 (`fix/scaffold-reuse-seeded-legs`) opened after this list was made, and the re-verification reads it. - **Landed since the last draft of this list:** #343, #346, #355, #358, #373, #378, #382, #387, #394, #397, #401, #404, #409, #419, #452, #456, #465, #466 and #484. #394's `_commit_the_split` conflict and #401's `docs/cli.md` conflict are both resolved in f9ed46f, above. - **Clean, 20:** #266, #347, #356, #374, #376, #384, #395, #453, #467, #468, #473, #474, #490, #491, #493, #494, #495, #497, #503 and #505. #453, #468 and #491 were updated onto main since the first read and now merge clean. #507 (lane 5's #486-#488) shares only the generated `docs/cli.md` with this branch and merges clean with it. - **Conflicting with main alone, and this branch adds no hunk:** - #402: `shape-doctor.py` 5; - #496: `shape-doctor.py` 10 and `tests/test_shape_doctor.py` 1. **Conflicts this branch adds:** - **#435** (3efcc7a, lane 2's `fix/adopt-plan-field-types`, for #386). - It adds 2 hunks in `adopt-project.py`: 5 here, against 3 with main alone. They are `render_plan`'s `pins:` line and `Plan.pins`. - Keep this branch's `_pin_lines` and `_pin_products`. The rest is under "Where the meaning meets". - **#504** (37ef29a, `fix/adopt-root-kept-symlinks`). - It adds 1 hunk in `adopt-project.py`: 2 here, against 1 with main alone. It is the materializer call in `_commit_the_split`. - Keep both arguments and both comments: `append=append, neutral_pins=_recorded_pins(plan), spell=_spelled`. - **#506** (119d8c4, `fix/scaffold-reuse-seeded-legs`). - It adds 1 hunk in `scaffold-project.py`. With main alone, #506 conflicts only in `docs/handbook.html`. - #506 inserts `LEG_TEMPLATES` right after `declared_pin_products`, which this branch moved out, and rewrites `_reusable_remotes` further down. Keeping both sides breaks the file with an unterminated string literal. - Keep only #506's `LEG_TEMPLATES` block. Drop #506's `declared_pin_values`/`declared_pin_products`, which moved here, and this branch's three-line `_reusable_remotes` opener, which #506 rewrites below. Resolved that way, pyflakes is clean and #506's scaffold tests pass (88). - **`fix/adopt-plan-quote-yaml-keywords`** (ed9968f; a branch with no pull request). - It adds 2 hunks in `adopt-project.py`: 3 here, against 1 with main alone. - One is `render_plan`'s `pins:` line. Keep `_pin_lines`, which already sends product, repository and `digest_source` through `y`. - The other is `_refuse_unholdable_values`' signature. Keep `pins: dict`, with that branch's `_validator_judged` and `_named_as_read` above it. - `fix/adopt-late-failure-notice` is no longer on origin. #445, the late-failure notice, is on main and merged here. **Where the meaning meets:** - **#435 and #386.** The coordinator has said this on #435: #435 (comment) - #435's `_plan_key_findings` (`plan-bad-pins`) keeps only text items in `pins:`, and its `PINS_REMEDIATION` tells a person to write `pins: [openGlass]`. - This branch writes each pin as a mapping, and it makes a name alone `plan-pin-unrecorded`. - Read from #435's code, not run: on a tree with both, the plan `plan --pin` writes is `plan-bad-pins` in `check`. - Whichever lands second reconciles them, with three changes: - `_plan_key_findings` takes this branch's mappings, or leaves `pins:` items to `_read_pins`; - a `pins:` that is not a list gives one finding, not both `plan-bad-pins` and `plan-pin-malformed`; - `PINS_REMEDIATION` says the mapping `plan` writes. - Until that is done, `tests/test_adopt_pins.py::test_a_plan_with_a_whole_pin_checks_ok` fails. - **#314.** It landed, and it is merged here (above). - **`fix/adopt-plan-quote-yaml-keywords`.** Its pin tests are written against the name-only `--pin`. They need the `name@commit` form, and their expected `pins: [...]` line becomes `_pin_lines`'s mapping. - **Two unwritable values are refused in two runs (lane 5's second review, n3).** `plan --elected-by 'A<LF>B' --pin openGlass@<sha> --pin-source <a path holding an ESC>` refuses the elector alone, exit 2. Re-run with the elector retyped, it refuses the source; #270's refusal names every unwritable value at once. Reproduced on 421e4e5, with `gh` called 0 times. One refusal would need a code change, not made here: - `_refuse_an_unrecordable_pin_source` would split into a function that returns the sentence; - `_checked_plan_inputs` would ask it before #270's `unwritable_values` raise, and raise one `adopt-unwritable-value` that names both, with both remediations; - a test would add the two-value row. N2's question is `one_printable_line`, not `unwritable_reason`, so it cannot simply join #270's list. - **Docs.** The README, AGENTS.md and `docs/handbook.html` are unchanged, because none of them calls adopt's `--pin` a name. A sentence saying adopt now takes the scaffold's `--pin` is a follow-up. It costs an AGENTS.md line-cap raise, and a README edit forces the handbook's README blob to change. - **A product spelled `null`, `true` or `false`.** The scaffold already accepts `--pin null@<sha>`: it exits 0 and builds a root whose `validate-pins.py` exits 1 `neutral-pin-entry-malformed`. Adopt's `plan` writes `product: null` through `y`, and `check` then finds `plan-pin-malformed` and refuses it before any leg exists. The one fix for both tools is in `parse_pin`, and it would change the scaffold's behaviour. - **Wording shared with the scaffold.** `digest_source` is written bare into the pin file, so a `--pin-source` path holding ` #` is cut short there. `templates/assembly-root/contracts/neutral-product-pin.yaml` and `validate-pins.py`'s `neutral-pin-missing` remediation name only the scaffold as the writer. Two comments in `scripts/repo_shape.py` still name the scaffold's `pin_digest_from_gh`, in a digest-pinned shipped copy. - **Filed as follow-ups:** - #449: S3, a hand-edited `commit:` or `tree_sha256:` that disagrees, with options (a) and (b); - #450: the scaffold half of N2, a `--pin-source` path no file can hold; - #451: #339's third decision (N5), a `--pin-source` that nothing reads. - **`validate-pins.py` prints a `--pin-source` path raw** in `neutral-pin-source-unreadable`, the twin of S1. It is a shipped template that #409 and other open PRs edit, so it is left for its own change. No issue is filed for it yet. - **Windows and macOS** were not run here; CI ran both (below). Two assertions that depend on what Windows answers for a path (a name past the filesystem's limit, a path beneath a file) check only the refusal and its remediation there. Fix round 1's rows that need a control character or a byte that is not UTF-8 in a path, or a symlink to `git`, are skipped on Windows by a mark, and so is fix round 2's ESC row. The rows for a no-break space and a zero-width joiner run everywhere, and so does the in-process `gh-not-found` test. Reviewed independently by lane 5 at 7a94b6b (3 should-fix, 5 nits: fix round 1) and at 421e4e5 (0 blockers, 2 should-fix, 3 nits: fix round 2). After #409's squash, the re-verification of f9ed46f and 73d65c6 found the code correct; its 1 should-fix and 3 nits were in this description and are fixed. GitHub's update-branch then merged main twice (33bae3f, 2170526), with no hand edit. CI's tests, tests-macos and tests-windows and SonarCloud all passed at 2170526 (main cac6bb2 merged in). On that head merged with main 57c85e9 (#490, #474 and #468, none of which touches this branch's files), the pin and adopt set with `tests/test_update_shape_landing.py` gave 1112 passed, 2 skipped, and the doctor, adopt end-to-end, unfinished-merge and bootstrap set gave 1056 passed, 3 skipped (the doctor's PowerShell line, which exists only on a Windows host). Closes #273 Closes #339 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… false are corrected to what the tools do on main (#483) (#505) **Closes #483.** #483 is the v1.0.0 "README and AGENTS.md accurate" criterion. Three read-only audits (AGENTS.md; README.md lines 1-727; README.md lines 711-end) checked every statement that names a command, a flag, an exit code, a refusal, a file or a step against the code at `ec4a7a0e` and ran the tools offline (local bare remotes, a stub `gh`, a blank git identity). They found 38 false or incomplete statements. This pull request corrects 28 of them (27 whole, and D2-F1 in part), plus two the independent review found (the `MISPLACED` text after #394, and `openRepoShape --preflight` "offers to install" git-filter-repo): 29 rows in all, D1-F9 and D3-F7 sharing one. The other ten, and the rest of D2-F1, were settled by pull requests that landed while this one was open: #484 and #465 settle six (D1-F8, D2-F7, D3-F6; D2-F13, D2-F14, D3-F8), and #491 and #468, to which this branch had routed them, settle four (D1-F1, D2-F5, D1-F2; D3-F11) and the rest of D2-F1. The two routed to #490 (D1-F9, D3-F7) were still false once it landed, and are corrected here. A last read of every line main has added to README.md and AGENTS.md since `ec4a7a0e` found nothing else false, so nothing is left open under #483. Two independent reviews, six rounds. The first head (`bfbd4eeb`) came back CHANGES NEEDED: two corrections that were themselves untrue (exit 3; "each of those tools has its own section") and nine gaps. The second (`ccf520bd`, on a merge of `7a8b981d`, where #394 landed) fixed them and came back with no blocker and three landing changes, which the third commit (`f35e59fa`, on a merge of `5a253877`, where #484 landed) makes: D2-F5 and D1-F1's block go back to #491 (at `2d59d141` its `setup-project.py` reads the yes from the controlling terminal, so the piped one-liner asks once it lands, and its new test strips `<...>` from README.md, which a `bash <(` line breaks); D3-F5 is fixed here now that #484 has landed; and the routes are re-derived at the current heads. The same commit leaves AGENTS.md's family steps 3 and 5 as main has them, so #486-#488 (`10c93ded`) still merges clean. The fourth round merges `48585e62`, where #465 landed (README.md at 699-709, 1028 and 1095 and the handbook beside them, none of which this branch edits), and re-reads the three #465 rows on main. The fifth merges `57c85e92`, where #468 landed (the doctor's `merge` row), as `fb6a9ab4`: the doctor's verdict table, in README.md and in the handbook, takes this branch's `COMPLIANT`, `SHAPE BEHIND` and `DRIFTED` rows and main's `MISPLACED` and `INVALID` rows, and the handbook footer keeps both clauses (#424, then #483) with its blob recomputed. `aa4e1f59` (#508) then merged clean, as `83cfad54`. The sixth merges `70d0d1b3`, where #491 landed (setup asks at the person's own terminal), as `94793aeb`: README.md's "What Dana sees" paragraph takes #491's text whole with this branch's D2-F6 ("a hyphenated name"), and the handbook footer keeps #476's and #492's clauses ahead of #483's, with its blob recomputed. The same round re-reads every routed row on main, corrects D1-F9 and D3-F7 (`ea634043`), and reads what main changed in both files since `ec4a7a0e`. The first reviewer's round four came back APPROVE WITH NITS (0/0/3) and asked for one word, made at `9ac90d16`: "a hyphenated project name", since the leg names are hyphenated and pass. **What.** `README.md` and `AGENTS.md` only, plus `docs/handbook.html` where it repeats a corrected sentence, with its footer's `README.md blob` recomputed (`git hash-object README.md` = `1417b9ec0235675da8f73ca023756411bc3fbe8a`). No tool, test, template or `docs/cli.md` changes. `README.md` hashes identically at `ec4a7a0e`, `437a360a`, `d222a124` and `7a8b981d`, so its audit line numbers held; `AGENTS.md` changed only by #394 (it adds 5 lines after line 154 and 3 more after the doctor's item 4); #484, #465 and #491 since edited both files, and #468 README.md, without changing either total. Each fix is the smallest true sentence, keeps the surrounding voice, and states only what the code or a run showed. **The line caps.** `tests/test_repo_hygiene.py` holds `AGENTS.md` to 490 lines (#394 raised it from 482) and `README.md` to 1422, and both files stand exactly at their caps, so every correction is line-neutral. To give back the lines the longer sentences need, paragraphs that no correction touches are re-wrapped to 80 columns with no word changed: - README.md, eight paragraphs no correction touches: macOS (`and refuses anything below 3.9`), `2. Login.`, the InkRouter ruling quote, `Each leg is pinned TWICE`, the neutral-product election paragraph (`Ruled by Brett Heap on 2026-09-05`), `Its entries are adopt-project.py plan's entries`, `This repository implements neither`, `A FAMILY holder carries both verbs too`; and the tail of the `--link-source` paragraph that D2-F12 edits. None is in README.md's family section (903-925 on main), which #486-#488 edits. - AGENTS.md, four paragraphs no correction touches: the opening paragraph, the NO-code seeding paragraph, doctor item 1 and the park overlay item; and the first half of the doctor intro, whose second half D1-F7 edits. The family steps are main's exact bytes: an earlier head re-wrapped steps 3 and 5 (`make siblings`) and #486-#488 edits both. - Three sentences this pass corrects give up a few words to keep a line: D2-F1's "offers to run" (no "for you"), D2-F11's "asks the same of one name", and AGENTS.md's `check` verdict list and doctor next-command item (D3-F9, D1-F7). Word-level diffs of both files (`rvD-worddiff.py`) show only the edits in the tables below, `rvD-hunks.py` marks each re-wrapped hunk `WHITESPACE-ONLY`, and `rvD-mdstruct.py` finds the Markdown block structure, links and HTML identical (AGENTS.md 320/320 blocks, README.md 678/678). "Where" is the audited `ec4a7a0e` for README.md (it hashes the same up to #394, and #484 and #465 changed its text and not its line count, so those numbers still point at the same places on main); AGENTS.md lines after 154 read 5 higher on main, and 8 higher after the doctor's item 4, since #394. "Evidence" is code at `7a8b981d` (the line numbers below are that file's, re-derived at the merged head `48585e62` for every tool file that #394, #484, #465 and the later landings moved), and "run" is the audit's or this pass's offline run. | finding | where | was | now | evidence | |---|---|---|---|---| | D1-F3 | AGENTS.md:121 | "the preflight prints the install command" for git-filter-repo | "`execute` refuses without it, printing the install command" | adopt-project.py:4148-4158 (`_require_filter_repo`, `filter-repo-missing` + `FILTER_REPO_HINT` at 261-267), only call at 5652; `--preflight` checks git, Python, bootstrap, gh, helper, autocrlf only (setup-project.py:1110-1134); openRepoShape:212-216 | | D1-F4 | AGENTS.md:176-178 | the row reads `code: 0 of N source paths (seeded from template)` | the table's `code` row reads `0 of N source paths (seeded from template)` | adopt-project.py:6107-6108 prints `f" {leg:<6} {counts[leg]:>5} of {len(before)} source " f"paths{note}"`, no colon; run: ` code 0 of 2 source paths (seeded from template)` | | D1-F5 | AGENTS.md:182 | "`MedxGlass` descends from `openGlass` only if it PINS it (2026-09-02)" | "...or pins a layer that does and records the chain (`--referent-chain`; 2026-09-02, chains 2026-09-05)" | scaffold-project.py:379-391; run: `--pin openXglass@<sha> --referent-chain openXglass,openGlass --dry-run` printed "classifies as domain-descendant / assembly" | | D1-F6 | AGENTS.md:205-210, 258-259 | `openRepoShape --doctor` and `./shape-doctor.py` presented as one tool; "`--json` is the same report" | installed, it runs from a temporary clone deleted on exit and takes a path and nothing else; to act on a row (`--json`, `--placement-plan`, its next command) run `./shape-doctor.py --root <path>`; item 7 names `./shape-doctor.py --json` | openRepoShape:112-116 (EXIT trap), 329-352 (`shape_checkout` into `$WORKDIR/shape`), 379-392 (`run_doctor` clones when no checkout is beside it), 463-471 (`--doctor takes a path and nothing else`); run: the installed copy printed `NEXT python3 <tmp>/shape/adopt-project.py plan ...` and the directory was gone after exit; `--doctor <root> --json` printed `REFUSED: --doctor takes a path and nothing else`, exit 2 | | D1-F7 | AGENTS.md:226-232 | next commands are "`update-shape.py apply …`, `make bootstrap`, `bump-leg.py …`"; every one "carries the exact next command"; "each of those commands has its own section above" | "Every one carries the exact next command, where one can be named — `update-shape.py check …` and its `apply`, `git submodule update …` (in a holder, its `scripts/bootstrap.py`), the project's own validator"; "Each of those commands keeps its own refusals" | shape-doctor.py:2813 and 2828 (check NEXT), 3075-3089 (`legs` NEXT is `git submodule update`, `make bootstrap` only in the trailing comment), 5044 (holder `members` NEXT); no row's NEXT is a `bump-leg.py` command (`grep -c -i 'bump.leg\|bump_leg' shape-doctor.py` was 0 at `7a8b981d`; since #495 the `pins` row's reason names two `bump-leg.py --to` lines, as text, at shape-doctor.py:1847-1856 and 1904-1922 of `ea634043`); rows with no next command: 1393, 1526-1532, 5758 (`could not be run: {exc}`); only update-shape has a section in AGENTS.md | | D1-F9, D3-F7 | AGENTS.md:344-346, README.md:994-995, and the handbook beside it | after a bump the leg is left detached, and "`make bootstrap` in the root re-places it on its tracking branch AT the new pin; say so in your relay" (README.md: "`make bootstrap` re-places it on its tracking branch AT the new pin.") | AGENTS.md: "`make bootstrap` in the root puts it on its tracking branch AT the new pin, or says why not; relay which."; README.md and the handbook: "...re-places it on its tracking branch AT the new pin, or says why not." | templates/assembly-root/scripts/bootstrap.py at `ea634043`, `checkout_tracking_branch` (393-448): a branch behind the pin is fast-forwarded (275-340), and one ahead of it with no commit of its own is moved back (343-390, 177-187), both since #490 (`7348c57b`); a branch with commits of its own, or one that diverged, is not moved, and bootstrap prints `not moving an existing branch` (444-448); a commit made on the detached leg is left where it is, said as "which is neither" (313-322); a branch another worktree has checked out is not moved, and git's refusal is printed (226-228, 238-243) | | D2-F1 (part) | README.md:175-178, 217-220 | step 4's preflight "prints the command for THIS machine and asks once"; git commands are "the same commands that preflight offers to run for you" | "for `gh`, its login and its credential helper"; "the same commands `setup-project.py --preflight` offers to run" | setup.sh:85 (`command -v git ... \|\| die`, exit 2 by setup.sh:40) and :76; setup-project.py:1829-1831 (`bad(GIT_MISSING); die(PREREQ_MISSING)` ahead of `preflight(opts)`); the git offer (setup-project.py:926-932, rows 787-796) is reached only by `setup-project.py --preflight` (1820-1821); the `gh`, login and `gh auth setup-git` offers at 982-1078; run, no git on PATH: `openRepoShape --preflight` printed `REFUSED: git is not installed`, exit 2, no offer. The third place (README.md:434-436) is routed to #491 | | D2-F2 | README.md:181-183 | `openRepoShape --preflight` "exits 0 when everything is present and 1 when it is not" | "(`openRepoShape --preflight` exits 2 when `git` or a Python 3.9 or newer is missing)" | openRepoShape:446, 564 run setup.sh; setup.sh:76, 85 `die` (exit 2, setup.sh:40) before `preflight_only` (setup-project.py:1142-1166) | | D2-F3 | README.md:269-271 | "add `--visibility private` if you want that" | "`--visibility` defaults to `private`: add `--visibility public` (or `internal`) if you want that" | setup-project.py:428 (`self.visibility = "private"`), usage :138; scaffold-project.py:310-316; run: no `--visibility` printed `[ok] visibility private` | | D2-F4 | README.md:306-308 | the rehearsal leaves behind `./Atlas` and three bare repositories | "...and the scaffold's three working trees, in the temporary directory its `Working trees are at` line names" | scaffold-project.py:1570-1571 (`tempfile.mkdtemp(prefix="openreposhape-")`, never removed), 1611; run: `/tmp/openreposhape-q5eeici3` held `atlas atlas-code atlas-spec` | | D2-F6 | README.md:436-438 | "a hyphenated or lowercase name is refused here" | "a hyphenated project name is refused here" (the review's N1: `Atlas-spec` and `Atlas-code` are hyphenated and pass) | contracts/repository-naming.yaml:335-345 (`^[A-Za-z][A-Za-z0-9]*(?:-spec\|-code)?$`); run: `validate-repository-naming.py --explain atlas atlas-spec atlas-code` exit 0, `At-las` exit 1 `naming-unclassified`; the rehearsal for `atlas` ended `DONE. atlas is scaffolded and bootstrapped.` | | D2-F8 | README.md:573-574 | every other family "is CamelCase" | "from the neutral-product, domain-descendant and family forms, all of which are CamelCase words" | contracts/repository-naming.yaml:412 (`^[a-z0-9]+(?:-[a-z0-9]+)*-wip$`), 182 (`-Install` is hyphenated) | | D2-F9 | README.md:619-620 | the rewrite covers "both HTTPS and SSH leg URLs" | "both HTTPS and scp-form SSH (`git@github.com:`) leg URLs" | templates/assembly-root/.github/workflows/validate.yml:160-161 (two `insteadOf` rewrites) and :69-73 (`*) continue` skips any other spelling); run: `ssh://git@github.com/...` went to ssh unrewritten | | D2-F10 | README.md:625-628 | a leg under another owner "can only be reached via `SHAPE_LEGS_TOKEN`" | "`SHAPE_LEGS_TOKEN` is read only when the App is not configured, so that leg is reachable only with the App secrets removed" | validate.yml:146-154 (`if [ -n "$SHAPE_LEGS_APP_TOKEN" ] ... elif [ -n "$SHAPE_LEGS_TOKEN" ]`); run: both set printed `leg credential source: app` | | D2-F11 | README.md:680-682 | `validate-repository-naming.py --referent-chain openXdox,openDox` asks the same question for one name | `... --pins openXdox --referent-chain openXdox,openDox ...` asks "the same of one name" | scripts/validate-repository-naming.py:522-528 ("The first entry must be in `--pins`"); run: without `--pins`, `codexDox` classified `project-leg/assembly` (exit 0); with it, `domain-descendant` | | D2-F12 | README.md:687-689 | `--link-source [PRODUCT=]<path>`, env, or a checkout beside the root | `--link-source PRODUCT=<path>`, env, a checkout beside the root, then a bare `--link-source <path>` | scripts/repo_shape.py:2589-2621 (`resolve_link_source`, bare path 4th); templates/assembly-root/scripts/validate-pins.py:597-632 uses the same order | | D3-F1 | README.md:736 | "the only change to it is ONE split commit" | "the only change to its history is ONE split commit" | adopt-project.py:5355-5376 (`_set_the_topic` runs `gh repo edit <source> --add-topic` on all three), called at 5734; the topic is not history | | D3-F2 | README.md:764-765 | `plan` "writes it into `follow_ups:`" for every such file | "lists up to eight per directory in `follow_ups:`, counting the rest" | adopt-project.py:1329-1337 (`hits[:8]`, then `… and N more file(s) under the code leg naming ...`) | | D3-F3 | README.md:810-811 | the row reads `code: 0 of N source paths (seeded from template)` | the table's `code` row reads `0 of N source paths (seeded from template)` | as D1-F4; run: ` code 0 of 3 source paths (seeded from template)` | | D3-F4 | README.md:958-960 | `sorted-ls-tree-r-v1` records "joined with LF" | "each followed by LF, the last one included" | scripts/repo_shape.py:904-928 (`digest.update(record); digest.update(b"\n")`); run over `HEAD:templates/code-root`: LF-joined gave `f44766c9...`, LF-terminated and `tree_digest()` both `fca315e6...` | | D3-F5 | README.md:976 | `--dry-run` "prints the move and writes nothing", and (with #484) "`--dry-run` writes nothing, so it runs there" | "# --dry-run prints the move; it fetches into the leg and writes nothing else"; "`--dry-run` fetches into the leg and writes nothing else, so it runs there and prints that command" | scripts/bump-leg.py at `48585e62`: `fetch_leg` is called at :1453 (refspec `+refs/heads/*:refs/remotes/origin/*`, :854) and the `--dry-run` exit is at :1473, so the fetch comes first; the tool says so itself ("--dry-run: nothing was changed. The leg's object store was fetched into, which is how the digest above was computed", :1474-1477; docstring :447). Run (audit, offline): before the dry run the target object was absent from the leg and `origin/main` was f85673e; after it the object was present and `origin/main` was b9bce3e. #484 (landed) lets `--dry-run` run on the tracking branch (:1030 `and not dry_run`) and print `git switch -c`; it kept "writes nothing" at README.md:975 and in the refusal sentence, and the handbook (code comment and paragraph) says the same | | D3-F9 | README.md:1041-1044, AGENTS.md:285-288 | `check` prints `unchanged`, `upstream-changed`, `locally-modified` or `both` | "also `already-at-target` (the root has the target's bytes) and `upstream-removed`, `unmapped` and `copy-missing`; `apply` refuses the last three" (README; AGENTS.md appends the same four after `both`: `already-at-target`, `upstream-removed`, `unmapped` and `copy-missing`) | update-shape.py:239-256 (states, `CONFLICTS`, `ORDER`), 541-642 (`_classify_row`, `_row_verdict`) | | D3-F10 | README.md:1059-1063 | a `both` file "is refused outright" | "refused until a human merges it, commits the merge and re-runs with `--accept-local <path>`" | update-shape.py:2515-2535 (`_refuse_conflicts` excludes an accepted `both`), :1582 (`check` says "Merge it by hand, commit it, then `apply --accept-local <path>`"); run: `apply ... --accept-local Makefile` after the hand merge committed, exit 0 | | D3-F12 | README.md:1137-1139 | "every one that is not `ok` names the exact command that fixes it" | "a row that is not `ok` names the exact command that fixes it whenever there is one to run" | shape-doctor.py:1429, 3026, 5337 build `Row(..., None, ...)` with no fix; run: `--json` on a plain directory gave `contents n/a` with no next | | D3-F13 | README.md:1151-1153 | `COMPLIANT`: "every row ok"; one `SHAPE BEHIND` spelling; `DRIFTED`: "a copy was edited here, or a leg is off its pin" | "no row is a `FINDING`"; adds `(pin <a> -> <b>, no copied file differs)`; "edited, deleted or no longer maps onto the standard, or a leg (a member, in a holder) is off its pin" | shape-doctor.py:5942 (`COMPLIANT` is the end of the ladder, `n/a machine` is always there), 5875-5899 (`behind_verdict`), 5775-5795 (`drifted_file_reasons`), 5796-5824 (`drifted_reasons`) | | D3-F14 | README.md:1158 | "Exit 3 is usage or environment" | "Exit 3 is usage or environment (a flag argparse rejects is exit 2) — nothing about it is a statement about the tree you pointed at: no such path, a refused `--placement-plan`, a `shape-doctor.py` not sitting in a checkout of the standard, or a pin naming a commit **this** checkout does not carry (`CANNOT ANSWER`)" | shape-doctor.py:6029-6051 (plain argparse, exit 2), 6061-6082 (the exit-3 paths, among them `except Refusal: return 3` for the flag), 4547-4553 (`shape-doctor-no-placement-audit`), docstring :227 (`3 usage or environment`); tests/test_shape_doctor.py:5844-5858 asserts `returncode == 3`; runs: `--bogus` rc 2, `--root <missing>` rc 3, `--root <plain dir> --placement-plan pp.yaml` rc 3 | | D3-F15 | README.md:1273-1278 | bare `park` "PARKS EVERY ESTATE it finds" | "lists every estate ... and ASKS whether to park them all (with no terminal on stdin it refuses and names `--all`); `park --all`, or `-a`, parks them all unasked ... skipping a standalone root with no Speckit overlay (Brett Heap's RULINGs of 2026-09-10, the later superseding #91's unasked sweep)"; `resume` has no `--all` | openRepoTools `park` at GitHub's main `f87fd5a` (the review fetched it: nothing newer): header 17-41, usage 77-100, sweep skip at 802-808, `resume` refuses `--all` at 863-867; runs: bare `park </dev/null` outside an estate gave `REFUSED ... park --all parks every estate below without asking`, rc 2; `park --all --dry-run` gave `skipped Solo (no Speckit overlay)`, rc 0 | | MISPLACED (new, #394) | README.md:1176-1180 | `placement` "names the paths the policy would have put somewhere else" | "...and a submodule in a leg other than the one its `submodules:` record names" | since #394 (`7a8b981d`) a submodule in a leg other than the one `project.yaml`'s `submodules:` records for it is `misplaced`, counted in `MISPLACED`, exit 1: shape-doctor.py:3604-3624 (`recorded_placement`), 3754-3769 (`leg_submodules`); tests/test_shape_doctor.py:6770 (`test_a_submodule_in_the_other_leg_from_its_record_is_misplaced`), 7219. AGENTS.md already says it (#394's item 4). The verdict table's `MISPLACED` row sits directly above #468's `INVALID` row, so it is not edited here: #468 is asked for it | | N7 | AGENTS.md:259-262 (item 6) | "`openRepoShape --preflight` is what offers to install one" | "`openRepoShape --preflight` offers only the `gh` set-up" | the preflight never checks git-filter-repo (setup-project.py:1110-1135; openRepoShape:212-216) and setup.sh:85 exits 2 for a missing git before the preflight; the offers are `gh`, its login and its credential helper (982-1078) | `docs/handbook.html` repeats these and is corrected where it does: the preflight offers, `git` and the credential helper (four platform panels), the `openRepoShape --preflight` exit codes, the visibility default, the rehearsal leftovers, the CamelCase sentence, the credential rewrite and `SHAPE_LEGS_TOKEN`, the source repository's one change, the digest definition, `check`'s verdicts and the `both` refusal, the doctor's row sentence, verdict table and exit 3, the placement paragraph's submodule, and bare `park`. The handbook says more than the README in four places (the `git`-before-the-preflight sentences in the Linux panel, the Windows git offer "only under `py setup-project.py --preflight`", glosses for the three non-comparable `check` states, park "from anywhere"); all are true. Its footer's history prose gains "added (#483)". | finding | where | evidence at `71359744`, the merge of `48585e62` | |---|---|---| | D1-F8 | AGENTS.md:323-328, 337-339 | the block carries `git -C <path> switch -c <branch>` (AGENTS.md:335), item 3 says "A new project stands on its tracking branch; the dry run runs there and prints the `git switch -c`, but the real run is refused" (:348-349); scripts/bump-leg.py:1030 refuses on the tracking branch only `and not dry_run`; tests/test_bump_leg_dry_run_tracking.py | | D2-F7 | README.md:502-504 | README.md:502 `git -C ../Atlas switch -c <branch> # the tool will not commit on main` | | D3-F6 | README.md:974-977, 987-992 | README.md:988-991 "`git switch -c <branch>` first; `--dry-run` ... runs there and prints that command" | | D2-F13 | README.md:705-707 | #465 rewrote the sentence: README.md:705-707 now reads "Finding neither is a named SKIP, exit 0: no checkout and no network does not make a descent a lie. A checkout that cannot answer is the finding `neutral-pin-source-unreadable`", and the handbook says the same (1139-1141). True: a source that is found but cannot answer is a FINDING, templates/assembly-root/scripts/validate-pins.py:796; the SKIP is only "no source found and `gh api` fails" | | D2-F14 | README.md:708-709 | #465 deleted the whole sentence, so "a pin file the declaration names but that is not there, is reported as drift" is gone from README.md and the handbook (the missing file is the refusal `neutral-pin-missing`, validate-pins.py:699). It also dropped the true half, "A digest that recomputes differently is reported as drift" (the finding `neutral-pin-digest-mismatch`, validate-pins.py:853): not a false statement; see "Not holding #483 open" below | | D3-F8 | README.md:1028, 1095-1097 | #465 rewrote both: README.md:1027-1029 "a project runs its own gate, which never consults this standard (its validators' one possible network read is `validate-pins.py`'s `gh api` recheck of a neutral product's pin, above)" and :1095-1098 "That line adds no network call ... (Its pins gate may ask `gh api`; see above.)"; the handbook the same (1403, 1448). True: `validate-pins.py` falls back to `gh api` when no local checkout answers (scripts/repo_shape.py:931 `tree_digest_from_gh`) | Evidence is at `ea634043`, the merge of `70d0d1b3` with the D1-F9/D3-F7 correction; `9ac90d16` changes one README word in place and the footer blob, so the line numbers hold at this head. The runs are this round's, offline: local bare remotes, a stub `gh`, a blank identity. | finding | where | evidence | |---|---|---| | D1-F1 | AGENTS.md:22-24, 56-62, 78-80 | #491 reads the yes from the controlling terminal (setup-project.py:1322, `ask()` at 1325-1432; the confirmation at 1770-1779), and setup.sh hands the child the pipe (282-283). Run under a pty: the piped `cat setup.sh \| bash -s -- … --local-remote-dir …` asked `Type yes to continue:`, and `no` gave `REFUSED: not confirmed; nothing was created.`, exit 1. With no terminal at all (AGENTS.md:23, 83-86) it prints the plan and refuses (1770-1776): `REFUSED: this will create THREE repositories and there is no terminal to confirm on…`, exit 2 | | D2-F5 | README.md:315-336, 414-420 | the same read, for the README's three one-liner blocks | | D1-F2 | AGENTS.md:76-77 | #491 took the audit's text (AGENTS.md:73-75). `_choose_org` (setup-project.py:1244-1258) takes a fork's `origin` organisation (run: `[ok] organisation acmefork (from the \`origin\` remote…)`) and refuses opensoft's own without `--org` (run: `REFUSED: you are running from the upstream checkout (…); pass --org <your-org>.`, exit 2) | | D2-F1 (rest) | README.md:434-436 | #491's words, kept whole in the merge (README.md:432-445). The `gh` offers are the install (setup-project.py:992-1005, `INSTALL_OFFERS` at 795-798), the login (1009, 1015-1032) and git's credential helper (1051-1083, asked only once `gh` is ready, 1134-1137); git and Python are refused before any offer (setup.sh:76 and 85, exit 2 by :40). Run with no git on PATH: `openRepoShape --preflight` printed `REFUSED: git is not installed. Install it: https://git-scm.com/downloads`, exit 2, and offered nothing | | D3-F11 | README.md:1130-1133 | #468's sentence (README.md:1130-1134) names `agent files` for a holder and `machine` for a plain directory, as the doctor's `CHECKS` registry has them (shape-doctor.py:6115-6155, at 6135 and 6153). Run, `--json` on a plain directory: the rows naming, contents, way-in, machine | | `MISPLACED` table row (the review's) | README.md:1151, and the handbook's verdict table | #468's row reads "...or a submodule in a leg other than the one its `submodules:` record names" (README.md:1155, handbook:1509); shape-doctor.py:3843-3862, 3993-4008 | Every finding of the three audits is corrected above or settled by a pull request that has landed. A fourth read took what main changed in README.md and AGENTS.md since the audits' `ec4a7a0e` (#394, #484, #465, #468, #491: `git diff -U0 ec4a7a0 70d0d1b`), every line this branch does not itself rewrite, and asked the audits' question by their method. It found one false statement, D1-F9/D3-F7's own sentence as #484 kept it, corrected above, and checked the rest true: AGENTS.md 23, 63-67, 73-81, 83-86, 88-96, 155-158, 252-254, 335, 347-349 and 357-361; README.md 333-338, 343, 432-445, 503, 508, 699-709, 987-992, 1028-1033, 1095-1098, 1118-1136, 1155-1156 and 1163-1171 (line numbers at `ea634043`). Read in code only: the offers' real-GitHub paths and Windows (the real `gh` never ran). Not read: `park` and `resume`, which live in openRepoTools. **True but loose, left as they are.** AGENTS.md:83 and README.md:440 say a run with no terminal "stops after the plan"; a missing prerequisite stops it earlier, at the preflight (exit 2), which AGENTS.md:88 covers. README.md:1216-1218 says the machine check runs "with stdin closed"; the doctor also starts it in a session of its own (shape-doctor.py:1284-1285), which is what keeps the terminal out since #492. AGENTS.md's doctor item 3 does not say to ask the human which of #495's two `bump-leg.py --to` lines to run: an omission, not a false statement. **Outside these files.** `scripts/bump-leg.py:1513` still prints "re-places it on its tracking branch AT the new pin" after every bump. It is on #458's checklist with the rest of #490's follow-ups, and a docs-only pull request leaves it there. **Not holding #483 open: an omission.** #465 deleted "A digest that recomputes differently is reported as drift" together with its false clause. So README.md (698-709) and the handbook beside it no longer say what a recomputed neutral-pin digest that disagrees does. Nothing there is false, so it does not hold #483 open. Both files are at their caps, and the sentence needs about two more README lines that no untouched paragraph gives back, so it is not added here. Re-checked at `ea634043`: the finding is validate-pins.py:968-970 and 1140-1142 (exit 1), the refusal 814-816 and 1172-1175 (exit 2). A sentence for whoever next edits that paragraph: "A digest that recomputes differently is the finding `neutral-pin-digest-mismatch` (exit 1); a pin file the declaration names but that is not there is the refusal `neutral-pin-missing` (exit 2)." A robustness note on main's `test_the_documents_say_what_an_assistant_does_with_no_terminal` (#491): it strips `<...>` from README.md as well as from the HTML page, and a README line holding `<(` and no `>` swallows lines up to the next `>`; stripping tags from the `.html` page only would make that test indifferent to such a line. `git merge-tree --write-tree` of `ea634043` against the open pull requests that edit these files: clean with #503 (`6fe17be5`); #506 (`f184a51c`) and #486-#488's #507 (`708f3bbb`) each conflict in the handbook footer alone (its history prose and `README.md blob` line), and README.md and AGENTS.md merge clean with all three and stay at 1422 and 490 lines. Whichever lands later recomputes `git hash-object README.md` and keeps every "added (#n)" clause. #394, #484, #465, #468 (`57c85e92`), #490 (`7348c57b`), #508 (`aa4e1f59`) and #491 (`da197359`) are merged here. - At this head (`9ac90d16`): `tests/test_repo_hygiene.py tests/test_agents_md_codes.py tests/test_cli_reference.py tests/test_setup_project_py.py tests/test_setup_sh.py`, 277 passed; `scripts/validate-repository-naming.py --explain Atlas Atlas-spec Atlas-code` exits 0 (`project-leg`: assembly, spec, code) and `--explain At-las At-las-spec At-las-code` exits 1 (`FINDING naming-unclassified`), so only the project name's hyphen is refused. - At `ea634043`: `tests/test_repo_hygiene.py tests/test_agents_md_codes.py tests/test_cli_reference.py`, 122 passed (the same on the merge `94793aeb`); `tests/test_setup_project_py.py tests/test_setup_sh.py`, which read README.md and the handbook for #491's no-terminal route, 155 passed; `tests/test_free_plan_hint.py` with the AGENTS.md step 7 tests of `tests/test_adopt_late_failure.py` and `tests/test_adopt_github_exits.py`, 18 passed; and on the merge, `tests/test_bootstrap_after_bump.py`, 23 passed. A stub `gh` first on PATH (it logged no call), `GH_TOKEN` bogus, an empty `GH_CONFIG_DIR`, a blank identity. - `rvD-worddiff.py`, `rvD-hunks.py` and `rvD-mdstruct.py` over `ea634043` against its parent: four `WORDS CHANGED` hunks, all of them the D1-F9/D3-F7 sentence and the blob, and no re-wrap; block structure, links, HTML and code spans identical (README.md 678/678 blocks, AGENTS.md 322/322). The merge `94793aeb` against `70d0d1b3` changes the same lines as `83cfad54` against `aa4e1f59` (366 insertions, 336 deletions), apart from the two resolved hunks. - Review round four (the first reviewer, at `ea634043`): APPROVE WITH NITS (0 blockers, 0 should-fixes, 3 nits). - It ran `git show --remerge-diff` on every merge since round three (`71359744`, `fb6a9ab4`, `83cfad54`, `94793aeb`). Each hand resolution is a named conflict hunk, and no row, clause or fact was lost. - It reproduced the D1-F9/D3-F7 sentence with a real bump and `make bootstrap` in seven cases, and each placed the leg or said why not: a branch behind the pin; ahead with no commit of its own; with commits of its own; diverged; checked out in another worktree; a commit on the detached leg; a move a local change blocks. - It re-checked every routed row and ten moved-evidence rows, and read the whole of main's change since `ec4a7a0e`, finding nothing else false. - It ran the hygiene, codes, CLI-reference, setup, bootstrap-after-bump and free-plan tests: 314 passed. - Its nits: N1, the hyphenated project name, made at `9ac90d16`; N2, a run with no terminal stops at the preflight rather than after the plan when something is missing, which AGENTS.md:88 covers; N3, this description. - Two `bootstrap.py` defects it met while reproducing are on #458's checklist (issuecomment-6142614866). - At `83cfad54`: `tests/test_repo_hygiene.py tests/test_agents_md_codes.py tests/test_cli_reference.py`, 119 passed. On `fb6a9ab4`, the merge of `57c85e92`, the same three gave 119 passed, and `tests/test_shape_doctor.py tests/test_doctor_unfinished_merge.py tests/test_free_plan_hint.py tests/test_openreposhape_command.py tests/test_shape_advisory.py` gave 465 passed, 3 skipped (the doctor's PowerShell line, which prints only on a Windows host). The table's "no row is a `FINDING`" holds with the `merge` row: `shape-doctor.py` returns `COMPLIANT` only at :6171, after :6168-6170 answer `INVALID (<ids>)` for any `FINDING` row, and `merge` is a `FINDING` only for an unmerged path (:5879; a merge with none left is a `note`, :5862). All runs with a stub `gh` first on PATH, `GH_TOKEN` bogus, an empty `GH_CONFIG_DIR`. - At `71359744`, the merge of `48585e62`: the three files, 119 passed (a stub `gh` first on PATH, `GH_TOKEN` bogus, an empty `GH_CONFIG_DIR`; the stub logged no call). - The same three files on a throwaway merge of `f35e59fa` with #491's head (merge-tree, commit-tree, a detached worktree, footer resolved, then removed): 122 passed, and `tests/test_setup_project_py.py tests/test_setup_sh.py -k no_terminal`: 8 passed. The failure the second review found (`test_the_documents_say_what_an_assistant_does_with_no_terminal[README.md]`, 1 failed, 122 passed) came from the `bash <(` lines this commit removes. - `tests/test_adopt_late_failure.py::test_agents_md_step_7_names_the_exit_the_tool_prints` and `tests/test_adopt_github_exits.py -k "step_7 or agents"` (round two, on `ccf520bd`): 2 passed; they read AGENTS.md step 7, which is untouched. - `rvD-worddiff.py`, `rvD-hunks.py` and `rvD-mdstruct.py` over the third commit against its merge parent: every changed region is one of the corrections above (the `bash <(` blocks back to main's text, D3-F5, the park parenthetical, the placement sentence and the three word trims), and the two `WHITESPACE-ONLY` hunks are the family steps going back to main's bytes; block structure 320/320 and 678/678, links, HTML and code spans identical. - No test greps a phrase this pull request changed: the old phrases appear under `tests/` only in history comments and the doctor's docstring. The third head (f35e59f) was re-verified: APPROVE WITH NITS (0/0/4), all four in this description and fixed there. Main 48585e6 (#465) was then merged as 7135974, and 57c85e9 (#468) as fb6a9ab; aa4e1f5 (#508) merged clean as 83cfad5. Main 70d0d1b (#491) was merged as 94793ae, and ea63404 corrects D1-F9/D3-F7. A fourth review round at ea63404 covered every merge since f35e59f (remerge-diff: no row, clause or fact lost), the bootstrap sentence in seven reproduced cases, and the whole docs delta since ec4a7a0: APPROVE WITH NITS (0/0/3), and #483 can close. Its N1, "a hyphenated project name", is 9ac90d1. CI is green on Linux, macOS and Windows, with SonarCloud passing, at 9ac90d1. Main cc8f2da (#376), which landed after its last merge, touches none of its files, and the combined tree passed the docs and setup tests (277 passed). Closes #483 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
…s a runnable `scripts/family.py`, printed commands quote paths, and `add`, `bump` and `remove` refuse the tracking branch and print their branch's push (#488, #486, #487) (#507) Three defects from lane 3's v1.0.0 acceptance pass of AGENTS.md's "Creating a family, and adding a member", one commit each, in this order, then one review commit for #486 and one for #487. All three change `scripts/family.py` and `add`'s NEXT line, so they are one pull request. **What was wrong.** - AGENTS.md spelled the command `python3 scripts/family.py init --org <org> --family <Name>`, a path that runs from the standard's root. `--into` defaults to where the person stands. - So the command, followed exactly, printed `landing <standard>/Orbit/Orbit` with no word, and `make siblings` would then clone every member into the standard's checkout too. - `init`'s NEXT STEPS printed `python3 family.py add …`, built from `Path(__file__).name`. From the root that fails `can't open file`, exit 2. **What changed** (commit 1). - With neither `--into` nor `--work-dir`, `init` refuses a landing inside a checkout of openRepoShape: `family-landing-in-standard`. A checkout is a directory holding `scripts/family.py` beside `templates/family-root/`, found in the landing's ancestry, so another clone or an install counts too. - The refusal comes before anything is created, under `--dry-run` alike. - The remedy names `--into ..` from the root. From deeper inside the checkout it names the relative path that reaches the checkout's parent. - An explicit `--into` or `--work-dir` is not second-guessed. `family-holder-already-here` is still asked first. - NEXT STEPS, and the `family-holder-already-here` remedy, name this file relative to the directory `init` ran in. From the root that is `scripts/family.py`, as setup-project.py, validate-family.py, AGENTS.md and README spell it. It is absolute where no relative path exists. - AGENTS.md's init command passes `--into ..`, as README's already does, and step 1 says why. The edit is line-neutral: step 1 is one line shorter. - `init --into --help` says it too, and docs/cli.md is regenerated. **How tested.** `tests/test_family_init_landing.py`, 9 tests: - the old command from the root is refused, as a dry run, so nothing can land in the checkout; - a real run inside a fake checkout creates nothing; - the remedy from a subdirectory names the right parent; - an explicit `--into .` or `--work-dir .` passes; - outside a checkout the default is unchanged; - AGENTS.md's line, as written, lands beside the checkout; - the printed NEXT runs, both from the root and from elsewhere. On origin/main code 6 of them fail. Removing the refusal turns 3 red, and putting `Path(__file__).name` back turns 2 red. **What was wrong.** `family.py` and the holder's `siblings.py` printed every path in their pasteable commands bare. Under a holder at `…/my work/Sonata/Sonata`: - the `member-git-dir-cached` remedy `rm -rf <store>` split into `rm -rf …/my`, an absolute directory that is NOT the store, and a relative `work/…`; - `init`'s NEXT STEPS and `add`'s NEXT split `--family-root` and `git -C` (`fatal: cannot change to '…/my'`, exit 128); - the `mv` lines for a misnamed family folder pasted as four arguments. **What changed** (commit 2, and the review commit after #487's). - `scripts/repo_shape.py` gains `shell_quoted(value, platform=None)` and `shell_line(words, platform=None)`, in a new section at its end, the library `family.py` already imports. - It is the doctor's `quote_arg` rule (#101/#102): the same two alphabets, and the same two escapes, `sh`'s `'\''` and PowerShell's doubled apostrophe. The dialect comes from `os.name` or a `platform` argument. An ordinary word is left bare. - It does not flatten to ASCII, as the doctor's report does by contract. A command that deletes or moves a directory has to name that directory. So PowerShell's typographic single quotes are doubled too. - A word that begins with `=` is quoted, in both dialects. zsh, macOS's login shell, expands a bare `=word` to a command's path (`=ls` reaches the command as `/bin/ls`). This is the one place it differs from the doctor's rule on ASCII. - `family.py`: - `init`'s NEXT STEPS print the `add` on ONE line. It used to paste only into a POSIX shell, because of its `\` continuation. `cd <holder>` and `make siblings` are two lines, with no `&&`, which Windows PowerShell 5.1 rejects between two statements. - Those lines and the `family-holder-already-here` remedy quote every path. - `add`'s NEXT quotes `git -C <holder>`, and `member-path-taken` quotes each spelling it names. - `member-git-dir-cached`, and `remove`'s note about the same store, print the directory alone on its own line. Then comes `rm -rf` with its one argument quoted for the POSIX shell `rm -rf` belongs to, and the sentence names that shell: Git Bash on Windows. PowerShell refuses `-rf` and deletes nothing. - `siblings.py`: the parent-folder warning's `mv` and `mkdir -p` lines are quoted for a POSIX shell with the standard library's `shlex.join`, and the warning says which shell. It does NOT import the new quoter: a holder that kept its own `repo_shape.py` through `update-shape.py apply --accept-local` takes the new `siblings.py` without it, and an import of a name its copy lacks stopped `make siblings`, `park` and `resume` with an ImportError that `check` and the validator never reported. - `setup-project.py`'s family hand-over quotes the holder in its `add` line (and names the branch first, see #487). It runs before any checkout of the standard exists, so it carries its own copy of the rule, and a test holds that copy to `shell_quoted`. **How tested.** `tests/test_family_printed_quoting.py`, 69 tests: - The quoter round-trips through `shlex.split`, and through a PowerShell reader, for a space and an apostrophe, `é` and a typographic apostrophe, shell syntax, the empty string, and every printable ASCII character alone and before, inside and after a word. - The host's own `sh`, `bash` and `zsh` hand each quoted word back byte for byte through `printf`, a leading `=` among them (skipped where a shell is absent, and on Windows). - It agrees with the doctor's `quote_arg` on every one of those ASCII values but a leading `=`, and its two alphabets are the doctor's two patterns. - Under a holder in `my work` and in `Dana's work`, each of these splits into the argv it means: `init`'s NEXT (with `cd` and `make siblings` on their own lines), `add`'s NEXT, the cached-store remedy and note, the `mv` lines, and setup-project's `switch` and `add`. - The `rm -rf`, run as split, deletes the store, and the decoy `<tmp>/my` survives. - The `mv` lines, run as split, move the holder. - The holder's own `siblings.py` runs `--help`, the misnamed-folder warning and `--make park` beside a `repo_shape.py` that lacks every name this change added. - Three existing tests' expected lines follow the new wording. Mutations, each turning tests red: - a bare quoter: 35 red; - bare `mv` lines in `siblings.py`: 3 red; - a bare `rm -rf`, a bare `add` NEXT, a bare `init` `add` (round 1, on the 85-test file): 2 red each; - `siblings.py` importing `shell_line` again: 1 red (the older-`repo_shape.py` test); - `%` dropped from the POSIX alphabet: 2 red (the every-ASCII agreement and the alphabet test). The round-1 sample test passed it, 36 of 36; - a leading `=` left bare: 10 red, among them the real-`zsh` test; - `cd … && make siblings` back on one line: 2 red; - setup-project's holder unquoted: 2 red. **What was wrong.** - AGENTS.md says each act's ONE commit lands as a pull request, and no step made the branch that pull request needs. - After `init` the holder stands on `main`, and all three acts committed straight onto it. - `add` printed `NEXT git -C <holder> push -u origin <branch> && open a pull request`. No branch is called `<branch>`, so the push failed: `src refspec … does not match any`. - `bump` and `remove` printed no NEXT at all. - `bump-leg.py` refuses exactly this for a leg (`bump-leg-on-tracking-branch`). **What changed** (commit 3, and the review commit after it). - The real run of each act refuses the holder's tracking branch, the one `family.yaml` declares (`family-on-tracking-branch`), and a HEAD on no branch (`family-holder-detached`, which is what `bump-leg.py`'s `bump-leg-root-detached` refuses too). - The check comes after the refusals about the arguments and the holder, and before the member is read, fetched or cloned, and before anything is written. - The remedy is a quoted `git -C <holder> switch -c <branch>`. The branch is `family/add-<project>`, `family/bump-<id>-<sha12>` or `family/remove-<id>`, and a name that is not a plain branch word becomes `member`. - `--dry-run` runs on any branch. On a work branch it prints where the commit would land. On the tracking branch it prints that the real run refuses there, with the refusal's code, and the `switch` command on a line of its own. The module's EXIT CODES paragraph names this as the one refusal a dry run does not make. - Each act that commits ends with ONE pasteable line: `NEXT git -C <holder> push -u origin <the branch it committed on>`, quoted, then `then open a pull request against <tracking>; never push to it directly`. `bump` and `remove` gain that NEXT. - `init`'s NEXT STEPS open with the `switch` before the `add`. - `add`, `bump` and `remove` `--help` say the branch rule, and that each ends by PRINTING the push; docs/cli.md is regenerated. - AGENTS.md's family block gains the `switch` step before the `add`, and step 3 says the tracking branch is refused, as `family-on-tracking-branch`, and that the branch is pushed as its NEXT line says. README's InkRouter block gains the same step, and its paragraph says it. Both edits are line-neutral: AGENTS.md tightens steps 2 and 5, and README tightens the `make bootstrap` paragraph. `docs/handbook.html` mirrors the block and the sentence, and its footer names #487 and the new README blob. - The holder's own docs, `templates/family-root/AGENTS-shape.md` and `templates/family-root/README.md`, open their add/bump/remove block with the `switch` and say the act refuses the tracking branch. `setup-project.py`'s family hand-over prints the `switch` before its `add`. - Test fixtures that committed on `main` now commit on a branch: - `tests/test_family.py`'s module fixture grows the family on `family/members` and pushes that branch to the remote's `main`, as the merged pull request would leave it; - `test_shape_doctor.py`'s holder fixture and `test_family_add_excludes.py`'s `empty_holder` switch to a branch; - `test_family_add_refusals.py` changes in four places: the `NEXT` expectation, `unborn_holder`, the empty-family test, and the corrupt-HEAD test. In that last test, the tag state's branch check is answered in-process, because the test is about the rollback's reading of HEAD. **How tested.** `tests/test_family_tracking_branch.py`, 17 tests: - For each act, on `main`: refused, with the holder byte for byte as it was, and for `add` nothing cloned. Then the printed `switch`, run as split, makes the branch; the act goes through on it; and its printed NEXT, run as split, pushes the branch, which the remote then holds at the act's commit. - A detached HEAD is refused. - Dry runs on `main` and on a branch say what they should. - A family made with `--tracking-branch trunk` refuses `add` on `trunk`; on a branch named `main` the dry run says the commit lands there for a pull request against `trunk`, the real run commits, and its NEXT pushes `main`. - The suggested names pass `git check-ref-format --branch`. - `init`'s NEXT STEPS open with the `switch`. - AGENTS.md, README.md and the holder's two docs show the `switch` in the same block as, and before, the first `add`. Mutations, each turning tests red: the branch check letting `main` through, 4 red; the NEXT line naming `<branch>` again, 3 red; `init`'s NEXT STEPS without the `switch`, 1 red; the tracking branch hard-coded to `"main"`, 1 red (the `trunk` test; every family test passed it before); the holder docs without the `switch`, 2 red; setup-project without the `switch`, 2 red. An independent review at 10c93de gave CHANGES NEEDED (0 blockers, 1 should-fix, 6 nits). The should-fix was a holder that kept an older `scripts/repo_shape.py` through `apply --accept-local`: its new `siblings.py` could not import the shared quoter, so `make siblings`, `park` and `resume` died. The round at bb11b63 fixes it and all six nits, as described above. Re-verified at bb11b63: APPROVE WITH NITS (0 blockers, 0 should-fixes, 3 nits), with SonarCloud's quality gate OK and 0.0% new duplication. The nits were the holder README's pointer to the push (now "as its `NEXT` line says"), two Sonar smells (S1192: `refs/heads/` is now `HEADS_PREFIX`; S3415: the two expected-first assertions now put the actual value first, as the file's other 32 do), and this description's list of other pull requests. All three are fixed at 3451a56: commit 11ae0a8, then a clean merge of main 994be39. The family set gave 566 passed, 8 skipped there. GitHub's update-branch then merged main cac6bb2 as 8e1471c, and main aa4e1f5 (#508, after #490, #474 and #468) was merged as 708f3bb. There only the `docs/handbook.html` footer conflicted: it takes main's clauses, #468's (#424) included, with the README blob recomputed, and `docs/cli.md` auto-merged and passes `--check`. Main 47b1e06 (#505 and #395, after #376, #374, #384, #495, #493 and #491) was then merged as fcbc7a2, again with only the footer in conflict: main's clauses stay in main's order, this branch's (#487) moves to the end, after (#483), and the README blob is recomputed. Every run used `/usr/bin/python3 -m pytest -q -p no:cacheprovider`, with a stub `gh` first on PATH, a bogus `GH_TOKEN`, an empty `GH_CONFIG_DIR` and git's identity blanked. Run on Linux. At e42ef74 (main d41cc29, #402, merged in by GitHub's update-branch), on its tree 655c927: the first set listed below for fcbc7a2, plus every `tests/test_update_shape*.py`, gave **1242 passed, 22 skipped**; the second set, plus `tests/test_leg_path_yaml_scalars.py`, `tests/test_ruleset_hint_tracking_branch.py`, `tests/test_scaffold_reuse_seeded_legs.py`, `tests/test_scaffold_pin_and_reuse.py` and `tests/test_doctor_validator_rows.py`, gave **1260 passed, 12 skipped**. The skips are the same as at 5a2776d, below. At 654df49 (main 8cd7dbe merged in, with #506 and #347): the first set listed below for fcbc7a2, plus every `tests/test_update_shape*.py`, gave **1242 passed, 22 skipped**; the second set, plus `tests/test_leg_path_yaml_scalars.py`, `tests/test_ruleset_hint_tracking_branch.py`, `tests/test_scaffold_reuse_seeded_legs.py` and `tests/test_scaffold_pin_and_reuse.py`, gave **1231 passed, 12 skipped**. The skips are the same as at 5a2776d, below. At 5a2776d (main f391e58 merged in, with #266, #509, #496 and #435): the first set listed below for fcbc7a2, plus every `tests/test_update_shape*.py`, gave **1213 passed, 22 skipped** (the template-quoting skips below, and three NTFS-junction cases, Windows only); the second set, plus `tests/test_leg_path_yaml_scalars.py`, gave **1171 passed, 12 skipped** (the four Windows-only skips below, and eight in that file, main's new one, whose cases the quoted templates (#259) leave with nothing to refuse). At 22733ce (main 631f087, #503, merged in by GitHub's update-branch): the first set listed below for fcbc7a2, plus every `tests/test_update_shape*.py`, gave **1213 passed, 22 skipped** (the template-quoting skips below, and three NTFS-junction cases, Windows only); the second set gave **944 passed, 4 skipped**. At fcbc7a2 (origin/main 47b1e06 merged in, with #505 and #395): - every `tests/test_family*.py`, `tests/test_validate_family*.py`, `tests/test_park_resume_targets.py`, `tests/test_free_plan_hint.py` and `tests/test_template_text_placeholders.py`: **844 passed, 19 skipped** (`tests/test_family_init_values.py:219` and `:291`, which skip because the template now quotes the placeholder and `id:`). - `tests/test_repo_hygiene.py tests/test_agents_md_codes.py tests/test_cli_reference.py tests/test_shape_doctor.py tests/test_setup_project_py.py tests/test_setup_sh.py tests/test_template_quoted_values.py tests/test_stdout_encoding.py tests/test_windows_paths.py tests/test_commit_trailers.py`: **944 passed, 4 skipped** (the doctor's PowerShell line and `tests/test_stdout_encoding.py:286`, Windows only). At 708f3bb (origin/main aa4e1f5 merged in): - every `tests/test_family*.py`, `tests/test_validate_family*.py`, `tests/test_park_resume_targets.py` and `tests/test_free_plan_hint.py`: **718 passed, 8 skipped** (`tests/test_family_init_values.py:242`). - `tests/test_repo_hygiene.py tests/test_agents_md_codes.py tests/test_cli_reference.py tests/test_shape_doctor.py tests/test_update_shape_landing.py tests/test_doctor_unfinished_merge.py tests/test_template_values_single_pass.py tests/test_stdout_encoding.py tests/test_windows_paths.py`: **813 passed, 4 skipped** (the doctor's PowerShell line and `tests/test_stdout_encoding.py:286`, Windows only). Earlier, with origin/main 48585e6 merged in: - `tests/test_family*.py tests/test_validate_family*.py tests/test_stdout_encoding.py tests/test_windows_paths.py tests/test_repo_hygiene.py tests/test_agents_md_codes.py tests/test_cli_reference.py tests/test_park_resume_targets.py tests/test_bootstrap_spelling.py tests/test_free_plan_hint.py tests/test_template_values_single_pass.py`: **1303 passed, 9 skipped**. - `tests/test_shape_doctor.py tests/test_update_shape*.py tests/test_commit_trailers.py tests/test_tracking_branch_ref.py tests/test_scaffold_ignored_shape_files.py tests/test_ruleset_hint_tracking_branch.py tests/test_bump_leg_dry_run_tracking.py tests/test_setup_project_py.py`: **999 passed, 6 skipped**. - The new files alone: `test_family_init_landing.py` 9, `test_family_printed_quoting.py` 69, `test_family_tracking_branch.py` 17, all passed. The full suite was not run locally. CI's three jobs are the check. #505 and #395 have since landed, and both are merged here at fcbc7a2. Each of #505's family sentences was read against this branch's code, and none is false: AGENTS.md 3 and 233; README 569, 822, 1152, 1269 and 1348; the handbook 1126 and 1506. #395 landed with its test switching to `family/members` before its real `add`, as the last bullet below says, and the first run above includes it. #503 then landed as 631f087, and GitHub's update-branch merged it here as 22733ce with no conflict. Main f391e58 (#266, #509, #496 and #435) was then merged by hand as 5a2776d, again with only the footer in conflict: main's clauses stay in main's order, this branch's (#487) moves after #509's (#498), and the README blob is recomputed; `git show --remerge-diff` shows nothing else. #509's reworded sentences in AGENTS.md, README.md and the handbook were read beside this branch's, and none contradicts or repeats another. Its `setup-project.py` changes (the clone-path check, the plan line, the step-(7) backstop and `_main`) and this branch's `shell_word` lines in the hand-over do not meet, and `docs/cli.md` passes `--check`. Main 8cd7dbe (#506 and #347) was then merged by hand as 654df49, again with only the footer in conflict: this branch's (#487) moves after #506's (#462), the README blob is recomputed, and `docs/cli.md`, which both sides changed, still passes `--check`. #506's two `scripts/family.py` hunks (the `FAMILY_RULESET_HINT` import, and its print when the holder's push is refused) sit unchanged inside this branch's code, and that hint prints no path. Under `--reuse-empty-repo`, `init` still refuses its default `--into` inside a checkout of this standard (`family-landing-in-standard`) before it reads the remote, as a dry run with a stub `gh` showed. #506's sentences in AGENTS.md, README.md and the handbook are still true beside this branch's. Main d41cc29 (#402, for #277), which rewrote the doctor's NEXT lines and `tests/test_shape_doctor.py`, was then merged by GitHub's update-branch as e42ef74, with no conflict; the `holder` fixture still switches to `family/members` before its `add`. AGENTS.md stays at 490 lines and README.md at 1422. `git merge-tree --write-tree` of 708f3bb, at 04:47Z on 2026-10-13: - **Clean:** #503 (`fix/family-makefile-without-siblings`) and #395. Each merged tree keeps AGENTS.md at 490 lines and README.md at 1422. - **`docs/handbook.html` footer only:** #505 (`docs/v1-accuracy`) and #491. #468 has landed and is merged here. #506 conflicts there too, and in `scaffold-project.py`, as it does with main. README.md and AGENTS.md merge clean with #505, and `setup-project.py` merges clean with #491. Whichever lands second keeps every footer clause and recomputes the README blob. - **#395 merges clean but one of its tests fails with this branch.** `tests/test_template_text_placeholders.py::test_a_member_whose_id_yaml_would_misread_is_pinned_under_that_id` inits its own holder and runs a real `add` on `main`, which #487 refuses with `family-on-tracking-branch`. Whichever of the two lands second adds `git("switch", "-q", "-c", "family/members", cwd=root)` after that test's `init`; lane 5 is adding it on #395's own branch now, so this branch needs no change for it. (#395's head d526f76 also conflicts with this head in `tests/test_adopt_manifest_values.py`, a file this branch does not touch: that is #395 against main, which the same round resolves.) - On Windows `shell_quoted` speaks PowerShell, the native shell this standard documents (`setup-project.py`), as the doctor's NEXT lines do. `git -C …`, `python scripts\family.py …` and `cd …` are shell-agnostic, so they get the platform's quoting, and no printed line joins two commands with `&&`, which Windows PowerShell 5.1 rejects. - `rm -rf`, `mv` and `mkdir -p` are POSIX commands. They are quoted for a POSIX shell on every host, and the text says "in a POSIX shell (Git Bash on Windows)". PowerShell refuses `rm -rf`'s `-rf` and deletes nothing. - The tests split a host-quoted line with a PowerShell reader on Windows and with `shlex` elsewhere, and split a POSIX-only line with `shlex` everywhere. The real-shell test is skipped on Windows. - On Windows the tests do not execute `rm` or the `mv` lines. They delete the split path with `shutil.rmtree`, and stop after checking the `mv` argv. - Paths: the cached store keeps git's POSIX spelling (`as_posix`), as before. Test paths are `resolve()`d, which expands an 8.3 short name such as `RUNNER~1`. - Nothing here was run on Windows or macOS locally. The `tests-windows` and `tests-macos` CI jobs are the check; on macOS the real-shell test runs the shipped `zsh`. - **A shipped template that imports a newer shared helper can break a holder that kept an older copy.** This change no longer does (`siblings.py` uses `shlex.join`), but nothing stops the next one: `update-shape.py apply --accept-local scripts/repo_shape.py` keeps a `repo_shape.py` that lacks a name a newly copied file imports, and `check` and the validator then read the holder as fine. The general guard is for `apply` to refuse that, as rv456 found for `bootstrap.py` and `spelled` (#456). - **The doctor's `quote_arg` leaves a word that begins with `=` bare**, which zsh expands. `shell_quoted` quotes it; the doctor is left as it is here, and the agreement test excludes that one case. - **The doctor and `update-shape.py` keep their own quoters.** The doctor's `quote_arg` flattens to ASCII by its report's contract, so moving it onto `repo_shape.shell_quoted` is more than an import change. `update-shape.py`'s POSIX `shlex` lines are lane 3's open #374. A test holds `shell_quoted` and `quote_arg` to one answer on every printable ASCII value but a leading `=`. - **`family.py`'s by-hand rollback commands (#365) and its `git config -f .gitmodules` removals (#317, #360) keep `shlex.quote`.** They were already quoted, they are POSIX-only (`printf … | git update-index`), and they are worded for a POSIX shell. - **The holder's `validate-family.py` remedies that name `family.py add`** (`member-gitlink-absent`, `member-gitlink-unnamed`) print no branch step: they are a holder copy, and the `add` they name refuses the tracking branch with its own `switch` remedy. - **`remove`'s store note still computes `.git/modules/<path>`** rather than asking `git rev-parse --git-path`, as `add` does. In a linked worktree it therefore prints no note. That predates this change. - **PowerShell verbs** (`Remove-Item`, `Move-Item`) are not printed for the POSIX-only commands. - **README's `--into` sentence gives the default but not the refusal.** README.md (861) and the handbook (1246) say `--into <dir>` is the PARENT directory, "the directory you are standing in, by default". They do not say that `init` refuses that default inside a checkout of this standard (`family-landing-in-standard`). AGENTS.md item 1 says so, and the README's own example passes `--into ..`. README.md is at its 1422-line cap, so the sentence is on #459's checklist. - **`setup-project.py`'s own `cd <clone>` line and its step-(7) backstop lines print their paths unquoted.** This change quotes only the family lines it prints. Those two predate it and #509, and they are on #459's checklist. Reviewed independently at 10c93de: CHANGES NEEDED (0 blockers, 1 should-fix, 6 nits). The round at bb11b63 fixed the should-fix and all six nits, and its re-verification gave APPROVE WITH NITS (0/0/3), with SonarCloud's quality gate OK; those three are fixed at 3451a56. Main was then merged by GitHub's update-branch (8e1471c), by hand at 708f3bb and at fcbc7a2, where each time only the handbook footer conflicted, and by update-branch again at 22733ce (main 631f087, #503), with no conflict. CI was green there on Linux, macOS and Windows, with SonarCloud passing, but main had moved on: main f391e58 (#266, #509, #496 and #435) was merged by hand at 5a2776d, where again only the footer conflicted. CI was green there too, but main moved on again: main 8cd7dbe (#506 and #347) was merged by hand at 654df49, where once more only the footer conflicted. CI was green there as well, and main d41cc29 (#402) then came in by update-branch at e42ef74, with no conflict. CI is green on Linux and macOS, with SonarCloud passing, at e42ef74, a clean merge of main. All three OSes and SonarCloud passed at 654df49, and under Brett Heap's landing rule of 22:08:06Z (ruling 27) macOS and Windows carry over from that head. Main e6fbd74 (#515, for #514), which landed after it, touches none of its files, and the combined tree passed `tests/test_adopt_pins.py`, `tests/test_shape_doctor.py`, `tests/test_scaffold_leg_path_collisions.py`, the hygiene, AGENTS-code and CLI-reference tests, `tests/test_family_printed_quoting.py` and `tests/test_family_tracking_branch.py` (777 passed, 3 skipped). Closes #488 Closes #486 Closes #487 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…er before it clones, clones each member on its tracking branch, and `family.py siblings` runs the holder's own copy (#543) (#578) Three lines of #543, in one change and one re-pin, because `scripts/siblings.py` is a pinned copy in every family holder. Head: `8d3d1cceb30dfc93c7ceec130131038dc4f89e40`. The branch's own commits: the change (43d32eb, on bfed143), a merge of main at 9f80939 (8b4fc0b), the review round (2156a01), round 3 (5ce5b68, a family folder reached through a link; 0112dcb, a test skipped on Windows), and round 4 (8d3d1cc, a Windows junction counts as a link, and rv543b's nits). **1. The family folder is asked first** (`templates/family-root/scripts/siblings.py`, `_run_placement`, `parent_folder_warning`). - Before: in a holder whose parent folder is not named after the family, `make siblings` cloned every member into that folder, then printed the warning and the `mv` lines, and exited 0. After the move, the re-run cloned every member a second time beside the moved holder. The first clones stayed behind. - Now: the check runs before any `git` command runs or any member is cloned. The run prints the same WARNING and `mv` lines, says `NOTHING WAS CLONED` and why, ends with `siblings: stopped before cloning: the holder is not in a folder named <Family>. Nothing was cloned, fetched or moved.`, and exits 1 (`make siblings` exits 2, make's own code). `--dry-run` stops the same way. After the move, one run clones each member once. - Why a stop and not a later warning: no flag or documented answer lets a person place siblings outside the family folder. AGENTS.md, README.md, the handbook and the holder's own README and AGENTS-shape.md all describe a warning plus a printed `mv`, and the warning itself told the person to move the holder and re-run. A stop gives the same outcome without the stray clones. Exit 1 is the file's "finding a human can act on" (exit 2 is kept for "the question could not be asked"). - **The printed move runs as typed on every disk** (review round, S3). The name test stays exact on Linux, macOS and Windows. So the remedy has to work on each: - On a disk that ignores case (macOS, Windows), a holder in `inkrouter/` for the family `InkRouter` used to get `mkdir -p <p>/InkRouter` and `mv <p>/inkrouter/InkRouter <p>/InkRouter/InkRouter`. There `<p>/InkRouter` IS `<p>/inkrouter`, so that moved the holder onto itself and failed. Now, when `<p>/InkRouter` exists and `os.path.samefile` says it is this folder, the FOLDER is renamed through a staging name: `mv <p>/inkrouter <p>/InkRouter.folder`, then `mv <p>/InkRouter.folder <p>/InkRouter`. - At a filesystem or drive root (`/InkRouter`, `C:\InkRouter`), `<root>/InkRouter` is the holder itself, and the old lines were `mv X X/X`. Now the holder moves through `<root>/InkRouter.holder`, the staged move the common case already used. The warning names the root (`'/'`) where it used to say `''`. - Linux is unchanged: with nothing at `<p>/InkRouter` the holder moves there through `InkRouter.holder`; with a separate `<p>/InkRouter` folder it moves into it. - A link named `InkRouter` beside the folder and leading to it passes the check: the family folder, reached through a link (see "Round 3"). - **The header says `parent folder` until the check passes** (C3). It used to print `family folder <parent>` right above a warning saying the parent is not the family folder. Through a link it names the folder the clones go into, and the link. **2. auF-13: the tracking branch** (`siblings.py`, `tracking_branch()`, used in `place()`). - Before: a plain `git clone`, so a sibling landed on its remote's default branch. - Now: the clone passes `--branch=<branch>`, where `<branch>` is the `tracking_branch:` of the member's own `project.yaml` in the pinned copy under `members/`. That copy is the holder's record of the member, and the same field the member's own bootstrap reads for its legs. - Not read: `.gitmodules` (both templates say it carries no `branch=`, and nothing writes one) and `family.yaml`'s `tracking_branch:` (the holder's own branch: `family.py add`, `bump` and `remove` refuse to commit on it). - No record: when `members/<Project>` is not checked out, or its `project.yaml` names no branch, the clone keeps today's behaviour (the remote's default branch) and its line says why. - **A value that is no branch name is not used** (review round, N1). A number or a list (`5`, `[main]`) now reads `declares tracking_branch 5, which is not a branch name`, where it used to say it declares none. A string holding a character that does not print (an ESC sequence, a newline) is not handed to git and is shown in its `repr`, for example `declares tracking_branch '\x1b[2Jcleared', which holds a character that does not print, so it is not used as a branch`. It used to reach stdout raw. The member's own bootstrap escapes the same field (#391). Each such clone keeps the remote's default branch. No import was added to the pinned file. - A declared branch the remote does not have is git's own refusal: a `CLONE FAILED` finding naming it, exit 1, and no directory left behind. **3. auF-14: the holder's pinned copy** (`scripts/family.py`, `cmd_siblings`). - Before: `family.py siblings` used `runpy` to run this checkout's `templates/family-root/scripts/siblings.py`, inside family.py's own process. A holder pinned at another shape ran different code than `make siblings`. With no holder copy at all it still ran, and its `siblings-script-missing` remedy said it "runs the holder's own copy". - Now: it runs `<holder>/scripts/siblings.py` with the same interpreter, in its own process, from the holder, as the Makefile's `siblings` target does, and returns that exit code unchanged. The command is `scripts/siblings.py --root .` (plus `--dry-run`). Every word of it is a constant, so no command-line value reaches its argv (the case SonarCloud's `pythonsecurity:S8705` flags). - **The child keeps family.py's escaped stdout** (review round, S1). family.py escapes what its stdout cannot encode (#214), but a child takes its stdout settings from its environment. So the child now gets `inherited_child_environment()`, the helper `update-shape.py` already uses for the validators it runs (#254): this stdout's own encoding and a handler that cannot raise. Under `PYTHONIOENCODING=ascii` the child used to die on the em dash of its first line, `UnicodeEncodeError`, exit 1. Now it prints `\u2014` and goes on. The helper already guards a stdout with no encoding, and it never passes on the handler only family.py registers, which a child cannot look up. - A holder without the file is refused: `siblings-script-missing`, exit 2, before anything is cloned, in the words the holder's Makefile refuses with since #503. **4. `init --work-dir` no longer promises clones** (`scripts/family.py`, `_print_next_steps`; review round, S2). `--work-dir <dir>` makes no family folder. Where `<dir>` is not named after the family, the NEXT STEPS used to say `make siblings` "then puts each member's WORKING clone beside the holder, in the family folder", and `family folder <dir>`. That `make siblings` now stops. The block now says `--work-dir made no family folder, so make siblings clones nothing: it prints the mv that puts the holder in a folder named <Family>, and stops until it is there`, and `family folder none: <dir> is not named <Family>`. The `--into` output is byte for byte what it was. **The half of #461 that #543 names.** The sibling clone is `--no-local`. A member url that is a path on disk is then read through git, not copied file by file, so a repack in the remote cannot break it halfway (#440). **Docs.** - AGENTS.md family item 5 and the holder's AGENTS-shape.md said the stop "exits 1"; `make siblings` itself exits 2, make's own code. Both now say exit 1 (`make`: 2), rewrapped within the same lines. AGENTS.md stays at 490 lines. - The holder's `README.md` says the run stops before cloning, and which tracking branch it clones on. - `templates/family-root/family.yaml`'s `tracking_branch:` comment said the family bootstrap places each member on that branch. It now says what it is: the holder's own branch, which `family.py` refuses to commit on. The family bootstrap leaves each member detached, and `make siblings` reads the member's own `project.yaml`. No test or pin compares those bytes. - README.md and docs/handbook.html are untouched (see "Left out"). | point | what was found | what this head does | |---|---|---| | S1 | the child process lost family.py's escaped stdout (#214) | `env=inherited_child_environment()`; test with `PYTHONIOENCODING=ascii` | | S2 | `init --work-dir`'s NEXT STEPS promised a `make siblings` that stops | the `folder is None` branch says there is no family folder; test on the fixture's own `init` stdout | | S3 | on a disk that ignores case, and at a root, the printed move cannot run | the exact name test is kept on every disk; the folder rename and the staged holder move; tests by simulation on all three systems | | N1 | a `tracking_branch:` holding control characters reached stdout raw; `5` and `[main]` said "declares no tracking_branch" | not used, shown in its `repr`; "is not a branch name"; three tests | | N2 | "before any credential is resolved" was unguarded | the claim is now "before any `git` command runs", pinned with `GIT_TRACE`; reading the environment had nothing a test could observe | | N3 | a symlinked family folder now stops, and its printed move cannot run | round 3: a link named after the family, beside the folder and leading to it, passes; see "Round 3" | | N4 | AGENTS.md:409 said "exits 1" for `make siblings` | exit 1 (`make`: 2), within the same lines; the same fix in the holder's AGENTS-shape.md | | N5 | the merge section was stale | rewritten below | | C2 | the `family.yaml` comment was false | fixed | | C3 | `family folder` printed above a stop | `parent folder` until the check passes | **S3, the decision: keep the strict name test on every OS.** Brett Heap's rule for the case variants of `members/` (#362-#364, "Refuse on every disk") is one answer on every disk. The same spirit applies here. Accepting `os.path.samefile(folder, wanted)` instead would pass a holder on macOS that Linux stops. So every printed remedy now has to work as typed. The evidence: - **Tests, on all three systems.** The template's `siblings.py` is loaded in-process with `load_without_bytecode`, so nothing is written under `templates/`. `Path.exists` and `os.path.samefile` are patched to answer as a disk that ignores case does. The case-only test then does the printed moves with `os.rename` and checks that the next check passes. A case-sensitive disk is simulated with nothing or a separate folder at `<p>/InkRouter` (the separate folder skipped on Windows, see "Round 3"), and a link there, which now passes. The roots are asked as `PurePosixPath("/InkRouter")`, `PureWindowsPath("C:\InkRouter")` and `PureWindowsPath("C:\inkrouter")`. - **Run as typed** (a throwaway probe, not committed). Each printed line went to `sh -c` as printed. A disk that ignores case was emulated with a symlink that spells the folder in the family's case, and that exists only while the folder does (a step that leaves it dangling removes it). Then a real `siblings.py` ran in the moved holder: | case | lines printed | run as typed | then `siblings.py` in the moved holder | |---|---|---|---| | a disk that ignores case, holder in `inkrouter/`: 43d32eb | `mkdir -p <t>/InkRouter`, then `mv <t>/inkrouter/InkRouter <t>/InkRouter/InkRouter` | the `mv` fails, rc 1: `cannot move ... to a subdirectory of itself` | the holder is not placed | | the same, this head | `mv <t>/inkrouter <t>/InkRouter.folder`, then `mv <t>/InkRouter.folder <t>/InkRouter` | both rc 0 | rc 0, no WARNING, IRRS and IRSS cloned beside the holder | | the holder at `/InkRouter`: 43d32eb | `mkdir -p /InkRouter`, then `mv /InkRouter /InkRouter/InkRouter` | the `mv` fails the same way | the holder is not placed | | the same, this head | `mv /InkRouter /InkRouter.holder`, `mkdir -p /InkRouter`, `mv /InkRouter.holder /InkRouter/InkRouter` | all rc 0 | rc 0, no WARNING, both cloned | | Linux, nothing at `<t>/InkRouter` | the three staged lines, the same on both heads | all rc 0 | rc 0, both cloned | | Linux, a separate `<t>/InkRouter` folder | `mkdir -p` and one `mv`, the same on both heads | both rc 0 | rc 0, both cloned | The filesystem root ran with `/` mapped to a scratch folder, because this machine has no root access and no user namespaces. `C:\InkRouter` and `C:\inkrouter` were asked of the function only. Both print `WARNING the holder's parent folder is 'C:\\', not 'InkRouter'.`, then `mv 'C:\InkRouter' 'C:\InkRouter.holder'` (or `'C:\inkrouter'`), `mkdir -p 'C:\InkRouter'` and `mv 'C:\InkRouter.holder' 'C:\InkRouter\InkRouter'`. Nothing here ran on a real macOS or Windows disk. CI's macOS and Windows jobs do run the case-only test's two renames on their own disks. New file: `tests/test_family_siblings_placement.py`, 29 tests: 10 from the first round, 13 from the review round, 2 from round 3, 4 from round 4. The runs below are the review round's, at 2156a01; round 3's are in its own section. All runs used a stub `gh` first on PATH, a bogus token, an empty `GH_CONFIG_DIR`, a blanked git identity, `/usr/bin/python3 -m pytest`: - `tests/test_family_siblings_placement.py` on the working tree, before the commit: `23 passed in 50.77s`. - Set A at 2156a01: every `tests/test_family*.py` (15 files), `test_park_resume_targets.py`, `test_windows_paths.py`, `test_doctor_legs_members_next.py`, `test_commit_trailers.py`, `test_free_plan_hint.py`, `test_refusal_default_remediation.py` and `test_stdout_encoding.py`: `1007 passed, 20 skipped in 983.34s (0:16:23)`. The skips are `test_family_init_values.py:219` (16) and `:291` (3), which the file documents, and `test_stdout_encoding.py:286` (Windows only). - Set B at 2156a01: `test_cli_reference.py`, `test_repo_hygiene.py`, `test_agents_md_codes.py`, the three `test_template_*` files, the four `test_validate_family_*.py`, the ten `test_update_shape*.py` of the first round, `test_shape_doctor.py`, and now `test_update_shape_kept_helper.py` (main's check of the template's imports) and `test_validator_child_encoding.py` (the helper S1 uses): `1478 passed, 6 skipped in 1658.89s (0:27:38)`. The skips are NTFS junctions (3) and the doctor's PowerShell line (3), both Windows only. - The merge commit 8b4fc0b on its own, before the fix: the new file, `test_family.py`, `test_family_printed_quoting.py`, `test_family_makefile_siblings.py`, `test_park_resume_targets.py`, `test_windows_paths.py`, `test_clone_just_pushed.py` and `test_doctor_legs_members_next.py`: `375 passed in 686.29s (0:11:26)`. And `test_cli_reference.py`, `test_repo_hygiene.py`, `test_agents_md_codes.py`, `test_update_shape_kept_helper.py` and `test_template_text_placeholders.py`: `340 passed in 245.06s (0:04:05)`. - Nothing is written under `templates/`: after the mutation baseline run (the new file's 23 tests, the in-process ones included, plus four more) a clean tree had no `__pycache__` under `templates/`. The script builds two scaffolded members (Atlas on `main`; Borealis on `trunk`, with its remote's default branch set to `main`) and a holder made with `init --work-dir`, so its parent folder is `fam`, not `Orbit`. | | bfed143 | this branch | |---|---|---| | first `make siblings` in `fam/Orbit` | exit 0; Atlas and Borealis cloned into `fam/`; WARNING after | exit 1 (make: 2); nothing cloned; `NOTHING WAS CLONED` | | clones on disk after move + re-run | 4: `fam/Atlas`, `fam/Borealis`, `Orbit/Atlas`, `Orbit/Borealis` | 2: `Orbit/Atlas`, `Orbit/Borealis` | | Borealis sibling branch | `main` (remote default) | `trunk` (its tracking branch) | | `family.py siblings` marker from the holder's copy | 0 lines (ran the standard's) | 1 line | | `family.py siblings`, holder without `scripts/siblings.py` | exit 0, ran the standard's copy | exit 2, `REFUSED siblings-script-missing`, the Makefile's words | | `make siblings`, same holder | exit 2, the Makefile's refusal | same | The review round's reproductions are rv543's probes: `PYTHONIOENCODING=ascii` through `family.py siblings` (rc 1 with a traceback at 43d32eb; the new S1 test holds rc 0), the `--work-dir` NEXT STEPS, and the printed moves above. Each probe reverts one hunk in a throwaway worktree. Every one is caught. **First round** (at 43d32eb; the new file plus the four changed existing tests, 15 tests): | probe | what it puts back | result | caught by | |---|---|---|---| | M1 | no `--branch=` on the clone | 3 failed | tracking branch, branch the remote lacks, the issue's flow | | M2 | no `--no-local` | 1 failed | no shared object file | | M3 | the warning no longer stops the run | 6 failed | both new misplaced tests, and the four changed existing tests | | M3b | the folder check back after the placement, as on main | 6 failed | the same six | | M4 | `tracking_branch()` never finds a branch | 4 failed | tracking branch, branch the remote lacks, dry-run line, the issue's flow | | M5 | the clone line names no branch | 3 failed | tracking branch, not-checked-out words, dry-run line | | M6 | the warning no longer says NOTHING WAS CLONED | 2 failed | both new misplaced tests | | M7 | `scripts/family.py` as on bfed143 | 2 failed | holder's copy and its imports; the refusal | | M8 | `runpy` of the HOLDER's file inside family.py | 1 failed | holder's copy and its imports | | M9 | one word of the refusal differs from the Makefile's | 1 failed | the refusal | | M10 | no refusal for a holder without its copy | 1 failed | the refusal | **Review round** (at 2156a01; the new file plus the same four, 28 tests; the unmutated baseline is `28 passed`): | probe | what it puts back | result | caught by | |---|---|---|---| | S1 | no `env=` on the child process | 1 failed | the ASCII stdout test | | S2 | `--work-dir` prints the old promise | 1 failed | the `--work-dir` NEXT STEPS test | | S3a | a root is not taken for the holder itself | 3 failed | the three root cases | | S3b | no rename of the folder | 1 failed | the case-only test | | S3c | a link named after the family is renamed through | 1 failed (at 2156a01; equivalent since round 3) | case-sensitive disk, `a link` | | S3d | `same_directory` asks the disk before comparing the paths | 3 failed | the three root cases | | S3e | the warning names a root `''` | 3 failed | the three root cases | | N1a | a control character is used as a branch | 1 failed | `escape-sequence` | | N1b | `5` and `[main]` "declare no tracking_branch" | 2 failed | `a-number`, `a-list` | | N2 | the credential and `.gitmodules` read before the stop (rv543's mutation B, which nothing caught before) | 1 failed | the `GIT_TRACE` test | | C3 | the header always says `family folder` | 1 failed | the misplaced dry run | The doc and comment hunks have no test: AGENTS.md, the holder's AGENTS-shape.md, the `family.yaml` comment, the docstrings, and the `tests/conftest.py` comment. Round 3's mutants are in its own section. **What was wrong.** Take `<t>/InkRouter`, a link to `<t>/real-x`, which holds the holder. Both entry points resolve the holder to `<t>/real-x/InkRouter` and saw a parent named `real-x`. On bfed143 that was a warning, and the members were cloned into `real-x`. This PR turned the warning into a stop. The printed `mkdir -p <t>/InkRouter` passes, then `mv <t>/real-x/InkRouter <t>/InkRouter/InkRouter` fails with `cannot move ... to a subdirectory of itself`, rc 1 (rv543 had read it as working). So the PR made a dead end. **The rule** (`siblings.py`, new `family_link`). Let R be the resolved holder, P its parent and G P's parent. The check also passes when `G/<Family>` is a link (`os.path.islink`) and is the same directory as P (`os.path.samefile`). The holder is then reachable as `G/<Family>/<R.name>`. The members are cloned into P, the link's own target, so the family layout holds through the link. - Both entry points give the same answer, because both resolve R: `make siblings` through `find_repo_root`, and `family.py siblings` by running the child in the resolved holder. - The header then reads `family folder <P> (through the link G/<Family>)`. It names the folder the clones go into. - A link named after the family that leads to another folder is not accepted. Its printed move runs as typed: the holder moves into the link's target, and there the link passes. - A link elsewhere (`~/InkRouter` leading to `/data/real-x`) still stops. Its printed move runs as typed. - The case-only rename of S3 is unchanged and stays strict. On a disk that ignores case, a case-only alias is one directory entry and never a link, so it gets the rename. The two disks differ at one edge only: a link spelled in another case stops on Linux, with a staged move that runs, and passes on a disk that ignores case (round 4 corrected this sentence). **Run as typed** (a throwaway probe; each printed line went to `sh -c` as printed, then a real `siblings.py` ran where the holder ended): | layout | 2156a01 | this head | |---|---|---| | `<t>/InkRouter` links to `<t>/real-x`, which holds the holder | stops; `mkdir -p <t>/InkRouter` rc 0, then the `mv` fails, rc 1, `cannot move ... to a subdirectory of itself`; the next run stops again | passes, rc 0, no WARNING; header `family folder <t>/real-x (through the link <t>/InkRouter)`; IRRS and IRSS cloned into `<t>/real-x` | | `<t>/home/InkRouter` links to `<t>/data/real-x` (a link elsewhere) | stops; the three staged lines (`mv <t>/data/real-x/InkRouter <t>/data/InkRouter.holder`, `mkdir -p <t>/data/InkRouter`, `mv <t>/data/InkRouter.holder <t>/data/InkRouter/InkRouter`) all rc 0; the next run passes and clones both | the same | | `<t>/InkRouter` links to `<t>/other`; the holder is in `<t>/real-x` | (not run) | stops; `mkdir -p <t>/InkRouter` and `mv <t>/real-x/InkRouter <t>/InkRouter/InkRouter` both rc 0; the next run passes through the link, header `family folder <t>/other (through the link <t>/InkRouter)`, both cloned into `<t>/other` | **Tests** (`tests/test_family_siblings_placement.py`, two new; links made with `os.symlink`, each test skipping first where the system refuses one; none under `templates/`): - the link layout passes through `family.py siblings --dry-run` and through `make siblings`' own script, which clones each member into the link's target; - a link named after the family to another folder stops, and its move done as written leads to a run that passes; - the case-only test still prints the two-line rename; - the simulated `a link` case of the case-sensitive disk test now patches `os.path.islink` too, and expects no warning. **Windows** (rv543b). `[a folder]` of the simulated case-sensitive disk test is skipped on Windows, before any call. A WindowsPath compares without case, so `same_directory()` calls `<p>/InkRouter` and `<p>/inkrouter` one path there, and the case-only rename is printed. That answer is right for Windows, whose disk cannot hold the two folders, but it is not the disk the parameter simulates. Emulated on Linux with rv543b's mechanism (`pathlib._is_case_sensitive` returning False during each test's call): `1 failed, 24 passed in 31.32s` with the parameter collected (only `[a folder]`, since `os.name` is `posix` here), and `24 passed, 1 deselected in 31.49s` without it. **Mutants** (at 0112dcb, the new file, 25 tests; the baseline is `25 passed`, and no `__pycache__` appeared under `templates/`): | probe | what it changes | result | caught by | |---|---|---|---| | R3a | `family_link` never finds a link | 3 failed | the link layout; the link to another folder (its moved holder no longer passes); the simulated `a link` | | R3b | any same directory counts, link or not | 5 failed | the case-only rename (a case-only alias would pass); the three roots; the placed dry run's header | | R3c | any link named after the family counts, wherever it leads | 1 failed | the link to another folder | | R3d | the folder check does not ask `family_link` | 3 failed | the link layout; the link to another folder; the simulated `a link` | | R3e | the header does not name the link | 1 failed | the link layout | Round 2's S3c (the `is_symlink` test inside the case-only branch) is now an equivalent mutant: every link is answered by `family_link` first, so that test can no longer decide anything (`25 passed` with it removed). It is kept, as the S3 branch is kept as it was. **Docs.** `siblings.py`'s module docstring (FIRST, EXIT CODES) and `parent_folder_warning`'s, and the holder's README.md and AGENTS-shape.md, say a link of the family's name beside the folder, leading to it, counts. AGENTS.md's family item 5 was left as it was in this round, on the belief that the clause needed a ninth line; rv543b showed it fits, and round 4 adds it. **Runs at 5ce5b68** (0112dcb changes only the test file; see round 4 for where `25 passed` was run): - the new file, `test_family_makefile_siblings.py`, `test_family_printed_quoting.py`, the whole of `test_family.py` and `test_family_tracking_branch.py`: `248 passed in 196.84s (0:03:16)`; - `test_template_text_placeholders.py`, `test_update_shape_kept_helper.py`, `test_cli_reference.py`, `test_repo_hygiene.py`, `test_free_plan_hint.py` and `test_update_shape.py`: `397 passed in 213.21s (0:03:33)`. | point | what was found | what this head does | |---|---|---| | S-1 | `family_link` asked only `os.path.islink`, which is False for an NTFS directory junction, the link Windows makes without privilege. `Path.resolve()` follows a junction, so that layout stopped and its printed `mv` failed, where bfed143 warned and cloned | `os.path.isjunction` counts too (Python 3.12 and later, False off Windows; read with `getattr`, so an older Python still runs the file, and there the junction case stays as it was). Not an "any alias" rule, which would make a link spelled in another case a dead end on macOS. A simulated junction test runs on every system | | F2 | AGENTS.md item 5 lacked the link clause the holder's README and AGENTS-shape.md have; this body said it could not fit | it fits: "(nor linked as it)", rewrapped within the same eight lines by dropping one "yourself"; AGENTS.md stays at 490. README.md:883 and docs/handbook.html:1306 make the same claim and are left to a later README change (see "Left out") | | F3(a) | the comment and this body said the case-only rename and the link rule "never meet" | corrected: a link spelled in another case (`inkrouter` leading to `real-x`) stops on Linux, with a staged move that runs, and passes as the family's link on a disk that ignores case. No dead end either way | | F3(b) | 0112dcb's skip reason said a Windows disk cannot hold the two folders | it says the simulation assumes Windows' default path equality, which ignores case; an NTFS folder made case-sensitive can hold them | | F4 | three conditions no test guarded | one test each: `init --work-dir` into a folder named after the family keeps the promise (S2c); a blank `tracking_branch: " "` is not a branch name (N1s); a second path to the folder under another name, neither link nor junction, is not answered by the case-only folder rename (S3f) | | F5, F6 | the staging names are not checked; a dangling link named after the family breaks the staged move halfway | checklist candidates, below | Also corrected: round 3's `25 passed in 34.28s` ran on the working tree at 5ce5b68 with 0112dcb's change not yet committed. At 0112dcb itself, the round-3 mutant baseline gave `25 passed in 32.69s`. **Mutants** (at 8d3d1cc, the new file, 29 tests): | probe | what it changes | result | caught by | |---|---|---|---| | R4a | a junction no longer counts (`isjunction` dropped) | 1 failed, 28 passed | the simulated junction | | S2c | `init --work-dir` always says there is no family folder | 1 failed, 28 passed | `--work-dir` named after the family | | N1s | a blank `tracking_branch` is used as a branch | 1 failed, 28 passed | `[blank]` | | S3f | the case-only folder rename for any second path to the folder | 1 failed, 28 passed | the rename only for a difference of case | The unmutated baseline is `29 passed`, and no `__pycache__` appeared under `templates/` after it. **Runs at 8d3d1cc:** - the new file on the working tree, before the commit: `29 passed in 21.84s`; - the new file, `test_family_makefile_siblings.py`, `test_repo_hygiene.py`, `test_agents_md_codes.py`, and `test_family.py -k "siblings or init"`: `195 passed, 71 deselected in 66.86s (0:01:06)`. **Main merged at 8b4fc0b** (`git merge --no-ff origin/main`, main at 9f80939: #551, #552, #554, #553, #561, #562, #579, #567). One hunk conflicted, in `tests/test_family_printed_quoting.py`: #554 added `env=NO_BYTECODE` to the `run_script(FAMILY, "siblings", ...)` call, and this branch changed the next line from `returncode == 0` to `== 1`. The resolution keeps both lines whole: ``` result = run_script(FAMILY, "siblings", "--family-root", str(holder), "--dry-run", env=NO_BYTECODE) assert result.returncode == 1, result.stdout + result.stderr # #543 ``` `tests/conftest.py`'s comment above `NO_BYTECODE` said `family.py siblings` runs the template's copy through `runpy`, which this branch makes false. It now says what the constant guards and why it stays on those two calls (lane 3 agreed to the wording on #554). `NO_BYTECODE` stays at both call sites; dropping it is a later cleanup. The merged tree differs from main only in this branch's files and that comment. **merge-tree at 8d3d1cc:** origin/main f5fc705 is clean and not merged (one more commit since dd3f23a, `update-shape.py`'s refusal of a hard-linked file); AGENTS.md is 490 lines in the merged tree. Lane 5's `fix/family-bump-to-checks` (8024f9c), `fix/family-members-case-variants` (e5aac93), `fix/family-remove-gitmodules-missing` (ebe6b51) and `fix/link-source-chain-defaults` (a707d4b): clean. #573 and #575 were not re-checked at this head: round 4 changes `siblings.py`, AGENTS.md and the new test file, none of which they touch. **merge-tree at 0112dcb** (`git merge-tree --write-tree`, after a fresh fetch): - origin/main dd3f23a: clean, and not merged; this PR is behind it by three commits. AGENTS.md is 490 lines in the merged tree and keeps this branch's line 409. - #573 `fix/537-dry-run-asks-remotes` (69c58aa) and #575 `fix/539-validators-lockstep-remedies` (0680799): clean. - Lane 5's `fix/family-bump-to-checks` (8024f9c), `fix/family-members-case-variants` (8a95345), `fix/family-remove-gitmodules-missing` (ebe6b51) and `fix/link-source-chain-defaults` (a707d4b): clean. **At 2156a01, for the record:** - origin/main 64fc9a8: clean. Main moved after the merge, by a35f1be (`update-shape.py`) and 64fc9a8 (the assembly root's `bootstrap.py`). Neither touches a family file. A scratch merge of this head with 64fc9a8 (no ref, since removed) ran the new file, `test_family_makefile_siblings.py`, `test_family_printed_quoting.py`, and main's new `test_update_shape_messages.py`, `test_update_shape_header_comments.py`, `test_update_shape_branch_any_directory.py` and `test_bootstrap_after_bump.py`: `269 passed in 156.48s`. Then `test_update_shape.py`, `_landing`, `_next_quoting`, `_pr_base`, `_kept_helper`, `_add`, `test_cli_reference.py` and `test_repo_hygiene.py`: `411 passed in 322.52s (0:05:22)`. - origin/main dd3f23a (`openRepoShape --doctor` wording; `shape-doctor.py`, `render-cli-reference.py`, AGENTS.md, README.md, the handbook, `docs/cli.md`): clean, not merged, not test-run. - #573 `fix/537-dry-run-asks-remotes` (69c58aa) and #575 `fix/539-validators-lockstep-remedies` (93b68f6): clean. - Lane 5's `fix/family-bump-to-checks` (8024f9c), `fix/family-members-case-variants` (40eda84), `fix/family-remove-gitmodules-missing` (b1120c3) and `fix/link-source-chain-defaults` (a707d4b): clean. - **README.md and docs/handbook.html.** README:882-884 and the handbook at :1305 say `make siblings` "WARNS and prints the exact `mv`". That stays true: the output still opens with `WARNING` and prints the `mv`. README:871-872 ("the same file through the other entry point") becomes true with this change. Neither says the run now stops; that is incomplete, not false, so this PR does not touch README.md. Adding "and clones nothing until it is moved" could be a later README change. That change should also add the link clause round 3 gave the holder's README and AGENTS-shape.md and round 4 gave AGENTS.md: README.md:883 and the handbook at :1306 say the run warns whenever the parent folder is not named after the family, which a family folder reached through a link no longer does. They are left alone here because a README change needs the hash step and README PRs land one at a time. - **`scripts/shape_materialize.py:519`**: its comment says siblings "warns about the parent folder without moving anything", which is still true. It is left alone because PRs that touch that file meet SonarCloud's S8705 finding at `run()`. - **`templates/family-root/scripts/validate-family.py`**: not touched (lane 2's #539). - **The `--make` dispatch header** still prints `family folder`. The dispatch runs no folder check (it is the placement's business), so it was left as it is. - **`make siblings` crashes on a stdout that cannot encode its em dash or a path.** Pre-existing on bfed143. The holder's `siblings.py` never escapes its own stdout, so under `PYTHONIOENCODING=ascii`, or a Windows code page without a character in the holder path or a member name, `make siblings` dies with a `UnicodeEncodeError` on its first line, before any clone. `family.py siblings` no longer does (S1), because it hands the child its own escaped settings. The family `bootstrap.py` already calls `repo_shape.escape_unencodable_stdout()`; a holder that kept an older `repo_shape.py` may not have it, which main's kept-helper check now looks at. - **`shape-doctor.py:5640`: the `members` row's note says "`make siblings` places them"**, a command that now stops, clones nothing, in a holder whose parent folder is not named after the family. - **A family name a disk cannot hold exactly never passes the folder test on that disk.** Windows drops a trailing `.` or space from a folder name and reserves names like `CON` and `NUL`. `--family` goes through `SAFE_ARG_RE` (`scripts/repo_shape.py:106`), which allows a trailing `.`. Such a holder stops on every run there. - **A `tracking_branch:` that names only a tag gives a detached clone.** `git clone --branch=<name>` takes a tag too, so a member that declares `v1` where its remote has only a tag `v1` is cloned detached at the tag, rc 0, and the line calls it "its tracking branch". - **`tests/test_cli_reference.py` writes `templates/family-root/scripts/__pycache__/bootstrap.cpython-312.pyc`** (found by rv441b on #576 and here). `scripts/render-cli-reference.py` runs the template's `siblings.py --help` with `PYTHONPATH=scripts`, and that file imports `bootstrap` from beside it. Shown in clean trees: no `__pycache__` before, the file after `tests/test_cli_reference.py` alone (`22 passed`), on 04c9d77, on two merged trees, and on this branch's merge. `.gitignore` hides it. A `.pyc` under `templates/` breaks `template_values()` and its twin in `test_scaffold_leg_path_collisions.py` in a later run in the same tree, which read every template file as UTF-8. #554's `load_without_bytecode` does not cover it; `PYTHONDONTWRITEBYTECODE=1` in the renderer's child environment would. - **A staging name that already exists sends the moves astray** (F5). Neither `<Family>.folder` (round 2) nor the older `<Family>.holder` is checked before it is printed. With `<t>/InkRouter.folder` already a folder, on an emulated disk that ignores case, both printed lines exit 0 and leave the holder at `InkRouter/inkrouter/InkRouter` (rv543b's probe). The next run stops again with a rename that works, so it is recoverable. The move text predates this PR; the stop is what makes it the only way forward. - **A dangling link named after the family breaks the staged move halfway** (F6). `not wanted.exists()` follows the link, so a dangling `<t>/InkRouter` reads as nothing there: `mv <t>/real-x/InkRouter <t>/InkRouter.holder` exits 0, then `mkdir -p <t>/InkRouter` fails, `File exists`, and the holder is left at `<t>/InkRouter.holder` (rv543b's probe). `os.path.lexists` would send it to the two-line move, whose `mkdir -p` fails before anything moves. The move text predates this PR; the stop is what makes it the only way forward. - **`tests/test_validator_child_encoding.py`'s launcher guard does not see `family.py`.** It looks for `subprocess.run([sys.executable, ...])`, and `family.py` runs `[sys.executable or PYTHON, ...]`, so the guard would not notice if `env=` were dropped again. The new S1 test catches that by behaviour (probe S1), but the static guard could list `family.py` too. Review: rv543 found 3 should and 5 nits on 43d32eb; the fix round merged main 9f80939 by hand (rv543's resolution) and fixed them; re-review rv543b found that the stop's printed move had no way forward through a family folder reached by a link (rounds 3 and 4 accept a symlink and a Windows junction), and its nits were fixed or listed as checklist candidates on #459. CI (Brett Heap's ruling 27): H0 8d3d1cc passed tests, tests-macos, tests-windows and SonarCloud; the head 3e3c833 is a clean update-branch merge of main fb786c1, with tests and SonarCloud green. Landed on Brett Heap's word of 2026-10-15T13:32:47Z, "All five, once green". Closes #543 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>



Lane: openreposhape-5 (openRepoShape-5)
Claim: #489 (comment)
A family holder cut before 2026-09-09 (#76) has no
scripts/siblings.py. Updating it the way AGENTS.md prescribes,update-shape.py applywithout--add scripts/siblings.py, still copies the changed Makefile, and that Makefile'ssiblings,parkandresumetargets run the script.applyexited 0,validate-family.pywas green, onlynote ... not addedwas printed, and then all three targets died onpython3: can't open file '.../scripts/siblings.py'.This takes the issue's second option, which keeps per-file consent and never leaves a dead target.
applystill refuses nothing, copies the Makefile and adds nothing it was not told to.What changed
templates/family-root/Makefile: each of the three targets asks make itself,$(wildcard scripts/siblings.py), whether the script is there, and when it is not it refuses by name with$(error): exit 2, one shared message, and no shell involved (notest -f, noecho), so the answer is the same wherever make runs. The message says what is missing, thatupdate-shape.pyadds a file only on the human's word,python3 update-shape.py check --root "<this holder>"to read the verdict, then the apply command it prints with--add scripts/siblings.py, and, for a holder that took the script and then deleted it (wherechecksayscopy-missingand--addrefuses a pinned path), to restore it from the holder's history instead. With the file there, each recipe is what it was: same command line, same arguments.update-shape.py:checkandapplysay, on the line that already reports the file, that the Makefile calls it.checkappends it to theupstream-addeddetail andapplyto itsnote ... not addedline. It is a table (CALLED_BY_A_COPY) with one row, and it speaks when the calling copy isupstream-changed(the one verdictapplywrites on its own: "the Makefile thatapplycopies calls it, somake siblings,make parkandmake resumecannot run until it is added") orunchanged(the holder after that firstapply, pinned at the target, where every latercheckandapplystill finds the scriptupstream-added: "this root's Makefile already calls it, so ... cannot run until it is added"). Alocally-modifiedorbothMakefile says nothing. Nothing is refused and the exit codes are unchanged.AGENTS.md, item 6 of the family section: one clause, in place, with the same line count, so the file stays at its cap (490/490). It keeps "per file":apply --add scripts/siblings.pytakes it on the human's word, per file, and without itmake siblings,parkandresumerefuse.Evidence
main(7a8b981) with a real holder cut at 0170edd, the commit the issue names, local remotes and a stubgh:checksaysMakefileupstream-changedandscripts/siblings.pyupstream-added;applywithout--addexits 0 withfamily ok;make siblings,make parkandmake resumeeach printcan't open fileand exit 2.checkandapplycarry the sentence above,applystill exits 0, and the three targets exit 2 withmake <verb> needs scripts/siblings.py, which this holder does not have. ...and nocan't open file. Withapply --add scripts/siblings.pyall three exit 0 and run the real script.tests/test_family_makefile_siblings.py(new, 31 tests; skipped only wheremakeis absent) cuts a holder from a standard before Family workstation layout: a plain <Family>/ folder holding the holder and the members' working clones — family.py init lands there, the scaffold learns --family, the holder gains a siblings utility that warns and never moves #76 (a clone with noscripts/siblings.py, its copy-list entries stripped and the v0.4 Makefile: built, not read from history, so a shallow clone runs it), updates it to this checkout's bytes withapplywith and without--add, and runs the three targets in each. It also runs the Makefile alone: the refusal is one message for the three targets, it needs no shell (SHELL=/nonexistentstill refuses),make -nrefuses too, the present-script path calls the script with the arguments it always had,help,bootstrap,validateandpinsare untouched, andCALLED_BY_A_COPYis held to the template. It also covers a secondcheckand a secondapplyon a holder already at the target (theunchangedclause), and thecopy-missingsentence in the refusal. It carries noWINDOWS_SKIP, unliketests/test_park_resume_targets.py: the refusal needs no shell, andwindows-latestships a GNU make (the runner-images install script copiesmingw32-make.exetomake.exe), so the tests are expected to run on all three runners. CI's logs are quiet-mode, so they do not show whether they ran.origin/main's, 13 of the 31 fail (the refusal tests, the one-message and no-shell tests,make -n, and everymakerun on the updated holder, which printcan't open fileagain). Withupdate-shape.pyreverted, 9 fail (thecheckandapplysentence, theleft_dead_bycases and the table). Taking only theunchangedbranch out ofleft_dead_byfails 2 (the unit case and the secondcheck/apply), and taking only thecopy-missingsentence out of the Makefile fails 3 (the refusal test, once per verb).git grepof its sha256 finds nothing, and everyshape-pin.yamlrow is rendered from the bytes atinitandapply. A holder's pinned digest moves with the file, as for any template change.Tests
All at 49330dc (origin/main 5a25387 merged in), with a stub
ghfirst on PATH, a bogus token, an emptyghconfig directory and the git identity blanked:tests/test_family_makefile_siblings.py: 31 passed.tests/test_update_shape*.py(seven files), the new file,tests/test_park_resume_targets.py,tests/test_repo_hygiene.py,tests/test_agents_md_codes.pyandtests/test_cli_reference.py: 438 passed, 3 skipped (the NTFS-junction cases, Windows only).tests/test_family*.pyandtests/test_shape_doctor.py(the doctor loadsupdate-shape.pyin process): 842 passed, 11 skipped (tests/test_family_init_values.py:242, which the file documents, and the doctor's PowerShell line, which prints only on a Windows host).At 44000f6, after GitHub's update-branch merged main af01068 (no hand edit), the same sets ran again with the same stubs. The update-shape set, which now has eight files with #374's
tests/test_update_shape_next_quoting.py: 535 passed, 3 skipped. Everytests/test_family*.pyandtests/test_shape_doctor.py: 858 passed, 11 skipped. That head was then merged with main 47b1e06 (#395, the templates quote their text placeholders) as a local gate commit. On it, the two sets, together withtests/test_template_text_placeholders.py, gave 1481 passed, 25 skipped.Merge-tree. At 49330dc, which contains
origin/main5a25387 (merged as f08129d),git merge-tree --write-treeis clean againstorigin/main, against every open pull-request head that touchesAGENTS.md,update-shape.pyortemplates/family-root/(#347, #374, #395, #465, #474), and against lane 5'sfix/family-commands-and-branch(10c93de, #486-#488). #491 conflicts only indocs/handbook.html, as it does withorigin/main. The trees merged with #474 and withfix/family-commands-and-branchwere run: the new test file and the neighbouring suites pass on both, andAGENTS.mdstays at 490 lines.Review. An independent review at 8643774 gave APPROVE WITH NITS (0 blockers, 0 should-fixes, 5 nits). It reproduced the defect on
mainand the fix on this head, and ran 10 mutations, all of them caught. Round 2 (49330dc) took four of the nits: the test file's docstrings say where it runs; the refusal also covers a holder that deleted a pinnedscripts/siblings.py; AGENTS.md keeps "per file"; and a latercheckon a holder already at the target names the Makefile that calls the script. The fifth nit was this description. Re-verified at 49330dc: APPROVE WITH NITS (0/0/1). That nit was this description's merge-tree paragraph, now as above.Left out
applydoes not refuse to take the Makefile without the script (the issue's first option), and it does not take the script on its own: both break per-file consent.AGENTS-shape.mdandREADME.mdare pinned copies and say nothing about this; editing them moves every holder's digests for a sentence the Makefile's own refusal already carries. README and the handbook are untouched.shape currencyrow does not carry the sentence and is unchanged; the per-file detail line ofcheckandapply's note do, and the Makefile's own refusal covers the moment of use.$(if),$(call),$(wildcard)and$(error), which 3.81 documents. It was run on 4.3 here. CI'stests,tests-macosandtests-windowsand SonarCloud all passed at 8643774, and again at ca4778e (49330dc with main 48585e6 merged in by GitHub's update-branch), at 6fe17be (main 57c85e9 merged in the same way, after update-shape.py:apply --branchcompares the pin, the manifest and every pinned copy with HEAD before its first write and refuses a difference asupdate-branch-uncommitted, so an in-placeapplyleft uncommitted no longer ends in a commit its own validate-pins.py refuses, in git's "nothing to commit", or in "nothing to do" with no branch made (#436) #474 changedupdate-shape.py; no hand edit) and at 44000f6 (main af01068 merged in the same way, after update-shape.py: every NEXT line a run prints quotes its root, upstream,--accept-localpaths, branch and lane trailer withshlex.quote, socheck'sapply,apply --branch'sgit -C <root> push && gh pr createandapply's owngit -C <root> checkout -b && git -C <root> commitpaste whole from a root undermy projects/,it's here/or$HOME/(#325) #374 changedupdate-shape.pyand setup-project.py: every question - the yes, each install offer, the project name - is asked at the person's own terminal, read from the controlling terminal when stdin is the pipe the script arrived down, and a run with no terminal at all refuses naming that terminal as the way on and--yesonly as the person's own answer, so the README'scurl ... | bash -s --asks in the person's terminal instead of refusing, and an assistant's shell is no longer sent to re-run with the flag its first rule forbids it (#476, #492) #491 and README.md and AGENTS.md: 28 statements the v1.0.0 accuracy pass found false are corrected to what the tools do on main (#483) #505 changedAGENTS.md; no hand edit).Closes #489
🤖 Generated with Claude Code
Summary by Sourcery
Make family commands fail clearly and provide actionable update guidance when scripts/siblings.py is missing.
Bug Fixes:
Enhancements:
Tests: