Repository navigation
shape-doctor.py and shape_advisory.py: a .git or an agent file linked into a folder this user cannot search is said once in what is here or agent files, <name>: cannot be read (<reason>), never absent; a .git offers no way in (#1110) - #1144
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>
Reviewer's GuideImplementationMake unreadable
Report unreadable agent files as present, preserving pinned-file findings and the appropriate repair command and advice.
Have “what is here” name unreadable root entries once, with the doctor command and the cause-specific advice.
TestsAdd regression coverage for unreadable links, absent-path controls, naming and advisory behavior, and pinned versus rendered agent files.
Remove obsolete open-path exceptions now covered by the new guarded reads.
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
(lane openRepoShape-1, 2026-10-21T22:41:03Z) r1144, lane 1's independent review of #1144 at
The full reviewr1144: independent review of #1144 at Verdict: READY on conditionNo MUST findings. The code does what the body says for the The two conditions:
FindingsMUSTNone. SHOULDS1. The title and design point 1 overclaim for agent files. An unreadable leg
S2. Seven mutants of the new code survive every test I ran. Each survived the 17 new rows and a broad set of 298 tests: the new rows,
NIT
Observation, outside this PRA shape root whose own
Body claims
Base against head, measuredIn the tables, "the probes" means the doctor run over the fixture with
My mutants
Merge with main
A process slip on my sideMy first shard launch used
I made no GitHub write, commit or push. FilesEverything is under
Lane: openreposhape-1 (openRepoShape-1) |
…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>
.git or an agent file in the root linked 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).git or an agent file linked into a folder this user cannot search is said once in what is here or agent files, <name>: cannot be read (<reason>), never absent; a .git offers no way in (#1110)
|
(lane openRepoShape-1, 2026-10-22T02:50:40Z) r1144b, lane 1's independent re-check of the fix round at
The full re-checkVerdict: READY. No MUST and no SHOULD. One NIT, which needs no action. Head is Checks1. The diff. 2. S2, with the rows that kill each mutant. I ran each of the seven alone against the whole file on 3.12.
3. The new rows.
4. S1.
5. N1–N6.
6. The body.
7. Runs on head.
8. CI at 02:49:52Z.
FindingsMUST: none. SHOULD: none. NIT
Not run: Python 3.14, Windows, and the State left behind: I made no commit, push or GitHub write. My worktree is clean at Paths
Lane: openreposhape-1 (openRepoShape-1) |
…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>
…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>
|
|
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 5 hours by commenting |
…carries #1144's change to shape-doctor.py and scripts/shape_advisory.py Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…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>
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>
…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) On #1144's head, `shape_advisory.py`'s `_leg_by_agents_md` read any exception as "no leg's file". When a leg's `AGENTS.md` was one this user cannot read, as a link into a folder at mode 000 or a file at mode 000, whether the folder is a leg was unknown. The doctor still 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". That held on 3.12 and on a 3.14-like shim, where `Path.is_file` and the rest answer False to any `OSError`. - `_leg_by_agents_md` answers None only for an `OSError` whose errno says nothing is there (ENOENT, ENOTDIR, EBADF and ELOOP, and Windows' 21, 123 and 1921), as #430's rule has it. Any other `OSError` is raised. Bytes that are not UTF-8 still declare no leg, by #385's rule. A `Refusal` ends in `read()`'s net, `not-answered`. - A leg's name still answers. On a refused `AGENTS.md`, or a `Refusal` from `cat-file blob` in `adopt-project.py`'s reading of a commit, `_read` turns to `_leg_by_name` first, so a folder named as a leg is still `leg-clone` and `plan` and `check` print its note. With no such name, the answer is `not-answered`, and the refused `OSError` is kept in the new `Reading.refused`, which `as_dict` does not carry. - The doctor raises what the advisory could not read. `leg_clone` raises `reading.refused`. `check_the_way_in` asks it right after `carries_git`, and on an `OSError` gives `way_in_unread_row("AGENTS.md", ...)`, shared with the `.git` row, whose words are unchanged. The row says 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. In `--json` it keeps `origin_org`. `check_the_other_way_in` is silent. - `what is here` reads a regular `AGENTS.md` through (`read_through`), so a file at mode 000, whose `stat` succeeds, is named there with the cure. A directory or a named pipe called `AGENTS.md` is listed and not read, since the open of a pipe nobody writes to blocks the doctor for ever. Only `AGENTS.md` is read through. - `tests/test_doctor_unsearchable_links.py` gains 24 rows, 49 in all. Two of #1144's rows change: one expects `PermissionError` from `leg_clone` over a refused `.git`, and one also checks the type of `reading.refused`. - Measured with `shape-doctor.py --root <fixture>/Atlas`, base #1144's head `89486f69` against `0ca33611`, 32 runs, on 3.12 and on the shim: - Over a locked `AGENTS.md` of the spec or the code leg, as a link or at mode 000, base gave NEXT `adopt-project.py plan` in `the way in`, NEXT `setup.sh` in `the other way in` and a Triad row. Head gives the reason above and no NEXT, with no `the other way in` row and no Triad row. Exit 2 at both, and `what is here` names the file with the cure. - The readable fixtures are byte-identical to base (8 of 8). Every report on 3.12 equals its report under the shim (32 of 32). - In process, `read()` over a locked file went from `advise` to `not-answered`, and `leg_clone` from False to raising `PermissionError`. - `adopt-project.py plan` and `check` give equal output on base and head over 64 runs, and the tool is not changed. Where git cannot give a commit's `AGENTS.md` and no name is a leg's, `plan` and `check` now print no advisory where base printed the advice. No fixture meets this. - The new file at base fails 17 rows and passes 32, on 3.12 and on the shim alike (the same 17), each on the row's own assertion. At head 49 pass on both. - Mutants: 31, of which 28 are killed. The 3 survivors are equivalent: `_leg_by_agents_md` also catching `ValueError` (`UnicodeDecodeError` is the only one `text()` can raise), `check_the_other_way_in` asking `leg_clone` first (`leg_clone` raises a `.git` it cannot read as `carries_git` does), and `_nothing_there` dropping the Windows arm (CI is Linux only). - Known limits: - A leg's file that is not UTF-8 is still "no leg's file", by #385's rule against salvaging text that is not UTF-8. Over such a file the doctor still offers adoption and the Triad advisory, on base and head alike. Two rows hold this. - `leg_clone` now raises over a refused `.git`, where #1144's row said False. Both callers ask `carries_git` first, so no report changes. - A committed symlink `AGENTS.md` is read by `adopt-project.py`'s commit reader as its target's path, so `plan` and `check` say "This is a single repository" over a link to a leg's file, while the doctor, which reads the disk, calls the checkout a leg. It is the same on base and head, and filed as #1163. Review: lane 1's independent reviewers reviewed each round. r1159 found `0f9aff05` NOT READY, for two SHOULDs and five NITs with no MUST (#1159 issuecomment-6344638023). The fix round `0ca33611` answered all seven, and r1159b found it READY, with no MUST or SHOULD and two optional NITs (issuecomment-6350397420). NIT-A is applied to the body. NIT-B, a docstring row name broken across a line, is applied in `66e68f94`, the head, whose one commit after `0ca33611` changes that docstring in `tests/test_doctor_unsearchable_links.py` and nothing else (4 lines added, 3 removed). d1159's delta check carried READY to `66e68f94` (issuecomment-6385555505). After #1144 landed as `b1f45fa7`, the branch was restacked onto main by three merges, with no rebase, and its changed lines equal the reviewed delta (sha256 `0a7cfc86`). Lane 1's delta check carries READY to `5370b018` (issuecomment-6723286228). Tests, under a stub `gh` with a bogus token: the PR body's gate ran at `0ca33611`, and the one commit after it is that docstring. At `0ca33611` the gate gave 49 passed for `tests/test_doctor_unsearchable_links.py` on 3.12 and 49 on the shim, 96 passed for `tests/test_shape_advisory.py` and `tests/test_path_guard_hygiene.py`, `tests/test_shape_doctor.py` in 4 shards (112, 112, 112 passed; 109 passed, 3 skipped), 61 passed for `tests/test_doctor_next_is_a_command.py`, and 290 passed for the 7 other files that name `shape_advisory`. At the restacked head `5370b018`, `tests/test_doctor_unsearchable_links.py` with `tests/test_shape_advisory.py`, the two scans with `tests/test_scaffold_leg_path_collisions.py`, and the doc trio gave 550 passed and 2 skipped over 3 calls. It lands by fast refresh (Brett Heap, 2026-10-15T22:48:35Z). Its H0 `5370b018` is green on Linux and SonarCloud. The branch was never rebased. After H0, the only commit is update-branch's clean merge of main, giving 9e4aab8, which is green on SonarCloud and Git Bash. Lane 1's gate passed on its tree: 9 pytest calls over `tests/test_doctor_unsearchable_links.py`, `tests/test_shape_advisory.py`, `tests/test_doctor_next_is_a_command.py`, `tests/test_doctor_unreadable_paths.py`, `tests/test_doctor_validator_verdicts.py`, `tests/test_adopt_single_repository_record.py`, `tests/test_bom_reads.py`, `tests/test_bootstrap_non_utf8_branch.py`, `tests/test_doctor_gitmodules_not_utf8.py`, `tests/test_scaffold_leg_path_collisions.py`, `tests/test_path_guard_hygiene.py`, `tests/test_no_auto_maintenance.py`, `tests/test_agents_md_codes.py`, `tests/test_cli_reference.py`, `tests/test_repo_hygiene.py` and `tests/test_shape_doctor.py` in 4 shards: 1,281 passed, 5 skipped. Part of #1110 Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ose own `.git` this user cannot read is named `.git: cannot be read (<reason>)`, so the doctor never reads it as DRIFTED, and both pinned validators refuse it, exit 2 (#1146) (#1147) #1146's case, found by r1144: an assembly root whose own `.git` is a link into a folder at mode 000, so this user cannot search it. On base `038dafba` the doctor read it as `DRIFTED (2 leg(s) not at the pin)`, with `no gitlink recorded` for each leg, and its `manifest` and `pins` rows said each validator `crashed before reaching a verdict (exit 1): PermissionError`. Run directly, `validate-pins.py` and `validate-manifest.py` each ended in a `PermissionError` traceback, exit 1, on Python 3.12, and in `REFUSED not-a-git-repo`, exit 2, under a 3.14-like shim in which `Path.is_file`, `is_dir`, `exists` and `is_symlink` answer False to any `OSError`. Both raised inside `find_repo_root` of `scripts/repo_shape.py`, at `(candidate / ".git").exists()`. - The validators each call `_refuse_an_unreadable_root(start)` first in `main`. With the file's own `_is_a` (`scripts/repo_shape.py` is a pinned copy too, and a pinned validator must not rely on a newer one, #370 and #610), it asks each `.git` that `find_repo_root` would ask, in the same order, from the real path of the `--root` given, and stops at the first one that is there, a directory or a gitfile. One that answers absent (ENOENT, ENOTDIR, EBADF, ELOOP, and Windows 21, 123 and 1921) is passed over, as `exists()` passes it. A refused `stat` ends in the new code `git-repo-unreadable`, the sibling of `not-a-git-repo`: `REFUSED git-repo-unreadable: .git: cannot be read (Permission denied)`, exit 2, with the cause's advice (the `chmod` cure for a permission). A `.git` above the `--root` given is named whole. `find_repo_root` is not changed and answers as before after it. - `validate-pins.py` also asks the git directory of that `.git`, as #968 asks of a leg's: a gitfile it cannot open, a git directory whose `HEAD` it cannot reach. On base git said of each that the root records no gitlink, and the gate refused `pin-gitlink-absent`. `validate-manifest.py` runs no git and asks only the `stat`, so over a readable `.git` and an unreadable git directory it passes, a true answer about `project.yaml`. - The doctor's `root_git_read` asks what `validate-pins.py` asks and names the git directory as the gate does (relative to the root inside it, whole outside it, a gitfile ending in LF or CR LF). The `legs` row says the gate's line once, as a FINDING, with the doctor itself as NEXT and the gate's advice as WHY. Each leg's entry has `gitlink: null`, the line in `gitlink_cannot_be_read`, and `pin`, `populated`, `head`, `own_git` and `unborn` null, so a reader of `--json` gets no `KeyError`. `drifted_reasons` counts no such leg, and still counts a readable leg beside it that is off its pin. - `placement` audits every leg git can read, and gives the root's line to a leg whose `.git` is there and leads to a repository git names no top level for; its row says the root's line once, `n/a`, only where it is every leg's. The verdict follows the ladder (an unfinished merge, DRIFTED, MISPLACED, INVALID, SHAPE BEHIND): no leg is compared with its pin while the root's `.git` cannot be read, so none is drift, and a path in the wrong leg of a leg git reads is `MISPLACED (1 path)`. - Over #1200 (`a2fc1da8`): #1200's `mount_read` refused a leg's git directory before the root's line was reached, so `placement` named `<leg>: .git/modules/<leg>: cannot be read` once per leg instead of the root's line once. `audit_leg` now gives the root's line first where the leg's git directory is inside the root's own, under its `.git` or under the directory its gitfile names (where git keeps a submodule's for a root that is a linked worktree or a submodule itself, measured with git 2.43.0). Every other refusal keeps `mount_read`'s line: a mount, a `.git` or a gitfile, and a leg's own `.git` directory. With the root readable, #1200's line stands. - The S1 round (`4d299dd3`): `audit_leg` asks `populated`, which is True only where `mount_read` got as far as the mount's `.git`, before `read_through_the_root`. A spec mount this user can search and not list (mode 0100 or 0300) had been given the root's line, because `spec/.git` could still be read through the root, against the text above. It now keeps `spec: cannot be listed (Permission denied)`. The verdict stays `INVALID (pins)`, so only the text of the `placement` row changes. - Both validators are in `COPIED_VERBATIM`, and no file is added to or taken from a pinned list. Against a project one commit past its pin, `update-shape.py check` says `upstream-changed` for the two and `unchanged` for the other 9; a project that holds the old copies keeps `crashed ... PermissionError` in the validator rows until `update-shape.py apply`. No `--help` text changes, so `docs/cli.md` is unchanged, and no refusal code is added to `AGENTS.md`. Not touched: #1144's eight sites, the doctor's gitmodules-on-disk code (#1109), and `scripts/repo_shape.py`. - Measured in a scaffold's recursive clone, on Python 3.12.3 and under the shim, with base `038dafba` against `b75babee`: the issue's case went from the `DRIFTED` above to `INVALID (manifest, pins)`, the `legs` row `.git: cannot be read (Permission denied), so no gitlink is read and no leg is compared with its pin`, and each validator to `REFUSED git-repo-unreadable`, exit 2, no traceback. A `.git` that is a directory at 000 or 444, or a gitfile at 000, went from `DRIFTED` to `INVALID (pins)` (`validate-manifest.py` passes). A spec leg with a repository of its own and a misplaced path, beside a root `.git` at 000, went from `DRIFTED (1 leg(s) not at the pin)` to `MISPLACED (1 path)`. The controls (a readable `.git`, a link to nothing, a link to itself) give the base's reports byte for byte, with paths and commit SHAs normalized, on both Pythons. - `tests/test_root_git_unreadable.py` is new and holds 51 rows (47 at `b75babee`, 50 at `a2fc1da8`). Each round was run base against head, with the new file laid over base. At `b75babee`, on 3.12.3 and under the shim, base failed 37 and passed 10 (controls, and the walk rows that stop at a gitfile), and the head passed 47. For the order round, over 54 ids with #1200's 4 rows, base failed 8 and passed 46, and the head passed 54. For the S1 row, `a2fc1da8`'s `shape-doctor.py` failed it and the head passes it. Mutants: 49 of 50 killed at `b75babee` (the survivor, r1147's Da, differs only in a race between the `stat` and the read), 7 of 7 at `a2fc1da8`, and 5 of 7 at `4d299dd3`, where M6 and rv1147c's K4 are equivalent on a real filesystem. - Known limits: - Four other pinned callers of `find_repo_root` keep the defect, a `PermissionError` traceback on 3.12 and `REFUSED not-a-git-repo` under the shim: the assembly root's `bootstrap.py`, and the family root's `validate-family.py`, `bootstrap.py` and `siblings.py`. They need the same local walk in four more pinned copies, filed as #1148. - The `chmod` cure says "every directory above it inside the root", but in the issue's case the locked folder is outside the root and the line names `.git`, a link, so `chmod u+rx .git` is refused. These are the gate's words, unchanged, as r1111's N5 left them. - A root with no `.git` at all (a link to nothing or to itself, which #430's rule calls absent, or none under a parent whose `.git` cannot be read) still reads `DRIFTED (2 leg(s) not at the pin)`; under such a parent `validate-pins.py` walks up and refuses, and the doctor asks only the root's own `.git`. - While the root's `.git` cannot be read, the `legs` row says that alone, so the verdict can read `MISPLACED (1 path)` where the run after it is mended reads `DRIFTED (1 leg(s) not at the pin)`. - `placement` also gives the root's line to a leg whose gitfile names a folder that does not exist and to a leg whose `.git` folder has no `HEAD`; the run after the root is mended gives git's answer for both. - A root whose `.git` is a link, with legs whose gitfiles name the link's target and not `.git/modules/<leg>`, keeps `mount_read`'s line for each leg in `placement`, since `read_through_the_root` follows no link. The `legs` row still says the root's line once. - A family holder whose own `.git` cannot be read, and the `members` row, are not changed (#1109's code is avoided); #1148 asks for them to be measured. - Only what #968 asks of a leg is asked: the `stat` of `.git`, the open of a gitfile, the `stat` of the git directory's `HEAD`. Review: lane 1's independent reviewers reviewed the first two rounds, lane 3 noted the clash with #1200, and lane 5 re-reviewed the order round. r1147 found `e86fb8f4` READY on condition, with no MUST, two SHOULDs and eight NITs (#1147 issuecomment-6332417236): S1, no rows for a relative `--root` or a walk that stops at a gitfile, and S2, `placement` turning down legs it could still read. It killed 13 of its 21 mutants; the survivors were the rows S1 and the NITs asked for, and Da, which is equivalent. The fix round `b75babee` answered both SHOULDs and the NITs, and r1147b found it READY, with no MUST or SHOULD and three NITs (issuecomment-6333323566): each named mutant was killed by its named row, 20 of r1147's 21 are killed (Da is equivalent), N1 and N2 are answered in the text above, and N2's docstring and N3 are optional and left. Lane 3's note (issuecomment-6362488848), from its review of #1200 (issuecomment-6362479557), found that on a merge of #1200 with `b75babee` 5 of the 47 rows failed, all on `placement`'s reason, and named the order that `a2fc1da8` gives. Lane 5's re-review of `a2fc1da8`, by rv1147c (rv1147 and rv1147b hung before they measured anything), found it READY, with no MUST, one SHOULD and four NITs (issuecomment-6723841826): the SHOULD, S1, was a mount this user can search and not list that got the root's line; the NITs were K2, K4 and K7 surviving, rv1248's note, which the text does not contradict, and the draft, to be marked ready. rv1147c killed 8 of its 11 mutants, and found the tree of the merge `569ea056` to be exactly that of `git merge-tree --write-tree b75babe 50869f3`, with no hand edit. Lane 1 took the code for S1, not a narrower text, as `4d299dd3` (issuecomment-6724251407). Lane 5's delta check of `4d299dd3`, by rv1147d, found it READY, with no MUST or SHOULD and three NITs: M6 has no row (an optional stand-in row), the PR was a draft, and CI's `tests` was still running (issuecomment-6724684319). Tests, under a stub `gh` with a bogus token: at `a2fc1da8`, lane 1's gate, on tree `50a97fd7`, gave 1,018 passed and 5 skipped over 8 calls: `tests/test_root_git_unreadable.py` 50 passed, `tests/test_doctor_unreadable_paths.py` 68 passed, `tests/test_path_guard_hygiene.py` and `tests/test_no_auto_maintenance.py` with `tests/test_scaffold_leg_path_collisions.py` 283 passed and 2 skipped, the doc trio (`test_agents_md_codes`, `test_cli_reference`, `test_repo_hygiene`) 171 passed, and `tests/test_shape_doctor.py` in 4 shards 446 passed and 3 skipped (the PowerShell line that is printed only on a Windows host). Lane 5's rv1147c ran the first two files with `tests/test_shape_doctor.py` in one pytest at `a2fc1da8`, on git 2.43.0 and on 2.47.1: 564 passed and 3 skipped each time. The wider gate at `b75babee`, with the 37 other test files that name `placement` and the validator files, gave 6,740 ids: 6,737 passed and 3 skipped; the validators have not changed since `e86fb8f4`. At `4d299dd3`, w1147s's gate (Python 3.12.3, git 2.43.0) gave `tests/test_root_git_unreadable.py` with `tests/test_doctor_unreadable_paths.py` 119 passed; `tests/test_shape_doctor.py`, 4 shards, 446 passed and 3 skipped; the scans with `tests/test_scaffold_leg_path_collisions.py` 283 passed and 2 skipped; the doc trio 171 passed. That is 1,024 ids: 1,019 passed and 5 skipped. It lands by fast refresh (Brett Heap, 2026-10-15T22:48:35Z). Its H0 `4d299dd3` is green on Linux and SonarCloud. The branch was never rebased. After H0, the only commit is update-branch's clean merge of main, giving 95ea564, which is green on SonarCloud and Git Bash. Lane 1's gate passed on its tree: 8 pytest calls over `tests/test_root_git_unreadable.py`, `tests/test_doctor_unreadable_paths.py`, `tests/test_path_guard_hygiene.py`, `tests/test_no_auto_maintenance.py`, `tests/test_scaffold_leg_path_collisions.py`, `tests/test_agents_md_codes.py`, `tests/test_cli_reference.py`, `tests/test_repo_hygiene.py` and `tests/test_shape_doctor.py` in 4 shards: 1,019 passed, 5 skipped. Closes #1146 Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>



Part of #1110. This is slice A: the eight sites in
shape-doctor.pyandscripts/shape_advisory.py. It is outside v1.2.0 by Brett Heap's ruling of 2026-10-21T10:32:04Z ("leave the follow-ups out of v1.2.0"), and it lands after the tag, on a later word.What
Eight sites asked
exists()oris_file()of a name directly in the root. Both calls follow a link. Each site now asks the file's own stat helper, which applies #430's rule:statthe path; ENOENT, ENOTDIR, EBADF and ELOOP (and Windows' 21, 123 and 1921) mean nothing is there; any otherOSErroris raised so the caller can name it. The helper isis_ainshape-doctor.pyand_is_ainscripts/shape_advisory.py. Inshape-doctor.pythe four.gitsites share a newcarries_git(root), which isis_a(root / ".git", <any mode>).Measured over a folder
Atlaswhose.gitlinks to the git directory of a checkout ofacme/Orbitin a folder at mode 000, and over anAGENTS.mdlinked the same way. "Shim" means a 3.14-likesitecustomizein whichis_file,is_dir,existsandis_symlinkanswer False to anyOSError.check_agent_filesrun_checksmakes itFINDING agent files AGENTS.md: cannot be read (Permission denied), and the verdict gainsagent-filesnote absent: AGENTS.md, NEXTscaffold-project.py --helpnote AGENTS.md: cannot be read (Permission denied), NEXT this doctor again, WHY the chmod advice; the verdict is the project's own. The pinnedAGENTS-shape.mdis a FINDINGcheck_what_is_hereFINDING what is here <name>: cannot be read (...)no git repository, no manifest, no contracts/ -- an ordinary directoryn/a <name>: cannot be read (Permission denied), then what it did find; NEXT this doctor again, WHY the chmod advicecheck_the_way_inFINDING the way in .git: cannot be read (...)there is no repository here to adopt: a new project is SCAFFOLDED, NEXTsetup.sh --org <your-org> --project Atlasn/a its .git cannot be read, so it is not known whether this is a git repository to adopt, and neither way in is offered until it can be: `what is here` says how; no NEXT;git_repository: nullcheck_the_other_way_inFINDING the other way in .git: cannot be read (...)origin_orgPermissionErrorNone, the answer for a folder with no repository (acmereadable)stat;the way inasks.gitfirst, so the doctor never reaches itrepo_local_origin_nameFINDING naming .git: cannot be read (...)None, sonamingclassifiesAtlasand dropsOrbitnamingcatches it:Atlas: classifies under the naming policy; origin's name is not read while .git cannot be read (see `what is here`)shape_advisory._readread()'s net:not-answered, why... (PermissionError) ...not-a-repositorynot-answered, why.git: cannot be read (Permission denied), so whether this is a repository is not known here, and the advisory says nothingshape_advisory.repository_names['Atlas'](['Atlas', 'Orbit']readable)statThe whole doctor over the
.gitfixture (--root Atlas; text report; themachinerow omitted):naming,what is here,the way inandthe other way inare eachFINDING .git: cannot be read (Permission denied), with the same NEXT and the same chmod WHY, four times. There is no Triad row.NOT A SHAPE ROOT, exit 2.namingreadsok Atlas.what is herereadsn/a ... an ordinary directory.the way inreadsn/a there is no repository here to adopt, with NEXTsetup.sh --org <your-org> --project Atlas. Exit 2.cmp):The eight entries leave
KNOWN_OPENintests/test_path_guard_hygiene.py; its stale-entry check fails until they do. Measured: putting thecheck_the_way_inentry back fails it with0 found, 1 listed.Why: the design choices
One row carries the line and the cure:
what is here. It is the one row that reads every name in a directory that is no shape root, so a.gitand an agent file follow one rule there, and the cure is said once whatever combination is unreadable. In a project theagent filesrow says the same line for the three agent files (2). The rows that depend on.gitdo not repeat it:the way inpoints atwhat is here;namingadds one clause;the other way inand the Triad row are silent.What that covers for an agent file, and what it does not. An agent file the doctor cannot read is said once,
<name>: cannot be read (<reason>), and never read as absent, inwhat is hereand inagent files. It is not the only reader of anAGENTS.md, and over an unreadable one a way in is offered as on main._leg_by_agents_mdinscripts/shape_advisory.py(except Exception: return None, "an unreadable file says nothing") is called byshape_advisory.read, whose readers areleg_clone(whichthe way inandthe other way inask), the Triad row andadopt-project.py'striadlines. AnAGENTS.mdit cannot read is still read as absent there, so those rows still offer adoption, the scaffold and the Triad advice over it, as on main (Known limits). It is not one of family.py, the family root's siblings.py, shape-doctor.py and shape_advisory.py: a name in the root that is a link into a folder this user cannot search is still read as absent at twelve sites (follow-up of #430 slice Z) #1110's eleven sites, and it is listed there now as a twelfth. It is not fixed here:_leg_by_agents_mdis unchanged. For a.gitno way in is offered (3).This is the doctor's own precedent for a mount (Every
Path.is_file()/is_dir()/exists()guard in the shipped tools answers False to any OSError on Python 3.14 (3.12 and 3.13 raise PermissionError), so a directory this user cannot search reads as ABSENT: validate-pins.py skips its workflow@<sha>check, update-shape.py sayscopy-missingandupdate-pin-missing("adopt the project") for files that are there, the doctor says DRIFTED (5 copy-missing), and bump-leg.py's remaining guards, and every other tool's, skip or misreport instead of refusing by name #430's slice C). There thelegsrow names the mount, with the NEXT and the advice, andleg shape filesandplacementsay the line without them. Rejected: makingthe way inthe owner. It asks about the.git, and reads anAGENTS.mdonly through the advisory (above), so it has no line of its own for an agent file, and with two owners the cure would be said twice when both a.gitand an agent file are unreadable.Status follows the README's rule that a
FINDINGmeans something else asserts it.n/a. Nothing asserts a name there, and that verdict reads no row (NOT A SHAPE ROOT, exit 2, as before).agent files, an unreadable file is anotewhen it is rendered and nothing pins it (AGENTS.md,CLAUDE.md). It is a FINDING when it is the pinnedAGENTS-shape.md, which the project's ownvalidate-pins.pyrefuses by name. This is the rule the row already applied to an absent file.INVALID (..., agent-files)for an unreadableAGENTS.mdis gone.the way inoffers neither command over a.gitit cannot read. Whether the folder is a repository is not known. The scaffold is the defect, and an adoption over a.gitgit cannot read would fail atplan. The row has no NEXT, anddetail.git_repositoryisnull(unknown) in this one case.origin_org,repo_local_origin_nameandrepository_namesraise the refusedstatinstead of answeringNoneor dropping a name. This isis_a's contract ("for the caller to name"). Each caller handles it:the way inandthe other way inask.gitfirst;namingcatches it;_readasks.gitfirst.shape_advisory.read()gets no new state.NOT_ANSWEREDis by its own definition "the advisory could not be read here, and says nothing", and every reader already keeps it silent:leg_clone;adopt-project.py'striadlines.A new state would be a second name for the same answer, which every reader would have to treat identically. What changes is its
why, which now names.git: cannot be read (<reason>)invalidate-pins.py's words.adopt-project.pyis not touched. The constant's comment says so.carries_gitaccepts any mode. A gitdir file (--separate-git-dir, a linked worktree, a submodule's checkout), a directory, or a link to either is a repository, asexists()said wherever thestatsucceeds.mount_readasks a mount's.gitthe same way.detailkeepstruefor a name that is there and addscannot_be_read. That is a list inwhat is hereandagent files, and a string inthe way inandnaming. A--jsonreader keeps its booleans, and nofalsesays "absent" for a path that IS there.Beside an absent agent file, that file's command stays the NEXT and the advice is added to its
why. This is how thelegsrow keeps its checkout line beside a pin it cannot read.namingkeeps the policy's own status whileorigin's name is not read. It classified the folder's own name, so a name that classifies staysok, with the clause sayingorigin's was not read; a name the policy does not classify, or a naming script that refuses, is the policy's FINDING with the same clause. That is a half-asked question, said as one, and it is pinned for each exit (0, 1 and 2) in a row of its own.The change stays inside the eight sites,
carries_git, and the callers that must handle the raise (check_not_a_root_naming). Other lanes' DRAFTs changeshape-doctor.pytoo, #1109 among them; this keeps a later merge small.Tests
New rows:
tests/test_doctor_unsearchable_links.py, 25 rows. 17 came with the first two commits, and 8 were added in the fix round after the review (the third commit). The fourth commit splits two composite asserts, one per condition (SonarCloud'spython:S9073), and changes no result..git, with a readable control first;AGENTS.mdin a plain directory;AGENTS.mdand itsCLAUDE.md.refused_statandrefusing; on every platform and as root):.gitquestions, with controls for a gitdir file, no.git, and a name under a file;what is herebeside a readableMakefile;--separate-git-dircontrol (a gitdir file is a repository to_readand torepository_names);agent files: rendered, I/O error, pinned, and beside an absent file.test_a_git_link_that_leads_nowhere_is_still_no_repository[loop, dangling]: controls,.git -> .git(ELOOP) and.git -> ../nowhere/.git(ENOENT).the way inis the scaffold,what is herean ordinary directory, no cure and nocannot be readanywhere, nooriginasked, the advisorynot-a-repository;test_carries_git_says_no_to_a_link_that_leads_nowhere[loop, dangling]:carries_gitis False for the same two links;test_an_unreadable_pinned_agent_file_beside_an_absent_one_is_a_finding:refused_statonAGENTS-shape.mdwithCLAUDE.mdunlinked is a FINDING, the absent file's command still the NEXT, andverdict_forsaysINVALID (agent-files);test_naming_says_origin_was_not_read_whatever_the_name_comes_to[classifies, does-not-classify, script-refuses]: thenamingrow with the.gitrefused, for exit 0 (Atlas, statusok), exit 1 (my-service, FINDING) and exit 2 (a stand-in naming script that refuses, FINDING): the clause thatorigin's name was not read is on every branch.038dafba(a--sharedscratch clone, with the new file laid over it)89486f69The two that pass on base are the two controls. Every other base failure is on the row's own assertion: a status, a reason, the
namingclause,DID NOT RAISE, or awhy. Two groups of rows fail differently:test_a_git_this_user_cannot_reach_is_named_once_on_every_python[io-error]andtest_an_agent_file_this_user_cannot_reach_is_named_on_every_python[io-error]fail on base 3.12 (and only there) withOSError: [Errno 5] Input/output errorescapingexists()(inrepo_local_origin_name, called bynaming) andis_file()(incheck_agent_files). Under the shim the same two fail on a reason (an ordinary directory,absent: AGENTS.md).[carries_git]and the twotest_carries_git_says_no_to_a_link_that_leads_nowhererows fail on base as missing,AttributeError: the helper is new. Their siblings[origin_org]and[repo_local_origin_name]carry the proof (DID NOT RAISE).Each added row, on base and on head:
..._is_still_no_repository[loop],[dangling]test_carries_git_says_no_to_a_link_that_leads_nowhere[loop],[dangling]AttributeError, a new helper..._pinned_agent_file_beside_an_absent_one_is_a_findingabsent: AGENTS-shape.md, CLAUDE.md(the FINDING already holds there, so it pins this change's words)test_naming_says_origin_was_not_read...[classifies]ok, exit and names hold)...[does-not-classify]...[script-refuses]--separate-git-dirlines oftest_the_advisory_names_a_git_it_cannot_reach_and_says_nothingDID NOT RAISEfor a permission, awhyfor an I/O error)Mutants: 47 of 47 killed. The writer's 18 (D1 to D14 and A1 to A4) and the review's 29 (R01 to R29), each applied alone to one of four
--sharedscratch clones of the head (run again after the fourth commit, with the same result), with the 25 rows run and the file put back from its bytes. After each one its sha256 matched its HEAD blob:shape-doctor.py4841bc5b...(39 mutants) andscripts/shape_advisory.pyd48d565d...(8). The worktree was never changed.os.path.islink. With the 8 added rows all 29 are killed by the rows alone:carries_gitreads an ELOOP.gitas a repository:[loop]of both link rows;.gitread as a repository:[dangling]of both, so the hygiene scan no longer stands alone;note: the pinned-beside-absent row;naminga FINDING while.gitcannot be read:[classifies];[script-refuses];[does-not-classify];_readtakes a gitdir file for no repository, and R28,repository_namesdrops itsorigin: both rows of the advisory test.carries_gitasking for a directory only, so a gitdir file is no repository;_readcatching onlyPermissionError.The second commit adds the gitdir-file control and the advisory's I/O-error case.
agent files: D1 a refused file counted absent; D2 a pinned one a note; D3 no NEXT; D4 the advice dropped beside an absent file.what is here: D5falseindetail; D6 no advice; D7 3.14's absent answer.the way infalls through to the scaffold. D10:the other way inoffers it.namingdropsoriginsilently. D14:namingraises torun_checks, a second telling.origin_organdrepo_local_origin_nameswallow the refusal asNone.not-a-repository; A2 is above; A3repository_namesswallows it; A4 the exception's name in place of the system's reason.Gate on head
89486f69. Python 3.12.3 and git 2.43.0, with a stubghfirst onPATH,GH_TOKENbogus, an emptyGH_CONFIG_DIR,GIT_CONFIG_GLOBAL=/dev/nullandGIT_SSH_COMMAND=false. At most 4 calls in parallel, each under 9 minutes (the longest took 3 min 24 s).origin/mainis still038dafba, the branch's base, so no merge was made.tests/test_doctor_unsearchable_links.pytests/test_path_guard_hygiene.pytests/test_shape_doctor.py, 448 tests in 4 shards by node idtests/test_scaffold_leg_path_collisions.pytests/test_doctor_unreadable_paths.pyandtests/test_doctor_unreadable_pin.pyshape_advisory:test_shape_advisory,test_bom_reads,test_adopt_single_repository_record,test_doctor_gitmodules_not_utf8,test_doctor_validator_verdicts,test_bootstrap_non_utf8_branchtest_agents_md_codes,test_cli_reference,test_repo_hygienetests/test_doctor_unreadable_paths.py, under the shimKnown limits
refused_stat.AGENTS.mdis still read as absent where_leg_by_agents_mdreads it (scripts/shape_advisory.py,except Exception: return None), so a way in is still offered over it. Its readers areleg_clone(whichthe way inandthe other way inask), the Triad row andadopt-project.py'striadlines. Measured over a clone with originacme/OrbitwhoseAGENTS.mdis a spec leg's, linked into a folder at mode 000: main and this head both saythe way in ... it is ADOPTED in placewithadopt-project.py plan ... --project Orbit,the other way in ... a NEW project is SCAFFOLDEDwithsetup.sh --org <your-org> --project Orbit, and theadvisory Triadrow. Readable, the same folder isNAMED OR WRITTEN AS A LEGwith no NEXT, and the Triad row says it is a clone of the spec leg. This head addswhat is here AGENTS.md: cannot be read (Permission denied); a git repository, with the cure. The site is not one of family.py, the family root's siblings.py, shape-doctor.py and shape_advisory.py: a name in the root that is a link into a folder this user cannot search is still read as absent at twelve sites (follow-up of #430 slice Z) #1110's eleven, and it is listed there now as a twelfth. It is not fixed here:_leg_by_agents_mdis unchanged..gitthat is a gitdir file pointing into a folder this user cannot search is a name the doctor can read, socarries_gitsays True. git then fails to readorigin, and the adoption line ends--org <your-org>, as before. Nothing in this slice's guards touches that case..gitthat is a DIRECTORY at mode 000.carries_gitasks the.gititself, which this user can stat, so it is still called a repository. Main and this head both offer adoption,adopt-project.py plan --source <root> --project Atlas --org <your-org>, and the scaffold, and print the Triad advice. Measured over a real repository with originacme/Orbit, its.gitat 000.shape_advisory.NOT_ANSWERED's newwhy,.git: cannot be read (<reason>), so whether this is a repository is not known here, and the advisory says nothing, is printed by nothing._triad_advisory_rowreturns no row for that state andadopt-project.pyprints nothing for it, so the text is true but reachable only in process, where the rows hold it. The constant's comment says--json, and no--jsonreport carries it.the way in'sdetail.git_repositoryisnullin this one case, where it was always a boolean.AGENTS.md's "Report the verdict and both commands" (theNOT A SHAPE ROOTstep) is not amended for the case in whichthe way inoffers none. The row's own words say why. A one-clause amendment would close it;AGENTS.mdis not changed in this PR.--helpchange (sodocs/cli.mdis unchanged), and the README lists the rows, not their texts. The doc-pinning trio passes.Not in this PR
Slice B's three sites stay on
KNOWN_OPENunder #1110:scripts/family.py_gitmodules_names;scripts/family.py_sections_giving;templates/family-root/scripts/siblings.pysubmodule_urls.They wait for other lanes' open DRAFTs on
family.py'sremove(#1117 and #1098). #1110 stays open for them.A shape root whose own
.gitis a link into a folder this user cannot search still reads DRIFTED, and both of its pinned validators crash. That was r1144's observation outside this PR. Base and head behave alike, and it is filed as #1146.Lane: openreposhape-1 (openRepoShape-1)
🤖 Generated with Claude Code
Summary by Sourcery
Make shape discovery and advisory checks distinguish unreadable linked
.gitand agent files from absent paths, report the access problem with appropriate guidance, and avoid unsafe repository recommendations.New Features:
.gitand agent-file links explicitly instead of treating them as absent, including actionable recovery guidance..gitcannot be read, avoiding adoption or scaffolding recommendations.Bug Fixes:
Enhancements:
Tests: