Skip to content

adopt-project.py: a --work-dir that is, or runs through, a symlink round a loop is refused in words that name the link and the loop, not "(File exists)", and a symlink to a file is named as one (#978) - #1065

Merged
brettheap merged 3 commits into
mainfrom
fix/978-work-dir-symlink-loop
Oct 20, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

What

plan, check and execute refused a --work-dir that is a symlink
round a loop as "making it a directory failed (File exists)". os.mkdir
of such a link fails with EEXIST, the error a file in the way gets, so the
person was told a file is there and found a link. The way out, "Pass a
--work-dir this user can write in", was the one for a folder this user
cannot write in, and said nothing of a link. #963's review (r903) left it
tracked as #978.

_work_root now asks a new _unmade_work_dir why the directory could not
be made. The code stays adopt-work-dir-unwritable, exit 2, and nothing
is made.

Measured with plan of a local source, <A> the directory that holds the
links, before on 1a47ed07 and after on this head (exit 2 in every row):

--work-dir Before After
self, a link to itself --work-dir `<A>/self` cannot be made: making it a directory failed (File exists) --work-dir `<A>/self` cannot be made: it is a symbolic link that leads round a loop
a, with a -> b and b -> a --work-dir `<A>/a` cannot be made: making it a directory failed (File exists) --work-dir `<A>/a` cannot be made: it is a symbolic link that leads round a loop
into -> a, into that pair --work-dir `<A>/a` cannot be made: making it a directory failed (File exists) --work-dir `<A>/into` cannot be made: it is a symbolic link that leads round a loop, at the symbolic link `<A>/a`
through -> loop/sub, with loop -> loop --work-dir `<A>/loop/sub` cannot be made: making it a directory failed (Too many levels of symbolic links) --work-dir `<A>/through` cannot be made: it is a symbolic link that leads round a loop, at the symbolic link `<A>/loop`
loop/sub, no link itself --work-dir `<A>/loop/sub` cannot be made: making it a directory failed (Too many levels of symbolic links) --work-dir `<A>/loop/sub` cannot be made: the way to it leads round a loop, at the symbolic link `<A>/loop`
tofile -> a-file --work-dir `<A>/a-file` cannot be made: making it a directory failed (File exists) --work-dir `<A>/tofile` cannot be made: it is a symbolic link that leads to `<A>/a-file`, which is not a directory
a-file, a file (File exists) unchanged
a-file/under (Not a directory) unchanged

The way out:

  • Before, in every row: "Remediation: Pass a --work-dir this user can
    write in, or none: without one, each run makes a new one."
  • After, in the six symlink rows: "Remediation: Remove the symbolic link
    <link>, or make it lead to a directory, or pass another --work-dir, or
    none: without one, each run makes a new one." <link> is <A>/self,
    <A>/a, <A>/into, <A>/through, <A>/loop and <A>/tofile.
  • execute still says "no leg repository was created and nothing was
    pushed." first. The two file rows keep the way out they had.

Wider than the issue. The issue asks for the EEXIST case, where the
--work-dir is itself the link. A path that runs through a loop fails
os.mkdir with ELOOP instead, and said only the system's words, with the
same way out for a folder this user cannot write in. It is named the same
way now: --work-dir through and --work-dir loop/sub reach the same
place. So the words tests/test_adopt_clone_claimed.py held for a loop
above the work directory change. That row's point, #650's code
adopt-work-dir-unwritable and not adopt-work-dir-shared, is unchanged.

The comment wording in #963's code

r903's round-2 NITs a-c (#963 issuecomment-6253937080). Comments and
docstrings only.

  • (a) _ask_git_branch passes an empty temporary directory as
    --git-dir, which is no repository.
    • adopt-project.py, _checked_flag_inputs: "That question is asked of
      a throwaway repository in the system temp folder, which it removes"
      is now "That question is asked with an empty temporary directory as
      its --git-dir, which plan removes".
    • adopt-project.py, cmd_plan: "runs git check-ref-format in a
      throwaway repository first" is now "runs git check-ref-format first,
      with an empty temporary directory as its --git-dir, which plan
      removes".
    • tests/test_adopt_plan_flag_refusals_first.py, the module docstring:
      "in a throwaway repository" is now "with an empty temporary directory
      as its --git-dir, which plan removes".
    • The same file's
      test_the_branch_a_tracking_branch_names_is_asked_before_the_source:
      "is asked of a throwaway repository in the temp folder ... and it
      removed what it made" is now "is asked of git with an empty temporary
      directory as its --git-dir ... and the directory was removed".
    • "which it removes" in r903's words is "which plan removes" here, so
      that it does not read as git removing it.
  • (b) The refusals still made after the source are listed "among
    them", in _checked_flag_inputs' docstring and the test's module
    docstring: a --project of 251 to 255 characters is refused
    unsafe-value after git's reads.
  • (c) The naming refusals come "with a remedy about the source where
    it names a repository".

Tests

  • tests/test_adopt_work_dir_refusal.py, 53 tests:
    • test_a_work_dir_that_is_a_symlink_loop_is_refused held "(File
      exists)" through mkdir_says. It now holds the new words, for a link
      to itself and for a pair, through plan, check and execute: 6.
    • New: a link into a pair and a link through a loop (6), a path under a
      loop (3), and a link to a file (3), each through the three commands;
      each asserts exit 2, the whole of stderr, no plan written, no leg, and
      nothing made or changed under the test's directory.
    • New: _the_loop_in asked directly, which calls a link to a file or to
      a directory no loop (1).
    • New controls: a dangling link to target-a and to
      missing-dir/target-b, through plan and check, exit 0 and the
      target made (4).
  • tests/test_adopt_clone_claimed.py, 42 tests:
    test_a_loop_or_a_file_above_the_work_dir_keeps_its_650_refusal's three
    a-symlink-loop rows hold the new words; its file rows keep the
    system's.
  • tests/test_adopt_plan_flag_refusals_first.py: docstrings only.
  • Fail on base. With 1a47ed07's adopt-project.py in this head's
    tree, the two changed files: 22 failed, 73 passed. The 22 are exactly
    the adopt-project.py: a --work-dir that is a symlink leading round a loop is refused as "making it a directory failed (File exists)", which names a file in the way and not the loop #978 rows: the 19 in test_adopt_work_dir_refusal.py and the 3
    a-symlink-loop rows. The 4 dangling-link controls pass on both. On
    this head: 95 passed.
  • The gate, on this head, one basetemp, EMAIL exported, LANES_LANE
    unset, a stub gh first on PATH, GH_TOKEN=bogus-w978, an empty
    GH_CONFIG_DIR, GIT_CONFIG_GLOBAL=/dev/null and
    GIT_SSH_COMMAND=false, in 17 chunks and 4 shards, each under 9
    minutes:
    • 46 files: the 34 that name adopt-work-dir-unwritable, _work_root
      or work_dir; test_adopt_plan_flag_refusals_first,
      test_repo_hygiene, test_agents_md_codes, test_cli_reference,
      test_readme_windows_quoting_sentence and
      test_scaffold_leg_path_collisions; and the 7 that read
      adopt-project.py's source (test_git_output_decoding,
      test_no_auto_maintenance, test_non_utf8_git_failures,
      test_shape_advisory, test_pinned_copy_text,
      test_openreposhape_package, test_template_values_single_pass):
      3444 passed, 2 skipped, in 17 chunks.
    • test_shape_doctor.py, all 448 tests in 4 shards: 445 passed and 3
      skipped.

Mutants, each applied alone to adopt-project.py, then
tests/test_adopt_work_dir_refusal.py run, then restored by sha256. All
12 killed. They ran on a file that differs from this head's only in two
comment lines ("which plan removes").

Mutant Failed Killed by
M1 ELOOP dropped from the errno check 6 through-a-loop, under-a-loop
M2 the same-link branch always taken 6 into-a-pair, through-a-loop
M3 the "is a link" test dropped from the link-to-a-file branch 3 the file AT the --work-dir (keeps File exists)
M4 the way out under a loop names no link 3 under-a-loop
M5 the --work-dir named as realpath reads it 9 into-a-pair, through-a-loop, link-to-a-file
M6 only the last part of the path asked for a link 6 through-a-loop, under-a-loop
M7 no loop ever found 15 every loop row
M8 the link named where its target goes 3 link-to-a-file
M9 the old way out kept 18 every symlink row
M10 _same_entry always false 6 to-itself, a-pair
M11 realpath's reading passed for the path as typed 9 into-a-pair, through-a-loop, link-to-a-file
M12 any link left in the path called a loop, with no ELOOP 1 test_only_a_symlink_that_loops_is_called_a_loop

M12 survives every command-level row: on Linux realpath leaves no link
in the path but one that loops. The direct test of _the_loop_in holds
the guard.

Merge with main

git merge-tree --write-tree of this head with main at eee089ca is
clean (tree 02c1717d). Of this branch's files, only adopt-project.py
moved on main. #1046 changed other functions. #1041 added the
adopt-bad-project-name refusal inside _checked_flag_inputs, below the
docstring and the comment this branch rewords. A --project the naming
policy refuses as well (NUL.git) is still refused only after the
source, and the docstring's list of what comes after the source, read
"among them", stays true. The branch was not merged, and the gate ran on
this head.

Not in this PR

  • A link to a path under a file (w -> a-file/sub) still names the path
    realpath reads, <A>/a-file/sub, with "(Not a directory)". True, but
    it does not name the link.
  • Not measured on Windows, where a loop is named only if os.stat of the
    link reports ELOOP.

Closes #978

Lane: openreposhape-1 (openRepoShape-1)

🤖 Generated with Claude Code

Summary by Sourcery

Improve work-directory diagnostics so symlink loops and symlinks to files are refused with precise paths and actionable remediation.

New Features:

  • Provide clear, actionable refusals for work directories that are symlinks to loops or non-directory targets, naming the relevant link and preserving the user-provided path.
  • Continue allowing dangling symlink work directories by creating the directory at their resolved target.

Bug Fixes:

  • Correctly distinguish symlink loops and symlinks to files from ordinary occupied or unwritable work directories without changing existing refusal codes or exit status.
  • Ensure work-directory refusals identify the original --work-dir rather than an intermediate resolved path.

Enhancements:

  • Make remediation messages identify the symlink that must be removed or corrected.
  • Clarify comments and documentation about temporary Git directories and the ordering of flag refusals.

Tests:

  • Expand coverage across plan, check, and execute for self-referential links, linked loops, paths through loops, symlinks to files, and dangling symlinks.
  • Add direct coverage ensuring only links confirmed with ELOOP are classified as loops and verify no files or plans are created after refusal.

Brett Heap and others added 2 commits October 20, 2026 11:14
…round a loop is refused in words that name the link and the loop, not "(File exists)", and a symlink to a file is named as one (#978)

`plan`, `check` and `execute` refused a `--work-dir` that is a symlink to
itself as "making it a directory failed (File exists)", the words a file
in the way gets, with the way out for a folder this user cannot write in.
`_work_root` now asks `_unmade_work_dir` why the directory could not be
made:

- a symlink that leads round a loop: "it is a symbolic link that leads
  round a loop", and ", at the symbolic link `<L>`" where the loop is at
  another link;
- a path that runs through such a link: "the way to it leads round a
  loop, at the symbolic link `<L>`";
- a symlink to a file, or to anything else that is no directory: "it is
  a symbolic link that leads to `<target>`, which is not a directory".

The `--work-dir` is named as given, made absolute and not resolved. The
way out is "Remove the symbolic link `<link>`, or make it lead to a
directory, or pass another --work-dir, or none: without one, each run
makes a new one." The code stays `adopt-work-dir-unwritable`. A loop is
named only where the link `os.path.realpath` left in the path fails
`os.stat` with ELOOP. A file at the `--work-dir` or above it, and a
dangling link whose target cannot be made, keep the system's words.

Also the wording of three comments in #963's code (r903's round-2 NITs
a-c): `git check-ref-format` is asked with an empty temporary directory
as its `--git-dir`, which `plan` removes, and not "in a throwaway
repository"; the refusals still made after the source are listed "among
them"; and the naming refusals' remedy is about the source "where it
names a repository".

Tests: tests/test_adopt_work_dir_refusal.py has 19 new or changed tests
that fail at 1a47ed0: a link to itself and a pair of links, a link into
a pair and one through a loop, a path under a loop and a link to a file,
each through plan, check and execute, and `_the_loop_in` asked directly.
4 dangling-link controls pass there and here.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…p is held to #978's words, which name the link, where it held the system's "Too many levels of symbolic links" (#978)

`test_a_loop_or_a_file_above_the_work_dir_keeps_its_650_refusal` pins
that `--work-dir <T>/loop/W` (`loop -> loop`) is refused as #650 refuses
it, `adopt-work-dir-unwritable`, and not `adopt-work-dir-shared`. It held
that refusal to main's words through `refused_alike`, so its three
`a-symlink-loop` rows failed on b8a7416, which names the loop there: "the
way to it leads round a loop, at the symbolic link `<T>/loop`", with the
way out to remove that link or make it lead to a directory. The rows now
hold those words, through `refused_for_a_link` and a new
`through_a_loop` in `tests/test_adopt_work_dir_refusal.py`, which its own
under-a-loop test uses too. The file rows keep main's words. All three
loop rows fail on 1a47ed0's `adopt-project.py`, and the file passes on
this head (42 passed).

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

sourcery-ai Bot commented Oct 20, 2026

Copy link
Copy Markdown

Reviewer's Guide

Improves --work-dir refusal diagnostics by preserving the user’s spelling, identifying symlink loops and symlinks to files, and offering link-specific remediation, with comprehensive command-level and regression tests while leaving ordinary file errors and dangling-link behavior unchanged.

Flow diagram for work-directory refusal diagnosis

flowchart TD
    A[_work_root attempts _made_0755] --> B{Directory creation fails}
    B -->|EEXIST or ELOOP| C[_unmade_work_dir]
    B -->|Other error| D[Keep system error for resolved root]
    C --> E{Symlink loop found by _the_loop_in}
    E -->|Yes| F[Name --work-dir as given and identify loop link]
    E -->|No, work-dir is symlink| G[Name symlink and its non-directory target]
    E -->|No symlink case| D
    F --> H[_work_dir_refusal with link remediation]
    G --> H
    D --> I[_work_dir_refusal with WORK_DIR_FIX]
    H --> J[adopt-work-dir-unwritable exit 2]
    I --> J
Loading

File-Level Changes

Change Details Files
Diagnoses failed work-directory creation to distinguish symlink loops, symlinks to files, and existing non-directory paths while preserving existing refusal behavior for ordinary filesystem errors.
  • Adds _unmade_work_dir to format user-facing details and symlink-specific remediation.
  • Detects the loop symlink left by realpath using _the_loop_in and ELOOP, with _same_entry handling loops at the requested path.
  • Keeps dangling symlinks and regular file/non-directory failures on their prior behavior and system error wording.
  • Allows _work_dir_refusal to receive a custom remediation message.
adopt-project.py
Expands work-directory refusal coverage across all commands and symlink topologies, including controls for non-loop cases and filesystem side effects.
  • Tests self-loops, paired loops, links into or through loops, paths below loops, and symlinks to files.
  • Asserts precise stderr, exit status, remediation link naming, absence of plans and leg repositories, and no unintended filesystem changes.
  • Verifies dangling symlinks remain usable and directly tests that only ELOOP links are classified as loops.
tests/test_adopt_work_dir_refusal.py
Updates clone-path regression coverage to validate the new loop-specific wording while retaining the existing refusal semantics for files.
  • Uses shared symlink setup and assertions for loops above the work directory.
  • Confirms loop cases remain adopt-work-dir-unwritable and file cases retain system error wording.
tests/test_adopt_clone_claimed.py
Clarifies comments, docstrings, and test documentation about temporary Git directories and refusal ordering.
  • Replaces ambiguous 'throwaway repository' wording with the empty temporary --git-dir behavior and its cleanup responsibility.
  • Clarifies that additional naming and validation refusals occur after source processing.
adopt-project.py
tests/test_adopt_plan_flag_refusals_first.py

Assessment against linked issues

Issue Objective Addressed Explanation
#978 When creating a work directory fails because the typed --work-dir is a symlink loop, refuse with wording that identifies the loop rather than reporting “File exists,” and provide remediation that mentions removing or mending the link as well as choosing another work directory. ✅
#978 Add coverage for the symlink-loop refusal across plan, check, and execute, while preserving the existing behavior for a regular file in the way. ✅
#978 Address the tracked comment-wording nits: accurately describe the temporary --git-dir, qualify the list of later refusals, and clarify when a naming refusal’s remedy refers to the source. ✅

Possibly linked issues


Tips and commands

Interacting with Sourcery

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

Customizing Your Experience

Access your dashboard to:

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

Getting Help

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-1, 2026-10-20T18:09:18Z) r1065, the independent review of #1065 at a4e179dd: READY on condition that CI tests passes on this head. It found no MUST. The lane rules that S1, S2 and N1-N7 go to a follow-up issue, and that #1065 lands as it is. Its landing pilot waits for tests, gates over the current main, and takes the HOLD only when no lane holds one.

r1065's report, as written:

r1065 review of PR #1065 (closes #978), head a4e179dd

You called a checkpoint, but the review was nearly done when it came. The merged-tree gate was the only run in progress; I let it finish and started nothing new. What is left is at the end. Nothing I touched is still changed: adopt-project.py in my worktree, in the mutant clone (_lane1-scratch/r1065/mut/) and in my pristine copy all read sha256 68d4fda4cb2beec6b304cd0def9cf6d52df5d73f2da2b2e0465801391cd31d8c. The worktree is clean at a4e179dd, and no pytest or helper process is still running.

Verdict: READY on condition. The condition is that CI's tests job passes on this head; it was still pending when I read gh pr checks. SonarCloud, package and tests-git-bash had passed. I found nothing at MUST level. The two SHOULDs are untested guards: each survived a mutant. They can be folded in now or go to a follow-up.

Findings

MUST: none.

