Repository navigation
shape_advisory.py: a project.yaml or family.yaml committed as a symlink is read through it inside the commit, and a linked single-repository.yaml gets a NOTE that says it is a symlink not followed, never could not be read (YamlError) (#1272) - #1295
brettheap wants to merge 30 commits into
Conversation
…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>
…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>
… 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>
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>
… through it inside the commit `adopt-project.py plan` and `check` read, and one that leads out of the commit leaves whether it is a leg unknown, so neither calls the leg a single repository (#1163) `Tree.text(rel, follow=True)` follows a symlink at `rel` inside the commit as git follows one there (`git cat-file --batch-check --follow-symlinks`): to a file, which is read out of the commit's objects; to nothing that is a file (a link that dangles, loops or runs through a file, or leads to a directory or a submodule), which is no leg's file, as on the disk. A link that leads out of the commit raises `OutOfTheCommit`, and `_read` answers it as a file this user cannot read (#1110): by the leg's name, or `not-answered`, silent. Only `_leg_by_agents_md` asks it; the doctor reads the disk, which the operating system follows either way. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…` 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>
…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>
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>
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>
…th a `.` segment (`./docs/AGENTS.md`, `docs/./AGENTS.md`, `.//docs/AGENTS.md`), which git 2.43's `--follow-symlinks` answers `dangling`, is asked again with those segments dropped, so `plan` and `check` name the leg there too (#1163) r1163's SHOULD-1. git 2.43 looks a `.` segment up as a name no tree holds, so the follow answered `dangling` and the commit reading advised over a leg the doctor names. Where git says `dangling`, `Tree.text_through` now reads the link's own target (`cat-file blob <commit>:AGENTS.md`), and one that holds a `.` segment before its last is asked again with those dropped (`_dot_segments_dropped`). A last `.` or `/` is kept, so a link through a file stays `notdir`. A target that then begins with `..` is out of the commit, as git answers `../x`; git is never asked a name beginning `../`, which it reads from the working directory and refuses (exit 128, measured). A target with a line break, a carriage return or a NUL is not asked again, and only a line that begins with an object's name is a blob to read, since git's line for a name it did not find begins with that name. `text_through`'s docstring no longer says git follows as `open()` does. Rows: `..._is_the_leg_in_the_commit[dot-slash]`, `[dot-in-the-middle]`, `[dot-and-a-double-slash]` and `..._out_of_the_commit_is_not_answered [relative-after-a-dot]` fail on 2abd817; four controls hold the second ask to the disk's answer. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…link to the leg's own text and one byte 0xff, advised by both readers, so the followed blob is held to "a file that is read and is not UTF-8 declares no leg", and decoding it with `errors="replace"` fails a row (#1163) r1163's SHOULD-2. Its mutant R6 (`.decode("utf-8", "replace")` on the blob `Tree.text_through` reads) passed every row, yet it named the leg in the commit where the disk advises. The control writes `docs/AGENTS.md` as bytes (`linked_clone` takes bytes since the previous commit) and asks both readers: each advises, on 2abd817 as here. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…k out of the commit spelled `.//../locked/AGENTS.md` and a leg link that ends in a carriage return get a row each, and `_dot_segments_dropped` says what git 2.43 does with an LF, a CR and a NUL in a target (#1163) r1163b's re-review of #1270: one SHOULD and the first NIT. SHOULD-1: no row pinned a cleaned target that leaves the commit on the second ask, where git answers `symlink`. `[relative-after-a-dot]` is caught by the `..` check before git is asked again. The row `[relative-after-a-dot-and-a-double-slash]` (`.//../locked/AGENTS.md`, cleaned to `/../locked/AGENTS.md`) is. It fails on the base module (`a0b4ab7c`, `advise`), and it kills r1163b's K5 (the second answer's `symlink` read as nothing there) and K12 (the second answer kept only when it is a blob). NIT-1: the control `[dot-and-a-carriage-return]` (`./docs/AGENTS.md\r`, advised by both readers) kills K1, the CR check dropped: git removes a trailing CR from the name it is asked, so it would answer for `docs/AGENTS.md` where the disk has no file of that name. The docstring of `_dot_segments_dropped` now says what git 2.43 does: an LF splits the batch input into two requests, git removes a trailing CR, and cuts the name at a NUL. No code changes; `cat-file -z` is not used. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ymlink is read through it inside the commit, and a linked `single-repository.yaml` is named as a link it does not follow, not `could not be read (YamlError)` (#1272) `adopt-project.py plan` and `check` read the Triad advisory from the commit the plan is written against, where a symlink is a blob holding the path it leads to. `_read_mapping` and `read_record` read that path as YAML, so an assembly root or a family holder whose manifest is a link was advised, and a linked record got the false NOTE `it could not be read (YamlError)`. The doctor reads the disk, through the link. `_read_mapping` now follows a manifest's link inside the commit, through #1270's `Tree.is_link` and `Tree.text_through`. A link to nothing that is a file in the commit, or one out of it, reads as no manifest. `read_record` names a linked record as a symlink it does not follow, and names no exception. Following it would give a linked record the record's question and its default `drop` in `plan` (#528), which waits on Brett Heap's ruling. So `ask_about_a_record_only` and the doctor's leg audit read it as before, but for a link whose path parsed as a record, which `plan` no longer takes for one. tests/test_advisory_record_symlink.py: 35 ids, 18 of them failing on #1270's head `8e54c32de`. Stacked on #1270. Part of #1272 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewer's GuideImplementationUpdate manifest and record readers so committed project.yaml and family.yaml symlinks are followed within the commit, while single-repository.yaml symlinks are explicitly reported as not followed.
TestsAdd regression and control coverage for committed manifest and record symlinks, including invalid or out-of-commit targets and unchanged doctor index behavior.
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
Independent review of #1295 (#1272) for lane openRepoShape-5, by rv1295 (opus), at 16bee38: READY There are no MUST or SHOULD findings, and four NITs. One rule breach is mine, not the PR's: one of my mutant runs had 109 ids, over the 100 cap (details under Mutants). What I measured
1. The change against #1272's Goal and Verify list
2. The choice put up for review: the PR's reading is right
3. The rows
4. Mutants (25 runs: n1272's 13 and my 6, every file proved back at its sha256 after each)
5. Gates at
6. Merge-trees
7. The body's claims and counts
NITs
Left behind: both trees are clean at Lane: openreposhape-5 (openRepoShape-5) |
…ontrols, and new rows pin a leg's index reading a linked record as before with the link committed too, and a regular manifest under a git that cannot follow a link (#1272) rv1295's review of #1295 found no MUST or SHOULD and four NITs. This commit takes the three that touch the file: NIT 1: the module docstring said every case makes a symlink. The controls of regular files and of a git that cannot list the commit or follow a link make none, and run everywhere. NIT 3 (mutant R5, `Tree.is_link` asking HEAD where the Tree reads the index): the doctor's audit of a leg's index reads a linked record as before, which rests on git refusing `ls-tree ""`. The row had the link only staged, with a regular record in HEAD. Two ids commit the link in the leg too, and `[spelled-as-a-record-committed-too]` fails under R5. NIT 4 (mutant R3, `_read_mapping` following every manifest, link or not): a stand-in git whose `cat-file --follow-symlinks` exits 128, as #1270's `test_a_git_that_cannot_follow_the_link_is_silence_not_advice` has, for a regular `project.yaml` and `family.yaml`. They are read as the blob they are, so the commit still says `triad-root` and `family-holder`. Two ids, both failing under R3. Test only: `scripts/shape_advisory.py` is unchanged. The four new ids are controls: they pass on #1270's head as here. Stacked on #1270. Part of #1272 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
Delta check of #1295 (#1272) for lane openRepoShape-5, by rv1295b (sonnet), 16bee38..0c3999d: READY I found no MUST, no SHOULD and no NIT in the delta. There are two observations below. I wrote nothing to GitHub: reads only ( 1. Nothing else changed
2. NIT 1 (docstring and body sentence) is true
3. R5, R3 and my rivals (the mut tree is
4. The 4 new ids are controls
5. Gates at
6. Merge-trees (object-only, on git 2.43.0 and 2.47.1, with the same results)
7. The record
Observations (not findings)
I started the mutant script once by mistake without Lane: openreposhape-5 (openRepoShape-5) |
de55beb) into fix/1272-record-symlink Step 1 of the three-merge restack of #1295 after #1270 landed as a squash. 0c19a3c is b24382a (the head this branch carried) plus lane 1's refresh with main; the merge is clean and changes no file this PR owns. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ape_advisory.py took the branch's side Step 2 of the three-merge restack of #1295. #1270's final head was 0c19a3c (merged in the step before); its squash on main is 3bf35b2 (parent de55beb), which adds the same two files as that head. One conflicted file, the one #1270 itself changed: scripts/shape_advisory.py, two hunks. - the Tree class docstring: this PR's text says that project.yaml and family.yaml are read through a link too (_read_mapping, #1272) and that single-repository.yaml is not followed (read_record); the squash's older text says only a leg's AGENTS.md is read so. Resolved to the branch's side. - Tree.text_through's docstring: "as AGENTS.md and the two manifests are" against "as AGENTS.md is". Resolved to the branch's side. The resolved file's blob is 129a4c8, equal to 0c3999d's. The squash's own copy of the file (blob 9122f59) equals b24382a's and 0c19a3c's, so main changed nothing in it after #1270's head. tests/test_advisory_agents_md_symlink.py merged without conflict: the squash's blob 62bf0a2 equals the branch's. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|



Was stacked on #1270 (lane 1's, for #1163), which landed on main as a squash,
3bf35b22e, at 06:24:35Z on 2026-11-10. This PR was restacked onto main by three merges, with no rebase (see The restack after #1270's squash), and its base is main. It was made at #1270's head8e54c32de, which addsTree.is_linkandTree.text_throughtoscripts/shape_advisory.py. Against main the diff is this PR's own change: 2 files, +557/-4, from two commits of its own,126165514and the round after rv1295's review (0c3999d5a, tests only). The merge commitsc6d9a9ab4and129f4dce8(the restack) change no line of them.Part of #1272
Goal
adopt-project.py planandcheckread aproject.yamlor afamily.yamlthat the commit holds as a symlink the way the doctor reads it on the disk. They also say plainly that they do not follow asingle-repository.yamlheld as one, so noit could not be read (YamlError)NOTE is printed over a record that can be read. Whether the record's link should be followed is a ruling not yet made (see The ruling left open), so this PR does not follow it.What held at #1270's head (measured)
Measured with git 2.43.0 and CPython 3.12.3, in the probe environment: a stub
ghfirst on PATH, a bogusGH_TOKEN, an emptyGH_CONFIG_DIR,GIT_CONFIG_GLOBAL=/dev/nullandGIT_SSH_COMMAND=false, with no network. Each fixture is a one-commit repository in a folderAtlaswhoseoriginisacme/Orbiton github.com, never contacted, as #1272's are. Each gotplan --source <Atlas> --project Orbit --org acme ..., thencheck --plan, thenshape_advisory.read()of the commit and of the disk, in process. The same 15 cases gave the same answers at8e54c32deand atb24382a87, and the same answers at this change on both bases.single-repository.yaml -> docs/record.yaml, a recordadvise, NOTEit could not be read (YamlError)advise, the link's NOTE (below)recorded-single./docs/record.yamlYamlErrorNOTErecorded-singleYamlErrorNOTEadvise, NOTEit declares kind 'release-config', ...YamlErrorNOTEadvise, no NOTEdocsYamlErrorNOTEadvise, NOTEit is not a file-> ../elsewhere/record.yaml, a record out of the commitYamlErrorNOTErecorded-single-> kind: single-repository-record(that is the PATH; it dangles)recorded-single; entryleg: drop, ruleambiguous-single-repository-record;checkexits 0advise, the link's NOTE; entryleg: null, ruleambiguous-top-level-yaml;checkexits 1advise, no NOTErecorded-single;leg: drop;checkexits 0recorded-singleproject.yaml -> docs/project.yaml, a Triad manifestadvise: the twotriadlinestriad-root: no Triad linetriad-rootfamily.yaml -> docs/family.yaml, a holder's manifestadvise: the twotriadlinesfamily-holder: no Triad linefamily-holderfamily.yaml -> kind: family-manifest(the PATH; it dangles)family-holderadviseadviseproject.yaml -> ../elsewhere/project.yaml, a Triad manifest out of the commitadviseadvisetriad-rootproject.yaml -> docs/project.yaml, committed dangling and written on the disk afteradviseadvisetriad-rootproject.yaml(control)triad-roottriad-rootcheckare the same on both sides:leg: null, ruleambiguous-top-level-yaml, andcheckexits 1 withplan-unresolved.planexits 0 in every row.checkon both sides, link and control alike: a top-levelproject.yamlorfamily.yamlof the repository's own is an open question there. A linkedproject.yamladdsplan-root-symlinkon both sides. Only the printed advisory differs.single-repository.yamlcommitted as a symlink is read as its path, soplanandcheckadvise with a false(YamlError)NOTE where the doctor saysrecorded-single(#1163) #1272, theproject.yamllink beside a regular spec leg'sAGENTS.md, readleg-cloneat shape_advisory.py: a leg'sAGENTS.mdcommitted as a symlink is read through it inside the commit, and one out of the commit leaves whether it is a leg unknown, soadopt-project.py planandcheckstop calling the leg a single repository (#1163) #1270's head, andplanandcheckprinted the leg's NOTE. It readstriad-roothere, with no NOTE (row[project-yaml-beside-a-leg-agents-md]).shape-doctor.py's placement row callsask_about_a_record_onlywithSource(mount, None, "", ""), so the record is read throughTree(mount, ""). Theregit ls-tree --full-tree "" -- single-repository.yamlexits 128 ("fatal: Not a valid object name"), sois_linksays no link.read_recordover such an index gives('silences-nothing', ['it could not be read (YamlError)'])for a link to a record in the leg, and('silences', [])for the spelled link, at shape_advisory.py: a leg'sAGENTS.mdcommitted as a symlink is read through it inside the commit, and one out of the commit leaves whether it is a leg unknown, soadopt-project.py planandcheckstop calling the leg a single repository (#1163) #1270's head and here alike.What changed
Only
scripts/shape_advisory.py.adopt-project.pyandshape-doctor.pyare untouched._read_mappingfollows a manifest's link inside the commit, forproject.yamlandfamily.yaml. It readstree.text_through(rel) if tree.is_link(rel) else tree.text(rel), as shape_advisory.py: a leg'sAGENTS.mdcommitted as a symlink is read through it inside the commit, and one out of the commit leaves whether it is a leg unknown, soadopt-project.py planandcheckstop calling the leg a single repository (#1163) #1270's_leg_by_agents_mdreads a leg'sAGENTS.md.text_through. One that leads out of the commit raisesOutOfTheCommit._read_mappingalready swallows both, as it swallows every manifest it cannot read, so each reads as no manifest. The plan is written against the commit, and a manifest the commit does not hold declares nothing in it. The link's own path is never read as the manifest.is_linkis always False.read_recorddoes not follow the record's link, and names it. Afterkind()says "file", it askstree.is_link(RECORD_FILE). A link silences nothing, with the new constantRECORD_LINK_NOT_FOLLOWED, for every link, whatever it leads to.planandcheckprint:ask_about_a_record_onlyasks only whether the record silences, so a linked record is still "no record" there: the general question,leg: null, andcheckexits 1, as before. The one exception is the spelled link (see Known limits).TreeandTree.text_throughsay which readers follow a link now.read(commit=...)makes onegit ls-treeper manifest, and one more where a record is a file.Rows
tests/test_advisory_record_symlink.pyis new, with 39 ids (35 at126165514, 4 added in the round after rv1295's review) beside #1270'stests/test_advisory_agents_md_symlink.py. It imports what it needs from the files that own it:AGENTS.mdcommitted as a symlink is read through it inside the commit, and one out of the commit leaves whether it is a leg unknown, soadopt-project.py planandcheckstop calling the leg a single repository (#1163) #1270's file:committed,adopt_says,told_nothingandSINGLE;AGENTS.mdthis user cannot read leaves whether it is a leg unknown, sothe way inoffers neither command and points atwhat is here, andthe other way inand the Triad row are silent (#1110) #1159's:no_workspaceandsymlink;test_shape_advisory.py:commit_all,doctor_json,record_text,rendered_leg_agentsandrepository;test_adopt_single_repository_record.py:entry_of,leg_audited,GENERALandRULE.It imports no fixture. Its one builder,
linked_here, links the name under test where #1270'slinked_clonelinksAGENTS.md. Every case that needs a symlink makes one and skips where none can be made. The 9 controls of regular files make none and run everywhere: 4 intest_the_three_files_committed_as_regular_files_read_as_before, 3 intest_a_git_that_cannot_list_the_commit_reads_a_regular_file_as_beforeand 2 intest_a_git_that_cannot_follow_a_link_reads_a_regular_manifest_as_before.The new file was run at #1270's production code: its untracked copy, in a detached tree at
8e54c32de, gave 18 failed and 17 passed (11.34 s). Every failure is the defect: theYamlErrorreason where the link's is asserted (11 rows),adviseorleg-clonewhere the disk's state is asserted (5),family-holderfor the spelled manifest link, andrecorded-singlefor the spelled record link. Atb24382a87, #1270's head now, the same 18 fail and 17 pass (9.90 s). At this head all 35 pass. The round after rv1295's review added 4 ids, all controls. Withb24382a87's production file swapped in (sha256e0b0ef03) the 39 give the same 18 failed and 21 passed (12.05 s), the 4 among the passes, and at this head all 39 pass.test_a_manifest_linked_inside_the_commit_is_read_through_it:[project-yaml],[project-yaml-beside-a-leg-agents-md],[family-yaml],[project-yaml-dot-slash],[family-yaml-through-a-linked-folder]. These are #1272's rows 2a, 2b and 3: both readings, no Triad line or leg NOTE inplanorcheck, andcheck's findingstest_a_manifest_link_to_no_file_in_the_commit_is_no_manifest[spelled-as-a-manifest][dangling],[loop],[a-directory],[through-a-file],[not-utf-8],[dangling-in-the-commit-there-on-the-disk],[out-of-the-commit],[out-of-the-commit-by-its-absolute-path]. The commit readsadvise; where the disk readstriad-root,planandcheckprint the advisorytest_a_record_linked_to_a_record_in_the_commit_is_named_as_a_link. This is #1272's row 1: the exact NOTE line once in each ofplanandcheck, nocould not be read, the entryleg: nullunderambiguous-top-level-yaml,check's one findingplan-unresolved, and the doctor's exit 2 with no Triad rowtest_every_link_to_the_record_is_named_as_one_and_never_followed:[dot-slash],[through-a-linked-folder],[another-kind],[dangling],[loop],[a-directory],[through-a-file],[not-utf-8],[out-of-the-commit],[out-of-the-commit-by-its-absolute-path]test_a_record_link_whose_path_is_spelled_as_a_record_is_no_recordtest_the_doctors_audit_of_a_legs_index_reads_a_record_link_as_before:[to-a-record-in-the-leg](the general question) and[spelled-as-a-record](the record's), the link staged in the leg's index only, and[to-a-record-in-the-leg-committed-too],[spelled-as-a-record-committed-too], the same with the link committed in the leg as well (R5)test_the_three_files_committed_as_regular_files_read_as_before:[project-yaml],[project-yaml-beside-a-leg-agents-md],[family-yaml],[single-repository-yaml]. These are #1272's controls 2a-c, 2b-c, 3c and 1c: the entry, andcheck's findings (none for the record)test_a_git_that_cannot_list_the_commit_reads_a_regular_file_as_before(stand-in:ls-treerefused):[project-yaml],[family-yaml],[single-repository-yaml]test_a_git_that_cannot_follow_a_link_reads_a_regular_manifest_as_before(stand-in:cat-file --follow-symlinksexits 128):[project-yaml],[family-yaml](R3)Mutants
19 mutants, 19 killed: the 13 of the first round (M1-M10), and rv1295's own six (R1-R6), of which R3 and R5 survived rv1295's run of the 35 ids and are killed by the new ids. All 19 were run again against this round's file. Each was applied alone in a detached tree (
16bee386dwith this round's test file laid in, its blob equal to0c3999d5a's), and the new file's 39 ids were run, one run each. After each, the file was written back from the bytes saved before it and proved equal by sha256:scripts/shape_advisory.py40a766ca7adf,adopt-project.py6e2402aaeec3,shape-doctor.py140100bcaae9. The counts of M6b, M8 and M10 grew by the new ids that also fail them; every other count is the first round's._read_mapping..._read_through_it[project-yaml]and the other four,[spelled-as-a-manifest]project.yamlonly, notfamily.yaml..._read_through_it[family-yaml]..._is_no_manifest[spelled-as-a-manifest]..._is_no_manifest[out-of-the-commit],[...-by-its-absolute-path]test_a_record_linked_to_a_record_in_the_commit_is_named_as_a_link, everytest_every_link_..., the spelled record... (OutOfTheCommit))it could not be read (Symlink)ask_about_a_record_onlygives a linked record the record's rule..._named_as_a_link, the spelled record (leg: drop)ask_about_a_record_onlyhanded the disk's tree, which follows a link..._named_as_a_link, the four leg-audit controlsread_record(the link's path parsed, as before)Source(mount, None, "", None)), following whereplandoes noterrors="replace"(r1163's R6)..._is_no_manifest[not-utf-8]is_linksays link where git cannot answer[single-repository-yaml], the leg-audit[spelled-as-a-record]and[spelled-as-a-record-committed-too]_read_mappingletsOutOfTheCommitescape, #1270's silence for a manifest out of the commit..._is_no_manifest[out-of-the-commit],[...-by-its-absolute-path]..._named_as_a_link, everytest_every_link_..., the spelled record_read_mappingfollows every manifest, link or nottest_a_git_that_cannot_follow_a_link_reads_a_regular_manifest_as_before[project-yaml]and[family-yaml], both new..._is_no_manifest[dangling-in-the-commit-there-on-the-disk],[out-of-the-commit],[...-by-its-absolute-path]is_linkasks HEAD where the Tree reads the index (commit"")..._reads_a_record_link_as_before[spelled-as-a-record-committed-too], new126165514's files on8e54c32de, where 12 of the same 13 were killed. The[not-utf-8]manifest then put the byte 0xff after the manifest's last line. Witherrors="replace"that made a last line of its own, which does not parse, so the mutant read no manifest either. The row now puts the byte inside a value (name: Atlas<0xff>), and M9 fails it. All 13 were then run again at this head's files, with the table's result.Runs
The gate ran at
0c3999d5aon git 2.43.0 and on git 2.47.1 (built from kernel.org's tarball), each leg in 8 calls of at most 78 ids (620 ids: the 616 of the first gate and the 4 added in the round after rv1295's review, collected at this head). It used CPython 3.12.3, pytest 9.1.1 and git-filter-repo 2.47.0 (CI's pin), in the probe environment,-rsalone, one basetemp per call, at most two calls at once, the tree clean. Each leg gave 618 passed and 2 skipped. The files are the new file,tests/test_advisory_agents_md_symlink.py,tests/test_shape_advisory.py,tests/test_adopt_single_repository_record.py,tests/test_doctor_recorded_answers.py, the hygiene five (test_agents_md_codes.py,test_cli_reference.py,test_path_guard_hygiene.py,test_pinned_copy_text.py,test_repo_hygiene.py) andtests/test_no_auto_maintenance.py.tests/test_advisory_record_symlink.py(39),tests/test_advisory_agents_md_symlink.py(27),tests/test_shape_advisory.py(12)tests/test_shape_advisory.py(35),tests/test_adopt_single_repository_record.py(39),tests/test_doctor_recorded_answers.py(4)tests/test_doctor_recorded_answers.py(25),tests/test_agents_md_codes.py(47),tests/test_cli_reference.py(6)tests/test_cli_reference.py(17),tests/test_path_guard_hygiene.py(61)tests/test_path_guard_hygiene.py(60),tests/test_pinned_copy_text.py(18)tests/test_pinned_copy_text.py(69),tests/test_repo_hygiene.py(9)tests/test_repo_hygiene.py(78)tests/test_repo_hygiene.py(14),tests/test_no_auto_maintenance.py(60)tests/test_no_auto_maintenance.py:222: "the session began with GIT_CONFIG_GLOBAL set, and its file includes that one". The probe environment setsGIT_CONFIG_GLOBAL=/dev/null.16bee386d(616 ids): 614 passed and 2 skipped on each git. rv1295 ran 815 ids there, which included 18 rows oftests/test_shape_doctor.pyand the doctor's other rows that readshape_advisory: 813 passed and 2 skipped on both gits.scripts/shape_advisory.pyand every other production file are unchanged in this round, so those were not run again.adopt-project.pyis unchanged, sotests/test_shape_doctor.pyandtests/test_scaffold_leg_path_collisions.pywere not run. The Python 3.14 shim was not run.Merges
Read fresh after the restack and before the push (
git fetchof main, andgh pr list, read only, 30 open PRs): main3bf35b22e, which is #1270's squash, and #1270 MERGED at its final head0c19a3c77. This head contains both, so a merge of either adds nothing. No other open PR touchesscripts/shape_advisory.py,tests/test_advisory_record_symlink.pyortests/test_advisory_agents_md_symlink.py. #1278 and #1171 touchtests/test_pinned_copy_text.py, which the gate runs and this PR does not change.git merge-tree --write-tree3bf35b22ee3ca2459c, this head's own tree (main is an ancestor)0c19a3c77e3ca2459c, this head's own tree (an ancestor)3bf35b22eThe 29 other open PRs: 19 of them touch
adopt-project.py,shape-doctor.pyortests/conftest.py, which these rows run, and none touches this PR's two files. A change tocheck's findings for a root link would show in the rows that assert them.The ruling left open
Whether a
single-repository.yamlcommitted as a symlink to a file inside the commit should be followed. Followed, it would take the record ruleambiguous-single-repository-record, with the defaultdropthat Brett Heap ruled on 2026-10-12 for a regular record (contracts/path-classification.yaml,adopt-project.py'sask_about_a_record_only, #528). This PR leaves every such link unfollowed, with a true NOTE. #1272's measurement 4 shows what following would do:recorded-single,leg: drop, andcheckexits 0, as for a regular record. What the ruling may weigh:dropdoes to a link.dropremoves the linksingle-repository.yaml. Its target (docs/record.yamlin shape_advisory.py: asingle-repository.yamlcommitted as a symlink is read as its path, soplanandcheckadvise with a false(YamlError)NOTE where the doctor saysrecorded-single(#1163) #1272's fixture) is its own path, under its own rule, and goes where that rule sends it: the spec leg, in the fixture. The rule's reason ("the file no longer describes the project it sits in") assumes the file is the record.Tree(mount, "")), wheregit ls-treehas nothing to list andis_linkcannot answer. Following inplanalone would splitplanfrom the doctor's audit of a leg (shape-doctor.py: theplacementrow asks a leg's top-levelsingle-repository.yamlthe staying-single record's question by its name alone, so a file that declareskind: release-configis told it records that the repository stays single, on every run of a project adopted before #617 #691,tests/test_doctor_recorded_answers.py), unless the audit moves too: throughgit ls-files -s, say, which is not used here.planand in the leg audit, until the ruling settles both readers.Not the record's ruling, but a choice for review: a manifest's link out of the commit reads as no manifest here, so
planadvises where the doctor names the root. #1270 chose silence (not-answered) for a leg'sAGENTS.mdout of the commit. This PR reads such a manifest as_read_mappingreads any manifest it cannot read: as none.update-shape.pyalready refuses to re-pin a root whose manifest is a link (update-rewrite-symlink).Known limits
plan, but is by the doctor's leg audit.single-repository.yaml -> kind: single-repository-recordreadrecorded-singleat shape_advisory.py: a leg'sAGENTS.mdcommitted as a symlink is read through it inside the commit, and one out of the commit leaves whether it is a leg unknown, soadopt-project.py planandcheckstop calling the leg a single repository (#1163) #1270's head, withleg: dropandcheckexiting 0, from the path alone. Here it is the link's NOTE, the general question, andcheckexits 1, as for every other link. The doctor's audit of a leg's index still reads that path as a record, as it documents ("a link is read as its blob and never followed"). That is the only case whereask_about_a_record_only's answer moves. Both are pinned by rows.triad-root. This is the stated choice above.AGENTS.mdcommitted as a symlink is read through it inside the commit, and one out of the commit leaves whether it is a leg unknown, soadopt-project.py planandcheckstop calling the leg a single repository (#1163) #1270's squash is done. shape_advisory.py: a leg'sAGENTS.mdcommitted as a symlink is read through it inside the commit, and one out of the commit leaves whether it is a leg unknown, soadopt-project.py planandcheckstop calling the leg a single repository (#1163) #1270 landed as a squash, and a plain merge of main alone would have conflicted inscripts/shape_advisory.py(rv1295 measured it). This PR was restacked by three merges, with no rebase: shape_advisory.py: a leg'sAGENTS.mdcommitted as a symlink is read through it inside the commit, and one out of the commit leaves whether it is a leg unknown, soadopt-project.py planandcheckstop calling the leg a single repository (#1163) #1270's final head0c19a3c77, then its squash3bf35b22e(the one conflicted file,scripts/shape_advisory.py, resolved to this branch's side), then main (already contained). See The restack after shape_advisory.py: a leg'sAGENTS.mdcommitted as a symlink is read through it inside the commit, and one out of the commit leaves whether it is a leg unknown, soadopt-project.py planandcheckstop calling the leg a single repository (#1163) #1270's squash.The round after rv1295's review
rv1295 found this PR READY at
16bee386d, with no MUST or SHOULD and four NITs (#1295 issuecomment-6757105722). This round takes them as rv1295 measured them and changes nothing else: one commit,0c3999d5a, intests/test_advisory_record_symlink.pyonly.scripts/shape_advisory.pyand every other production file are unchanged. #1270's head was read fresh and is stillb24382a87, so no merge of it was needed.Tree.is_linkasking HEAD where the Tree reads the index, whichls-tree ""refuses to list). The leg-audit control staged the link in the leg's index only, with a regular record in HEAD, so asking HEAD changed nothing. A row pins it, and no guard inread_recordis needed:test_the_doctors_audit_of_a_legs_index_reads_a_record_link_as_beforetakes a new parameter, and the ids[to-a-record-in-the-leg-committed-too]and[spelled-as-a-record-committed-too]commit the link in the leg as well. Under R5,[spelled-as-a-record-committed-too]fails: the audit would ask the general question where it asks the record's. R5 is KILLED._read_mappingfollowing every manifest, link or not). The two readings differ only under a git that cannot follow.test_a_git_that_cannot_follow_a_link_reads_a_regular_manifest_as_beforeis a stand-in row like shape_advisory.py: a leg'sAGENTS.mdcommitted as a symlink is read through it inside the commit, and one out of the commit leaves whether it is a leg unknown, soadopt-project.py planandcheckstop calling the leg a single repository (#1163) #1270'stest_a_git_that_cannot_follow_the_link_is_silence_not_advice, for a regularproject.yamlandfamily.yaml:cat-file --follow-symlinksexits 128, and the commit must still saytriad-rootandfamily-holder. Under R3 both fail. R3 is KILLED.AGENTS.mdcommitted as a symlink is read through it inside the commit, and one out of the commit leaves whether it is a leg unknown, soadopt-project.py planandcheckstop calling the leg a single repository (#1163) #1270 lands as a squash, restack by three merges.AGENTS.mdcommitted as a symlink is read through it inside the commit, and one out of the commit leaves whether it is a leg unknown, soadopt-project.py planandcheckstop calling the leg a single repository (#1163) #1270's production file as they do here, so the rows failing at shape_advisory.py: a leg'sAGENTS.mdcommitted as a symlink is read through it inside the commit, and one out of the commit leaves whether it is a leg unknown, soadopt-project.py planandcheckstop calling the leg a single repository (#1163) #1270's head are still the 18. Their value is in the mutants: R3 and R5 each fail only the new ids.The restack after #1270's squash
#1270 (lane 1's, for #1163) landed at 06:24:35Z on 2026-11-10 as the squash S70 =
3bf35b22e(parentde55beb03; #473 issuecomment-6760664498). Lane 1 had refreshed it first, so its final head H70 is0c19a3c77(b24382a87plus a merge of mainde55beb03), not theb24382a87this PR's earlier text names. GitHub deleted #1270's branch and retargeted this PR to main, where it was CONFLICTING at0c3999d5a. This round restacks it by three merges, by hand, with no rebase, and changes nothing else: no line of this PR's two files.0c19a3c77gavec6d9a9ab4. It was clean: the 29 files it brought are main's (lane 1's refresh merged main), none of them this PR's.3bf35b22egave129f4dce8, with one conflict, inscripts/shape_advisory.py, the file shape_advisory.py: a leg'sAGENTS.mdcommitted as a symlink is read through it inside the commit, and one out of the commit leaves whether it is a leg unknown, soadopt-project.py planandcheckstop calling the leg a single repository (#1163) #1270 and this PR both changed. It had two hunks, both in docstrings: theTreeclass docstring (this PR's text saysproject.yamlandfamily.yamlare read through a link too,_read_mapping, shape_advisory.py: asingle-repository.yamlcommitted as a symlink is read as its path, soplanandcheckadvise with a false(YamlError)NOTE where the doctor saysrecorded-single(#1163) #1272, and thatsingle-repository.yamlis not followed,read_record; the squash's text says only a leg'sAGENTS.mdis read so) andTree.text_through's docstring ("asAGENTS.mdand the two manifests are" against "asAGENTS.mdis"). Both were resolved to this branch's side. The resolved blob129a4c86b7ffequals0c3999d5a's. S70's own copy of the file (9122f59bd22b) equalsb24382a87's and H70's, so main changed nothing in it after shape_advisory.py: a leg'sAGENTS.mdcommitted as a symlink is read through it inside the commit, and one out of the commit leaves whether it is a leg unknown, soadopt-project.py planandcheckstop calling the leg a single repository (#1163) #1270's head.tests/test_advisory_agents_md_symlink.pymerged without a conflict: S70's blob62bf0a2220d4equals this branch's. The merge commit's tree equals merge 1's.3bf35b22e, fetched fresh just before this step and again before the push, is S70 itself, and an ancestor of the head:Already up to date, no commit.129f4dce8:git diff origin/main HEADtouches exactlyscripts/shape_advisory.pyandtests/test_advisory_record_symlink.py: 2 files, +557/-4;-U0changed lines (561 of them, sha25685cc8b101b02) are identical to those ofgit diff b24382a87 0c3999d5a, the reviewed change, with no difference;git merge-tree --write-tree origin/main HEADgivese3ca2459c, the head's own tree;0c3999d5a's:scripts/shape_advisory.py129a4c86b7ff,tests/test_advisory_record_symlink.py49630a405c18.scripts/shape_advisory.pyis still the Mutants section's sha25640a766ca7adf, and so isshape-doctor.py,140100bcaae9. Main changedadopt-project.pysince (sha2566e2402aaeec3in the Mutants section,2d5459ab2feeat this head, all main's), and M6a and M6b ran against the older file; the gate below runs every row against this head's.129f4dce8, the tree clean, on git 2.43.0 and on git 2.47.1 (built from kernel.org's tarball), each leg in 8 calls of at most 78 ids (620 ids, the same list as the Runs section's, collected again at this head: 0 ids differ). It used CPython 3.12.3, pytest 9.1.1 and git-filter-repo 2.47.0 (CI's pin), in the probe environment,-rsalone, one basetemp per call, at most two calls at once. Each leg gave 618 passed and 2 skipped, in the same files as the Runs section. The 2 skips aretests/test_no_auto_maintenance.py:222in call 8, "the session began with GIT_CONFIG_GLOBAL set, and its file includes that one" (the probe environment setsGIT_CONFIG_GLOBAL=/dev/null).adopt-project.pyis unchanged by this PR, sotests/test_shape_doctor.pyandtests/test_scaffold_leg_path_collisions.pywere not run; the Python 3.14 shim was not run.tests/test_advisory_record_symlink.py(39),tests/test_advisory_agents_md_symlink.py(27),tests/test_shape_advisory.py(12)tests/test_shape_advisory.py(35),tests/test_adopt_single_repository_record.py(39),tests/test_doctor_recorded_answers.py(4)tests/test_doctor_recorded_answers.py(25),tests/test_agents_md_codes.py(47),tests/test_cli_reference.py(6)tests/test_cli_reference.py(17),tests/test_path_guard_hygiene.py(61)tests/test_path_guard_hygiene.py(60),tests/test_pinned_copy_text.py(18)tests/test_pinned_copy_text.py(69),tests/test_repo_hygiene.py(9)tests/test_repo_hygiene.py(78)tests/test_repo_hygiene.py(14),tests/test_no_auto_maintenance.py(60)Lane: openreposhape-5 (openRepoShape-5)
🤖 Generated with Claude Code
Summary by Sourcery
Handle committed YAML symlinks accurately in shape advisories by following manifests and clearly reporting record links that are intentionally not followed.
Bug Fixes:
project.yamlandfamily.yamlsymlinks through their targets when evaluating repository manifests.single-repository.yamlsymlinks explicitly as un followed instead of incorrectly reporting YAML read failures.Enhancements:
Tests: