Skip to content

scaffold-project.py: a re-run whose read of a leg ssh refused for the host key is told that key's fix, checked against GitHub's published fingerprints, in place of "Let git read it" (#1137) - #1156

Merged
brettheap merged 8 commits into
mainfrom
l3-1137-probe-host-key
Oct 23, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

What changed

A follow-up outside v1.2.0 (Brett Heap, 2026-10-21T10:32:04Z). It lands only after the v1.2.0 tag, on a later merge word.

Stacked on #1130: its base is #1130's branch. Once #1130 lands, lane 3 brings main into this branch and moves its base to main.

A re-run's read that ssh refused for the host key is told that key's fix, not "Let git read it"

A --reuse-empty-repo re-run over a leg with commits asks git (git ls-remote) whether the leg's one commit is the seed. Where a url.<base>.insteadOf rule sends that read over ssh and ssh refuses GitHub's host key, the run is still refused as scaffold-remote-exists, naming the same reason line. In place of the login fix ("Let git read it", and an ssh -T that says whether GitHub takes your key), it now gives #1130's host-key fix, in a read's words, and then the same re-run sentence:

  • An unknown key (BatchMode=yes, StrictHostKeyChecking=yes, or answered no): keep it first, once you know it is GitHub's, with HOST_KEY_CHECK (ssh -T -o BatchMode=no -o StrictHostKeyChecking=ask, both options first), and answer yes only to a fingerprint GitHub publishes (GITHUB_FINGERPRINTS).
  • A changed key: accept no key from that host, and push nothing to it, until you have checked that the fingerprint ssh prints when you run git ls-remote of that URL again is one GitHub publishes. Only then remove the old key (the ssh-keygen -R line ssh prints with it) and keep the new one (HOST_KEY_CHECK), answering yes only to a fingerprint GitHub publishes, which can be another of its keys than the one you checked.
  • Any other refused read (a refused ssh key, a closed connection, an https login): the login fix, byte for byte as before.

Security, as on #1130. A changed key is never kept before its fingerprint is checked against GitHub's published list. Each ssh -T is answered yes only to a published fingerprint.

Decisions the Direction left open

  • git's whole output is handed on, not its one line. The Direction says "where unread carries ssh's host-key words". Measured at scaffold-project.py: a push ssh refused the host's key for names how to check that key against GitHub's fingerprints, not the ruleset hint, and the hint on GitHub says what its re-run can still wait at (#1108) #1130's head, unread (_unread_because, one line) is Host key verification failed. for an unknown key and a changed key alike: only the lines before it tell them apart. So _held_seed now returns git's refused read itself (the CommandFailed), and _exists_remedy names its one line as before and reads the whole output for the fix. Mutant h below proves the one line is not enough.
  • One shared reader. The new _host_key_refused(output) returns "changed", "unknown" or "", skipping remote: lines with _without_remote_lines and cutting all from a server's fatal: remote error: on (the fix round). _host_key_remedy (the push's) now uses it too, and behaves as before: its tests are unchanged and pass.
  • A read's words (_read_host_key_fix):
    • "the host git reached for that read" in place of the push's repository (the refused command can be only_commit's git fetch, RV1156's N1);
    • an insteadOf rule only, as no pushInsteadOf rule touches a read;
    • nothing on what was pushed;
    • the changed key's fingerprint is the one "ssh prints when you run git ls-remote of that URL again", not "printed above". The refusal prints only git's one line. Measured: that git ls-remote, run again in a terminal without BatchMode, printed ssh's banner, the SHA256: fingerprint and the ssh-keygen -R line, then exited 128 without asking, and known_hosts was unchanged.
  • No new refusal code and no docs change. scaffold-remote-exists is kept, and the docs do not quote this remedy.

Fix round: RV1156

RV1156's review, posted on this PR, found one SHOULD (S1) and two NITs (N1, N2). This round changes the code and its tests for S1 and N1, and the body for N2. Nothing else in the diff moved.

  • S1, text a server sends could still steer a read into the host-key fix. Measured with git 2.43, over a fake ssh that stands for a server already reached and answers git ls-remote with an ERR packet (run again here at 49bfb99): git dies on fatal: remote error: <message>, keeps the message's newlines and puts remote: on none of its lines, so _without_remote_lines kept all of it. _host_key_refused gave changed for a message holding the changed-key banner and Host key verification failed., and unknown for the other two messages, so the read was given a host-key fix. Now said = _without_remote_lines(output).split("fatal: remote error:", 1)[0]: all from that line on is the server's. The same three messages give "" and the login fix. Where ssh itself refused the key, no server answered, so the cut hides none of ssh's lines.
    • Two rows are added to test_what_a_leg_git_could_not_read_is_told, each expecting the login fix. They FAIL without the cut (2 failed, 7 passed) and PASS with it (9 passed).
    • The comments on HOST_KEY_FAILED and _host_key_refused claim as measured only what was measured, the error packet. Stderr the far side writes through ssh's channel would reach git unprefixed too and cannot be told from ssh's own lines. That is NOT measured: it follows from how OpenSSH relays a remote command's stderr. Only a host ssh has already verified can write it, and it leads only to a fix that keeps no key before its fingerprint is checked against GitHub's.
  • N1, the refused command can be only_commit's git fetch. The read's fix now says "the host git reached for that read" and "when you run git ls-remote of that URL again". The two literals in the reuse test follow. No other file pins them (git grep).
  • N2. The Sourcery summary that followed the robot line is removed.

At the fix round's head f914e38, each file alone, under a stub gh: tests/test_scaffold_reuse_seeded_legs.py 134 passed, tests/test_ruleset_hint_tracking_branch.py 23 passed, tests/test_cli_reference.py 23 passed, tests/test_readme_sections.py 76 passed, tests/test_repo_hygiene.py 100 passed, tests/test_agents_md_codes.py 47 passed.

Mutants, each against the whole reuse file and cmp-equal after:

mutant result
h: the fix reads git's one line (_unread_because) KILLED, 2 failed
swap: the changed-key and unknown-key fixes swapped KILLED, 7 failed
norerun: the re-run sentence dropped where the host-key fix is given KILLED, 7 failed
cut: the split("fatal: remote error:", 1)[0] dropped KILLED, 2 failed (the two new rows)

git merge-tree --write-tree origin/main f914e38, with main at 038dafb, is clean (tree 47a44f9). Checked out as a detached commit in my clone, tests/test_path_guard_hygiene.py gave 49 passed and the reuse file 134 passed. The fix round adds no is_file, is_dir, exists, is_symlink or os.path guard.

Measurements

Each case ran in a pseudo-terminal, with real OpenSSH 9.6p1 behind a copy of W915's fake sshd (Host * is ProxyCommand /bin/false), under a stub gh, GH_TOKEN=bogus and an empty GH_CONFIG_DIR. Each run is a --reuse-empty-repo re-run over a seeded leg that git reads through an insteadOf rule.

case #1130's head dee55dc this PR 49bfb99
p1: unknown key, BatchMode=yes exit 2 in 0.7 s, login fix exit 2 in 0.5 s, the unknown-key fix
p2: changed key, BatchMode=yes exit 2 in 0.5 s, login fix exit 2 in 1.2 s, the changed-key fix
p3: unknown key, answered no exit 2, login fix exit 2, the unknown-key fix
p4: Permission denied (publickey). exit 2, login fix exit 2, login fix, unchanged
p5: ssh connection closed exit 2, login fix exit 2, login fix, unchanged
p6: https, no login exit 2, login fix exit 2, login fix, unchanged

In every case, gh repo create was called 0 times.

Tests

Each file ran alone at 49bfb99, under a stub gh first on PATH, with GH_TOKEN=bogus, an empty GH_CONFIG_DIR, GIT_SSH_COMMAND=false and GIT_CONFIG_GLOBAL=/dev/null:

file result
tests/test_scaffold_reuse_seeded_legs.py 132 passed (121 at the parent)
tests/test_ruleset_hint_tracking_branch.py 23 passed
tests/test_cli_reference.py 23 passed
tests/test_readme_sections.py 76 passed
tests/test_repo_hygiene.py 100 passed
tests/test_agents_md_codes.py 47 passed
tests/test_update_add_codes_documented.py 6 passed
tests/test_no_auto_maintenance.py 58 passed, 2 skipped ("the session began with GIT_CONFIG_GLOBAL set")
tests/test_dry_run_asks_remotes.py 32 passed
tests/test_reuse_visibility.py 2 passed

The new tests:

  • test_a_leg_whose_read_ssh_refused_the_host_key_for_names_the_key: end to end with a fake ssh, for an unknown key, one checked strictly, and a changed key.
  • test_what_a_leg_git_could_not_read_is_told: in-process, 9 rows: three host-key cases, one among remote: lines, remote: lines alone, a refused ssh key, no username, and two of a server's error packet (the fix round).
  • A new row in test_each_re_run_remedy_says_the_prompts_it_can_still_wait_at. The test now passes a CommandFailed.

No other file pins _exists_remedy's text (git grep).

Path-guard hygiene. git merge-tree --write-tree origin/main 49bfb99, with main at 038dafb, is clean (tree 2273e68). Checked out as a detached commit in my clone, tests/test_path_guard_hygiene.py gave 49 passed. The diff adds no is_file, is_dir, exists, is_symlink or os.path guard.

Mutants

Each ran against the whole reuse file. The file was restored and cmp-equal afterwards.

mutant result
a: _exists_remedy never calls _read_host_key_fix (always the login fix) KILLED, 7 failed
b: a changed key gets the unknown-key fix KILLED, 2 failed
c: HOST_KEY_CHECK dropped from the unknown-key fix KILLED, 5 failed
d: "answering yes only to a fingerprint GitHub publishes, which can be another of its keys ..." dropped KILLED, 2 failed
e: "accept no key from that host, and push nothing to it, until you have checked" dropped KILLED, 2 failed
f: _host_key_refused reads remote: lines too KILLED, 6 failed (4 push rows, 2 read rows)
g: the login fallback dropped KILLED, 7 failed
h: the fix reads git's one line (_unread_because) in place of its whole output KILLED, 2 failed (the changed-key rows)

Left out of scope

Closes #1137

Lane: openreposhape-3 (openRepoShape-3)

🤖 Generated with Claude Code

Summary by Sourcery

Give reuse re-runs safe, host-key-specific remediation when GitHub reads are refused over SSH while retaining the existing login guidance for other access failures.

New Features:

  • Provide targeted remediation for re-run reads that fail because GitHub’s SSH host key is unknown or has changed, including fingerprint verification guidance.
  • Reuse host-key refusal detection for both remote reads and pushes while preserving the existing login fallback for other failures.

Bug Fixes:

  • Prevent server-provided error text from being misclassified as an SSH host-key refusal during remote reads.
  • Preserve the complete failed command output when determining the appropriate re-run remedy.

Enhancements:

  • Keep the existing scaffold-remote-exists error and re-run guidance while distinguishing unknown, changed, and non-host-key SSH failures.

Tests:

  • Add end-to-end and in-process coverage for unknown, strictly rejected, and changed host keys during re-run reads, plus server error and fallback cases.
  • Extend remedy tests to validate prompt guidance and mutation-resistant host-key classification.

Brett Heap and others added 6 commits October 21, 2026 15:09
…to check that key against GitHub's fingerprints and keep it, then the re-run, instead of the ruleset hint, and a ruleset's refusal on GitHub says what the hint's re-run can still wait at (#1108)

Follow-up to #915 (U037), from RV1101's S1 on #1101.

A host key ssh refused. Where a `url.<base>.insteadOf` or
`url.<base>.pushInsteadOf` rule sends a push over ssh and ssh cannot
verify the host's key, ssh says `Host key verification failed.` and the
push printed `RULESET_HINT` alone, whose re-run fails the same way.
Measured on main at 823624d with the real OpenSSH 9.6 against a fake
server: an unknown key under `BatchMode=yes` (which #1101 advises),
answered no, and on a `--reuse-empty-repo` re-run, and a changed key with
and without `BatchMode=yes`; each exits 2 in under 2 s with the hint
alone. `_host_key_remedy` now names the cause, how to see the key's
fingerprint (`ssh -T` without `BatchMode=yes`), that it must be one of the
fingerprints GitHub publishes (`gh api meta --jq .ssh_key_fingerprints`),
and then the re-run, in place of the hint: ssh refused before git sent
anything, so no ruleset was asked. A key that CHANGED
(`REMOTE HOST IDENTIFICATION HAS CHANGED`) is never told to be accepted
as it comes: nothing is accepted, removed or pushed until its fingerprint
is checked. The re-run is the one the login remedies name, now shared as
`_then_re_run`: the push fails before git sends anything, so the
repositories are left empty or holding their seed, and
`--reuse-empty-repo` reuses them.

The ruleset hint on GitHub. `RULESET_HINT`'s one exit is the
`--reuse-empty-repo` re-run, and it did not say what every other remedy
into that re-run says (`PROMPTS_STILL_ASK`). The hint is printed under
`--local-remote-dir` too, where no prompt can come, and its text is pinned
byte for byte beside `family.py`'s and the adoption's, so the words stay
in the hint and `_refused_push` appends `RULESET_HINT_PROMPTS` after it on
GitHub alone, and not where a login remedy before it has said the same.
`_refused_push` takes `local` from both call sites. The two comments that
named #1108 now say where it is said.

No refusal code: a refused push is printed as `REFUSED pushing ...`, with
no code, as the login remedies of #915 and #993 are.

Tests: `test_what_a_refused_push_prints` gains the host-key rows and runs
every row on GitHub and on disk; two end-to-end tests run the scaffold on
GitHub against a fake ssh saying OpenSSH's measured words and against a
ruleset hook; `test_ruleset_hint_tracking_branch.py` pins that the
scaffold's hint is the last thing it prints under `--local-remote-dir`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… for names `ssh -T -o StrictHostKeyChecking=ask` first, so ssh shows the fingerprint and asks whatever the person's ssh_config says, and a changed key is kept only on a yes to the fingerprint checked; GitHub's fingerprints come from `gh api --hostname github.com meta`; and `remote:` lines the server sent no longer switch the remedy (#1108)

RV1130's review of #1130:
- M1: `HOST_KEY_CHECK` names `ssh -T -o StrictHostKeyChecking=ask`, that
  option first. ssh keeps the first value it is given for an option, so it
  wins over an ssh_config's `accept-new`, which kept the key without showing
  or asking (measured, known_hosts 0 -> 98 bytes), over its `yes`, which
  refused without asking, and over a later `-o`. The changed-key remedy ends
  "answering yes only to the fingerprint you checked", as the unknown-key
  remedy does.
- N1: `GITHUB_FINGERPRINTS` is `gh api --hostname github.com meta --jq
  .ssh_key_fingerprints` and says ssh shows each with `SHA256:` before it.
- N2: the host-key match skips lines that start with `remote:`
  (`_without_remote_lines`).
- N3, N4, N5: the comments say what `LogLevel=QUIET`, `LogLevel=FATAL` and a
  revoked key give, `_then_re_run` says "before any ref is updated", and the
  two comments over `REMOTE_ENV` and `PROMPTS_STILL_ASK` say
  `_refused_push` appends `RULESET_HINT_PROMPTS`.

Tests: `SEE_THE_KEY`, `PUBLISHED` and `CHANGED_HOST_KEY_REMEDY` follow the
words; `test_what_a_refused_push_prints` gains a ruleset whose `remote:`
lines carry ssh's words, a `\r` among them, and an unknown key beside
`remote:` lines that say it changed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…atchMode=no -o StrictHostKeyChecking=ask` first, so ssh shows the fingerprint and asks over an ssh_config's `BatchMode yes` and a later `-o`, and the filter's comment says git prefixes each part of a server's line after a carriage return with `remote:` (#1108)

RV1130's round 2:
- N1: `HOST_KEY_CHECK` names `ssh -T -o BatchMode=no -o
  StrictHostKeyChecking=ask`, both options first, and drops "but
  `BatchMode=yes`". Measured in a terminal against the fake sshd: with
  `BatchMode yes` and `StrictHostKeyChecking accept-new` or `yes` in
  ssh_config, and again with a later `-o BatchMode=yes` and `-o
  StrictHostKeyChecking=accept-new`, ssh showed the fingerprint and asked;
  answering no left known_hosts unchanged. `-o StrictHostKeyChecking=ask`
  alone refused without asking under `BatchMode yes`.
- S1: the `_without_remote_lines` docstring and the test comment say git
  prefixes each line the server sent, and each part after a carriage
  return in it, with `remote:`; the test row is git's real shape ("remote:
  see the log        \rremote: Host key verification failed.        \n",
  measured with git 2.43 under LC_ALL=C).

Tests: `SEE_THE_KEY` follows the words.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…to a fingerprint GitHub publishes, which can be another of its keys than the one you checked", since ssh asks first about a key type it holds, and a form feed in a server's `remote:` line keeps it one line for the filter (#1108)

RV1130's round 3:
- N1: after a changed RSA or ECDSA key is checked and `ssh-keygen -R`
  removes it, `ssh -T` asks about GitHub's ED25519 key, not the key that was
  checked (ssh asks first about the key types it already holds, RFC 4253
  7.1), so the remedy no longer says "the fingerprint you checked"; the
  docstring and the pin say the same.
- S1: the test row `"remote: see the log\x0cHost key verification
  failed.        \n"` is git's real shape for a form feed (git 2.43, `cat
  -A`: one `remote:` prefix over the separator), and kills the filter's
  mutant that splits at every separator `str.splitlines` knows.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… ssh asked first for the key type it held, and once that key is removed the `ssh -T` asks first for ssh's default, ED25519 (#1108)

RV1130c's NIT: the docstring said the `ssh -T` "asks first about a key type
ssh holds", which is the push's ssh. After `ssh-keygen -R` ssh holds no key
for the host, so the `ssh -T` asks first for its default, ED25519. A
docstring alone: no printed word and no test changes.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…g ssh refused for the host key is told that key's fix, unknown or changed, checked against GitHub's published fingerprints, in place of "Let git read it", and keeps `scaffold-remote-exists` and its re-run sentence (#1137)

`_held_seed` hands on git's refused read whole, because its one line
(`_unread_because`) is `Host key verification failed.` for an unknown key
and a changed one alike. `_exists_remedy` names that line as before, then
gives `_read_host_key_fix`, #1108's two host-key fixes in a read's words,
or else the login fix, then the same re-run. `_host_key_refused` is shared
with `_host_key_remedy`, which behaves as before.

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

sourcery-ai Bot commented Oct 22, 2026

Copy link
Copy Markdown

Reviewer's Guide

The PR improves --reuse-empty-repo diagnostics when a seeded leg cannot be read: it carries the complete git ls-remote failure, classifies SSH host-key refusals using shared logic, and replaces misleading login instructions with fingerprint-verified unknown or changed-key remediation while preserving existing fallbacks and refusal behavior.

Sequence diagram for host-key-aware seeded-leg re-run diagnostics

sequenceDiagram
    participant R as Re-run
    participant S as _held_seed
    participant G as git ls-remote
    participant E as _exists_remedy
    participant H as _host_key_refused
    participant F as _read_host_key_fix

    R->>S: only_commit(url, scratch)
    S->>G: read seeded leg
    G-->>S: CommandFailed(output)
    S-->>E: failed output and command
    E->>H: classify complete output
    alt changed host key
        H-->>E: changed
        E->>F: _read_host_key_fix(output)
        F-->>E: verify fingerprint, remove old key, keep checked key
    else unknown host key
        H-->>E: unknown
        E->>F: _read_host_key_fix(output)
        F-->>E: HOST_KEY_CHECK and published fingerprints
    else other refused read
        H-->>E: no host-key refusal
        E-->>R: existing login remediation
    end
    E-->>R: scaffold-remote-exists and re-run instructions
Loading

Flow diagram for classifying a refused git read

flowchart TD
    A[git ls-remote fails] --> B["_host_key_refused(output)"]
    B -->|changed| C[Check fingerprint from a repeated git ls-remote]
    C --> D[Only then remove old key and keep new key with HOST_KEY_CHECK]
    B -->|unknown| E[Run HOST_KEY_CHECK and accept only a GitHub-published fingerprint]
    B -->|no host-key refusal| F[Use existing git login remediation]
    D --> G[Keep scaffold-remote-exists and re-run guidance]
    E --> G
    F --> G
Loading

File-Level Changes

Change Details Files
Preserve the full failed git ls-remote result so re-run remediation can distinguish SSH host-key failures from other access failures.
  • Return CommandFailed from _held_seed instead of only its summarized reason line.
  • Pass the failure through _exists_remedy, while retaining the existing one-line refusal text in user-facing output.
  • Update remediation tests to construct and validate full command failures, including non-host-key fallbacks.
scaffold-project.py
tests/test_scaffold_reuse_seeded_legs.py
Add host-key-specific remediation for SSH failures during seeded-leg reads, with GitHub fingerprint verification safeguards.
  • Share host-key classification between push and read paths, ignoring remote: lines and identifying unknown versus changed keys.
  • Provide read-specific instructions for retaining an unknown key or safely replacing a changed key after checking GitHub-published fingerprints.
  • Fall back byte-for-byte to the existing login guidance for refused SSH authentication, closed connections, HTTPS failures, and other non-host-key errors.
scaffold-project.py
tests/test_scaffold_reuse_seeded_legs.py
Expand end-to-end and unit coverage for the new diagnostics without changing refusal codes or repository-creation behavior.
  • Test unknown, strictly refused, and changed host keys through an insteadOf-routed git ls-remote re-run.
  • Verify host-key messages are not triggered by server-supplied remote: text and that existing login remediation remains intact.
  • Assert scaffold-remote-exists behavior, re-run prompt guidance, and zero gh repo create calls.
tests/test_scaffold_reuse_seeded_legs.py

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-3, 2026-10-22T15:38:56Z) RV1156, independent review of #1156 at 49bfb99: NOT READY for one SHOULD only. No MUST.

SHOULD S1 - text the server sends can still steer the read into the host-key fix. scaffold-project.py:2367 (_host_key_refused, used for the read at :893). Measured with real git 2.43: a fake "ssh" standing for a server already reached answers git ls-remote with an ERR packet; git dies with "fatal: remote error: <msg>" and keeps the newlines in it, so no line starts with "remote:". With msg "see the log\n@ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @\nHost key verification failed." the head gives the changed-key fix; with "see the log\nHost key verification failed." or the one line "Host key verification failed." it gives the unknown-key fix. The parent gave "Let git read it" for all three. The outcome is only the safer remedy, and only a host ssh or TLS already verified can send it, but it fails the rule that server text must not steer the branch. Fix, proven in my clone: said = _without_remote_lines(output).split("fatal: remote error:", 1)[0] (git dies right after that line, so all from it on is the server's; where ssh refused the key no server was reached). Add two rows to test_what_a_leg_git_could_not_read_is_told (tests/test_scaffold_reuse_seeded_legs.py:1894-1905), each expecting the login fix: "fatal: remote error: see the log\n@ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @\nHost key verification failed." and "fatal: remote error: Host key verification failed.". The head fails both (2 failed); with the fix the whole reuse file passes (134). Reword the comments at :2365-2366 and :2177-2180: stderr the far side writes through ssh's channel arrives with no prefix and cannot be told apart (not measured), but only a host ssh already verified can write it, and it leads only to the safe remedy.

NITs:

  • scaffold-project.py:2382/:2394/:2402/:2409 always say git ls-remote, but the refused command can be only_commit's git fetch -q --depth=1; then "that git ls-remote" points to nothing. Suggest "the host git reached for that read" and "when you run git ls-remote of that URL again".
  • Body: Sourcery appended "Summary by Sourcery" after the robot line (lines 94-109); ASCII, no closing keyword; strip it before landing.

Verified:

Lane 3's plan: S1 goes into a fix round in the next usage window, as RV1156 proved it (the fatal: remote error: cut, the two test rows and the reworded comments), with N1's wording. The Sourcery block comes off before landing. #1156 lands only after #1130, after the v1.2.0 tag.

Lane: openreposhape-3 (openRepoShape-3)

… steers a re-run's read into the host-key fix, and the read's fix names "the host git reached for that read", so it holds for `only_commit`'s `git fetch` as for its `git ls-remote` (#1137)

RV1156's S1. git dies on a server's error packet with `fatal: remote error:
<message>`, keeps the message's newlines and prefixes none of its lines
(measured, git 2.43, over a fake ssh that sends the packet), so
`_without_remote_lines` kept all of it, and a message that said `Host key
verification failed.` gave a read the unknown-key or changed-key fix.
`_host_key_refused` now cuts from `fatal: remote error:` on: the server's
words, and where ssh itself refused the key no server answered. Two rows of
`test_what_a_leg_git_could_not_read_is_told` expect the login fix for such a
message, and fail without the cut. The comments on `HOST_KEY_FAILED` and
`_host_key_refused` no longer say `remote:` alone separates the server's
lines: stderr the far side writes through ssh's channel arrives unprefixed
too (not measured; from how OpenSSH relays a remote command's stderr), but
only a host ssh already verified can write it, and it leads only to a fix
that keeps no key before its fingerprint is checked against GitHub's.

RV1156's N1. The refused command can be `only_commit`'s `git fetch`, so the
read's fix says "the host git reached for that read" and "when you run `git
ls-remote` of that URL again", words that hold for either command. The two
literals in the reuse test follow.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-10-22T17:41:42Z) RV1156 delta review of #1156 at f914e38: READY once CI's tests job passes on f914e38 (in progress at 17:40Z). No MUST, no SHOULD; S1, N1 and N2 are fixed.

NIT N3 (optional) - scaffold-project.py:2177-2179 says "git prefixes each line the server sent with remote: (measured, git 2.43)", and the next sentence names error-packet lines the server also sent, unprefixed. Suggested: "each line the server sends on git's side band (a hook's, a ruleset's)".

Note: f914e38's trailer says Claude Sonnet 5.5 and 49bfb99's says Opus 5.5; set the one you want in the explicit squash message.

Verified:

  • S1: the cut in _host_key_refused (:2387) is the change proven in round 1. My far-side probe re-run at f914e38 (real git 2.43, a fake ssh sending an ERR packet) gives "" and the login fix for all three messages (lines after the error line saying the key changed, lines saying it failed, and the one line "Host key verification failed."); 49bfb99 gave a host-key fix for each.
  • The two new rows in test_what_a_leg_git_could_not_read_is_told are the ones proven in round 1: they fail without the cut (2 failed) and pass with it.
  • The comments at HOST_KEY_FAILED and in _host_key_refused mark as measured only the error packet (git 2.43), mark stderr relayed through ssh's channel NOT measured, and are true apart from N3.
  • N1: both fixes say "the host git reached for that read" and "run git ls-remote of that URL again", and both pinned literals follow. A refused git fetch now reads correctly.
  • Remedy diff 49bfb99 -> f914e38, in-process, all 184 cases as expected: the login remedy byte-identical in all 136 non-host-key cases (ls-remote and fetch, local and GitHub, spec and code); the host-key fixes changed only by N1's three phrases; the error-packet cases moved from a host-key fix to the login fix. The push's _host_key_remedy is byte-identical in 42 of 42.
  • Tests at f914e38, one at a time under a stub gh: test_scaffold_reuse_seeded_legs.py 134 passed, test_repo_hygiene.py 100 passed. pyflakes clean; 0 bare path guards added; merge-tree with main 038dafb clean (tree 47a44f94).
  • Body: ends Closes scaffold-project.py: a re-run whose probe ssh refused for GitHub's host key is told "Let git read it" and sent to ssh -T, with no word on checking that key against GitHub's published fingerprints #1137 / Lane / robot with blank lines; the only closing keyword is Closes scaffold-project.py: a re-run whose probe ssh refused for GitHub's host key is told "Let git read it" and sent to ssh -T, with no word on checking that key against GitHub's published fingerprints #1137; the only non-ASCII is the robot; no Sourcery block; draft, no milestone; closingIssuesReferences stays empty until the base moves to main.
  • CI run 39824110867 at 17:40Z: package and tests-git-bash passed, the SonarCloud check passed, tests in progress (started 17:35:58Z).

Lane 3's plan: #1156 is READY once tests passes on f914e38. N3's comment wording goes into the commit that brings main into this branch once #1130 lands, so it costs no extra CI run. The explicit squash message will name both models that wrote the commits. #1156 lands only after #1130, after the v1.2.0 tag, on a later word.

Lane: openreposhape-3 (openRepoShape-3)

…arent's own files

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

Copy link
Copy Markdown

@brettheap
brettheap marked this pull request as ready for review October 23, 2026 19:48
@sourcery-ai

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

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