Skip to content

shape_materialize.py, adopt-project.py and bump-leg.py: a tag a clone holds without its commit is one it cannot answer for, check says a commit it cannot ask was not asked, and a pinned product YAML 1.1 or 1.2 reads as no text is quoted (#912) - #1090

Merged
brettheap merged 2 commits into
mainfrom
fix/912-pin-questions
Oct 21, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Lane: openreposhape-5 (openRepoShape-5)
Claim: #912 (comment)

Goal

Each pin question is answered from the commit actually pinned, with words that say why, and a pinned product is written so that any YAML reader reads it back as text. This is #912, which groups three lines of #457's release-commit triage: L1-81, L1-82's second case, and L2-14.

One commit, on main 5e539a87. It changes scripts/shape_materialize.py, adopt-project.py and scripts/bump-leg.py, and six test files.

What still held on main, and what had moved

The issue's line numbers are at 5017830. Each claim was measured again by name, on main 6c861635 first and again on 5e539a87:

What changed

L1-81: a tag whose commit the clone lacks is a clone that cannot answer

  • commit_in_clone returns (oid, "") where a tag's peel cannot be read (names is None), as it already does for an id the clone does not hold. plan and the scaffold then read the digest there. git ls-tree finds no tree at the id, so they refuse it pin-source-unreadable, whose remediation says to point --pin-source at a clone that HAS the pinned commit, "git fetch --all first". That is what main said before adopt-project.py and scaffold-project.py: a --pin naming an annotated tag's object id is written as the pin's commit:, under the pin file's "A COMMIT, NEVER A TAG" #651. check says plan-pin-source-unreadable.
  • peeled_target returns the id as typed there. The reachability verdict answers it, as it answers an id the leg does not hold: bump-leg-commit-not-on-remote, " has no branch containing ", from both runs.
  • A tag of a tree or a blob, which the clone can peel, is still pin-not-a-commit or bump-leg-target-not-a-commit by name.

L1-82: check says it did not ask a recorded commit it could not ask

L2-14: a product YAML 1.1 or 1.2 reads as no text is double-quoted

  • New shape_materialize.YAML_NOT_TEXT_RE. Of the plain scalars PLAIN_SCALAR_RE takes, which begin with a letter, _ or ., it matches the ones YAML 1.1 or YAML 1.2's core schema resolves to a bool, a null or a float:
    • YAML 1.1's bools y, n, yes, no, on and off, and both versions' true and false, in their three cases;
    • null in its three cases;
    • .inf and .nan in their three cases;
    • a point before digits, as YAML 1.1's pattern is written (\.[0-9.]* with a signed exponent, so ., .. and .5.5 too);
    • and as its own example 685.230_15e+03 and its readers read it, with _ among the digits (PyYAML takes .5_ for 0.5, ruamel.yaml takes ._ for a float), with YAML 1.2's unsigned exponent .5e3.
  • Nothing else either version resolves to no text is a plain scalar PLAIN_SCALAR_RE takes: ~, the empty null, <<, =, and ints, timestamps and floats that begin with a digit or a sign.
  • neutral_pin_entry writes a name bare only where it is a plain scalar, the new rule does not match it, and the project's reader reads it back. So both tools double-quote these names. Every other name is the line it was, openGlass byte for byte.
  • The plan's product:, which y writes and only this tool reads, is unchanged. The pin file's product: was always double-quoted.

Probes, before and after

The probe is probe912.py (not committed). It runs with a stub gh that exits 97 first on PATH, a bogus token, an empty GH_CONFIG_DIR, GIT_CONFIG_GLOBAL=/dev/null and GIT_SSH_COMMAND=false. The lacking clone holds the tags v1 and v1-again but not their commit.

Case main 6c861635 and 5e539a87 This head
commit_in_clone(lacking, <tag>) (<tag>, 'is an annotated tag that names no commit') (<tag>, '')
peeled_target(lacking, <tag>) REFUSED bump-leg-target-not-a-commit: --to <tag> is an annotated tag that names no commit in the leg returns <tag>
scaffold --dry-run and plan, --pin openGlass@<tag> --pin-source lacking exit 2 REFUSED pin-not-a-commit: ... is an annotated tag that names no commit in the clone at <lacking>, not a commit exit 2 REFUSED pin-source-unreadable: <lacking> could not answer for opensoft/openGlass @ <tag>: ... exited 128: fatal: not a tree object, remediation "git fetch --all first"
check, recorded clone lacking exit 1 FINDING plan-pin-not-a-commit: ... which is an annotated tag that names no commit exit 1 FINDING plan-pin-source-unreadable: ... cannot answer for it
check, a tag's id, clone whole (control) exit 1 plan-pin-not-a-commit, "an annotated tag of commit " the same
check, a tag's id, clone partial / gone / gh api / none named exit 0, NOTE names only the digest exit 0, the NOTE above; gh never ran
names --pin admits of up to three characters (278915) 231714 written bare; PyYAML misreads 132 of them, ruamel.yaml 0.19 (YAML 1.2) 132 231543 bare; 0 misread by either

#457 counted 138 at 3dd806f. On 6c861635, PyYAML misread yes, no, on and off in three cases each, .0 to .99, and .0_ to .9_. ruamel.yaml misread the dot forms, ._, ._0 to ._9 and .__, and its YAML 1.1 mode also reads y and n as bools.

Found while testing. git 2.43's fetch follows a remote tag whose object is already in the store, and brings the tag's commit with it. Its tag-following check counts the tag object itself, not only the commit. So in bump-leg.py a tag that the leg holds without its commit is a tag the leg's remote does not advertise, or one met after a fetch that failed. The bump row uses a tag that was never pushed.

Rows

New rows, in each file's style and with its helpers:

  • tests/test_pin_annotated_tag.py:
    • a lacking fixture (git hash-object -t tag -w of the tags' bytes);
    • commit_in_clone answers (id, "") (2 rows);
    • plan and the scaffold's --dry-run refuse pin-source-unreadable with no "annotated tag" in the output (4);
    • check finds plan-pin-source-unreadable and execute refuses before any leg (1);
    • the NOTE for a tag's id, clone gone, partial or gh api, with gh never run (3).
  • tests/test_bump_leg_dry_run_truth.py: a tag the leg holds without its commit is bump-leg-commit-not-on-remote in both runs, with nothing written, through --local-remote-dir and origin (2).
  • tests/test_neutral_pin_entry_one_spelling.py:
    • each of 25 YAML 1.1 and 1.2 words and 20 dot floats is double-quoted and reads back (45);
    • the same 45, with the project's reader stubbed to read everything back, are double-quoted by the rule alone (45);
    • 20 look-alikes (openGlass, openYes, yEs, .e5, ._. ...) stay bare (20);
    • the whole enumeration, read by PyYAML, ruamel.yaml 1.2 and ruamel.yaml 1.1 where they are installed (1; skipped without either reader, so CI holds the rule by the rows above).
  • Changed rows, whose old expectation was the bug:
    • SPELLED["yes"], and the entry-equals-adopt's-old-spelling row, which now expects exactly ., .., .0, .9, ._ and yes to differ;
    • .5, .inf, yes and off move out of the bare groups in test_scaffold_pin_and_reuse.py and test_adopt_pins.py into STOCK_MISREAD;
    • the NOT-rechecked constants in test_adopt_pins.py, and one line in test_pin_source_top_level.py.

No assert joins conditions with and or or.

The rows fail on main. The six test files were run on main's code, the scripts as on main with the new tests beside them:

  • on 6c861635: 96 failed, 98 passed, and 16 failed, 232 passed;
  • on 5e539a87: 112 failed, 330 passed.

Every failure is a new or changed row:

  • spelling 85;
  • tag file 10;
  • top-level partial-clone NOTE 1;
  • test_adopt_pins.py 10;
  • bump 2;
  • scaffold entry 4.

Mutants

Each mutant was applied alone to a scratch clone of the head and its rows were run. The results were the same on 6c861635 and on the rebased head.

Mutant Result
M1 commit_in_clone without the names is None return killed, 3 failed
M2 peeled_target without it killed, 2 failed
M3 if not peeled in place of if names is None survives: equivalent. The two differ only where rev-parse <id>^{} prints an id and cat-file -t of that id fails, and rev-parse parsed that object to print it.
M4 the NOTE's old words killed, 3 failed
M5 neutral_pin_entry without the rule killed, 85 failed
M6 rule without y/n killed, 9
M7 without yes/no/on/off killed, 28
M8 without true/false/null killed, 9 (the reader-stubbed rows)
M9 without .inf/.nan killed, 12
M10 without YAML 1.1's pattern as written killed, 9
M11 without the _-digits pattern killed, 14
M12 that pattern's exponent signed only killed, 4
M13 no end anchor killed, 15

Runs

All runs go through lane 5's run9.sh: a stub gh first on PATH, a bogus token, every LANES_* variable cleared, EMAIL=service@opensoft.dev, GIT_SSH_COMMAND=false, GIT_CONFIG_GLOBAL=/dev/null, and one basetemp per run.

  • The six test files on the head d5389eb6a71e:
    • git 2.43: 442 passed;
    • git 2.47.1 (first on PATH): 442 passed.
    • They also passed on 6c861635 before the rebase: 442 on 2.47.1, and on 2.43 in groups of 115, 31, 24, 149 and 123.
  • git 2.43, every other test file that runs a changed script. These are the files that name adopt-project, ADOPT, shape_materialize, bump-leg or BUMP, plus test_shape_doctor.py, test_scaffold_leg_path_collisions.py and the trio (test_repo_hygiene.py, test_agents_md_codes.py, test_cli_reference.py), less the six. That is 177 test files in two shards, 179 paths with tests/conftest.py and tests/gh_guard.py:
    • shard a, 88 files: 6371 passed, 33 skipped (END 2026-10-20T23:52:07Z rc=0);
    • shard b, 91 files: 6296 passed, 3 skipped (END 2026-10-20T23:51:58Z rc=0).

Merge-trees

git merge-tree --write-tree of the head against origin/main and against each open PR that touches a file this changes. Each merge is clean, and no PR's added lines quote the changed words or name the changed functions:

Against Head Result
origin/main 5e539a878fc9 clean, tree 95f9b53262fc (the head's own tree)
#1073 l3-914-adopt-record-and-doctor b58a4e2ccae0 clean, 70f85d401ede
#1072 l3-918-source-and-locale 2ffd82356c7b clean, 2e341d250ea1
#1071 l3-916-adopt-test-strength 24273dd47e42 clean, 0d183517f958
#1070 fix/979-named-link-one-finding 20d2d4ba67f6 clean, a5beb7be1408
#1031 fix/910-check-throwaway-repos 74c17705edc2 clean, 324855e1983d
#1081 fix/1060-pin-lane-in-process 7d4f2d82df4b clean, fc9fe407b03b (it touches none of these files)

Main moved after the runs above, which were made on 5e539a87: #1071 (bccae280), #1076 (c13c3985), #1081 (06cbd478) and #1072 (1e7971cd) have landed. #1071 and #1072 touch two of this change's files, tests/test_adopt_pins.py and adopt-project.py. The head merges clean with origin/main 1e7971cd (tree ffb8249e, from git merge-tree --write-tree), and the landing gates the predicted squash tree over the main of the day.

#1068 and #1064 have landed and are under this commit. fix/904-kept-links was not on origin when this was pushed; it is now #1091, open at 8f5fd0fd, and merges with this head clean (tree 0153f3e7e3c9, measured by rv1090). #1062 touches none of these files.

Review

Closes #912

🤖 Generated with Claude Code

Summary by Sourcery

Make pin validation accurately distinguish unavailable tag data and preserve pinned product names across standard YAML readers.

New Features:

  • Ensure annotated tags whose target commits are unavailable are treated as unreadable pin sources rather than as non-commits.
  • Quote pinned product names that YAML 1.1 or 1.2 would interpret as booleans, nulls, or floats.
  • Report when check cannot ask a recorded commit what it names, alongside the existing digest recheck note.

Bug Fixes:

  • Correct pin and bump diagnostics for tags held without their referenced commits.
  • Prevent standard YAML readers from changing pinned product names when reading manifests.

Tests:

  • Add coverage for unavailable annotated-tag targets, unverifiable recorded pins, YAML-sensitive names, and bump behavior.

… holds without its commit is one it cannot answer for, `check` says a commit it cannot ask was not asked, and a pinned product YAML 1.1 or 1.2 reads as no text is quoted (#912)

Three of #457's lines (L1-81, L1-82's second case, L2-14), measured on main
6c86163:

- A tag the `--pin-source` clone holds without the commit it names cannot
  be peeled there (git: "missing object referenced by" the tag), and
  `commit_in_clone` refused it `pin-not-a-commit`, "an annotated tag that
  names no commit", in `plan`, the scaffold and `check`; `bump-leg.py`'s
  `peeled_target` refused one in the leg the same way. Nothing in that
  clone says what the tag names, so it is now `(oid, "")`: the digest's
  reader finds no tree there and says `pin-source-unreadable`, "`git fetch
  --all` first", as before #651, `check` says `plan-pin-source-unreadable`,
  and `bump-leg.py` gives the id to its reachability verdict,
  `bump-leg-commit-not-on-remote`.
- Where the clone `plan` read a digest from is gone or partial, or `gh
  api` read it, `check` asks the pin nothing, so a recorded tag's id
  passed with a NOTE that named only the digest. `check` and `execute`
  read no network for a pin (#273's review, #449), and a partial clone's
  git would fetch an id it lacks, so the forge is not asked: the NOTE now
  says the commit was not asked either, and that `plan` asks it.
- `neutral_pin_entry` wrote `yes`, `no`, `on`, `off`, `.5`, `.inf` and
  `.nan` bare, which the project's reader reads back and PyYAML reads as
  True, False, 0.5, inf and nan: of the names `--pin` admits up to three
  characters, PyYAML and ruamel.yaml each misread 132 written bare. A name
  YAML 1.1 or YAML 1.2's core schema resolves to a bool, a null or a float
  (`YAML_NOT_TEXT_RE`) is double-quoted now; none is misread, and
  `openGlass` is byte for byte.

No refusal code changes, and none is added.

Closes #912

Lane: openreposhape-5 (openRepoShape-5)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Oct 20, 2026

Copy link
Copy Markdown

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 2 days and 21 hours by commenting @sourcery-ai review. Upgrade to get a review now.

@sourcery-ai

sourcery-ai Bot commented Oct 20, 2026

Copy link
Copy Markdown

Reviewer's Guide

The PR aligns pin answers with what the local clone can actually determine, clarifies when check could not query a recorded commit, and makes shared manifest generation quote names that standard YAML 1.1/1.2 readers would otherwise deserialize as booleans, nulls, or floats, with comprehensive regression and mutation coverage.

Sequence diagram for unreadable annotated pin handling

sequenceDiagram
    participant User
    participant Plan as plan_or_check
    participant Clone
    participant Git

    User->>Plan: commit_in_clone(clone, oid)
    Plan->>Git: cat-file -t oid
    Git-->>Plan: tag
    Plan->>Git: rev-parse oid^{}
    Git-->>Plan: peel unavailable
    Plan-->>Plan: return oid, empty_reason
    Plan->>Clone: read pin source tree
    Clone-->>Plan: pin-source-unreadable
    Plan-->>User: refuse and recommend git fetch --all first
Loading

Sequence diagram for check when commit verification is unavailable

sequenceDiagram
    participant User
    participant Check
    participant Clone
    participant Validate as validate_pins.py

    User->>Check: check recorded pin
    Check->>Clone: recompute digest and ask commit
    Clone-->>Check: unavailable: gone, partial, or no named clone
    Check-->>User: NOTE digest and commit not rechecked
    Check-->>User: exit successfully
    User->>Validate: recompute pin where product is readable
    Validate-->>User: report digest result
Loading

Flow diagram for YAML-safe neutral pin serialization

flowchart LR
    A["neutral_pin_entry(product)"] --> B{PLAIN_SCALAR_RE matches}
    B -- no --> D["double_quoted(product)"]
    B -- yes --> C{YAML_NOT_TEXT_RE matches}
    C -- yes --> D
    C -- no --> E{project reader reads back text}
    E -- no --> D
    E -- yes --> F[write product bare]
Loading

File-Level Changes

Change Details Files
Treat annotated tags whose target commit is unavailable as unanswered pin sources rather than invalid commits.
  • Return the original tag ID when peeling cannot determine its target.
  • Surface pin-source-unreadable with fetch remediation in planning, scaffolding, and check.
  • Pass unavailable tag targets through bump-leg reachability checks as commit-not-on-remote.
scripts/shape_materialize.py
scripts/bump-leg.py
tests/test_pin_annotated_tag.py
tests/test_bump_leg_dry_run_truth.py
Make check notes explicitly distinguish an unverified digest from a commit that could not be queried.
  • Update the note wording and follow-up guidance for gone, partial, forge-read, and unnamed digest sources.
  • Preserve the no-network behavior and existing finding/refusal codes.
adopt-project.py
tests/test_adopt_pins.py
tests/test_pin_annotated_tag.py
tests/test_pin_source_top_level.py
Quote pin names that YAML 1.1 or 1.2 core schemas resolve as non-text values.
  • Add a shared YAML_NOT_TEXT_RE covering boolean, null, infinity/NaN, and dot-float spellings.
  • Require neutral pin entries to be double-quoted for those spellings while preserving bare output for safe names.
  • Expand coverage across project-reader stubs, PyYAML, ruamel.yaml 1.1/1.2, look-alikes, and scaffold/adopt behavior.
scripts/shape_materialize.py
tests/test_neutral_pin_entry_one_spelling.py
tests/test_adopt_pins.py
tests/test_scaffold_pin_and_reuse.py

Assessment against linked issues

Issue Objective Addressed Explanation
#912 Treat an annotated tag whose target commit is missing from the clone as something the clone cannot answer for, rather than incorrectly reporting that the tag names no commit. ✅
#912 When check cannot ask a recorded commit: what it names because the relevant clone is unavailable or partial, or the digest came from gh api, make clear that the commit was not asked; the issue permits this in a NOTE instead of querying the forge. ✅
#912 Quote pinned-product names that YAML 1.1 or YAML 1.2 resolves as a boolean, null, or float, so other YAML readers read the names back as text. ✅

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-5, 2026-10-20T23:54Z) @codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-20T23:59:01.376450Z d5389eb Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Swish!

Reviewed commit: d5389eb6a7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

This was referenced Oct 21, 2026
@brettheap

Copy link
Copy Markdown
Contributor Author

Independent review of #1090 (#912) at d5389eb6, for lane openRepoShape-5: READY

There is one SHOULD. It is a missing test row, not a bug in the code that ships: on every name I measured, the head quotes correctly. Lane 5 can land #1090 as it is and file the row as a follow-up after the v1.2.0 tag.

Findings

SHOULD (missing test row): no row has an exponent of two or more digits.

  • Where: YAML_NOT_TEXT_RE at scripts/shape_materialize.py:542-543, and YAML_FLOATS at tests/test_neutral_pin_entry_one_spelling.py:92.
  • Scenario: change [0-9]+ to [0-9] in both exponent groups of the rule (my mutant R2). All 180 spelling and entry rows still pass. Yet --pin .5e-10@<sha>, .05E-12 and .5_e-10 are then written bare, and PyYAML reads them as 5e-11, 5e-14 and 5e-11. All three names are ones --pin accepts.
  • Why no row catches it: every exponent in YAML_FLOATS has one digit, and PIN_NAMES stops at three characters, too short to hold .5e-10.
  • The head is right: it double-quotes all three, and PyYAML reads each back as text.
  • Fix: add .5e-10, .5E+10, .5_e-10 and .5e10 to YAML_FLOATS.

NIT: one corner where the new bump refusal says something untrue. scripts/bump-leg.py:1263.

  • Scenario: the leg has no remote named origin, and --to is a tag id the leg holds without its commit.
  • It now says bump-leg-commit-not-on-remote: <tag id> is not in the leg's own object store, but the tag object is in the store.
  • Measured by calling _unreachable_refusal(..., fetched=False, has_origin=False) on my lacking clone.
  • Before this PR the same case got target-not-a-commit "names no commit", which was also wrong. With origin present, or under --local-remote-dir, the words are true: "has no branch containing <id>".

NIT: dead branch. scripts/bump-leg.py:1037-1038.

  • else: what = "is an annotated tag that names no commit" can no longer be reached from peeled_target. For a tag, a missing peel now returns, a commit returns, and a tree or blob takes the elif.
  • not_a_commit still needs that wording, because commit_on_forge uses it.

NIT: the PR body.

Checked and true

The head. It reads d5389eb6a71e080673f5fa1b88b750120158fa4f: one commit on 5e539a87, 9 files, tree 95f9b53262fc.

  • git patch-id --stable gives 427e9e35fd2464cb for both d5389eb6 and the pre-rebase 4d777df52.
  • The body's line numbers are true: commit_in_clone at 3650 (6c861635) and 3809 (5e539a87); peeled_target at 972/977; _recomputed_pin_finding at 872/871; neutral_pin_entry at 524.

L1-81 (my own probe, git 2.43 and 2.47.1, offline).

  • The probe used a stub gh that logs its calls: 0 calls.
  • I built the lacking clone differently from the writer: pack-objects of the tag objects alone, then unpack-objects into a clone of the first commit only. The writer used hash-object.
  • rev-parse <tag>^{} exits 1 there with "missing object referenced by".
  • At the head:
    • commit_in_clone(lacking, tag) and the same for a tag of a tag give (id, "");
    • peeled_target returns the id as typed;
    • scaffold --dry-run and plan exit 2 with REFUSED pin-source-unreadable ... exited 128: fatal: not a tree object, the remediation says "git fetch --all first", and no plan is written;
    • check exits 1 with FINDING plan-pin-source-unreadable.
  • Controls at the head: a whole clone still gives plan-pin-not-a-commit "an annotated tag of commit <c>", and a tag of a tree still gives "annotated tag of a tree" and bump-leg-target-not-a-commit.
  • At base 5e539a87, on both gits, every lacking case says "is an annotated tag that names no commit".

M3 is equivalent, as the writer argues.

  • In every lacking case rev-parse prints nothing, so not peeled and names is None agree.
  • git's ^{} reads the final object before it prints its id. So a printed id whose cat-file -t then fails would need the object to vanish between the two calls.
  • Re-run alone, M3 survives (3 passed).

L1-82.

L2-14 (PyYAML 6.0.3 with libyaml; ruamel.yaml 0.19.1, pure, as YAML 1.2 and as 1.1).

Names measured Base 5e539a87 Head
Every --pin name up to 3 characters: 278,915 231,714 bare; misread by PyYAML 132, ruamel 1.2 132, ruamel 1.1 148 231,543 bare; misread 0, 0, 0
Every name PLAIN_SCALAR_RE takes up to 3 characters, its full alphabet: 260,874 257,094 bare; misread 132, 132, 148 256,923 bare; misread 0, 0, 0
All 4-character dot names, 300,000 random 4-8 character names and 144 exponent forms: 573,680 not run 520,938 bare; misread 0, 0, 0
  • openGlass is byte for byte the same. Of the 278,915 entries exactly 171 change. Each was bare on base, is double_quoted at the head and matches the rule. The other 278,744 are identical.
  • YAML 1.1's other forms on PyYAML: 2001-12-14, 1:20, 190:20:30.15, 017, 0o17, 0x1F, 0b101, +12, ~, <<, =, the empty string, +.inf and -.inf. Each is double-quoted, and all three readers read each back as text. None is written bare, as the rule's comment says.
  • One side note on the comment: ruamel.yaml 1.2 reads .5e3 as text, but the YAML 1.2 core schema's float pattern matches it, so quoting it is correct.

Runs, all through run9.sh.

Run git Result
Head, the six test files 2.43 442 passed
Head, the six test files ($Q/v115/w764/git247/bin/git first on PATH; it printed git version 2.47.1) 2.47.1 442 passed
Head, test_shape_doctor.py, test_scaffold_leg_path_collisions.py and the trio 2.43 719 passed, 3 skipped
Head's six test files on base 5e539a87's code 2.43 112 failed, 330 passed
The six test files on the predicted merge with main 2a119f28 (commit 0b9e78aa) 2.43 443 passed (one row more, from main)
  • The 112 failures split by file exactly as the body says: spelling 85, tag file 10, test_adopt_pins.py 10, scaffold 4, bump 2, top-level 1.

Mutants, each applied alone and then restored (sha256 checked).

  • The writer's, re-run:
    • M2 killed (2 failed);
    • M4 killed (8 failed, across all the NOT-rechecked rows);
    • M10 killed (9 failed);
    • M3 survives, and is equivalent.
  • My own:
    • R1, the rule compiled with re.IGNORECASE: killed (3);
    • R3, \.[0-9_]+ reduced to one character: killed (7);
    • R4, commit_in_clone returning (peeled, ""): killed (3);
    • R6, the NOTE without its plan clause: killed (7);
    • R7, the rule without off: killed (9);
    • R2, one-digit exponents: survives (the SHOULD above);
    • R5, \Z changed to $: survives, but is equivalent, because PLAIN_SCALAR_RE's \Z already refuses a trailing newline.

The writer's shards (logs read, not re-run): shard a 6371 passed, 33 skipped, END 2026-10-20T23:52:07Z rc=0; shard b 6296 passed, 3 skipped, END 2026-10-20T23:51:58Z rc=0.

Merges (git merge-tree --write-tree), all clean.

  • Every tree in the body reproduced:
Against Tree
5e539a87 95f9b53262fc
#1073 b58a4e2c 70f85d401ede
#1072 2ffd8235 2e341d250ea1
#1071 24273dd4 0d183517f958
#1070 20d2d4ba a5beb7be1408
#1031 74c17705 324855e1983d
#1081 7d4f2d82 fc9fe407b03b
1e7971cd ffb8249edba4

Semantic clashes with main.

The body's "Found while testing" claim holds, on git 2.43 and 2.47.1. A leg that holds the object of a tag its remote advertises, but not the tag's commit, gets the commit and refs/tags/v1 from a plain branch fetch. So the bump row's choice of a tag that was never pushed is the right one.

Title and body.

Code checks.

  • No assert in the six files is an and/or (ast scan).
  • No added line is wider than 79 characters, and none holds a non-ASCII character.
  • Cognitive complexity by hand, against 15:
Function Now Before
commit_in_clone 9 7
peeled_target 14 12
neutral_pin_entry 2 new
_pin_digest_findings 10 unchanged
  • None of the changed scripts is a pinned copy.
  • SonarCloud for PR 1090: quality gate OK, 0 open issues, 0.0% new duplication.
  • CI at d5389eb6: tests, tests-git-bash, package and SonarCloud all SUCCESS.

Could not verify

  • The counts on 6c861635: 96 failed / 98 passed and 16 failed / 232 passed, and the 2.43 groups of 115, 31, 24, 149 and 123.
  • The 138 at 3dd806f; I only confirmed that Tracking: adopt-project.py, converting an existing repository in place #457 states it.
  • Whether CI has PyYAML or ruamel.yaml installed. tests.yml installs neither explicitly, so CI may skip the enumeration row; the 45 + 45 + 20 rows hold the rule without it.
  • The writer's other-file selection missed 9 files that run scaffold-project.py --pin or the pin validators: test_scaffold_pin_collisions.py, test_neutral_pin_validator.py, test_validate_pins_spelled_and_named.py, test_scaffold_referent_chain_links.py, test_chain_link_fold.py, test_link_source_chain_defaults.py, test_legs_not_a_list.py, test_doctor_validator_rows.py and test_doctor_validator_verdicts.py. I did not run them; CI's full tests job is green at the head.

One rule I broke

A scratch probe I wrote and ran ($Q/v118/rv1090/tools/followprobe.sh, second run) included git -C leg config --get remote.origin.tagOpt.

  • It ran in my own scratch clone, $Q/v118/rv1090/follow2-243/leg and follow2-247/leg.
  • It only read the value, which was empty, and changed nothing.
  • It still breaks "never git config, not even --get". I have removed that line from the script.

Checklist candidates

  • A rule-based quoting change should have one row longer than the enumeration's length cap, for every quantifier in the rule (here, multi-digit exponents).
  • When one pytest call is meant to filter several files, pass a single -k expression: pytest keeps only the last -k. My first runs of M4 and R6 selected the wrong rows that way and had to be redone.

Scratch I leave

  • Notes, logs and tools under /tmp/claude-1000/-workspace-projects-openRepoShape/50cede29-ab82-4ca3-9f67-d98e1a58c8d2/scratchpad/v118/rv1090/. This includes STATE.md, the run logs, mut/, tools/, the probe directories pr-* and follow*, and the entry dumps entries-*.json.
  • Basetemps $Q/v110/bt/rv1090-*.
  • One unreferenced commit object, 0b9e78aa (the predicted merge), in the shared repository.
  • I removed all four scratch worktrees under /workspace/projects/openRepoShape-worktrees/_lane5-scratch/ (rv1090-head, rv1090-base, rv1090-mut, rv1090-mrg) with git worktree remove. I deleted refs/l5rv/rv1090-pr1090, -pr1091 and -pr1087; none of my refs is left.
  • Nothing of mine is running.

(Posted by lane openRepoShape-5's coordinator. rv1090 is lane 5's independent opus reviewer; it wrote none of #1090.)

Lane: openreposhape-5 (openRepoShape-5)

brettheap added a commit that referenced this pull request Oct 21, 2026
… fetch that carry it run in bootstrap.py's `untraced()` environment, so `make siblings` and `family.py siblings` write the token to no trace the caller asks git for (#1055) (#1075)

`make siblings` and `family.py siblings` now write the token in `SHAPE_LEGS_TOKEN` or `SHAPE_LEGS_APP_TOKEN` to no trace the caller asked git for. #996 (#1045) did the same for `make bootstrap`.

Lane 5 found this in its independent review of #1045 (rv996, #1045 issuecomment-6288783192) and measured it in #1055.

`templates/family-root/scripts/siblings.py` gives the credential's `-c url.https://x-access-token:<token>@github.com/.insteadOf=...` rewrite, in argv, to two gits:
- the clone in `place`, for a member with no clone beside the holder;
- the fetch in `place`, for a member whose clone is there.

Both ran in the caller's own environment. So every trace2 target the caller named wrote the token in the `start` event of each, one line per member. `scrub` kept it out of the output, and only that part held.

Now:
- **`rewrite_environment(token)`**, a new function in `siblings.py`:
  - with a token in use, it returns `untraced()` from the `bootstrap.py` beside it. That is every `GIT_TRACE*` variable taken out, and `GIT_TRACE2`, `GIT_TRACE2_EVENT` and `GIT_TRACE2_PERF` set to `0`, which also turns off a target the global or the system config names;
  - with no token, it returns None, so the two gits keep the caller's environment and stay traced where the caller asked, as before.
- **The clone and the fetch** each run with `env=rewrite_environment(token)`. Nothing else in the file changes environment.
- **The member's own `scripts/bootstrap.py`**, which `place` runs after a clone, is given no token in its argv, and no `env`, so it keeps the caller's environment and its traces. That environment holds `SHAPE_LEGS_TOKEN` and `SHAPE_LEGS_APP_TOKEN` as before; the shipped member bootstrap (`templates/assembly-root/scripts/bootstrap.py`) reads neither. A test row holds this, and so does `tests/test_validator_child_encoding.py`, which reads that call as handing no `env`.
- **#1023's jobserver handling is untouched**: `dispatch` still runs each member's make with `member_make_environment(make)`, and no `MAKEFLAGS` reading changed.
- **The docstrings** now say the promise in full: `git_prefix` says the token is "never written into `.git/config`, never persisted, never printed, and never traced". `place`'s ABSENT paragraph in the module docstring says the same, and `rewrite_environment` says what was measured.

**One thing the issue did not ask for.** `GIT_TRACE_PERFORMANCE` writes a git's whole command line, `-c` included, on git 2.43.0 and 2.47.1 alike. So at main it held the token too, two lines per run, and the rows below set it. `untraced()` already takes it out: every `GIT_TRACE*` variable goes.

This follows #1045's `untraced()` (#996): the clone and the fetch run in the very environment `make bootstrap` gives every git it hands the token.

`siblings.py` imports `untraced` from the `bootstrap.py` beside it, next to `REWRITE_TARGETS`, `credential`, `member_make_environment`, `member_path` and `members_of`.

The issue left this to the writer, because a holder may keep a `bootstrap.py` older than its `siblings.py` (`update-shape.py apply --accept-local`), and an import of a name that copy lacks would stop `make siblings`, `park` and `resume` with an `ImportError`. The issue's text was written against main `1a47ed07`, where `siblings.py` still kept its own copy of the `MAKEFLAGS` readings. Since then #1023 (`9e154d87`, lane 4's) has decided the question for this pair of files:
- it replaced that copy with an import of `member_make_environment`, because a copy in each script failed SonarCloud's duplication gate;
- it named the price, in the import's comment and its commit: a holder that keeps its own `bootstrap.py` while `apply` writes `siblings.py` is refused, `update-accept-local-missing-name` (#536, #604), before a byte is written, and merges `bootstrap.py` by hand first.

`untraced` takes the same road for the same reasons:
- **One rule.** Which traces a command carrying the token is kept out of has one answer, for `make bootstrap` and `make siblings` alike. A copy here could drift from `bootstrap.py`'s, and the drift would be a token in a file.
- **A kept old `bootstrap.py` is refused in the one-step case, and not in a two-step one.** The guard reads names. `untraced` is new with #996 (#1045), so a kept `bootstrap.py` from before it is refused by that name, where `--accept-local` names it in the same `apply`. Measured with `update-shape.py`'s own readers, in process:
  - `siblings.py` at this head takes `REWRITE_TARGETS`, `credential`, `member_make_environment`, `member_path`, `members_of` and `untraced` from `bootstrap`;
  - the holder's `bootstrap.py` at main `00f6d7f7` lacks none of them;
  - the holder's `bootstrap.py` at `6476c170` (main just before #1045) lacks `untraced` alone, which is the line the refusal names.
- **The one-step case, measured end to end (rv1075).** A holder cut at `6476c170`, and `apply --at c3cf515 --accept-local scripts/bootstrap.py`: exit 2, `scripts/bootstrap.py (kept) lacks untraced, which scripts/siblings.py imports from it`, and the holder's files, HEAD and status unchanged.
- **Two applies in a row are not refused (rv1075's SHOULD 1, filed as #1082).** A holder whose earlier apply kept a `bootstrap.py` without `untraced` (rv1075 applied `--at 00f6d7f --accept-local scripts/bootstrap.py`: that target's `siblings.py` imports no `untraced`, so there was nothing to refuse), and then an apply at this head without `--accept-local`, gets `siblings.py` copied. `scripts/siblings.py` then fails with `ImportError: cannot import name 'untraced' from 'bootstrap'`, and so do `make siblings`, `park` and `resume`. The cause is the guard in `update-shape.py` (`:4288-4289`): it checks only the files `--accept-local` names in the same run. That is the gap of #536's guard, and it is on main already for #1023's `member_make_environment`; this change puts `untraced` into it and does not widen it. The text above this paragraph said no `ImportError` could reach a holder, which was too strong; #1082 is the follow-up, and it is not done here.
- **No copy, so no test to hold two copies to one answer.** `tests/test_update_shape_kept_helper.py`'s `test_what_siblings_takes_from_the_holders_bootstrap_is_found_defined` already holds that every name `siblings.py` takes is defined in the holder's `bootstrap.py`. Its docstring now says six names, not five.

**Setup:**
- git 2.43.0 (`/usr/bin/git`) and git 2.47.1, Python 3.12.3;
- the family of `tests/test_family.py` (two members, local bare remotes, the file transport allowed through `GIT_CONFIG_COUNT`), laid out as `<tmp>/InkRouter/InkRouter`;
- offline: a stub `gh` first on PATH, a bogus `GH_TOKEN`, an empty `GH_CONFIG_DIR`, `GIT_SSH_COMMAND=false`;
- the holder's own `scripts/siblings.py` was main's (sha256 `86bf57e0...`) or this change's;
- the fake token `w1055-a-token-no-forge-takes`.

**Each case was run three ways:**
- `make siblings`, which clones both members;
- `make siblings` again, which fetches both;
- `family.py siblings --family-root <holder>` in a fresh layout, which clones both.

**Traces were asked for in one of three ways:**
- the seven variables `GIT_TRACE`, `GIT_TRACE2`, `GIT_TRACE2_EVENT`, `GIT_TRACE2_PERF`, `GIT_TRACE_PACKET`, `GIT_TRACE_CURL` and `GIT_TRACE_PERFORMANCE`, each set to a file under the run's TMPDIR;
- `trace2.eventTarget`, `normalTarget` and `perfTarget` in a scratch `GIT_CONFIG_GLOBAL`;
- the same three in a scratch `GIT_CONFIG_SYSTEM`.

Each way was run with `SHAPE_LEGS_TOKEN`, with `SHAPE_LEGS_APP_TOKEN`, and with no token.

That is 108 runs at main and at the change, 54 for each git. Each run's TMPDIR and the whole family folder (the clones' `.git/config` included) were searched for the token.

| traces asked by | main, a token set (both variables, both gits) | this change, a token set | no token, either |
|---|---|---|---|
| the seven variables | `trace2-event.json`, `trace2-normal.txt`, `trace2-perf.txt`, `trace-performance.txt`: 2 lines each, in the clone, the fetch and `family.py` alike | none | no token anywhere; the same files written |
| the global config | `cfg-event.json`, `cfg-normal.txt`, `cfg-perf.txt`: 2 lines each | none | the same |
| the system config | `sys-event.json`, `sys-normal.txt`, `sys-perf.txt`: 2 lines each | none | the same |

**What the table rests on:**
- Every one of the 108 runs (and the 54 runs again on this head's final bytes, sha256 `b1e7bb21...`) exited 0 and ended `siblings ok: 2 member(s) beside the holder in ...`, with the token in neither stdout nor stderr.
- The 2 lines in each file are the `start` (or `performance:`) line of the git given the `-c`, one for each member. Each carries the token twice, once for each spelling the rewrite covers.
- With every member a local path, `GIT_TRACE`'s file was written and held no token, and `GIT_TRACE_CURL` wrote nothing, with no http remote. An https member is a different case, measured below.
- With the change and a token set:
  - every trace file but `GIT_TRACE_PACKET`'s was still written, by the run's other gits and the member's own bootstrap;
  - the packet trace was not written, since only the clone and the fetch write one here.

**An https remote, measured offline (rv1075, then the row of Round 2).** The rewrite reaches only `github.com`, and the probes above are offline, with local paths. A member whose `.gitmodules` url is `https://github.com/<org>/<repo>.git` is reached by it, and can be tried offline behind a dead proxy, `http.proxy=http://127.0.0.1:9`, so that nothing leaves the host:
- rv1075 set one member's url so, with `make siblings`, a fake `SHAPE_LEGS_TOKEN`, and the seven variables set to files, on git 2.43.0 and 2.47.1. Main's `siblings.py` (`86bf57e0`) held the token in `trace.txt`, `trace2-event.json`, `trace2-normal.txt`, `trace2-perf.txt` and `trace-performance.txt`; this head held it in no file and no output. The clone failed cleanly, `git clone exited 128`, and `make siblings` exited 2.
- `GIT_TRACE`'s file holds it too, in the `run_command: git remote-https origin https://x-access-token:<token>@github.com/...` lines: 3 lines on git 2.43.0, 4 on 2.47.1 (rv1075).
- Round 2 below holds this in a test row.

**Still not measured:** a github.com clone or fetch that succeeds, since no run here reaches the network.

`tests/test_family_siblings_untraced.py` is new. It uses `tests/test_family.py`'s `family` and `estate`, `offline` from `tests/test_family_add_refusals.py`, `files_holding` and `CREDENTIALS` from `tests/test_family_bootstrap_stops_named.py`, and `NO_CREDENTIAL` and `needs_make` from `tests/test_siblings_family_name.py`. No assert joins conditions with `and`. The token is the fake `w1055-a-token-no-forge-takes`.

**The rows.** Each entry point is crossed with each way traces are asked for:
- entry points: `make siblings` cloning, `make siblings` again fetching (after a first run with no token and no trace), and `family.py siblings` cloning;
- ways: the seven variables, the three targets in a scratch global config (a copy of the session's, which keeps git's automatic maintenance off), and the three targets in a scratch system config.

**Three tests** (the third is round 2's):
- **`test_with_a_token_in_use_the_token_is_in_no_trace`**: 18 rows (3 entries x 3 ways x 2 variables). Each asserts:
  - exit 0 and `siblings ok: 2 member(s) ...`;
  - the token in neither stdout nor stderr;
  - the token in no file under the test's directory: TMPDIR, every trace and config file, and the clones and their `.git/config`;
  - every trace file the way asks for written: 5 for the variables, 3 for each config;
  - the member's own bootstrap traced where a member was cloned: a git traced in a leg of each clone, which only that bootstrap runs. Where nothing was cloned, nothing ran there.
- **`test_with_no_token_the_clone_and_the_fetch_are_traced_as_asked`**: 9 controls (3 entries x 3 ways). Each asserts exit 0, `siblings ok` and the same trace files written. It also asserts the trace2 event file holds `siblings.py`'s own git at each member, read from the events: `git clone -q --no-local ...` to each member's slot, or `git fetch --quiet origin` in each member's clone.

**Fails on main.** The file was run against main's `siblings.py` (sha256 `86bf57e0...`, put in place and then restored, sha256 checked):
- **18 failed, 9 passed**: every row with a token failed, and the 9 controls passed;
- the variables rows found the token in `trace-performance.txt`, `trace2-event.json`, `trace2-normal.txt` and `trace2-perf.txt`;
- the global-config rows found it in `cfg-event.json`, `cfg-normal.txt` and `cfg-perf.txt`;
- the system-config rows found it in `sys-event.json`, `sys-normal.txt` and `sys-perf.txt`.

- **`test_an_https_member_with_a_token_in_use_is_in_no_trace`**: 3 rows, one for each of `CALLERS_CONFIG` (round 2 and round 3, below).

Each mutant was applied alone to the worktree's file and the new test file run (git 2.43.0). The bytes were restored and checked by sha256 after each:
- `siblings.py` sha256 `b776c69b...`: this head's code; it differs from the head only in two docstring lines;
- `bootstrap.py` sha256 `50a16751...`, main's, unchanged here.

All are **killed**.

| mutant | result |
|---|---|
| M1 the token condition dropped: `untraced()`'s environment always | killed, 9 failed (every control) |
| M1b the condition inverted | killed, 27 failed |
| M1c the clone untraced always, the fetch as it is | killed, 6 failed (the clone controls) |
| M2 the clone left in the caller's environment | killed, 12 failed (every clone row with a token) |
| M3 the fetch left in the caller's environment | killed, 6 failed (every fetch row with a token) |
| M4a `GIT_TRACE2_EVENT` not set to `0` (`TRACE2_OFF`) | killed, 12 failed (the global- and system-config rows) |
| M4b `GIT_TRACE2` not set to `0` | killed, 12 failed (the same) |
| M4c `GIT_TRACE2_PERF` not set to `0` | killed, 12 failed (the same) |
| M5 the `GIT_TRACE*` removal dropped (`env = dict(os.environ)`) | killed, 6 failed (the variables rows, by `GIT_TRACE_PERFORMANCE`) |
| M6 the prefix narrowed to `GIT_TRACE2` | killed, 6 failed (the same) |
| M7 the member's own bootstrap run untraced too | killed, 12 failed (the clone rows' bootstrap check) |

M5 and M6 are killed only because the variables rows set `GIT_TRACE_PERFORMANCE`. With a local-path remote, of the issue's six variables none but trace2's writes the `-c`, and trace2's three are set to `0` whatever the prefix does. With an https remote `GIT_TRACE` writes it too, in the `run_command: git remote-https` lines; that is what the row of Round 2 holds, and the mutant R4 below, which keeps the caller's `GIT_TRACE`, passed all 27 earlier rows.

**How every run was made:**
- through lane 5's runner: `/usr/bin/python3 -m pytest -q -rs -p no:cacheprovider -o tmp_path_retention_policy=failed`, one basetemp per run;
- `LANES_*` cleared, a stub `gh` first on PATH, `GH_TOKEN=bogus`, an empty `GH_CONFIG_DIR`;
- `EMAIL=service@opensoft.dev`, `GIT_SSH_COMMAND=false`, `GIT_CONFIG_GLOBAL=/dev/null` at the start of the session.

| git | files | result |
|---|---|---|
| 2.43.0 | `test_family_siblings_untraced.py`, on this change before the docstring fix | 27 passed |
| 2.43.0 | the same file against main's `siblings.py` | 18 failed, 9 passed |
| 2.43.0 | gate shard A: 19 of the 41 files below | 1623 passed, 4 skipped, in 19:51 |
| 2.43.0 | gate shard B: the other 22 of the 41 files | 2415 passed, 8 skipped, in 15:03 |
| 2.47.1 | `test_family_siblings_untraced.py` | 27 passed, in 1:14 |

**What the 41 gate files are:**
- the new file;
- `test_family_member_make_jobserver.py`;
- every file `grep -l siblings tests/` names;
- the pinned-copy and upgrade-row files: `test_pinned_copy_text.py`, `test_update_shape.py`, `test_update_shape_add.py`, `test_update_shape_kept_helper.py`, `test_no_auto_maintenance.py`, `test_template_text_placeholders.py`, `test_bootstrap_spelling.py`;
- the trio: `test_repo_hygiene.py`, `test_agents_md_codes.py` and `test_cli_reference.py`.

**The gate runs** (both shards at once, then the 2.47.1 run) were made on this change as it is committed: `siblings.py` sha256 `b1e7bb21...` and the new test file sha256 `7ba7a416...`, at main `00f6d7f7` (this branch's base; no rebase was needed, see Merges). The runner's `START` line read `dirty=2`, the two tracked files this commit changes, and a sha256 check after the runs found `siblings.py` unchanged (neither it nor the test files were modified after the runs started). The first pair of shards was stopped part way and re-run whole; only the re-runs count here.

**The 41 files, by shard:**
- shard A (19): `test_shape_doctor.py`, `test_validator_child_encoding.py`, `test_family_py_unreadable.py`, `test_pinned_copy_text.py`, `test_siblings_inputs.py`, `test_siblings_family_name.py`, `test_family_member_make_jobserver.py`, `test_update_shape_add.py`, `test_adopt_printed_sentences.py`, `test_family_printed_quoting.py`, `test_family_makefile_siblings.py`, `test_template_text_placeholders.py`, `test_park_resume_targets.py`, `test_windows_paths.py`, `test_doctor_gitmodules_not_utf8.py`, `test_local_remote_dir_urls.py`, `test_family_siblings_untraced.py`, `test_setup_family_folder.py`, `test_bootstrap_refusal_exit.py`;
- shard B (22): `test_repo_hygiene.py`, `test_doctor_legs_members_next.py`, `test_update_shape.py`, `test_family.py`, `test_update_shape_kept_helper.py`, `test_yaml_reader_spaces.py`, `test_bootstrap_spelling.py`, `test_agents_md_codes.py`, `test_family_siblings_placement.py`, `test_no_auto_maintenance.py`, `test_cli_reference.py`, `test_family_siblings_bootstrap_unreadable.py`, `test_bootstrap_branch_bytes.py`, `test_family_init_asks_every_value.py`, `test_validate_workflow_gitmodules_urls.py`, `test_adopt_contracts_dir.py`, `test_doctor_unfinished_merge.py`, `test_family_members_not_a_list.py`, `test_family_init_landing.py`, `test_family_docs_one_spelling.py`, `test_doctor_members_spelled.py`, `test_pins_child_kind_and_siblings_root_spelling.py`.

**The skips are the environment's, each named by its reason:**
- shard A: 3 in `test_shape_doctor.py` (the PowerShell line is printed, and `powershell.exe` exists, only on a Windows host) and 1 in `test_family_member_make_jobserver.py` (`make -j2` here writes `--jobserver-auth=3,4`, not GNU make 4.4's named pipe);
- shard B: 2 in `test_doctor_legs_members_next.py` (this git crashes on `submodule init`, exit 139, as git 2.43 does on a bare `branch`), 4 in the same file (this git dies on a `shallow` of -2**31, as git 2.43 does), and 2 in `test_no_auto_maintenance.py` (the session began with `GIT_CONFIG_GLOBAL` set, and its file includes that one);
- the 2.47.1 run of the new file skipped none.

The full suite was not run.

**Static checks:**
- flake8 finds in `siblings.py` only the three lines it finds at main (two E501, one E127, each moved down by the added lines);
- in the new test file it finds only the F811s every fixture-importing file has;
- both files are ASCII, apart from the dashes and the CJK example already in `siblings.py`.

Fetched with `-c gc.auto=0 -c maintenance.auto=false`, on 2026-10-20 at about 21:03Z. Round 1, `git merge-tree --write-tree` of the first head `c3cf5154` against (round 2's are in its own section):
- **origin/main `6c861635`**: clean, exit 0, tree `225b2a99`.
  - Main has moved two commits past this branch's base `00f6d7f7`: `9cbba9d5` (#1063: `scripts/family.py`, `scripts/shape_materialize.py`, `docs/cli.md` and two tests) and `6c861635` (#1065: `adopt-project.py` and three tests).
  - Neither touches `siblings.py`, `bootstrap.py`, `tests/test_update_shape_kept_helper.py` or the new test file, so there was no rebase and the gate runs above stand.
- **#1062** (`fix/1040-windows-start-errors`, head `957ba038`), the one open PR that touches a family-root script this change reads (`templates/family-root/scripts/bootstrap.py`, with the assembly root's and `validate-family.py`, `validate-pins.py` and three tests): clean, exit 0, tree `84d4b54a`.
  - Its diff does not touch `untraced()`.
  - In the merged tree `bootstrap.py` still defines `untraced` (and the five other names `siblings.py` takes from it), and `siblings.py` is byte-identical to this head's (sha256 `b1e7bb21...`).
- **The other 9 open PRs** (#1073, #1072, #1071, #1070, #1069, #1068, #1067, #1064, #1031): none touches `siblings.py`, `tests/test_update_shape_kept_helper.py`, or a file the new test imports from (`conftest.py`, `test_family.py`, `test_family_add_refusals.py`, `test_family_bootstrap_stops_named.py`, `test_siblings_family_name.py`), read from `gh pr list --state open --json number,headRefName,headRefOid,files`.

`templates/family-root/scripts/siblings.py` is in `FAMILY_COPIED_VERBATIM`, so each holder gets one `upstream-changed` row for it, as #1045 gave one for `bootstrap.py`:
- a holder that takes it with `bootstrap.py` from #1045 or later needs nothing more;
- a holder that keeps an older `bootstrap.py` with `--accept-local` in the same apply is refused, `update-accept-local-missing-name`, naming `untraced`, before anything is written;
- a holder whose earlier apply kept a `bootstrap.py` without `untraced`, and that then applies this change without `--accept-local`, is not refused: `siblings.py` is copied, and `make siblings`, `park` and `resume` meet an `ImportError`. That is the gap of #536's guard, which `member_make_environment` has on main already, and it is #1082's.

No Windows or macOS run was made, since CI is Linux only (#597).

rv1075, lane 5's independent reviewer, found `c3cf5154` READY (#1075 issuecomment-6302090874): no MUST, two SHOULDs and a NIT. One commit on top of it, `041f7831`, with no rebase: main has touched none of this PR's files.

**The three findings:**
- **SHOULD 1** (the body said no `ImportError` reaches a holder, and two applies in a row make one): answered in the body above, in "Where the helper comes from" and "The pinned copy". The gap is #536's guard's, it is on main for `member_make_environment` already, and it is #1082's; this PR does not change `update-shape.py`.
- **SHOULD 2** (every row used a local file remote, so a leak through `GIT_TRACE` on an https member had no test, and rv1075's mutant R4 passed all 27 rows): answered by the new row below.
- **NIT** (three places said the member's own bootstrap "is given no token"): `siblings.py`'s module docstring, `rewrite_environment`'s docstring and the test file's docstring now say it is given no token in its argv. It inherits `SHAPE_LEGS_TOKEN` and `SHAPE_LEGS_APP_TOKEN` in its environment, because `run_member_bootstrap` passes no `env`, and the shipped one reads neither. `rewrite_environment`'s docstring also names the https measurement.

**The code of `siblings.py` is unchanged.** Its syntax tree with the docstrings taken out is the same at `c3cf5154` and at `041f7831` (compared with `ast`); the file's sha256 moved from `b1e7bb21...` to `19107ad4...` over comments and docstrings only. So round 1's two gate shards (1623 passed, 4 skipped; 2415 passed, 8 skipped) were not re-run; the targeted runs below are.

**The new row,** `test_an_https_member_with_a_token_in_use_is_in_no_trace` (the file has 28 rows now). Its setup:
- the estate of `tests/test_family.py`, with `IRRS`'s url in the holder's `.gitmodules` given as `https://github.com/InkRouter/IRRS.git`, and `IRSS` still a local path;
- `http.proxy=http://127.0.0.1:9`, a dead proxy, in git's command scope after the entries the session holds, and `NO_PROXY` and `no_proxy` empty, so nothing leaves the host;
- a fake `SHAPE_LEGS_TOKEN` and the seven trace variables set to files;
- `make siblings`, cloning.

It asserts:
- the proxy is set where the row sets it: `http.proxy=http://127.0.0.1:9` is in the command scope the run's git reads (`command_scope_at`, which runs `git config --show-origin --list` and writes nothing);
- exit 2 and `IRRS: git clone exited 128:` on stderr, which is how the head fails (rv1075 measured the same);
- `IRSS` was cloned and `IRRS` was not;
- the token is in neither stdout nor stderr, and in no file under the test's directory;
- the five trace files the variables ask for are written, by the run's other gits.

No assert names the proxy in git's message. The git 2.47.1 this lane builds has no `git-remote-https` (`git: 'remote-https' is not a git command`, `fatal: remote helper 'https' aborted session`), so there the clone fails at once without reaching the proxy, still with 128. `GIT_TRACE` has written the `run_command: git remote-https origin https://x-access-token:<token>@github.com/...` line before that, so the row finds the leak on both gits. A first local version asserted the proxy's message and failed on 2.47.1 for that reason; the assert was dropped, and the commit amended, before anything was pushed.

**It fails on main.** The file was run against main's `siblings.py` (sha256 `86bf57e0...`, put in place and then restored, sha256 checked each time):
- git 2.43.0, the whole file: **19 failed, 9 passed**, the 18 rows of round 1 and the new row. The new row fails at `assert files_holding(tmp_path, TOKEN) == []`, with 5 files holding the token: `temp/trace-performance.txt`, `temp/trace.txt`, `temp/trace2-event.json`, `temp/trace2-normal.txt` and `temp/trace2-perf.txt`. The round 1 rows find 4 of those, since a local path leaves `temp/trace.txt` clean;
- git 2.47.1, the row alone: **1 failed**, the same 5 files.

**It kills R4.** R4 is rv1075's mutant, in `rewrite_environment`: `return {**untraced(), **{k: v for k, v in os.environ.items() if k == "GIT_TRACE"}} if token else None`, which keeps the caller's `GIT_TRACE` for the clone and the fetch. Applied alone, then restored and sha256-checked:
- git 2.43.0, the whole file: **1 failed, 27 passed**: the new row alone, with `temp/trace.txt` the one file holding the token;
- git 2.47.1, the row alone: **1 failed**, the same one file.

**The runs on the pushed head `041f7831`** (the runner's `START` line read `dirty=0`), through the same runner as round 1:

| git | files | result |
|---|---|---|
| 2.43.0 | the new file, `test_update_shape_kept_helper.py`, `test_family_makefile_siblings.py`, `test_family_member_make_jobserver.py`, `test_pinned_copy_text.py`, and the trio (`test_repo_hygiene.py`, `test_agents_md_codes.py`, `test_cli_reference.py`) | 589 passed, 1 skipped (the jobserver file's named-pipe row, as in round 1), in 5:09 |
| 2.47.1 (`git version 2.47.1`, printed from the exact path first on PATH) | the new file | 28 passed, in 2:13 |
| 2.43.0 | the new file against main's `siblings.py` | 19 failed, 9 passed |
| 2.47.1 | the new row against main's `siblings.py` | 1 failed |
| 2.43.0 | the new file with R4 | 1 failed, 27 passed |
| 2.47.1 | the new row with R4 | 1 failed |
| 2.43.0 | on a predicted merge with main (below): the new file, `test_update_shape_kept_helper.py`, `test_pinned_copy_text.py`, `test_family_makefile_siblings.py` | 305 passed, in 2:50 |

The full suite was not run. flake8 finds in `siblings.py` the same three lines as at round 1, and in the test file only F811s, from the fixtures it imports. Both files are ASCII, apart from the dashes and the CJK example already in `siblings.py`.

**The merges of round 2.** Fetched with `-c gc.auto=0 -c maintenance.auto=false` at about 23:00Z; `git merge-tree --write-tree` of `041f7831` against:
- **origin/main `c13c3985`** (#1076's squash, which touches `templates/family-root/scripts/bootstrap.py` and `tests/test_family_bootstrap_stops_named.py`, among others): clean, exit 0, tree `8ae10d0c`. None of the three files this PR changes is among the files main changed since the base `00f6d7f7`, so there is no rebase. In that tree `siblings.py` is byte-identical to this head's, `untraced()` has the same syntax tree as at the base, and `CREDENTIALS` and `files_holding`, which the new file imports from `test_family_bootstrap_stops_named.py`, are still defined. The 305-passed run above is on that tree;
- **#1062** (`fix/1040-windows-start-errors`, head `957ba038`): clean, exit 0, tree `e6ed1ba2`. Its `untraced()` has the same syntax tree as main's;
- **the other open PRs** (#1031, #1069, #1070, #1072, #1073, #1081, #1084): none touches `siblings.py`, `tests/test_update_shape_kept_helper.py`, `bootstrap.py`, or a file the new test imports from, read from `gh pr list --state open --json number,headRefName,headRefOid,files` at about 23:00Z. #1076 (`fix/1056-signal-anywhere`, head `73906948`) was open when the runs were made, and merged clean with this head then (tree `6ae3e65f`); it has landed since as `c13c3985`, above.

**The failure.** Lane 5's landing gate (log `c1075.log`, #1075 issuecomment-6304830183) failed the https row of round 2 at `041f7831`: 1 failed, 356 passed. The change in `siblings.py` was not at fault; the row depended on the caller's git config.
- The gate ran with neither `GIT_CONFIG_GLOBAL=/dev/null` nor `GIT_SSH_COMMAND=false`, which the writers' runs and round 2's runs had.
- `conftest.py`'s session global file includes the caller's global config (or `~/.gitconfig`, where `GIT_CONFIG_GLOBAL` is unset). The operator's `~/.gitconfig` has `url.git@github.com:.insteadOf=https://github.com/`.
- Git keeps the first of two `insteadOf` prefixes of the same length, and the config files are read before the command line, so that rewrite beat the token's `-c` rewrite of the same prefix. Measured with scratch files and a stand-in token, on git 2.43.0: under a global file with that rewrite, `git -c url.https://x-access-token:T@github.com/.insteadOf=https://github.com/ ls-remote --get-url https://github.com/InkRouter/IRRS.git` printed `git@github.com:InkRouter/IRRS.git`.
- So the row's `https://github.com/InkRouter/IRRS.git` went over ssh to `InkRouter/IRRS`, a real private repository. The dead proxy never applied, the clone succeeded, `make siblings` exited 0 and the placed members were both there, and the row failed `assert result.returncode == 2`.

**The fix** is in `tests/test_family_siblings_untraced.py` only. No other file changed in this round, and `siblings.py` is byte-identical to `041f7831`'s (sha256 `19107ad4...`).
- **`hermetic_config`** gives the https row's git a global file of its own, `session_global_config("")` (`conftest.py`'s own function: automatic maintenance off, and no include of the caller's file), `GIT_CONFIG_NOSYSTEM=1` and `GIT_SSH_COMMAND=false`.
  - It is the least invasive way: only the one row with an https url sets it, `conftest.py` and every other row stay as they were, and the other rows' remotes are local paths, which no rewrite of GitHub's url reaches.
  - `GIT_CONFIG_GLOBAL` set takes git's two user-level files out of the reading, and `GIT_CONFIG_NOSYSTEM` the system file whatever `GIT_CONFIG_SYSTEM` says (checked on git 2.43.0 and 2.47.1 with scratch files).
  - `GIT_SSH_COMMAND=false` is a second wall: a url somehow sent over ssh runs `false`. No mutant can show it needed, since the runner exports it too.
- **`url_after_config`** is a new assert before the clone: `git ls-remote --get-url <url>` (it applies the config's `insteadOf` and asks nothing of the network) must give the url unrewritten under the row's environment. A config that reaches the row now fails by name, before any clone.
- **The row is parametrized by `CALLERS_CONFIG`**: `none` (round 2's row), and `global` and `system`, each a stand-in for a caller's config that would send the url somewhere reachable. `as_the_callers_config` writes a scratch file of the test's own, `url.<file URI of the family's own IRRS remote>.insteadOf=https://github.com/InkRouter/IRRS.git`, into this process's `GIT_CONFIG_GLOBAL` (the session file includes it) or `GIT_CONFIG_SYSTEM`. It is built from tmp files only: no network, and never the real `~/.gitconfig`. The file now has 30 rows.
- With the fix, in each of the three, the clone fails at the proxy with git's 128, `make siblings` exits 2, `IRSS` is placed and `IRRS` is not, and the token is in no output and in no file under tmp_path.

**The variants fail without the fix.** Each was applied alone to the worktree's test file (or `siblings.py`) and run through the runner, with the file's sha256 checked after it was restored:
- **`041f7831`'s file as pushed, under a caller's global config.** Lane 5's runner now honours `L5_GIT_CONFIG_GLOBAL=<scratch file>` as the caller's global config, with ssh impossible. The scratch file rewrote `https://github.com/InkRouter/IRRS.git` to the run's own IRRS remote (a `file://` url under the run's basetemp). The whole file, git 2.43.0: **1 failed, 27 passed**, the https row at `assert 0 == 2`, the gate's failure reproduced with no network.
- **T2, `041f7831`'s row plus the variants:** the new file with `hermetic_config` and the `url_after_config` assert taken out. The https rows, git 2.43.0: **2 failed, 1 passed**, `[global]` and `[system]` at `assert 0 == 2` (the clone went to the local remote and exited 0), and `[none]` passes. On git 2.47.1 the same.
- **T1,** the row's global file dropped and the assert kept: `[global]` fails at the assert, which sees `file:///...IRRS.git` for the url; the other two pass.
- **T3,** `GIT_CONFIG_NOSYSTEM` dropped and the assert kept: `[system]` fails at the assert; the other two pass.

**R4 stays killed.** rv1075's R4 (`rewrite_environment` keeps the caller's `GIT_TRACE`), git 2.43.0, the whole file: **3 failed, 27 passed**, the three https rows.

**The runs on the pushed head `962ae43c`** (`START` read `dirty=0`), through the same runner as before:

| git | files | result |
|---|---|---|
| 2.43.0 | the new file, `test_update_shape_kept_helper.py`, and the trio (`test_repo_hygiene.py`, `test_agents_md_codes.py`, `test_cli_reference.py`) | 359 passed, in 3:05 |
| 2.47.1 (`git version 2.47.1`, printed from the exact path first on PATH) | the new file | 30 passed, in 1:51 |
| 2.43.0 | the new file, with `L5_GIT_CONFIG_GLOBAL` naming that scratch file | 30 passed, in 2:21 |
| 2.43.0 | `041f7831`'s file, with the same | 1 failed, 27 passed |
| 2.43.0 / 2.47.1 | T2, the https rows | 2 failed, 1 passed / 2 failed, 1 passed |
| 2.43.0 | T1, T3, the https rows | 1 failed, 2 passed each |
| 2.43.0 | R4, the whole file | 3 failed, 27 passed |

The full suite was not run. flake8 finds in the test file only F811s from the fixtures it imports, and nothing over 79 columns; the file is ASCII.

**The merge-tree of round 3.** Fetched with `-c gc.auto=0 -c maintenance.auto=false` at about 10:53Z; `git merge-tree --write-tree` of `962ae43c` against:
- **origin/main `2a119f28`** (#1092): clean, exit 0, tree `b5ea0114`. Main has landed #1076 (`c13c3985`) and #1062 (`ff0c72eb`) since the last round, and has changed `templates/family-root/scripts/bootstrap.py` and `tests/test_family_bootstrap_stops_named.py` among others, but none of this PR's three files. In that tree `siblings.py` is byte-identical to this head's, `untraced()` has the same syntax tree as at the base `00f6d7f7`, and `CREDENTIALS` and `files_holding` are still defined where the new file imports them from.
- **#1088** (`fix/1085-nit-texts`, head `88af1b7b`), which touches the same two files of main's: clean, exit 0, tree `365df092`, with the same findings.
- **The other open PRs** (#1087, #1090, #1091, #1097): none touches `siblings.py`, `bootstrap.py`, `tests/test_update_shape_kept_helper.py` or a file the new test imports from, read from `gh pr list --state open --json number,headRefName,headRefOid,files` at about 10:53Z.

Lane: openreposhape-5 (openRepoShape-5)

Review: rv1075, lane 5's independent reviewer, not the writers (w1055 and f1055): READY at `c3cf5154` (issuecomment-6302090874) with no MUST; its two SHOULDs and its NIT were taken in one new commit, and the re-check is READY at `041f7831` (issuecomment-6303900180), with no MUST or SHOULD; its one NIT, a merge-tree number in the body, is fixed in the body. CI (Brett Heap's ruling 27): H0 is the head 962ae43 itself, which passed tests, tests-git-bash and SonarCloud; main has touched none of its files since its base 00f6d7f; and the local gate passed on its predicted squash tree fc2afb3 over main 16f9a0e: the family_siblings_untraced, update_shape_kept_helper, repo_hygiene, agents_md_codes, cli_reference sets, 359 passed. Landed on Brett Heap's word of 2026-10-18T14:45:28Z, relayed to every lane by lane 3 (#473 issuecomment-6251255596): "merge v1.2.0 PRs once reviewed and green".

Closes #1055

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@brettheap
brettheap merged commit 6da757c into main Oct 21, 2026
5 checks passed
@brettheap
brettheap deleted the fix/912-pin-questions branch October 21, 2026 12:51
brettheap added a commit that referenced this pull request Oct 21, 2026
…keeps refs, and asks a pin's file through the links the root keeps, so a link into `.git` or onto a pin's file is refused before any leg or adopted with the copy beside it (#904) (#1091)

A symlink the plan keeps in the root, where it leads into the clone's own `.git` or through a kept link to a pin's file, is refused before any leg exists for a reason that is true, or adopted with the shape's copy beside it (#904, lines L1-J1, L1-J2, L1-K2 and L1-K9 of #457, with L1-K8 as the rider).

#904's line numbers are at `5017830`. Each claim was measured again by name on main `6c861635`, and again on `5e539a87` after #1064 landed (#1071 and #1076 since then touch neither `adopt-project.py` nor `scripts/shape_materialize.py`).

- **L1-J1 held.** `CLONE_GIT_TREES` still counted `.git/objects`, `.git/refs`, `.git/logs` and `.git/modules` whole. `CLAUDE.md -> .git/refs/remotes/origin/main`, `-> .git/logs/refs/heads/nope` and `-> .git/modules/nope`: `check` 0 (`plan ok`), `execute` 0, `adoption verified`, 2 leg repositories, and the work clone's `.git/<target>` holds the shape's `CLAUDE.md`. Changed: `-> .git/objects/zz/nothing` now ends `REFUSED committing the split. materialize-blocked: ... there is no directory there to write it in`, still after both legs, where `5017830` printed a bare `REFUSED committing the split.`
- **L1-J2 held.** `CLAUDE.md -> .git/refs/../HEAD` and `-> .git/../AGENTS.md`: `check` 1, `plan-root-symlink` "it passes through `.git/refs` (`.git`), which is not a directory in the assembly root", 0 legs.
- **L1-K2 held**, and the same is true of the shape's own pin. `contracts -> governance`, `governance/` kept, `--pin openGlass@<commit>`: a file `governance/openglass-pin.yaml` gives `check` 0, `execute` 0, `beside shape/contracts/openglass-pin.yaml`, `adoption verified`, then the first bootstrap exits 2 with `REFUSED yaml-unparsable`; a link from it to `README.md` gives `REFUSED neutral-pin-unreadable`. With no pin declared and a file `governance/shape-pin.yaml`: `check` 0, `execute` 1 with `FINDING adopt-shape-pin-unreadable` after both legs.
- **L1-K9 held.** `shape/README.md -> ../README.md` kept: `check` 1 says "and `execute` would write the copy through it after both leg repositories were created and pushed" ("over it" for a file), and `execute` exits 2 before any leg. `place` asks `copy_in_the_way`, which stops at a link there, dangling or not, and writes nothing there.
- **L1-K8 is real.** Measured with git 2.47.1 and `GIT_DEFAULT_REF_FORMAT=reftable`: `CLAUDE.md -> .git/reftable` and `-> .git/reftable/tables.list` are refused (`plan-root-symlink`), though the clone holds both; `-> .git/packed-refs` checks ok and `execute` writes the shape's `CLAUDE.md` into `.git/packed-refs`; `-> .git/logs/HEAD` checks ok and stops after both legs. The clone's ref format comes from `GIT_DEFAULT_REF_FORMAT` or `init.defaultRefFormat`, never from the source, and git 2.43 ignores both.

What the clone's `.git` holds when the shape is written was listed from inside `execute` (the materializer wrapped), on git 2.43.0 and 2.47.1, files and reftable. Edge cases were measured too: a source whose HEAD is detached gets a guessed branch, and `core.logAllRefUpdates=false` (global or `GIT_CONFIG_*`) leaves no `.git/logs`.

All in `adopt-project.py`; the tests in `tests/test_adopt_root_symlinks.py` and `tests/test_adopt_root_shape_paths.py`.

- **Named, not counted whole (L1-J1).** `_CloneGit` replaces `CLONE_GIT`, `CLONE_GIT_FOLDED` and `CLONE_GIT_TREES`. It names what git makes there: `.git`, `HEAD`, `config`, `index`, `objects`, `objects/info`, `objects/pack`, `refs`, and `modules` with each leg's store at `modules/<mount path>`. On the files backend it adds `packed-refs`, `refs/heads`, `refs/tags` and the three loose refs `git clone` and `execute` write (the branch the clone checks out, `refs/remotes/origin/HEAD` and the split branch), with a log of each and `logs/HEAD` where git writes reflogs. On the reftable backend it adds `reftable`, `reftable/tables.list` and a `refs/heads` that is a file, and there is no `packed-refs` and no `logs` (L1-K8).
  - A name inside the object store, inside a leg's store, inside the reftable tables, or under `refs/heads` and `refs/remotes` when the source's HEAD is on no branch is neither there nor not there. `check` refuses a link to one, saying that it reads nothing there, so whether `place` writes the copy beside it or the shape's file into `.git` cannot be told before any leg exists.
  - Any other name in `.git` is nowhere. The finding says the shape would write its file through the link into `.git`, where no commit holds it, or would find no directory there and stop after both legs: whichever is true of its directory.
- **How git keeps refs is asked of the machine (L1-K8).** `_clone_ref_store` asks a throwaway repository in the work directory whether it is on the reftable backend and whether it writes reflogs, as `_checks_out_symlinks` asks about `core.symlinks`. `_SymlinkProbe.ref_store` makes that probe the first time a walk is led into the clone's `.git`, and keeps the answer. `check` makes it once, and `execute` twice, as it asks `_checks_out_symlinks` twice: once for `check`'s findings and once for `_refuse_symlinks_checked_out_as_files`. Without a work directory, or where no repository can be made, git's defaults are taken. `_entry_findings` now makes its one probe before `_leg_findings`, so the pins' walks share it.
- **`..` in `.git` (L1-J2).** For a link AT a path the shape writes, a `..` steps back out of a directory `_CloneGit` names, as the end of a target is found there. A link ABOVE one is still never followed into `.git` (D9); its finding now says so, for a `..` and for a target, where it said "not a directory in the assembly root" or "not in the source tree".
- **A pin's file through the kept links (L1-K2).** `_kept_pin_findings` asks each pin's file, the declared pins' and the shape's own, where the root walk reaches it through the links the plan keeps above it (`_Walk.reached`). Where that walk is stopped, the walk's own `plan-root-symlink` still names the link (`PIN_THROUGH`), so one link is one finding. The finding names the link and its remedy: "at `contracts/openglass-pin.yaml` through the symlink `contracts` to `governance` ... send `contracts` to a leg or `drop` it, or rename `governance/openglass-pin.yaml` in the source and re-run `plan`". The code is still `plan-root-holds-pin`, and `_pin_findings` and `_leg_findings` take `source` and `probe` from `_entry_findings`.
- **What the materializer does (L1-K9).** `plan-root-blocks-shape-copy` now says "the materializer writes no copy through it" (or "over it", "cannot write a file where a directory is", "cannot make a directory where a file is"), then ": it would stop there (`materialize-copy-blocked`), writing nothing there, after both leg repositories were created and pushed, so `execute` refuses this plan before either exists." The comment at "A SYMLINK OR A SUBMODULE" and the test's docstring and assertion say the same.
- **Codes kept.** No refusal code changed. Only tests pin the changed words: the README, the handbook mirror, `docs/cli.md` and `AGENTS.md` name none of these sentences.

The probes use #457's setup: a stub `gh` exiting 97, `GH_TOKEN=bogus`, an empty `GH_CONFIG_DIR`, `GIT_SSH_COMMAND=false`, `GIT_CONFIG_GLOBAL=/dev/null`, local remotes, and every null `leg:` answered `root`. They ran on git 2.43.0 unless stated.

| Case | main `5e539a87` | this branch |
|---|---|---|
| `CLAUDE.md -> .git/refs/remotes/origin/main` | check 0, execute 0, verified, 2 legs, the shape's file in that `.git` file | check 1 `plan-root-symlink`, execute 2, 0 legs |
| `-> .git/logs/refs/heads/nope`, `-> .git/modules/nope` | the same as above | check 1, execute 2, 0 legs |
| `-> .git/objects/zz/nothing` | check 0, execute 2 after 2 legs | check 1 (object store not read), execute 2, 0 legs |
| `-> .git/refs/../HEAD`, `-> .git/../AGENTS.md` | check 1 "not a directory in the assembly root", 0 legs | check 0, execute 0, `beside shape/CLAUDE.md`, verified |
| `scripts -> .git/refs/..` (above) | check 1, 0 legs | check 1, 0 legs, saying a link above is never followed into `.git` |
| K2: `governance/openglass-pin.yaml` a file | check 0, execute 0, bootstrap `yaml-unparsable` | check 1 `plan-root-holds-pin`, execute 2, 0 legs |
| K2: it links to `README.md` | check 0, execute 0, bootstrap `neutral-pin-unreadable` | check 1 `plan-root-holds-pin`, execute 2, 0 legs |
| K2: it links nowhere | check 1 `plan-root-symlink`, 0 legs | unchanged |
| K2: nothing kept there (control) | check 0, execute 0, bootstrap 0 | unchanged |
| shape's own pin, `governance/shape-pin.yaml` | check 0, execute 1 `adopt-shape-pin-unreadable` after 2 legs | check 1 `plan-root-holds-pin`, execute 2, 0 legs |
| K9: `shape/README.md -> ../README.md`, and a file there | check 1 "`execute` would write the copy through (over) it" | check 1 "the materializer writes no copy through (over) it: it would stop there ..." |
| git 2.47.1 reftable: `-> .git/reftable`, `-> .git/reftable/tables.list` | check 1, 0 legs | check 0, execute 0, `beside shape/CLAUDE.md`, verified |
| git 2.47.1 reftable: `-> .git/packed-refs` | check 0, execute 0, the shape's file in `.git/packed-refs` | check 1, execute 2, 0 legs |
| git 2.47.1 reftable: `-> .git/logs/HEAD` | check 0, execute 2 after 2 legs | check 1, execute 2, 0 legs |

These names also adopt with the copy beside them, as on main: `.git/refs/heads/main`, `.git/refs/remotes/origin/HEAD`, `.git/refs/heads/adopt/three-repo-shape`, `.git/logs/refs/heads/main`, `.git/modules/code`, `.git/objects/info` and `.git/refs/tags`.

**Rows that fail on main.** The two test files of this branch were run on a clone of main `5e539a87` (whose `adopt-project.py` and `scripts/shape_materialize.py` are those of main `c13c3985f`): 42 failed, 277 passed, 3 skipped.

The 42 failures are every row this branch adds or changes, except those that hold on main too:
- L1-K9: the three #262 rows, the case-aside row, `test_a_symlink_at_the_copy_itself_is_in_the_way` and `test_a_link_at_the_copy_is_one_finding_by_the_rule_that_sees_it`.
- L1-J1, refused: the D9 row and the `.git/description` and `.git/hooks` rows (new words), four `J1-at-a-path-to-no-*` rows, `at-a-path-to-no-directory-in-dot-git`, `J1-at-a-path-into-the-object-store` and `at-a-path-into-a-legs-store`.
- L1-J2: three refused `J2-*` rows, two accepted `J2-*` rows, and the end-to-end `J2-back-out-of-refs-and-origins-head`.
- L1-K2: three `HELD_THROUGH` rows and the shape's own pin.
- The ref store: three no-probe rows, the probe's answer, no work directory, eight `ON_REFTABLE` rows, no reflogs, a detached source HEAD, and the probe made once.

The rows that pass on main too are kept as guards:
- the ten `GIT_NAMED` accepted rows and the end-to-end `the-cloned-branch-and-the-split-branchs-log`, names that were counted whole and are now named;
- the no-pin-file control;
- `test_the_materializer_stops_at_a_link_at_the_copy`, which is L1-K9's evidence that the materializer stops there.

The three rows that need git 2.45 or later were skipped there (git 2.43).

**Mutants.** Each mutant was applied alone to a copy of this head, and the rows that hold it were run on git 2.43. All 21 were killed, each by an `AssertionError`, with failed and selected counts as shown. None was equivalent.

| Mutant | Failed / selected |
|---|---|
| M1 an unread name counts as there | 2/2 |
| M2 the cloned branch is not named | 2/2 |
| M3 the split branch is not named | 2/2 |
| M4 reflogs always | 1/1 |
| M5 reftable never | 6/8 |
| M6 a `..` above is followed into `.git` | 1/1 |
| M6b no `..` out of a `.git` directory | 4/6 |
| M7 a link above is taken as one at | 1/2 |
| M8 a pin's file is not asked through links | 4/7 |
| M9 asked through a stopped walk too | 4/4 |
| M10 K9's old sentence | 2/47 |
| M11 the probe says reflogs always | 1/1 |
| M12 "written into `.git`" and "no directory" swapped | 5/6 |
| M13 the cloned branch is the branch read | 1/1 |
| M14 the clone's `.git` asked eagerly | 3/3 |
| M15 the ref probe's answer not kept | 1/1 |
| M16 no work directory gives no defaults | 1/1 |
| M17 the through-link remedy is the entry's | 4/7 |
| M18 the pins are asked without the source | 4/7 |
| M19 `reached` is taken from any part | 1/7 |
| M20 the `.git` reasons are gone | 9/9 |

**Runs** (`run9.sh`, one shared stub `gh`, `EMAIL`, `GIT_SSH_COMMAND=false`, `GIT_CONFIG_GLOBAL=/dev/null`):
- git 2.43.0, both own test files: 319 passed, 3 skipped (the reftable rows).
- git 2.47.1 first on PATH, both own test files: 322 passed.
- git 2.43.0 gate: every test file that runs `adopt-project.py` (`grep -l` in `tests/`), with `test_shape_doctor.py`, `test_scaffold_leg_path_collisions.py`, `test_repo_hygiene.py`, `test_agents_md_codes.py` and `test_cli_reference.py`. That is 122 files in two shards of 61: shard A, 4075 passed, 10 skipped (`END 2026-10-20T23:41:03Z rc=0`); shard B, 4713 passed, 9 skipped (`END 2026-10-20T23:53:03Z rc=0`).

`git merge-tree --write-tree` of this head against:
- origin/main `c13c3985f`: clean. Main moved after the runs above, which were made on `c13c3985`: #1081 (`06cbd478`) and #1072 (`1e7971cd`) have landed, and #1072 touches `adopt-project.py`. Against origin/main `1e7971cd` the head is clean too, tree `cf4e2e68`.
- #1073 (`l3-914-adopt-record-and-doctor`, `b58a4e2cc`): clean. #1072 (`l3-918-source-and-locale`, `2ffd82356`) was clean too, and has landed since as `1e7971cd` (above).
- #1031 (`fix/910-check-throwaway-repos`, `74c17705e`): conflicts in `adopt-project.py`, 3 hunks. Both moved the one `_SymlinkProbe` in `_entry_findings`, and both added attributes to `_SymlinkProbe.__init__`. To resolve, make the probe before `_leg_findings` as this branch does, keep #1031's `unasked`, `linked` and `asked` lines, and keep both sets of attributes.
- #1070 (`fix/979-named-link-one-finding`, `20d2d4ba6`): conflicts in `adopt-project.py`, 7 hunks. Both added parameters to `_kept_pin_findings` and `_pin_findings` and changed their docstrings, the `_leg_findings` docstring and the "A SYMLINK OR A SUBMODULE" comment. To resolve, take both parameters (`named_links`, then `source` and `probe`) and both texts, and in that comment keep this branch's sentence on what `place` does at the copy, with #979's clause after it.
- #1081 and the other open pull requests touch none of these files; lane 5's #1090 (`fix/912-pin-questions`, `d5389eb6`, for #912) also changes `adopt-project.py`, and the head merges with it clean, tree `0153f3e7`.

Main moved to `2a119f28` while this was in review. Two of the pull requests that landed change `adopt-project.py`: #1031 (#910, the throwaway repositories `check` asks) and #1070 (#979, a named link at a copy or a pin is one finding). The branch was merged with main, not rebased, as `1cc9180fd` (parents `8f5fd0fd3` and `2a119f281`). Ten hunks of `adopt-project.py` conflicted, seven with #1070 and three with #1031. Each was resolved by reading both sides:

- **#1070.**
  - `_kept_pin_findings` and `_pin_findings` take both sets of parameters: `named_links` first, as main passes it, then `source` and `probe`, which the calls pass by keyword.
  - #979's `named_links` check stands in this branch's body as it is. A link `named_links` holds at the pin's file is left out. A walk that meets one through a link above has stopped, so the pin's file is asked at its literal path, where nothing is found, and the walk's own finding is left out too (`walk.empty`).
  - Both docstrings are kept, #979's paragraph first. `_leg_findings` passes `named_links, source=source, probe=probe`.
  - The "A SYMLINK OR A SUBMODULE" comment keeps this branch's sentence, that `place` stops at a link AT the copy and writes nothing through it. Main's "and as a write THROUGH it" is dropped, since it is not true. #979's clause follows.
- **#1031.**
  - In `_entry_findings`, #1031's `_paths_git_refuses` question comes first. The one `_SymlinkProbe` is then made before `_leg_findings`, because the walks of the pins' files ask it. #1031's `linked`, `asked` and `unasked_notes` lines and its `probe.notes()` are kept.
  - `_SymlinkProbe` keeps #1031's `unasked` beside this branch's `_refs`. `ref_store` now does what #1031 has `symlinks` do: where `git init` fails otherwise than with an `OSError`, it takes git's defaults (the files backend, with reflogs) and keeps git's words in `refs_unasked`. Before the merge, that failure raised out of `check` as "a git or gh command failed".
  - `notes()` gives each NOTE. The symlink NOTE keeps #1031's words, and the ref NOTE is new. Both take git's words from one `_init_refused`.
- Nothing else #1031 or #1070 changed is called by this branch's code. #1070's other hunks are the docstrings of `_links_at_a_copy` and `_links_at_a_pin`. #1031 leaves `_PlannedRoot`, `_checks_out_symlinks` and `_refuse_symlinks_checked_out_as_files` as they were.

New rows in `tests/test_adopt_root_symlinks.py` cover the ref probe's failed `git init`, raised as a `CommandFailed` and as a `Refusal`. Further rows check that an `OSError` gives no NOTE, and that `check` under `GIT_DEFAULT_HASH=bogus` says where git would not make the ref probe.

**Runs on `1cc9180fd`** (`run9.sh`, as before):
- git 2.43.0, both own test files: 323 passed, 3 skipped (the reftable rows).
- git 2.47.1 first on PATH, both own test files: 326 passed.
- The two test files on main `2a119f28`'s code: 46 failed, 277 passed, 3 skipped. These are the 42 of before plus the four new ref-probe rows.
- Mutants, re-run where the resolution touched them, each killed:
  - M8, M9, M15, M16, M17, M18 and M22 by an `AssertionError`.
  - New: M21, the ref probe's failed `git init` raising as before. It fails two rows because the `CommandFailed` or `Refusal` it lets out escapes, and one row by an `AssertionError`.
  - M23 removes #979's `named_links` skip at a pin's file. Only #979's own `test_a_named_link_at_a_copy_or_a_pin_is_one_finding` holds it, and that test fails by a `ValueError`, not an `AssertionError`: it unpacks exactly one finding (`tests/test_adopt_symlink_target_bytes.py:667` and `:818`), and the mutant gives two.
- Gate, git 2.43.0: the 125 test files the merged tree has for `adopt-project.py`, with `test_shape_doctor.py`, `test_scaffold_leg_path_collisions.py` and the trio, in two shards. Shard B (62 files): 4507 passed, 15 skipped, `END 2026-10-21T13:14:42Z rc=0`. Shard A (63 files) was running at the checkpoint: at 87% at 13:43Z, with no failure so far.

Commit `160de1186`, on top of the merge, answers rv1091's review of `8f5fd0fd`. rv1091 re-checked it: SHOULD 1, SHOULD 2 and NITs 1-3 are fixed, and the two test files gave 326 passed, 3 skipped.

- **SHOULD 1: "writing nothing" was false.** `place` writes the shape's files in order and stops only at the blocked copy, so every copy before it is written. rv1091 measured 0, 5, 9 and 12 files written before the stop, at `shape/README.md`, `shape/CLAUDE.md`, `shape/Makefile` and `shape/.github/workflows/validate.yml`. `plan-root-blocks-shape-copy` now says "it would stop there (`materialize-copy-blocked`), writing nothing there, after both leg repositories were created and pushed". That matches the materializer's own "and nothing was written there". The comment above the sentence says the same, and `STOPS` in `tests/test_adopt_root_shape_paths.py` pins the new words.
- **SHOULD 2: SonarCloud's two `python:S5778`.** Both are in rows this PR adds, and each made a second call inside `pytest.raises`. `template_values()` (the materializer's link-at-the-copy row) and `loaded.open_source(None, work)` (the row that checks the ref probe is made once) are now made before the block.
- **NIT 1: `python:S3776` on `_kept_pin_findings`.** Sonar's analysis of the merged function gives 30. The finding for one pin's file on one disk is now `_kept_pin_finding`, and the through-link words and their fix are `_through_the_kept_link`. The nested conditional in `_shape_pin_held`'s argument is now a statement. Sonar's analysis of the new functions (`analyze_code_snippet`, project `opensoft_openRepoShape`) finds no issue in them.
  - Each return value stands for what the loop did before. None asks the next disk, `[]` is #979's named link, and a list of one line is the finding.
- **NIT 2: what a guessed refs directory holds.** The case is a source whose HEAD is on no branch. The text "where `git clone` writes the branch it checks out" was given for `.git/refs/remotes`, `.git/logs/refs/heads` and `.git/logs/refs/remotes` as well as `.git/refs/heads`. `_guessed_refs` now says what `git clone` writes in each one:
  - in `refs/heads`, the branch it checks out;
  - in `refs/remotes`, `origin/HEAD` for that branch;
  - in the two `logs/` directories, the reflog of either.

  This was measured on git 2.43.0 and 2.47.1, for a source detached at a branch's tip and for one detached off any tip. In the first case git checks out `main` and writes `refs/remotes/origin/HEAD` (`ref: refs/remotes/origin/main`) and the reflogs of both. In the second it writes neither, and `refs/heads` holds only the split branch. Each link is still refused, as "cannot be told". The detached-HEAD row is now four rows, one per directory.
- **NIT 3: the ref probe is made "once per command".** That was false for `execute`. Counted in process with `CLAUDE.md -> .git/HEAD`, `check` makes the probe once, with or without a kept `.gitattributes`, and `execute` makes it twice. The first is for `check`'s findings, which `_refuse_what_check_finds` asks. The second is for the walks of `_refuse_symlinks_checked_out_as_files`, which make their own `_SymlinkProbe`, as they ask `_checks_out_symlinks` again on main. The code is unchanged. The sentence under "What changed" now says so.

- **NIT 4: two mutants of rv1091's that survived the first round.**
  - R8 guesses only `refs/heads` for a detached source, not `refs/remotes`. Round 2's detached-HEAD rows for `.git/refs/remotes/origin/HEAD` and its reflog kill it (M28 below).
  - R6 has the fix rename the kept file rather than the path the walk reaches. It still survives here, because no row keeps a directory at the reached pin file. That is a gap in the tests, not in the code. The row is a follow-up for after the v1.2.0 tag, written and run but not in this PR: `contracts -> governance`, with `governance/openglass-pin.yaml/notes.md` kept, gives "inside `contracts/openglass-pin.yaml` through the symlink ... rename `governance/openglass-pin.yaml`", and main checks it with exit 0.

**Words.** A grep of `tests/`, `README.md`, `docs/` (outside the release notes), `AGENTS.md` and `templates/` finds the changed words only in the two test files. No refusal code changed.

**Rows that fail on main.** The detached-HEAD rows for `.git/refs/remotes/origin/HEAD`, `.git/logs/refs/heads/main` and `.git/logs/refs/remotes/origin/HEAD` are new. A probe on main `2a119f28` gives `check` exit 0 for all four links, so main refuses none of them, and every row expects a refusal. The pytest run of these rows on main was not made before the checkpoint. On the merge `1cc9180fd` all four are refused, but the three new ones say "the branch it checks out".

**Mutants** on `160de1186` were not run before the checkpoint. Each is written, and its anchor occurs once in `160de1186`'s `adopt-project.py`:
- M17 and M23, re-anchored on the new helpers: the through-link fix becomes the kept entry's, and `_kept_pin_finding`'s `return []` for #979's named link is dropped;
- M24: every guessed directory says what `refs/heads` holds;
- M25: no "the reflog of";
- M26: the K9 sentence's old "writing nothing,";
- M28: rv1091's R8;
- M8, re-run.
rv1091 ran R8 at `160de118`, and the NIT 2 rows kill it (2 of 4 failed).

**Runs on `160de1186`** (`run9.sh`, as before):
- git 2.43.0, both own test files: rv1091 ran them at `160de118`, 326 passed, 3 skipped (the reftable rows).
  - Mine ran at `3abf3b1f5`: 327 passed, 3 skipped, `END 2026-10-21T13:32:51Z rc=0`.
  - `3abf3b1f5` is `160de118` plus the R6 row alone, the follow-up kept off this branch. Its `adopt-project.py` and `tests/test_adopt_root_shape_paths.py` are `160de118`'s, and its `tests/test_adopt_root_symlinks.py` adds that one test function.
- git 2.43.0, `test_shape_doctor.py`, `test_scaffold_leg_path_collisions.py`, `test_repo_hygiene.py`, `test_agents_md_codes.py` and `test_cli_reference.py`: running at the checkpoint, at 19% at 13:43Z, with no failure so far.
- Not started before the checkpoint:
  - the seven other test files that pin `plan-root-holds-pin` or the K9 sentence (`test_adopt_pins.py`, `test_adopt_shape_pin_and_work_dir.py`, `test_adopt_symlink_target_bytes.py`, `test_adopt_ignored_shape_files.py`, `test_adopt_root_manifest.py`, `test_adopt_nested_attributes.py` and `test_adopt_reencoded_files.py`), though all seven are in the gate on `1cc9180fd`;
  - the two own files on git 2.47.1.
- CI runs the full suite on the push.

**Merge-trees** of `160de1186`, by `git merge-tree --write-tree`:
- origin/main `7e00834cf`: clean, tree `0b8a5bc9`. #1087 (`16f9a0e81`) and `7e00834cf` landed after the merge and change none of this branch's files. #1087 changes `scripts/shape_materialize.py`, whose `place` and `copy_in_the_way` the K9 sentence describes, but only the fetch for a branch's answer.
- #1090 (`1ad76055b`): clean, tree `108c3a8e`.
- origin/main `6da757cbe`, re-fetched at about 13:10Z, after #1090 landed: clean, tree `283d7b52`.
  - #1090 changes `adopt-project.py` in the NOTE `check` prints for a pin's digest and in the docstring of `_neutral_product_pins`.
  - Neither is code this branch changes, and no row here pins those words.

Lane: openreposhape-5 (openRepoShape-5)

Review: rv1091, lane 5's independent reviewer, not the writer (w904): NOT READY at `8f5fd0fd` (#1091 issuecomment-6316443939) for two SHOULDs, with the merge `1cc9180f`'s resolution READY; the fix round took both SHOULDs and three NITs, and the re-check is READY at `160de118` (#1091 issuecomment-6317666157); its one open gap, R6's row, is filed as #1116. CI (Brett Heap's ruling 27 and his fast-refresh ruling of 2026-10-15T22:48:35Z): H0 160de11 passed tests, tests-git-bash and SonarCloud; the head 551452b is a clean update-branch merge of main 823624d, with tests-git-bash and SonarCloud green; and the local gate passed on its predicted squash tree c1dcef6 over main 823624d: the adopt_root_shape_paths, adopt_root_symlinks, repo_hygiene, agents_md_codes, cli_reference sets, 496 passed, 3 skipped. Landed on Brett Heap's word of 2026-10-18T14:45:28Z, relayed to every lane by lane 3 (#473 issuecomment-6251255596): "merge v1.2.0 PRs once reviewed and green".

Closes #904

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 22, 2026
…eviews and the Known limits continuation, with lanes 2, 4 and 5's items: eleven sites left to #1110, the doctor's 000 and 444 mount rows, the #1082 upgrade warning and the #891 and #1103 limits said as main's code does, #1112 and lane 2's two doctor limits named, and the shape copies measured (#921)

The fix round on RV1155a, RV1155b, RV1155c and RV1155d, the continuations
RV1155d2 and RV1155a2, and the comments lanes openRepoShape-4 and
openRepoShape-5 left on #1155:

- RV1155c's two MUSTs: `spec/` at 000 is `cannot be listed` and at 444
  `spec/.git: cannot be read`; `scripts/` at 444 is
  `INVALID (manifest, pins)` on 3.14 as on 3.12.
- Every SHOULD (a 3, b 2, c 3, d 13) and every NIT but RV1155a's NIT 7,
  RV1155a2's eight among them.
- Where lane 5 and a reviewer word one line, one text covers both:
  #891 (d SHOULD 6, lane 5 SHOULD 7), #1103 (d SHOULD 7, lane 5
  SHOULD 4) and #1090 (c's first NIT, lane 5 NIT 14).
- KNOWN_OPEN in tests/test_path_guard_hygiene.py holds eleven sites, so
  both lines say eleven.
- #1112 is a known limit; lane 2's two doctor limits (#975) and its
  test-only gaps (#975, #1050) are listed, and the "INVALID, not DRIFTED"
  sentence holds for a mount this user cannot list or read.
- The shape-copy measurement fills its placeholder; the "Before tagging"
  pointer is gone.
- The pipx claim carries RV1155d's qualifier everywhere it is made.
- v1.0.0's tab-in-a-pin line is closed (#1015); its bare number or
  boolean in a plan's text field is narrowed (#1016).
- `update-shape.py check` is named with `--at <release-commit>`, and the
  measurement says main `038dafb` was the upstream.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 23, 2026
…nt of the commit `--to` names, so a tag above it reads as one that reaches it and a tag below it keeps the plain words, and `--points-at` for `--contains` no longer survives (#1019) (#1107)

No test held that a tag at a DESCENDANT of the refused commit counts as a tag that reaches it, so `--points-at` in place of `--contains` survived in `bump-leg.py` and `family.py` (mutant M9 of #988's review, filed as #1019). This adds the rows the issue asks for to the two reachability test files, and no production code changes. Opened as a draft with no milestone: Brett Heap's ruling of 2026-10-21T10:32:04Z ("leave the follow-ups out of v1.2.0") held it until the v1.2.0 tag, and it is now on the v1.2.1 milestone.

Measured on main `2a119f28` before any change, and checked again on `16f9a0e81` and `7e00834c` (the tag questions did not change, only their line numbers):

- `scripts/bump-leg.py` `_remote_tag_reaches` and `scripts/family.py` `_origin_tag_reaches` list the tags with `git for-each-ref --contains <commit> --format=%(objectname) refs/tags/`. No other copy of either exists (the `--contains` in `on_a_remote_branch` and `_on_a_remote_branch` asks about branches and is not this question).
- The mutant (`--contains` -> `--points-at` on that one line in each script) SURVIVES every row on main: `test_bump_leg_reachability_words.py` 34 passed, `test_family_bump_reachability_words.py` 20 passed, 54 rows, as the issue says.
- The production code is right: the new rows pass on unmutated main. So they do not "fail on main"; they fail on the mutant, which is the issue's own acceptance ("With `--points-at`, that row must fail"). No defect was found.

`tests/test_bump_leg_reachability_words.py`:
- `made_in_the_leg(project, name, on="HEAD")` takes an optional `on`, the commit the new commit is made on top of. Every existing call is unchanged.
- `test_a_tag_at_a_descendant_of_the_commit_reaches_it`: a commit `made` and a child `above` on top of it, on no branch of the leg's remote. The leg and the remote each tag `above` (`v1-w1019`), made apart, in three kinds: both annotated (the reviewer's case J), leg lightweight and remote annotated, and the reverse. Both runs (dry run and real) refuse `made` `bump-leg-commit-local-only` in the TAG words, through `origin` and through `--local-remote-dir` (3 kinds x 2 routes = 6 rows), with nothing moved and the leg's own tag as it was. The premises say the tag names the child, that no tag is AT `made` (`for-each-ref --points-at`), and, through #1087's `only_a_tag_reaches`, that a tag of the remote reaches it, no branch does and no remote-tracking ref of the leg does.
- `test_a_tag_at_an_ancestor_of_the_commit_alone_keeps_the_plain_words`: the control (x 2 routes). The commit named is the child, and the leg and the remote tag only its parent. The plain words stay, "a pin the rest of the world cannot fetch" included.

`tests/test_family_bump_reachability_words.py`:
- `made_on(holder, env, parent, name)`: a commit never pushed, made in the mount on top of `parent`, through `commit_in_mount` (which checks `parent` out first), so the holder stays clean.
- `test_a_tag_at_a_descendant_of_the_commit_reaches_it`: the same three kinds (3 rows), both `bump --dry-run` and the real `bump` refuse `member-to-local-only` in the tag words, with nothing moved and the mount's own tag as it was.
- `test_a_tag_at_an_ancestor_of_the_commit_alone_keeps_the_plain_words`: the control (1 row).

No assert joins conditions with `and` or `or`; each premise is its own assert. No README, docs, AGENTS.md or handbook text names these tests or their row counts (grep), so none changes.

| Mutant | Rows before (main's) | With these rows |
| --- | --- | --- |
| `--points-at` in `bump-leg.py` | survives: 34 of 34 pass | killed: 6 failed, 36 passed (all six descendant rows; the two controls pass, as they should) |
| `--points-at` in `family.py` | survives: 20 of 20 pass | killed: 3 failed, 21 passed (all three descendant rows) |
| `--merged` in `bump-leg.py` (extra: a check read the other way round) | survives: 34 of 34 pass | killed: 8 failed (6 descendant rows and both controls) |
| `--merged` in `family.py` (extra) | survives: 20 of 20 pass | killed: 4 failed (3 descendant rows and the control) |

The failure is the one the issue names: the plain words ("is reachable from no branch of ...") where the tag words ("and a tag of ... reaches it, but no branch ... does") are expected. Logs: `r-mut-points-at-bump.log`, `r-mut-points-at-family.log`, `r-mut-merged-bump.log`, `r-mut-merged-family.log` (and `mut-main-*.log` for the 34 and 20 on `2a119f28`).

On the pushed head `01fcb259`:
- git 2.43.0 (`/usr/bin/git`): the two test files, 66 passed (42 + 24; 34 + 20 before).
- git 2.43.0: the trio `tests/test_repo_hygiene.py`, `tests/test_agents_md_codes.py`, `tests/test_cli_reference.py` and `tests/test_update_shape_kept_helper.py`, 329 passed.

On `1201680f` (the same two files on top of `16f9a0e8`, before `7e00834c`, whose three files no row of mine touches or runs):
- git 2.43.0: the trio alone, 170 passed; the two test files that import helpers from mine (`test_bump_leg_fetch_refspecs.py`, `test_family_bump_fetch_refspecs.py`), 102 passed; the two test files, 42 and 24 passed.
- git 2.47.1 (`$Q/v115/w764/git247/bin/git` first on PATH, `git --version` printed `git version 2.47.1`): the two test files, 66 passed.

No script changed, so no other test runs a changed script.

- `git merge-tree --write-tree HEAD origin/main` against a re-fetched `origin/main` (`7e00834c`, which is also the merge base): exit 0, tree `3a852e87fcfda41766ece8c1771e47689c622ad6`, equal to this head's tree. Against `16f9a0e8` before the last rebase: exit 0 too.
- Open PRs read with `gh pr list --repo opensoft/openRepoShape --state open --json number,headRefName,isDraft,files`: 8 (#1102, #1101, #1098, #1097, #1091, #1090, #1088, #1075). None touches `tests/test_bump_leg_reachability_words.py` or `tests/test_family_bump_reachability_words.py`, so there is no merge-tree to run against any of them.

- **The NIT.** The docstrings of the two descendant rows said "Every row before this one tagged the commit itself, so ... survived all 54 of them". 54 counts the two files together (34 + 20), several earlier rows tag nothing (the no-`origin` and prune rows), and a count goes stale with the next row.
- **The fix, docstrings only.** Each now says only what holds: "No row before this one tags a descendant of the commit: the tags in them are at the commit itself or below it, so `--points-at` in place of `--contains`, the mutant M9 of #988's review, survived every row of this file." No row, assert or helper changed. The 54 in "What still held on main" above is the two files together, as it says.
- **One commit on top of `01fcb259`, a fast-forward push, no rebase:** head `ee48b5fe9693ebda099d1c0aadaca7bdc16b91c1`, read back with `git ls-remote`. Its tree is `615e571bb62fd1cf481e4152278b0e3628bab059`.
- **Runs** (git 2.43.0, through `run9.sh`, `EMAIL=service@opensoft.dev`): `tests/test_bump_leg_reachability_words.py` 42 passed, `tests/test_family_bump_reachability_words.py` 24 passed. They ran on `7186f3cf`, the same tree as the pushed head; only the commit message was shortened after.
- **Merge-tree** against a re-fetched `origin/main` (`6da757cb`, merge base `7e00834c`): exit 0, tree `453566df33c886cf7fc2b1c39af02acbbff365dc`. Main moved by nine files, none of mine; its `scripts/bump-leg.py` change (9 lines added) leaves `_remote_tag_reaches` and its `--contains` line alone (now line 1214).

Review: rv1104-1107, lane 5's independent opus reviewer, which wrote none of #1107: READY at `ee48b5fe`, the head that lands (issuecomment-6318124100), with no MUST or SHOULD and one optional test NIT that needs no round. CI (Brett Heap's ruling 27): H0 is the head ee48b5f itself, which passed tests, tests-git-bash and SonarCloud; main has touched none of its files since its base 7e00834; and the local gate passed on its predicted squash tree 2f7f587 over main 422a590: the bump_leg_reachability_words, family_bump_reachability_words, repo_hygiene, agents_md_codes, cli_reference sets, 237 passed. Landed on Brett Heap's word of 2026-10-23T13:10:26Z, relayed to every lane by lane 3 (#473 issuecomment-6365697123): "merge v1.2.1 PRs once reviewed and green".

Closes #1019

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant