Skip to content

scaffold-project.py: a leg gh sees and git cannot read is refused scaffold-remote-exists with git's terminal prompt off, and the reuse re-run's visibility lines go by each leg as it stands (#542) - #565

Merged
brettheap merged 5 commits into
mainfrom
fix/542-scaffold-rerun-unreadable-leg
Oct 15, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Lane: openreposhape-2 (openRepoShape-2)
Claim: #542 (comment)

Two re-run paths of scaffold-project.py, from the review of #506 and carried on #459 as v1.0.0 known limits. Both did their harm through the message. Base: main bfed143, merged with main b4c1f50 at 26a5f6a6 (no conflicts; main has moved on since, by files this PR does not touch). Head: 6889d991.

What changed

A leg gh sees and git cannot read. _reusable_remotes asks gh whether a leg is empty. For a leg that is not empty, _held_seed then asks git, through only_commit, whether its one commit is this scaffold's seed. gh can see a repository that git cannot read: gh is logged in and git has no credential for the host. In that case only_commit's CommandFailed reached main as the generic REFUSED a git or gh command failed, with --reuse-empty-repo and without it. Typed in a terminal, git stopped before that at its Username for ...: prompt, because run() set no GIT_TERMINAL_PROMPT.

The reuse re-run's visibility lines.

  • _leg_visibility's NOTE and WARNING spoke of "the two new legs". They are printed before _reusable_remotes knows which legs exist, so a re-run that reused one leg or both still called them new. Now they say what happens to a leg this run creates, and that a reused leg keeps its own visibility.
  • _private_leg_note went by the run's visibility alone. So a private run that reused a public leg called that leg private, right beside the WARNING that it is public. And a public run that reused private legs printed no note at all. Now _create_remotes returns each repository's visibility as it stands: the run's for one it created, GitHub's for one it reused. The note names each private or internal leg at its own visibility. Two legs at the same visibility print the sentence they always did.

The Free-plan hint follows the root. _private_leg_note printed free_plan_secret_hint whenever a leg was private or internal. The hint says the organisation's secrets "resolve to EMPTY" inside the validate workflow, and offers repository secrets or an upgrade to Team. That is true of the repository the workflow runs in, the assembly root, and only when the root is private or internal: on the Free plan an organisation secret reaches a public repository. So a run with --visibility public, or a reused public root, that reused a private leg printed a false hint (rv542's matrix found 15 such runs this PR had added).

  • _create_remotes now also carries the root's visibility out of the run: GitHub's for a reused root, read with remote_visibility and no second WARNING (_leg_visibility has already said where the run's flag disagrees with it), else the run's.
  • The hint is printed only when that value is private or internal. The per-leg NOTE is unchanged: a public root still needs a credential to read a private leg.

Tests

  • New, in tests/test_scaffold_reuse_seeded_legs.py: eighteen tests.
    • Seventeen run against a fake gh, which are POSIX-only, like that file's existing gh test. Three of them rewrite git's https://github.com/<org>/, to a missing path (the two unreadable-leg cases) or to a local server that answers 401 under a pseudo-terminal (the pty case). The other fourteen set no rewrite and reach no git remote: the five visibility-text cases and nine Free-plan-hint cases (the fake gh answers api orgs/<org> with the plan FAKE_GH_PLAN names).
    • One runs in-process and checks that both probe commands carry PROBE_ENV. It runs on every platform.
    • The nine hint cases: a public run over a new root, a reused public root, a public root the run inherits, and --visibility private over a public root print the NOTE and no hint; a public flag over a private root, an internal root, a private root with reused legs and a fresh private run print both; a paid plan prints the NOTE alone.
  • Changed: tests/test_reuse_visibility.py's assertion of the old NOTE wording, and the in-process test's composite assert, now two (Sonar S9073).
  • Red at the first head fdb0fa53, with this PR's test file: the first four hint cases, which print the hint at a public root.
  • Red on main bfed143: 13 of the 18 new tests, and the updated assertion.
    • The terminal test hangs at Username for 'http://127.0.0.1:…': until the harness kills it at 90 s.
    • The unreadable-leg tests get REFUSED a git or gh command failed.
    • The in-process test fails because main has no PROBE_ENV.
    • Four of the five visibility-text cases fail on the old NOTE wording or the missing note; both-public is a control and passes.
    • Five of the nine hint cases fail: private-flag-over-public-root prints the false hint, and public-run-new-root, public-root-reused, public-root-inherited and public-flag-over-private-root get no NOTE at all for the reused private leg, since main goes by the run's visibility. The other four are controls and pass.
  • Targeted run on the head 6889d991: 1183 passed, 19 skipped (1174 and 19 on the merge 26a5f6a6, plus the nine hint cases). It covered every tests/test_scaffold*.py, test_setup*.py and test_family_init*.py, plus test_template_text_placeholders.py, test_cli_reference.py, test_repo_hygiene.py, test_reuse_visibility.py, test_free_plan_hint.py and test_ruleset_hint_tracking_branch.py. scripts/render-cli-reference.py --check passes, and no --help text changed.
  • Mutants of the hint, 5 of 5 killed at 6889d991: the hint ignores the root; a reused root takes the run's flag instead of GitHub's; an internal root is not closed; the hint's root condition inverted; the NOTE gated by the root too.
  • Mutants of the first commit, 6 of 6 killed at fdb0fa53, not re-run: no CommandFailed catch; no PROBE_ENV on ls-remote; no PROBE_ENV on fetch; the reused leg's visibility not kept for the note; the old NOTE wording; git's last line in place of its first fatal: line.

Left out

README follow-up (after #519): none needed.

Closes #542

🤖 Generated with Claude Code

Summary by Sourcery

Make scaffold-project reuse checks fail clearly for unreadable remotes and report repository visibility and credential guidance according to each leg and the assembly root as they actually stand.

Bug Fixes:

  • Refuse existing GitHub legs that gh can see but git cannot read with a specific access-related remediation instead of a generic command failure or terminal hang.
  • Report reused and newly created leg visibility accurately, including per-leg private/internal access notes and Free-plan hints based on the assembly root's actual visibility.

Enhancements:

  • Disable terminal prompts for scaffold seed probes and support per-command environment overlays.
  • Improve reuse visibility messaging so it reflects each repository's current state and avoids describing reused legs as newly created.

Tests:

  • Add coverage for unreadable legs, disabled git prompts, mixed reused-leg visibility, and Free-plan hint behavior across root visibility and billing plans.

…scaffold-remote-exists`, saying git could not read it, with git's terminal prompt off, and the reuse re-run's visibility lines go by each leg as it stands, so a re-run neither waits on a username, nor fails as `a git or gh command failed`, nor calls a reused public leg private (#542)

Two re-run paths from the review of #506, carried on #459 as v1.0.0
known limits, whose harm was the message. Base: main bfed143.

A leg git cannot read. `_reusable_remotes` asks `gh` whether a leg is
empty, and `_held_seed` then asks git, through `only_commit`, whether a
leg that is not empty holds this scaffold's seed. `gh` sees a repository
git cannot read when `gh` is logged in and git has no credential for the
host, and `only_commit`'s `CommandFailed` reached `main` as the generic
`a git or gh command failed`, with the flag and without it. Typed in a
terminal, git stopped first at its `Username for ...:` prompt, because
`run()` set no `GIT_TERMINAL_PROMPT`.
- `only_commit` runs `git ls-remote` and `git fetch` under `PROBE_ENV`,
  `GIT_TERMINAL_PROMPT=0`, through a new optional `env` overlay on
  `shape_materialize.run` (unset, `run` behaves as before).
- `_held_seed` catches `only_commit`'s `CommandFailed` and returns why, in
  one line: the command, its exit and git's first `fatal:` line.
  `_reusable_remotes` refuses the leg as `scaffold-remote-exists`, as it
  refuses any leg with commits (#462's must-keep), and the remedy says git
  could not read it, so whether it holds the seed cannot be asked: let git
  read it (on GitHub, `gh auth setup-git`), then re-run.
- `seed_tree` reads nothing remote, so a failure there is still the
  generic `CommandFailed`.

The visibility lines. `_leg_visibility`'s NOTE and WARNING spoke of "the
two new legs", printed before `_reusable_remotes` knows which legs exist,
so a re-run that reused one or both said legs were new. They now say what
happens to a leg this run creates, and that a reused leg keeps its own.
`_private_leg_note` went by the run's visibility, so a private run that
reused a public leg called it private beside the WARNING that it is public,
and a public run that reused private legs said nothing. `_create_remotes`
now returns each leg's visibility as it stands, the run's for a leg it
created and GitHub's for one it reused (`_reused_leg_visibility` returns
what it read), and the note names each private or internal leg at its own
visibility. Two legs at one visibility print the sentence they always did.

Tests: tests/test_scaffold_reuse_seeded_legs.py gains nine, eight against a
fake `gh` with git's `https://github.com/<org>/` pointed at a path or a
local server answering 401 (POSIX), and one in-process test that both
probe commands carry `PROBE_ENV` (every platform);
tests/test_reuse_visibility.py's assertion of the old NOTE is updated.

Left out: `--filter=tree:0` on the probe fetch (optional on #459; git
refuses `--filter` in a repository with no promisor remote set up), and
the manifest's `visibility:`, which still records the run's value.

Lane: openreposhape-2 (openRepoShape-2)
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 hardens scaffold reruns by preventing Git credential prompts during seeded-leg probes, turning Git-inaccessible but GitHub-visible legs into precise existing-remote refusals, and making reuse visibility messaging reflect each leg’s actual visibility, with comprehensive regression tests.

Sequence diagram for unreadable seeded-leg refusal

sequenceDiagram
    participant Scaffold as scaffold-project.py
    participant GH as gh
    participant Git as git
    participant Run as shape_materialize.run

    Scaffold->>GH: remote_is_empty
    GH-->>Scaffold: repository has commits
    Scaffold->>Git: only_commit
    Git->>Run: git ls-remote and git fetch
    Run->>Git: GIT_TERMINAL_PROMPT=0
    Git-->>Run: CommandFailed
    Run-->>Scaffold: command, exit code, first fatal line
    Scaffold-->>Scaffold: refuse as scaffold-remote-exists
    Scaffold-->>Scaffold: advise git access, then re-run
Loading

Flow diagram for per-leg reuse visibility messaging

flowchart TD
    A[_create_remotes] --> B{Leg reused?}
    B -->|No| C[Record run visibility]
    B -->|Yes| D[_reused_leg_visibility]
    D --> E[Read GitHub visibility]
    C --> F[Return each leg visibility]
    E --> F
    F --> G[_private_leg_note]
    G --> H{Private or internal legs?}
    H -->|Yes| I[Name each affected leg at its actual visibility]
    H -->|No| J[No visibility note]
Loading

File-Level Changes

Change Details Files
Make remote-leg probing non-interactive and convert unreadable GitHub legs into actionable existing-remote refusals.
  • Add an environment overlay to command execution while preserving the caller environment.
  • Disable Git terminal prompts for git ls-remote and git fetch probes.
  • Capture Git command, exit status, and diagnostic text when a leg cannot be read.
  • Refuse unreadable legs as scaffold-remote-exists and recommend configuring Git authentication before rerunning.
scaffold-project.py
scripts/shape_materialize.py
Report visibility based on each leg’s actual state during reuse reruns.
  • Clarify that visibility notes apply to legs created this run while reused legs retain their existing visibility.
  • Return each leg’s effective visibility from remote creation and use it for private/internal-leg guidance.
  • Generate per-leg or combined notes only for legs that are actually private or internal.
scaffold-project.py
tests/test_reuse_visibility.py
Expand regression coverage for unreadable remotes, prompt suppression, and per-leg visibility messaging.
  • Add fake-gh, rewritten Git-host, and pseudo-terminal scenarios covering unreadable legs with and without reuse.
  • Verify both probe commands receive the non-interactive environment.
  • Test mixed, matching, and public/private/internal visibility combinations and update wording assertions.
tests/test_scaffold_reuse_seeded_legs.py
tests/test_reuse_visibility.py

Assessment against linked issues

Issue Objective Addressed Explanation
#542 When GitHub CLI can see a non-empty leg but Git cannot read it, avoid a terminal credential prompt and refuse the leg as scaffold-remote-exists with a leg-specific explanation and remedy, rather than the generic command-failed message. ✅
#542 Make reuse-run visibility messages accurately describe legs created versus reused, and base private/internal-leg notes on each leg’s actual visibility rather than only the run’s requested visibility. ✅

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-15T10:55Z)

This lane reviewed its own PR #565 at fdb0fa5, read-only in its own detached worktrees, and authored nothing.

CHANGES NEEDED, one small fix. A fix round follows on this branch. The PR lands only on Brett Heap's word.

Findings:

  • P3, fix in this PR. The Free-plan hint now prints when the assembly root is public (scaffold-project.py:1193-1196). It is keyed to the legs, but the workflow runs in the root. A public run that reuses a private leg says org secrets "resolve to EMPTY" and suggests upgrading to Team. Key the hint to the root's own visibility (read at :982) and add a test.
  • Nit, fix in this PR: split the composite assert at tests/test_scaffold_reuse_seeded_legs.py:484 (Sonar S9073).
  • Nit, fix in this PR: the PR body and squash message say eight tests rewrite git's URL. Three do.
  • Follow-ups for Tracking: setup, scaffold, family and the shared helpers, creating projects and families #459:
    • the push of a reused empty leg still waits at Username for (:1249);
    • ssh, prompting credential helpers and askpass still prompt;
    • the SSH-rewrite remedy (:512);
    • the manifest visibility:;
    • --filter=tree:0 fails on a local path.

Tests at fdb0fa5: 5170 passed, 32 skipped (61 targeted files). Main with the new tests: 9 failed, 14 passed, each for its stated reason. 22 refusal probes all refused scaffold-remote-exists with nothing created. In a 272-run text matrix, the NOTE, the WARNING and the private-leg note agreed every time. CI: Linux and macOS passed; Windows was still running.

Merges: clean with main, #570, #569 and fix/522.

The full report is handoffs/xFactory/attachments/lane-openRepoShape-2/review565-fdb0fa5/final-report.md in opensoft/brett-wip.

…w and its both-ways-in line, `clone_just_pushed`) so the PR's CI merge ref and this fix round run on current main

The brief named main 2247dbf; the shared store's origin/main had advanced
to b4c1f50 (2247dbf plus #553), so this merges that. No conflicts: the
PR's four files (scaffold-project.py, scripts/shape_materialize.py,
tests/test_reuse_visibility.py, tests/test_scaffold_reuse_seeded_legs.py)
are not among those main changed since bfed143, apart from the test files
below. #554's `clone_just_pushed` is kept as main has it.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Brett Heap and others added 2 commits October 15, 2026 11:13
…he assembly root, where the `validate` workflow runs, is private or internal as it stands, so a public root that reuses a private leg is no longer told its organisation secrets "resolve to EMPTY" (#542)

`_private_leg_note` printed `free_plan_secret_hint` whenever a leg was
private or internal. The hint is true of the repository whose workflow
reads the secrets, the root: on the Free plan an organisation secret
reaches only a public repository. So a run with `--visibility public`, or
a reused public root, that reused a private leg was told its secrets
"resolve to EMPTY inside the workflow" and offered repository secrets or
an upgrade to Team, both pointless for a public root. rv542's N2 matrix
found 15 such runs the head added.

`_create_remotes` now returns each repository's visibility as it stands,
the root's included: the run's for a repository it created, GitHub's for
a reused one (the reused leg's by `_reused_leg_visibility`, the reused
root's by a plain `remote_visibility` read, since `_leg_visibility` has
already said where the run's flag disagrees with it). The hint follows
the root's value; the per-leg NOTE is unchanged, as a public root still
needs a credential to read a private leg.

tests/test_scaffold_reuse_seeded_legs.py: the fake `gh` answers
`api orgs/<org>` with `FAKE_GH_PLAN` when set, and nine rows pin the hint:
a public run over a new root, a reused public root, a public root the run
inherits, and `--visibility private` over a public root print the NOTE
and no hint (red at fdb0fa5); a public flag over a private root, an
internal root, a private root with reused legs and a fresh private run
print both; a paid plan prints the NOTE alone.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…sserts that the leg's one commit was found and that it is the leg's tip in two statements, so Sonar's S9073 no longer flags its composite assert and a failure says which half broke (#542)

`assert held is not None and held[0] == tip(...)` is one assert for two
claims. The first says `only_commit` found a commit at all, the second
that it is the one the leg holds; split, a `None` fails on the first
with `asked` beside it, instead of on a comparison of `None[0]`. rv542's
F7; it was the PR's only Sonar issue.

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

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-2, 2026-10-15T11:49Z)

This lane re-verified its own PR #565 at 6889d99, read-only in its own detached worktrees, and authored nothing.

MERGEABLE WITH NITS. Land it before #573: #573's --dry-run reaches only_commit, and alone it meets a leg git cannot read with the generic refusal or a Username for wait. Landing waits for the v1.0.0 tag, Brett Heap's word and three-OS CI.

  • Fixed: the Free-plan hint prints only for a private or internal root. Of 256 runs, it printed in all 159 with a closed root and leg, and in none of the 66 with a public root.
  • Fixed: Sonar reports 0 issues; the texts' counts hold.
  • Nit, follow-up: the hint still names the legs as its reason (scaffold-project.py:1210).
  • Nit: squash-message.md:4 omits the b4c1f50 merge.

Tests at 6889d99: 1183 passed, 19 skipped (21 files). fdb0fa5 with the new tests: 4 failed, each a hint at a public root.

Merges: clean with main 9f80939 and #570. #573 conflicts with main itself, in two test files.

The report is handoffs/xFactory/attachments/lane-openRepoShape-2/review565-fdb0fa5/reverify-6889d99.md in opensoft/brett-wip.

brettheap pushed a commit that referenced this pull request Oct 15, 2026
… edited mount path on a line of its own, so Sonar's S9073 composite-assert rule has nothing to fire on (#522)

The fixture that edits `legs.spec_path` and `legs.code_path` checked both
landed with one `and`. Two asserts say which of them did not, and the
repository's gate has refused a composite assert before (#565, #570).

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

Copy link
Copy Markdown

@brettheap
brettheap merged commit 26c1600 into main Oct 15, 2026
5 checks passed
@brettheap
brettheap deleted the fix/542-scaffold-rerun-unreadable-leg branch October 15, 2026 15:53
brettheap pushed a commit that referenced this pull request Oct 15, 2026
Brings in #597 (654c476, CI on Linux only), with #565 and #555 before it.
Three conflicts:
	.github/workflows/tests.yml: main's file whole. #587's
	  `/bin/bash -n install.sh` and its comment lived in the macOS job #597
	  removed; no bash 3.2 step is added anywhere, as #597 decided, and this
	  pull request no longer touches tests.yml.
	README.md: the macOS sentence keeps #587's `install.sh` in the list and
	  takes #597's wording: "`setup.sh`, `install.sh` and the `openRepoShape`
	  command are written to use no syntax newer than that, though CI runs on
	  Linux only and no run of it checks them against bash 3.2", with "all
	  three entry points" after it. Four lines, as both sides had: README.md
	  stays 1449 lines, its cap with tests/readme-cap.d/564.txt (+22).
	docs/handbook.html: the macOS panel's sentence says what the merged
	  README says. The seven markers that read README.md § Starting a
	  project in a new organisation (masthead, start, partway, ci, legs,
	  doublepin, park) move to a0e0f23c306cebdf627d90225e33c277f05de64b, as
	  the hygiene check printed; the macOS sentence is the only part of that
	  section the merge changed, and no other part of the seven speaks of
	  CI's runners or bash 3.2. The footer is the same on both sides.
test_install_sh.py's pin check still holds: the Linux job installs
git-filter-repo==2.47.0, install.sh's pin. The tests' dated history that
names the macOS job stays, as #597 left it.

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>
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