SHOULD

  • S1. The EEXIST guard on the "link to a non-directory" branch has no test.

    • Where: adopt-project.py:15998, elif link is not None and exc.errno == errno.EEXIST:.
    • Evidence: my mutant R9 drops the errno test and passes all 59 tests.
    • What R9 prints for w -> locked/x, where locked is mode 555:
      --work-dir `<A>/w` cannot be made: it is a symbolic link that leads to `<A>/locked/x`, which is not a directory
    • What head prints there, which is right (base prints the same):
      --work-dir `<A>/locked/x` cannot be made: making it a directory failed (Permission denied)
    • R9 also says "which is not a directory" for w -> a-file/sub, where nothing exists. That is the row the body lists under "Not in this PR", and no test holds it.
    • Fix: add rows to tests/test_adopt_work_dir_refusal.py for w -> a-file/sub and for a link into a folder this user cannot write in. Each should expect the system's words about root, with FIX.
  • S2. _same_entry exists for a --work-dir spelled through a symlinked parent, and no test spells one.

    • Where: adopt-project.py:15993 and :16028.
    • Evidence: my mutant R5 compares spellings instead of lstat results and passes all 59 tests. On Linux CI the tests' tmp_path has no symlink in it, so the spelling and the real path always match.
    • Probe: --work-dir <A>/viadir/self, with viadir -> realdir and realdir/self -> self.
      • Head: cannot be made: it is a symbolic link that leads round a loop
      • R5: cannot be made: it is a symbolic link that leads round a loop, at the symbolic link `<A>/realdir/self`, which names the same link twice.
    • Fix: add that case as a parametrised row.

NIT

  • N1. On Python 3.12, _the_loop_in can miss a loop.
    • Where: adopt-project.py:16023-16024.
    • Its premise is that the first symlink in root is the looping one. That fails when a link's target has .. after a loop: 3.12's realpath returns <A>/loop/../d/self2 and abspath folds it to <A>/d/self2, leaving d unresolved.
    • Probe: w -> loop/../d/self2, with d -> realdir, realdir/self2 -> self2 and loop -> loop.
      • Head: --work-dir `<A>/w` cannot be made: it is a symbolic link that leads to `<A>/d/self2`, which is not a directory. That is a loop, not a non-directory. The way out still names <A>/w, which is correct.
    • Fix: continue instead of return None after a stat that succeeds. This is my R12; it passes every test and also names this case. Then relax the docstring sentence "So the first symlink in root ... is that one". This is contrived; 3.13 and later resolve d and do not hit it.
  • N2. The direct test of _the_loop_in has no dangling-link row.
    • Where: tests/test_adopt_work_dir_refusal.py:298.
    • Mutant R3 survives: it calls any failing stat a loop.
    • Fix: add ("dangling", "missing"), expecting None.
  • N3. "Named as given, made absolute and not resolved" is true but no test pins it.
    • Mutant R19 (not made absolute) survives, and prints --work-dir `self` for a relative --work-dir self.
    • Mutant R20 (abspath folds ..) survives.
    • Every test passes an absolute path with no .. in it.
  • N4. The fallback naming root is not pinned either. Mutant R11 survives. A row for tofile/sub (with tofile -> a-file) would kill it.
  • N5. One contrived wording case.
    • --work-dir <A>/nothere/../self, where nothere does not exist and self -> self.
    • Head says the way to it leads round a loop, at the symbolic link `<A>/self`. As the tool reads it, the path is that link itself, not a way to it.
    • The way out is right. Wording only.
  • N6. Windows.
    • _the_loop_in tests only errno.ELOOP. The module itself counts winerror 1921 as Windows' "cannot resolve" (_ABSENT_WINERRORS, adopt-project.py:1704).
    • Accepting winerror == 1921 too would be a cheap hedge. Unverified; the body says Windows was not measured.
  • N7. A gap in the body's "Not in this PR" list.
    • --work-dir tofile/sub (a path through a link to a file) still names `<A>/a-file/sub` with "(Not a directory)", as on base.
    • Only w -> a-file/sub is listed. One more bullet would cover it.

Correctness probes, base 1a47ed07 vs head

Every probe ran with the required environment: a stub gh, GH_TOKEN=bogus-r1065, an empty GH_CONFIG_DIR, GIT_CONFIG_GLOBAL=/dev/null, GIT_SSH_COMMAND=false, EMAIL set, and no LANES_LANE. All exit 2 with adopt-work-dir-unwritable, except the exit-0 rows.

These cases name the link and the loop correctly on head:

  • a link to itself, with a relative or an absolute target;
  • one of a pair, a link into a pair, a three-link cycle, and a chain of 45 links ending in a loop (at the symbolic link `<A>/c0`);
  • through -> loop/sub, and paths under a loop (loop/sub and loop/sub/deeper);
  • a trailing slash (self/), a relative path (self from inside <A>), and x/../self with x existing;
  • a link reached through a symlinked parent (viadir/self, viadir/loop/sub);
  • a link into a folder that holds a loop, and a relative target with .. (sub/up -> ../a);
  • a non-UTF-8 name (`<A>/\udce9loop`) and a newline in a name (`<A>/nl\nloop`);
  • an org/repo --source (refused before any clone).

These are named "which is not a directory", never called a loop: a link to a file, to a FIFO, to a link to a file, to a file in a folder, and a chain of 45 links to a file.

These keep the system's words: a file, a-file/under, w -> a-file/sub, a loop under a mode-000 folder ((Permission denied)), and a link into a mode-555 folder.

These exit 0 and the target is made, on both base and head: dangling links to target-a and to missing-dir/target-b, a link to a folder, and loop/../fresh.

adopt-work-dir-shared and #650's refusals: the diff does not touch _refuse_a_shared_chain or _shared_work_dir. All of test_adopt_clone_claimed.py (42) passes on both head and the merge.

Body claims

Claim Measured
Base said "(File exists)" or "(Too many levels…)", with #524's way out true (8 table rows, base)
All eight table rows, the After column (plan) true, word for word
The same for check and execute; execute says "no leg repository was created and nothing was pushed." first true
The six symlink rows' way out names self, a, into, through, loop and tofile true
The two file rows keep their old way out true
Code stays adopt-work-dir-unwritable, exit 2, nothing made true (listed() is unchanged, no plan written)
--work-dir named as given, made absolute, not resolved true (self-rel and dotdot probes); no test pins it (N3)
On base, the reason named realpath's reading, a path never typed true (into on base names `<A>/a`)
A link to a file, or a long chain, is never called a loop true (tofile, the 45-link chain to a file, the FIFO)
A dangling link is made, as on base true (exit 0; target-a and missing-dir/target-b exist)
fix= defaults to #524's way out; _spelled used; no shell_quoted added true (diff: 0 shell_quoted)
Comment wording (a)-(c), comments and docstrings only true (_ask_git_branch does use an emptied mkdtemp as --git-dir)
test_adopt_work_dir_refusal.py has 53 tests, test_adopt_clone_claimed.py 42; the breakdown 6+6+3+3+1+4 true (collect-only)
Fail on base: 22 failed, 73 passed, exactly the 19+3 #978 rows; 4 controls pass on both true
On head: 95 passed true (all of them passed inside a 184-test head run of 3 files)
Gate: 46 files, 3,444 passed and 2 skipped file count true (union is 46); 3,446 collected; the pass count was not re-run
test_shape_doctor.py: 445 passed, 3 skipped true (on the merge)
Merge with eee089ca is clean, tree 02c1717d true
Only adopt-project.py moved on main; #1046 and #1041 touched other places true (hunk headers: _root_keeps, follow_ups_for, _checked_flag_inputs below the docstring, and so on)
NUL.git is still refused only after the source true (a missing source gives source-unresolvable first)
The writer's 12 of 12 mutants killed my analogues of M1, M2, M3, M5/M11, M4, M6, M7, M9, M10 and M12 are all killed; their per-mutant counts are unverified (my M6 analogue also fails the direct test: 7 in that file, not 6)
Not measured on Windows unverified

Body hygiene:

  • Closes #978 is the only closing keyword, and closingIssuesReferences is [978].
  • The keyword scan hit only `fix=`, #524, a code span that GitHub does not take as one.
  • The commit messages have no closing keywords. Both commits are authored by service@opensoft.dev and carry Lane and Co-Authored-By lines.
  • Docs: no "File exists" anywhere in the docs. docs/cli.md's --work-dir help ("One that cannot be made … adopt-work-dir-unwritable") is still true. README, AGENTS.md and the handbook say nothing that is now untrue.

Base vs head

My mutants: 17 of 27 killed

Ten survived:

  • Real gaps:
    • R5, _same_entry by spelling (S2);
    • R9, the EEXIST guard dropped (S1);
    • R11, the fallback names the typed path (N4);
    • R19, not made absolute (N3);
    • R20, abspath (N3).
  • Gap in the direct test only: R3, any failing stat called a loop (N2).
  • Equivalent on Linux:
    • R4, the walk goes from the leaf up;
    • R12, continue past a link that does not loop (better in N1's case);
    • R14, the loop question asked for every errno;
    • R23, _same_entry answers True on OSError.

Merge with main

  • git merge-tree --write-tree a4e179dd origin/main (main 00f6d7f7) is clean: tree e34476740918.
  • Since eee089ca, main changed only family-root files.
  • I built scratch commit 7a8c1aa7 in a separate --shared clone and ran:
Files Result
The PR's 3 test files 184 passed
test_scaffold_leg_path_collisions.py 104 passed
test_cli_reference.py, test_agents_md_codes.py, test_repo_hygiene.py 170 passed
test_shape_doctor.py, 4 shards 118 + 110 + 117 + 100 = 445 passed, 3 skipped (Windows PowerShell only)

Left to review if resumed

  1. CI's tests result on a4e179dd (pending at my read).
  2. The pass count of the writer's 46-file gate: collected only, not re-run.
  3. Python 3.13 and later, which CI's 3.x uses: not run locally. I reasoned that realpath gives the same output for every tested shape, but did not measure it.
  4. Windows behaviour (N6).

Scratch files, all under /workspace/projects/openRepoShape-worktrees/_lane1-scratch/r1065/:

  • the probe scripts: probe.sh, cases.sh, mksrc.sh;
  • the mutants: mutants.py (results in mutants.out) and mutprobe.py;
  • the merge gate: merged_gate.py (results in merged_gate.out);
  • the clones: basetree/, mut/ and merged/.

Lane: openreposhape-1 (openRepoShape-1)

@sonarqubecloud

Copy link
Copy Markdown

@brettheap
brettheap marked this pull request as ready for review October 20, 2026 18:34
@sourcery-ai

sourcery-ai Bot commented Oct 20, 2026

Copy link
Copy Markdown

Sorry @brettheap, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 1 day by commenting @sourcery-ai review. Upgrade to get a review now.

@brettheap
brettheap merged commit 6c86163 into main Oct 20, 2026
5 checks passed
@brettheap
brettheap deleted the fix/978-work-dir-symlink-loop branch October 20, 2026 18:34
brettheap added a commit that referenced this pull request Oct 21, 2026
…ymlink-target` names is no longer named again by `plan-root-blocks-shape-copy` or `plan-root-holds-pin`, so it gets one finding, and the #908 comments say Linux's bound (#979) (#1070)

Part of #979: part (b) and r908's round-2 comment wordings (N1-N3). Part (a), the 1024-4095-byte window on macOS and XFS, stays open for a measurement on those hosts; `SYMLINK_TARGET_MAX` stays 4095.

## What

#965 (#908, L1-80) gave a bad link at `project.yaml` one finding: `_root_shape_findings` leaves out `plan-root-blocks-manifest` for a link that `plan-empty-symlink` or `plan-bad-symlink-target` names one by one. The same link at two other paths the shape writes still got a second finding. That finding's remedy ("send it to a leg, `drop` it, or rename it in the source") cannot help a link that no clone makes:

- a bad link at `shape/README.md`, with `shape/` and `README.md` kept in the root, was also `plan-root-blocks-shape-copy`;
- a bad link at `contracts/shape-pin.yaml`, with `contracts/` kept, was also `plan-root-holds-pin`.

Now each such link gets one finding, its own, and #908's remediation is said once after the findings.

- **`_root_shape_findings`:** the skip no longer asks `code == ROOT_BLOCKS_MANIFEST`, so a named link AT any copy under `shape/` is left out, as at the manifest. This is the review's mutant M04.
- **`_kept_pin_findings`:** a new skip, fed from the same `named_links` through `_pin_findings` and `_leg_findings`. A named link AT a pin's file is left out, for the shape's own pin and for a declared pin's file.
- **What is not left out:**
  - a link BELOW a directory kept at either place, because the directory is in the way whatever the link holds;
  - a link past the first 20, which is only counted, so nothing else would name it.
- **Comments and docstrings:** `_leg_findings`, `_named_symlinks`, `_links_at_a_copy`, `_links_at_a_pin` and the copy, walk and #908 comment blocks now say this.
- **N1-N3:**
  - "GIT'S AND LINUX'S" reads "LINUX'S: git sets no bound of its own".
  - "made on no host" reads "made on no Linux or macOS host", since NTFS is thought to take longer targets.
  - The 1024-4095-byte window comment and the named-link limit both name #979.

README.md, AGENTS.md, `docs/` and the handbook state no manifest-only rule, so no doc changes.

## Decisions

- **No location tail.** Unlike `project.yaml`'s finding (`_at_the_manifest`), the link's own finding does not say where it sits. If the link is given a target git makes and is still kept there, the next `check` names it by `plan-root-blocks-shape-copy` or `plan-root-holds-pin`, whose remedy then helps.
- **The pin skip stops at the disk that keeps case.** When that disk finds a named link at a pin's file, the disk that ignores case is not asked about that file, as it never was after a finding there. `_links_at_a_pin` therefore reads the same disk. A case twin kept beside the link (`contracts/Shape-pin.yaml`) is named once the link is gone, and a test pins this. The copy's skip, like the manifest's, still asks both disks.

## Tests

All runs used a stub `gh` first on PATH, `GH_TOKEN=bogus-w979`, an empty `GH_CONFIG_DIR`, `GIT_CONFIG_GLOBAL=/dev/null` and `GIT_SSH_COMMAND=false`.

`tests/test_adopt_symlink_target_bytes.py`:

- `test_only_the_manifest_finding_leaves_a_named_link_out` is removed on purpose. Both of its cases fail on this code.
- 8 new functions, 32 cases:

| Test | Cases | On `eee089ca`'s code |
| --- | --- | --- |
| one finding at the README copy, the Makefile copy and the shape pin, for empty, NUL-led, late-NUL and 5000-byte targets | 12 | fail |
| the same at a case twin (`Shape/README.md`, `contracts/Shape-pin.yaml`) | 4 | fail |
| left out only if named (20th named, 21st counted) | 4 | 2 fail; the 2 counted are controls |
| a link below a directory kept there is not left out | 2 | controls |
| a link whose target git makes is unchanged | 3 | controls |
| in memory: any pin file, the shape's own and a declared pin's | 3 | fail |
| in memory: no case twin is asked after a named link at the pin | 1 | fail |
| `execute` refuses with the one finding before any leg exists | 3 | fail |

On base, 25 of the 32 fail and the 7 controls pass. On this head the file gives 130 passed.

Mutants of the change, each restored. All 9 are killed:

| Mutant | Failed |
| --- | --- |
| the copy skip reverted to the manifest only | 13 |
| the pin skip reverted | 12 |
| `_leg_findings` passes no `named_links` | 8 |
| `_pin_findings` passes no `named_links` | 12 |
| pin skip without the AT check | 1 |
| copy skip without the AT check | 2 |
| pin skip goes on to the disk that ignores case | 1 |
| copy skip for any symlink at the copy, named or not | 6 |
| pin skip for any symlink at the pin's file, named or not | 6 |

Gate on this tree: 30 files, run in 11 pytest calls with one basetemp, 3634 passed and 5 skipped.

- The 2 skips in `test_no_auto_maintenance.py` come from the session's `GIT_CONFIG_GLOBAL`.
- The 3 skips in `test_shape_doctor.py` run on Windows only.

The files:

- every adopt test file that names a touched function or code: `symlink_target_bytes`, `empty_symlinks`, `execute_refusals`, `finding_spelling`, `ignored_shape_files`, `leg_path_collisions`, `leg_paths`, `nested_attributes`, `pins`, `plan_yaml_quoting`, `question_and_submodule_spelling`, `reencoded_files`, `root_manifest`, `root_shape_paths`, `root_symlinks`, `shape_pin_and_work_dir`, `verify`;
- `test_scaffold_leg_path_collisions.py`;
- `test_shape_doctor.py`, in 4 shards;
- the tests that read adopt-project.py's source: `repo_hygiene`, `shape_advisory`, `non_utf8_git_failures`, `ruleset_hint_tracking_branch`, `template_values_single_pass`, `doctor_next_is_a_command`, `openreposhape_package`, `no_auto_maintenance`, `agents_md_codes`, `git_output_decoding`, `cli_reference`.

## Merge with main

`git merge-tree --write-tree` of this head with `main` at `06cbd478` is clean (tree `01c6fa16`). Since `eee089ca`, main has changed `adopt-project.py` twice (#1065 and #1064) and has not changed the test file. The review, r1070, ran the test file, the doctor and 15 more files on the merge with `5e539a87`: 2,151 passed, 3 skipped, 0 failed. The landing refresh merges main again.

It lands by fast refresh (Brett Heap, 2026-10-15T22:48:35Z). Its reviewed head `20d2d4ba` is green on Linux and SonarCloud. The branch merged main once and was never rebased; the last merge is update-branch's, clean, giving c9a796b, which is green on SonarCloud and Git Bash. Lane 1's gate passed on its tree: the 34 test files that run the named-link findings, the pins, the root copies and the source scanners, and the doctor's, 3,934 passed and 3 skipped.

Part of #979: part (b) and the #908 comment wording. Part (a) stays open for a measurement on macOS and XFS.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 21, 2026
… fetch that carry it run in bootstrap.py's `untraced()` environment, so `make siblings` and `family.py siblings` write the token to no trace the caller asks git for (#1055) (#1075)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

All are **killed**.

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

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

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

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

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

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

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

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

The full suite was not run.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Lane: openreposhape-5 (openRepoShape-5)

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

Closes #1055

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

Labels

None yet

Projects

None yet

1 participant