diff --git a/README.md b/README.md
index 824842fb..45cc23fb 100644
--- a/README.md
+++ b/README.md
@@ -505,6 +505,13 @@ manifest, sets the topic `xf-project-atlas` on all three, clones the root as
> refused is finished by an operator holding the bypass right re-running it.
> A leg with other commits is refused, and a root that HAS commits is a live
> repository, which `adopt-project.py` converts in place instead.
+> That re-run, as every run, turns off git's own terminal prompt and no
+> other: an ssh passphrase or host-key prompt, a credential helper that
+> prompts, or `GIT_ASKPASS`, `SSH_ASKPASS` or `core.askPass` can still make it
+> wait. A key in an ssh agent, a helper that answers without prompting, or
+> `GIT_SSH_COMMAND='ssh -o BatchMode=yes'` (with anything your
+> `core.sshCommand` adds) set in your shell for the run, which makes ssh fail
+> at once instead, keeps it from waiting.
> `--visibility internal` is the third value every tool here accepts; only a
> GitHub Enterprise Cloud organisation offers it at all, and one that does not
> is refused by GitHub rather than by this tool.
diff --git a/docs/cli.md b/docs/cli.md
index d0aa512d..837c7524 100644
--- a/docs/cli.md
+++ b/docs/cli.md
@@ -369,7 +369,16 @@ options:
right finishes a run a ruleset refused by re-running
it with this. A root with commits, and a leg with any
other, is still refused: that is a live repository,
- not a slot.
+ not a slot. A run with it on GitHub, like every run
+ there, turns off git's own terminal prompt and no
+ other, so it can still wait at an ssh passphrase or
+ host-key prompt, at a credential helper that prompts,
+ or at `GIT_ASKPASS`, `SSH_ASKPASS` or `core.askPass`:
+ a key loaded into an ssh agent, a helper that answers
+ without prompting, or `GIT_SSH_COMMAND='ssh -o
+ BatchMode=yes'` (with anything `core.sshCommand` adds)
+ set for the run, which makes ssh fail at once instead,
+ keeps it from waiting.
--pin openProduct@COMMIT
declare a neutral-product pin, e.g. --pin
openGlass@<40 hex>. It writes
diff --git a/docs/handbook.html b/docs/handbook.html
index 75b04198..dc40513d 100644
--- a/docs/handbook.html
+++ b/docs/handbook.html
@@ -472,7 +472,7 @@
-
+
@@ -612,7 +612,7 @@
The one thing to read before electing it
-
+
Setup
Start here
@@ -1092,7 +1092,7 @@ What you have afterwards — the same on every platform
-
+
Setup
If it fails part-way
@@ -1124,6 +1124,14 @@ If it fails part-way
re-running it. A leg with other commits is refused, and a root that HAS commits is a live
repository, which adopt-project.py converts in place
instead.
+ The --reuse-empty-repo re-run, as every run, turns off git’s own
+ terminal prompt and no other: an ssh passphrase or host-key prompt, a credential helper
+ that prompts, or
+ GIT_ASKPASS, SSH_ASKPASS or core.askPass can still
+ make it wait. A key in an ssh agent, a helper that answers without prompting, or
+ GIT_SSH_COMMAND='ssh -o BatchMode=yes' (with anything your
+ core.sshCommand adds) set in your shell for the run, which makes ssh fail at
+ once instead, keeps it from waiting.
--visibility internal is the third value every tool here accepts; only a
GitHub Enterprise Cloud organisation offers it at all, and one that does not is refused by
GitHub rather than by this tool.
@@ -1133,7 +1141,7 @@ If it fails part-way
-
+
@@ -1196,7 +1204,7 @@ Reading private legs in CI
-
+
@@ -1452,7 +1460,7 @@ The InkRouter example
-
+
@@ -1751,7 +1759,7 @@ Is this repository compliant? openRepoShape --doctor
-
+
diff --git a/scaffold-project.py b/scaffold-project.py
index e289d7ac..107b7b24 100755
--- a/scaffold-project.py
+++ b/scaffold-project.py
@@ -401,7 +401,20 @@ def main(argv: list[str] | None = None) -> int:
"right finishes a run a ruleset refused by "
"re-running it with this. A root with commits, "
"and a leg with any other, is still refused: "
- "that is a live repository, not a slot.")
+ "that is a live repository, not a slot. A run "
+ "with it on GitHub, like every run there, turns "
+ "off git's own terminal prompt and no other, so "
+ "it can still wait at an ssh passphrase or "
+ "host-key prompt, at a credential helper that "
+ "prompts, or at "
+ "`GIT_ASKPASS`, `SSH_ASKPASS` or "
+ "`core.askPass`: a key loaded into an ssh "
+ "agent, a helper that answers without "
+ "prompting, or "
+ "`GIT_SSH_COMMAND='ssh -o BatchMode=yes'` (with "
+ "anything `core.sshCommand` adds) set for the "
+ "run, which makes ssh fail at once instead, "
+ "keeps it from waiting.")
parser.add_argument("--pin", action="append", default=[],
metavar="openProduct@COMMIT",
help="declare a neutral-product pin, e.g. "
@@ -503,11 +516,22 @@ def seed_tree(role: str, values: dict, tracking: str, scratch: Path) -> str:
#: read, and a push, a first run's as a re-run's, is refused with how to give
#: git a login (`_refused_push`).
#:
-#: ssh, a credential helper and an askpass program are not turned off: they
-#: are how people give git a credential, and each of them still asks under
-#: this rule (#915's probes). Whether the rule must never wait for input,
-#: ssh run with `BatchMode=yes` for one, is #915's open ruling question; it
-#: is decided here, once, for every command above.
+#: THE RULE TURNS OFF GIT'S OWN PROMPT AND NO OTHER (#915, U037). ssh, a
+#: credential helper and an askpass program (`GIT_ASKPASS`, `SSH_ASKPASS`,
+#: `core.askPass`) are the operator's, and are left as they are: they are
+#: how people give git a credential, and turning one off would refuse a read
+#: or a push that works. So every command above, a first run's push as a
+#: re-run's probe and push, can still wait for input: at ssh's passphrase or
+#: host-key prompt, at a credential helper that prompts, or at an askpass
+#: program, each measured waiting until killed. Nor is ssh given
+#: `BatchMode=yes` here: git's `GIT_SSH_COMMAND` overrides a configured
+#: `core.sshCommand`, so setting it would replace the operator's own ssh
+#: command. Brett Heap ruled it on 2026-10-21, "U039 and U037: take your
+#: recommendations": say it where it is true. The `--reuse-empty-repo` help,
+#: `docs/cli.md`, the README, the handbook and each remedy that sends a
+#: person into that re-run on GitHub say a prompt can still come, and how a
+#: person keeps a run from waiting in their own setup (`PROMPTS_STILL_ASK`).
+#: `RULESET_HINT`, the ruleset's refusal, does not say it yet (#1108).
REMOTE_ENV = {"GIT_TERMINAL_PROMPT": "0"}
#: What `only_commit`'s two commands run with: the prompt rule, and git's
@@ -529,6 +553,28 @@ def seed_tree(role: str, values: dict, tracking: str, scratch: Path) -> str:
#: a push that goes through shows nothing of git's.
PUSH_ENV = {**REMOTE_ENV, "LC_ALL": "C"}
+#: What each remedy that sends a person into a `--reuse-empty-repo` re-run
+#: says of the prompt rule, after "That re-run" (#915, U037): the prompts
+#: `REMOTE_ENV` leaves as the operator set them up, and the three ways to keep
+#: a run from waiting at one, each in the person's own setup. ssh under
+#: `BatchMode=yes` refuses the host-key and passphrase prompts and fails at
+#: once (measured: the probe is refused at once, as `scaffold-remote-exists`
+#: with ssh's `Host key verification failed.`). `GIT_SSH_COMMAND` replaces
+#: `core.sshCommand`, so the person carries over what theirs adds. Said by
+#: `_exists_remedy` and `_login_remedy`; not under `--local-remote-dir`,
+#: whose paths on disk git reads with no ssh, helper or askpass at all.
+#: `RULESET_HINT`, the ruleset's refusal, also sends a person into that
+#: re-run and does not say it yet (#1108).
+PROMPTS_STILL_ASK = (
+ "turns off git's own terminal prompt and no other, so it can still wait "
+ "at an ssh passphrase or host-key prompt, at a credential helper that "
+ "prompts, or at `GIT_ASKPASS`, `SSH_ASKPASS` or `core.askPass`: to keep "
+ "it from waiting, load your key into an ssh agent, use a helper that "
+ "answers without prompting, or set "
+ "`GIT_SSH_COMMAND='ssh -o BatchMode=yes'` (with anything your "
+ "`core.sshCommand` adds) in your shell for the run, which makes ssh fail "
+ "at once instead")
+
def only_commit(url: str,
scratch: Path) -> tuple[str, str, tuple[str, ...]] | None:
@@ -756,7 +802,7 @@ def _reusable_remotes(names: dict, urls: dict, repositories: dict,
+ ("" if local else " on GitHub")
+ ("" if empty else " and has commits"),
_exists_remedy(role, empty, reuse_flag, seed is not None,
- unread))
+ unread, local))
return reused
@@ -783,7 +829,8 @@ def _reusable_remotes(names: dict, urls: dict, repositories: dict,
def _exists_remedy(role: str, empty: bool, reuse_flag: bool,
- seeded: bool = False, unread: str | None = None) -> str:
+ seeded: bool = False, unread: str | None = None,
+ local: bool = False) -> str:
"""The remedy line for a remote that is already there.
Each situation answers to its own exit, and a refusal that named only
@@ -798,7 +845,15 @@ def _exists_remedy(role: str, empty: bool, reuse_flag: bool,
git logs in with an ssh key, and the command named is still the https
URL this run gave git. So the ssh key's clause (`SSH_KEY_CLAUSE`) stands
beside the `setup-git` one, each saying where it applies.
+
+ EACH EXIT THAT IS THE `--reuse-empty-repo` RE-RUN SAYS WHICH PROMPTS IT
+ CAN STILL WAIT AT (#915, U037): its probe and its pushes turn off git's
+ own prompt alone, and leave ssh, a credential helper and an askpass
+ program as the operator set them up (`PROMPTS_STILL_ASK`). Not where
+ `local` (`--local-remote-dir`): there git reads paths on disk, and none
+ of them is asked.
"""
+ still = "" if local else " That re-run " + PROMPTS_STILL_ASK + "."
if role != "assembly":
if unread:
return (f"Remediation: git could not read it ({unread}), so "
@@ -808,7 +863,7 @@ def _exists_remedy(role: str, empty: bool, reuse_flag: bool,
+ SSH_KEY_CLAUSE + "), then re-run: with "
"--reuse-empty-repo a leg whose one commit is that seed "
"is reused, and a leg holding anything else is still "
- "refused. There is no --force.")
+ "refused. There is no --force." + still)
if empty or seeded:
return ("Remediation: "
+ ("it has ZERO commits" if empty else
@@ -817,7 +872,7 @@ def _exists_remedy(role: str, empty: bool, reuse_flag: bool,
+ f", so re-run with --reuse-empty-repo to use it as the "
f"{role} leg. A run an organisation's ruleset refused is "
"finished that way, by an operator holding the bypass "
- "right.")
+ "right." + still)
return ("Remediation: it holds commits other than the one seed this "
"scaffold would push to it, so it is not a slot this scaffold "
"may fill. Delete it, or scaffold under a different "
@@ -828,7 +883,7 @@ def _exists_remedy(role: str, empty: bool, reuse_flag: bool,
if empty and not reuse_flag:
return ("Remediation: it has ZERO commits, so it is a reserved name "
"rather than a live project — re-run with --reuse-empty-repo "
- "to use it as the assembly root.")
+ "to use it as the assembly root." + still)
return ("Remediation: it HAS commits, so it is a live repository and this "
"is not a scaffold. Use `adopt-project.py plan --source "
"/ --project ` to convert it in place, which "
@@ -2090,7 +2145,11 @@ def _login_remedy(output: str, repo: str, reused: bool) -> str:
(`setup-project.py`'s `_credential_helper_configured` reads it so). So
it replaces a refused login too. Over ssh it changes nothing, so a key
ssh was refused is told the ssh check alone (`SSH_KEY_CHECK`). A run
- that was itself the re-run (`reused`) is re-run as it was."""
+ that was itself the re-run (`reused`) is re-run as it was. Last, that
+ re-run's prompts: git's own is off and ssh's, a helper's and an askpass
+ program's are not (`PROMPTS_STILL_ASK`, #915 U037). A login git could not
+ get was refused rather than asked for, and the login a person gives git
+ next, an ssh key above all, can ask on the re-run."""
if any(words in output for words in NO_LOGIN):
fix = (f"git could not get a login for {repo}: every push this "
"scaffold makes runs with git's terminal prompt off, so where "
@@ -2121,7 +2180,8 @@ def _login_remedy(output: str, repo: str, reused: bool) -> str:
"through setup.sh) and a new --work-dir or none")
return (f"\nRemediation: {fix}, then re-run {again}: it reuses the "
"repositories this run left empty, and a leg whose one commit is "
- "the seed this scaffold pushes.")
+ "the seed this scaffold pushes. That re-run "
+ + PROMPTS_STILL_ASK + ".")
def _refused_push(failed: CommandFailed, what: str, repo: str,
diff --git a/tests/readme-cap.d/915.txt b/tests/readme-cap.d/915.txt
new file mode 100644
index 00000000..4b852b31
--- /dev/null
+++ b/tests/readme-cap.d/915.txt
@@ -0,0 +1,11 @@
++7
+2026-10-21
+Seven lines for the prompts a `--reuse-empty-repo` re-run can still wait at
+(#915, U037; Brett Heap, 2026-10-21, "U039 and U037: take your
+recommendations"). The scaffold turns off git's own terminal prompt alone and
+leaves ssh, credential helpers and askpass programs as the operator set them
+up, so the re-run this section sends a person into can stop at an ssh
+passphrase or host-key prompt, a prompting helper or `GIT_ASKPASS`,
+`SSH_ASKPASS` or `core.askPass`, each measured waiting until killed. The lines
+name those prompts and the three ways a person keeps the run from waiting,
+`GIT_SSH_COMMAND='ssh -o BatchMode=yes'` among them, and nothing else.
diff --git a/tests/test_scaffold_reuse_seeded_legs.py b/tests/test_scaffold_reuse_seeded_legs.py
index 83e7c73c..e5997d5c 100644
--- a/tests/test_scaffold_reuse_seeded_legs.py
+++ b/tests/test_scaffold_reuse_seeded_legs.py
@@ -154,6 +154,10 @@ def test_an_empty_leg_without_the_flag_is_refused_naming_the_flag(tmp_path):
assert "it has ZERO commits" in result.stderr
assert "re-run with --reuse-empty-repo to use it as the spec leg" in (
result.stderr)
+ # #915, U037: a re-run under --local-remote-dir reads and pushes paths
+ # on disk, where no ssh, credential helper or askpass is asked, so no
+ # prompt can follow and the remedy does not say one can.
+ assert STILL_ASKS.strip() not in result.stderr, result.stderr
assert not bare(tmp_path, "code").exists(), "nothing was created"
@@ -949,12 +953,27 @@ def git_reads_github_from(base: str, helper: str = "") -> dict:
SSH_KEY = ("where a `url..insteadOf` rule in your git config sends "
"`https://github.com/` over ssh, git logs in with an ssh key "
f"instead, and {SSH_KEY_CHECK}")
+#: What each remedy that sends a person into the `--reuse-empty-repo`
+#: re-run on GitHub ends with (#915, U037; Brett Heap, 2026-10-21, "U039 and
+#: U037: take your recommendations"): that re-run turns off git's own
+#: prompt alone, and ssh, a credential helper and an askpass program, which
+#: are the operator's and stay as they set them up, can still make it wait.
+#: A literal, so the test cannot agree with whatever the code's words become.
+STILL_ASKS = (
+ " That re-run turns off git's own terminal prompt and no other, so it "
+ "can still wait at an ssh passphrase or host-key prompt, at a credential "
+ "helper that prompts, or at `GIT_ASKPASS`, `SSH_ASKPASS` or "
+ "`core.askPass`: to keep it from waiting, load your key into an ssh "
+ "agent, use a helper that answers without prompting, or set "
+ "`GIT_SSH_COMMAND='ssh -o BatchMode=yes'` (with anything your "
+ "`core.sshCommand` adds) in your shell for the run, which makes ssh fail "
+ "at once instead.")
UNREAD_EXIT = ("so whether its one commit is the seed this scaffold pushes "
"cannot be asked. Let git read it (on GitHub, `gh auth "
f"setup-git` gives git the login `gh` has; {SSH_KEY}), then "
"re-run: with --reuse-empty-repo a leg whose one commit is "
"that seed is reused, and a leg holding anything else is "
- "still refused. There is no --force.")
+ "still refused. There is no --force." + STILL_ASKS)
@WINDOWS_SKIP
@@ -1090,17 +1109,18 @@ def test_a_reuse_re_run_over_empty_legs_does_not_wait_at_its_push(tmp_path):
"first (on GitHub, `gh auth setup-git` gives git the login `gh` has, or "
"store a token for the host in your credential helper; "
f"{SSH_KEY}), then re-run ")
-#: The re-run it names after a first run: the same command, the flag added.
+#: The re-run it names after a first run: the same command, the flag added,
+#: and the prompts that re-run can still wait at (`STILL_ASKS`, #915 U037).
RE_RUN_WITH_THE_FLAG = (
"the same command with --reuse-empty-repo added (after `--` through "
"setup.sh) and a new --work-dir or none: it reuses the repositories this "
"run left empty, and a leg whose one commit is the seed this scaffold "
- "pushes.")
+ "pushes." + STILL_ASKS)
#: And after a run that was that re-run: the same command again.
RE_RUN_AS_IT_WAS = (
"the same command with a new --work-dir or none: it reuses the "
"repositories this run left empty, and a leg whose one commit is the "
- "seed this scaffold pushes.")
+ "seed this scaffold pushes." + STILL_ASKS)
#: How it names the fix for a push whose login the host refused (#993): git
@@ -1383,6 +1403,37 @@ def scaffold_module():
return load_without_bytecode(SCAFFOLD, "scaffold_for_unread_because")
+@pytest.mark.parametrize("role,empty,reuse_flag,seeded,unread,re_run", [
+ ("spec", False, True, False,
+ "`git ls-remote x` exited 128: Host key verification failed.", True),
+ ("spec", True, False, False, None, True),
+ ("code", False, False, True, None, True),
+ ("spec", False, True, False, None, False),
+ ("assembly", True, False, False, None, True),
+ ("assembly", False, True, False, None, False),
+], ids=["a-leg-git-cannot-read", "an-empty-leg", "a-leg-holding-the-seed",
+ "a-leg-with-other-commits", "an-empty-root", "a-root-with-commits"])
+def test_each_re_run_remedy_says_the_prompts_it_can_still_wait_at(
+ scaffold_module, role, empty, reuse_flag, seeded, unread, re_run):
+ """#915, U037 (Brett Heap, 2026-10-21, "U039 and U037: take your
+ recommendations"). The prompt rule turns off git's own prompt alone:
+ measured at 2a119f2, a re-run's probe and push waited until killed at
+ ssh's host-key prompt, at a passphrase prompt, at a prompting credential
+ helper, and at `GIT_ASKPASS`, `SSH_ASKPASS` and `core.askPass`. Those
+ are the operator's and stay as they are, so each remedy whose exit is
+ the `--reuse-empty-repo` re-run ends saying so, and how to keep it from
+ waiting (`STILL_ASKS`); one whose exit is no re-run, deleting a leg or
+ adopting a root, does not. Under `--local-remote-dir` git reads paths on
+ disk, where none of them is asked, so no remedy says it there.
+ In-process, so it runs on every platform."""
+ on_github = scaffold_module._exists_remedy(role, empty, reuse_flag,
+ seeded, unread)
+ on_disk = scaffold_module._exists_remedy(role, empty, reuse_flag,
+ seeded, unread, local=True)
+ assert STILL_ASKS.strip() not in on_disk, on_disk
+ assert on_github == on_disk + (STILL_ASKS if re_run else ""), on_github
+
+
#: git's advice after its generic `fatal:` line, which no reason keeps.
ADVICE = ("\n\nPlease make sure you have the correct access rights\n"
"and the repository exists.")