Skip to content

adopt-project.py: check finds, and execute refuses before any leg exists, a path the root keeps at the shape's own pin and a --work-dir this user cannot write in, so contracts/shape-pin.yaml kept in the root and a chmod 500 work directory no longer pass check and fail only after both legs are pushed (#524) - #581

Merged
brettheap merged 2 commits into
mainfrom
fix/524-adopt-shape-pin-and-work-dir
Oct 15, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Lane: openreposhape-1 (openRepoShape-1)
Claim: #524 (comment)

What

Two plans passed adopt-project.py check, and execute failed on them only after it had made and pushed both leg repositories. check now finds both, and execute refuses both before any leg repository exists. They are lines L1-K1 and L1-K10 of #457, filed as #524.

Why (repro on main)

Measured on main bfed143 (the v1.0.0 release commit). Each run used a local source, --local-remote-dir, and a stub gh first on PATH that nothing called.

case check execute leg repositories made
contracts/ kept, contracts/shape-pin.yaml -> openglass-pin.yaml, --pin openGlass@<commit> (the issue's) 0, plan ok 1, FINDING adopt-shape-pin-unreadable both
contracts/ kept, a file contracts/shape-pin.yaml, --pin 0, plan ok 1, adopt-shape-pin-unreadable both
the same file, no pin 0, plan ok 1, adopt-shape-pin-unreadable both
contracts/shape-pin.yaml -> README.md (a kept file), no pin 0, plan ok 1, adopt-shape-pin-unreadable both
--work-dir with chmod 500 0, plan ok 2, REFUSED extracting the spec leg. both
the same, with .github -> ci kept 0, plan ok 2, REFUSED extracting the spec leg. both
control: neither 0, plan ok 0, adoption verified both

On this branch (17dd55ca), every case above except the control gives check exit 1 with exactly one finding: plan-root-holds-pin for the first four rows, and adopt-work-dir-unwritable for the last two. execute exits 2 with the same line, and no leg repository exists. The control still adopts and verifies.

How

  • _pin_files(plan) lists each declared pin's file, then the shape's own pin, in the order place writes them. _kept_pin_findings and _links_at_a_pin both read this one list. So the kept-pin rule and the walk's exclusion cannot disagree about which files are pins.
  • Which code L1-K1 uses. It keeps adopt-project.py --pin <name> is accepted by plan, check and execute ("adoption verified"), but writes only the name into neutral_product_pins: and no contracts/<name>-pin.yaml, so the adopted root's own validate-pins.py (and its validate.yml) refuses with neutral-pin-missing from the first run #273's code, plan-root-holds-pin, its opening and its fix ("send contracts/ to a leg, drop it, or rename it in the source and re-run plan"). This is the same rule, extended to one more pin file. Only the middle of the sentence differs, because the two pins are read by different things. It names the shape's own pin (the openRepoShape commit the root's copies came from, and each copy's digest). It names what reads that pin there: execute's verification, the root's own validate-pins.py and make bootstrap. And it says what the split would hold: the source's file, directory or symlink at that path, with the shape's pin beside it under shape/. For a symlink it adds "or written through it where it leads nowhere".
  • The order of the checks. _unwritable_work_dir(args, work_root) runs only when --work-dir is given. It calls tempfile.mkdtemp(dir=work_root) and then os.rmdir. On an OSError it returns a sentence that names the directory and the operating system's reason. Without --work-dir, _work_root makes a new directory that this user owns, and nothing is checked.
    • cmd_execute runs _refuse_an_unwritable_work_dir right after _work_root, before open_source. So it comes after only Plan.load and _refuse_a_mistyped_plan, which read the plan, and before open_source, _require_filter_repo, every other refusal and _create_leg_remotes.
    • cmd_check runs the same probe at the same point. It lists the result first among its findings. If the probe failed, it turns the OSError from making an org/repo source's clone directory there into a refusal with the same code and words.
  • The new code. adopt-work-dir-unwritable sits next to adopt-work-dir-occupied. It has the same remediation shape: "no leg repository was created and nothing was pushed. Pass a --work-dir this user can write in, or none: without one, each run makes a new one."

Tests

New: tests/test_adopt_shape_pin_and_work_dir.py, 17 tests. Every one fails with adopt-project.py as on bfed143 (the revert table's last row: 17 of 17, with git filter-repo). The first 16 were written before the change and run on bfed143 itself: 16 failed with git filter-repo, and 15 failed and 1 skipped without it. The file imports its helpers and fixtures from tests/test_adopt_root_symlinks.py, including J4's unwritable_work, instead of copying them.

  • L1-K1, a path kept at the shape's pin: a file, a case twin (contracts/Shape-Pin.yaml) and a directory of that name, each with a pin declared and without (6 tests). check prints exactly that one finding, and execute refuses with that line alone, before any leg exists.
  • L1-K1, a symlink at the shape's pin (5 tests): to the pin's file that place writes first (the issue's case), to a kept file, leading nowhere with a pin and without, and a case twin leading nowhere. Each gives exactly one finding, plan-root-holds-pin, and no plan-root-symlink.
  • The issue's run on the command line (1 test, needs git filter-repo): execute exits 2 with REFUSED plan-root-holds-pin, and no leg remote exists.
  • L1-K10 (5 tests):
    • check --work-dir with no link and with .github -> ci. A writable directory passes and is left empty. An unwritable one gives the finding, word for word.
    • check --source acme/thing with an unwritable --work-dir refuses with adopt-work-dir-unwritable, where it ended in a PermissionError traceback. Nothing asks gh.
    • execute with no link and with .github -> ci, in process and on the command line. Each refuses with the same words before any leg exists.

Changed: three tests that used the source's own contracts/shape-pin.yaml kept in the root to make a verification fail after both legs. That plan is now refused before any leg exists, so each test needed another way to fail the verification:

Runs at 17dd55ca. The head was clean (dirty 0) and every log ends EXIT=0. All runs used /usr/bin/python3 -m pytest with a stub gh first on PATH, a bogus GH_TOKEN and an empty GH_CONFIG_DIR. The stub logged no call. The file set has 70 files and 5,720 tests: every file that names adopt-project, adopt_project or conftest's ADOPT, every tests/test_adopt_*.py, tests/test_shape_doctor.py and tests/test_scaffold_leg_path_collisions.py. It ran in five chunks per mode.

mode passed skipped failed
git filter-repo on PATH 5,707 13 (none of this PR's) 0
hidden (PATH=/usr/local/bin:/usr/bin:/bin, stub first) 5,393 327 0

In hidden mode, the only test of the new file that skipped is the one command-line execute run that needs git filter-repo.

Revert table. Each change was reverted alone in adopt-project.py, in a scratch checkout of 17dd55ca. Each row lists the tests that then fail, out of the 17 new tests and the 5 touched ones. Nothing fails with no change reverted (22 passed).

reverted what that takes away fails
C1: _kept_pin_findings asks about the shape's pin the L1-K1 finding 13: the 6 kept-path tests, the 5 link tests, the command-line test, and the reworked test_adopt_ignored_shape_files test
C2: _links_at_a_pin names the shape's pin one link, one finding 3: leading-nowhere, leading-nowhere-with-a-pin, a-case-twin-leading-nowhere (two findings each)
C3: check's adopt-work-dir-unwritable finding check agreeing with execute 2: test_check_names_a_work_directory_it_cannot_write_in[no-link, a-link-above]
C4: execute's refusal the L1-K10 refusal 2: test_execute_refuses_a_work_directory_it_cannot_write_in_before_any_leg[no-link, a-link-above] (both legs made)
C5: check's OSError from the clone becomes the refusal no traceback in check 1: test_check_refuses_a_source_it_would_clone_into_that_directory
all: adopt-project.py as on bfed143 everything 18: all 17 new tests and the reworked test_adopt_ignored_shape_files test

The other four touched tests pass with any change reverted, by design. Three of them only change how a verification is made to fail after both legs, which does not depend on #524. The fourth is a docstring.

Windows

Not in this PR

Measured on 17dd55ca. Each is a new edge case for #457's checklist, and none is fixed here:

  • A symlink ABOVE a pin's file that lands on a file the root keeps. Example: contracts -> governance with governance/shape-pin.yaml kept. check gives 0; execute gives 1 with adopt-shape-pin-unreadable after both legs. For a declared pin (governance/openglass-pin.yaml, --pin openGlass@<commit>), check gives 0 and execute gives 0 with adoption verified. But the root's contracts/openglass-pin.yaml then leads to the source's file, which the root's own validate-pins.py reads. Neither the kept-pin rule (it follows no link) nor the walk (the path is taken, and the copy goes under shape/) asks this.
  • A --work-dir that cannot be made, or that names a file. _work_root's mkdir raises PermissionError or FileExistsError, as a traceback, in plan, check and execute. No leg exists by then.
  • plan --source <org/repo> with an unwritable --work-dir. This is a PermissionError traceback at source-readonly. check and execute now refuse the same case.
  • The release notes. docs/release-notes/v1.0.0.md keeps its two known-limit lines for adopt-project.py check: refuse a kept file at the shape's pin path, and an unwritable --work-dir, before any leg exists #524, because they describe 1.0.

Known limits

Closes #524

🤖 Generated with Claude Code

Summary by Sourcery

Reject shape-pin collisions and unwritable work directories during validation so adoption fails before any leg repository is created or pushed.

Bug Fixes:

  • Detect plans that keep a file, directory, or symlink at the shape's own pin path and reject them before creating leg repositories.
  • Detect unwritable --work-dir locations during check and execute, refusing execution before any leg repositories are created.

Enhancements:

  • Unify pin-path handling for declared pins and the shape's own pin, including case-insensitive paths and symlink reporting without duplicate findings.
  • Align source-cloning failures in check with the new work-directory refusal behavior.

Tests:

  • Add coverage for kept shape-pin paths, symlink edge cases, unwritable work directories, and early-refusal behavior.
  • Update late-verification tests to induce failures through a pre-commit hook now that conflicting root pins are rejected during planning.

… exists, a path the root keeps at the shape's own pin and a `--work-dir` this user cannot write in, so `contracts/shape-pin.yaml` kept in the root and a `chmod 500` work directory no longer pass `check` and fail only after both legs are pushed (#524)

Two v1.0.0 known limits on #457, L1-K1 and L1-K10, measured on main
8beb3d9 and again on bfed143.

L1-K1. `_kept_pin_findings` (#273) asked whether the plan keeps a path
where `execute` writes each declared pin's file, and never asked it of the
shape's own pin, `contracts/shape-pin.yaml`. A file kept there, with a pin
or without, and a link there that leads to something
(`-> openglass-pin.yaml`, which finds the pin `place` writes first, or
`-> README.md`) passed `check`; `execute` made and pushed both legs, wrote
the shape's pin beside it under `shape/`, and exited 1 with
`adopt-shape-pin-unreadable`. The same rule now asks it of the shape's pin
(`_pin_files`), under #273's code, `plan-root-holds-pin`, with a sentence
that names the shape's pin and what reads it. A link there that leads
nowhere was #504's walk's finding (`plan-root-symlink`); the kept-pin rule
names it now, as it names a link at a declared pin's file, and the walk
does not say it again (`_links_at_a_pin`): one link, one finding.

L1-K10. `--work-dir` was made and never written in before
`_create_leg_remotes`. With `chmod 500` on it, `check --work-dir` said
`plan ok`, and `execute --work-dir` made and pushed both legs and stopped
`extracting the spec leg`, exit 2. `execute` now makes and removes a
directory in it first, before it opens the source, and refuses as
`adopt-work-dir-unwritable`, naming it. `check --work-dir` names the same
as a finding beside the plan's own, and refuses it as `execute` does where
an `org/repo` source would be cloned there, which ended in a
PermissionError traceback.

Three tests used the source's own `contracts/shape-pin.yaml` kept in the
root to make a verification fail after both legs; that plan is refused
before any leg now. `test_adopt_late_failure.py` and
`test_adopt_github_exits.py` fail it with a `pre-commit` hook that takes a
pinned copy out of the split commit (`conftest.a_hook_that_unstages`), and
`test_adopt_ignored_shape_files.py` runs `execute` in process with the
shape's pin left out of the rule, as before #524, so the verification
stays tested as the backstop.

tests/test_adopt_shape_pin_and_work_dir.py adds 17 tests, each failing on
bfed143.

Closes #524

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

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

@sourcery-ai

sourcery-ai Bot commented Oct 14, 2026

Copy link
Copy Markdown

Reviewer's Guide

The PR closes two adoption preflight gaps: it detects root content at the shape's own pin and rejects unwritable explicit work directories before source access or leg creation, while updating collision, symlink, and late-failure tests to reflect the earlier refusals.

Sequence diagram for adoption preflight refusals

sequenceDiagram
    participant User
    participant Execute as cmd_execute
    participant WorkDir as _unwritable_work_dir
    participant Source as open_source
    participant Remotes as _create_leg_remotes

    User->>Execute: execute(plan, --work-dir)
    Execute->>WorkDir: _refuse_an_unwritable_work_dir(args, work_root)
    WorkDir->>WorkDir: tempfile.mkdtemp(dir=work_root)
    WorkDir->>WorkDir: os.rmdir(probe)
    alt work directory is unwritable
        WorkDir-->>Execute: Refusal(adopt-work-dir-unwritable)
        Execute-->>User: No source opened and no leg repository created
    else work directory is writable
        WorkDir-->>Execute: None
        Execute->>Source: open_source(args.source, work_root)
        Execute->>Remotes: _create_leg_remotes(...)
        Remotes-->>User: Leg repositories created
    end
Loading

Sequence diagram for check work-directory validation

sequenceDiagram
    participant User
    participant Check as cmd_check
    participant Probe as _unwritable_work_dir
    participant Source as plan.open_source

    User->>Check: check(--work-dir)
    Check->>Probe: _unwritable_work_dir(args, work_root)
    Probe->>Probe: tempfile.mkdtemp(dir=work_root)
    Probe->>Probe: os.rmdir(probe)
    alt probe fails
        Probe-->>Check: adopt-work-dir-unwritable
        Check->>Source: open_source(args.source, work_root)
        alt source requires cloning into work directory
            Source-->>Check: OSError
            Check-->>User: Refusal(adopt-work-dir-unwritable)
        else source is readable
            Source-->>Check: source
            Check-->>User: Finding adopt-work-dir-unwritable
        end
    else probe succeeds
        Probe-->>Check: None
        Check->>Source: open_source(args.source, work_root)
        Check-->>User: Plan findings
    end
Loading

Flow diagram for shape-pin collision detection

flowchart TD
    A[check or execute loads plan] --> B["_pin_files(plan)"]
    B --> C[Declared pin files]
    B --> D[contracts/shape-pin.yaml]
    C --> E[_kept_pin_findings]
    D --> E
    E --> F{Root keeps a file, directory, or symlink at pin path?}
    F -->|yes| G[plan-root-holds-pin]
    F -->|no| H[Continue preflight]
    G --> I[check reports finding or execute refuses]
    I --> J[No leg repository exists]
Loading

File-Level Changes

Change Details Files
Extend root pin-collision detection to include the shape's own pin and unify pin-path handling across findings and symlink traversal.
  • Added _pin_files to enumerate declared pin files followed by contracts/shape-pin.yaml.
  • Emitted plan-root-holds-pin for files, directories, symlinks, and case variants kept at the shape pin.
  • Prevented symlink walks from reporting a second finding for links already covered by the kept-pin rule.
  • Added shape-pin-specific remediation text describing verification and bootstrap consumers.
adopt-project.py
tests/test_adopt_shape_pin_and_work_dir.py
tests/test_adopt_root_symlinks.py
tests/test_adopt_ignored_shape_files.py
Probe explicit work directories before source access and repository creation, making check and execute consistently reject directories that cannot be written.
  • Added a temporary-directory create/remove probe for --work-dir.
  • Added adopt-work-dir-unwritable with shared diagnostic and remediation text.
  • Made check report the finding and convert clone-directory errors into the same refusal.
  • Made execute refuse before opening the source or creating leg repositories.
adopt-project.py
tests/test_adopt_shape_pin_and_work_dir.py
Revise late-failure tests to use a commit hook rather than a plan that is now rejected during checking.
  • Added a reusable pre-commit hook fixture that removes a pinned file from the split commit.
  • Updated late verification and GitHub exit tests to inject the hook through core.hooksPath.
  • Adjusted ignored-shape-file assertions to verify the new early plan finding while retaining a direct verification backstop.
tests/conftest.py
tests/test_adopt_late_failure.py
tests/test_adopt_github_exits.py
tests/test_adopt_ignored_shape_files.py

Assessment against linked issues

Issue Objective Addressed Explanation
#524 Have check detect a file, directory, or symlink kept at the shape’s own pin path (contracts/shape-pin.yaml), and have execute refuse the plan before creating any leg repository. ✅
#524 Probe a supplied --work-dir for write access: report it from check and have execute refuse an unwritable directory before opening the source or creating any leg repository. ✅

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

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@sonarqubecloud

Copy link
Copy Markdown

@brettheap
brettheap merged commit fb786c1 into main Oct 15, 2026
5 checks passed
@brettheap
brettheap deleted the fix/524-adopt-shape-pin-and-work-dir branch October 15, 2026 15:25
brettheap pushed a commit that referenced this pull request Oct 15, 2026
…y-hand test and #581 among them) into #569, so the PR's CI runs on current main

Merges origin/main fb786c1, read from the
shared store without a fetch. Main moved from 7d9bec9 (#569's last merge) by
#556, #557, #558, #559, #560, #562, #567, #568, #579, #580 and #581. #579
(#550) landed tests/test_setup_by_hand_commands.py, which this PR's
`--no-next-steps` makes red; the next commit answers it. The merge itself is
clean, and its tree is the pm569 prediction's #581 step (53b6d0f7)'s, so the
resolution prepared against the predicted main carries over.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 15, 2026
…3-pypi-package

Brings in #587's refresh onto main fb786c1 (#568, #581), 25b2aee. No
conflict: the refresh touches none of this branch's files, and every hunk
of #563 stays as it was.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 15, 2026
…581's conftest import with #582's follow-up import, and fail #582's verification test with #581's hook

Two resolutions, one textual and one semantic:

- tests/test_adopt_github_exits.py, the import block (the only conflict).
  Kept #581's whole `from conftest import (ADOPT, REPO, WINDOWS_SKIP,
  a_hook_that_unstages, git, make_source_repo, run_script, write_plan)`,
  and #582's `from test_adopt_tracking_branch import OPEN_THE_SPLIT,
  PULL_REQUEST_FOLLOW_UP` after it.
- tests/test_adopt_github_exits.py,
  `test_a_failed_verification_says_what_it_left_in_place` (#582's).
  #581 (#524) refuses a kept `contracts/shape-pin.yaml` before any leg
  exists, so it deleted `FAILING_TREE`, `CONTRACTS_TO_SPEC` and
  `KEEP_CONTRACTS`, and renamed `github_run`'s `keep_contracts` to
  `failing`, which fails the verification with `a_hook_that_unstages`.
  The test now calls `github_run(tmp_path, refuses, failing=True)`. No
  other use of the removed names is left in tests/.

AGENTS.md step 7 and the auA-12 texts name no cause of a failed
verification, only what one leaves in place, and #581 changes neither, so
neither needed an edit. The auto-merged adopt-project.py and
tests/test_adopt_late_failure.py carry exactly main's change (the same
`git patch-id` as 2e6c6fa..fb786c1).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 15, 2026
… fix/540-pin-source-unread

This merges origin/main 2bf8fd4 as the shared store held it at
2026-10-15T16:50Z, thirteen landings past the 7d9bec9 this branch last
merged: #555 (shape-doctor's manifest kinds), #565 (scaffold's
`scaffold-remote-exists`), #581 (adopt's `check` of a kept pin path and
an unwritable work directory), #568, #559, #557, #560, #580, #556, #558,
#567, #579 and #562. It is done so that the PR's CI merge ref and this
round's rewording run on current main.

The merge was clean, with no conflict hunk. `adopt-project.py`,
`scaffold-project.py`, `scripts/shape_materialize.py`, `docs/cli.md` and
`tests/test_adopt_root_symlinks.py` took main's changes beside this PR's
by the textual merge, and `scripts/render-cli-reference.py --check`
passes on the result.

Main: 2bf8fd4.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 15, 2026
Main moved while this merge was being made: a61cf47 (#589) at the
coordinator's request, then 70331aa (#586, shape-doctor.py only) and
2989b4c (#573: family.py, scaffold-project.py, docs/cli.md's scaffold and
family sections, their tests). This merges 2989b4c, the tip at merge
time. 10 commits since 2e6c6fa: 8890f57, fb786c1 (#581), 26c1600,
2bf8fd4, 654c476 (#597), 3f768a6, 024ea55, a61cf47 (#589),
70331aa (#586), 2989b4c (#573).

One conflict, adopt-project.py, the end of `_entry_findings`' return:

  HEAD (#520) added `names_remediation` to the remediation list, so a
  misnamed `adopt_branch:` or `tracking_branch:` prints its remedy;
  main (#589, #449) added `pin_notes` to the notes, so a pin whose digest
  `check` could not recompute is said once at the end.

  Resolved by keeping both, each where its side put it:

            + names_remediation + tracking_remediation + moved_remediation
            + field_remediation,
            pin_notes + notes)

  The two edits touch different elements of the returned tuple, so
  neither changes what the other means: #589's
  `digest_findings, pin_notes = _pin_digest_findings(plan)` and its
  docstring paragraph auto-merged beside #520's `_branch_name_findings`
  call and paragraph.

docs/cli.md auto-merged (#573 changed the scaffold and family sections,
this branch the adopt section); `scripts/render-cli-reference.py --check`
passes on the merge.

Against the new main, each of this branch's six files (adopt-project.py,
docs/cli.md, tests/test_adopt_branch_names.py,
tests/test_adopt_plan_yaml_quoting.py, tests/test_adopt_split_branch.py,
tests/test_adopt_template_values.py) differs by exactly the +/- lines it
did against 2e6c6fa at c598ec3 (adopt-project.py: 399).

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 15, 2026
…eck`'s finding for a link kept above a path the shape writes says the clone holds it as a file, so `scripts -> tools/deep` no longer says where the scripts land and sends the person to change where it points, on to `adopt-symlinks-off` (#531) (#584)

Some disks check a symlink out as a plain file: under `core.symlinks=false`, which is Git for Windows' default and what `git init` writes on a filesystem that cannot hold a symlink. On such a disk, a symlink the plan keeps in the root ABOVE a path the shape writes is a plain file in the assembly's clone, and nothing can be written beneath it.

When `check` reports such a link as `plan-root-symlink`, its finding now says that, in `adopt-symlinks-off`'s words, and gives a fix that works on that disk: send the link to a leg or `drop` it in the plan, and run `check` again. `execute` refuses with the same line, word for word, before any leg exists, as it refused the old line. Only the text of a finding `check` already made changes. A link above that the walk follows cleanly (`scripts -> ci`) stays `execute`'s `adopt-symlinks-off` before any leg, as #504 designed. With symlinks on, the output is byte-identical to `main`'s.

`_a_file_above(walk, probe)` runs only for a walk that is about to be reported, so a plan with no such finding makes no new probe, and no network or gh call is added.

Tests: tests/test_adopt_root_symlinks.py holds 149 tests on `main` and 156 here. `test_with_symlinks_off_a_link_above_a_path_is_named_as_a_file` (6 cases) and `test_with_symlinks_off_the_fix_the_finding_gives_works_there` (`code`, `drop`) are new. Three existing tests of a followed link gain `links_as_links`, so they keep asserting the symlinks-on line wherever the runner's checkout writes a plain file, which a Windows runner's may (Git for Windows' default). Main's test of the old reason is now the D6 row.

Review: the adversarial review found the change PR-READY at 0a178e4, with five text corrections, all in the PR body. The edge cases it found outside #531 are left for #457's checklist.

`main` was merged in five times, and the branch was never rebased.
- **The first merge** was GitHub's update-branch, at 348f8a8 (main 2e6c6fa). 348f8a8 is green on Linux, macOS, Windows and SonarCloud.
- **The second merge was by hand,** at cae5a98 (main 654c476), after #581 (#524) changed `adopt-project.py`. Its one conflict was a comment in `_SymlinkProbe`, about a work directory no repository can be made in. The resolution keeps both #531's sentence and #524's.
- **cae5a98c** passed 71 adopt test files in both modes: 5,796 passed and 5,482 passed, with 0 failed.
- **The third merge** was GitHub's update-branch, at 7f439a5 (main a61cf47), after #589 changed `adopt-project.py`. It was a clean merge.
- **The fourth and fifth merges** were also GitHub's update-branch, both clean, after #566 and #582 changed `adopt-project.py`: 250b070 (main 1d809ea) and c43cde1 (main 526cd2c).
- **Since #597, CI is Linux only.** The head is green on Linux and SonarCloud (ruling 27).

Closes #531

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

adopt-project.py check: refuse a kept file at the shape's pin path, and an unwritable --work-dir, before any leg exists

1 participant