Skip to content

shape_advisory.py and shape-doctor.py: a leg's AGENTS.md this user cannot read leaves whether it is a leg unknown, so the way in offers neither command and points at what is here, and the other way in and the Triad row are silent (#1110) - #1159

Merged
brettheap merged 15 commits into
mainfrom
fix/1110-leg-agents-md-unreadable
Nov 8, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Part of #1110: its twelfth site, scripts/shape_advisory.py _leg_by_agents_md.

Was stacked on #1144, which squash-landed on main as b1f45fa7 on 2026-11-08. GitHub retargeted this PR to main, and the branch is restacked on main by three merges (see ## Restacked after #1144 landed).

Outside v1.2.0. Brett Heap ruled at 2026-10-21T10:32:04Z: "leave the follow-ups out of v1.2.0". This PR lands after the v1.2.0 tag, on a later word.

66e68f94 changes one docstring in tests/test_doctor_unsearchable_links.py and nothing else (r1159b's NIT-B); every figure measured at 0ca33611 carries.

Restacked after #1144 landed

#1144 squash-landed on main as b1f45fa7. This branch is restacked on it by three merges, with no rebase and no force-push:

merge commit parents tree
1. #1144's final head 5b38c3a3, update-branch's merge of main bd3e9c14 into 89486f69 9d93487d 66e68f94, 5b38c3a3 c5d0bc70
2. #1144's squash b1f45fa7, ours for its four files ab31e2e5 9d93487d, b1f45fa7 c5d0bc70, merge 1's
3. main 0d5704b3 (#1169, #1223, #1166, #1133, #1104) 5370b018 ab31e2e5, 0d5704b3 fd53e4c0

The gate at 5370b018 (3.12, the probe environment, one basetemp per call):

call result
tests/test_doctor_unsearchable_links.py, tests/test_shape_advisory.py 96 passed (49 and 47)
tests/test_path_guard_hygiene.py, tests/test_no_auto_maintenance.py, tests/test_scaffold_leg_path_collisions.py 283 passed, 2 skipped (121, 60 and 104 collected)
tests/test_agents_md_codes.py, tests/test_cli_reference.py, tests/test_repo_hygiene.py 171 passed (47, 23 and 101)

The 2 skips are tests/test_no_auto_maintenance.py's, at line 222: "the session began with GIT_CONFIG_GLOBAL set, and its file includes that one". The probe environment sets GIT_CONFIG_GLOBAL=/dev/null.

What

On #1144's head, _leg_by_agents_md read any exception as "no leg's file" (except Exception: return None). The fixtures are clones of acme/Orbit whose AGENTS.md is the spec leg's or the code leg's file, rendered from its template, in two forms:

  • link: the working-tree file is a link into a folder at mode 000;
  • mode: the file itself is at mode 000.

Over either form, the doctor offered adopt-project.py plan ... --project Orbit in the way in and setup.sh in the other way in, and its Triad advisory said "This is a single repository". This held on 3.12 and on a 3.14-like shim, where Path.is_file, exists and the rest answer False to any OSError.

Base 89486f69 against head 0ca33611: shape-doctor.py --root <fixture>/Atlas, 32 runs, made at 0f9aff05. Re-run at 0ca33611, the 16 head reports are byte-identical to 0f9aff05's, paths normalised. In --json, the locked fixtures' the way in row now keeps origin_org: "acme" where 0f9aff05 gave null (4 of 4: spec and code, link and mode), and nothing else differs.

fixture (spec and code leg alike) base head
link, locked what is here names AGENTS.md: cannot be read (Permission denied). the way in gives NEXT adopt-project.py plan, the other way in gives NEXT setup.sh, and an advisory Triad row says "This is a single repository". Exit 2. what is here names it, with the cure. the way in gives the reason below and no NEXT. There is no the other way in row and no Triad row. Exit 2.
mode, locked what is here says a git repository, an AGENTS.md. The adopt, scaffold and Triad rows are as above. Exit 2. The same as link, locked.
readable (link and mode) NAMED OR WRITTEN AS A LEG. The Triad row says "this is a clone of the R leg". Byte-identical to base, paths normalised (8 of 8).

The new the way in reason: its AGENTS.md cannot be read, so it is not known whether this is a clone of a Triad's leg, which is not adopted or scaffolded on its own, and neither way in is offered until it can be: what is here says how.

At 0f9aff05, every report on 3.12 is byte-identical to its report under the shim (32 of 32). With paths normalised, the code leg's locked reports equal the spec leg's (16 of 16). The readable ones differ only in the leg's name.

In process (leg_clone and shape_advisory.read(), 3.12 and shim):

AGENTS.md base head
locked read() is advise and leg_clone is False read() is not-answered, with why AGENTS.md: cannot be read (Permission denied), so whether this is a leg clone is not known here, and the advisory says nothing, and leg_clone raises PermissionError
readable leg-clone, True leg-clone, True

adopt-project.py plan and check: 64 runs at 0f9aff05, equal on base and head, 3.12 and shim. Where git cannot give a commit's AGENTS.md, a folder named as a leg still gets the leg's note, as on base; with no such name, plan and check print no advisory, where base printed the advice. No fixture meets this. Adopt reads the commit's AGENTS.md, never the disk's, so a locked working-tree file changes nothing there. adopt-project.py is not changed.

The whole doctor report: spec leg, link, locked, 3.12

<standard> is the checkout and <fixture> the fixture's folder.

Base 89486f69:

root        <fixture>/Atlas
kind        not a shape root
standard    <standard>

  ok        naming            Atlas, Orbit: classifies under the naming policy
  n/a       what is here      AGENTS.md: cannot be read (Permission denied); a git repository
            NEXT              python3 <standard>/shape-doctor.py --root <fixture>/Atlas
            WHY               Make what this user may not open readable by this user: `chmod u+r` on a file, `chmod u+rx` on a directory (and on every directory above it inside the root), or fix who owns it. Then run this doctor again
  n/a       the way in        this is a git repository with no shape manifest: it is ADOPTED in place, keeping its name, identity and history
            NEXT              python3 <standard>/adopt-project.py plan --source <fixture>/Atlas --project Orbit
  n/a       the other way in  a NEW project is SCAFFOLDED instead of adopting this repository: it creates three repositories and asks first
            NEXT              <standard>/setup.sh --org <your-org> --project Orbit
            WHY               replace <your-org> with the GitHub organisation to create it in; without --yes, it asks
  advisory  Triad             This is a single repository. The Triad -- an assembly root with a spec leg and a code leg -- is the preferred project shape; preferred, not required: it confers nothing, and a single repository is reviewed identically. openRepoShape's adopt-project.py converts a repository in place when a person deciding for this project runs it, and a project that stays single can say so in single-repository.yaml. Nothing here changes; work continues.
  n/a       machine           this machine is missing something the fixes above may need (setup-project.py --preflight, exit 1): gh is not authenticated
            NEXT              python3 <standard>/setup-project.py --preflight
            WHY               it names each missing prerequisite and offers to install it, on a typed yes

NOT A SHAPE ROOT   (exit 2)
the machine check is `openRepoShape --preflight` now; `--doctor` is this report
exit 2

Head 0ca33611 (and 0f9aff05, byte for byte):

root        <fixture>/Atlas
kind        not a shape root
standard    <standard>

  ok   naming        Atlas, Orbit: classifies under the naming policy
  n/a  what is here  AGENTS.md: cannot be read (Permission denied); a git repository
       NEXT          python3 <standard>/shape-doctor.py --root <fixture>/Atlas
       WHY           Make what this user may not open readable by this user: `chmod u+r` on a file, `chmod u+rx` on a directory (and on every directory above it inside the root), or fix who owns it. Then run this doctor again
  n/a  the way in    its AGENTS.md cannot be read, so it is not known whether this is a clone of a Triad's leg, which is not adopted or scaffolded on its own, and neither way in is offered until it can be: `what is here` says how
  n/a  machine       this machine is missing something the fixes above may need (setup-project.py --preflight, exit 1): gh is not authenticated
       NEXT          python3 <standard>/setup-project.py --preflight
       WHY           it names each missing prerequisite and offers to install it, on a typed yes

NOT A SHAPE ROOT   (exit 2)
the machine check is `openRepoShape --preflight` now; `--doctor` is this report
exit 2

For mode, locked, base differs only in what is here, which says a git repository, an AGENTS.md with no NEXT or WHY. Head's report is the same as for link.

Why

Tests

tests/test_doctor_unsearchable_links.py gains 24 rows, for 49 in all:

  • 3 mode rows: 2 over a link into a folder at mode 000, 1 over a file at mode 000;
  • 2 whole runs of the doctor, given 120 seconds each (doctor_within), over an AGENTS.md that is a directory and one that is a named pipe. A run that opens the pipe fails the row instead of hanging the suite. The pipe row skips where os.mkfifo is absent, and nothing opens the pipe in the test's own process;
  • 19 in process.

Two of #1144's rows change:

  • test_the_doctor_reads_the_advisory_as_silent now expects PermissionError from leg_clone over a refused .git, where it said False;
  • test_the_advisory_names_a_git_it_cannot_reach_and_says_nothing also checks type(reading.refused) is type(error).
run base 89486f69 (new file laid over) head 0ca33611
3.12 17 failed, 32 passed 49 passed
3.14-like shim 17 failed, 32 passed (the same 17 tests) 49 passed

Every base failure is on the row's own assertion:

  • an extra way-in-scaffold or triad-advisory row (7);
  • DID NOT RAISE (4);
  • 'Reading' object has no attribute 'refused' (3);
  • 'advise' == 'not-answered' (2);
  • what is here saying an AGENTS.md for a file at mode 000 where there is no repository, 'an AGENTS.md' == 'AGENTS.md: cannot be read (Permission denied)' (1).

The new controls pass on base:

  • the leg by its name, where the disk's AGENTS.md is refused and where git cannot give the commit's;
  • an AGENTS.md that is not there (dangling, gone before it is read, loop);
  • an AGENTS.md that is a directory or a named pipe: listed, not read;
  • a Makefile whose read is refused: listed like any other name;
  • the not-UTF-8 decision rows.

Two of this round's changes are pinned by rows that fail without them:

  • the row for a leg by its name where git cannot give its AGENTS.md fails on 0f9aff05 (not-answered);
  • with origin_org back to None, the 7 rows that use no_leg_way_in fail (7 failed, 42 passed).

Mutants: 31, 28 killed. The 3 survivors are those the review accepted as equivalent. Each ran alone, in a clone, over tests/test_doctor_unsearchable_links.py on 3.12, and the file's bytes matched its HEAD blob after each one. N2, and M3 over its named-pipe row, ran at 0ca33611. The rest ran at e356013b, which differs from 0ca33611 only by one assertion in N2's row, a row no other mutant fails.

id file mutant rows failed
A1 scripts/shape_advisory.py refused read answers None 12
A2 scripts/shape_advisory.py an absent errno raises too 1
A3 scripts/shape_advisory.py the broad except put back 13
A4 scripts/shape_advisory.py bytes that are not UTF-8 go to read()'s net 2
A5 scripts/shape_advisory.py anything else is no leg's file 1
A6 scripts/shape_advisory.py no turn to the name 1
A7 scripts/shape_advisory.py a refused AGENTS.md falls through to the advice 12
A8 scripts/shape_advisory.py refused not set for AGENTS.md 11
A9 scripts/shape_advisory.py refused not set for .git 3
A10 scripts/shape_advisory.py the exception's name for the system's reason 7
D1 shape-doctor.py leg_clone does not raise 8
D2 shape-doctor.py the way in falls through to the adoption 7
D3 shape-doctor.py the AGENTS.md row said as the .git one 7
D4 shape-doctor.py the other way in offers the scaffold over it 7
D5 shape-doctor.py what is here does not read through 4
D6 shape-doctor.py read_through raises a read that finds nothing 1
D7 shape-doctor.py read_through only asks the stat 4
D8 shape-doctor.py what is here reads through only in a repository 1
M3 shape-doctor.py read_through reads any kind of file 2
M8 shape-doctor.py the AGENTS.md row gives git_repository None 7
M9 scripts/shape_advisory.py _cannot_be_read uses str(exc) 7
M10 shape-doctor.py read_through only opens the file 3
M14 shape-doctor.py what is here reads through every name 1
M16 scripts/shape_advisory.py _leg_by_agents_md also catches ValueError survived: equivalent, since UnicodeDecodeError is the only ValueError text() can raise
M18 shape-doctor.py the other way in asks leg_clone first survived: equivalent, since leg_clone raises a .git it cannot read as carries_git does, and is False where there is none
M18b shape-doctor.py the way in asks leg_clone first 3
M19 scripts/shape_advisory.py _nothing_there drops the Windows arm survived: Windows only, and CI is Linux only
N2 scripts/shape_advisory.py a Refusal skips the name again (0f9aff05's _read) 1
N2b scripts/shape_advisory.py a Refusal with no leg's name falls through to the advice 1
N5 shape-doctor.py the AGENTS.md row drops origin_org again 7
N5b shape-doctor.py way_in_unread_row ignores org 7

A1-A10 and D1-D8 are the writer's first 18. M3-M19 are the review's (r1), and N2-N5b are this round's. M3 fails both of its rows: the directory row on its assertion, and the named-pipe row on its timeout, Failed: the doctor did not end within 120 seconds over .../Atlas: something opened its AGENTS.md, after 126 s. The blocked doctor is killed, and no doctor process is left. A9 is caught only by #1144's two rows, which this change extends: no brand-new row catches it.

The gate, call by call (3.12 unless named, the probe environment, one basetemp per call, at 0ca33611):

call result
tests/test_doctor_unsearchable_links.py 49 passed
tests/test_doctor_unsearchable_links.py, shim 49 passed
tests/test_shape_advisory.py, tests/test_path_guard_hygiene.py 96 passed (47 and 49)
tests/test_shape_doctor.py, shard 1 of 4 112 passed
tests/test_shape_doctor.py, shard 2 of 4 112 passed
tests/test_shape_doctor.py, shard 3 of 4 112 passed (215.6 s)
tests/test_shape_doctor.py, shard 4 of 4 109 passed, 3 skipped (82.0 s)
tests/test_doctor_next_is_a_command.py (names check_the_way_in) 61 passed (79.5 s)
tests/test_adopt_single_repository_record.py, test_bom_reads.py, test_bootstrap_non_utf8_branch.py, test_doctor_gitmodules_not_utf8.py, test_doctor_unreadable_paths.py, test_doctor_validator_verdicts.py, test_repo_hygiene.py (the rest of the files that name shape_advisory) 290 passed (162.4 s)

Known limits

Not in this PR

Lane: openreposhape-1 (openRepoShape-1)

🤖 Generated with Claude Code

Summary by Sourcery

Prevent the doctor from offering adoption or scaffolding when an unreadable AGENTS.md leaves a repository's Triad-leg status unknown.

Bug Fixes:

  • Treat unreadable leg AGENTS.md files as unknown rather than as evidence that a repository is not a Triad leg, preventing inappropriate adoption and scaffolding suggestions.
  • Report unreadable AGENTS.md files in what is here with actionable permission details while preserving leg detection by directory name.
  • Keep unreadable repository and leg metadata reads silent in unrelated advisory rows and propagate the underlying refusal when callers need to explain it.

Enhancements:

  • Refine advisory read outcomes to distinguish absent files, invalid UTF-8, and access refusals, including carrying refused operating-system errors through the reading model.
  • Add regular-file-only read-through behavior for AGENTS.md and preserve existing behavior for directories, named pipes, missing files, and non-UTF-8 content.

Tests:

  • Add coverage for unreadable leg files through inaccessible links, file permissions, mocked I/O failures, and commit-read refusals.
  • Add controls for named legs, absent or non-regular AGENTS.md files, non-UTF-8 content, and ensuring adoption reads committed rather than working-tree metadata.

Brett Heap and others added 5 commits October 21, 2026 21:31
…he root that is a link into a folder this user cannot search is said once, `<name>: cannot be read (<reason>)` with the chmod cure, never absent, and no way in is offered over it (#1110)

Slice A of #1110: the eight sites in `shape-doctor.py` and
`scripts/shape_advisory.py` that asked `exists()` or `is_file()` of a name
directly in the root now ask `is_a` / `_is_a`, which raise a refused `stat`.

- `what is here` names each name it cannot read in validate-pins.py's
  words, with this doctor again as the NEXT and the gate's advice as the
  WHY: the one row that carries the cure.
- `the way in` offers neither command while the `.git` cannot be read, and
  points at `what is here`; `the other way in` and the Triad row are silent.
- `naming` classifies the folder's own name and says origin's was not read.
- `origin_org`, `repo_local_origin_name` and `repository_names` raise the
  refused `stat` (`carries_git`, `_is_a`) where they answered as for a folder
  with no repository.
- `shape_advisory.read()` answers `not-answered` (silent) with the `.git`'s
  line in `why`, where 3.14 said `not-a-repository`; no new state.
- `agent files` says the line with the cure: a `note` for a rendered file
  nothing pins, a FINDING for the pinned `AGENTS-shape.md`.

The eight entries leave `KNOWN_OPEN`; the three family sites stay, slice B.
New rows: tests/test_doctor_unsearchable_links.py.

Part of #1110

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…le is still a repository to the doctor's `.git` questions, and the advisory names a `.git` refused for another reason than permission (#1110)

Two rows the first round of mutants showed missing: `carries_git` asked
for a directory only (D8) and `_read` catching only `PermissionError` (A2)
both passed the new rows. The `.git` questions now also answer for a
`--separate-git-dir` checkout, whose `.git` is a file, and the advisory row
runs for an I/O error as well as a permission.

Part of #1110

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he review found alive: a `.git` that loops or dangles is no repository, a pinned agent file beside an absent one is a FINDING, `naming` says origin was not read on every exit, and the advisory reads a gitdir file as a repository (#1110)

The review of #1144 (r1144, READY on condition) ran 29 mutants of the new
code against the 17 rows; R01, R07, R13, R14, R15, R27 and R28 survived.
Eight rows and one control kill them, and all 47 mutants (the writer's 18
and the review's 29) are now killed.

- `.git -> .git` (ELOOP) and `.git -> ../nowhere/.git` (ENOENT): controls
  that hold on main (`the way in` is the scaffold, `what is here` an
  ordinary directory, no `origin` asked, the advisory `not-a-repository`),
  and `carries_git` False (R01, and the hygiene scan's R29 as well).
- A pinned `AGENTS-shape.md` it cannot read beside an absent `CLAUDE.md`:
  a FINDING, the verdict `INVALID (agent-files)` (R07).
- `naming` with the `.git` refused, for a name that classifies, one the
  policy does not classify (exit 1) and a naming script that refuses
  (exit 2): the status is the policy's, `ok` for the first, and the clause
  that origin's name was not read is on every branch (R13, R14, R15).
- The advisory row gets the `--separate-git-dir` control: `_read` and
  `repository_names` take a gitdir file for a repository (R27, R28).
- Line 98 is cut to the file's 79 columns.

Part of #1110

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…e split, one per condition, so SonarCloud's python:S9073 has nothing to say of the new rows (#1110)

One is a new row's `not row["why"] or PERMISSION_WHY not in row["why"]`, now
`PERMISSION_WHY not in (row["why"] or "")`; the other is the first
round's `"AGENTS-shape.md" in pinned and "AGENTS.md" not in pinned`, now
two asserts. No behaviour changes: the 25 rows and the 47 mutants give the
same results.

Part of #1110

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…cannot read leaves whether the folder is a leg unknown, never no leg's file, so `the way in` offers neither command and points at `what is here`, which names it, and `the other way in` and the Triad row are silent (#1110)

#1110's twelfth site, found by r1144: `shape_advisory._leg_by_agents_md`
read any exception as "no leg's file" (`except Exception: return None`).
Over a clone of acme/Orbit whose AGENTS.md is the spec or the code leg's,
linked into a folder at 000 or itself at 000, #1144's head offered
`adopt-project.py plan ... --project Orbit` in `the way in`, `setup.sh` in
`the other way in`, and the Triad advice, on 3.12 and on a 3.14-like shim.

- `_leg_by_agents_md` raises a refused `stat` or read (#430's rule: only
  ENOENT, ENOTDIR, EBADF and ELOOP, and Windows' 21, 123 and 1921, are
  nothing there). Bytes that are not UTF-8 still declare no leg; anything
  else ends in `read()`'s net.
- `_read` answers `not-answered`, with `AGENTS.md: cannot be read
  (<reason>)` in `why` and the OSError in the new `Reading.refused`, unless
  the folder's name is a leg's, which still answers. The `.git` case
  carries `refused` too.
- `leg_clone` raises `refused`; `the way in` (`way_in_unread_row`, shared
  with the `.git` row) offers neither command, and `the other way in` is
  silent.
- `what is here` reads AGENTS.md through (`read_through`), so a file at 000
  is named there with the cure.

tests/test_doctor_unsearchable_links.py: 20 new rows (3 mode, 17 in
process), and `leg_clone` over a refused `.git` now raises in #1144's row.

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

sourcery-ai Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

Data structures

Add Reading.refused to carry an operating-system refusal for callers that need to name it, without changing the serialized advisory interface.

  • scripts/shape_advisory.py L319-329, L336

Implementation

Centralize the distinction between an absent path and a refused read, and apply it consistently to path checks and leg AGENTS.md reads.

  • scripts/shape_advisory.py L363-377, L417-420, L592-623 (+1 small edits)

Carry refused .git and leg-file reads through _read, while preserving name-based leg identification and returning silence when the leg remains unknown.

  • scripts/shape_advisory.py L140-149, L855-858, L866-889

UI

Read a regular AGENTS.md through in “what is here” so a refused file is named, and make leg_clone propagate the advisory's refusal.

  • shape-doctor.py L11366-11383, L11403-11410, L11761-11777 (+1 small edits)

Make “the way in” offer neither command when a leg's identity is unknown because its AGENTS.md cannot be read, and keep the other-way-in row silent.

  • shape-doctor.py L11845-11854, L11860-11870, L11910-11931 (+1 small edits)

Tests

Add regression and control coverage for refused leg files, absent or non-regular files, UTF-8 handling, name-based identification, doctor output, and commit-based reads.

  • tests/test_doctor_unsearchable_links.py L275-378, L479-526, L812-1090 (+7 small edits)

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-1, 2026-10-22T15:30:29Z) Independent review r1159 of #1159 at 0f9aff05: NOT READY, two SHOULDs and five NITs; no MUST. The code measured correct. SHOULD-2 is a missing control row: without it, a mutant of read_through makes the doctor hang on a FIFO named AGENTS.md. Lane 1 runs the fix round in the next window. It was started by r1159 and finished by r1159-c across an account swap.

Review of PR #1159 (head 0f9aff0, base #1144's head 89486f6)

Verdict: NOT READY. The code is correct as I measured it, and nothing is a MUST. Two SHOULDs are left: one PR-body edit and one missing control row in the tests. There are also five NITs.

I stopped at your LAST STEP word. The mutant that was already running (M18b) finished and restored itself, and I started nothing after it. mut0, mut1 and the review checkout are all at 0f9aff0 with status --short empty. After every mutant, the file matched its HEAD blob again. None of my runs is still live; the pytest processes left on the box belong to w1136 and w1138. The live PR still matches the copy we saved: same head and base, still a draft, closingIssuesReferences is [], and the body is byte-equal to body.md.

Findings

SHOULD-1: PR body, "Not in this PR", lines 181-183

SHOULD-2: shape-doctor.py line 11168. No test row protects the regular-file check in read_through, and without it the doctor can hang.

  • Evidence: mutant M3 changes that check to is_a(path, lambda _m: True), and all 45 rows still pass. I ran the doctor with M3 applied over a repository whose AGENTS.md is:
    • a FIFO: the doctor never returns (timeout 30 ended it with exit 124);
    • a directory: what is here wrongly says AGENTS.md: cannot be read (Is a directory); a git repository.
  • Head and base both give a git repository, an AGENTS.md for these two cases, exit 2. read_through is new in this PR, so this one check is all that stops a hang.
  • Fix: add a control row to tests/test_doctor_unsearchable_links.py, next to test_an_agents_md_that_is_not_there_is_no_legs_file (line 874). Use a repository whose AGENTS.md is a directory (all platforms) and a FIFO (os.mkfifo, skipped where it does not exist). Assert:
    • contents.reason == "a git repository, an AGENTS.md";
    • no row says cannot be read;
    • offered_both_ways_in.
  • Run the FIFO case through the subprocess doctor() with a timeout, so a regression fails instead of hanging the suite.

NIT-1: PR body, Tests, line 110

  • Evidence: "3 over a file at mode 000" is wrong. Two of the three mode rows are a link into a folder at 000 ([spec|code]), and only one is a file at 000. The total of 3 + 17 = 20 is right.
  • Fix: "3 mode rows (2 over a link into a folder at mode 000, 1 over a file at mode 000) and 17 in process".

NIT-2: scripts/shape_advisory.py _read, lines 861-876. A Refusal no longer falls back to the leg's name, which base did.

  • Evidence: I measured this with probes/nit2_probe.py on fx/named-spec/Orbit-spec, with Tree.text("AGENTS.md") raising Refusal:
    • base: leg-clone, found by its name;
    • head: not-answered, "the advisory could not be read here (Refusal)".
  • Effect: when cat-file blob fails while reading a commit, adopt-project.py no longer prints the leg's note for a folder named <P>-spec.
  • Fix: add except Refusal: leg = _leg_by_name(names, policy); if leg is None: raise ahead of except OSError. This is mutant M17, and it keeps all 45 rows green. Or name the case in Known limits instead.

NIT-3: PR body, Tests, lines 118 and 120-125. Base figures are stale and incomplete.

  • Evidence:
    • The shim row says "16 failed, 28 passed (44 rows, before the 45th was added)". On 45 rows it is now 17 failed, 28 passed: the same 17 tests that fail on 3.12.
    • The list of failure kinds leaves out 'an AGENTS.md' == 'AGENTS.md: cannot be read (Permission denied)', from test_an_agents_md_nobody_may_open_is_named_where_there_is_no_git.
  • Fix: change the shim row to "17 failed, 28 passed", and add a bullet for what is here saying an AGENTS.md where there is no repository.

NIT-4: shape-doctor.py line 11225. Nothing pins that only AGENTS.md is read through.

  • Evidence: mutant M14 reads through every listed name (if there:), and all 45 rows pass.
  • Fix: add an in-process control row in a leg clone that refuses the read of Makefile (refusing(monkeypatch, "read_bytes", "Makefile", PermissionError(...))). Assert that contents lists a Makefile like any other name and that no row says cannot be read.

NIT-5: shape-doctor.py lines 11658 and 11703 (way_in_unread_row). The AGENTS.md row drops origin_org even though it is known.

  • Evidence: in head's --json for link-spec, locked, the way-in detail has origin_org: null, while the naming row in the same run reads origin Orbit. The leg's own row (11663-11677) keeps origin_org: org even though it offers no command either. So the docstring's reason, "None because no command is offered with them", does not hold.
  • Fix (optional): pass origin_org(root) into the AGENTS.md row, or say in the docstring that the row keeps the .git row's values. The no_leg_way_in assertion (about line 320) pins the current value, so change both together.

Observations, not findings

  • In a repository, a person's own non-leg AGENTS.md that cannot be read now withholds both ways in (fixture person-x). This is by design.
  • In named-spec, locked, the name says the spec leg while the unreadable file is the code leg's. The sentence is hedged ("if it is one"), and the report is the same on base and head.

Mutants

Each ran alone in a clone, on 3.12, over tests/test_doctor_unsearchable_links.py:

id mutant result
M3 read_through reads any kind of file survived: SHOULD-2
M8 AGENTS.md row gives git_repository None killed, 7 rows
M9 _cannot_be_read uses str(exc) killed, 7 rows
M10 open('rb').close() instead of read_bytes() killed, 3 rows
M14 what is here reads through every name survived: NIT-4
M16 _leg_by_agents_md also catches ValueError survived, accepted as equivalent: UnicodeDecodeError is the only ValueError that text() can raise
M17 NIT-2's fix stays green, 45 passed
M18 check_the_other_way_in asks leg_clone first survived, accepted as equivalent
M18b check_the_way_in asks leg_clone first killed, 3 .git rows, so the order the body relies on is pinned
M19 _nothing_there drops the Windows arm survived, accepted: Windows only, and CI is Linux only

Test counts

run result
links tests, head, 3.12 45 passed
links tests, head, shim 45 passed
links tests, base with the new file, 3.12 17 failed, 28 passed
links tests, base with the new file, shim 17 failed, 28 passed (the same 17 tests)
tests/test_shape_advisory.py, head 47 passed
tests/test_path_guard_hygiene.py, head 49 passed, as the body says

Every base failure is on the row's own assertion. The control rows (leg by name, AGENTS.md not there, not UTF-8) pass on base.

Doctor-report comparison

256 report files, paths normalised.

  • Errors: no Traceback, and every .err file is empty.
  • 3.12 against the shim: identical in all 32 pairs (text, --json and exit code).
  • Base against head:
    • The readable reports are equal for all 8 fixtures.
    • Locked, named-spec and gitlink-spec are equal. For gitlink-spec, carries_git is asked first, so the .git change alters no report.
    • Locked, link-*, mode-* and person-x change exactly as the body's table says: no NEXT in the way in, and no the other way in row or Triad row. Exit 2 on both sides.
    • Locked, plain-x: head now names AGENTS.md: cannot be read (Permission denied) with the cure, and still offers the scaffold.
  • Spec against code, and link against mode (locked, head): byte-equal. On base, mode differs from link only in what is here, as the body's line 88 says.
  • The body's two whole-report blocks match the measured reports line for line, except the machine row. That difference comes from this bench, where git-filter-repo is not on PATH.

Greps and static checks

  • Nothing uses vars( or __dict__ on a Reading, and .refused is read only at shape-doctor.py lines 11568-11569.
  • leg_clone has two callers, and both catch OSError.
  • I found no version-dependent assertion.
  • The body has no closing keyword before an issue number and no HTML tag outside code. Its only non-ASCII character is the emoji in the attribution line.

Not done (outside this review's list): a re-run of the writer's 18 mutants, and the test_shape_doctor shards and the other gate files.

Files

  • /workspace/projects/openRepoShape-worktrees/_lane1-scratch/r1159/review-r1-partial2.md — this review
  • /workspace/projects/openRepoShape-worktrees/_lane1-scratch/r1159/probes/mutants.py
  • /workspace/projects/openRepoShape-worktrees/_lane1-scratch/r1159/probes/mut.sh
  • /workspace/projects/openRepoShape-worktrees/_lane1-scratch/r1159/probes/m3_probe.sh
  • /workspace/projects/openRepoShape-worktrees/_lane1-scratch/r1159/probes/nit2_probe.py
  • /workspace/projects/openRepoShape-worktrees/_lane1-scratch/r1159/logs/ — mut-*.log and mut-*.diff
  • /workspace/projects/openRepoShape-worktrees/_lane1-scratch/r1159/fx2/ — fifo-x holds a FIFO named AGENTS.md, so anything that reads it will block
  • /workspace/projects/openRepoShape-worktrees/_lane1-scratch/r1159/runs2/

Lane: openreposhape-1 (openRepoShape-1)

Brett Heap and others added 4 commits October 22, 2026 19:16
…ere` to reading through a regular `AGENTS.md` alone: a directory or a named pipe of that name is listed and not read, the pipe's doctor run under a timeout, and a refused `Makefile` is listed like any other name (#1110)

The review of #1159 (r1) found two mutants of `shape-doctor.py` alive
against all 45 rows:

- M3, `read_through` reading any kind of file (`is_a(path, lambda _m:
  True)`): over a repository whose `AGENTS.md` is a directory, `what is
  here` said `AGENTS.md: cannot be read (Is a directory)`, and over one
  whose `AGENTS.md` is a named pipe the doctor never returned (SHOULD-2);
- M14, `what is here` reading through every name it lists (NIT-4).

New rows, each a control that holds on #1144's head as here:

- `test_an_agents_md_that_is_no_regular_file_is_listed_and_not_read`
  [directory, named-pipe]: the doctor run as a person runs it, given
  `DOCTOR_SECONDS` (120) by `doctor_within`, so a run that opens the pipe
  fails the row instead of hanging the suite. `what is here` says `a git
  repository, an AGENTS.md`, nothing says `cannot be read`, and the clone
  is offered both ways in. The pipe row skips where `os.mkfifo` is absent,
  and nothing opens the pipe in the test's own process.
- `test_what_is_here_reads_through_agents_md_alone`: a leg's clone whose
  `Makefile` read is refused, in process: `what is here` says `a git
  repository, an AGENTS.md, a Makefile`, and no row says `cannot be read`.

`offered_both_ways_in` now reads its rows through `offers_both_ways_in`,
which a whole run's rows (with `machine`) can be given too.

Part of #1110

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lder named as a leg is still that leg, as it was before #1159 and as it is where the disk's file is refused, so `adopt-project.py plan` and `check` print the leg's note over it again (#1110)

The review of #1159 (r1, NIT-2) measured that a `Refusal` raised while
reading a commit's `AGENTS.md` (`Tree.text`, `cat-file blob`) no longer
fell back to the leg's name. #1144's head read it as no leg's file and
then asked the name, so a folder named `<P>-spec` was `leg-clone`; #1159's
head sent the `Refusal` straight to `read()`'s net, `not-answered`, and
`adopt-project.py` stopped printing the leg's note there.

`_read` now catches a `Refusal` from `_leg_by_agents_md` as it catches a
refused `OSError`: the folder's name answers where it is a leg's, and with
none the `Refusal` goes on to `read()`'s net, `not-answered`, as on #1159's
head. This is the review's mutant M17. `refused` stays None: no `OSError`
was refused.

New row, a control that holds on #1144's head as here:
`test_a_leg_by_its_name_is_one_while_git_cannot_give_its_agents_md`, a
committed clone named `Orbit-spec` whose `cat-file blob` of `AGENTS.md`
raises `Refusal`: `leg-clone`, by name, the spec leg. On #1159's head
`0f9aff05` it fails with `not-answered`.

Part of #1110

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ot read keeps `origin_org` in its `--json` detail, as the leg's own row and the policy row keep it with no command, where it said None while `origin` was read (#1110)

The review of #1159 (r1, NIT-5) found that the `AGENTS.md` row gave
`origin_org: null` while the `naming` row of the same run read `origin`,
and that the docstring's reason, "None because no command is offered with
them", did not hold: the leg's own row keeps `origin_org` with no command,
and so does the policy row (`way_in_policy_row`).

`way_in_unread_row` now takes `org`: None for a `.git` that cannot be
read, whose `origin` is not read either (unchanged), and `origin_org(root)`
for the `AGENTS.md` row, read only where `carries_git` answered and
`leg_clone` raised. `suggested_project` stays None, since no `--project`
is offered, and the docstring says so.

`no_leg_way_in` now expects `origin_org: "acme"` (ORIGIN is
`acme/Orbit`), so the 7 rows that use it pin the value: with the doctor
of `34ed0a95` they fail, 7 failed and 42 passed.

Part of #1110

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ts name where git cannot give its `AGENTS.md` no longer asks `Reading.refused`, which #1144's `Reading` does not have, so it is the control on #1144's head its docstring says it is (#1110)

Measured with the file laid over #1144's head `89486f69`, the row's state,
`by` and role checks pass there, and only `assert reading.refused is
None` failed, with `'Reading' object has no attribute 'refused'`.
`refused` is set only together with `not-answered`, so the line asked
nothing of a `leg-clone` reading. Without it the row passes on
`89486f69` and on this head, and still fails on `0f9aff05`, whose
`Refusal` skips the name (`not-answered`, at the state check).

Part of #1110

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

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-1, 2026-10-22T20:18:30Z) Independent re-check r1159b of #1159 at 0ca33611, after f1159's fix round: READY. Every r1159 finding is answered, and there is no new MUST or SHOULD. Two NITs are optional. Landing still needs GitHub's Linux tests job green on this head, and #1159 lands only after #1144, after the v1.2.0 tag, on a later word.

Verdict: READY. f1159's round answers every finding from r1159 (SHOULD-1, SHOULD-2 and NITs 1 to 5). My measurements show no new MUST or SHOULD. Two optional NITs are below.

One thing outside the PR: GitHub's Linux tests job for head 0ca33611 was still "pending" at 20:17Z. SonarCloud, package and tests-git-bash pass. Landing still needs that job green.

The live PR still matches what we expected: head 0ca336110b67…, base fix/1110-doctor-advisory-unsearchable-git, still a draft, closingIssuesReferences: [], last updated 19:50:38Z.

The r1 findings

  • SHOULD-1: answered. Body line 210 now reads "Filed as shape_advisory.py: a committed AGENTS.md that is a symlink is read, in adopt-project.py's commit reading, as the text of its target's path, so a leg cloned with its AGENTS.md as a link is told it is a single repository #1163." gh issue view 1163 gives OPEN, titled "shape_advisory.py: a committed AGENTS.md that is a symlink is read, … as the text of its target's path …". No close or fix form is used.

  • SHOULD-2: answered. test_an_agents_md_that_is_no_regular_file_is_listed_and_not_read[directory|named-pipe] runs the doctor through doctor_within with a 120 s limit. I re-ran M3 (read_through reads any kind of file) in clone h, restoring the file in a finally. Result: 2 failed, 47 passed in 146.97 s, and the restored blob equals HEAD's.

    • The directory row fails on its own assertion: 'AGENTS.md: cannot be read (Is a directory); a git repository' against 'a git repository, an AGENTS.md'.
    • The pipe row fails with Failed: the doctor did not end within 120 seconds over …/bt-mut-M3/…/Atlas: something opened its AGENTS.md, raised from subprocess.TimeoutExpired.
    • The blocked doctor was killed. Its command line holds the basetemp path, and pgrep -af r1159b/bt-mut found nothing left afterwards.
    • The row cannot hang the run on head: the whole file takes 27 s. Nothing in the test's own process opens the pipe, and the advisory's Tree.text returns None for anything that is not S_ISREG.
    • Both rows pass on base 89486f69, so they are true controls.
  • NIT-1: answered. Body line 116: "3 mode rows: 2 over a link into a folder at mode 000, 1 over a file at mode 000".

  • NIT-2: answered. _read now has except Refusal: leg = _leg_by_name(...); if leg is None: raise ahead of except OSError. Refusal subclasses Exception, not OSError, so the bare raise reaches read()'s net. I probed it with r1159b/probes/nit2.py, making cat-file blob …:AGENTS.md raise Refusal:

    folder head 0ca33611 base 89486f69
    Orbit-spec (a leg's name) leg-clone, by name leg-clone, by name
    Atlas (no leg's name) not-answered, "the advisory could not be read here (Refusal), so it says nothing" advise

    The only source of a Refusal there is Tree.text's cat-file blob, because kind() catches its own. Two mutants confirm the rows hold it:

    • N2 (except Refusal disabled) kills only the new row test_a_leg_by_its_name_is_one_while_git_cannot_give_its_agents_md (1 failed, 48 passed).
    • N2c (no re-raise) kills only test_a_failure_the_leg_reader_does_not_expect_is_silence_not_advice (1 failed, 48 passed).
  • NIT-3: answered. Body line 128 now says "17 failed, 32 passed (the same 17 tests)", and line 136 adds the 'an AGENTS.md' == … bullet. I sorted my 17 base failures by their assertion and got the body's split exactly: 7 extra-row, 4 DID NOT RAISE, 3 no refused, 2 advise/not-answered, 1 an AGENTS.md.

  • NIT-4: answered. I re-ran M14 (what is here reads through every name) in clone h2, restoring in a finally: 1 failed (test_what_is_here_reads_through_agents_md_alone), 48 passed, and the blob was restored.

  • NIT-5: answered. way_in_unread_row takes org. The AGENTS.md call passes origin_org(root), and the .git call passes None. The new docstring is now correct: the leg's own row (line 11699) and the policy row (11677) both keep origin_org with no command.

    • origin_org is only reached after carries_git succeeds, so it brings no new raise path.
    • Mutants N5 (pass None again), N5b ("origin_org": None inside the helper) and M8 each give 7 failed, 42 passed.
    • No .md file in the repo names origin_org, so no doc goes stale.

New problems

None at MUST or SHOULD.

  • Behaviour: except Refusal changes the answer only in adopt-project.py's commit reading, and only where a leg's name answers. That case goes back to base's answer.
  • Rows passing for the wrong reason: none. Each new row is killed by the mutant aimed at it: M3 by the directory and pipe rows, M14 by the Makefile row, N2 by the leg-by-name row. All four new rows pass on base.
  • Version-dependent asserts: none. The new asserts compare row reasons and detail keys only, and "Is a directory" appears only in docstrings. Head under the 3.14-like shim gives 49 passed; I confirmed the shim loads (pathlib._w1110c_shim True).
  • NIT-A, PR body line 35, optional. The sentence "This round changes adopt-project.py's reading only where git cannot give a commit's AGENTS.md" talks about the review round, and the base-to-head change it hides is never stated. That change: with no leg's name, plan and check go from the advice on base to silence on head. Fix: replace it with "Where git cannot give a commit's AGENTS.md, a folder named as a leg still gets the leg's note, as on base. With no such name, plan and check print no advisory, where base printed the advice. No fixture meets this."
  • NIT-B, tests/test_doctor_unsearchable_links.py lines 1045-1046, optional. The backticked row name is broken across the line as …_cannot_ / be_read, so a grep for test_a_leg_by_its_name_is_one_while_its_agents_md_cannot_be_read (line 885) does not find this reference. Fix: break the line before the backticks.

Measurements

Everything ran on 3.12 in the probe environment, one basetemp per call, at most 2 calls at once.

run result
links file, head 0ca33611 49 passed (27 s)
links file, head, shim 49 passed
links file, base 89486f69 + head's test file 17 failed, 32 passed
links file, base + head's test file, shim 17 failed, 32 passed, the same 17 (diff empty)
links file, 0f9aff05 + head's test file 8 failed (the leg-by-name row and the 7 origin_org rows), 41 passed. This bears out body lines 148-149.
tests/test_shape_advisory.py + tests/test_path_guard_hygiene.py, head 96 passed; collected 47 and 49, as body line 195 says

Mutants I re-ran at 0ca33611. After every one, the file's blob equalled HEAD's.

mutant result
M3 2 failed
M14 1 failed
N2 1 failed
N2c 1 failed
N5 7 failed
N5b 7 failed
M8 7 failed

These all match the body's table.

The body's gate rows I did not re-run: the test_shape_doctor shards, test_doctor_next_is_a_command and the 7-file list. f1159's logs for them in w1110c/logs agree with the body (112, 112, 112, 109+3 skipped, 61, 290). All are timed 19:40-19:50Z, after 0ca33611's commit at 19:34:02Z, and l1-1110c is at 0ca33611 with a clean tree. I also did not re-run the whole-report blocks; the rows hold their structure.

PR body: title 245 characters; no close, fix or resolve form before an issue number; no @; the only non-ASCII character is the emoji.

State: my checkout l1-r1159 is clean at 0ca33611. Mutant clones h and h2 are clean. Clones b and p carry the overlaid test file, as intended. No process of mine is running.

Files

All in /workspace/projects/openRepoShape-worktrees/_lane1-scratch/r1159b:

  • body.md: the live PR body, as read
  • pr-meta.json
  • probes/mut.py: the mutant runner, which restores in a finally and checks the blob
  • probes/nit2.py
  • logs/:
    • links-head-312.log, links-head-shim.log
    • links-base-312.log, links-base-shim.log, links-0f9-newfile.log
    • mut-M3.log, mut-M14.log, mut-N2.log, mut-N2c.log, mut-N5.log, mut-N5b.log, mut-M8.log
    • gate-adv-hyg.log
  • penv.sh, run.sh, and the clones h, h2, b, p

Lane: openreposhape-1 (openRepoShape-1)

… row names its sibling row on one line, so a grep for the row's name finds the reference (#1110)

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-1, 2026-10-24T14:43:48Z) d1159, lane 1's independent delta checker (not the writer): READY carries from 0ca33611 to 66e68f94, with no MUST, no SHOULD and three NITs.

The full delta check

d1159 report

Part 1 is complete: READY carries from 0ca33611 to 66e68f94, with three NITs and no MUST or SHOULD.

Part 2 is partial. #1164's head has not moved since the review that names it, and I re-measured nothing on it. I stopped at lane 1's LAST STEP. No runs of mine are live.

Part 1: #1159 (0ca33611..66e68f94)

Commit table

commit parents what it changes only what its message says?
66e68f948 "tests/test_doctor_unsearchable_links.py: the docstring of the control row names its sibling row on one line, so a grep for the row's name finds the reference (#1110)" 0ca336110 (one parent, not a merge) One file, +4/-3. It rewraps the docstring of test_a_leg_by_its_name_is_one_while_git_cannot_give_its_agents_md, so the backticked row name sits on one line. This is r1159b's NIT-B. Yes for the change. See NIT-2 for the grep wording in the message.

Commands and output:

  • git log --format='%h %p %s' 0ca33611..66e68f94 prints that one commit. git rev-list --count gives 1 and --merges gives 0.
  • git diff --name-only 0ca33611 66e68f94 prints tests/test_doctor_unsearchable_links.py alone.
  • Every other file is byte-identical. A hash of git ls-tree -r, with that file excluded, is f8d8b6d9858e3836c015e86ffb7d380cc6fc6290 at both commits. So shape-doctor.py and scripts/shape_advisory.py did not change.
  • The AST of the old and new test file, with docstrings blanked, is equal: AST equal with docstrings blanked: True. Exactly one docstring differs, and the old and new texts are equal after whitespace normalisation and healing the _ be_ break. Only one string constant differs, and the file is one line longer (1089 to 1090).
  • Nothing in the repo cites a line number of the file (grep for test_doctor_unsearchable_links.py:[0-9] finds nothing).
  • No added line is non-ASCII or over 80 characters.

The delta is not a behaviour change, so the brief's "at least 3 mutants of the changed lines" does not strictly apply. I ran 4 spot-check mutants anyway, to show the rows still bite at 66e68f94.

Test runs

Run in the probe environment: stub gh, bogus token, empty GH_CONFIG_DIR, GIT_CONFIG_GLOBAL=/dev/null, GIT_SSH_COMMAND=false, env -u LANES_LANE, one basetemp per call, at most 2 calls at once. I ran df -h /workspace and df -i / and counted pytest processes before each call; the minimum free was 16G.

call result
links file at 66e68f94, 3.12 49 passed (149.8 s)
links file at 66e68f94, 3.14-like shim 49 passed (147.6 s)
tests/test_shape_advisory.py, tests/test_path_guard_hygiene.py, tests/test_no_auto_maintenance.py at 66e68f94 154 passed, 2 skipped (280 s). The collect counts are 47 + 49 + 60 = 156.
base 89486f69 plus head's test file, 3.12 17 failed, 32 passed
base 89486f69 plus head's test file, shim 17 failed, 32 passed

Base comparison:

  • The failing set on 3.12 equals r1159b's set at 0ca33611 (diff empty), and the shim's set equals the 3.12 set.
  • The base file has 25 rows and head has 49, so "gains 24 rows" is right.
  • The shim was loaded: shim loaded: True. On a file inside a mode-000 folder, plain 3.12 is_file raises PermissionError and the shim answers False.

GitHub checks on 66e68f94: tests, tests-git-bash, package and SonarCloud all completed with success.

Mutants

Spot-check, not required by the delta. Run on 3.12 in a clone at 66e68f94. Each file was restored in a finally, and the blob matched HEAD's afterwards.

mutant file rows failed blob restored
M3: read_through reads any kind of file shape-doctor.py 2 failed, 47 passed (directory row and named-pipe row) True
N2: except Refusal disabled scripts/shape_advisory.py 1 failed (..._by_its_name_is_one_while_git_cannot_give_its_agents_md), 48 passed True
N5: the AGENTS.md way-in row drops origin_org shape-doctor.py 7 failed, 42 passed True
M14: what is here reads through every name shape-doctor.py 1 failed (test_what_is_here_reads_through_agents_md_alone), 48 passed True

All 4 were killed, with the same counts as the body's table. After M3 no stray doctor process was left over the FIFO.

The body

Findings

No MUST and no SHOULD.

NIT-1: the PR body does not name the delta.

  • PR body lines 16, 67, 125, 151 and 189 label head, "Head 0ca33611" and the measurements "at 0ca33611". The body names neither 66e68f94 nor a docstring-only commit ('66e68f94' in body is False, and so is "docstring").
  • All of those measurements still hold. I re-measured the counts that matter at 66e68f94 (49/49, 17/32, 47 + 49).
  • Fix: add one sentence, for example "66e68f94 changes one docstring in tests/test_doctor_unsearchable_links.py and nothing else (r1159b's NIT-B); every figure above carries from 0ca33611."

NIT-2: the commit message overstates the grep claim.

  • tests/test_doctor_unsearchable_links.py:1046 now reads `..._by_its_name_is_one_while_its_agents_md_cannot_be_read`). It stays abbreviated with ..._.
  • grep -n "test_a_leg_by_its_name_is_one_while_its_agents_md_cannot_be_read" finds only the def at line 885. That is r1159b's own example string. A grep for the suffix by_its_name_is_one_while_its_agents_md_cannot_be_read finds both lines 885 and 1046.
  • r1159b's fix, "break the line before the backticks", was applied as asked, so the NIT is answered as asked.
  • Optional fix: spell the full test_a_leg_by_... name in the docstring.

NIT-3 (observation): non-ASCII in the body.

  • The body holds one ' (U+2019) besides the emoji. It is in the Sourcery-generated block ("doctor's"), not in lane text.
  • r1159b said the emoji was the only non-ASCII character. If the squash message is cut from that block, replace it.

Not covered in Part 1

Part 2: #1164 (read-only checks, no test or mutant run)

Flags, not findings

Not covered in Part 2: I re-ran no tests, mutants, gate rows or pin measurements. I did not diff #1164 against anything, because there is no later commit.

State

  • Review checkout /workspace/projects/openRepoShape-worktrees/l1-d1159: detached at 66e68f94, status --short empty.
  • Scratch clones m and m2 are at 66e68f94 and clean. Clone b is at base 89486f69 and carries the head test file overlaid, plus an untracked tests/test_zzbase_links.py (I run no rm). Both are intentional.
  • No process of mine is running.

Files

All in /workspace/projects/openRepoShape-worktrees/_lane1-scratch/d1159/:

  • checkpoint.md
  • logs/ (the run logs: links-head-312.log, links-head-shim.log, links-base-312.log, links-base-shim.log, gate-adv-hyg-nam.log, mut-M3.log, mut-N2.log, mut-N5.log, mut-M14.log)
  • probes/mut.py
  • shim/sitecustomize.py
  • penv.sh, run.sh
  • body.md, body1164.md, pr-meta.json, pr-view.json, pr1164.json

Lane: openreposhape-1 (openRepoShape-1)

Lane: openreposhape-1 (openRepoShape-1)

Base automatically changed from fix/1110-doctor-advisory-unsearchable-git to main November 8, 2026 13:45
#1144's final head is update-branch's merge of main bd3e9c1 into
89486f6, the tree that squash-landed as b1f45fa. This is the first of
the three merges that restack #1159 after #1144 landed.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Nov 8, 2026
…py: an `id:`, a date or a `repository:` ending in a line feed is refused by name, and a pin's `source_repository:` that is no path or `owner/repo` name is named as the pin's (#1139) (#1166)

The value patterns the issue names refuse a value that ends in a line feed, by name, as #1126 (#1017) makes `COMMIT_RE`, `SHA256_RE` and `COMMIT_40` do:
- `DATE_RE` and `QUALIFIED_RE` in `validate-manifest.py` and `validate-family.py`, `MEMBER_RE` in `scripts/family.py`, `DATE_RE` in `scripts/shape_advisory.py` and `REPOSITORY_RE` in `update-shape.py`;
- `PROJECT_ID_RE`, read with `fullmatch` where the two validators and `family.py add` read an id from a file.

A shape pin's `source_repository:` that is neither a path on this machine nor an `owner/repo` name is refused in the pin's name, not `--upstream`'s. Values without a line feed keep their outcome, and no refusal code is added or renamed. Two inputs do move from one existing code to another (see "Behaviour changed", below).

**What this PR does not cover, and the title does not claim:**
- the naming-policy patterns that `scripts/repo_shape.py` reads from `contracts/repository-naming.yaml`, which `validate-repository-naming.py` still reads with `.match`;
- `update-upstream-not-a-repo`, which still says `--upstream` when a pin names a folder that exists and is not a repository.

Both are follow-ups, with the other findings of the independent review, under "Follow-ups (not in this PR)".

This does not need #1126 and does not touch its files or its test file.

Measured on main `038dafba` (Python 3.12.3, git 2.43.0) by a probe of each tool, before any change. Each value is written in the file as a YAML double-quoted `"...\n"`, which the reader takes as the value and a line feed.

| Read from | Value | On main |
|---|---|---|
| `project.yaml` (`validate-manifest.py`) | `id: "atlas\n"` | only `FINDING manifest-topic: topic is 'xf-project-atlas', expected 'xf-project-atlas\n' derived from id 'atlas\n'`, exit 1 |
| `project.yaml` | `elected_on: "2026-09-02\n"` | `manifest ok`, exit 0 |
| `project.yaml` | a leg's `repository: "testorg/Atlas-spec\n"` | `manifest ok`, exit 0 |
| `family.yaml` (`validate-family.py`) | `id: "inkrouter\n"` | `family ok: InkRouter (inkrouter` and a line break, exit 0 |
| `family.yaml` | `created_on: "2026-09-04\n"` | `family ok`, exit 0 |
| `family.yaml` | a member's `repository: "InkRouter/IRRS\n"` | `FINDING member-repository-name: ... whose name is 'IRRS\n'`, exit 1 |
| a member's `project.yaml`, read by `family.py add` | `id: "irqs\n"` | mounted and committed, exit 0, with the row `id: "irqs\n"`; the holder's gate then said `family ok` |
| `contracts/shape-pin.yaml`, `update-shape.py check` with no `--upstream` | `source_repository: "opensoft/openRepoShape\n"` | `REFUSED unsafe-value: --upstream is 'opensoft/openRepoShape\n'`, exit 2 |
| the same | `source_repository: "opensoft/open Repo"` | `REFUSED update-upstream-unresolvable: --upstream 'opensoft/open Repo' is neither ...`, exit 2 |
| the same | `source_repository: "-opensoft/openRepoShape"` | `REFUSED unsafe-value: --upstream is '-opensoft/openRepoShape'`, exit 2 |
| an adoption plan (`adopt-project.py check`) | `id: "northwind\n"` | `FINDING plan-manifest-invalid: id is 'northwind\n', which the project's own validate-manifest.py refuses: manifest-topic: ...`, exit 1; `execute` refused it, exit 2, before any leg |

In-process on the same main: `repo_shape.PROJECT_ID_RE.match('atlas\n')`, `shape_advisory.DATE_RE.match('2026-09-02\n')` and `REPOSITORY_RE.match('a/b\n')` each matched, and `date.fromisoformat('2026-09-02\n')` raised `ValueError`.

- **`DATE_RE` and `QUALIFIED_RE`** in `templates/assembly-root/scripts/validate-manifest.py` and `templates/family-root/scripts/validate-family.py` end in `\Z`. So do `MEMBER_RE` in `scripts/family.py` (the third spelling `tests/test_family_member_name.py` holds equal to the two `QUALIFIED_RE`), `DATE_RE` in `scripts/shape_advisory.py` and `REPOSITORY_RE` in `update-shape.py`.
- **`PROJECT_ID_RE` keeps its `$`.** Every id refusal prints its text (`must match ^[a-z0-9][a-z0-9-]*$`, pinned in six test files), the validators run against each project's own copy of `repo_shape.py`, and its definition sits inside #1126's hunk. The three callers that read an id from a file ask it with `fullmatch` instead: `validate-manifest.py` (`manifest-id`), `validate-family.py` (`family-id`) and `family.py`'s reading of a member's `project.yaml` (`member-bad-id`, which `add` now says before anything is mounted).
- **`update-shape.py`.** `open_upstream(spec, pin_path=None)` takes the pin the value was read from, which `prepare` passes when no `--upstream` was given. Then `update-upstream-unresolvable` says ``<root>/contracts/shape-pin.yaml: `source_repository:` is '...', which is neither a path on this machine nor an `owner/repo` name``, with a remedy to write the pin's field or pass `--upstream`, and `checked_value` names the same field. **Code kept:** `update-upstream-unresolvable`, the existing code for a value that is not an `owner/repo` name. A line feed now reaches it, since the pattern no longer takes one. Before, `checked_value` refused it as `unsafe-value`.
- **One existing test line:** `tests/test_family_init_values.py` finds validate-family's `DATE_RE` line by its text, which now ends `\Z`.
- **One existing stand-in:** the `temporaries` fixture in `tests/test_update_shape_unreadable.py` stands in for `open_upstream` with `def opening(spec)`, which the new two-argument call refused with `TypeError: ... takes 1 positional argument but 2 were given`. It now takes `pin_path=None` and hands it on to the real `open_upstream`, so `test_a_read_refused_in_prepare_closes_the_upstream[check|apply]` asks what it asked before.
- **The rows** are in a new file of their own, `tests/test_value_pattern_line_break.py`.
- **A second guard, in a second commit (`5102b4294`, a test only, no production file).** The first guard holds the four pattern names this PR ends in `\Z`, and nothing held `PROJECT_ID_RE`, which keeps its `$`. `test_every_reader_of_the_id_pattern_is_in_the_table` reads every tracked `.py` outside `tests/` with `ast` (it runs none) and holds each call of a method of `PROJECT_ID_RE` (`PROJECT_ID_RE.x(...)` or `repo_shape.PROJECT_ID_RE.x(...)`) to the table `ID_READERS`:
  - the three `fullmatch` readers of an id from a file (`validate-manifest.py`, `validate-family.py`, `scripts/family.py`);
  - the modules that still `match` it, each with its reason: argv `--id` in `scaffold-project.py` and `scripts/family.py`, where `checked_value` refuses a line feed after the pattern; argv `--id` in `adopt-project.py`, which the project's own `validate-manifest.py` refuses (`adopt-manifest-invalid`, below; `ID_READERS` still gives it the `checked_value` reason); and `adopt-project.py`'s two `plan-bad-id` readers, of the plan's `id:` and of the id derived from `legs.assembly`, which a line feed never passes (below).

  A new reader of `PROJECT_ID_RE`, `fullmatch` included, fails it by file and method until it is listed in `ID_READERS` with its reason. The table is exact both ways, so a reader taken away fails it too.

**Left as they are, and why:**
- the argv readers of `PROJECT_ID_RE` (`--id` in `scaffold-project.py`, `family.py init` and `adopt-project.py`). The first two hold the id to `checked_value` after the pattern, which refuses a line feed as `unsafe-value` naming `--id`, and `tests/test_derived_id_refusals.py` pins that answer for them. `adopt-project.py` never calls `checked_value` on `--id`: `plan` with `--id` set to `northwind` and a line feed exits 2 with `adopt-manifest-invalid`, in the words of the project's own `validate-manifest.py` (`manifest-id`), and writes no plan (lane 5's re-check, issuecomment-6371408915, NIT 2; measured on `plan` only);
- `adopt-project.py`'s reading of a plan's `id:`: `check` and `execute` hold it to the project's own `validate-manifest.py`, which refused it on main (above) and now names it in `manifest-id`'s words. It is the same value, refused under the same code, `plan-manifest-invalid`, before any leg exists. The id derived from `legs.assembly` is read with `.match` too, and a line feed in `legs.assembly` is `unsafe-value` before that id is asked (the review measured both, on main and here alike). This keeps the open PRs on `adopt-project.py` (24 at 12:02Z) clear;
- `PIN_ARG_RE`, and `NAME_RE`/`REF_RE`/`PATH_RE` in `setup-project.py`, as the issue says;
- `update-upstream-not-a-repo` still says `--upstream`. A pin's value reaches it only when it names a path that exists, not through the pattern.

**Behaviour changed, stated plainly, and what the issue's Verify means by "No refusal code changes":** the issue's Verify line reads as no code added or renamed, and none is. Two inputs do move from one existing code to another, exit 2 both before and after:
- `update-shape.py --upstream "opensoft/openRepoShape<LF>"` was `unsafe-value` and is now `update-upstream-unresolvable`, still naming `--upstream`;
- a pin's `source_repository:` that ends in a line feed, with no `--upstream`, was `unsafe-value` naming `--upstream` and is now `update-upstream-unresolvable` naming the pin's field.

No test pinned either old answer: every gate below passed. `family.py add --member` is stripped before `MEMBER_RE`, so `add` answers as before.

**Not changed, seen while measuring:** with `--upstream` passed, `update-shape.py` does not read the pin's `source_repository:` through `REPOSITORY_RE` at all, and it keeps the value as the upstream's repository name. `validate-family.py` does not hold a member row's `id:` to `PROJECT_ID_RE`; it compares the row with the member's manifest. Neither is a `$` question. Both are under "Follow-ups (not in this PR)".

All in `tests/test_value_pattern_line_break.py`, run against main's production code: the five production files swapped in place from `origin/main` (`961640c68`), then put back with their sha256 matching (`dirty=5` in the run's START line is those five files). On the head `5102b4294` (30 rows) the result is **20 failed, 10 passed in 23.12s, git 2.43.0**. The 10 that pass are baselines: the seven `test_a_pattern_takes_its_value` rows, the spelling-table guard and the two double-quoted-without-a-line-feed rows.

The first 19 are the first commit's rows (19 failed, 10 passed in 22.12s on its 29 rows at `038dafba`). The independent review reproduced them from main's bytes of the same five files (19 failed, 10 passed in 26.78s, the same 19 ids). The 20th, `test_every_reader_of_the_id_pattern_is_in_the_table`, is the second commit's: it fails on main because the three readers of an id from a file are `.match` there, not `.fullmatch`.

- `test_a_pattern_refuses_its_value_and_a_line_feed[manifest-DATE_RE]`
- `test_a_pattern_refuses_its_value_and_a_line_feed[manifest-QUALIFIED_RE]`
- `test_a_pattern_refuses_its_value_and_a_line_feed[family-DATE_RE]`
- `test_a_pattern_refuses_its_value_and_a_line_feed[family-QUALIFIED_RE]`
- `test_a_pattern_refuses_its_value_and_a_line_feed[family.py-MEMBER_RE]`
- `test_a_pattern_refuses_its_value_and_a_line_feed[shape_advisory-DATE_RE]`
- `test_a_pattern_refuses_its_value_and_a_line_feed[update-shape-REPOSITORY_RE]`
- `test_a_manifest_value_that_ends_in_a_line_break_is_a_finding[id]`
- `test_a_manifest_value_that_ends_in_a_line_break_is_a_finding[elected_on]`
- `test_a_manifest_value_that_ends_in_a_line_break_is_a_finding[leg-repository]`
- `test_adopt_check_names_a_plan_id_that_ends_in_a_line_break_as_an_id`
- `test_a_family_value_that_ends_in_a_line_break_is_a_finding[id]`
- `test_a_family_value_that_ends_in_a_line_break_is_a_finding[created_on]`
- `test_a_family_value_that_ends_in_a_line_break_is_a_finding[member-repository]`
- `test_add_refuses_a_member_whose_id_ends_in_a_line_break`
- `test_a_pins_source_that_is_no_owner_repo_is_named_as_the_pins[line-feed]`
- `test_a_pins_source_that_is_no_owner_repo_is_named_as_the_pins[space]`
- `test_a_pins_source_checked_value_refuses_is_named_as_the_pins`
- `test_an_upstream_passed_with_a_line_feed_is_named_as_passed`
- `test_every_reader_of_the_id_pattern_is_in_the_table`

**13 of 13 killed.** Each mutant was applied alone to one file of the branch's production bytes (the same bytes as the first commit and as the head, which adds a test only), the rows were run, and the file was put back with its sha256 checked (`restored=yes` for all 13). Each run is `tests/test_value_pattern_line_break.py` plus `tests/test_family_member_name.py::test_every_spelling_of_the_member_name_rule_is_the_same_pattern`, git 2.43.0.

| Mutant | File | Killed by |
|---|---|---|
| M1 `DATE_RE` back to `$` | `validate-manifest.py` | 2: `test_a_pattern_refuses_its_value_and_a_line_feed[manifest-DATE_RE]`, `test_a_manifest_value_that_ends_in_a_line_break_is_a_finding[elected_on]` |
| M2 `QUALIFIED_RE` back to `$` | `validate-manifest.py` | 3: `...[manifest-QUALIFIED_RE]`, `...[leg-repository]`, `test_every_spelling_of_the_member_name_rule_is_the_same_pattern` |
| M3 the id read with `.match` | `validate-manifest.py` | 2: `test_a_manifest_value_that_ends_in_a_line_break_is_a_finding[id]`, `test_adopt_check_names_a_plan_id_that_ends_in_a_line_break_as_an_id` |
| M4 `DATE_RE` back to `$` | `validate-family.py` | 2: `...[family-DATE_RE]`, `test_a_family_value_that_ends_in_a_line_break_is_a_finding[created_on]` |
| M5 `QUALIFIED_RE` back to `$` | `validate-family.py` | 3: `...[family-QUALIFIED_RE]`, `...[member-repository]`, `test_every_spelling_of_the_member_name_rule_is_the_same_pattern` |
| M6 the id read with `.match` | `validate-family.py` | 1: `test_a_family_value_that_ends_in_a_line_break_is_a_finding[id]` |
| M7 `MEMBER_RE` back to `$` | `scripts/family.py` | 2: `...[family.py-MEMBER_RE]`, `test_every_spelling_of_the_member_name_rule_is_the_same_pattern` |
| M8 a member's id read with `.match` | `scripts/family.py` | 1: `test_add_refuses_a_member_whose_id_ends_in_a_line_break` |
| M9 `DATE_RE` back to `$` | `scripts/shape_advisory.py` | 1: `...[shape_advisory-DATE_RE]` (its pattern row alone: `date.fromisoformat` refuses the line feed after the pattern, so no tool row sees it) |
| M10 `REPOSITORY_RE` back to `$` | `update-shape.py` | 3: `...[update-shape-REPOSITORY_RE]`, `test_a_pins_source_that_is_no_owner_repo_is_named_as_the_pins[line-feed]`, `test_an_upstream_passed_with_a_line_feed_is_named_as_passed` |
| M11 `prepare` passes no pin | `update-shape.py` | 3: `test_a_pins_source_that_is_no_owner_repo_is_named_as_the_pins[line-feed]` and `[space]`, `test_a_pins_source_checked_value_refuses_is_named_as_the_pins` |
| M12 `checked_value` names `--upstream` again | `update-shape.py` | 1: `test_a_pins_source_checked_value_refuses_is_named_as_the_pins` |
| M13 the pin's refusal sentence removed | `update-shape.py` | 2: `test_a_pins_source_that_is_no_owner_repo_is_named_as_the_pins[line-feed]` and `[space]` |

No mutant is added for the stand-in in `tests/test_update_shape_unreadable.py`: it is a test change.

The independent review re-ran M3, M8, M11 and M12, and one mutant of its own (X1: `prepare` names the pin even when `--upstream` is passed). All five were killed, and each file was put back with its sha256 checked. The other nine of the 13 are this writer's runs alone.

**The second commit's guard: 6 of 6 probes caught.** Each was applied alone to one tracked file at `5102b4294`, `test_every_reader_of_the_id_pattern_is_in_the_table` was run, and the file was put back with its sha256 checked (`restored=yes` for all six). The first commit's guard, `test_every_spelling_of_a_value_pattern_is_in_the_table`, was run with each plant too: it passes every one of them, which is the gap the second guard closes.

| Probe | File | The new guard | The first guard |
|---|---|---|---|
| P1 a new `PROJECT_ID_RE.match(value)` reader appended | `update-shape.py` | failed: `('update-shape.py', 'match'): 1` is more than the table holds | passed |
| P2 a new `repo_shape.PROJECT_ID_RE.search(value)` reader appended | `scripts/shape_advisory.py` | failed | passed |
| P3 `fullmatch` back to `match` | `templates/assembly-root/scripts/validate-manifest.py` | failed | passed |
| P4 `fullmatch` back to `match` | `templates/family-root/scripts/validate-family.py` | failed | passed |
| P5 `fullmatch` back to `match` | `scripts/family.py` | failed | passed |
| P6 main's bytes of those three files together (the 20th row above) | the three | failed | not run |

**The second commit `5102b4294` (dirty=0)**, Python 3.12.3, with the same stub `gh`, bogus token and no global git config:

| Run | Git | Result |
|---|---|---|
| `tests/test_value_pattern_line_break.py` | 2.43.0 | **30 passed** in 23.00s |
| `tests/test_value_pattern_line_break.py` | 2.47.1 | **30 passed** in 23.30s |
| `tests/test_pinned_copy_text.py`, `tests/test_path_guard_hygiene.py`, `tests/test_repo_hygiene.py`, `tests/test_agents_md_codes.py`, `tests/test_cli_reference.py` | 2.43.0 | **306 passed** in 61.54s |

That commit changes only the test file (62 lines added, no assert that joins conditions with `and` or `or`, ASCII), so the first commit's runs below are not repeated: they hold the production files and every other test file, and those are unchanged. CI on `5102b4294`, and an independent look at the second commit, were not done when this was written.

**The independent review of the first commit** (rv1166c, READY, read-only) reports these gates on `2593952c9`, all passing: the stand-in gate's 6 files (298 passed), the new file on 2.47.1 (29 passed), the five hygiene files (306 passed), 15 more test files that name a changed function, pattern or code (1611 passed, 19 skipped) and `tests/test_shape_doctor.py` (445 passed, 3 skipped). It also read CI on that head at about 20:04Z: `tests` (the full suite on 3.14), `tests-git-bash`, `package` and SonarCloud all passed.

**The first commit's gates ran on `2593952c` (dirty=0)**, Python 3.12.3, with the real `gh` unreachable (a stub first on `PATH`, a bogus token) and no global git config:

| Run | Git | Result |
|---|---|---|
| the files that held every failure and setup error of the shards below: `tests/test_family.py`, `tests/test_update_shape_pr_base.py`, `tests/test_update_shape_unreadable.py`, `tests/test_update_shape_header_comments.py`, `tests/test_update_shape_messages.py` and the new `tests/test_value_pattern_line_break.py` | 2.43.0 | **298 passed** in 343.60s |
| `tests/test_value_pattern_line_break.py` | 2.47.1 | **29 passed** in 21.31s |
| `tests/test_pinned_copy_text.py`, `tests/test_path_guard_hygiene.py`, `tests/test_repo_hygiene.py`, `tests/test_agents_md_codes.py`, `tests/test_cli_reference.py` | 2.43.0 | **306 passed** in 69.12s |

**The two shards, on the uncommitted tree** holding the same production bytes as the first commit (the 7 files' sha256 checked before and after), git 2.43.0: the validators' test files, the `REPOSITORY_RE`/`source_repository` files and the `update-shape.py`, `shape_advisory.py` and `family.py` files, 125 in all.

- shard 1 (62 files): 4 failed, 5290 passed, 13 skipped in 56 min;
- shard 2 (63 files): 3 failed, 4 setup errors, 3761 passed, 29 skipped in 50 min.

Every failure and setup error is in the five existing files of the first row of the table above, which all pass on the first commit:
- 2 were real: `test_a_read_refused_in_prepare_closes_the_upstream[check|apply]`, the `opening(spec)` stand-in under the two-argument call. The stand-in is fixed in the first commit.
- The other 5 failures and the 4 setup errors were `update-conflict` on `scripts/validate-family.py` ("not a verbatim copy ... at the pinned commit"). This is an artifact of an uncommitted tree: `tests/test_family.py`'s `upstream_clone` commits the working tree into its clone, while `family.py init` pins the holder at the real `HEAD`, so an uncommitted `templates/family-root/` file is never verbatim at its pin. On the first commit they pass.

Every other file of the two shards passed on these production bytes, so the shards were not re-run.

`git merge-tree --write-tree <other> 5102b42` (git 2.43.0) at 2026-10-23T12:08Z, after re-fetching main (still `961640c6`; read again at 12:10Z, just before the push) and re-listing the open PRs at 12:02Z (55 open). Main has moved from the `038dafba` that this body measured to `961640c6` by one file, `docs/release-notes/v1.2.0.md`. Each open PR that touches one of this PR's 8 paths, at its current head, plus #1126:

| Against | Head | Shares | Result |
|---|---|---|---|
| origin/main | `961640c6` | | clean, tree `4613729516ff` |
| #1098 | `dad7961a` | `scripts/family.py` | clean, tree `4d1ff9517588` |
| #1117 | `10dc011c` | `scripts/family.py` | `scripts/family.py` merges clean; conflict in `docs/handbook.html` only, which this PR does not touch (see below) |
| #1124 | `8dfa32bd` | `validate-family.py` | clean, tree `cb2ec1073646` |
| #1126 | `4c1f80cf` | none (its `scripts/repo_shape.py` and tests) | clean, tree `e82c8945ac37` |
| #1133 | `a7bfee1b` | `update-shape.py` | clean, tree `a710870d1ffc` |
| #1144 | `89486f69` | `scripts/shape_advisory.py` | clean, tree `2d3e3e1ef43d` |
| #1147 | `b75babee` | `validate-manifest.py` | clean, tree `a82ac3b47a3f` |
| #1159 | `66e68f94` | `scripts/shape_advisory.py` | clean, tree `4abeb7b38665` |
| #1164 | `74096eb4` | `scripts/family.py` | clean, tree `ee285e1cb8cf` |
| #1172 | `5aa0f116` | `validate-family.py` | clean, tree `aa32d41e6e2c` |
| #1187 | `cc8a87ee` | `update-shape.py`, `tests/test_update_shape_unreadable.py` | clean, tree `25711eb1cac4` |
| #1201 | `eac177af` | `scripts/family.py` | `scripts/family.py` merges clean; conflict in `docs/handbook.html` only (see below) |

- #1117 and #1201 conflict in `docs/handbook.html` alone. #1201 is stacked on #1117 (their merge-base is #1117's head `10dc011c`). Each of them against origin/main alone gives the same conflict in the same file, so it is theirs: #1117's, from its older base `6da757cb`, which #1201 inherits.
- #1187 and #1201 are new since the first merge-trees. #1187 shares `tests/test_update_shape_unreadable.py` with this PR; its diff adds or removes no line that names `open_upstream`, `def opening` or `REPOSITORY_RE`, so it brings no one-argument stand-in for the two-argument call. That was read for all 55 open heads at 12:12Z; only this PR's own lines name them.
- No other open PR adds or removes a call of a method of `PROJECT_ID_RE`, which the second guard counts: the lines each of the 55 open heads adds or removes outside `tests/`, `docs/` and `*.md` were read at 12:04Z. Only this PR's own name it, and #1201's one new `MEMBER_RE.fullmatch` is another pattern. So a sibling landing first does not move the guard's table.

Each is a candidate for its own issue. They come from the independent review (rv1166c, READY at `2593952c9`, issuecomment-6352173084), which measured each of them on main and at the head unless the item says it did not. The review's finding numbers are in brackets.

- **The naming-policy patterns [1].** `scripts/repo_shape.py` reads the families, the roles and the topic pattern of `contracts/repository-naming.yaml` with `.match` (`family["_re"].match(name)`, `family["_split_re"].match(name)`, `role["_re"].match(name)`, and `topic_pattern`, which defaults to `^xf-project-[a-z0-9-]+$`). `validate-repository-naming.py --project project.yaml` runs in the root template's `Makefile` and `validate.yml`. At the head, and on main, it exits 0 on a leg `repository: "testorg/Atlas\n"` and prints the line feed inside its table; `-spec` and `-code` the same. The project's gate as a whole still fails, because `validate-manifest.py` now refuses that manifest.
- **`update-upstream-not-a-repo` with a pin [3].** With no `--upstream`, a pin whose `source_repository:` names a folder that exists and is no repository (the review measured `contracts` and `scripts/repo_shape.py`) is `REFUSED update-upstream-not-a-repo: --upstream contracts exists but is not a git repository`, exit 2, on main and here. `pin_path` is in scope at `update-shape.py:686-687`, so that refusal can name the pin as `update-upstream-unresolvable` now does.
- **A pin's line feed when `--upstream` is passed [9].** `update-shape.py check --upstream <clone>` over a pin whose `source_repository:` ends in a line feed exits 0, `nothing to do`. The value is kept as `upstream.repository`, which reaches the README shape line's check, `apply`'s commit subject and the pull request words (`update-shape.py:2525`, `3017` and `5121`). The review measured `check`, not `apply`. It belongs with the previous item.
- **A member row's `id:` [4].** On main, `add` of a member whose `project.yaml` reads `id: "irqs\n"` commits the row `id: "irqs\n"`, and the holder's `validate-family.py` then says `family ok`. Under mutant M8 (this branch with main's `.match` in `add`) the same row is written, and the gate still says `family ok`. By code reading, not measured end to end: `validate-family.py` compares a row's id with the member manifest's by `str` equality (`member-id-mismatch`, `validate-family.py:1608`), and `_check_member_ids_unique` does the same, so neither asks `PROJECT_ID_RE` of a row. This PR makes `add` refuse such a member, so no new holder gets such a row.
- **Values that no pattern reads [8].** The review's sweep found these `manifest ok` or `family ok`, exit 0, with a trailing line feed, at the head and on main alike:
  - in `project.yaml`: `name`, a spec or code leg's `path`, `shape.repository` and `shape.digests.digest_algorithm`;
  - in `family.yaml`: `org`, the family's own `repository`, `tracking_branch`, and the shape's and each member pin's `digest_algorithm`.

  No `^...$` pattern reads them, so they are not this PR's. A leg's `path` is checked only by `_leg_path_findings`, which asks no pattern, and the family's `tracking_branch` is refused in `project.yaml` (`manifest-tracking-branch`, U+000A) but not in `family.yaml`. `shape.commit` and `tree_sha256` with a line feed are also `manifest ok`; that is #1126's.

The first commit's message says "the three readers of an id from a file ask it with `fullmatch` (`manifest-id`, `family-id`, `member-bad-id`)". That is true of those three, but it reads as if there were no others [2]. What the review measured instead:
- `adopt-project.py` still reads the id derived from `legs.assembly` (`:6780`) and the plan's `id:` (`:6791`), both `plan-bad-id`, with `.match`;
- neither lets a line feed through. A plan `id: "northwind\n"` is `plan-manifest-invalid`, now in `manifest-id`'s words. A plan with no `id:` and `legs.assembly: "NorthWind\n"` is `REFUSED unsafe-value: legs.assembly is 'NorthWind\n'`, exit 2, before the derived id is asked, on main and here alike.

So it is the wording alone that overstates. The second commit's `ID_READERS` lists both readers with that reason.

The title's pin clause is the narrower one (the pin's name only where the value is no path and no `owner/repo` name), and two inputs change from `unsafe-value` to `update-upstream-unresolvable` (see "Behaviour changed", above).

Review: rv1166 and rv1166c, lane 5's independent opus reviewers, at `2593952c` (issuecomment-6352173084); then rc1166b, lane 5's independent sonnet re-checker, which wrote none of #1166: READY at `5102b429`, H0 of this landing (issuecomment-6371408915), with no MUST or SHOULD; its NITs 1 and 2 are corrected in the body. CI (Brett Heap's ruling 27 and his fast-refresh ruling of 2026-10-15T22:48:35Z): H0 5102b42 passed tests, tests-git-bash and SonarCloud; the head 5c8afef is a clean update-branch merge of main 93d2931, with tests-git-bash and SonarCloud green; and the local gate passed on its predicted squash tree 3c89065 over main 11560eb: the family_init_values, update_shape_unreadable, value_pattern_line_break, repo_hygiene, agents_md_codes, cli_reference sets, 365 passed, 19 skipped. 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 #1139

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brett Heap and others added 2 commits November 8, 2026 15:10
b1f45fa is #1144's squash on main. Its own files (scripts/shape_advisory.py,
shape-doctor.py, tests/test_doctor_unsearchable_links.py and
tests/test_path_guard_hygiene.py) take the branch's side: S's tree is #1144's
final head 5b38c3a's tree, and merge 1 already brought that in. The merged
tree equals HEAD's tree, c5d0bc7. This is the second of the three merges that
restack #1159 after #1144 landed.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main 0d5704b is five commits past #1144's squash b1f45fa (#1169, #1223,
#1166, #1133, #1104). This is the third of the three merges that restack
#1159 after #1144 landed.

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

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-1, 2026-11-08T15:21:59Z) Lane 1's delta check of #1159 after #1144 landed: the restack to 5370b018 leaves this PR's own change byte for byte as reviewed, so READY carries to 5370b018.

Lane: openreposhape-1 (openRepoShape-1)

brettheap pushed a commit that referenced this pull request Nov 8, 2026
…` committed as a symlink, each failing on #1159's head `5370b018`: to its own file, beside it or through a linked folder, at 000 in or beside the repository, and out of the commit; with controls for a link to nothing, a regular leg and a single repository (#1163)

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Nov 8, 2026
…a committed link through `Tree.text_through`, and `Tree.text` keeps its one argument, so #1159's row that stands in for `Tree.text` with a refusal reads `Refusal` again, not `TypeError` (#1163)

`text(rel, follow=True)` made `_leg_by_agents_md` pass a keyword that the
stand-in in `tests/test_doctor_unsearchable_links.py`'s
`test_a_failure_the_leg_reader_does_not_expect_is_silence_not_advice`
(`def text(self, rel)`) does not take, so `read()`'s net named a
`TypeError` where the row asks for `Refusal`. The decision is unchanged.

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

sonarqubecloud Bot commented Nov 8, 2026

Copy link
Copy Markdown

@brettheap
brettheap marked this pull request as ready for review November 8, 2026 17:03
@sourcery-ai

sourcery-ai Bot commented Nov 8, 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 6 days and 20 hours by commenting @sourcery-ai review. Upgrade to get a review now.

@brettheap
brettheap merged commit 46fe6ac into main Nov 8, 2026
5 checks passed
@brettheap
brettheap deleted the fix/1110-leg-agents-md-unreadable branch November 8, 2026 17:04
brettheap pushed a commit that referenced this pull request Nov 8, 2026
9e4aab8 is #1159's final head: update-branch's merge of main 1212295 into
5370b01, the commit this branch was built on. This is the first of the three
merges that restack #1163 after #1159 landed as 46fe6ac.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Nov 8, 2026
46fe6ac is #1159's squash on main. Its own files (scripts/shape_advisory.py,
shape-doctor.py and tests/test_doctor_unsearchable_links.py) take the branch's
side: S's tree is #1159's final head 9e4aab8's tree, 863f70c, and merge 1
already brought that in. The merged tree equals HEAD's tree. This is the
second of the three merges that restack #1163 after #1159 landed.

Lane: openreposhape-1 (openRepoShape-1)
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

Development

Successfully merging this pull request may close these issues.

1 participant