Skip to content

shape-doctor.py and update-shape.py: run from a partial clone of the standard, the shape currency row asks without fetching whether the clone holds what the comparison reads, and is CANNOT ANSWER with a fetch for a person to run where it does not, and a read that fails is refused, never a copy called both (#683) - #742

Merged
brettheap merged 2 commits into
mainfrom
l3-683-partial-clone
Oct 17, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Lane: openreposhape-3 (openRepoShape-3)
Claim: #683 (comment)

Closes #683.

Re-measured on main de926142 (git 2.43.0)

The issue's two cases still hold. The standard is a bare clone of de926142 with uploadpack.allowFilter, and Atlas was scaffolded at bf7b19ba (de926142~30).

  • Online. shape-doctor.py --root Atlas, run from a --no-local --filter=blob:none clone, answers COMPLIANT, SHAPE BEHIND (5 upstream-changed, 0 upstream-added) (exit 1).
    • The objects missing at the pin go from 1255 to 1250 (rev-list --objects --missing=print).
    • A trace2 log shows five lazy fetch origin ... --filter=blob:none --stdin. Their parents are git show bf7b19ba…:<source> for templates/assembly-root/scripts/validate-pins.py, validate-manifest.py, bootstrap.py, scripts/repo_shape.py and scripts/validate-repository-naming.py.
  • Offline. The same clone with remote.origin.url leading nowhere answers DRIFTED (5 both) (exit 1). update-shape.py check --upstream <that clone> prints the same five both rows (exit 1).

Two claims of the issue's needed amending:

  • remote.<name>.promisor = false is not the only key. git 2.43's promisor-remote.c also makes a remote a promisor when remote.<name>.partialclonefilter is set, whatever promisor says.
    • Measured on a blob:none clone: promisor = false plus the filter git's clone writes still fetched (missing 48 → 47).
    • promisor = false alone fetched nothing, and the read failed (exit 128).
    • So the old regexp was right for promisor = false only by accident: it never read the filter key.
  • Upstream.resolve fetches too. rev-parse --verify <sha>^{commit}, prepare's first read of the pinned commit, lazily fetches a commit the clone lacks: 24 trace2 fetch events.
    • rev-list --objects --no-walk --missing=print <sha> exits 128 (bad object) on that commit, with no fetch.
    • So the check has to come before prepare, not only before the blob reads.

What changed and why

shape-doctor.py

  • partial_clone_row (shape-doctor.py:4405). It is asked from check_shape_currency at :4522, after read_pin and before prepare.
    • Where partial_clone holds, objects_lacking (:4378) runs git rev-list --objects --no-walk --missing=print at the pinned commit and at HEAD. That command never fetches, and it runs before any ls-tree.
    • Where either commit lacks an object, or rev-list cannot list a commit (the clone does not hold it), the row is CANNOT ANSWER: detail.environment, exit 3, error: shape-doctor-partial-clone (:4402).
    • The detail holds pinned, standard, remote and lacking ({commit: count}).
    • Where nothing is missing (a partial clone whose pin names its own HEAD), the row answers as it does today.
  • Whole tree, not the copies' paths.
    • --missing=print names a missing object by its id alone. Finding its path means reading the tree that holds it, which a tree:0 clone also lacks.
    • The paths the comparison reads are prepare's to define. A second list of them in the doctor would drift.
    • The cost: a partial clone pinned behind its HEAD is CANNOT ANSWER even where no copy changed. The NEXT ends that.
  • promisor_remotes (:4019), with partial_clone (:3999) now bool(promisor_remotes(shape)). This is git's own reading of its config:
    • a remote whose promisor is true (git config --type=bool, so a bare promisor key counts);
    • a remote with a partialclonefilter;
    • the remote extensions.partialclone names, last, as git asks it.
  • check_shape_currency (:4529). A prepare refusal of a read that failed (update-upstream-unreadable, see below) gets a why of its own and no NEXT.
    • The precedent row's fetch --all and its "does not carry the commit" would both be false there.
    • This branch is reached only from a clone that lacks objects with no promisor remote (promisor = false alone), or from a damaged one.
  • as_the_standard_wrote_it (:4073) now catches Refusal beside OSError and ValueError, so a read Upstream.blob refuses is "not proved", as None was.
    • This is required, not optional: a side's pin naming a commit the checkout lacks now raises from files_at where it used to give None.
    • pin_restore itself is untouched.
  • Module docstring (:27, :318). The doctor fetches nothing, and that is true again. A partial clone is now asked first, and exit 3 now names "a partial clone lacking what the comparison reads".

The NEXT, chosen by the precedent row's rule

The precedent row (shape-doctor.py:4548) gives a checkout short of the pin's commit git -C <shape> fetch --all: the command a person runs to put into THIS checkout what it lacks. Its why names the other way, a clone that has the commit. This row follows the same rule:

  • NEXT: git -C <shape> fetch --refetch --no-filter <promisor remote>.
    • fetch --all would bring no blob, because a promisor remote's fetch keeps its partialclonefilter.
    • --refetch --no-filter measured: 1294 missing → 0, with the config left as it was.
    • Run as printed, the doctor run again gives the full clone's answer. That is measured and tested.
    • It names the remote because fetch --all --refetch --no-filter with two remotes passed neither option on (1294 → 1294).
  • why: "fetch every object holds, then run the doctor again, or run it from a full clone of the standard".
  • Not "none". A CANNOT ANSWER with a working one-line remedy should print it, as the precedent does.
  • Not openRepoShape --doctor <root>.
    • It needs the package installed.
    • It answers against the package's stamped commit, not this checkout's HEAD.
    • From an editable install beside this clone, it runs this same doctor and gets this same row.
  • --refetch needs git 2.36. On an older git the NEXT fails with an unknown option, and the why gives the full-clone way.

update-shape.py

  • Upstream.blob (update-shape.py:393) reads with git cat-file blob <rev>:<path>.
    • That is the same <rev>:<path> and the same bytes as git show for every file, ./x included (measured).
    • Where the read fails, files_at(rev) (:427, one ls-tree -r -z --full-tree per revision, cached, only after a failed read) says whether rev lists a file at that exact path.
      • It does not: None, as before.
      • It does, or the listing itself fails: update-upstream-unreadable (:452, built by unreadable_upstream at :455), quoting git's exit and words.
    • So check and apply refuse in prepare, before any write, rather than reach _classify_row's pinned None (:766) and _row_verdict's both.
  • open_upstream's docstring (:472) no longer says "NOTHING IS FETCHED" of a path. A partial clone's git fetches on a read; this tool fetches nothing into it; a failed fetch refuses.

Every caller of Upstream.blob, checked for changed behaviour:

  • source_for (:664).
    • Absent candidates are still None, so the next candidate is tried.
    • A candidate that exists but cannot be read now refuses. It used to fall through to the next candidate, or to unmapped.
    • A candidate that is a directory is now None. git show used to return its tree listing as "bytes" with exit 0.
  • _classify_row (:794-795).
    • Pinned or target read failed: refuses. It used to be both, or upstream-removed for the target.
    • Absent path: unchanged.
  • upstream_copies (:941, MATERIALIZER). A failed read refuses. It used to give {}, which silently meant "no additions".
  • find_additions (:1199). A failed read refuses. The addition used to be skipped silently.
  • _accepted_in_the_pin (:5013, _finish_landing).
    • A failed read now refuses, before anything is pushed or opened.
    • It used to drop the path from the PR body's accepted list.
    • prepare read the same target in the same run, so this is reachable only through a race.
  • shape-doctor.py's as_the_standard_wrote_it. Refusal → False, as above.

Tested

  • New file tests/test_doctor_partial_clone.py (21 cases). The partial clone is built as tests/test_doctor_pin_restore_merges.py:702-743 builds one: uploadpack.allowFilter, FILE_PROTOCOL, --no-local over file://.
  • "Fetched nothing" is asked of git: rev-list --objects --all --missing=print lists the same objects before and after the run, and lists at least one.
  • A partial clone behind its pin (C2 checked out, root pinned at C1): CANNOT ANSWER, exit 3, shape-doctor-partial-clone, nothing fetched. Four cases: blob:none and tree:0, each with its remote reachable and out of reach.
  • The row's detail and NEXT: lacking == {C1: 1}, remote == "origin", NEXT pasted argv git -C <clone> fetch --refetch --no-filter origin.
  • The NEXT run as printed: the clone then lacks nothing, and the doctor answers COMPLIANT, SHAPE BEHIND (1 upstream-changed, 0 upstream-added).
  • A partial clone pinned at its own HEAD (C1 checked out, C2's blob still missing): the same verdict as the full clone, with nothing fetched.
  • HEAD is asked too: C1 complete, HEAD moved to C2 with no checkout gives lacking == {C2: 1}, with nothing fetched. A mutant that asks only the pinned commit fails it.
  • promisor = false alone, remote reachable: the row is update-upstream-unreadable, NEXT None, exit 3, nothing fetched, no both.
  • update-shape.py check against an unreachable partial clone (blob:none, tree:0): exit 2, REFUSED update-upstream-unreadable: <clone>, no both.
  • partial_clone / promisor_remotes on a full --no-local clone, with config appended as bytes:
  • Upstream.blob on a full clone: a file's bytes, None for an absent path, and None for a directory.
  • Against main's code, the new file fails 17 of its first 20 cases. The 3 that pass are the ones main already got right: pinned at its own HEAD, and a present or absent blob.

Command, run in four groups at once (stub gh first on PATH, GH_TOKEN=bogus, empty GH_CONFIG_DIR):

/usr/bin/python3 -m pytest -q -p no:cacheprovider --basetemp=<scratch>/bt-w683-<group> -o tmp_path_retention_policy=failed <files>

Results:

Pinned copies

None. shape-doctor.py and update-shape.py are not copied into projects, so no project sees an upstream-changed row from this.

Docs

Lands after / before

🤖 Generated with Claude Code

Summary by Sourcery

Handle partial and unreadable standard clones safely by refusing unverifiable comparisons instead of fetching or reporting false drift.

New Features:

  • Detect partial standard clones before comparison and report when required objects are unavailable without fetching.
  • Provide a corrective fetch command for partial clones that need the missing objects.

Bug Fixes:

  • Prevent failed upstream reads from being interpreted as missing files or producing false both copy classifications.
  • Correctly recognize promisor remotes using Git's partial-clone configuration semantics.

Enhancements:

  • Make upstream file reads distinguish absent paths from unreadable files and refuse comparisons when reads fail.
  • Ensure shape currency checks inspect both the pinned commit and the standard checkout's HEAD without fetching.

Documentation:

  • Update shape-doctor guidance and exit-status documentation for partial-clone handling and unreadable upstream data.

Tests:

  • Add coverage for partial clones, promisor-remote configuration variants, non-fetching checks, corrective fetches, and unreadable upstream reads.

…standard, the `shape currency` row asks without fetching whether the clone holds what the comparison reads, and is CANNOT ANSWER with a fetch for a person to run where it does not, and a read that fails is refused, never a copy called `both` (#683)

Measured again on main de92614 with git 2.43: run from a
`--filter=blob:none` clone of the standard, the doctor made five lazy
fetches, one under each `git show` of a copy the standard changed since
the pin (missing objects at the pin 1255 -> 1250), and with the clone's
remote out of reach it said `DRIFTED (5 both)` of copies nobody edited;
`update-shape.py check` printed the same five `both` rows.

shape-doctor.py:
- `partial_clone_row`: where `partial_clone` holds, `objects_lacking`
  asks `git rev-list --objects --no-walk --missing=print` of the pinned
  commit and of HEAD, before `prepare` or any `ls-tree` reads them. Where
  either lacks an object, the row is CANNOT ANSWER (`environment`, exit
  3, error `shape-doctor-partial-clone`), and its NEXT is
  `git -C <shape> fetch --refetch --no-filter <promisor remote>`.
- `promisor_remotes`: git 2.43's own reading of a promisor remote.
  `promisor = false` alone is no longer partial (git fetches nothing
  there), a `partialclonefilter` is (git fetches there whatever
  `promisor` says), and `extensions.partialclone` names one.
- `check_shape_currency`: a `prepare` refusal of a read that failed
  (`update-upstream-unreadable`) gets a `why` of its own and no
  `fetch --all`, which would bring nothing to read it.
- `as_the_standard_wrote_it` takes that refusal as "not proved", as it
  took a None.

update-shape.py:
- `Upstream.blob` reads with `git cat-file blob`, and where that fails,
  `files_at` (one `ls-tree -r` per revision) says whether the revision
  lists a file at the path: None where it does not, a directory included,
  and `update-upstream-unreadable` where it does, so `check` and `apply`
  refuse rather than print `both`.

tests/test_doctor_partial_clone.py: new; nothing is fetched where
`rev-list --missing=print` lists the same objects before and after.

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

sourcery-ai Bot commented Oct 16, 2026

Copy link
Copy Markdown

Reviewer's Guide

Prevents partial-clone lazy fetches and misleading both verdicts by checking required objects before comparison, accurately recognizing Git promisor configuration, and refusing unreadable upstream files with actionable diagnostics; extensive tests cover the new behavior.

Sequence diagram for partial-clone shape currency check

sequenceDiagram
    participant Doctor as shape-doctor.py
    participant Git as Git repository
    participant Person as Person

    Doctor->>Git: config --type=bool --get-regexp
    Git-->>Doctor: promisor remotes
    Doctor->>Git: rev-list --objects --no-walk --missing=print pinned
    Git-->>Doctor: missing-object count
    Doctor->>Git: rev-list --objects --no-walk --missing=print HEAD
    Git-->>Doctor: missing-object count
    alt objects are missing
        Doctor-->>Person: CANNOT ANSWER
        Doctor-->>Person: git -C shape fetch --refetch --no-filter remote
    else nothing is missing
        Doctor->>Git: prepare and compare
        Git-->>Doctor: shape currency verdict
    end
Loading

Sequence diagram for unreadable upstream file refusal

sequenceDiagram
    participant Tool as update-shape.py
    participant Git as Upstream repository
    participant Person as Person

    Tool->>Git: cat-file blob rev:path
    alt read succeeds
        Git-->>Tool: file bytes
    else read fails
        Tool->>Git: ls-tree -r -z --full-tree rev
        alt path is absent
            Git-->>Tool: no file record
            Tool-->>Person: treat as absent
        else path exists or listing fails
            Git-->>Tool: file record or listing error
            Tool-->>Person: REFUSED update-upstream-unreadable
        end
    end
Loading

File-Level Changes

Change Details Files
Detect partial clones before comparison reads and report CANNOT ANSWER without fetching.
  • Identify promisor remotes using Git’s promisor, partial clone filter, and extension configuration semantics.
  • Check all objects reachable from the pinned commit and HEAD with non-fetching rev-list calls.
  • Add structured diagnostics and a remote-specific fetch --refetch --no-filter NEXT command.
  • Keep the existing full-clone behavior when required objects are present.
shape-doctor.py
tests/test_doctor_partial_clone.py
Distinguish missing upstream files from read failures and refuse unsafe comparisons.
  • Read upstream files with git cat-file blob and treat directories or absent paths as None.
  • Cache ls-tree file listings to determine whether a failed read targets an existing file.
  • Raise update-upstream-unreadable for existing files or failed tree listings.
  • Propagate refusals through check, apply, source selection, additions, and accepted-copy handling instead of classifying failures as both or silently skipping them.
update-shape.py
shape-doctor.py
tests/test_doctor_partial_clone.py
Update documentation and regression coverage for non-fetching doctor behavior and failure handling.
  • Document that partial clones are checked before reads and that the doctor itself never fetches.
  • Cover blob:none and tree:0 clones, reachable and unreachable remotes, promisor configuration variants, HEAD checks, remediation execution, and unreadable upstream refusals.
  • Verify Upstream.blob behavior for files, absent paths, directories, and failed reads.
shape-doctor.py
update-shape.py
tests/test_doctor_partial_clone.py

Assessment against linked issues

Issue Objective Addressed Explanation
#683 Prevent the doctor’s shape-currency comparison from triggering lazy fetches in a partial clone; if required objects are missing, report CANNOT ANSWER with a person-run remedy, and otherwise preserve the comparison result. ✅
#683 Distinguish a genuinely absent upstream path from a failed read, so update-shape check/apply and the doctor refuse an unreadable comparison instead of classifying copies as “both.” ✅
#683 Recognize partial clones using Git’s relevant promisor configuration, including the extensions.partialclone alternative, and cover detection and no-fetch behavior with tests. ✅

Possibly linked issues


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

One conflict, in shape-doctor.py's EXIT CODES list, exit 3: this branch
added a partial clone lacking what the comparison reads (#683), and main
added a validator that crashed or cannot be given what it needs (#278,
#410) and a naming policy that does not load (#678). All are kept, in
main's form.

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

Copy link
Copy Markdown

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-10-16T23:32:49Z) Lane 3's independent review of #742 at 3027937d (RV-f, ruling 8): READY. There is no MUST. Two SHOULDs and three NITs go to a v1.1.1 follow-up, and the docs NIT goes to lane 3's README turn. It lands once the Linux tests job at the head is green.

Measured on git 2.43.0. The standard was a bare copy, and Atlas was scaffolded 30 commits back with 5 copies changed since. Fetches were counted twice: by rev-list --missing=print before and after each run, and by trace2 fetch events.

run main 66b230ed head
doctor, blob:none, remote in reach COMPLIANT, SHAPE BEHIND, exit 1; 5 lazy fetches CANNOT ANSWER (… lacks 67 objects at the pinned commit …), exit 3; 0 fetches
doctor, blob:none, out of reach DRIFTED (5 both); 5 failed fetches exit 3; 0 fetches
doctor, tree:0, in reach and out of reach 6 and 11 fetch attempts exit 3; 0 fetches
update-shape.py check, out of reach 5 both rows (blob:none), or git-failed (tree:0) REFUSED update-upstream-unreadable, naming the path or the listing; no both

What else was checked

  • The writer's two amended claims hold: a filter beside promisor = false still fetches, and rev-parse --verify <sha>^{commit} lazily fetches.
  • No doctor path fetches: there are 23 git starts, and none is a fetch.
  • A full clone gives --json identical to main's on five roots.
  • The NEXT, run as printed, gives the full clone's exact answer with one remote, with blob:none and with tree:0.
  • Upstream.blob never refuses a correct full-clone read. That covers a file, a symlink, a spaced name, a non-UTF-8 name, a directory, a gitlink and an absent path. All 666 update-shape tests pass.
  • The merge resolution in 3027937d keeps both sides' EXIT CODES lines.

Mutants: ten of eleven are killed. The survivor:

  • M9: a commit the clone lacks entirely counted as complete. It survives all 21 tests, and on a --single-branch blob:none clone it makes the doctor fetch 6 times. That is S1.

SHOULD, to the v1.1.1 follow-up

  • S1: add a test where the partial clone lacks the pinned commit, so objects_lacking returns None (shape-doctor.py:4448-4451). Assert exit 3 and lacking == {C1: None}.

  • S2: the NEXT names remotes[0] (:4462). It loops in two cases:

    • two promisor remotes with the pin only on the second;
    • a --single-branch partial clone whose pin is on another branch.

    Pick the remote whose tracking refs contain the commit (for-each-ref --contains, which does not fetch), and say in the why when the commit is not in the clone at all. The full-clone precedent row has the same single-branch weakness.

NIT

Tests at the head (stub gh):

Merges: clean with #734, #736, #737 and #740.

Lane: openreposhape-3 (openRepoShape-3)

@sourcery-ai

sourcery-ai Bot commented Oct 17, 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 23 hours and 42 minutes by commenting @sourcery-ai review. Upgrade to get a review now.

@brettheap
brettheap merged commit defc0fa into main Oct 17, 2026
5 checks passed
@brettheap
brettheap deleted the l3-683-partial-clone branch October 17, 2026 00:35
brettheap added a commit that referenced this pull request Oct 17, 2026
… check` says REFUSAL AHEAD beside a merged-away row, so it no longer says that the `apply` `check` prints drops the row when that `apply` is refused, and the `shape currency` counts name every state `check` counts (#676) (#751)

From #460's triage, on Brett Heap's word of 2026-10-16T12:24:41Z ("give #460 to
lane 3"). It lands on his standing word, "standing word covers the #460 PRs"
(12:58:08Z), after #742, on which it was stacked. Closes #676.

The `pins` row named `update-shape.py check` for a merged-away row and said
the `apply` it prints drops that row, even where `check` says REFUSAL AHEAD and
that `apply` is refused. Now:

- The row asks `check`'s own two questions first: a conflict state, and a
  hard-linked rewrite. Where either holds, it keeps the gate as its NEXT and
  says why.
- Where it cannot ask (an unreadable pin, a partial clone short of what
  `check` reads, or a `prepare` that refuses), it keeps the gate. It never
  fetches.
- The `shape currency` counts name every state `check` counts, so no row stands
  under no count.

Lane 3's independent reviewer (RV-f) found it READY. The row agrees with
`check` in every state built, and each printed `apply` was pasted. Its gate on
the squash predicted onto main is in the landing line. Follow-up for v1.1.1:
#755.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 17, 2026
main gained 25 pull requests since de92614: #634, #699, #671, #719,
#701, #717, #700, #702, #722, #720, #725, #721, #728, #705, #727, #718,
#744, #743, #749, #740, #737, #734, #742, #751 and #736. No conflict:
AGENTS.md, adopt-project.py and docs/cli.md merged as text, and
README.md and docs/handbook.html are unchanged by the merge. README.md
is 1473 lines, its cap; AGENTS.md 494, its cap; every handbook marker is
current; `render-cli-reference.py --check` is ok.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 17, 2026
…es every reason THIS checkout cannot answer, and the notes are current to main bf1f099 with the 25 pull requests since de92614 (#563, #683)

- W683 and RV-f's N4 on #742: README's exit-3 list (Keeping a project's
  shape current, :1208-1220) and its handbook twin (`doctor`) named only a
  pin whose commit this checkout does not carry. They now say, as
  shape-doctor.py's EXIT CODES does: a pin's commit it lacks, a partial
  clone missing what is compared (#683), a read that fails
  (`update-upstream-unreadable`), a crashed validator of its own (#278,
  #410), or its own naming policy not loading (#678). +1 line, with
  tests/readme-cap.d/683.txt; README.md is 1474, its cap (1422 + 2 + 5 +
  12 + 22 + 10 + 1). The two `Keeping a project's shape current` markers
  move: `doctor` text and marker, `current` marker only.
- The notes, "What changed since v1.0.1", one entry per pull request from
  `git log --first-parent de92614..bf1f099`, each from its squash commit and
  the issues GitHub says it closes:
  - adopt-project.py: #699, #719, #749, #727, #717, #728 (with #621's
    hint), #743 + #720 + #718, #722, #721;
  - update-shape.py, bump-leg.py and a project's own scripts: #702 (with
    the pinned validators), #634;
  - scaffold, setup, family and the shared readers: #725, #671, #744;
  - shape-doctor.py: #742, #701, #737, #751, #736, #734, #700 + #705,
    #740;
  - install, CI and the docs: the exit-3 sentence above.
- Counts (99 pull requests since bfed143; 109 of 135 milestone issues
  closed at 10:28Z), the "current to main" sha and the open list (26) are
  at main bf1f099, still placeholders for tag time.

Left for round B: lane 5's eight lines (#630 issuecomment-6227129879),
whose pull requests #726, #747, #682, #724, #746, #745 and #738 are open
drafts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 17, 2026
Main moved from 4ce9045 to bf1f099. The only change to update-shape.py on
main in that span is #742 (`Upstream` and `_classify_row`, the partial-clone
currency row); none of #674's functions. No conflicts, so nothing was
resolved by hand, and no pinned copy changed under the files #674 touches.

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 17, 2026
… verification row's colon, `run_validator`'s "costs no child an import" and the decoy test's `sitecustomize` claim say what main does (#666) (#730)

Four comments and docstrings said something main no longer does. They now say what the code beneath them does. A fix round after review added two more of the same kind (see "Fix round"). Comments and docstrings only: no behaviour, no `--help` text, no pinned copy, so no README, handbook or `docs/cli.md` change.

- **`adopt-project.py`, the module docstring.** It said the verification table reads `code: 0 of N source paths (seeded from template)`. The printed row has no colon (`  {leg:<6} {count:>5} of {N} source paths{note}`). It now says the table's `code` row reads `0 of N source paths (seeded from template)`, the words `AGENTS.md` and `README.md` use since #505 and the docstring of the verification function uses too. `git grep 'code: 0 of'` finds nothing now.
- **`shape-doctor.py`, `submodule_config`.** The last sentence of the docstring gave a reason that is true of the string and not decisive: "It is also what the row PRINTS, and a lone surrogate is a string no UTF-8 stream can write." The table goes through `ascii_text` and `--json` through `json.dumps`, so neither would raise on a lone surrogate. I dropped the sentence, the issue's first direction. The comparison with `origin_url`'s reading of a clone's remote is the reason, and the paragraph already gives it first. **This hunk carries the bytes of lane 3's #700**, which landed as `55c6ba7d`: the two removed lines and the one added line (`    working clone.`) are the same. So the hunk is on main already and drops out of the squash (see "Evidence").
- **`shape-doctor.py`, `run_validator`.** "It costs no child an import" holds for an unedited copy only. A new paragraph says a root whose own copy of a validator gained an `import yaml` read `passes` while `site` was on, and now reads `crashed before reaching a verdict (exit 1): ModuleNotFoundError: No module named 'yaml'`, the crash its own CI gets, because `validate.yml` installs nothing. It calls that row a fair answer and says `-S` does not ask whether a copy was edited. No code change.
- **`tests/test_shape_doctor.py`, the decoy test's two comments** (the block above `DECOY_REPO_SHAPE` and the inline comment before `reached`). `run_validator` starts every child as `[sys.executable, "-S", script, ...]`, so no child imports a `sitecustomize`. The comments now say that, say that replace-not-prepend and the #306 review S1 case are held by `test_run_validator_gives_the_child_exactly_the_pythonpath_it_is_given`, and say that the decoy test holds the template row's real verdict under an exported `PYTHONPATH`.

The review of the first two commits approved with nits. Two new commits take S1 and N1: `7f2bb36b` makes both edits, and `426877bf` rewrites the last sentence of the N1 comment, which did not parse ("...a future removal of `-S`, which `test_run_validator_starts_every_child_with_no_site` ... is the test that holds"). It now ends "...a future removal of `-S`; `test_run_validator_starts_every_child_with_no_site` (in `tests/test_doctor_validator_site.py`) is the test that holds `-S`." Both are comments and docstrings only.

- **S1, a third stale sentence.** The docstring of `test_run_validator_gives_the_child_exactly_the_pythonpath_it_is_given` said that for the callers which pass no `pythonpath`, "only a probe like this one, or (b)'s decoy, can see that they have none". That is false under `-S`: a `run_validator` that keeps the variable for those callers passes the decoy test in all three cases and fails the probe test twice (the second row of the table below). It now says only a probe like this one can see that they have none, and that the decoy of (b) could before `-S` (#342). It is the same defect as the issue's, in a place the issue did not name. "Could" holds for the `manifest` and `pins` cases: with `-S` removed, 2 of the 3 decoy cases fail, and the `family` case never caught it.
- **N1, the inline comment overstated the loop.** The comment above `reached` said "What this loop still holds is that no row quotes the decoy or says it exited 86." Under `-S` the loop cannot fire. It now says so, says the loop is kept as a tripwire for a future removal of `-S`, and names `test_run_validator_starts_every_child_with_no_site` in `tests/test_doctor_validator_site.py` as the test that holds `-S`. That test is on main.
- **N2, this body.** The merge checks and the notes on #630 were stale (they cited `de926142` and a conflict that is gone). They are rerun and rewritten below.

**Only comments and docstrings changed.** `git diff 8062810 426877b` has 74 changed lines (44 added, 30 removed) in three files. A script checked each changed line against the file's comment tokens and docstring spans (both sides of the diff): 0 outside. A second check parsed each file at the base and at the head, blanked every docstring, and compared the ASTs: identical in all three files. For the two fix-round commits together, the AST of `tests/test_shape_doctor.py` with docstrings blanked is identical to the one at `a2113cff`, `git diff --check` is clean, and the longest added line is 75 columns (73 in the second commit).

**What the squash holds.** `git merge-tree --write-tree` of this head with origin/main `20c8873b` gives the tree `6ead2ffc`. `git diff 20c8873 6ead2ffc` is 71 changed lines in three files, and `shape-doctor.py` has 8 of them: the new `run_validator` paragraph and nothing else. `git diff` of the two trees has no hunk that mentions `submodule_config`, `surrogate`, `PRINTS` or `working clone`. On main, `submodule_config`'s docstring has no "PRINTS" sentence and ends in `working clone.`. The same two checks on that tree: 0 lines outside comments and docstrings, ASTs identical. On main `2d988200` with the head `426877bf` the squash is the same: 71 changed lines, 8 of them in `shape-doctor.py`, 0 outside comments and docstrings, ASTs identical, and no hunk that mentions `submodule_config`.

**`python3 scripts/render-cli-reference.py --check`**, at `426877bf`: `[ok] docs/cli.md is what the generator produces`. Only the first line of a module docstring is help text, and none of the first lines changed.

**The decoy comments, measured.** I did not take the issue's reading on trust. In a throwaway clone at the base I put each #306 regression into `run_validator` and ran the two tests (`test_an_exported_pythonpath_does_not_reach_a_template_validator`, three cases, and `test_run_validator_gives_the_child_exactly_the_pythonpath_it_is_given`, six):

| mutant | with `-S` (main) | with `-S` removed |
|---|---|---|
| the standard's `scripts/` put in FRONT of the operator's value | decoy test: 3 of 3 pass. Probe test: 1 of 6 fails | decoy test: 3 of 3 fail |
| the variable kept for the callers that pass none (S1) | decoy test: 3 of 3 pass. Probe test: 2 of 6 fail | decoy test: 2 of 3 fail |
| the operator's directory AHEAD of the standard's | decoy test: 3 of 3 fail. Probe test: 1 of 6 fails | not run |

So on main the decoy test catches neither #306 regression and the probe test catches both, as lane 4's R342 measured on #636's head. The third row is why the comment says the raising `repo_shape.py` "still bites": it kills an operator-first order. The review re-ran these mutants on `a2113cff` and got the same counts.

**The `run_validator` paragraph, measured.** A root whose own `validate-manifest.py` is `import yaml` plus a print, run through `run_validator` from this tree: exit 1 and `ModuleNotFoundError: No module named 'yaml'`. The same call with `-S` taken out of the child's argv: exit 0, `manifest ok`. `yaml` is importable from the machine's site-packages and not without `site`.

With the real `gh` unreachable (a stub first on PATH, a bogus token, an empty `GH_CONFIG_DIR`).

On `a2113cff`, before the fix round:

- `tests/test_cli_reference.py`, `tests/test_repo_hygiene.py`, `tests/test_agents_md_codes.py`, `tests/test_scaffold_leg_path_collisions.py`: 250 passed (57.5 s).
- `tests/test_doctor_gitmodules_not_utf8.py`, `tests/test_doctor_validator_rows.py`, `tests/test_doctor_validator_site.py`, `tests/test_doctor_validator_verdicts.py`, `tests/test_adopt_spec_only.py`, `tests/test_adopt_entry_paths.py`, `tests/test_adopt_submodules.py` (the files that drive `run_validator`, `submodule_config` or the seeded-leg row): 334 passed (28 min 18 s).
- `tests/test_shape_doctor.py`, in two halves run side by side (the first 204 test ids, then the last 204): 204 passed in 11 min 31 s, and 201 passed, 3 skipped in 4 min 58 s. That is all 408 tests. The 3 skips are the PowerShell-line tests, which need `powershell.exe` on a Windows host.
- The review ran all of `tests/test_shape_doctor.py` there: 405 passed, 3 skipped (512 s). On the predicted merge with main `2fac9790` it ran `test_shape_doctor.py`, `test_doctor_validator_site.py`, `test_doctor_validator_rows.py`, `test_doctor_validator_verdicts.py`, `test_doctor_gitmodules_not_utf8.py`, `test_repo_hygiene.py`, `test_cli_reference.py` and `test_agents_md_codes.py`: 632 passed, 3 skipped (760 s).

On `426877bf`, after the fix round, which changes comments and docstrings only, so the runs above stand:

- The two tests the fix round touches, `test_an_exported_pythonpath_does_not_reach_a_template_validator` (3 cases) and `test_run_validator_gives_the_child_exactly_the_pythonpath_it_is_given` (6 cases), run by node id: 9 passed (11.7 s). The same 9 also passed (16.1 s) at `7f2bb36b`.
- `tests/test_repo_hygiene.py`: 77 passed (1.9 s). It also passed (2.0 s) at `7f2bb36b`.

No test fails on the base, because nothing here changes behaviour. The AST comparison above is the proof in their place.

`git merge-tree --write-tree` of this head against origin/main and against every ref on origin that could meet it. The full run was on 2026-10-16, with the head `7f2bb36b` (the one before the last comment commit) and main `20c8873b`: the 41 branches (`fix/*`, `l3-*`, `wip/*`, `docs/*` and `release/v1.0`) and the 55 PR heads from #630 up. That is 96 refs. I ran #731's head `ab55c29f` through the same list. After `426877bf` I re-ran main, now `2d988200`, and the six heads that edit `tests/test_shape_doctor.py`, the one file that commit touches (#705, #737, #740, `l3-669-members-row`, `l3-680-doctor-signals` and `l3-697-crash-cure-next`), and #630: all clean.

- **origin/main: clean.** At `20c8873b`, with the head `7f2bb36b`, the predicted tree is `6ead2ffc`. At `2d988200` (three `adopt-project.py` commits later, with their tests and a change in `scripts/shape_materialize.py`), with the head `426877bf`, it is `7ee5caf4`, and the squash is the same three files (43 added, 28 removed). #731's head `ab55c29f` is clean with `2d988200` too. This head with #731's head: clean.
- **#630's head `6fd33a93`: clean.** It conflicted with main in `docs/handbook.html` when this body was first drafted, and no longer does, with or without this change.
- **#700 and #702 have landed** (`55c6ba7d` and `20c8873b`), and so has #701, and #699, #717 and #719 for `adopt-project.py`. Their heads still merge clean with this head.
- **Every ref that edits `shape-doctor.py`, `tests/test_shape_doctor.py` or `adopt-project.py` merges clean with this head**, except the two old heads named in the bullet after next. They include the heads of PRs not on main yet (#705, #721, #737 and #742, and lane 3's `l3-680`, `l3-683`, `l3-687`, `l3-691` and `l3-697`, which were not on origin when this body was first drafted). #673 has no branch or PR head on origin.
- **Four refs conflict with origin/main on their own**, and so with this head: `docs/release-notes-v1.0.0` and `release/v1.0`, and the pre-squash heads of the landed #637 and #641. Their conflicts do not come from this change.
- **Two landed pre-squash heads conflict with this head only because they are old.** #632 conflicts with main on its own. #636 merges clean with main, but its old head conflicts with this head in `run_validator`'s docstring, next to the text #636 added; main has that text, and this head merges clean with main.
- **Refs that conflict with main on their own are not made worse.** #682, #745, #747, `fix/648`, `fix/681` and `fix/704` conflict with main in `templates/family-root/scripts/validate-family.py`; #742 and `l3-683` in `shape-doctor.py`. Each merges clean with this head.
- **Every other open head and branch: clean.**

Nothing from the issue. Its four lines are done. The decoy test's `sitecustomize` machinery is dead code under `-S` and I left it, because the issue says comment only. The comment above `reached` now says it is kept as a tripwire.

- **The decoy test's `sitecustomize.py` and exit-86 trap never run.** `DECOY_SITECUSTOMIZE`, `os._exit(86)` and the `(exit 86)` and `DECOY` clauses in `reached` can only fire for a child started without `-S`. They are kept as a tripwire and the comment says so. The other choice is to remove them; `test_run_validator_starts_every_child_with_no_site` already holds `-S`. Small, tests only.

Review: rv666, lane 5's independent review, approved a2113cf with nits; 7f2bb36 and 426877b take its S1 and N1 (above), and codex found no major issue at a2113cf. CI (Brett Heap's ruling 27 and his fast-refresh ruling of 2026-10-15T22:48:35Z): H0 426877b passed tests, tests-git-bash and SonarCloud; the head 32722b3 is a clean update-branch merge of main bf1f099, with tests-git-bash and SonarCloud green; and the local gate passed on its predicted squash tree 55d5d3b over main dcf62ed: the shape_doctor, scaffold_leg_path_collisions, repo_hygiene, agents_md_codes, cli_reference sets, 686 passed, 3 skipped, 1 xfailed. Landed on Brett Heap's word of 2026-10-17T10:59:39Z, relayed by lane 3 on #473: "merge lane 5's approved v1.1.0 drafts".

Closes #666

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 17, 2026
…d A3 (three SHOULDs, four nits), and notes entries for the seven pull requests landed since bf1f099, current to main 2000488 (#563, #683)

Round A4, from lane 2's review (#630 issuecomment-6233033833, READY):
- SHOULD 1: the doctor's exit-3 list names "a validator of its own that
  crashed or could not be run", so the case of one that cannot be given
  its `PYTHONPATH` (shape-doctor.py's EXIT CODES, #278) is listed too:
  README.md:1208-1222, its handbook twin (`doctor`) and the notes' docs
  line. The paragraph re-wraps to one more line:
  tests/readme-cap.d/683.txt is +2, and its text now says which causes
  the EXIT CODES block names and that #742 added the partial clone and
  #701 the naming policy. README.md is 1475 lines, its cap (1422 + 2 + 5
  + 12 + 22 + 10 + 2). Both `Keeping a project's shape current` markers
  move to c728c970: `doctor` text and marker, `current` marker only.
- SHOULD 2: #727's entry names the leg description, whose em dash stopped
  every `execute` against GitHub at the first leg.
- SHOULD 3: #749's entry names `source-ambiguous`, for an `org/repo` that
  is also a directory here.
- Nits: the doctor section's three near-duplicate pairs are merged (#636
  with #737, #632 with #700 and #705, #586 with #736); #718 has its own
  bullet; #634's entry names `git_out`'s spelling for every caller and
  #744's the gate's `--link-source-label` and `--link-remedy`; the header
  comment says 2026-10-17 at main 2000488.

Wave B1, `git log --first-parent bf1f099..2000488`: #706 (#664), #703
(#696), #670 (#646), #682 (#648), #726 (#667), #730 (#666) and #724
(#665), each from its squash commit; #726's and #682's from lane 5's lines
(#630 issuecomment-6227129879), #682's with the unmerged `.gitmodules`
its last round added, and #724's with lane 5's note that v1.0.0's
test-hygiene line on the generator's `.pyc` files closes. The counts (106
pull requests; 116 of 135 milestone issues closed at 11:53Z), "current to
main" and the open list (19) are at main 2000488, still placeholders.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 17, 2026
…efusals and `family-operation-in-progress` name a path as `shown_source` names it, so an ESC or a line break in a path no longer reaches the terminal (#692) (#731)

#622's folder refusals printed a path as it was. `family.py init --into <a file named wd-ESC[31mRED> --dry-run` gave `REFUSED family-folder-cannot-be-made: the family folder .../wd-^[[31mRED/Orbit cannot be made: ...` through `cat -v`. A name with a line break and then `WARNING forged-line: pwned` printed that line as a line of its own. R612's `family-operation-in-progress` printed the mark's path across two lines for a holder whose path holds a newline.

Each path is now spelled as `shape_materialize.shown_source` spells a `--pin-source`: as it is, and as Python's `repr` spells it where a character in it is not printable.

- `scripts/shape_materialize.py`: both answers of `folder_cannot_be_made` (a file or a bad stat in the way, and a folder this user cannot write in), the folder in `refuse_unmakeable_folder`, and the folder in `make_folder`.
- `scaffold-project.py`: `scaffold-target-exists`.
- `scripts/family.py`: `family-target-exists`, and the mark's path in `family-operation-in-progress` (`where.as_posix()`).

A printable path is named byte for byte, so no existing row changes and `why.endswith(CANNOT_WRITE_IN)` in `refuse_unmakeable_folder` still holds. No help text changed, so `docs/cli.md` is untouched. No README, handbook or template sentence says how a refusal names a path (`git grep` of `shown_source`, `target-exists` and `folder-cannot-be-made` in AGENTS.md, README.md, `docs/handbook.html` and the templates found none), so no doc changes.

- **`shown_source`, not `shown_path`.** Since #634, `shape_materialize.shown_source` returns `repo_shape.shown_path(source)` (`scripts/shape_materialize.py`: `return shown_path(source)`), so the seven call sites this change adds (four in `shape_materialize.py`, one in `scaffold-project.py` and two in `family.py`) spell a path as `shown_path` does. Renaming them to `shown_path` stays out of this PR: it would only move imports in `scaffold-project.py` and `family.py` and reword docstrings in `family.py`, `shape_materialize.py` and the test file, and the review's N1 left it optional. I did not take it. The predicted merge with main works with the alias: `family.py --help` and `scaffold-project.py --help` exit 0 on it, and `shown_source` and `shown_path` return the same string for an ESC name, a line-break name and a plain path.
- **`shown_source` is imported by name into each file's existing `from shape_materialize import (...)` block.** The first commit put it on a line of its own after the block; lane 4's #660, #688 and #693 add an import at that same place, and `merge-tree` reported add/add conflicts in `scripts/family.py` and `scaffold-project.py`. The second commit moves the name into the block, and those conflicts are gone.
- **"The holder in the other refusals of the same block" is one refusal.** The `# What the holder is in the middle of, and what its sparse checkout leaves out` block holds `family-operation-in-progress`, `family-sparse-excluded` and `member-path-unaddable`. Only the first prints a path of the holder's. `family-sparse-excluded` names `members/<Project>` and `.gitmodules`, and `member-path-unaddable` names `members/<Project>`.
- **No scaffold row for `--local-remote-dir`.** A control character there is refused earlier as `scaffold-unwritable-value`, which already names the path with `repr`. The unit test covers `make_folder`, which only a race reaches by the command line.

`tests/test_dry_run_parity.py`:
- `SCAFFOLD_CASES` gains five rows: ESC and line break in the `--work-dir` name, ESC in a folder name above it, and ESC and line break in a work dir that holds a file named `Atlas` (`scaffold-target-exists`).
- `FAMILY_CASES` gains five rows: ESC and line break in `--work-dir`, ESC in `--into` (`the family folder`), and ESC and line break in a work dir that holds a file named `Orbit` (`family-target-exists`).
- Two rows join `UNWRITABLE_FOLDER_AT` (ESC in the scaffold's and in `init`'s `--work-dir`, a folder this user cannot write in).
- Rows named `ESC-...` and `line-break-...` are skipped on Windows, whose file names cannot hold them (`POSIX_NAMES`, `case_params`). Every expectation spells the path with `repr`, and not with `shown_source`.
- `assert_refused` now fails any stderr line that starts with `WARNING forged-line`.
- Three unit tests: `test_each_folder_question_spells_a_name_that_is_not_printable` (ESC, line break; `folder_cannot_be_made`, `refuse_unmakeable_folder` and `make_folder` by themselves), `test_a_folder_this_user_cannot_write_in_is_spelled_safe_to_show`, and `test_a_printable_name_is_named_byte_for_byte` (a name with a space and an accented letter stays unquoted).

`tests/test_family_refuse_up_front.py`: `test_a_holder_path_with_a_newline_still_shows_its_merge` asserted the raw newline in the refusal (`"Ink\nRouter/.git/worktrees/" in ran.stderr`). It is now `test_a_holder_path_with_a_control_character_still_shows_its_merge`, over a line break, an ESC and a forged line, and asserts the one-line `repr` form (`refusal()` fails unless the stderr is two lines). It is the one existing test this change had to edit.

Results at head `ab55c29f` (Python 3.12.3, Linux):
- `tests/test_dry_run_parity.py tests/test_dry_run_asks_remotes.py tests/test_family_dry_run.py tests/test_family_init_landing.py tests/test_repo_hygiene.py tests/test_pin_source_unreadable.py tests/test_scaffold_leg_path_collisions.py tests/test_scaffold_reuse_seeded_legs.py`: **494 passed**.
- `tests/test_family.py tests/test_family_refuse_up_front.py tests/test_family_init_values.py tests/test_family_add_refusals.py`: **356 passed, 19 skipped** (the skips are `test_family_init_values.py:219` and `:291`, template-quoting premises).
- Not run: the full suite (the coordinator's gate), and macOS and Windows (CI is Linux-only since #597). The new hostile-name rows are POSIX only by design.

On the base code: the review overlaid this head's two test files on main `2fac9790` and ran `tests/test_dry_run_parity.py` and `tests/test_family_refuse_up_front.py` with `-k "dry_run_parity or control_character"`. The result is **20 failed, 87 passed** (76 deselected, 43.5 s). The 20 are exactly the new and edited tests: the 5 scaffold and 5 `init` rows, the 4 unwritable-folder rows, the 3 unit tests (`test_each_folder_question_spells_a_name_that_is_not_printable` in two cases, and `test_a_folder_this_user_cannot_write_in_is_spelled_safe_to_show`), and the 3 cases of the edited `test_a_holder_path_with_a_control_character_still_shows_its_merge`. The one new test that passes on main's code is `test_a_printable_name_is_named_byte_for_byte`, by design: a printable name is named byte for byte on both. An earlier draft of this body said 21 failed, 2 passed. That run was on `8062810d` with a wider selection, and it held a scaffold row for ESC in the local remote dir's name (`ESC-in-the-local-remote-dir-name`) that this head's tests no longer have; its second pass was the existing `test_the_refusal_quotes_gh_s_first_line_and_escapes_what_is_unprintable`, which also passes in the new run, one of the 87.

`family.py init` and `scaffold-project.py --dry-run` against a file named `wd-ESC[31mRED`, a file named `wd-\nWARNING forged-line: pwned`, and a work dir with a file `Atlas` or `Orbit` in it, run with a stub `gh` and no real `gh`:
- On main, through `cat -v`: `... the family folder /.../wd-^[[31mRED/Orbit cannot be made: /.../wd-^[[31mRED is a file, not a folder`, and a stderr line `WARNING forged-line: pwned cannot be made: ...` of its own.
- On this head: `REFUSED family-folder-cannot-be-made: the family folder '.../wd-\x1b[31mRED/Orbit' cannot be made: '.../wd-\x1b[31mRED' is a file, not a folder`, and one line for the forged name: `--work-dir '.../wd-\nWARNING forged-line: pwned' cannot be made: '...' is a file, not a folder`. `scaffold-target-exists` and `family-target-exists` are one line each in the same form.

Each spelled site reverted alone in a throwaway clone at `19d126cd` (the call sites are the same at `ab55c29f`); the new tests that fail for each:
- `folder_cannot_be_made`, the `_not_a_folder` answer: 8 failed (six CLI rows and the two unit tests).
- `folder_cannot_be_made`, the `CANNOT_WRITE_IN` answer: 5 failed (the four unwritable rows and the unit test).
- `refuse_unmakeable_folder`: 12 failed.
- `make_folder`: 2 failed (the unit test only).
- `scaffold-target-exists`: 2 failed (its two rows).
- `family-target-exists`: 2 failed (its two rows).
- `family-operation-in-progress`: 3 failed (line break, ESC, forged line).

`git merge-tree --write-tree` of this head `ab55c29f` against origin/main `20c8873b` and against every ref on origin that could meet it, fetched 2026-10-16: the 41 branches (`fix/*`, `l3-*`, `wip/*`, `docs/*` and `release/v1.0`) and the 55 PR heads from #630 up. That is 96 refs. (The same list was run for #730's head `7f2bb36b`.)
- **origin/main `20c8873b`: clean** (the predicted tree `00b5b24c`, which differs from main in these five files only: 216 lines added, 25 removed).
- **#630's head `6fd33a93`: clean.** It conflicted with main in `docs/handbook.html` when this body was first drafted, and no longer does, with or without this change. This change touches no README or handbook.
- **The refs that edit one of this change's five files**, and so could meet it, all merge clean with this head: `fix/646`, `fix/651`, `fix/654`, `fix/660`, `fix/661`, `fix/684`, `fix/686`, `fix/688`, `fix/689` and `fix/693`, the heads of #670, #725, #728, #743 and #744, and the heads of the landed #634, #638, #639, #640, #643 and #671. #634 has landed (`30676c90`), so the `fix/660` note about a conflict with it is gone.
- **Four refs conflict with origin/main on their own**, and so with this head: `docs/release-notes-v1.0.0` and `release/v1.0`, and the pre-squash heads of the landed #637 and #641. Their conflicts do not come from this change. Every other ref, 92 of the 96 in all, is clean with this head, including the refs that conflict with main in `templates/family-root/scripts/validate-family.py` (#682, #745, #747, `fix/648`, `fix/681`, `fix/704`) or in `shape-doctor.py` (#742, `l3-683`) on their own. So do the old heads of the landed #638 and #639, and lane 3's `l3-677-pins-hand-step-wip` branch, and each of them merges clean with this head too.
- **Tests on the predicted merge**, run by the review on the merge with main `2fac9790`: `test_dry_run_parity`, `test_family_refuse_up_front`, `test_pinned_copy_text`, `test_pin_source_unreadable`, `test_dry_run_asks_remotes`, `test_family_dry_run` and `test_family_init_landing`: 445 passed in 215 s. `test_family`, `test_family_init_values`, `test_family_add_refusals`, `test_derived_id_refusals`, `test_repo_hygiene`, `test_naming_policy`, `test_scaffold_leg_path_collisions` and `test_scaffold_reuse_seeded_legs`: 758 passed, 19 skipped in 243 s. I did not re-run them on `20c8873b`; the merge is clean, and the two commits since `2fac9790` (#700 and #702) change only `shape-doctor.py`, `templates/assembly-root/scripts/validate-pins.py` and four test files, none of them a file of this change.
- Before the second commit, `fix/660`, `fix/688` and `fix/693` each conflicted with this head in the import region of `scripts/family.py` or `scaffold-project.py` (a separate import line each side added at the same place). That is why the import joins the existing block.

#634 is on main, and this change does not edit `shown_source` or its docstring in `scripts/shape_materialize.py`.

The review approved with nits. Its S1, the stale base-run numbers and merge lines, is taken here, in this body: no code changed, and the head is still `ab55c29f`. Its N1, renaming the call sites to `shown_path`, is not taken (see "Decisions and their evidence"). It also re-ran 5 of the 7 mutation probes on `ab55c29f` and got the same failure counts as above (12, 2, 3, 2 and 2), and ran 144 hostile-name dry runs (16 names in 9 scenarios): on main 69 rows put a control character on stderr and 6 print a forged line of their own; on this head all 144 exit 2 with no traceback, no control character and no forged line.

- `scripts/family.py` `init`'s plan prints `folder` and `landing` lines with the path raw (`print(f"landing      {holder}   ...")`, `print(f"folder       {folder}   ...")`). On a dry run or a refused run the plan is on stdout before the refusal, so `WARNING forged-line: pwned/Orbit   (--work-dir: no family folder)` still appears there. It is the plan, not a refusal, and not one of the issue's lines.
- Other refusals that name a holder or folder path raw, none of them in the issue's lines: `family-holder-detached` and `family-on-tracking-branch` (`{family.root}`), `family-index-locked` (`lock.as_posix()` in the detail), `family-landing-in-standard` (`{holder}`, `{checkout}`, `{beside / family / family}`), `family-holder-already-here` (`{into}`), and `family-remote-exists` and `scaffold-remote-exists` (`{target}`, a local remote path).

1. **family.py `init`'s plan prints the holder and folder paths raw.** `scripts/family.py` `landing` and `folder` rows (about :1756-1759) print `holder` and `folder` as they are, so a forged line or an ESC reaches stdout in a `--dry-run` and in a run refused after the plan. Direction: spell them with `shown_source`.
2. **The same raw spelling in the refusals listed above.** `family-holder-detached`, `family-on-tracking-branch`, `family-index-locked`, `family-landing-in-standard`, `family-holder-already-here`, `family-remote-exists` and `scaffold-remote-exists` each print a path the person supplied or git keeps. Direction: the spelling this change gives its sites.

Review: rv692, lane 5's independent review, approved ab55c29 with nits, and its body fix (S1) is published. CI (Brett Heap's ruling 27 and his fast-refresh ruling of 2026-10-15T22:48:35Z): H0 ab55c29 passed tests, tests-git-bash and SonarCloud; the head 13f643d is a clean update-branch merge of main 431c8cf, with tests-git-bash and SonarCloud green; and the local gate passed on its predicted squash tree 4327947 over main 431c8cf: the dry_run_parity, family_refuse_up_front, repo_hygiene, agents_md_codes, cli_reference sets, 330 passed. Landed on Brett Heap's word of 2026-10-17T10:59:39Z, relayed by lane 3 on #473: "merge lane 5's approved v1.1.0 drafts".

Closes #692

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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