Skip to content

adopt-project.py: check and execute iterate a plan's follow_ups: without asking for a list, so a hand-edited plan with follow_ups: 5 (or true) ends check in a TypeError: ... is not iterable traceback, and execute in the same one after both leg repositories are created and pushed #293

Closed

Description

@brettheap

(lane openRepoShape-2, 2026-10-12T00:11:30Z)

Found by this lane's adversarial review of PR #270 at aed9190 (the fix for #255), its follow-up FU2. It is the same on main d94d155, where it was reproduced again today. On main execute pushes both legs before it raises. At #270's head execute reads follow_ups: in _plan_values, before any leg exists (the review's :2962), so nothing is created there and the PR improves the execute half; the check half is unchanged (_print_what_will_happen, :1992 at that head). Not claimed.

What is wrong. plan writes follow_ups: as a block list. check and execute read it back with plan.get("follow_ups", []) and loop over it, with no test that it is a list. A hand-edited plan where follow_ups: is a truthy scalar (5, true, 1.5) ends both in TypeError: 'int' object is not iterable (or 'bool', 'float'), exit 1. check is the command that exists to say whether a plan is sound, and it ends in a traceback, with no FINDING naming the key. execute is worse on main: the read happens in the split-commit step, after _build_the_legs has created and pushed both leg repositories (two real repositories on the GitHub route), so the run dies with the legs left behind and no split branch.

# $U: a checkout of openRepoShape at d94d155 (git-filter-repo on PATH). $SRC: a three-file git repository (src/app.py, docs/spec.md, README.md), one commit. Local remotes only; no network.
python3 "$U/adopt-project.py" plan --source "$SRC" --project Northwind --org testorg --elected-by 'T H' --elected-on 2026-09-02 --out "$P"
python3 - "$P" <<'PY'            # replace the follow_ups block with a scalar
import re, sys
t = open(sys.argv[1], encoding="utf-8").read()
t, n = re.subn(r"^follow_ups:\n(?:  - .*\n)*", "follow_ups: 5\n", t, count=1, flags=re.M); assert n == 1
open(sys.argv[1], "w", encoding="utf-8", newline="\n").write(t)
PY
python3 "$U/adopt-project.py" check --plan "$P"
#   exit 1, after the `triad`, names and `WHAT WILL HAPPEN` lines:
#   adopt-project.py line 2222, in cmd_check / line 2185, in _print_what_will_happen:   for item in plan.get("follow_ups", []):
#   TypeError: 'int' object is not iterable
python3 "$U/adopt-project.py" execute --plan "$P" --yes --local-remote-dir "$R/remotes" --work-dir "$R/work"
#   exit 1
#   adopt-project.py line 3049, in cmd_execute / line 2977, in _commit_the_split:     follow_ups = [str(f) for f in plan.get("follow_ups", [])]
#   TypeError: 'int' object is not iterable
git --git-dir="$R/remotes/Northwind-spec.git" rev-parse --short main     # dc3f11f   (both legs hold a `main`)
git --git-dir="$R/remotes/Northwind-code.git" rev-parse --short main     # 02e2626
git -C "$SRC" rev-parse --verify adopt/three-repo-shape                  # fatal: no split branch
python3 "$U/adopt-project.py" execute ... (again)                         # exit 2, REFUSED leg-remote-exists: <R>/remotes/Northwind-spec.git already exists
the plan's follow_ups: check execute
5 exit 1, TypeError: 'int' at :2185 exit 1, TypeError: 'int' at :2977, both legs pushed, no split branch
true exit 1, TypeError: 'bool' exit 1, TypeError: 'bool', both legs pushed, no split branch
abc exit 0, prints three lines follow-up: a, b, c exit 0, adoption verified; the split commit's message ends FOLLOW-UPS, which this commit does NOT do: then - a, - b, - c
{a: 1} exit 0, prints follow-up: a exit 0
[], or the key bare (controls) exit 0, no follow-up lines exit 0

The string and mapping rows do not crash, but they are the same unchecked read: a string is iterated character by character.

Why. Two readers, neither guarded. _print_what_will_happen (adopt-project.py:2162, called from cmd_check at :2222) has for item in plan.get("follow_ups", []): at :2185. _commit_the_split (:2977, called from cmd_execute at :3049, after _build_the_legs and _create_leg_remotes) has follow_ups = [str(f) for f in plan.get("follow_ups", [])]. A value of [] or no value falls through to the default, so only a truthy non-iterable scalar crashes. cmd_execute does call _refuse_what_check_finds (:3012) before any leg exists (#168), but its list of findings (_entry_findings, _leg_findings, the coverage findings) does not look at follow_ups, so there is nothing for it to refuse.

What it should do. The plan is untrusted input (cmd_execute's own comment at :3006). check reports a plan whose follow_ups: is present and is not a list of strings as a FINDING that names the key and what it holds, exit 1, and it is on the list execute refuses through _refuse_what_check_finds, so execute exits 2 before any leg exists. A string or a mapping is refused too: iterated, it is not what the plan writer wrote. An empty list and a bare key stay valid.

How to test. In tests/test_adopt_execute_refusals.py, beside test_execute_refuses_each_class_and_creates_no_leg (:347) and with its helpers (write_edited, check_findings, run_check, no_leg_was_created): parametrise over follow_ups: 5, true, abc and {a: 1}:

  • check exits 1 with a finding that names follow_ups and no Traceback;
  • execute exits 2 with the same, no Traceback, and no *-spec.git or *-code.git under the local remote directory;
  • controls: [] and a bare follow_ups: still pass check and execute.

Reverting the guard fails the first two.

Related: #270 and #255 (the PR whose review found this; at its head the execute read is before any leg, so the leg-pushing half shows only on main); #196 (check admitting what execute refuses, the general class); #188 (a failure after both legs are pushed prints no notice of what exists, and the re-run meets leg-remote-exists); #262 and #263 (the same shape for other plan inputs: check passes, execute dies after the legs are pushed).

🤖 Generated with Claude Code

Activity

  1. brettheap commented on Oct 12, 2026

    @brettheap
    ContributorAuthor

    CLAIMED — lane openRepoShape-2, session 5b7b5f3d-dce7-452d-a33d-ab8537d91db3@Eagle, 2026-10-12T11:00:51Z, for #293

    Logged in lanes/log/openRepoShape-2.md at ef0cfa18c3e6d618bb710d19db811662b0bf9f08.

    The three reads (lane-collision-protocol Rule 1):

    1. existing `CLAIMED —` comments on opensoft/openRepoShape#293:
       none
    2. `gh pr list --repo opensoft/openRepoShape --state all --search "#293"`, kept where the row or its body names `#293`:
       none
    3. `git ls-remote --heads git@github.com:opensoft/openRepoShape` branches naming `293` as a whole token:
       none
    
  2. added 3 commits that reference this issue on Oct 12, 2026
  3. added a commit that references this issue on Oct 12, 2026
  4. added 8 commits that reference this issue on Oct 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions