Skip to content

scaffold-project.py: a first scaffold's pushes and a --reuse-empty-repo re-run's pushes and seed fetch no longer ask at the terminal: a push git has no login for fails naming the fix, and a leg git reaches over ssh is refused with ssh's reason (#915) - #971

Merged
brettheap merged 3 commits into
mainfrom
l3-915-scaffold-git-prompts
Oct 19, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Part of #915. Its sections U036 and U038 are done; U037's ruling question stays open, as the issue keeps it, so the issue stays open for that ruling.

What changed, by section

Username for on a reuse re-run (U036)

PROBE_ENV (GIT_TERMINAL_PROMPT=0) covered only_commit's two commands only. The prompt rule is now one constant, REMOTE_ENV = {"GIT_TERMINAL_PROMPT": "0"}, and the three calls the section names pass it through run's env argument: each leg's push (_seed_legs), the root's push (_build_assembly_root) and _take_held_seed's fetch. only_commit's two run under PROBE_ENV, which is now REMOTE_ENV plus LC_ALL=C (see U038). With these, every git command the scaffold sends to a remote is under the one rule. I checked this with a grep: the other git calls in scaffold-project.py read local repositories only (init, write-tree, submodule add from the leg's working tree, config, rev-parse). scripts/shape_materialize.py's run already lays env over the caller's environment (#542), so that file is unchanged.

The leg push is one call for a fresh scaffold and for a reuse re-run. So a first scaffold's push also no longer stops at git's prompt. It fails in git's words (terminal prompts disabled) and prints the refused-push block that every push failure prints. The README already says the push fails without a credential helper (gh auth setup-git), and setup-project.py's preflight offers that helper. setup-project.py runs this scaffold, so the change reaches it too.

ssh, credential helpers and askpass still prompt (U037): left open

This section's Direction asks for a ruling ("Decide whether the probe must never wait for input"), so it is out of scope here. I added no BatchMode and no helper or askpass handling. REMOTE_ENV's comment records that the question is open, and the ruling, once made, changes that one constant for every command above.

An ssh-rewrite operator's refusal (U038)

  • _unread_because: where git's first fatal: line is the generic fatal: Could not read from remote repository. (GENERIC_FATAL), the line before it is the reason. That line is ssh's last line, such as git@github.com: Permission denied (publickey). or GitHub's ERROR: Repository not found.. A fatal: line of git's own that comes before the generic one is kept as before ('<path>' does not appear to be a git repository), and so is the generic line when nothing comes before it.
  • The remedy keeps the gh auth setup-git clause and adds a clause beside it: "where a url.<base>.insteadOf rule in your git config sends https://github.com/ over ssh, git logs in with an ssh key instead, and ssh -T git@github.com says whether GitHub takes yours".
  • Of the Direction's two options, I took "run the probe's git with LC_ALL=C". only_commit's ls-remote and fetch run under PROBE_ENV = {**REMOTE_ENV, "LC_ALL": "C"}. Under the C locale gettext translates nothing and ignores LANGUAGE, so the fatal: prefix and the generic line are found whatever the operator's locale. The exit-status option was not taken: git exits 128 for both kinds of failure, so the exit status cannot pick out the line that gives the reason.

Three choices the Directions leave open, and why I made them this way:

  • The ssh clause is always printed, not only when ssh was detected. "Beside" reads as both clauses in one remedy, and each clause says where it applies. A clause shown on a guess could go missing from the very run that needs it.
  • LC_ALL=C is on the probe only, not on the pushes. A push failure is printed whole for a person to read, so it stays in their locale. The probe's output is the only output this code parses.
  • The command shown still names the https URL this run gave git. The remedy's ssh clause says that a rewrite sends it over ssh. I did not add a git ls-remote --get-url call to show the rewritten URL.

Out of scope

Tests

Each test command ran on head e40af32 (base 01a2b01), under a stub gh first on PATH that exits 97, with GH_TOKEN=bogus and an empty GH_CONFIG_DIR, one pytest process at a time. Every command was /usr/bin/python3 -m pytest -q -p no:cacheprovider --basetemp=<scratch>/bt-<name> -o tmp_path_retention_policy=failed <files>:

  • tests/test_scaffold_reuse_seeded_legs.py, the changed file: 39 passed in 42.99s. It has nine new cases:

    • test_every_git_command_that_reaches_a_remote_runs_under_the_rule: in-process, over legs holding the seed. It records the nine remote commands in order, each with the environment it was given.
    • test_a_reuse_re_run_over_empty_legs_does_not_wait_at_its_push: in a pty, against a local server that answers 401. This is U036's own symptom.
    • test_a_leg_git_reaches_over_ssh_is_refused_with_ssh_s_reason: an insteadOf rewrite to ssh, with a stand-in ssh that refuses every key, so nothing leaves the machine.
    • test_the_reason_git_could_not_read_a_leg: six cases, in-process.

    test_the_two_git_commands_that_read_a_leg_ask_no_terminal now pins REMOTE_ENV and PROBE_ENV. The two pty tests share one helper, in_a_terminal_asked_for_a_login. UNREAD_EXIT carries the new remedy.

  • The docs guards and the maintenance guard (tests/test_agents_md_codes.py tests/test_cli_reference.py tests/test_update_add_codes_documented.py tests/test_repo_hygiene.py tests/test_no_auto_maintenance.py): 217 passed in 54.17s. No refusal code is new.

  • The other 59 test files that name scaffold-project.py or SCAFFOLD, in four batches:

    • batch 0, tests/test_adopt_pins.py tests/test_bootstrap_after_bump.py tests/test_derived_id_refusals.py tests/test_doctor_next_is_a_command.py tests/test_doctor_validator_verdicts.py tests/test_dry_run_parity.py tests/test_family.py tests/test_github_limits_init_check_org.py tests/test_github_name_limits.py tests/test_local_remote_dir_urls.py tests/test_link_source_chain_defaults.py tests/test_pinned_copy_text.py tests/test_reference_default.py tests/test_scaffold_free_text_values.py tests/test_scaffold_mount_ignored.py: 999 passed in 767.06s (0:12:47)
    • batch 1, tests/test_setup_project_py.py tests/test_shape_advisory.py tests/test_openreposhape_package.py tests/test_family_add_refusals.py tests/test_scaffold_leg_paths.py tests/test_chain_link_fold.py tests/test_pin_annotated_tag.py tests/test_clone_url_bytes.py tests/test_openreposhape_command.py tests/test_scaffold_e2e.py tests/test_scaffold_leg_path_collisions.py tests/test_scaffold_pin_and_reuse.py tests/test_setup_by_hand_commands.py tests/test_template_quoted_values.py tests/test_free_plan_hint.py: 1253 passed in 420.04s (0:07:00)
    • batch 2, tests/test_update_shape_add.py tests/test_neutral_pin_validator.py tests/test_git_output_decoding.py tests/test_pin_source_env_collisions.py tests/test_dry_run_asks_remotes.py tests/test_doctor_validator_rows.py tests/test_leg_path_yaml_scalars.py tests/test_naming_policy.py tests/test_stdout_encoding.py tests/test_next_refspec_branch_length.py tests/test_gh_guard.py tests/test_push_branch_refspec.py tests/test_validate_pins_named_and_spelled.py tests/test_pin_rewrite_layout.py tests/test_pin_source_top_level.py: 1443 passed, 9 skipped in 525.95s (0:08:45)
    • batch 3, tests/test_non_utf8_git_reads.py tests/test_update_shape.py tests/test_reuse_visibility.py tests/test_pin_source_unreadable.py tests/test_scaffold_manifest_values.py tests/test_scaffold_pin_collisions.py tests/test_scaffold_referent_chain_links.py tests/test_tracking_branch_ref.py tests/test_ruleset_hint_tracking_branch.py tests/test_scaffold_ignored_shape_files.py tests/test_template_values_single_pass.py tests/test_setup_sh.py tests/test_template_text_placeholders.py tests/test_update_shape_kept_helper.py: 1202 passed in 648.37s (0:10:48)
  • tests/test_shape_doctor.py was not run, because neither shape-doctor.py nor a helper it imports changed.

Mutants

Each mutant was applied to scaffold-project.py alone and the named tests were run against it; the file was restored afterwards, and the restore was checked.

Mutant Killed by
M1: the leg push without env=REMOTE_ENV test_every_git_command_that_reaches_a_remote_runs_under_the_rule (at index 5, ('push', None)) and test_a_reuse_re_run_over_empty_legs_does_not_wait_at_its_push, whose screen shows Username for 'http://127.0.0.1:33809': until the harness kills it at 120 s (2 failed in 125.77s)
M2: the root push without it test_every_git_command_that_reaches_a_remote_runs_under_the_rule: "At index 8 diff: ('push', None)"
M3: _take_held_seed's fetch without it the same test: "At index 4 diff: ('fetch', None)"
M4: PROBE_ENV without LC_ALL=C test_the_two_git_commands_that_read_a_leg_ask_no_terminal
M5: the generic fatal: line kept, as on main test_a_leg_git_reaches_over_ssh_is_refused_with_ssh_s_reason, and test_the_reason_git_could_not_read_a_leg[ssh-before-the-generic-line]
M6: the and first guard dropped test_the_reason_git_could_not_read_a_leg[the-generic-line-alone]
M7: the ssh clause dropped from the remedy test_a_leg_git_reaches_over_ssh_is_refused_with_ssh_s_reason, and both cases of test_a_leg_git_cannot_read_is_refused_as_existing_not_as_a_failure

M4 is killed only by the constant's value. This container's git has no translation catalogue, so no test here can run git in a translated locale. The in-process test does show that the probe's two commands get PROBE_ENV.

Merges

git merge-tree --write-tree HEAD <other> with head e40af32:

Fix round 1

RV971's independent review found this PR NOT READY, with one MUST. Lane 3 ruled that the prompt stays off on every push, a first run's included. This round implements that ruling in one commit on e40af32: 155fcd2.

What changed

  • MUST-1: a first scaffold's push that git has no login for names the fix. Both pushes, in _seed_legs and _build_assembly_root, now print through _refused_push. Where git's output holds one of NO_LOGIN (terminal prompts disabled, could not read Username, could not read Password), a Remediation: line comes before RULESET_HINT. It names gh auth setup-git or a token stored for the host in the credential helper, then the ssh clause, then the same command re-run with --reuse-empty-repo added, which reuses the repositories this run left empty. A run that already had the flag is told to re-run its own command. Any other refused push prints exactly what it printed before. No refusal code is new.
  • A decision the review leaves open: the detection reads git's words without LC_ALL=C. I probed git 2.43 with a catalogue that translates each of those words, and git translated only the fatal: before them. So the markers are found in any locale, and a push's output stays in the person's language.
  • SHOULD-1: test_a_first_scaffold_s_push_git_has_no_login_for_names_the_fix runs a first scaffold in a pty, against a local server on 127.0.0.1 that asks for Basic auth. It has two cases: no credential helper, and credential.helper=store over an empty file. Each must exit 2 without waiting, and print the remedy and then the re-run, before the hint. in_a_terminal_asked_for_a_login now takes --reuse-empty-repo as one of its flags, and a helper.
  • SHOULD-2: RV971's two cases are in test_the_reason_git_could_not_read_a_leg.
  • NIT-1: test_a_leg_git_reaches_over_ssh_is_refused_with_ssh_s_reason[translated] asks for the ssh refusal where git speaks a test language, xx. The test copies glibc's C.utf8 into a LOCPATH as xx_XX.UTF-8, and writes the .mo bytes itself (mo_bytes), so CI needs no msgfmt. It is skipped on Windows, where /usr/lib/locale/C.utf8 is missing, and where git, asked once, does not read the catalogue.
  • NIT-2: SSH_KEY_CLAUSE is shared by both remedies: "ssh -T against the host and port your rule names, with anything your core.sshCommand adds, says whether GitHub takes yours". UNREAD_EXIT carries the new words.
  • Two more in-process tests: test_what_a_refused_push_prints, over six outputs, and test_the_root_s_push_git_has_no_login_for_names_the_fix_too, for the root's push.
  • The new helpers sit just before _seed_legs. Placed before _take_held_seed, they conflicted with scaffold-project.py, family.py init and shape_materialize.py: the plan names the tracking branch, and on disk the visibility; an unread root or reused leg is said to be unread, the root before the plan; every holder gets the members' NOTE (#913) #969, which adds a function at that spot.

Superseded above

  • U036 says a first scaffold's push "fails in git's words (terminal prompts disabled) and prints the refused-push block that every push failure prints". A push git has no login for now also prints the remedy and the re-run.
  • U038 quotes the ssh clause with ssh -T git@github.com. It now reads as NIT-2 above.
  • The mutants section says M4 is killed only by the constant's value. The [translated] case now kills it by behaviour.

Out of scope in this round

  • NIT-3 (setup-project.py:1307-1313). Lane 3 files it as a follow-up issue.
  • A token git has and GitHub refuses (fatal: Authentication failed) is none of the three markers, so it keeps today's words.

Tests, fix round 1

On 155fcd2, under a stub gh first on PATH that exits 97, with GH_TOKEN=bogus, an empty GH_CONFIG_DIR and GIT_SSH_COMMAND=false, one pytest process at a time. Each command was /usr/bin/python3 -m pytest -q -p no:cacheprovider --basetemp=<scratch>/bt-f1-<name> -o tmp_path_retention_policy=failed <files>:

Mutants, fix round 1

Each was applied to scaffold-project.py alone, with the named tests run, then the file restored and the restore checked. They ran on this round's code before the helpers moved; the code each mutant changes is the same at 155fcd2.

Mutant Killed by
M1: the leg push without env=REMOTE_ENV test_every_git_command_that_reaches_a_remote_runs_under_the_rule, and the first-run test [no-helper]
M3: _take_held_seed's fetch without it test_every_git_command_that_reaches_a_remote_runs_under_the_rule
M4: PROBE_ENV without LC_ALL=C the ssh test [translated] ([as-it-is] passes), and test_the_two_git_commands_that_read_a_leg_ask_no_terminal
M5: the generic fatal: line kept both ssh test cases, and the reason test [ssh-before-the-generic-line]
M7: the ssh clause dropped from the unread remedy both ssh test cases, and both cases of test_a_leg_git_cannot_read_is_refused_as_existing_not_as_a_failure
M8: the last fatal: line taken the reason test [two-fatal-lines-the-first]
M10: the rule on a re-run's pushes only both cases of the first-run test, each still waiting at Username for 'http://127.0.0.1:...' when the harness killed it at 120 s
M11: the == GENERIC_FATAL guard dropped the reason test [a-line-before-a-fatal-not-the-generic-one]
M12: NO_LOGIN is terminal prompts disabled alone test_what_a_refused_push_prints[no-terminal]
M13: the remedy on every refused push test_what_a_refused_push_prints [a-ruleset] and [ssh-refused-the-key], and test_the_flag_without_the_operator_is_refused_again_and_changes_nothing
M14: the root's push without the remedy test_the_root_s_push_git_has_no_login_for_names_the_fix_too

Merges, fix round 1

git merge-tree --write-tree 155fcd2 <other>:

Fix round 2

RV971's second review found this PR READY, with one SHOULD (SonarCloud S8993) and two NITs. This round fixes the SHOULD and the first NIT in one commit on 155fcd2: 0a6be99.

What changed

Superseded above

  • U038 and round 1's NIT-2 quote the clause as "the host and port your rule names, with anything your core.sshCommand adds". Round 2's words are above.

Tests, fix round 2

On 0a6be99, under a stub gh first on PATH that exits 97, with GH_TOKEN=bogus, an empty GH_CONFIG_DIR and GIT_SSH_COMMAND=false, one pytest process at a time. Each command was /usr/bin/python3 -m pytest -q -p no:cacheprovider --basetemp=<scratch>/bt-f2-<name> -o tmp_path_retention_policy=failed <files>:

  • tests/test_scaffold_reuse_seeded_legs.py: 51 passed in 55.61s. Both rows of the ssh test ran here: [as-it-is] and [translated] passed, none skipped, because this container has /usr/lib/locale/C.utf8 and its git reads the catalogue.
  • The skip rules, probed with a throwaway plugin that points C_UTF8 at a missing path, and then XX_FATAL at a line git never prints, run on the ssh test alone: each gave 1 passed, 1 skipped, 49 deselected, [as-it-is] passed and [translated] skipped, with the reason named.
  • tests/test_agents_md_codes.py tests/test_cli_reference.py tests/test_update_add_codes_documented.py tests/test_repo_hygiene.py: 157 passed in 41.16s
  • tests/test_no_auto_maintenance.py: 60 passed in 8.65s
  • No other test file pins the clause's words, so no other file was run.

Mutants, fix round 2

Each was applied to scaffold-project.py alone, the named tests run, the file restored and the restore checked.

Mutant Killed by
M4: PROBE_ENV without LC_ALL=C test_a_leg_git_reaches_over_ssh_is_refused_with_ssh_s_reason[translated] through the new fixture ([as-it-is] passes), and test_the_two_git_commands_that_read_a_leg_ask_no_terminal: 2 failed, 1 passed
N1: "user, " dropped from the clause both rows of the ssh test, both cases of test_a_leg_git_cannot_read_is_refused_as_existing_not_as_a_failure, both cases of test_a_first_scaffold_s_push_git_has_no_login_for_names_the_fix, and the four NO_LOGIN cases of test_what_a_refused_push_prints: 10 failed
N2: GIT_SSH_COMMAND dropped from the clause the same ten

The test's SSH_KEY is a literal copy of the whole clause, which the three remedies' tests compare byte for byte, so no test needed tightening.

Merges, fix round 2

git merge-tree --write-tree 0a6be99 <other>:

Lane: openreposhape-3 (openRepoShape-3)
🤖 Generated with Claude Code

…fetch run with git's terminal prompt off, as its probe does, and a leg git reaches over ssh is refused with ssh's reason and a remedy for an ssh key (#915)

U036. `PROBE_ENV` (`GIT_TERMINAL_PROMPT=0`) covered `only_commit`'s two
commands alone. A re-run whose legs are empty asks `only_commit` nothing,
and its first push, typed in a terminal with no login for the host,
stopped at `Username for '...':`. `REMOTE_ENV` is now the one prompt rule,
and every git command the scaffold sends to a remote runs under it:
`only_commit`'s two, `_take_held_seed`'s fetch, each leg's push and the
root's.

U037 stays open: whether the rule must never wait for input (ssh with
`BatchMode=yes`, say) is the issue's ruling question. ssh, credential
helpers and askpass are left as they were, and the rule is decided in
`REMOTE_ENV`, once.

U038. Where git's first `fatal:` line is the generic `Could not read from
remote repository.`, the line before it, ssh's, is the reason, so an
operator whose git config sends `https://github.com/` over ssh reads
`Permission denied (publickey).` and not git's generic line. The remedy
names an ssh key beside `gh auth setup-git`. `only_commit` runs git under
`PROBE_ENV`, now the prompt rule and `LC_ALL=C`, so both lines are found
in a locale whose catalogue translates git.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Oct 18, 2026

Copy link
Copy Markdown

Reviewer's Guide

The PR makes all scaffold Git operations that contact remotes fail fast on Git's terminal credential prompt, stabilizes probe parsing with the C locale, and surfaces SSH's actual refusal reason with targeted remediation guidance. It adds focused regression coverage for environment propagation, blocked reuse pushes, SSH rewrites, and diagnostic parsing while leaving the broader SSH/helper/askpass ruling open.

Sequence diagram for remote Git operations with terminal prompts disabled

sequenceDiagram
    participant Scaffold
    participant Git
    participant Remote

    Scaffold->>Git: only_commit(url, scratch)
    Git->>Remote: ls-remote / fetch with PROBE_ENV
    Remote-->>Git: repository result
    Git-->>Scaffold: probe result

    Scaffold->>Git: _take_held_seed fetch with REMOTE_ENV
    Git->>Remote: fetch
    Remote-->>Git: seed or access failure

    Scaffold->>Git: _seed_legs push with REMOTE_ENV
    Git->>Remote: push
    Remote-->>Git: push result

    Scaffold->>Git: _build_assembly_root push with REMOTE_ENV
    Git->>Remote: push
    Remote-->>Git: push result
Loading

Flow diagram for SSH refusal diagnosis and remediation

flowchart TD
    A[only_commit probes remote] --> B{Git can read the leg?}
    B -->|Yes| C[Reuse or continue scaffold]
    B -->|No| D{First fatal line is generic?}
    D -->|Yes| E[Use preceding SSH reason]
    D -->|No| F[Use Git fatal line]
    E --> G[Show refusal and remediation]
    F --> G
    G --> H[Use gh auth setup-git for HTTPS]
    G --> I[For insteadOf SSH rewrite, check ssh -T git@github.com]
Loading

File-Level Changes

Change Details Files
Unified non-interactive terminal-prompt handling for every Git command that reaches a remote, while preserving a separate locale-stable environment for parsed probe output.
  • Introduced REMOTE_ENV with GIT_TERMINAL_PROMPT=0 and derived PROBE_ENV with LC_ALL=C.
  • Applied the remote environment to held-seed fetches and leg/root pushes.
  • Kept the open SSH/helper/askpass ruling unchanged and documented its scope.
scaffold-project.py
Improved leg-access refusal diagnostics for SSH-mediated failures and expanded the remediation guidance.
  • Extracted the line preceding Git's generic fatal message as the underlying SSH reason, while preserving existing handling for Git-specific fatal messages and fallback output.
  • Added guidance for insteadOf HTTPS-to-SSH rewrites and checking GitHub SSH authentication with ssh -T.
  • Added coverage for generic, SSH, locale, and no-output diagnostic cases.
scaffold-project.py
tests/test_scaffold_reuse_seeded_legs.py
Added regression tests for prompt suppression across remote operations and reuse re-runs that previously blocked at credential prompts.
  • Recorded environments for all remote-reaching commands and verified the expected probe-versus-remote environment assignments.
  • Added a terminal test covering pushes from an empty-repository reuse re-run.
  • Added an SSH rewrite test using a refusing stand-in SSH executable to verify the reported reason and remedy.
tests/test_scaffold_reuse_seeded_legs.py

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-3, 2026-10-18T19:23:22Z) RV971, independent review of #971 at e40af32: NOT READY (one MUST).

U036 and U038 hold. Every git call in scaffold-project.py that reaches a remote is under the rule (ls-remote and fetch --depth=1 under PROBE_ENV; the seed fetch and both pushes under REMOTE_ENV), and neither shape_materialize.py nor repo_shape.py runs a remote git command. Reuse re-run rewritten to ssh, with a stand-in ssh: main gives "fatal: Could not read from remote repository."; the head gives "git@github.com: Permission denied (publickey)." and the ssh clause. LC_ALL=C is now proven end to end: with GIT_TEXTDOMAINDIR pointing at a hand-built git.mo and LOCPATH holding C.utf8 copied as xx_XX.UTF-8, main's reason is the advice's last line ("bestehen und das Repository existiert."), the head's is ssh's line, and M4 brings the advice line back.

MUST-1 (scaffold-project.py:1699-1707 and 1819-1827). A first scaffold typed in a terminal, with a credential helper that has nothing stored (credential.helper=store over an empty file; setup-project.py counts any helper as configured, so it says nothing): on main git asked once, all three pushed, exit 0, and the credential was stored. On the head: exit 2, "fatal: could not read Username for 'http://127.0.0.1:...': terminal prompts disabled", then RULESET_HINT. All three repositories are empty and nothing names gh auth setup-git; the hint's one exit, a re-run with --reuse-empty-repo, fails the same way until a credential is stored. Fix: where a push's output shows git could not get a credential, print a credential remedy (gh auth setup-git or a token in your helper, plus the ssh clause) before the hint, then the --reuse-empty-repo re-run. Add a first-run pty test (in_a_terminal_asked_for_a_login hard-codes --reuse-empty-repo at tests/test_scaffold_reuse_seeded_legs.py:751). Or put the first-run change to Brett Heap with U037, which the U036 Direction ties it to.

Ruling on the side effect: prompt off on every push, the first run's too, is the right rule. README.md:231-233 and 404-408, docs/handbook.html:904-911 and setup-project.py:1328-1329 already say the push fails without a helper, and the first and reuse pushes are one call. It is acceptable only with MUST-1.

SHOULD-1: nothing pins the first-run rule. Mutant M10 (the rule on reuse pushes only) survives 85 tests, and my probe of it waits at "Username for".
SHOULD-2: M8 (take the last fatal: line) and M11 (drop the GENERIC_FATAL guard) both survive. Two cases kill them and pass on the head: "fatal: remote error: upload-pack: not our ref SHA" followed by "fatal: the remote end hung up unexpectedly" must give the first line; "warning: redirecting to https://github.com/o/r.git/" followed by "fatal: could not read Username for 'https://github.com': terminal prompts disabled" must give the fatal: line.
NITs: a translated-locale test is possible here (see above); "ssh -T git@github.com" checks the default host and key, not an alias, port 443 or core.sshCommand -i; setup-project.py:1307-1313 says a storage helper answers git's question, which is false while nothing is stored (follow-up); the PR title should name the first-run change.

Tests, under a stub gh, GIT_SSH_COMMAND=false and GIT_CONFIG_GLOBAL=/dev/null: reuse_seeded_legs 39 passed; e2e + pin_and_reuse + dry_run_asks_remotes 153 passed; setup_project_py 230 passed; agents_md_codes + cli_reference + repo_hygiene 151 passed; no_auto_maintenance 60 passed. The head's test file against main's scaffold: 7 failed, 32 passed. Mutants: M1, M3, M5 and M7 are killed by the tests the PR names; M4 only by the constant.
Merges: clean with main d4d11bf, #969 f3802d6, #972 0094731, l3-907 8c5d605 and l3-917 e63ca75, and clean chained (main + #969 + #972 + #971), where the three files tested passed 68. W909 has no branch.

Lane 3's ruling on the first-run change (lane 3 coordinates #915): RV971's reading holds. The prompt stays off on every push, the first run's included: the README, the handbook and setup-project.py already say the first HTTPS push fails without a helper, and the first and reuse pushes are one call. #971 lands only with MUST-1's credential remedy and its first-run pty test, SHOULD-2's two cases, and a title that names the first-run change (NIT-4). If Brett Heap wants the first run to keep asking, that is U037's ruling, which is his. The fix round starts when lane 3 may start agents again (#473 issuecomment-6253954761). NIT-3 (setup-project.py:1307-1313) goes to a follow-up issue.

Lane: openreposhape-3 (openRepoShape-3)

…as a re-run's, is refused naming how to give git one and then the `--reuse-empty-repo` re-run, and the ssh clause asks `ssh -T` of the host the rule names (#915)

Fix round 1 of #971, after RV971's review.

MUST-1. A first scaffold's pushes are the same call as a re-run's, so they
run under `REMOTE_ENV` too, and git's terminal prompt is off for them. With
no credential helper, or with `store` over an empty file, the spec leg's
push was refused with `could not read Username for '...': terminal prompts
disabled`, and only the ruleset hint followed. Its one exit, a re-run with
`--reuse-empty-repo`, is refused the same way until git has a login. Both
pushes now go through `_refused_push`. Where git's output holds one of
`NO_LOGIN` (`terminal prompts disabled`, `could not read Username`, `could
not read Password`), it prints a remedy before the hint: `gh auth
setup-git`, or a token stored in the credential helper, or an ssh key, and
then the re-run with `--reuse-empty-repo`. A run that was the re-run names
its own command again. Any other refused push prints what it always
printed. No refusal code is new. Those three markers are git's own
untranslated words (git 2.43, asked under a catalogue that translates
each), so a push needs no `LC_ALL=C` to be read.

SHOULD-1. A first-run pty test, against a local server that asks for
Basic auth, with no helper and with an empty `store`. With the rule on
re-run pushes only (M10), it waits at `Username for` until the harness
kills it.

SHOULD-2. `_unread_because` is pinned on two more outputs: of two `fatal:`
lines the first is the reason (M8), and a line before a `fatal:` that is not
the generic one is not the reason (M11).

NIT-1. The ssh refusal is also asked where git speaks another language: a
`LOCPATH` copy of glibc's `C.utf8` and a `.mo` file the test writes itself,
so CI needs no `msgfmt`. It is skipped where `C.utf8` is missing, and where
git does not read the catalogue. It kills M4 by behaviour.

NIT-2. `SSH_KEY_CLAUSE`, shared by both remedies, asks `ssh -T` against
the host and port the rule names, with what `core.sshCommand` adds. The old
`ssh -T git@github.com` checked only the default host, port and key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@brettheap brettheap changed the title scaffold-project.py: a --reuse-empty-repo re-run's pushes and seed fetch run with git's terminal prompt off, as its probe does, and a leg git reaches over ssh is refused with ssh's reason and a remedy for an ssh key (#915) scaffold-project.py: a first scaffold's pushes and a --reuse-empty-repo re-run's pushes and seed fetch no longer ask at the terminal: a push git has no login for fails naming the fix, and a leg git reaches over ssh is refused with ssh's reason (#915) Oct 19, 2026
@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-10-19T12:06:36Z) RV971 round 2, independent review of #971 at 155fcd2: READY. No MUST is left; one new SHOULD (SonarCloud S8993). The tests check was still running at 12:05Z.

Round-1 items:

  • MUST-1 fixed. First scaffold in a pty against a local Basic-auth git server on 127.0.0.1. With no helper, and with credential.helper=store over an empty file, each run exits 2 without reaching git's prompt. It prints the refused push, then the remedy (gh auth setup-git or a stored token, then the ssh clause, then the re-run with --reuse-empty-repo added), then the hint. All three repositories are left empty, and the store stays empty. A run that already had the flag is told to re-run the same command with a new --work-dir or none. Following the remedy works: with a login stored, the --reuse-empty-repo re-run exits 0 with one commit in each repository. A ruleset refusal prints the same 25 lines on main cf66102 and on the head. git 2.43, under a catalogue that translates "could not read %s%s", "terminal prompts disabled", "Username", "Password" and "%s for '%s': ", translated only "fatal: ".
  • SHOULD-1 fixed: M10 is killed by both cases of the first-run test.
  • SHOULD-2 fixed: M8 is killed by [two-fatal-lines-the-first], M11 by [a-line-before-a-fatal-not-the-generic-one].
  • NIT-1 fixed: M4 is killed by [translated] and by the constant test. The test is skipped, not failed, when C.utf8 is missing or git reads no catalogue (both checked).
  • NIT-2 fixed: git runs "github-work", "git@github-work", "-p 443 git@ssh.github.com", with core.sshCommand's -i placed first, so the new clause is true for all three.
  • NIT-3: follow-up issue. NIT-4 fixed: the title names the first-run change (252 characters, GitHub's limit is 256).

New:

  • SHOULD: SonarCloud S8993 (MAJOR) at tests/test_scaffold_reuse_seeded_legs.py:1014, request.getfixturevalue("git_speaks_xx"). Parametrize a fixture that returns {} for as-it-is and the xx environment for translated, or rule it a false positive for this PR.
  • NIT: scaffold-project.py:668-672 should say "the user, host and port your rule names". GitHub takes ssh only as git, so "ssh -T -p 443 ssh.github.com" without git@ is refused whatever the key. GIT_SSH_COMMAND, which overrides core.sshCommand, is unnamed.
  • NIT: scaffold-project.py:1680-1706. git prints the NO_LOGIN words only over https, so _login_remedy's ssh clause never describes the refused push, and a push over ssh refused for its key still prints only the hint. Fits the NIT-3 follow-up.

Tests, under a stub gh, one at a time: reuse_seeded_legs 51 passed; e2e + pin_and_reuse + dry_run_asks_remotes 153 passed; ruleset_hint_tracking_branch + git_output_decoding + adopt_late_failure 178 passed; agents_md_codes + cli_reference + update_add_codes_documented + repo_hygiene 157 passed; no_auto_maintenance 60 passed. Chained merge of main ae63d45 + #972 + #981 + #971, five files including chain_pin_sources and plan_visibility_notes: 153 passed.
Merges (git merge-tree --write-tree with 155fcd2): clean with origin/main ae63d45 (#969 landed as c4dd19d), #972 4e44df4 and #981 499f7ba, and clean chained.
Checks at 155fcd2: SonarCloud pass (gate passed, 1 new issue), package pass, tests-git-bash pass, tests still in progress (run 39246667225, started 11:43Z).

Lane: openreposhape-3 (openRepoShape-3)

…nd port the rule names, with anything `GIT_SSH_COMMAND` or `core.sshCommand` adds, and the translated-git test takes its environment as an argument (#915)

Fix round 2 of #971, after RV971's second review.

SHOULD, SonarCloud S8993. test_a_leg_git_reaches_over_ssh_is_refused_with_ssh_s_reason
asked for `git_speaks_xx` at run time with `request.getfixturevalue`. The test
now takes `git_language`, a fixture parametrized over the same two ids,
`as-it-is` and `translated`, which returns `{}` for the first and
`git_speaks_xx`'s environment for the second. `git_speaks_xx` is a plain
function now, with its skip rules unchanged: a missing `C.utf8`, or a git
that reads no catalogue, skips the `translated` row only and never fails it;
the `as-it-is` row runs where it is skipped. The test ids are the same.

NIT. `SSH_KEY_CLAUSE` said `ssh -T` against "the host and port your rule
names". GitHub takes ssh only as the user `git`, so `ssh -T -p 443
ssh.github.com` without `git@` is refused whatever the key: the clause now
says "the user, host and port your rule names". `GIT_SSH_COMMAND` overrides
`core.sshCommand` and only the second was named: it says "with anything your
`GIT_SSH_COMMAND` or `core.sshCommand` adds". The comment above it and the
test's copy of the clause (`SSH_KEY`) say the same.

Not in this round: a push over ssh refused for its key still prints only the
ruleset hint (RV971's second NIT), which goes to #993.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-10-19T14:14:17Z) RV971 round 3, the delta 155fcd2..0a6be99 only: READY.

Lane 3's note: #971 goes to lane 3's landing pilot now. It lands when tests passes on 0a6be995 and lane 3's gate passes on the predicted squash over main, in turn behind any HOLD another lane has open.

Lane: openreposhape-3 (openRepoShape-3)

@sourcery-ai

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

@brettheap
brettheap merged commit 8346857 into main Oct 19, 2026
5 checks passed
@brettheap
brettheap deleted the l3-915-scaffold-git-prompts branch October 19, 2026 16:09
brettheap added a commit that referenced this pull request Oct 19, 2026
…tion failed`) or its ssh key (`Permission denied (publickey)`) names how to replace it and the `--reuse-empty-repo` re-run before the ruleset hint, in any locale (#993) (#1012)

A first scaffold's push that the host refuses because git sent a stored
login it rejects (git says `fatal: Authentication failed for '<url>'`)
now names how to replace that login: `gh auth setup-git`, a new token in
the credential helper, or the ssh clause. Then it names the re-run with
`--reuse-empty-repo`, and only then the ruleset hint. Where an
`insteadOf` or `pushInsteadOf` rule sends the push over ssh and the host
refuses the key (`Permission denied (publickey)`), the push names the
`ssh -T` check instead, and never offers `gh auth setup-git`, which does
nothing over ssh. Any other refusal, a 403 included, prints what it
printed before, and #971's no-login words are unchanged.

Both pushes now run with `LC_ALL=C` (`PUSH_ENV`), because git translates
its refused-login line; git's own lines on a refused push therefore
print in English. git erases a login the host refused from the `store`
helper, so a re-run before the login is replaced gets the no-login
words, with the same exit.

Closes #993

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant