Skip to content

adopt-project.py: execute refuses every finding check reports, so a plan with an uncovered path, a bad leg:, a path covered twice, a duplicated path or an entry that covers nothing is refused before the first leg repository exists, and the corrected plan no longer meets leg-remote-exists (#168) - #170

Merged
brettheap merged 2 commits into
mainfrom
fix/adopt-execute-refuses-check-findings
Oct 9, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

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

What

adopt-project.py execute now refuses every finding check reports, before the first leg repository is created. The five classes it did not refuse (plan-uncovered, plan-bad-leg, plan-covered-twice, plan-duplicate-path, plan-empty-entry) are now REFUSED <that code> with exit 2, no leg repository created and nothing pushed.

One new function, _refuse_what_check_finds(plan, source), and ONE added line in cmd_execute. cmd_check and _refuse_an_unrunnable_plan are not edited (see "Relation to #166").

Why (reproduced on main, 7f84ca4)

_refuse_an_unrunnable_plan refuses only plan-unresolved and plan-stale (and cmd_execute refuses the naming findings first). Any other plan check rejects runs on into _create_leg_remotes, which makes both legs. What happens next depends on the class, and each was run on main:

  • plan-uncovered and plan-bad-leg, on a source with no submodule: the run VERIFIES and exits 0, with the uncovered paths, or the paths whose leg: is not one of the four words, left in the assembly root. (With the src/ entry deleted, check says plan-uncovered for both files and execute exits 0 with both legs pushed and src/ still in the root.)
  • plan-covered-twice, plan-duplicate-path and plan-empty-entry: the run dies at git rm of the offending entry, after both legs were pushed (REFUSED a git or gh command failed, exit 2).
  • A source with a submodule, which is the issue's own three cases (a 1-commit source carrying one submodule): the verification fails as adopt-lost.
upstream/ entry deleted   check rc=1 FINDING plan-uncovered:upstream/dependency
                          execute rc=1 FINDING adopt-lost: upstream/dependency ...
                          remotes afterwards: Northwind-code.git, Northwind-spec.git
upstream/: Spec           check rc=1 FINDING plan-bad-leg:upstream/
                          execute rc=1 FINDING adopt-lost: upstream/dependency ...
                          remotes afterwards: Northwind-code.git, Northwind-spec.git
.gitmodules: Code         check rc=1 FINDING plan-bad-leg:.gitmodules
                          execute rc=1 FINDING adopt-lost: .gitmodules ...
                          remotes afterwards: Northwind-code.git, Northwind-spec.git

In every one the legs exist afterwards, so the corrected plan cannot make them again: under --local-remote-dir it meets REFUSED leg-remote-exists (there is no --force), and without it gh repo create is asked for names the first run already took.

On this branch alone, the same three submodule cases:

upstream/ entry deleted   execute rc=2 REFUSED plan-uncovered: 1 finding(s) ...   remotes: (directory absent)
upstream/: Spec           execute rc=2 REFUSED plan-bad-leg: 1 finding(s) ...     remotes: (directory absent)
.gitmodules: Code         execute rc=2 REFUSED plan-bad-leg: 1 finding(s) ...     remotes: (directory absent)

AGENTS.md step 4 has a human say yes after check, but execute is the one gate that cannot be skipped.

The fix

_refuse_what_check_finds runs _coverage_findings and _leg_findings over the same two lists check builds (the entry paths, the source tree's paths) and raises ONE Refusal:

  • code: the first finding's own (plan-uncovered, ...);
  • detail: the findings exactly as check prints them, capped at eight (REFUSED_FINDINGS_SHOWN) with an "and N more" tail, the way plan-unresolved caps its paths;
  • remediation: no leg repository was created and nothing was pushed; correct the plan; run check until plan ok; run execute again.

Order, which is part of the fix and is pinned by tests:

  1. after _refuse_an_unrunnable_plan, so plan-unresolved (which _leg_findings would repeat) and plan-stale keep their codes and their wording;
  2. after _leg_paths has checked, as a safe git argument, the path of every entry whose leg: is one of the four words. The brief put the call directly after _refuse_an_unrunnable_plan; there, a plan path that is a git option (--output=...) is plan-uncovered, not unsafe-value, and the existing test_a_plan_path_that_is_a_git_option_is_refused fails. The one line is therefore after paths_for = _leg_paths(plan). (An entry with any other leg: is not checked there and reaches no git command; it is refused by the new function under whichever finding check lists first.) Nothing between the two creates a leg;
  3. before _refuse_unconsented_seeding, _confirm and _create_leg_remotes. No leg repository is created and nothing is pushed until every refusal has passed. (_work_root has already made the work directory and an org/repo source has already been cloned into it; that was so before this PR, and neither is a leg.)

A plan check passes executes exactly as before (a check-clean plan, and a spec-only plan with --allow-empty-leg code, give identical output, refs and files on main and on this branch), and a leg no entry assigns a path to is still a check WARNING and an execute consent (--allow-empty-leg), not a finding. The module docstring's sentence about what execute refuses is updated.

Tests

tests/test_adopt_execute_refusals.py, 30 tests. All but one run without git filter-repo: they load adopt-project.py through importlib and call cmd_execute in-process with its filter-repo probe stubbed, which is safe because every refusal under test is raised before the tool would be used. The file builds a source with no submodule.

  • each of the five classes: Refusal with that code, the finding in the detail, and every FINDING line check prints for the same plan present word for word in the refusal;
  • the same five through cmd_execute: that code, no --local-remote-dir, no leg clone in the work directory, nothing pushed to the source;
  • the same five through cmd_execute WITHOUT --yes, on a stream that is not a terminal: still the finding's own code, not adopt-unconfirmed;
  • a plan check passes raises nothing; a spec-only plan with --allow-empty-leg code raises nothing, and without consent is still adopt-empty-leg-unconsented;
  • the first finding's code with two classes at once, and the cap with its count;
  • order: an unresolved plan that is also uncovered is plan-unresolved, a stale plan that is also uncovered is plan-stale, an option-shaped path in an uncovered plan is unsafe-value;
  • one end-to-end test (git filter-repo required): execute on an uncovered plan, run without --yes, exits 2 with REFUSED plan-uncovered, --local-remote-dir is never created, no leg clone exists, and the corrected plan then runs to adoption verified into the same directory instead of meeting leg-remote-exists.

What each regression fails, measured on this branch's file (30 tests) against a sandbox copy of the code:

change to cmd_execute / the function tests that fail
the call line removed 11 (the five through cmd_execute, the five without --yes, the end to end)
the function returns before looking 24
the whole change reverted (main's adopt-project.py) 26
the call moved past _confirm (still before _create_leg_remotes) 6 (the five without --yes, the end to end); the 25-test file at 95dbabd failed none
the call directly after _refuse_an_unrunnable_plan 1 here, and test_adopt_e2e.py::test_a_plan_path_that_is_a_git_option_is_refused
the call before _refuse_an_unrunnable_plan 3 (unresolved, stale, option-shaped path)

Full suite at 95dbabd: 1264 passed, 1 skipped (the MedxEHR clone). The follow-up commit (a25dcd7) changes docstrings, comments, the wording of the one remediation message and the tests, not the code path; on it, tests/test_adopt_execute_refusals.py tests/test_adopt_e2e.py tests/test_adopt_plan.py tests/test_adopt_spec_only.py tests/test_windows_paths.py tests/test_cli_reference.py tests/test_repo_hygiene.py: 217 passed, 1 skipped with git-filter-repo on PATH, and tests/test_adopt_execute_refusals.py alone 29 passed, 1 skipped (the end to end) without it.

Relation to #166 and the merge order

Merge order: before #165, which must not land without it. It landed before #166 because nothing orders the two: each only adds refusals, and git merge-tree of a25dcd7 with #166's head is clean. #165's narrowed _verify would otherwise verify a plan check rejects, and this PR is what stops execute from reaching verification with one.

#166 adds submodule_plan_problems to cmd_check and to _refuse_an_unrunnable_plan. This PR edits neither, so the two do not conflict (git merge-tree of this branch at a25dcd7 with the #166 branch at 09d7889 is clean, and test_adopt_execute_refusals.py plus test_adopt_submodules.py pass on that merged tree: 82 passed). Once both are merged, check, _refuse_an_unrunnable_plan and _refuse_what_check_finds can read ONE shared list of findings (the coverage and leg findings and #166's submodule problems); that unification is deliberately left until after both merge, so the two branches do not edit the same functions. The function's docstring says the same.

Closes #168

🤖 Generated with Claude Code

Summary by Sourcery

Refuse all plans rejected by check before any leg repository is created or pushed.

New Features:

  • Make adopt-project.py execute refuse every plan finding reported by check before creating or pushing any leg repositories.

Bug Fixes:

  • Prevent invalid plans from partially executing, leaving repositories behind, or failing later during verification and blocking corrected plans with leg-remote-exists.

Enhancements:

  • Preserve existing refusal precedence and unsafe-path handling while reporting the first finding code and a capped list of matching check findings with remediation guidance.

Documentation:

  • Update the execute documentation to state that all plan findings are refused before leg repositories are created or pushed.

Tests:

  • Add regression coverage for all newly refused finding classes, refusal ordering, finding aggregation and limits, consent behavior, repository side effects, and successful execution after correcting a refused plan.

brettheap and others added 2 commits October 9, 2026 00:52
… a plan with an uncovered path, a bad `leg:`, a path covered twice, a duplicated path or an entry that covers nothing is refused before the first leg repository exists, and the corrected plan no longer meets `leg-remote-exists` (#168)

`_refuse_an_unrunnable_plan` refused only `plan-unresolved` and `plan-stale`. A plan `check` rejected for any other reason ran on into `_create_leg_remotes`, created and pushed BOTH legs, and failed only at the verification as `adopt-lost` -- or, for an ordinary uncovered file, exited 0 with the file left in the root. Either way the corrected plan then met `leg-remote-exists`, which has no `--force`.

`_refuse_what_check_finds` runs the two functions `check` reads, `_coverage_findings` and `_leg_findings`, over the same two lists and raises ONE `Refusal`: its code is the first finding's own, its detail is the findings as `check` prints them (capped at eight, with an "and N more" tail), and its remediation names the exit. `cmd_check` and `_refuse_an_unrunnable_plan` are not touched, so this merges with #166; once both are in, those two and this function can read one shared list.

The call is ONE added line in `cmd_execute`, and it sits after `_leg_paths` rather than directly after `_refuse_an_unrunnable_plan`: placed there it turned a plan path that is a `git` option from `unsafe-value` into `plan-uncovered`, which fails the existing `test_a_plan_path_that_is_a_git_option_is_refused`. It is still before `_confirm` and `_create_leg_remotes`, so nothing is created. The refusals that already existed keep their codes and their order, and `--allow-empty-leg` seeding is unaffected.

tests/test_adopt_execute_refusals.py covers each class, the order, the cap and a plan `check` passes, through `cmd_execute` in-process with no `git filter-repo`, and once end to end: the refused run leaves no `--local-remote-dir`, and the corrected plan then adopts.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…main really did with each plan `check` rejects (verified and exited 0, died at `git rm`, or `adopt-lost`) and claim only that no leg repository was created, and a test now pins the refusal ahead of the question to confirm (#168)

The review of 95dbabd found no blocker and these corrections to what the first commit says and tests. The code path is unchanged: `_refuse_what_check_finds` is the same function and the same one call in `cmd_execute`; only the wording of one refusal's remediation changes.

What main at 7f84ca4 did with a plan `check` rejects was reproduced, not assumed, and is now what the function's docstring and the test file's header say: `plan-uncovered` and `plan-bad-leg` on a source with no submodule VERIFIED and exited 0 with the paths left in the assembly root; `plan-covered-twice`, `plan-duplicate-path` and `plan-empty-entry` died at `git rm` after both legs were pushed; #168's three submodule cases failed the verification as `adopt-lost`. The first commit said all five ended as `adopt-lost`. The same three submodule cases are refused on this branch alone under `plan-uncovered` and `plan-bad-leg`.

No "nothing was created" overclaim: `_work_root` has already made the work directory and an `org/repo` source has already been cloned into it, so the module docstring, the function's docstring and the remediation now say that no leg repository was created and nothing was pushed, and the test helper is renamed `no_leg_was_created`. `_leg_paths` checks only the entries whose `leg:` is one of the four words, and the docstring now says so. `leg-remote-exists` is named only where it exists, under `--local-remote-dir`. `execute` is said to have refused `plan-unresolved`, `plan-stale` and the naming findings, not only the first two.

tests/test_adopt_execute_refusals.py: a run WITHOUT `--yes` on a stream that is not a terminal is refused under the finding's own code, in-process for each of the five classes and end to end for the uncovered plan (the refused half has no `--yes`), so moving the call past `_confirm` fails 6 tests where it failed none; the comment that said the suite imports no sibling test module, which it does, is replaced by the true one; the header no longer claims the tests fail "the same way" with and without the tool; the shared literals are constants, the fixture is a small dataclass instead of a dict read by key, no line is over 79 columns, and the misnamed `seeded` is `opened`.

Run: tests/test_adopt_execute_refusals.py, test_adopt_e2e.py, test_adopt_plan.py, test_adopt_spec_only.py, test_windows_paths.py, test_cli_reference.py and test_repo_hygiene.py: 217 passed, 1 skipped (the MedxEHR clone).

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

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-1, 2026-10-09T02:02:14Z)
@codex review

@sourcery-ai

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

@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

sonarqubecloud Bot commented Oct 9, 2026

Copy link
Copy Markdown

@sourcery-ai

sourcery-ai Bot commented Oct 9, 2026

Copy link
Copy Markdown

Reviewer's Guide

adopt-project.py execute now gates execution on every finding reported by check, preserving higher-priority safety refusals and preventing any leg repository creation or push until the plan is clean; extensive tests validate finding parity, refusal ordering, side-effect absence, and successful retry after correction.

Sequence diagram for execute refusal before leg creation

sequenceDiagram
    participant User
    participant Execute as cmd_execute
    participant PlanCheck as _refuse_an_unrunnable_plan
    participant Paths as _leg_paths
    participant Findings as _refuse_what_check_finds
    participant Remotes as _create_leg_remotes
    participant Verify as adoption verification

    User->>Execute: execute(plan, source)
    Execute->>PlanCheck: _refuse_an_unrunnable_plan(plan, source)
    PlanCheck-->>Execute: pass or Refusal(plan-unresolved/plan-stale)
    Execute->>Paths: _leg_paths(plan)
    Paths-->>Execute: paths_for or Refusal(unsafe-value)
    Execute->>Findings: _refuse_what_check_finds(plan, source)
    alt check findings exist
        Findings-->>Execute: Refusal(first finding code)
        Execute-->>User: exit 2, no leg repository or push
    else plan is clean
        Findings-->>Execute: pass
        Execute->>Remotes: _create_leg_remotes(...)
        Remotes->>Verify: create and push legs
        Verify-->>User: adoption verified
    end
Loading

Flow diagram for execute plan safety gate

flowchart TD
    A[cmd_execute] --> B[_refuse_an_unrunnable_plan]
    B -->|plan-unresolved or plan-stale| R1[Refusal with existing priority code]
    B --> C[_leg_paths]
    C -->|unsafe git option| R2[Refusal unsafe-value]
    C --> D[_refuse_what_check_finds]
    D -->|any check finding| R3[Refusal with first finding code]
    D -->|no findings| E[_refuse_unconsented_seeding and _confirm]
    E --> F[_create_leg_remotes]
    F --> G[Verify adoption]
    R1 --> H[Exit 2; no leg repository or push]
    R2 --> H
    R3 --> H
Loading

File-Level Changes

Change Details Files
Make execute reject all plan findings before creating or pushing leg repositories.
  • Added _refuse_what_check_finds to recompute coverage and leg findings using the same inputs as check.
  • Preserved existing refusal precedence for unresolved, stale, and unsafe path cases.
  • Reports the first finding’s code, up to eight finding details, and remediation guidance.
  • Wired the refusal after _leg_paths and before consent, confirmation, and leg creation.
  • Updated the module documentation to describe the broader refusal behavior.
adopt-project.py
Add regression coverage for finding refusal, ordering, side effects, and recovery.
  • Covered all five previously unrefused finding classes and verified details match check output.
  • Verified refusals occur without confirmation, leg creation, cloning, or source pushes.
  • Tested finding caps, multiple-finding precedence, safe handling of option-shaped paths, stale/unresolved plans, and empty-leg consent.
  • Added an end-to-end test showing a corrected plan can reuse the previously requested remote directory because the failed run created nothing.
tests/test_adopt_execute_refusals.py

Assessment against linked issues

Issue Objective Addressed Explanation
#168 Make execute refuse every plan finding that check reports, including plan-uncovered, plan-bad-leg, plan-covered-twice, plan-duplicate-path, and plan-empty-entry, before creating or pushing any leg repository. ✅
#168 Ensure a rejected plan leaves no leg repositories or pushed changes, so correcting the plan allows a subsequent execute to complete without encountering leg-remote-exists. ✅
#168 Preserve existing behavior for plans that pass check, including unresolved/stale refusal precedence and --allow-empty-leg seeding behavior. ✅

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-2, 2026-10-09T02:41:02Z) ADVISORY REVIEW of fix/adopt-execute-refuses-check-findings at 95dbabd and a25dcd7 (this PR's head; the squash landed as b18cca9 while the review ran, tree identical to a25dcd7). Read-only, own detached worktrees; nothing authored, no claim held. Full report with drivers and logs: opensoft/brett-wip handoffs/xFactory/attachments/lane-openRepoShape-2/review168-a25dcd7/report.md.

Verdict: MERGEABLE WITH NITS; no P1, no P2; three P3s open at b18cca9. Four P3s found at 95dbabd were fixed by a25dcd7 (the account of main's behaviour, the "no repository was created" overclaim, the _leg_paths docstring, and a test pinning the refusal before the confirmation prompt). Class table, 23 cases on real local sources, each on main, 95dbabd and a25dcd7: on main a check-rejected plan either printed "adoption verified" with the uncovered or bad-leg file left in the root (plain files), crashed at the mount's git rm after both legs were pushed (covered twice, duplicate path, empty entry), or failed verification with adopt-lost (submodules), and in every case the corrected re-run met leg-remote-exists; on the branch every class is refused with exit 2, no leg remote, no adopt/* branch, source HEAD unchanged, and the corrected re-run into the same directories verifies. Check-clean, spec-only and code-only adoptions match main apart from the embedded shape commit; confirmation ordering unchanged; --help unchanged and docs/cli.md ok. All six rows of the PR's mutation table re-measured and matched. Merges clean with every head tried, including the announced order (09d7889, a25dcd7, 05fa0e0) and what exists now (#166 at 794828c plus 05fa0e0), with execute refusing both #166's submodule problems and these classes before any leg exists; adopt tests 222 passed / 1 skipped on the current heads, 136 passed / 1 skipped at a25dcd7 alone, no filter-repo skips.

P3-1. #168's "one list both commands read" is deferred (adopt-project.py:1403-1407 "ONE LIST, LATER"; the composition is built separately in cmd_check :1035-1038 and _refuse_what_check_finds :1409-1412, three copies once #166 lands). No drift today (every FINDING emitter check has is refused before _create_leg_remotes), but nothing tracks it now that #168 is closed, and #166's 794828c leaves _refuse_what_check_finds untouched. Open a follow-up issue, or do it in #166 before it merges, and cite it in the docstring.

P3-2. No test builds a source with a submodule (tests/test_adopt_execute_refusals.py:1-37), so neither #168's own three reproductions nor the reason #168 must land before #165 is pinned: with #166 (09d7889) and #165 (05fa0e0) merged WITHOUT #168, check-rejected plans (upstream/ leg cod, the upstream/ entry deleted, README.md uncovered) verify with exit 0. One in-process parametrized case over a source with a registered upstream/dependency (no filter-repo needed, the refusal comes first) pins it.

P3-3 (nit). The "N finding(s)" and "and N more" counts count LINES, including _coverage_findings' own 20-cap tail line: with 25 uncovered paths it prints "21 finding(s) … and 13 more" while 17 paths are hidden. check counts the same way.

Unverified: the PR's full-suite count, its gh repo create path and CI (local remotes only here).

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