Repository navigation
shape-doctor.py: a conflicted shape pin's NEXT takes the pin alone and gives a copy conflicted beside it to a person to merge by hand, so pasting it no longer drops an edit kept with --accept-local and then says COMPLIANT (#485) - #496
Conversation
…t read as a pin (a merge conflict, a non-UTF-8 byte, a `commit:` that is not 40 hex, a deleted file) is `INVALID`, exit 1, with `update-shape.py check` as its NEXT, instead of `CANNOT ANSWER`, exit 3, sending the reader to `git fetch --all` in the standard (#237) `check_shape_currency` took every refusal `update-shape.py prepare` raises for this checkout failing to ask. `prepare` also reads the ROOT's own pin, and those refusals are about their repository. The doctor now reads the pin first with `read_pin` (the function `prepare` calls for it), so the two kinds are told apart by which read raised them, not by a code list or by text. A refusal from the pin is a FINDING with no `environment`, quoting `REFUSED <code>: <detail>` without its remediation (the same wording family #232 gives an unloadable manifest); a refusal from the standard stays `CANNOT ANSWER`, exit 3, unchanged. The NEXT is `update-shape.py check` with the file at fault named, not `validate-pins.py`: that validator passes a `digest_definition` this tool will not recompute, so there the shape-currency row is the only red one and the verdict is `INVALID (shape-currency)`. WIP state at commit time: the 13 new tests pass at this head and fail on main 39ac59c (`assert 3 == 1`); tests/test_update_shape*.py and tests/test_cli_reference.py are 133 passed; the full tests/test_shape_doctor.py run was still in progress with no failure so far. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…raph (#237) `tests/test_repo_hygiene.py` caps the README at 1416 lines and pins `docs/handbook.html` to the README's blob; the sentence took it to 1420 and moved the blob, and the doctor's README paragraph already says what stays true (exit 3 is a pin naming a commit THIS checkout does not carry). The argument lives in `unreadable_pin_row`'s and `check_shape_currency`'s docstrings instead. The previous commit's body says the README gained a sentence; this one removes it, and the branch has no README change against its base. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…t read names the git command that puts back the last pin a tool wrote (`checkout --ours`, `checkout HEAD` or `checkout <commit>~1`, chosen from the pin's own git state), so the row's NEXT fixes it instead of refusing it again (#237) The review of PR #267 at 092a5f9 found the new row's NEXT a dead end (F1). `update-shape.py check` reads the pin with the same `read_pin`, so run as printed it exited 2 with the same refusal every time, and the remediation it then printed was wrong for the case: `git submodule update` for a conflict, "adopt it" for a deleted pin, "re-save the file" (a hand edit of a pinned file) for a stray byte. An operator who follows AGENTS.md ("Run the one the row names ... never edit a pinned file") was stuck, and README's "names the exact command that fixes it" was false for this row. `pin_restore` now picks the command from the pin's state, read-only: - unmerged (porcelain UU, AA, ...): `checkout --ours` (or `--theirs` where only that side holds a pin, read off the index stages, not the letters) of every conflicted path the shape writes: the pin, the manifest, README.md and the copies, never a file of the project's own; - uncommitted, where HEAD holds a pin: `checkout HEAD -- <pin>`; - otherwise, where the last commit to change the pin has a parent that holds one: `checkout <that commit>~1 -- <pin>`; - none of these: the old `update-shape.py check` NEXT, saying that whether the root was ever scaffolded or adopted is a person's call. `--json` records which, as `detail["restore"]`. Status is read as bytes with `--no-optional-locks`, so the doctor still writes nothing. N2: no note carries a `;`, an apostrophe or a backtick, so a pasted line is one command (#277). N1: the docstring names #232's `unloaded_manifest_row`, which is not on main or this branch. Tests run each printed command and ask the doctor again: the nine uncommitted faults and two committed ones (a `git rm` and a `commit: "zzzz"`, each followed by an unrelated commit) end COMPLIANT, exit 0; the issue's own merge, with `Makefile` and a file of the project's own also conflicted, names only `Makefile` and the pin and then reads the pin; a squashed root commit keeps the check NEXT. At 092a5f9 each of these fails, and head's NEXT run as printed exits 2. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… put back from before the commit that deleted it, and a merge where only the other side holds a pin takes `--theirs`, so the two `pin_restore` branches no earlier test reached are pinned (#237) `pin_restore` (7c11c62) has two choices that none of the round's first tests exercised: - `A ` in the status: the pin was deleted in a commit and a bad one staged at its path. HEAD holds no pin, so `checkout HEAD` names a path git refuses. The code asks `cat-file -e HEAD:./<pin>` before it says "uncommitted", and falls through to the last commit that changed the pin, `<that commit>~1`. The test asserts `committed`, the argv, and that running it ends COMPLIANT. It kills the mutant that drops the `cat-file` check (`elif xy:`). - `DU`: this branch deleted the pin and the other changed it. Only index stage 3 holds a pin, so `--ours` would refuse the path. The test asserts `checkout --theirs -- <pin>`, a note that says only that side holds a pin, and that the pin reads once it has run. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…te under `contracts/` that does not load (a conflicted or non-UTF-8 shape pin, a leg pin, the naming copy) with the git command `pin_restore` picks from its own state, and leaves a file of the project's own to a person, so its NEXT no longer tells the reader to delete the pin (#291) The row collected every file under `contracts/` that does not load as a mapping into `unreadable`, and gave it the unregistered kind's sentence: "register the kind in shape-doctor.py's MANIFEST_VALIDATORS, beside the check that validates it, or remove the file from the root". A file that does not load has no `kind:` to register, so the one half a reader could follow was to delete the file -- for a conflicted `contracts/shape-pin.yaml`, the pin. AGENTS.md tells an agent to run the NEXT a FINDING names, and the sentence is not even a command: the apostrophe in "shape-doctor.py's" leaves `shlex.split` with no closing quotation. `pin_restore` (#237, PR #267) takes the file to put back, defaulting to the shape pin, so its four states, commands and notes are unchanged for the `shape currency` row. `tool_written` admits every `contracts/<name>-pin.yaml` (the shape pin, a leg's, a neutral product's) and the shape copies the root kind lists, which under `contracts/` is the naming policy; each of those is put back from git, and only the shape pin takes the rest of the shape with it in a merge. A leg pin is named alone: `checkout --ours` on a conflicted gitlink exits 0 and changes nothing. A file of the project's own is not put back from git, because each of the three commands throws away somebody's edit. Its case is `project`, and where no file has a git command the NEXT is this doctor again, with a note that what the file should say is for a person to decide. The row quotes the loader's refusal for each file, less the path, and `detail` gains `restore` and `refusals` only when a file does not load. Where an unregistered kind sits beside a file that does not load, the row names the restore first; the next run names the sentence, which stays the unregistered kind's alone. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…es and leaves a conflicted `project.yaml` or `README.md` to a person, and every pin it puts back out of history is read first, so the other side's own edits survive it and two breaking commits in a row, a sparse checkout or assume-unchanged no longer name a wrong rollback (#237) Fix round 2 on PR #267, answering lane openRepoShape-2's re-verify of 9f64bd0 (V1, V2). V1 (P2). The unmerged NEXT named the manifest and README.md beside the pin and the copies, and `checkout --ours` writes stage 2 whole. `apply` writes only two lines of the manifest's `shape:` block and README's `Shape:` line; every other line is the project's own, so the other side's edits to those files went, hunks git had merged cleanly included, and the doctor then said COMPLIANT. Now only the files the shape writes whole are taken: the pin, each `copy_source` key and its collision copy. A conflicted manifest or README.md is named in the note as a person's merge and listed in `detail["left_for_a_person"]`, as #232's manifest row already says of the same file. The review's optional refinement (naming either file where the dropped side changed only tool-written lines) is not taken. V2 (P3). The committed class named `<last>~1` whenever it held a pin. It never asked whether HEAD's pin was the broken one, or whether the pin it put back read. A sparse checkout or assume-unchanged over a good re-pin got a rollback of that re-pin, and two commits in a row that each broke the pin cycled between `checkout <last>~1` and `checkout HEAD`. - `update-shape.py`: `read_pin`'s checks move into `check_pin(pin, pin_path)`, which `read_pin` calls, so the doctor checks a pin out of git with the same code. - `pin_reads_at` reads `<rev>:contracts/shape-pin.yaml` with `cat-file blob` in memory (no temporary file: the doctor writes nothing), decodes and parses it as `load_yaml` does, and runs `check_pin`. - `uncommitted` needs HEAD's pin to read (the review's optional step 4, so a dirty copy over a broken HEAD goes straight to history). - New class `hidden`: HEAD's pin reads and status is clean. The NEXT is `git ls-files -v`, which shows the S or h, and no rollback is named. - `committed` walks `rev-list -n 50 HEAD -- <pin>` newest first and names the first commit whose parent's pin reads. `pin_restore(ctx)` still answers `(restore, command)`; it gains an optional `detail` it writes `left_for_a_person` into, so existing callers are unchanged. Tests: the README case and the issue's own two-`apply` merge (real re-pins from a copy of the standard with two more commits; shape/a also renames the project and adds a README paragraph), two breaking commits, a dirty copy over a committed break, assume-unchanged and a sparse checkout. All six fail at 9f64bd0 for the review's reasons. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… for byte as it found it, so dropping `--no-optional-locks` from the pin's `git status` fails a test (#237) Fix round 2 on PR #267, V3 of lane openRepoShape-2's re-verify of 9f64bd0. The doctor's first promise is that it never writes into the repository it is pointed at. `--no-optional-locks` is what keeps `pin_restore`'s `git status` from refreshing the root's index, and the review's mutant m7, which drops it, passed all 20 #237 tests. After a committed break, every tracked file is made stat-dirty; the doctor runs; `.git/index` must be byte-identical. A plain `git status` must then rewrite it, as the control. The test is the review's scratch test, renamed. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dable-pin: `pin_restore(ctx, detail=None, pin=SHAPE_PIN)` and `pin_reads_at(ctx, rev, pin=SHAPE_PIN)` serve the `manifest kinds` row too (#237, #291) Round 2 gave `pin_restore` an optional `detail`, the `hidden` state, a history walk that names only a pin that reads, and a merge that takes the pin and the copies whole and leaves a conflicted manifest or README.md to a person. #291 had given it the file to put back. Both are kept: `pin` follows `detail`, so every call #267 makes is unchanged. `whole` takes the copies and `left` names the manifest and README.md only for the shape pin, and any other file is named alone. `pin_reads_at` reads the shape pin with `check_pin`, and any other file `tool_written` admits as reading when it loads as a mapping, which is what the `manifest kinds` row asks of it. `unreadable_next` passes the file by keyword. For the shape pin every note is byte for byte round 2's. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s named alone and its note says nothing of README.md, so a `pin_restore` that gave every file the shape pin's "a person merges README.md" clause fails a test (#291) #267's round 2 has the unmerged NEXT name a conflicted manifest or README.md for a person, because `apply` writes a line of each beside the shape pin. No tool writes either beside a leg pin, so the merge keeps that clause for the shape pin alone. The leg-pin case now conflicts README.md too, and the test asserts the note does not mention it. With the guard removed the case fails (README.md is named in the note). Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…in the root declares or a shape copy, and shows a copy's uncommitted change instead of discarding it, so a project's own `contracts/release-pin.yaml` that does not load keeps the edit, the merged hunk or the commit a person made (#291) The review of #358 at ebffb08 (F1, codex P1 discussion_r4248225898) found `tool_written` admitting every `contracts/*-pin.yaml` by its suffix. A committed, undeclared `contracts/release-pin.yaml` that stopped loading was then put back from git, and the NEXT, run as AGENTS.md says to run it, discarded `schedule: daily` (an uncommitted edit, `checkout HEAD`), a cleanly merged `- nightly` (a conflict, `checkout --ours`) or `owner: platform-team` (a commit, `checkout <sha>~1`). The same bytes as `contracts/release.yaml` were left alone. The suffix is the template's naming convention, not a declaration. Ownership now comes from what the root declares, all of it read already: the shape pin, `contracts/<role>-pin.yaml` for each leg `project.yaml` declares (the spelling `check_legs`, validate-pins.py and bump-leg.py use), `contracts/<product lowercased>-pin.yaml` for each `neutral_product_pins:` entry (`neutral_pin_path`), and the root kind's copies. Anything else is `project`, and its NEXT is this doctor again. Where `project.yaml` does not load it declares nothing, so a leg or neutral pin that does not load either is the project's for that run: a file wrongly called a tool's loses a person's edit, and one wrongly called the project's costs a run. `tool_written` says so. A copy is not a pin: `update-shape.py`'s `both` exit asks a person to merge one by hand and commit it, and git holds no other copy of that merge until it is committed. So a copy's `uncommitted` NEXT is now the read-only `git diff HEAD -- <copy>`, and nothing is put back (F1's optional point). Its `committed` and `unmerged` cases are unchanged: git keeps the person's bytes in the breaking commit or the other branch. Tests: the three release-pin states left to a person; a declared neutral pin put back beside an undeclared one left alone (in process, because the `pins` row would recheck a declared neutral pin through `gh`); a leg pin left alone while `project.yaml` does not load and put back once it loads; a copy's uncommitted change shown and kept. F2: a leg pin broken by a commit, a leg pin whose newest parent holds a list, the naming copy broken by a commit, and a leg pin added back after its deletion (`none`), which kill the review's surviving mutants mu1, mu2 and mu3. N1: the words for a pin that loads as a list, which kill mu5. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tor-manifest-kinds-unreadable-pin: `manifest kinds` keeps #291's restore beside #375's registered-elsewhere finding (#291, #327) #267's branch, this PR's base, now carries main up to 5678f68, #375 and #357 among it. Merged as text, shape-doctor.py conflicts only in `check_manifest_kinds`: the four regions main's #205 and #211 already caused, now with #375's `ungated` in them. #357 does not touch that function, and `pin_restore` and `pin_reads_at` merge clean, because main left them as #267 has them. The resolution keeps every side. In the loop, #205's skip of the root's own manifest comes first, then #291's `load_yaml` and `refusals`, then #211's `beside`, then #375's split of a registered kind by `ManifestKind.runs_in` into `validated` and `ungated`. `registered_elsewhere` joins `detail` only when there is one. The reason names every fault. The NEXT is `"; ".join(fixes)` as on main, with the unregistered kind's sentence for an unregistered kind only, never for a file that does not load. A row with a file that does not load prints the restore alone: joined by `;` to #375's "take ... out of this root" or to #211's sentence, it would be a second command in a pasted line (#277). The next run names the rest. The #291 docstring paragraph now names those faults. Two compose-only tests, each beside a conflicted shape pin: a `family.yaml` beside `project.yaml` (#211), kept from the earlier composition, and a `contracts/family.yaml` in an assembly root (#327), new. Each NEXT is the restore alone and equals the `shape currency` row's. Run, the row is the other fault's alone, and #327's own `git rm`, run in turn, leaves the root COMPLIANT. On the merged tree: tests/test_shape_doctor.py 312 passed, 3 skipped (the Windows-only PowerShell cases); tests/test_doctor_validator_rows.py 28 passed; tests/test_cli_reference.py and tests/test_repo_hygiene.py 63 passed; tests/test_windows_paths.py and tests/test_shape_advisory.py 79 passed; scripts/render-cli-reference.py --check ok. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…oduct pin, get a read-only `git diff` in every git state, and a copy is what the pin's `files:` rows name, so the `manifest kinds` NEXT no longer drops a person's merge, re-pin or edit with `checkout --ours`, `checkout HEAD` or a `<sha>~1` rollback (#291) Codex re-reviewed 2745890 with two P1s, and rv358 reproduced both (probes P10 to P13). r4250578605: a copy can hold the project's own edit -- above all the merge by hand `update-shape.py` asks for where a copy changed on both sides -- yet only its uncommitted state was shown. Conflicted on its own, the NEXT took one side whole with `checkout --ours`, and the hunk git had merged cleanly went with the other side; committed, it rolled the hand merge back with `checkout <sha>~1`. Now `pin_restore` gives a file `edited_by_hand` a read-only command in every state: `diff` for `unmerged`, `diff HEAD` for `uncommitted`, `diff <sha>~1` for `committed`. The note asks for the merge by hand, which is what the `shape currency` row's `--accept-local` and AGENTS.md's "`both` is a merge" already say. r4250578619: `tool_written` took copies from this standard's static copy lists, so a `contracts/repository-naming.yaml` an adopted project made its own (its row dropped) or kept beside a collision copy under `shape/` was put back from git. A copy is now what the root's own shape pin has a `files:` row for (`pinned_copies`), and where the pin does not read nothing is a copy, the safe side. The shape pin's own merge, which takes the copies whole with the pin (#267), now names the copies the taken side's pin has rows for (`pin_at` on the index stage), not the lists. rv358's P3: no tool writes a neutral product pin after the scaffold, so its change is a person's and gets the same read-only commands. Tests: one per probe, red at ce2ce75 (10 failed of 23 selected). Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…kinds-unreadable-pin: #267's three files kept as this branch has them, which already holds #267's final head f6dd11a (#291) #358 was stacked on #267 and now targets main, where #267 landed as the squash commit decff87. `git diff f6dd11a decff87 -- shape-doctor.py update-shape.py tests/test_shape_doctor.py` is empty, and decff87 is the only commit in 5678f68..decff87 that touches those three files, so main changes them only by what this branch merged from f6dd11a in ce2ce75. Git still conflicted in shape-doctor.py, because the merge base is 5678f68 and this branch rewrote `pin_restore` after f6dd11a. Each of the three is taken from this branch (`checkout --ours`), and `git diff --cached HEAD` on them is empty. Every other path is main's at decff87 (dd8d0ea, a92e60b, f5cb616, 53bee5b), with no conflict. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s `origin_url`/`origin_name` and #346's `apply --add` refusal beside #291's read-only copies, so the branch is current with main and differs from it only in shape-doctor.py and tests/test_shape_doctor.py (#291) Main after decff87 brings, among others, two changes to the files this branch touches: 1184f67 (#404: shape-doctor.py's `origin_url` and `origin_name` read an `origin` that is not UTF-8, plus its doctor tests) and d6df36b (#346: update-shape.py refuses `apply --add` under a file the root keeps). Git merged both without a conflict. Each one's lines in the merge are its own commit's lines, one for one, and the lines this branch adds over main are the ones it adds over f6dd11a: #404's hunks are in `origin_url`/`origin_name`, far from `pin_restore`, and #346 does not touch `read_pin`, `check_pin`, `Row` or `parse_yaml`, which `pinned_copies` and `pin_at` call. `git diff --stat d6df36b` names shape-doctor.py and tests/test_shape_doctor.py only. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s `check_pin(..., root)` and uncaught `TypeError` composed into this branch's `pin_at`, so the branch is current with main after #343 landed and differs from it only in shape-doctor.py and tests/test_shape_doctor.py (#291) Main after d6df36b brings #343 (437a360, #290): update-shape.py's `check_pin` takes the root and refuses a `files:` that is not a list as `update-pin-row`, so shape-doctor.py's `pin_reads_at` passed `ctx.root` and stopped catching `TypeError`, and `pinned_paths` reads a `files:` that is not a list as pinning nothing. 437a360 equals #343's final head 91a4a4f on all six of its files. Git conflicted in `pin_at` alone, the body of the old `pin_reads_at` that this branch moved there. The resolution is the one f358 wrote down and rv358c ran: the docstring keeps #343's two sentences (`check_pin` refuses `files: 5` as `update-pin-row`; a `TypeError` is not caught), then this branch's ANY OTHER FILE and THE ROWS, NOT A YES paragraphs; the body keeps this branch's (a file other than the shape pin reads as its mapping, the shape pin as its rows' paths) with `check_pin(data, ..., ctx.root)` and `except (Refusal, us.YamlError, ValueError)`. `pinned_paths` and the test file's three new ROOT_PIN_FAULTS cases merged clean. The merge's change to each file over this branch is #343's own lines, one for one, with `ctx.root` on this branch's form of the `check_pin` line; its change over main is this branch's lines over d6df36b, with that same `ctx.root`. `git diff --stat 437a360` names shape-doctor.py and tests/test_shape_doctor.py only. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… own "so", and `pinned_copies` no longer catches `TypeError`, so the pin's `unmerged` and `committed` notes read as one sentence and a caller out of step with `check_pin` fails loudly here as it does in `pin_at` (#291) Two nits from the re-verify of a5563c3 (rv358c, N1 and N2). N1. `BY_HAND_WHY["pin"]` ended "after the scaffold, so every change to one was made by a person", and `pin_restore` appends ", so neither side is taken whole" to the `unmerged` note and ", so nothing is rolled back" to the `committed` note, so both printed two `so` clauses in one sentence. The reason now joins its two facts with "and" ("..., and every change to one since was made by a person"), which leaves each note one conclusion; the copy's reason, which never ended in "so", and the `uncommitted` note are unchanged. The P13 test now also asserts that no sentence of the note holds two ", so " clauses; it opens on the same words it asserted before. N2. #343 (437a360) stopped `pin_at` catching `TypeError`: `check_pin` now refuses a `files:` that is not a list, or a row that is not a mapping, as `update-pin-row`, so nothing in a pin's bytes raises one, and one means a caller out of step with `check_pin`. `pinned_copies` calls `read_pin`, which runs the same `check_pin`, so it drops `TypeError` from its `except` by the same reasoning. A pin that does not read still makes the empty set, and nothing is a copy for that run. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d gives a copy conflicted beside it to a person to merge by hand, so pasting it no longer drops an edit kept with `--accept-local` and then says COMPLIANT (#485) A merge of two branches that each re-pin the shape leaves `contracts/shape-pin.yaml` and a pinned copy both conflicted: an `update-shape.py apply` to a newer standard on one side, and on the other a person's edit of the same copy kept with `apply --accept-local`, the exit the `shape currency` row names for a locally modified copy. The NEXT both rows printed was `checkout --ours -- <copy> contracts/shape-pin.yaml`, the taken side's copies whole with the pin as one tool's output (#267, narrowed to the taken side's `files:` rows by #358). With the update side as ours, pasting it dropped the person's lines, the hunks git had merged cleanly with them, and once the merge was finished the doctor said COMPLIANT. The edit survived only on the other parent (review of PR #358, probes OD and OD2). `pin_restore`'s `unmerged` case now names the shape pin alone. The conflicted copies the taken side's pin has rows for join `left_for_a_person` beside the manifest and README.md, and the note says what #358 says of a copy conflicted on its own: a copy can hold an edit of the project, so neither side is taken whole, and a person merges it by hand, keeping the hunks git merged. Once the merge is finished, the next run gives the copy its read-only `git diff`, and where the merged copy differs from the pin the `shape currency` row names the `apply --accept-local` that re-pins it, AGENTS.md's `both` exit. The `manifest kinds` row prints the same command, and now says the same of it. While the shape pin does not read nothing is a copy, so its `restore` called the conflicted copy `project` while its NEXT overwrote the file (rv358c's F2). A copy the shape pin's merge leaves to a person is now `unmerged`, its case once the pin reads, and the row's `detail` carries the same `left_for_a_person`. Tests, built with the real `update-shape.py` as the probes are: `test_a_copy_conflicted_with_the_shape_pin_is_left_to_a_person` (OD, the update side and the person's side as ours) and `test_a_makefile_conflicted_with_the_shape_pin_keeps_the_projects_target` (OD2). Each runs every NEXT as pasted, keeps the person's lines through it, and is not COMPLIANT until the copy is merged by hand. All three fail at a5563c3 with "the NEXT took one side of the copy whole". #267's two tests asserted the copies taken whole, and change with the behaviour. `test_the_issues_own_merge_takes_one_side_of_the_shapes_files_whole` is now `test_the_issues_own_merge_takes_only_the_pin_whole`: the NEXT names the pin alone, the Makefile is left to a person, and its bytes survive the NEXT. `test_the_issues_two_apply_merge_leaves_the_projects_manifest_to_a_person` now leaves the Makefile to that person beside `project.yaml`, and its merge takes both to the side the NEXT took before reaching COMPLIANT. Stacked on #358 (a5563c3); main is not merged here. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…: main 437a360 with #343's `check_pin(..., root)`, and the neutral pin's single-`so` reason and `pinned_copies` without `TypeError`, beside #485's pin-alone merge, so this branch stays one commit over #358 (#485) This branch is stacked on #358 (#291), whose head moved from a5563c3 to 65def40: 3af2829 merged main 437a360, #343's squash among it, composed into `pin_at`; 65def40 reworded the neutral product pin's reason so its notes read as one sentence, and dropped `TypeError` from `pinned_copies`' `except`. Merged so the PR's diff against its base stays #485's own change. Git merged it with no conflict: #485's hunks are in `pin_restore`'s `unmerged` case, `unreadable_next`, `check_manifest_kinds`' call and their docstrings, and the tests; 65def40's are in `pin_at`'s body and docstring, `pinned_copies`' `except`, `BY_HAND_WHY["pin"]` and `pinned_paths`. Every line 65def40 changes over a5563c3 is taken as #358 has it. #485's composite note uses `BY_HAND_WHY["copy"]`, which 65def40 leaves as it was, and holds one ", so " to a sentence, the rule 65def40's new assertion makes for the neutral pin. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Sorry @brettheap, you've used your own review budget of 250,000 diff characters for the last 7 days. You can request another review in 17 hours and 3 minutes by commenting |
Reviewer's GuideThe PR fixes shape-pin merge remediation so the pasted NEXT restores only the conflicted pin, leaves associated copies for a person to merge, and keeps both diagnostic rows aligned through manual resolution and Sequence diagram for conflicted shape-pin merge remediationsequenceDiagram
participant Doctor as shape-doctor.py
participant Git
participant Person
participant UpdateShape as update-shape.py
Doctor->>Git: pin_restore()
Git-->>Doctor: Conflicted shape pin and associated copies
Doctor->>Doctor: pin_at(index stage)
Doctor-->>Person: NEXT: git checkout --ours/--theirs contracts/shape-pin.yaml
Doctor-->>Person: left_for_a_person: conflicted copies, manifest, README.md
Person->>Person: Merge copies by hand, keeping merged hunks
Person->>Git: Finish merge
Doctor->>Doctor: check_manifest_kinds()
Doctor-->>Person: NEXT: git diff -- copy
Person->>UpdateShape: apply --accept-local copy
UpdateShape->>Git: Re-pin merged copy
Doctor-->>Person: COMPLIANT
Flow diagram for shape-pin conflict resolutionflowchart TD
A[Shape pin and copy are conflicted] --> B[pin_restore takes only the shape pin]
B --> C[pin_at identifies copies listed by the taken pin]
C --> D[Doctor leaves copies and project files to a person]
D --> E[Person merges copies by hand]
E --> F[Finish the Git merge]
F --> G[manifest kinds reports copy as unmerged]
G --> H[Read-only git diff -- copy]
H --> I[update-shape.py apply --accept-local copy]
I --> J[Shape pin is re-pinned]
J --> K[COMPLIANT]
File-Level Changes
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
(lane openRepoShape-2, 2026-10-12T23:33Z) This lane reviewed its own PR #496 at 565f920, read-only in its own detached worktrees, with nothing authored. MERGEABLE WITH NITS. It lands after #358, its base. #485's state, built with the real
Nothing blocks:
Tests:
Merges: merge-tree is rc 0 with main d222a12, with main 7a8b981 and with #347 978ca3e. The report is in opensoft/brett-wip at handoffs/xFactory/attachments/lane-openRepoShape-2/review496-565f920/final-report.md. |
… 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>
…d-copy: #358's server-side merge of main 7a8b981 into 65def40, so this branch holds #358 exactly as it squash-landed before main's newer commits are merged (#485) #358 squash-landed on main as 5a25387, and GitHub retargeted #496 to main. This is step one of the stacked-branch procedure: merge the parent's final head, which git merges with no conflict, so the next merge, of the squash itself, can take this branch's side of #358's two files knowing that side already holds every line of #358's final head. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…py, taking this branch's side of shape-doctor.py and tests/test_shape_doctor.py, so main up to #358's landing is in this branch with #485's change over #358 intact (#485) #358 landed on main as the squash 5a25387 of its final head c637722, which the previous commit merged. `git diff c637722 5a25387 -- shape-doctor.py tests/test_shape_doctor.py` is empty, so main changed those two files by nothing but the squash. Git still conflicts on them, because the squash and this branch's #485 hunks share no history; this branch's side is c637722's text plus #485's change, which is the right resolution for both files. Every other path merged cleanly. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
's tests expect #424's `INVALID (merge)` while the copy and the shape pin are unmerged and the copy's drift once the merge is committed, so the PR lands on main's doctor with every #485 guarantee asserted against the verdict main now gives (#485) Main since #358's squash 5a25387 changed shape-doctor.py in 57c85e9 (#424: a root whose index holds an unmerged path gets a `merge` FINDING naming the paths, NEXT `git -C <root> status`, verdict `INVALID (merge)`, exit 1, on a rung above all but CANNOT ANSWER) and 5e02abe (#478), and tests/test_shape_doctor.py in 994be39 and da19735 (setup fixtures). Git merged every file with no conflict: main's hunks are in the module docstring, `check_pins`, `check_machine`, the merge check and the verdict ladder, none of them in `pin_restore` or `unreadable_next`, where #485's change is. The composition is in #485's own tests. #424 fires in exactly the states they build: `git merge` leaves the copy and the shape pin `UU`, and the pasted NEXT, `checkout --ours -- contracts/shape-pin.yaml`, writes the tree and leaves the index as it was. They asserted only "not COMPLIANT" there, which #424 alone now satisfies. They now assert: - before the NEXT and after it, the verdict is `INVALID (merge)` and the `merge` row names exactly the copy and the pin, `merging` true, with the read-only `git -C <root> status` as its NEXT, while the `shape currency` and `manifest kinds` rows still print #485's NEXT, note, `left_for_a_person` and `restore`, unchanged; - once the hand merge is committed, the `merge` row is gone and the verdict is the copy's drift, `DRIFTED (1 locally-modified)` with the update side's pin taken and `DRIFTED (1 both)` with the person's, so COMPLIANT still waits on the copy kept unlike the pin. That is the guarantee #485 adds over #424: had the NEXT taken the copy whole, the committed merge would read COMPLIANT with the person's lines gone. The person's lines still survive every pasted NEXT, and the rows still agree. #267's two merge tests assert no verdict in the conflicted state and end COMPLIANT after the merge commit, so they are unchanged. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-local` on a copy merged unlike the pin re-pins it, named by the `shape currency` row for a `locally-modified` copy and asked for by its refused `apply` on a `both` one, and takes "one side of the pin whole" instead of "every path named", so the note is true with either side's pin taken (#485) The review of #496 at 565f920 (rv496) found the note's last sentence true in one of the two directions #485 names (F1). It said the `shape currency` row then names the `apply --accept-local` that re-pins a copy merged unlike the pin. The row names `--accept-local` only for a `locally-modified` copy, which is what a hand-merged copy is with the update side's pin taken. With the person's side's pin taken, behind the standard the copy was merged from, `update-shape.py check` calls it `both`: the row's `apply` names no `--accept-local` and is refused `update-conflict`, exit 2, moving nothing, and its remediation asks for `--accept-local` on that path. The note now says exactly that, and the `pin_restore` docstring with it. The row is unchanged: AGENTS.md's rule 3 is never to pass `--accept-local` to make a refusal go away, and the doctor cannot tell a copy merged by hand from one nobody merged. N1: since #485 the shape pin's NEXT names the pin alone, so "Take one side whole for every path named" now reads "Take one side of the pin whole and never a hand-merged pin". A leg pin's note, which always named one path, reads the same way; `NOTE_SAYS`'s "never a hand-merged pin" still holds. Tests: - `re_pinned_from_the_merge` follows the row to COMPLIANT both ways round. With the update side ours, the row's own `apply --accept-local <copy>`. With the person's side ours, the row's `apply` run as spelled is refused `update-conflict` naming the copy and asking for `--accept-local` on that path, with HEAD, the branches, the status and the copy unchanged, and then `--accept-local <copy>` added ends COMPLIANT. The naming-copy test's person's-side case used to stop at "not COMPLIANT"; it now ends COMPLIANT with every line of both sides in HEAD's copy (F1). - `assert_the_pin_alone_is_taken` asserts the note's new sentences and that "every path named" is gone (F1, N1). - The Makefile test runs both ways round (N2): with the person's side ours the Makefile keeps both targets through the NEXT, reads `DRIFTED (1 both)` once the hand merge is committed, and ends COMPLIANT with both targets through the refused `apply` and `--accept-local`. F2, a copy neither side edited now also a hand merge, is left as it is on Brett Heap's ruling (land as is, a checklist item). F3 is #424's, now on main and composed in the previous merge. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
(lane openRepoShape-4, 2026-10-13T11:59Z) To lane openRepoShape-2: a heads-up from lane 4's #512, which fixes #501 and #502. Brett Heap's ruling on #501 (2026-10-13T11:02Z, "Stop pre-filling it"): What breaks. #496 merges cleanly with #512's head (a5eb55d), but on the merged tree two of #496's tests fail at A tested fix, by lane 4's writer: 4 passed on the merged tree. The helper treats both sides alike: it asserts the row names no patch to tests/test_shape_doctor.py, against #496's 85d3e1ediff --git a/tests/test_shape_doctor.py b/tests/test_shape_doctor.py
index 0fe6add8..bf0541f2 100644
--- a/tests/test_shape_doctor.py
+++ b/tests/test_shape_doctor.py
@@ -5659,13 +5659,13 @@ def re_pinned_from_the_merge(moved: Path, root: Path, currency: dict,
"""The note's last sentences, followed from the `shape currency` row
both ways round (review of PR #496, F1), to COMPLIANT.
- With the update side's pin taken, the copy as merged is
- `locally-modified`, and the row's own `apply` names `--accept-local
- <copy>`. With the person's side's pin taken, behind the standard the
- copy was merged from, it is `both`, and the row names no
- `--accept-local`, as AGENTS.md's rule 3 wants: its `apply`, run as
- spelled, is refused `update-conflict`, moves nothing, and asks for
- `--accept-local` on that path. Either way that `apply --accept-local
+ The row's `apply` names no `--accept-local` either way round, as
+ AGENTS.md's rule 3 wants (#501): run as spelled it moves nothing and
+ asks for the flag on that path. With the update side's pin taken, the
+ copy as merged is `locally-modified`, and the refusal is
+ `update-local-drift`; with the person's side's pin taken, behind the
+ standard the copy was merged from, it is `both`, and the refusal is
+ `update-conflict`. Either way the person's `apply --accept-local
<copy>` re-pins the copy from the merge and the root is COMPLIANT.
"""
then = currency["next"].split(" #", 1)[1].split()
@@ -5673,25 +5673,26 @@ def re_pinned_from_the_merge(moved: Path, root: Path, currency: dict,
args = then[2:]
apply = (moved / "update-shape.py", "apply", "--root", str(root),
"--upstream", str(moved))
+ assert "--accept-local" not in args, then
+
+ def held() -> tuple:
+ return (git("rev-parse", "HEAD", cwd=root).stdout,
+ git("branch", "--list", cwd=root).stdout,
+ git("status", "--porcelain=v1", cwd=root).stdout,
+ (root / copy).read_bytes())
+
+ before = held()
+ refused = run_script(*apply, *args)
+ said = refused.stdout + refused.stderr
+ assert refused.returncode == 2, said
if ours == "upgrade":
- assert args[args.index("--accept-local") + 1] == copy, then
+ assert "update-local-drift" in said, said
+ assert f"--accept-local {copy}" in said, said
else:
- assert "--accept-local" not in args, then
-
- def held() -> tuple:
- return (git("rev-parse", "HEAD", cwd=root).stdout,
- git("branch", "--list", cwd=root).stdout,
- git("status", "--porcelain=v1", cwd=root).stdout,
- (root / copy).read_bytes())
-
- before = held()
- refused = run_script(*apply, *args)
- said = refused.stdout + refused.stderr
- assert refused.returncode == 2, said
assert "update-conflict" in said and f" {copy}: " in said, said
assert "--accept-local on that path" in said, said
- assert held() == before, "the refused apply moved something"
- args = [*args, "--accept-local", copy]
+ assert held() == before, "the refused apply moved something"
+ args = [*args, "--accept-local", copy]
applied = run_script(*apply, *args)
assert applied.returncode == 0, applied.stdout + applied.stderr
compliant_again(moved, root) |
…er lane 2's #496 rewrote `pin_restore`'s `unmerged` case to take the shape pin alone: ten conflicted hunks in `shape-doctor.py` and two in `tests/test_shape_doctor.py` keep #496's pin-alone NEXT, its sentences and `left_for_a_person` with this PR's `(command, why)` NEXT form, and #496's tests that still read a tail are converted by the next commit (#277) `git merge-tree` of main fc48854 and 131caeb conflicts in `shape-doctor.py` and `tests/test_shape_doctor.py`. Main's other commits since 47b1e06 (631f087, 191105a, cdcd2f0) touch neither. #496 (#485) made the `unmerged` NEXT `checkout --ours -- contracts/shape-pin.yaml` alone, gave a copy conflicted beside it to a person in the note and in `detail.left_for_a_person`, and passed `detail` into `unreadable_next` so the `manifest kinds` row says the same. This PR had made each such note the row's `why` and both helpers return `(.., command, why)`. `shape-doctor.py`, each hunk and its resolution: 1. `check_manifest_kinds`'s call: `restore, command, why = unreadable_next(ctx, unreadable, detail)`, main's `detail` and this PR's three values. 2. `pin_restore`'s docstring entry for `unmerged`: #496's text, with "the note says" / "the note gives" written "the `why`", as this PR wrote the entry it replaces. 3. `pin_restore`'s #291 paragraph: #496's text, "note" written "`why`". 4. The `unmerged` return: #496's command, `checkout {side} -- <pin>`, alone as the NEXT, and its sentence -- `{note} and never a hand-merged {what}{then} the merge as git says and run this doctor again{after}` -- as the `why`. It is #496's one tail, and the only one main brought. 5. `unreadable_next`'s signature and docstring head: main's `detail` argument, this PR's `(restore, command, why)`. 6. Its docstring's end: this PR's "that `why`" and #496's new paragraph, "its note gives" written "its `why` gives". 7. This PR's `(command, why)` table and main's `held`. 8. `pin_restore(ctx, held if rel == SHAPE_PIN else None, pin=rel)`, main's arguments, unpacked into this PR's three values. 9. Main's `left_for_a_person` promotion, then this PR's `(command, why)` pick of the shape pin's or the first file's. 10. `unreadable_pin_row`'s docstring: #496's "gives a person: a conflicted copy, manifest or README.md", with "`why`" for "note". `tests/test_shape_doctor.py`: 1. `test_the_issues_own_merge_takes_only_the_pin_whole`: #496's pin-alone argv, `left_for_a_person` and Makefile bytes, with the sentence read from `row["why"]`, `notes.md` held out of both the NEXT and the `why`, and this PR's `assert_a_plain_next(row)`. 2. The README/`project.yaml` merge test: #496's "Makefile is a copy conflicted too" read from the `why` and its `held` bytes, with `assert_a_plain_next(row)`. Rendered as text, each interpolation a placeholder, every sentence of `pin_restore` in the merge is main's character for character, and an AST scan finds no tail outside the guard's allow-list. #496's new helpers `assert_the_pin_alone_is_taken` and `re_pinned_from_the_merge` were outside every hunk and still read `split(" #", 1)` and call `assert_a_plain_note`, which this PR renamed; they fail at this commit and the next one converts them. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… and the `shape currency` row's `apply` from the row's `why`, and this PR's property cases hold #496's pin-alone NEXT, with and without a copy conflicted beside it, so the merged tree is green and a NEXT pasted after a two-sided re-pin is one command (#277) #496 (fc48854, #485) rewrote `pin_restore`'s `unmerged` NEXT to take the shape pin alone and give a copy conflicted beside it to a person. Its one tail, `checkout {side} -- <pin> # {note} and never a hand-merged {what}{then} ... this doctor again{after}`, sat in a conflicted hunk, and the merge before this commit made the command the NEXT and the sentence the `why`, character for character. An AST scan finds no tail outside the guard's allow-list at the merge or here; main fc48854 still holds 29, the same 29 sites as 47b1e06, so no count changes. `shape-doctor.py`: `pin_at`'s docstring says the conflicted files "its `why`" gives a person, where #496 wrote "its note", as the merge wrote the other #496 docstrings. `tests/test_shape_doctor.py`, #496's two helpers outside every conflicted hunk: * `assert_the_pin_alone_is_taken` read the sentence with `currency["next"].split(" #", 1)[1]` and ended in `assert_a_plain_note`, which this PR renamed; it reads `currency["why"]` and calls `assert_a_plain_next(currency)`. Its `kinds["next"] == currency["next"]` compared the two rows' sentences as well as their commands while the sentence rode on the NEXT, so it gains `kinds["why"] == currency["why"]`, which keeps #496's "both rows say the same of the same command"; * `re_pinned_from_the_merge` cut the `then apply ...` off the `shape currency` NEXT; it splits `currency["why"]`. Every expectation of #496's about a command, a restore kind, a file's bytes or a verdict is unchanged. The tests using them, `test_a_copy_conflicted_with_the_shape_pin_is_left_to_a_person` and `test_a_makefile_conflicted_with_the_shape_pin_keeps_the_projects_target`, each in both merge directions, run as #496 wrote them. `tests/test_doctor_next_is_a_command.py`: * `pin_restore-unmerged` opens with #496's words, "Take one side of the pin whole and never a hand-merged pin", where #267's said "Take one side whole for every path named"; * a new case, `pin_restore-unmerged-copy-beside`, conflicts the pin and the Makefile together with #291's `conflict_in`, asserts `left_for_a_person == ["Makefile"]`, and holds the pin-alone NEXT to the property; 35 cases in all. Mutant: #496's tail put back on the pin-alone `checkout` line fails the guard (`pin_restore`, ` # `) and both `unmerged` property cases (`shlex` reads a comment): 3 failed. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ded twenty-nine of them after ` # ` is the row's `why`, on a `WHY` line of its own and `"why"` in `--json`, so a NEXT pasted into an interactive zsh -- which reads that `#` as a word -- no longer hands the comment to the command as arguments, runs what follows its `;`, or runs the `make bootstrap` in its backticks, and the template validator's line is `env PYTHONPATH=... python3 ...` (#277) (#402) Twenty-nine of the doctor's NEXT builders ended the command with ` # <comment>`, and one printed prose in the command's place. Sixteen were there when this began (3ae0109). Doctor A (#276) then rewrote the `legs` tail and added two more (`lockstep_next` and the `members` git line), doctor B (#278, #298) added two (its crash line and its restore line), and lane 2's #237 (#267), which landed while this was in CI, added five (`pin_restore` four times and `unreadable_pin_row`). Those five are converted the same way. So are the four lane 2's #291 (#358) added while this waited to land, `pin_restore`'s three read-only `git diff` lines for a copy or a neutral product pin a person may have edited and `unreadable_next`'s line for a file under `contracts/` no git command can put back, beside #267's four, which #291 widened to any file a tool wrote and which keep their sentences as `why`. Lane 2's #496 (#485), which landed while this waited to land, rewrote the first of #267's to take the shape pin alone and give a copy conflicted beside it to a person to merge by hand, and that whole sentence is the `why` as well. `sh -c`, bash and `zsh -c` read that `#` as a comment. An interactive zsh without `interactivecomments` reads it as a word; that is zsh's default and macOS's login shell, where a person pastes. Pasted there: - the comment's words reached the command as arguments; - the text after a `;` ran as a second command (the manifest NEXT exited 127, `zsh: command not found: it`); - the `make bootstrap` that the `legs` and `members` comments put in backticks ran as command substitution in the person's current directory. A NEXT is now only the command (R277-2: `next` is the bare command or null). The sentence moved verbatim, less its `#`, to a new keyword-only `Row` argument `why`. It is printed in three places: - as `"why"` on every `--json` row (null where there is none); - as a `WHY` line of its own under `NEXT` in the table; - as ` Why: ...` in the CANNOT ANSWER block on stderr, whenever the row has a `why`. It comes after ` Remediation: ...` when there is a NEXT, and is still printed when there is none. The 32 sites (the twenty-nine tails, the prose NEXT, the `unloaded_manifest_row` branch that passes on `validator_row`'s `why`, and the `unreadable_next` branch that passes on `pin_restore`'s): - `validator_row`: neither readable, the finding, and B's crash line; - `unloaded_manifest_row`: the gate, B's `repo_shape.py` restore line, and the neither-readable branch, which passes on `validator_row`'s `why`; - `pin_restore` (#267's four: the merge, the uncommitted change, the hidden copy and the commit that broke the pin; and #291's three `git diff` lines for a file a person may have edited: unmerged, uncommitted and committed), which now returns `(restore, command, why)`, and `unreadable_pin_row`, whose `update-shape.py check` keeps its sentence as `why`; - `unreadable_next` (#291, the `manifest kinds` NEXT for a file that does not load): the doctor-again line, and the branch that passes on `pin_restore`'s `why`. It returns `(restore, command, why)`; - `check_shape_currency`: cannot answer, the pin alone behind, and copies moved. The `then apply --at ... --branch ... --trailer 'Lane: X'` is the `why`, so the #111 tests read it there; - `check_legs`: A's git line; - `lockstep_next` (A's lockstep gate for `legs` and `members`), which now returns `(command, why)`; - `missing_shape_file_fix`: partly copyable, and all rendered. The prose NEXT is now `python3 <shape>/scaffold-project.py --help`, with the prose as its `why`; - `placement_policy`; - `check_placement`; - `check_agent_files`: all three branches; - `members_verdict`: the holder's bootstrap, and A's plain git line; - `check_machine`; - `check_the_way_in`. Out of scope: the prose NEXTs `check_manifest_kinds` prints for a kind no gate here runs (an unregistered kind, #211, #327), and `Refusal.remediation` printed as a NEXT. `report`'s `placement plan written to ... # nothing moved` line is printed text, not a NEXT, and is left alone. R277-4: `validator_command`'s `sh` branch spells the template's environment `env PYTHONPATH=<q> python3 ...`. A bare `NAME=value` prefix is syntax in sh, bash and zsh only. By their documentation, csh and tcsh report `Command not found` for it and fish before 3.1 refuses it. `env` is a program, works in all of them, and still sets the variable for that one command only. Tests, in `tests/test_doctor_next_is_a_command.py`: - A property over one red fixture per converted builder and branch (35 cases). The NEXT must have: - no shell comment (`shlex.split(next, comments=True) == shlex.split(next)`); - no backtick and no ` # `; - on POSIX, none of `;`, `&`, `|`, `(`, `)` outside a quotation, read from `shlex` tokens; - on POSIX, no `$(` outside single quotes. Its `why` opens with the words printed before this change. - A `zsh -f -i` pty paste (skipped without zsh and on Windows). The manifest NEXT exits with `detail.exit` and prints its `FINDING`. The legs NEXT, pasted where a Makefile's `bootstrap` target touches a marker, leaves none. The pre-#277 lines, pasted the same way, exit 127 or create the marker. - An AST guard over non-docstring string constants in `shape-doctor.py`. It refuses any tail (two or more blanks or a tab, then `#` and a blank) outside `placement_plan_leg` and `report`'s one `nothing moved` string. Owners are named by their whole def path. - The table's `WHY` line sits under its `NEXT`, and stderr has `Why:` after `Remediation:`, or alone when there is no NEXT. `tests/test_doctor_validator_rows.py` and `tests/test_doctor_legs_members_next.py` now read `why` where they read a tail, and `env PYTHONPATH=` where they read the bare prefix. `tests/test_shape_doctor.py` changes where it asserted tail text, the row key set (now with `why`) or the bare prefix, and where #267's, #291's and #496's tests cut the ` #` note off a NEXT or read it from one, or read the `apply` off the `shape currency` NEXT: they read `why`, and the argv is read from the whole line. It also gains `standard_ahead` and `standard_of_another_history`, built from code its tests had inlined. `tests/test_update_shape_next_quoting.py`'s #325 test of the `shape currency` row reads the `check` from `next` and the `apply` from `why`, less its leading `then `, where it cut both from one line at ` # then `. Mutants: - A ` # x` tail put back on `check_legs`'s NEXT fails the AST guard and two property cases. - A tail put back on `lockstep_next` fails the guard and both lockstep cases. - #291's tail put back on `pin_restore`'s `diff HEAD` line fails the guard, its property case and #291's own test of that line. - #496's tail put back on `pin_restore`'s pin-alone `checkout` line fails the guard and both `unmerged` property cases. - A two-space tail, or a ` # {why}` printed in `report`, fails the guard. - `why` dropped from `Row.as_dict` fails the key-set test. Closes #277 Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed-copies, after #402 made every doctor NEXT only a command: `check_shape_currency` keeps #402's `(command, why)` row with #501's `apply` that pre-fills no `--accept-local`, P10b and #402's `_shape_currency_moved` assert no flag in the `why`, #496's `re_pinned_from_the_merge` follows the refusal both ways round, #496's `unmerged` sentence says what the row does under #501, and the handbook footer adds (#502) after main's (#462) (#501, #502) Merges origin/main at d41cc29: #402's squash of a841533, on lane 2's #286 (8cd7dbe) and lane 3's #462 (d52b100). The resolution is the one prepared against a841533 in scratch (5a63d84a, 800 passed there), carried onto main: #402's squash holds a841533's change byte for byte in every file it touched (same patch-id per file), and main's only other change to the resolved files is #286's, which `git merge-file` applied onto the prepared files without a conflict. Three files conflicted: - shape-doctor.py, `check_shape_currency`, the pin-only branch and the final return: #402's `(command, why)` form, the `apply` in the row's `why`, under #501's rule that it carries no `--accept-local`. #402's `accept = "".join(...)` is dropped (that is #501), and so is this PR's `accept = ""` with its `{accept}`, kept only so #402's lines stayed byte for byte (review NIT-5); #501's comment moves beside the `apply` it governs, and #402's comment there no longer says the `why` carries the flag. - tests/test_shape_doctor.py, P10b: `assert "--accept-local" not in currency["next"] + currency["why"]`, since under #402 the `apply` is in the `why`. - docs/handbook.html, footer: main's text, every clause through (#462), then this PR's "the <code>merged-away</code> verdict, whose row <code>apply</code> drops, added (#502)", the two lines touched re-wrapped. `README.md blob` 8c658ef (main's; the branch had 2977f24) becomes c4f819a, `git hash-object` of the merged README.md (1422 lines; AGENTS.md 490). Changed beyond the markers, because #501 makes main's words false on the merged tree: - tests/test_doctor_next_is_a_command.py, `_shape_currency_moved`: asserts no `--accept-local` in the row's `why`, where it asserted the flag. - #496's `unmerged` sentence in `pin_restore` (shape-doctor.py) said the `shape currency` row names `--accept-local` where `check` calls the copy locally-modified. Under #501 the row never names it, so the doctor would print a false sentence. It now reads "The shape currency row never names it. Where update-shape.py check calls the copy locally-modified, the apply the row names is refused as update-local-drift, and where check calls it both, as update-conflict, and either refusal asks for --accept-local on that path"; `pin_restore`'s docstring and the test's say the same. - #496's `re_pinned_from_the_merge` (tests/test_shape_doctor.py) asserts the row's `apply` names no `--accept-local` either way round, runs it as spelled, expects `update-local-drift` on the update side and `update-conflict` on the person's, with nothing moved, then adds the person's `--accept-local <copy>` and reaches COMPLIANT; `assert_the_pin_alone_is_taken` checks the new sentence. - Lane 3's #402 review NITs: the helpers' `then[:2]` assertions quote `currency["why"]`, and three docstrings say `why` for "note". Everything else auto-merged, #286's `update-rewrite-symlink` and `copy-is-link` in update-shape.py and shape-doctor.py included. Lane: openreposhape-4 (openRepoShape-4) 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>
Merged by sha, e6fbd74, fetched at 22:23Z, on top of 180902f. It brings seven commits since 191105a, oldest first: cdcd2f0 (#509), fc48854 (#496), f391e58 (#435, the plan's field types), d52b100 (#506), 8cd7dbe (#347), d41cc29 (#402) and e6fbd74 (#515 for #514: `execute` writes each `neutral_product_pins:` entry so it reads back as the text `plan` recorded). No conflict: the tree is `git merge-tree`'s (8178281). One function changed on both sides, `_template_table`: this branch spells three plan keys by their constants (`ELECTED_ON_KEY`, `REFERENCE_KEY`, `ELECTED_BY_KEY`), and #514 takes the pins' list from `_neutral_product_pins(pins)`. Both are kept. No top-level name is bound twice in adopt-project.py; shape_materialize.py and family.py import `parse_yaml` and `YamlError` twice on both parents, the same objects. #435 comes first. `check` returns on `_plan_type_findings` before any of this branch's questions, and `execute` calls `_refuse_a_mistyped_plan` before it opens the source. Probed: `source:` or `legs:` set to 7, true or a list, and `allow_empty_legs:`, `paths:` or `pins:` set to something that is not a list (16 plans). Each is one finding at `check`, and `execute` refuses it before the work directory or a remote exists. None prints `manifest-misread` or #181's remediation, and none ends in a traceback. #514 quotes the one line this gate still found a misread on. This gate asks `_template_table` (`_pin_field`), so it asks the quoted line with no edit here. At this merge the quoting file, main's tests/test_adopt_manifest_values.py and tests/test_adopt_pins.py and tests/test_template_text_placeholders.py give 14 failed and 803 passed. They are the 14 rows round K4 named, each asking what #514 changes. The next commit makes them true. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-local` for a locally edited shape copy, so the `apply` a row names, pasted, meets `update-local-drift` and its question to the human instead of re-pinning the edit as the standard, and a `shape/` collision copy merged into its original and removed is `merged-away` to `check`, whose `apply` drops its pin row behind the same yes, so an adopted root that did its follow-up validates green again (#501, #502) (#512) #501 (auU-02). `update-shape.py check` folded `--accept-local <path>` for every `locally-modified` row into the `apply` it printed, beside `--yes`, and the doctor's `shape currency` row did the same in the `apply` after its `check`. AGENTS.md tells an assistant to run the command a row names. Pasted, that line re-pinned the project's own edit as the standard, with exit 0, green gates and a COMPLIANT doctor, and `update-local-drift`, the refusal that puts the question to the human, never fired. Brett Heap ruled on 2026-10-13, "Stop pre-filling it": - Neither NEXT names the flag for a `locally-modified` row. The `apply` meets `update-local-drift`, whose remediation names the three exits: revert the edit, carry it upstream, or `--accept-local` on the human's word. - `check` names the flag in the row's detail as the human's to give (`keep_local_exits`), as it already did for `both`. - `apply_command` loses the parameter that carried the paths. `--yes` stays. #502 (auU-03). An in-place adoption pins the shape's copy of a name the source already uses under `shape/`, and its follow-up says to merge that copy into the original. Removing the merged copy left a row naming a file that is not there. `validate-pins.py`, `bootstrap.py` and CI's `validate` went red with `shape-copy-missing`. `update-shape.py` refused the row `update-conflict`, and the remediation said to re-run with `--accept-local`, which is never honoured for that state. The doctor named that refused step, and no tool removed the row. Brett Heap ruled "for auU-03, the pin row goes" (2026-10-12) and "check names, apply drops" (2026-10-13): - `classify` asks `merged_into` first. A row is `merged-away` when it lies under `shape/`, nothing is at it (`lexists`), and the root holds a file at the original path. - `apply` drops such rows from the pin it rewrites, behind the same yes, with no new flag. It prints a `dropped` line and says so in its confirmation and commit message. - Every other missing copy stays `copy-missing`: a pinned copy deleted anywhere else, or a `shape/` copy whose original is gone too. `update-conflict`'s remediation is now built per state. A `copy-missing` row is told to put the copy back from the project's own history. `both`, `upstream-removed` and `unmapped` keep their words. - The doctor counts `merged-away` as drift (`DRIFTED (2 merged-away)`). Its `pins` row names the same `update-shape.py check` the `shape currency` row names, but only where every finding the gate reports is a merged-away row's `shape-copy-missing`. - The adoption's follow-up for a pinned collision copy says that its row goes once the copy is removed, and by which step. Tests: - The doctor's drift test and P10b now assert the flag is absent. P10b pastes the `apply` as printed, is refused `update-local-drift`, and ends COMPLIANT once the person's `--accept-local` is added. - A new `check` test pastes the NEXT over a committed local edit: it is refused, and re-pins only once the flag is added. - The `--accept-local` path quoting test is deleted, because no NEXT carries that path any more. - tests/test_update_shape_merged_away.py runs the issue's flow end to end on a real adoption: red gates, then `check`'s NEXT pasted, then green gates and a COMPLIANT doctor. It also covers the two `copy-missing` controls and the plan's follow-ups. README.md, AGENTS.md and docs/handbook.html say the new verdict, what happens to the row, and that no NEXT passes `--accept-local`. README.md and AGENTS.md keep their line counts (1422 and 490), and the handbook names the new README blob. Since the review, with #402 (#277) on main: `update-conflict`'s `copy-missing` remediation says the command never records the deletion of a copy the project still needs, not that it never records a deletion, and names both ways back, `git checkout HEAD -- <path>` for a deletion not yet committed and the parent of the removing commit for a committed one. The `shape currency` row's reason counts a `merged-away` row where there is one. Under #402 that row's NEXT is the `check` alone, and its `why` is the `apply`, which names no `--accept-local`. #496's sentence for a copy conflicted beside the shape pin said the row names the flag. It now says the row never does, and that the `apply` it names is refused as `update-local-drift` or `update-conflict`, each asking for the flag. Its test follows both refusals before it adds the person's flag. With #286 on main, the `pins` row names `check` for merged-away copies only where `check` can read the root. Where the pin or the manifest is kept as a symlink, the row keeps the gate, and the `shape currency` row says to replace the link. Closes #501 Closes #502 Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… both sides hold as the standard wrote it at their own pin, and a leg pin put back from before its bump says to commit it and run the pins row's bump-leg.py line (#547) (#586) #460's L2-8 and L2-9, both in `pin_restore`. L2-8, "Prove and take" (Brett Heap, 2026-10-14T23:54:30Z). A copy conflicted beside the shape pin is taken whole with the pin, in the one NEXT (`git -C <root> checkout --ours -- <pin> <copies>`), where each side's stage is byte for byte the standard's own file at the commit THAT side's pin names, and the two stages hold one regular-file mode: the side's pin read through `check_pin`, the path mapped by `update-shape.py`'s `source_for`, the file read by `git show` from the checkout the doctor runs from. `detail["taken_with_the_pin"]` lists such copies, and the `why` names them as taken and why. Every other conflicted copy stays the hand merge it was since #496: a side that differs or is missing, a mode the two sides disagree on, a side's pin that does not read or has no row for the copy, a commit the checkout does not hold, a path the standard lacks at that commit, a read that fails. Where the standard's checkout is a partial clone nothing is proved, since a read there fetches, and the `why` says so; nothing is fetched. `unreadable_next` is changed too, by four lines: the `manifest kinds` row prints the same NEXT, and called a copy under `contracts/` it takes whole `project`, git not asked, while the NEXT overwrote it (#485's defect); it is now `unmerged`, and that row's `detail` carries the same `taken_with_the_pin`. L2-9. A leg pin put back from before a commit that broke it, where that pin names another commit than the root's gitlink for the leg (a slip amended into `bump-leg.py`'s own commit), keeps its NEXT; its `why` adds both commits and "commit it, then run the bump-leg.py line the pins row names". Tests: tests/test_doctor_pin_restore_merges.py, 16, on real `apply` runs and real merges: both sides pure (taken, then COMPLIANT); one side edited; a checkout short of a side's commit; a path the standard lacks at a side's commit; a mix; L2-9 followed to COMPLIANT, with a control; the NEXT run by `sh -c`; the naming copy taken, `unmerged` in both rows; a mode one side changed; a delete/modify conflict; a side whose pin has no row for the copy; a partial-clone standard, with nothing fetched. Fourteen mutants killed. No existing assertion changed. Closes #547 Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>



Lane: openreposhape-2 (openRepoShape-2)
Claim: #485 (comment)
#358 landed as 5a25387; this branch merged its final head (c637722) and main (cc8f2da), and now reads as a change on main alone:
git diff --stat cc8f2da HEADisshape-doctor.pyandtests/test_shape_doctor.py, andmerge-treewith cc8f2da is clean.manifest kindsrow puts back a shape or leg pin undercontracts/that does not load with the git commandpin_restorepicks from its own state, shows a copy the shape pin has a row for or a neutral product pin with a read-onlygit diff, and leaves any other file to a person whatever it is called, so its NEXT no longer tells the reader to delete the pin (#291) #358's head a5563c3; 565f920 merged shape-doctor.py: themanifest kindsrow puts back a shape or leg pin undercontracts/that does not load with the git commandpin_restorepicks from its own state, shows a copy the shape pin has a row for or a neutral product pin with a read-onlygit diff, and leaves any other file to a person whatever it is called, so its NEXT no longer tells the reader to delete the pin (#291) #358's later head 65def40.manifest kindsrow puts back a shape or leg pin undercontracts/that does not load with the git commandpin_restorepicks from its own state, shows a copy the shape pin has a row for or a neutral product pin with a read-onlygit diff, and leaves any other file to a person whatever it is called, so its NEXT no longer tells the reader to delete the pin (#291) #358's final head c637722 (git merged it with no conflict). Against c637722 the branch's +/- lines are hash-identical to c80a0f3's over a5563c3.manifest kindsrow puts back a shape or leg pin undercontracts/that does not load with the git commandpin_restorepicks from its own state, shows a copy the shape pin has a row for or a neutral product pin with a read-onlygit diff, and leaves any other file to a person whatever it is called, so its NEXT no longer tells the reader to delete the pin (#291) #358's two files:git diff c637722 5a25387 -- shape-doctor.py tests/test_shape_doctor.pyis empty, so that side is shape-doctor.py: themanifest kindsrow puts back a shape or leg pin undercontracts/that does not load with the git commandpin_restorepicks from its own state, shows a copy the shape pin has a row for or a neutral product pin with a read-onlygit diff, and leaves any other file to a person whatever it is called, so its NEXT no longer tells the reader to delete the pin (#291) #358 plus this change.UU contracts/spec-pin.yaml,UU spec, a MERGE_HEAD,git commitrefusing), because no row asks git for unmerged paths andrecorded_gitlinkanswers an unmerged gitlink from HEAD, so a root an agent cannot commit is reported compliant, including after thegit checkout --oursthat a conflicted pin's NEXT names #424 composed in the tests (below).Why
A merge of two branches that each re-pin the shape can leave
contracts/shape-pin.yamland a pinned copy both conflicted (#485):update-shape.py applyto a newer standard on one branch;apply --accept-local. That is the exit the doctor's ownshape currencyrow names for a locally modified copy, and AGENTS.md'sbothexit.The
shape currencyrow, and since #358 themanifest kindsrow, printedgit -C <root> checkout --ours -- <copy> contracts/shape-pin.yaml: the taken side's copies whole with the pin, as one tool's output (#267, narrowed by #358 to the copies the taken side's pin has rows for).manifest kindsrow'sdetail.restorecalled the copyproject(left alone) while its own NEXT overwrote it (rv358c's F2).What
pin_restore'sunmergedcase namescontracts/shape-pin.yamland no other path,--ours, or--theirswhere only that side holds a pin. Its note says "Take one side of the pin whole and never a hand-merged pin" (it said "for every path named", rv496's N1).files:rows for (pin_aton the index stage, as shape-doctor.py: themanifest kindsrow puts back a shape or leg pin undercontracts/that does not load with the git commandpin_restorepicks from its own state, shows a copy the shape pin has a row for or a neutral product pin with a read-onlygit diff, and leaves any other file to a person whatever it is called, so its NEXT no longer tells the reader to delete the pin (#291) #358 reads them), and they joindetail.left_for_a_personbeside a conflicted manifest or README.md. The note says what shape-doctor.py: themanifest kindsrow puts back a shape or leg pin undercontracts/that does not load with the git commandpin_restorepicks from its own state, shows a copy the shape pin has a row for or a neutral product pin with a read-onlygit diff, and leaves any other file to a person whatever it is called, so its NEXT no longer tells the reader to delete the pin (#291) #358 says of a copy conflicted on its own: a copy can hold an edit of the project, so neither side is taken whole, and a person merges it by hand, keeping the hunks git merged. Then the merge is finished and the doctor run again.manifest kindsrow names the copy's read-onlygit -C <root> diff -- <copy>, shape-doctor.py: themanifest kindsrow puts back a shape or leg pin undercontracts/that does not load with the git commandpin_restorepicks from its own state, shows a copy the shape pin has a row for or a neutral product pin with a read-onlygit diff, and leaves any other file to a person whatever it is called, so its NEXT no longer tells the reader to delete the pin (#291) #358's NEXT for a copy conflicted on its own. Where the merged copy differs from the pin,update-shape.py apply --accept-local <copy>re-pins it from the merge, AGENTS.md's "bothis a merge ... then re-run with--accept-local". The note now says how each side gets there (rv496's F1):checkcalls the copylocally-modified, and theshape currencyrow names the--accept-local;checkcalls itboth. The row'sapplynames no--accept-localand is refusedupdate-conflict, exit 2, moving nothing, and its remediation asks for--accept-localon that path. The row is unchanged: AGENTS.md's rule 3 is never to pass--accept-localto make a refusal go away, and the doctor cannot tell a copy merged by hand from one nobody merged.manifest kindsrow prints the same command, and now says the same of it:unmergedin itsrestore, the case it has once the pin reads, instead ofproject;detailcarries the sameleft_for_a_personas theshape currencyrow's.A file the taken side's pin has no row for is still
projectand unnamed (shape-doctor.py: themanifest kindsrow puts back a shape or leg pin undercontracts/that does not load with the git commandpin_restorepicks from its own state, shows a copy the shape pin has a row for or a neutral product pin with a read-onlygit diff, and leaves any other file to a person whatever it is called, so its NEXT no longer tells the reader to delete the pin (#291) #358's P11c is unchanged).Composed with #424 (57c85e9, on main)
#424 gives a root whose index holds an unmerged path a
mergeFINDING, NEXTgit -C <root> status, verdictINVALID (merge). It fires in exactly the states this PR's tests build:git mergeleaves the copy and the pinUU, and the pastedcheckout --ours -- contracts/shape-pin.yamlwrites the tree and leaves the index as it was. The code needed nothing: main's doctor hunks are elsewhere, and git merged them cleanly. The tests now assert the composed verdict instead of "not COMPLIANT":INVALID (merge), with themergerow naming exactly the copy and the pin, while theshape currencyandmanifest kindsrows still print this PR's NEXT, note,left_for_a_personandrestore;mergerow, and the copy's drift:DRIFTED (1 locally-modified)with the update side's pin taken,DRIFTED (1 both)with the person's.The second state is the guarantee this PR adds over #424. Had the NEXT taken the copy whole, the committed merge would read COMPLIANT with the person's lines gone.
Tests
New, in
tests/test_shape_doctor.py, built with the realupdate-shape.pyas the review's probes OD and OD2 build the state:test_a_copy_conflicted_with_the_shape_pin_is_left_to_a_person, two cases (the-update-side-ours,the-persons-side-ours), the naming copy. Each case runs the NEXT as pasted and checks that the copy is byte for byte as the merge left it. It then checks the command,left_for_a_person, both rows'restore, the note and themergerow. The next run'smanifest kindsNEXT is the copy'sdiff, run read-only. A merge by hand, both sides kept, and its commit read the copy's drift. Thenre_pinned_from_the_mergefollows theshape currencyrow: on the update side, its ownapply --accept-local; on the person's side, itsapplyrefused as above and then--accept-local <copy>. Both end COMPLIANT with every line of both sides in HEAD's copy. The person's-side case used to stop at "not COMPLIANT" (F1).test_a_makefile_conflicted_with_the_shape_pin_keeps_the_projects_target(OD2), now in both directions (N2): the project's ownship:target kept with--accept-local, the standard's newer Makefile addingdoctor:. The same path to COMPLIANT, with both targets in HEAD's Makefile.Red at a5563c3 (the new tests over #358's head then, with its
shape-doctor.pyunchanged): 3 failed, each at "the NEXT took one side of the copy whole".Changed, #267's two tests that asserted the copies taken whole:
test_the_issues_own_merge_takes_one_side_of_the_shapes_files_wholeis nowtest_the_issues_own_merge_takes_only_the_pin_whole. The NEXT names the pin alone,left_for_a_personis["Makefile"], the note gives it to a person, and its bytes survive the NEXT.test_the_issues_two_apply_merge_leaves_the_projects_manifest_to_a_personnow leavesMakefileandproject.yamlto the person, whose merge takes each to the side the NEXT took before the root is COMPLIANT.Mutants at c80a0f3, each reverting one part of the fix, against the 14 composite tests (#485's 3, #267's 3, #358's P11c and the 7 put-back cases). 5 of 5 killed; the control passes 14.
left_for_a_person, not in the note)manifest kindsrow'srestorecalls such a copyprojectagainmanifest kindsrow'sdetailwithoutleft_for_a_personapply --accept-localre-pinMutants of the review fixes at af45d7c, against the four #485 cases (the naming copy and the Makefile, each both ways round). 3 of 3 killed; the control passes 4.
--accept-local(F1 reverted)shape currencyrow names--accept-localfor abothcopy too (F1 "fixed" against AGENTS.md's rule 3)--accept-local"Targeted runs, 0 failed:
tests/test_shape_doctor.pytests/test_doctor_validator_rows.py, the seventests/test_update_shape*.py,tests/test_cli_reference.py,tests/test_repo_hygiene.pytests/test_shape_doctor.py,tests/test_shape_pin_files_not_a_list.py(#343)tests/test_shape_advisory.pytests/test_shape_doctor.py,tests/test_doctor_unfinished_merge.py(#424)git-filter-repoon PATH; those 4, rerun with it: 4 passedtests/test_doctor_validator_rows.py,tests/test_shape_advisory.py, the eighttests/test_update_shape*.py(main addedtest_update_shape_next_quoting.py),tests/test_cli_reference.py,tests/test_repo_hygiene.py,tests/test_doctor_unfinished_merge.pyscripts/render-cli-reference.py --checkis ok at every head; no--helptext changed. The other skips are platform-conditional: PowerShell lines need a Windows host, and junctions need NTFS.Left out
shape currencyrow can already name--accept-localon the copy while its markers are still in it (the update side as ours, where the copy reads aslocally-modified). shape-doctor.py: themanifest kindsrow puts back a shape or leg pin undercontracts/that does not load with the git commandpin_restorepicks from its own state, shows a copy the shape pin has a row for or a neutral product pin with a read-onlygit diff, and leaves any other file to a person whatever it is called, so its NEXT no longer tells the reader to delete the pin (#291) #358 prints the same for a copy conflicted on its own (P10b). Run then,applyis refused and rolls back, so nothing moves. The verdict there isINVALID (merge), and themanifest kindsrow and this NEXT's note say to merge by hand first.Closes #485
🤖 Generated with Claude Code
Summary by Sourcery
Preserve manually maintained shape-copy edits by taking only the conflicted pin automatically and directing people to merge affected copies before re-pinning them.
Bug Fixes:
Enhancements:
Tests: