Repository navigation
adopt-project.py: --source is read by its spelling, an org/repo in any spelling is cloned by its name, the plan records source.cloned, and a clone no plan records is removed, so a recorded clone that is gone or emptied is refused or cloned again for what it is (#652) - #749
Merged
Merged
Conversation
…refused as `adopt-work-dir-unwritable` by `plan`, `check` and `execute`, and `plan` refuses to clone into one it cannot write in, so none of them ends in a traceback before any leg, and `--help` says what `--work-dir` is and where `--org` comes from (#650) On main at a524542, before any leg existed: - `plan`, `check` and `execute --work-dir <a file>` ended in a FileExistsError traceback, exit 1; under a file, NotADirectoryError; under a `chmod 500` directory, PermissionError; and a symlink loop, a RuntimeError from Python 3.12's `Path.resolve` (L1-19). - `plan --source acme/Atlas --work-dir <that directory itself>` ended in a PermissionError traceback at `source-readonly` (L1-20). - With that directory, `check --source nonsense` refused `source-unresolvable` while `execute` refused `adopt-work-dir-unwritable` (L1-21), and with a plan whose recorded clone is gone, `check` printed the "is cloned again" NOTE before its refusal while `execute` printed none (L1-28). - `--work-dir` had no help on the three commands, nor `--org` on `plan` (L1-25, L1-49). Now: - `_work_root` reads `--work-dir` with `os.path.realpath` and refuses one it cannot make as `adopt-work-dir-unwritable`, naming it and the system's reason; `execute` says first that no leg repository was created and nothing was pushed. `_work_dir_refusal` is the one refusal of a work directory, which #524's refusals now use too. - `Source.open` refuses to clone an `org/repo` source into a work directory this user cannot write in, before it makes or says anything; `plan` passes it `_unwritable_work_dir`, and a local source is read in place as before. - `check` passes its answer to `Plan.open_source`, which refuses before the NOTE, and turns any failure to open the source (`OSError` or `Refusal`) into the work-directory refusal when the directory is unwritable, as `execute`, asking first, does. - `--work-dir` on `plan`, `check` and `execute`, and `--org` on `plan`, have help (`_work_dir_help`); docs/cli.md is regenerated. tests/test_adopt_work_dir_refusal.py: 24 tests, each failing with the change reverted. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…urce-clone-record Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n any spelling is cloned by its name, the plan records whether `plan` made its clone (`source.cloned`), and a clone no plan records is removed, so `acme/Atlas` no longer plans a directory of that name, a trailing `/` no longer clones into `source-readonly/` itself, and a recorded clone that is gone or emptied is no longer refused in words about `gh` (#652) - `_source_spec` reads a `--source` by its spelling first. A name that is also a path here is refused as `source-ambiguous`, naming `./<org>/<repo>` and the github.com url (L1-24). A missing path that is no `owner/repo` never reaches `gh`. - `_github_name` drops a trailing `/`, an empty segment and one `.git`, and a github.com url (or `github.com/<owner>/<repo>`) names its `owner/repo`; a url on another host is `source-unresolvable` (L1-43). - `Source.clone` is the one clone: it asks the work directory (#650), then whether its target already holds something (`adopt-work-dir-occupied`, where it was `source-unclonable` with a remedy about `gh auth status`), then says the NOTE, then clones. - `plan` records `source.cloned`. `check` and `execute` clone the plan's repository again, from GitHub, only where `plan` made the clone and it is gone or no longer whole (`_not_a_whole_clone`: not the top of a repository of its own, or without the plan's commit). Any other `local_path` that is gone, and a plan with no `cloned:`, is `plan-source-gone`, naming `--source` (L1-22, L1-26, L1-27). - `_leave_the_clones`, in `main`'s `finally`, removes the clone `plan` or `check` made that no plan records, read-only files included; never one through a symlink, never a path the person passed, never `execute`'s (L1-23). - `--help` for `--source` and `--work-dir` says so; docs/cli.md is regenerated. - tests/test_adopt_source_clone_record.py is new (45 cases). Three tests change: the NOTE's words, D2's re-clone test (its plan now records `cloned: true`) and its help codes, and six hand-edited `local_path` cases, now `plan-source-gone` by the key. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewer's GuideThe PR makes Sequence diagram for plan-created source recoverysequenceDiagram
participant Command
participant Plan
participant Source
participant GitHub
Command->>Plan: open_source(override, work_root)
Plan->>Plan: _recorded_source(local_path)
alt source.cloned is not true
Plan-->>Command: plan-source-gone
else clone is present and whole
Plan->>Source: in_place(local_path)
Source-->>Command: recorded source
else clone is gone or not whole
Plan->>Plan: _recorded_repository()
Plan->>Source: clone(repository, work_root)
Source->>GitHub: gh repo clone owner/repo
GitHub-->>Source: source-readonly/repo
Source-->>Command: recreated source
end
Entity relationship diagram for persisted source provenanceerDiagram
PLAN {
string local_path
string repository
boolean cloned
string commit
}
SOURCE_CLONE {
string path
string repository
string commit
}
PLAN ||--o| SOURCE_CLONE : records
Flow diagram for --source resolutionflowchart TD
A[--source spelling] --> B{Path spelling?}
B -->|Yes| C[Read path in place]
B -->|No| D{GitHub URL or github.com path?}
D -->|Yes| E{Valid owner/repo?}
E -->|No| F[source-unresolvable]
E -->|Yes| G[Normalize to owner/repo]
D -->|No| H{Valid owner/repo name?}
H -->|No| I[Read existing path or refuse source-unresolvable]
H -->|Yes| J{Also an existing path?}
J -->|Yes| K[source-ambiguous]
J -->|No| G
G --> L[Source.clone]
C --> M[Source.in_place]
Flow diagram for transient clone cleanupflowchart TD
A[Source.clone] --> B[Record clone in args.clones]
B --> C{Command result}
C -->|plan wrote a plan recording clone| D[Keep clone]
C -->|plan wrote no plan| E[_leave_the_clones]
C -->|check| E
E --> F{Symlinked path?}
F -->|Yes| G[Keep clone]
F -->|No| H[_remove_a_tree]
H --> I[Make read-only files writable]
I --> J[Remove clone]
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
Conflicts in adopt-project.py (check's `--source` help beside main's new `check --local-remote-dir`, #718/#417) and docs/cli.md: both arguments are kept, in the order --plan, --source, --local-remote-dir, --work-dir, and docs/cli.md is regenerated with scripts/render-cli-reference.py. check's `--work-dir` help is reworded so every code sits whole on one 80-column line in the narrower help column (r652's F3). Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e at, a plan once written keeps its clone, and a clone that cannot be removed is named and keeps every mode, so a work directory swapped for a symlink, a report cut short or a directory nobody can read no longer costs a file outside, the plan's clone or the run's exit (#652) r652's review of 4ea0b19: - F1: `Source.clone` lists each target with the real path it has when it is made, and `_leave_the_clones` removes it, and rmdirs its parent, only while its real path is still that one. A work directory swapped for a symlink during the run (r652's S1a) lost a file in the directory the symlink led to. - F2: `cmd_plan` sets `args.plan_written` right after `write_lf`, and the clone that plan records is kept whatever ends the run after, where a report cut short removed it, an `--out` put in it included. - F4: the removal's handler retries only `os.unlink`, `os.remove` and `os.rmdir`, adds the write bit where it set the mode to `S_IWRITE`, and never changes a symlink; a clone that cannot be removed is named in a NOTE on stderr, and the exit is the command's (a mode-000 directory ended `check` in a TypeError traceback, exit 1). - F5: `_source_spec`'s url branch is `_repository_in_url`, byte for byte (SonarCloud python:S3776). - F6: `plan-source-gone` says the plan "does not record it as a clone `plan` made", which is true of a plan main wrote too. - N1: `execute`'s `adopt-work-dir-occupied` says first that no leg repository was created and nothing was pushed, and that an earlier `execute` keeps the clone it made there. - N2: `_leave_the_work_root`'s docstring names `_leave_the_clones`. - N4: `plan-source-gone`'s way out names no recorded `org/repo` as GitHub's: the plan does not record the host of a clone's `origin`. Tests: five new cases in tests/test_adopt_source_clone_record.py (the symlink swap, the cut-short plan, two clones that cannot be removed, and `execute`'s occupied refusal); the refusal's words in it and in tests/test_adopt_template_values.py follow F6 and N4. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
brettheap
marked this pull request as ready for review
October 16, 2026 22:54
|
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 2 hours by commenting |
This was referenced Oct 16, 2026
brettheap
pushed a commit
that referenced
this pull request
Oct 17, 2026
main gained 25 pull requests since de92614: #634, #699, #671, #719, #701, #717, #700, #702, #722, #720, #725, #721, #728, #705, #727, #718, #744, #743, #749, #740, #737, #734, #742, #751 and #736. No conflict: AGENTS.md, adopt-project.py and docs/cli.md merged as text, and README.md and docs/handbook.html are unchanged by the merge. README.md is 1473 lines, its cap; AGENTS.md 494, its cap; every handbook marker is current; `render-cli-reference.py --check` is ok. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap
pushed a commit
that referenced
this pull request
Oct 17, 2026
…es every reason THIS checkout cannot answer, and the notes are current to main bf1f099 with the 25 pull requests since de92614 (#563, #683) - W683 and RV-f's N4 on #742: README's exit-3 list (Keeping a project's shape current, :1208-1220) and its handbook twin (`doctor`) named only a pin whose commit this checkout does not carry. They now say, as shape-doctor.py's EXIT CODES does: a pin's commit it lacks, a partial clone missing what is compared (#683), a read that fails (`update-upstream-unreadable`), a crashed validator of its own (#278, #410), or its own naming policy not loading (#678). +1 line, with tests/readme-cap.d/683.txt; README.md is 1474, its cap (1422 + 2 + 5 + 12 + 22 + 10 + 1). The two `Keeping a project's shape current` markers move: `doctor` text and marker, `current` marker only. - The notes, "What changed since v1.0.1", one entry per pull request from `git log --first-parent de92614..bf1f099`, each from its squash commit and the issues GitHub says it closes: - adopt-project.py: #699, #719, #749, #727, #717, #728 (with #621's hint), #743 + #720 + #718, #722, #721; - update-shape.py, bump-leg.py and a project's own scripts: #702 (with the pinned validators), #634; - scaffold, setup, family and the shared readers: #725, #671, #744; - shape-doctor.py: #742, #701, #737, #751, #736, #734, #700 + #705, #740; - install, CI and the docs: the exit-3 sentence above. - Counts (99 pull requests since bfed143; 109 of 135 milestone issues closed at 10:28Z), the "current to main" sha and the open list (26) are at main bf1f099, still placeholders for tag time. Left for round B: lane 5's eight lines (#630 issuecomment-6227129879), whose pull requests #726, #747, #682, #724, #746, #745 and #738 are open drafts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 17, 2026
brettheap
pushed a commit
that referenced
this pull request
Oct 17, 2026
…d A3 (three SHOULDs, four nits), and notes entries for the seven pull requests landed since bf1f099, current to main 2000488 (#563, #683) Round A4, from lane 2's review (#630 issuecomment-6233033833, READY): - SHOULD 1: the doctor's exit-3 list names "a validator of its own that crashed or could not be run", so the case of one that cannot be given its `PYTHONPATH` (shape-doctor.py's EXIT CODES, #278) is listed too: README.md:1208-1222, its handbook twin (`doctor`) and the notes' docs line. The paragraph re-wraps to one more line: tests/readme-cap.d/683.txt is +2, and its text now says which causes the EXIT CODES block names and that #742 added the partial clone and #701 the naming policy. README.md is 1475 lines, its cap (1422 + 2 + 5 + 12 + 22 + 10 + 2). Both `Keeping a project's shape current` markers move to c728c970: `doctor` text and marker, `current` marker only. - SHOULD 2: #727's entry names the leg description, whose em dash stopped every `execute` against GitHub at the first leg. - SHOULD 3: #749's entry names `source-ambiguous`, for an `org/repo` that is also a directory here. - Nits: the doctor section's three near-duplicate pairs are merged (#636 with #737, #632 with #700 and #705, #586 with #736); #718 has its own bullet; #634's entry names `git_out`'s spelling for every caller and #744's the gate's `--link-source-label` and `--link-remedy`; the header comment says 2026-10-17 at main 2000488. Wave B1, `git log --first-parent bf1f099..2000488`: #706 (#664), #703 (#696), #670 (#646), #682 (#648), #726 (#667), #730 (#666) and #724 (#665), each from its squash commit; #726's and #682's from lane 5's lines (#630 issuecomment-6227129879), #682's with the unmerged `.gitmodules` its last round added, and #724's with lane 5's note that v1.0.0's test-hygiene line on the generator's `.pyc` files closes. The counts (106 pull requests; 116 of 135 milestone issues closed at 11:53Z), "current to main" and the open list (19) are at main 2000488, still placeholders. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 17, 2026
brettheap
added a commit
that referenced
this pull request
Oct 18, 2026
… names no repository's top level is refused by name before git reads anything else in it, so a file, a directory inside a repository, an unknown `~user`, an empty value, `owner/.git` and a lost blob no longer end in a traceback, plan a subtree or reach `gh` (#796) (#831) `adopt-project.py` took a `--source`, or a plan's `source.local_path`, that names no repository's top level as if it did (#796). Measured on main `24a977c0`: - a file (L1-64) ended in `NotADirectoryError`, exit 1, after `plan` had made its work directory; - a directory inside a repository, or `.` run in one (L1-88), planned only that subtree under the repository's name; a directory inside a git directory (`<repo>/.git/objects`, `<bare>.git/refs`) planned the whole repository and recorded the inner path, and `check` and `execute` then failed on it; - `~nouser/x` (L1-95) ended in `RuntimeError`; `--source ''` (L1-96) planned the directory `plan` ran in, and a plan recording `local_path: ""` had `check` clone the plan's repository from GitHub; - `owner/.git` in any case (L1-100) reached `gh repo clone`; `www.github.com/o/r` (L1-101) was refused; and a source that lost a blob (L1-77) ended in `ValueError` over git's `BAD` size. Now: - **`_top_level`**, new: a local source must be a directory; inside a git directory it must be that git directory itself; and `git rev-parse --show-prefix` must print nothing. Otherwise it is refused under a new code, `source-not-top-level`, which names what was given (a file, `src/` inside the repository at its top level, or the git directory it is inside), with the way out: pass the top-level directory, which `git rev-parse --show-toplevel` prints, or its `org/repo`. A git directory itself (`<repo>/.git`, a bare repository, `.git/modules/<m>`, `.git/worktrees/<w>`) is read whole, as on main. - **`_source_spec`** refuses an empty value, expands `~` with `os.path.expanduser` (so an unknown `~user` is `source-unresolvable`, not a traceback), and hands every path that exists to `_top_level`. `plan` reads `--source` before it makes its work directory; only the `--pin-source` refusals and `--out` (#833) come first. In `check` and `execute` the work directory is still asked first, as #650 ordered. - **A recorded `source.local_path`** the person gave is held to `_top_level` too, named by its key, and an empty one is `plan-source-gone`; nothing is cloned for it. A clone `plan` made keeps #749's handling. - **`_github_name`** refuses the repository `.git` in any case, with no `gh` call, and drops a trailing `.git` in any case; `www.github.com` is read as `github.com`. `scripts/repo_shape.py` is unchanged. - **`Source.tree`** refuses a size that is not `-` or digits as `git-failed`, naming the path, the blob and `git fsck`. - **The `source` report line** names the directory read, with the repository its `origin` names beside it; a clone `plan` made is still named by its repository. - **The help** of `plan`'s, `check`'s and `execute`'s `--source` names the new code, and docs/cli.md is regenerated. Review: lane 1's independent adversarial review found defects at `adbc5e37`. The fix round fixed them, and the re-verification found `2d51f181` PR-READY. Main was merged twice, the second time (`db202f53`) over #830's and #833's changes to `adopt-project.py`, and the re-verification found that head PR-READY too. It lands by fast refresh (Brett Heap, 2026-10-15T22:48:35Z). Its reviewed head is green on Linux and SonarCloud. The branch merged main three times and was never rebased; the last merge is update-branch's, clean, giving 7f4d015, which is green on SonarCloud and Git Bash. The full suite passed on its tree in lane 1's gate: 240 test files, 14,677 passed and 69 skipped. Closes #796 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 18, 2026
…he run's own `mkdir` and removed only while it is still that directory, and a `--work-dir` another user can rename in is refused first, so a swapped `source-readonly` no longer turns the removal onto another directory (#803) (#843) `plan` and `check` make a clone in the work directory and, since #749, remove it when no plan records it. That removal went by path, so it could be turned onto another directory (#803). Measured on main `24a977c0`: - **L1-99 (b).** With `source-readonly` swapped for a symlink to a victim, or for the victim itself, at the start of the removal, `check` exited 0 and removed the victim's files. A skipped removal (L1-99 (a)) printed nothing. - **L1-98.** Two runs in one `--work-dir`: run A removed the clone run B had just planned from. - **L1-97.** A clone `check --source` made at the plan's own `source.local_path` was removed. - **L1-102.** On Windows spellings, the clone was kept in silence. - **L1-65.** A file at `source-readonly` ended `plan`, `check` and `execute` in `FileExistsError`. - **A work directory another user could rename** (mode 0777, under a 0777 directory, or a `source-readonly` another user owns) was used as any other, and a swap from the directory above turned the removal onto another tree. Now: - **A work directory that another user can rename in, or rename from above, is refused before anything is made in it,** under a new code, `adopt-work-dir-shared`. `_work_root` asks each directory above the real `--work-dir`, before and after it makes any, then the work directory itself: a directory owned by a user other than this one or root, or that every user can write in without the sticky bit, is refused by name. This is OpenSSH's StrictModes rule, with the sticky bit counted (so `/tmp` stays open) and group write not asked. Where the shared directory is this user's, the way out also names `chmod +t <A>`. A symlink loop or a file above the work directory keeps #650's refusal. Not asked on Windows, nor of the default work directory. - **`source-readonly/` is asked before the NOTE and before `gh`:** a symlink or a file there is `adopt-work-dir-occupied`, a shared one `adopt-work-dir-shared`, and one this user cannot make a directory in `adopt-work-dir-unwritable`. - **The clone's directory is claimed before `gh` runs,** with an exclusive `os.mkdir`; any target already there, an empty one included, is refused as `adopt-work-dir-occupied`. Every directory a run makes for its work directory, `source-readonly/` and the clone is made 0755 whatever the umask, which can still narrow it (0700 under `umask 077`). - **The removal is held to what this run made.** The identity (`st_dev`, `st_ino`) of the clone's directory and of `source-readonly/` is recorded right after the `mkdir`, and the removal goes ahead only while both match, never through a symlink: relative to a descriptor of `source-readonly/` from Python 3.11 on Linux and macOS, and by path after the same checks elsewhere. A skipped removal gets a NOTE; paths are compared with `os.path.normcase`; the plan's own record is kept; and `execute` removes exactly the claim its own `mkdir` made when the clone does not finish. - **What stays open is said in the body's "The residual window" and known limits:** group write, Windows, root, this user's own processes, drives mounted without ownership, and root over another user's directory. - The `--work-dir` help of all three commands names the new refusals, and docs/cli.md is rendered again. Review: lane 1's independent adversarial review found defects at `cf36411e`. Fix round 1 (`bba7e445`) and fix round 2 (`08f7a283`) fixed them, the second after `/workspace/projects` was given the sticky bit on Brett Heap's word. The merge round (`b1a40a64`) merged main over #830's and #831's changes to `Plan.open_source`, and the re-check found that head PR-READY. It lands by fast refresh (Brett Heap, 2026-10-15T22:48:35Z). Its reviewed head is green on Linux and SonarCloud. The branch merged main twice and was never rebased; the last merge is update-branch's, clean, giving cfe8e35, which is green on SonarCloud and Git Bash. The full suite passed on its tree in lane 1's gate: 247 test files, 15,381 passed and 48 skipped. Closes #803 Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Lane: openreposhape-1 (openRepoShape-1)
Claim: #652 (comment)
This PR goes straight to
main, and is not stacked: #650 (D2), which it was authored on, has landed as 3dc34b8. The branch mergedmainat 2fac979 before its first commit, and at 66b230e (with #718'scheck --local-remote-dir) before the review's fix round.What
How
adopt-project.pyreads--source, and what becomes of the clone it makes.A
--sourceis read by its spelling first (_source_spec)./,~,./,../, a drive or a share).org/reponame, or a github.com url.source-ambiguous, and the refusal names both spellings:./acme/Atlasreads the directory, andhttps://github.com/acme/Atlasclones the repository (L1-24).One name, however it is spelled (
_github_name). A trailing/, an empty segment and one.gitare dropped.https://github.com/<o>/<r>,git@github.com:<o>/<r>.gitandgithub.com/<o>/<r>, with no scheme, all name<o>/<r>.ghis handed<o>/<r>, the clone issource-readonly/<r>, and the plan recordsrepository: <o>/<r>(L1-43).source-unresolvable, by its host. A missing path that is noowner/repo(a/b/c) issource-unresolvableand never reachesgh.The plan records whether
planmade its clone:source.cloned: trueorfalse.planmade, gone or no longer whole, is cloned again from GitHub by its recorded name. It is never read through the current directory. "No longer whole" means it is not the top of a repository of its own, or no longer holds the plan's commit.local_paththat is gone isplan-source-gone, named by its key, with--sourceas the way past it (L1-22, L1-26, L1-27). The way out names no recordedorg/repoas GitHub's: the plan does not record the host a clone'soriginwas on.One clone path (
Source.clone). It asks, in order:--work-dirthat is a file or cannot be made endsplan,checkandexecutein a traceback, and--helpsays nothing of--work-diror of where--orgcomes from #650);adopt-work-dir-occupied, by name, where it wassource-unclonablewith a remedy aboutgh auth status.execute's way out begins by saying that no leg repository was created and nothing was pushed, and ends by saying that an earlierexecutegiven that--work-dirkeeps the clone it made there;A clone no plan records is removed (
_leave_the_clones, inmain'sfinally, L1-23):planremoves its clone when it writes no plan, and keeps it once the plan is written, whatever ends the run after;checkremoves its--sourceclone, and the one it makes again;executekeeps its clone, and the work directory it made says so on its last line, as before.--helpfor--source(plan, check and execute) and--work-dir(plan and check) says all this. docs/cli.md is regenerated.Why (repro on main)
Probed on 2fac979 and on d7b12c5 (#650's head), with a stand-in
ghthat parses names as gh does and clones from disk. No realghwas run../acme/Atlas, with one local-only commit.plan --source acme/Atlasexited 0, made 0 gh calls, and planned that directory.checkprinted "is cloned again, as--source acme/Atlaswould clone it", made no call, and read the directory:plan-staleandplan-uncovered: LOCAL.md.--source acme/Atlas/was refusedsource-unclonable: … expected the "[HOST/]OWNER/REPO" format, plus thegh auth statusremedy. With a stand-in that skips the parse, it cloned intosource-readonly/itself. Thenadopt-not-the-sourcerefused it, and the clone stayed.acme//Atlas,acme/Atlas.git, the urls andgithub.com/acme/Atlaseach went toghas typed. None wrote a plan.origin, moved away:checkandexecutehanded the path togh repo cloneand refusedsource-unclonablewith thegh auth statusremedy.D:\work\Atlaswassource-unresolvable: --source 'D:\\work\\Atlas' is neither…, naming a--sourcenobody passed.originisgitlab.com/acme/Atlaswas cloned again from github.com asacme/Atlas. Onlyplan-stalestopped it.planmade was cleaned four ways: emptied, files removed with directories kept (macOS's cleaner), packs removed,HEADremoved. Every one was refusedgit-failed.git ls-remotefailed as "a git or gh command failed".plan,check --source, a refusedplan,execute --sourceandcheck(its plan's clone gone) left 4 clones in the temp folder. Three of them said nothing.check --work-dir Wand thenexecute --work-dir W, as check's help says to run them, failed at the clonecheckleft:source-unclonablewith thegh auth statusremedy.--source Atlaswith no./Atlasissource-unresolvablebefore anyghcall. It is unchanged.github.com/acme/Atlascloned, and was then refusedadopt-not-the-source, with or without--org, so it never worked. It is now read asacme/Atlas.How
All of it is in
adopt-project.py._source_spec,_repository_in_url,_github_nameandSOURCE_URL_REread a--source:github.com/…, by its host;owner/reponame, refused assource-ambiguouswhen it is also a path here;A name that is not text, or begins with
-(S8705), is never anowner/repo, as before.Source.opentakes a--source.Source.in_placereads a path.Source.cloneclones a name, and asks the work directory, the target, and only then prints the NOTE.Plan.open_sourceand_recorded_sourcereplace_where_the_clone_is_gone.local_pathis read as a path and never as a--source.repositoryis only ever cloned from GitHub, by its name, and never read as a directory here.source.cloned: trueletscheckandexecuteclone again._not_a_whole_cloneasksgit rev-parse --show-toplevelandcat-file -e <commit>^{commit}.local_path(the same--work-dir) is the plan's source again, and is kept.render_planwritescloned:afterlocal_path:, with three comment lines abovesource:._leave_the_clonesruns before_leave_the_work_rootinmain'sfinally.Source.clonelisted onargs.clonesforplanandcheck, and keepsplan's oncecmd_planhas written the plan (args.plan_written, set right afterwrite_lf), whatever ends the run after.source-readonly/<repo>: a symlink at any level, there before the run or put there during it, is never followed.Source.clonelists each target with the real path it had when it was made, and only that path is removed, its parent only then._remove_a_treeretries only anos.unlink,os.removeoros.rmdirthat failed, after ADDING the write bit (onexcfrom Python 3.12), never changes a symlink, and raises anything else. A clone that cannot be removed is named in a NOTE on stderr, insidecontextlib.suppress(OSError, UnicodeError), so no traceback leavesmain'sfinally.--help:plan --source;check --sourceandexecute --source(SOURCE_OVERRIDE_HELP); and--work-diron plan and check (_work_dir_help's newbut, andCLONE_TARGET_HELD). Each code sits whole on one line at 80 columns.The two new codes,
source-ambiguousandplan-source-gone, are named in--help, and so in docs/cli.md. AGENTS.md names neither, and no registry lists codes; tests/test_agents_md_codes.py and tests/test_repo_hygiene.py ran in both modes.Compatibility
source.cloned. They are read asfalse: theirlocal_pathis read in place while it exists, and once it is gone it isplan-source-gone, never cloned again.--work-dirremoves the empty work directory it made, and a recorded clone that is gone is cloned again, so no emptyopenreposhape-adopt-*is left and nosource-unclonablefollows (#526) #594 (c972032), which is in neither v1.0.0 nor v1.0.1. So only plansplanwrote on unreleasedmainsince 2026-10-16T01:10Z lose it. Their refusal,plan-source-gone, says to pass--sourceagain: the clone's path, or theorg/repoplancloned from GitHub.--source <org>/<repo>that is also a directory here was read as that directory. It is now refused assource-ambiguous. Write./<org>/<repo>to read the directory.Tests
tests/test_adopt_source_clone_record.py is new, with 50 cases. They run in process, with
gh repo clonestood in for by agit cloneof a repository on disk (gh_clones). The helpers are imported from tests/test_adopt_temp_work_roots.py and tests/test_adopt_plan_leg_path_help.py, not copied.main2fac979 with adopt-project.py: a--work-dirthat is a file or cannot be made endsplan,checkandexecutein a traceback, and--helpsays nothing of--work-diror of where--orgcomes from #650) with only the new file added: 42 fail and 3 pass. The 3 are guards: the clone made again at the plan's own path is kept; a symlinkedsource-readonlyleads elsewhere; a local source and a--work-dirmarker are left alone. They pass on the base, where nothing is removed, and each fails with its guard reverted (below).execute's occupied refusal (N1).Changed tests:
gone()follows the NOTE's new ending.cloned: true, since a path the person gave is never cloned again; andadopt-work-dir-occupiedjoins plan's and check's--work-dircodes.local_pathcases are nowplan-source-gone, by the key, where they named a--sourcenobody passed. Their guards are kept: no traceback, nogh, exit 2, no leg.Revert table, on cecc7e0. Each change was reverted alone, in its own scratch copy. Each run covered the new file, the three changed files and tests/test_cli_reference.py: 185 tests, all passing with the change. "Fails" counts the tests that fail with the change reverted; the test names are in the writer's report.
source-ambiguous(a name that is also a path here is read as the path)a/b/cgoes toghagain-u/xand--upload-pack=x/ycases)testorg/Thing/, another host)cloned:not writtenorigin's nameSource.openagaintemplate_valuescases and the drive letter)_not_a_whole_cloneanswers "whole"islinkof the clone and its parent)planends 0os.openincludedS_IWRITErather than the bit addedexecute's old way outplan-source-gone's old sentenceorg/repoTargeted runs, on cecc7e0 with dirty 0 (after the merge of
main66b230e). Flags:/usr/bin/python3 -m pytest, a stubghthat refuses everything,GH_TOKEN=bogus, an emptyGH_CONFIG_DIR. The files: the 96 fromgrep -l -E 'adopt-project|adopt_project|\bADOPT\b' tests/*.py, which include test_agents_md_codes.py, test_repo_hygiene.py, test_shape_doctor.py and test_scaffold_leg_path_collisions.py, plus test_cli_reference.py. That is 97 files, in 4 shards per mode.PATH=stub:/usr/local/bin:/usr/bin:/bin, filter-repo hidden): 6446 passed, 388 skipped, 1 xfailed, 0 failed.Windows
--sourcewith a scheme, or in git'suser@host:form, is only ever a string and never touches the file system.github.com/<o>/<r>with no scheme is also asked whether it is a path here, forsource-ambiguous._remove_a_treemakes such a file writable and retries. The test emulates Windows' refusal on Linux, and it fails with the retry reverted (C13).local_pathis refused by its key on every platform. The symlink test skips where symlinks cannot be made.Not in this PR
--sourcethat is a SUBDIRECTORY of a repository plans only that subtree (files: 1of 3) and records the subdirectory. It goes on Tracking: adopt-project.py, converting an existing repository in place #457's checklist.executekeeps its own clone in a--work-dirit was given. A re-run given that directory is refusedadopt-work-dir-occupied, by name, rather than with the gh remedy.Known limits
planmade is asked whether it is whole: by--show-toplevel, and whether it still holds the plan's commit object. A clone with a damaged pack that still holds that commit is not asked further. That isgit fsck's question, and a large source would pay for it on every run.www.github.comis read as another host (adopt-project.py:--source <org/repo>reads a directory of that name under the current one, a trailing/clones intosource-readonly/itself, and a recorded clone that is gone or emptied is refused for the wrong reason #652's review, N3; on Tracking: adopt-project.py, converting an existing repository in place #457's checklist).Closes #652
🤖 Generated with Claude Code