Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,21 +134,24 @@ install command). Seven steps, in order, and step 4 is the one you must not skip
record). Set `leg:` to `spec`, `code`, `root` or `drop`
AND add a `resolution:` line saying why — read the repository to answer it
(does the specification cite `examples/golden-run/`, or do the tests?). An
answer with no reason beside it is a guess that will be read as a finding.
Never delete an entry to make the question go away: `check` refuses a path
that no entry covers, and it is right to.
answer with no reason is a guess, and `check` passes it: the human reads the
reasons at step 4. Never delete an entry to make the question go away:
`check` refuses a path that no entry covers, and it is right to.
4. **Show the human the plan and the follow-ups and let them say yes.** The
follow-ups are code changes the split MAKES NECESSARY — the harness that
reads `contracts/` from beside it will read it across the root mount — and
`execute` does not make them. Do not pass `--yes` on your own initiative;
it is the human's word, not your convenience.
`execute` does not make them; it opens the split's pull request itself,
because these organisations are pull-request only. Never suggest a direct
push to the default branch, and never `--admin`. Do not pass `--yes` on your
own initiative; it is the human's word, not your convenience.
7. **Read the verification table back to them.** It accounts for every source
path at the source commit, by blob sha (a kept root `.gitattributes` as the
source's bytes plus the line-ending block the split appends), in exactly one
of {spec leg, code leg, root tree} or as `drop`. If it exits non-zero, STOP
and relay it, and take the way out THAT exit prints. Exit 1, a failed
verification: the source is untouched — nothing is deleted, `main` is where
it was, the split is on a branch — so fix the plan, delete the split branch,
verification: nothing is deleted and `main` is where it was, but the split
branch is pushed and, on GitHub, its pull request may be open and the topic
set — so fix the plan, delete the split branch (closing that pull request),
and re-run into fresh legs and a fresh `--work-dir`, or none. Exit 2 saying
`IS pushed` (GitHub only: `gh pr create` or the topic refused) comes after
both legs and the split are pushed and verified: finish with the command it
Expand All @@ -159,9 +162,6 @@ the split's `project.yaml`, under `submodules:` — per submodule a leg keeps,
its `path`, `leg`, `resolution:` and `source_commit`. It confers nothing,
nothing rewrites it, and the doctor holds each submodule to it.

`execute` opens a pull request, because these organisations are pull-request
only. Never suggest a direct push to the default branch, and never `--admin`.

### Adopting a repository that has NO code yet (or no spec)

A leg no plan entry assigns a path to is SEEDED from `templates/<role>-root/`,
Expand Down
155 changes: 116 additions & 39 deletions adopt-project.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,9 @@
after the table, each with the commands that finish the job by hand; those
commands are the way out: a re-run into fresh legs abandons the pushed pair
for a second. A verification that fails outranks both, with exit 1 and no
command to open the pull request or to set the topic.
command to open the pull request or to set the topic; it says what the run
already put in place, the pull request open for the failed split, which
deleting the split branch closes, and the topic, which stays set (#523).
"""

from __future__ import annotations
Expand Down Expand Up @@ -2019,6 +2021,16 @@ def follow_ups_for(source: "Source", entries: list["Entry"], names: dict,
of `tracking_branch:`, which `plan-tracking-branch-missing`'s
remediation asks for, in `check`'s output and in the pull request's own
body (#493's review, F3).

It is a review, and never "open the split as a pull request" (#523).
`plan` prints it and the split commit lists it under the FOLLOW-UPS this
commit does NOT do, which is also the pull request's body, while against
GitHub `execute` opens that pull request itself, and under
`--local-remote-dir` opens none. So it says what `execute` does in either
run, and calls the legs what they are: new repositories `execute` creates
and pushes before the split, extracted or, with no path, seeded from a
template. "Seeded", said of both, named an extracted leg by the word kept
for a template's.
"""
out = cross_leg_references(source, entries, spec_path, code_path)
if any(e.leg == "spec" and (e.path == "contracts/"
Expand All @@ -2035,10 +2047,11 @@ def follow_ups_for(source: "Source", entries: list["Entry"], names: dict,
"owner rule cannot cross a repository boundary")
out.extend(collisions)
out.append(
"open the split as a pull request against the plan's "
"`tracking_branch`, the branch the assembly root and both legs track; "
f"the legs {names['spec']} and {names['code']} are seeded before it, "
"so review the root's diff and the two new repositories together")
f"review the root's diff together with {names['spec']} and "
f"{names['code']}, the two new repositories `execute` creates and "
"pushes before the split; against GitHub it opens the split's pull "
"request itself, whose base is the plan's `tracking_branch`, the "
"branch the assembly root and both legs track")
return out


Expand Down Expand Up @@ -7055,29 +7068,31 @@ def _refuse_an_unrunnable_plan(plan: Plan, source: Source) -> None:


#: The adoption's own ruleset hint (#177). `RULESET_HINT` is the scaffold's,
#: and it stays the scaffold's word for word. ITS SECOND EXIT IS NOT OFFERED
#: HERE, because it cannot finish an adoption (#188 N2). It pushed the leg to
#: a seed branch and opened a pull request against the tracking branch, but
#: the leg is a repository this run created and its first push was the one
#: refused, so there is no tracking branch for that pull request to target;
#: and even a merged one would resume nothing, since the rest of the adoption
#: (the other leg, the split) is made only by a re-run, into new legs. So an
#: adoption names its one exit, a re-run by somebody holding the bypass right,
#: and the notice printed after this hint (`_nothing_rolled_back`) names what
#: that re-run must clear first. The notice also says to fix what failed
#: before the re-run, and under such a ruleset the bypass IS that fix: the
#: hint says so last, or "fixed", read alone, would invite a change to the
#: ruleset this hint forbids (#188's re-verification, R4).
#: and it stays the scaffold's word for word. A SEED BRANCH AND A PULL
#: REQUEST ARE NAMED ONLY AS WHAT CANNOT FINISH AN ADOPTION (#188 N2). That
#: route pushed the leg to a seed branch and opened a pull request against the
#: tracking branch, but the leg is a repository this run created and its first
#: push was the one refused, so there is no tracking branch for that pull
#: request to target; and even a merged one would resume nothing, since the
#: rest of the adoption (the other leg, the split) is made only by a re-run,
#: into new legs. Nor is it "the scaffold's other exit", as this hint called
#: it until #523: #506 took it out of the scaffold's hint too, which names it
#: only as what cannot finish a scaffold. So an adoption names its one exit, a
#: re-run by somebody holding the bypass right, and the notice printed after
#: this hint (`_nothing_rolled_back`) names what that re-run must clear
#: first. The notice also says to fix what failed before the re-run, and
#: under such a ruleset the bypass IS that fix: the hint says so last, or
#: "fixed", read alone, would invite a change to the ruleset this hint
#: forbids (#188's re-verification, R4).
ADOPT_RULESET_HINT = """
If the organisation applies a ruleset requiring changes to arrive by pull
request, a direct push to the default branch is refused BY DESIGN and must not
be worked around. An adoption has ONE legitimate exit: have an operator
holding the bypass right re-run it, once what the notice below names is
cleared. The scaffold's other exit, a seed branch and a pull request, cannot
finish an adoption: {repo} has no {tracking} for that pull request to target,
and nothing would run the rest of this adoption after it was merged. Under
such a ruleset, that bypass is the fix the notice below asks for, never a
change to the ruleset.
cleared. A seed branch and a pull request cannot finish an adoption:
{repo} has no {tracking} for that pull request to target, and nothing would
run the rest of this adoption after it was merged. Under such a ruleset, that
bypass is the fix the notice below asks for, never a change to the ruleset.
"""

#: What a step that runs once a leg repository exists reports as a REFUSAL,
Expand Down Expand Up @@ -8231,14 +8246,19 @@ def _open_the_pull_request(repositories: dict, names: dict, tracking: str,


def _finish_by_hand(passed: bool, refused: list[str] | None, topic: str,
unset: list[str]) -> None:
unset: list[str], *, opened: str | None = None,
carried: list[str] | None = None) -> None:
"""What a refused `gh pr create` and a topic that would not set leave to
do, said ONCE, after the table, when `execute` knows whether the split it
pushed is the one the plan asked for (#304, #481).

`refused` is the `gh pr create` that `gh` refused, as
`_pull_request_command` built it, and None when the pull request is open;
`unset` names each repository `_set_the_topic` could not give `topic`.
`_pull_request_command` built it, and None when the pull request is open
or none was tried (`--local-remote-dir`); `unset` names each repository
`_set_the_topic` could not give `topic`. `opened` is the repository the
split's pull request IS open on, None where none is, and `carried` each
repository that has the topic: what a failed verification leaves in
place, and says (#523).

A split that PASSED verification gets the commands that finish the job:
a `gh repo edit` for each repository left without the topic, then the
Expand All @@ -8255,16 +8275,41 @@ def _finish_by_hand(passed: bool, refused: list[str] | None, topic: str,
the exact command that was tried, and a person who sees only that, with
findings under it, may paste it. No topic is offered there either: the
way out is the verification's, and the re-run it names sets the topic.

WHAT IT LEAVES IN PLACE IS SAID TOO (#523). `execute` sets the topic and
opens the pull request BEFORE it verifies, so against GitHub a split that
failed has its pull request open on the source and the topic on every
repository that took it, and the way out said neither: it said NOTHING
was deleted from the source, which is true, and AGENTS.md said the source
was untouched, which is not. So the open pull request is named as one
for a split that failed, with what closes it, the deletion of its branch
the way out already names; and the topic as staying set: the re-run sets
it again on the root and on the legs it makes, and a leg the person
keeps, re-running into fresh leg repositories rather than deleting it,
keeps the topic too (#582's review).

Stdout is flushed first. The table is on it and a pipe holds it in a
block buffer, so without the flush this would come before the table it
depends on in a merged log; a stdout that is closed, `| head` and gone,
must not take the commands down with it (`_flush_stdout`).
"""
_flush_stdout()
if not passed:
said = []
if refused is not None:
print("No pull request was opened, and none should be: the split "
"above did not pass verification.", file=sys.stderr)
said.append("No pull request was opened, and none should be: the "
"split above did not pass verification.")
elif opened is not None:
said.append(f"A pull request for that branch IS open on {opened}, "
"for a split that did not pass verification: "
"deleting the branch, as above, closes it.")
if carried:
said.append(f"The topic {topic} stays set on "
f"{_and_listed(carried)}: the re-run sets it again "
"on the root and the legs it makes, and a leg left "
"behind keeps it.")
if said:
print(" ".join(said), file=sys.stderr)
return
topics = "".join(f"\n gh repo edit {repository} --add-topic {topic}"
for repository in unset)
Expand Down Expand Up @@ -8696,13 +8741,17 @@ def cmd_execute(args) -> int:
# to know before finishing either by hand is whether the split is right
# (#304), so what is left is said once, after the table.
unset: list[str] = []
carried: list[str] = []
refused: list[str] | None = None
opened: str | None = None
if not local:
carried = _set_the_topic(repositories, topic)
unset = [repositories[role] for role in TOPIC_ROLES
if repositories[role] not in carried]
if not _open_the_pull_request(repositories, names, tracking, branch,
message):
if _open_the_pull_request(repositories, names, tracking, branch,
message):
opened = repositories["assembly"]
else:
refused = _pull_request_command(repositories, names, tracking,
branch, message)

Expand All @@ -8712,9 +8761,13 @@ def cmd_execute(args) -> int:
urls=urls, branch=branch)
if not verified and refused is None:
# Where the shape's Makefile and bootstrap script WENT, as the
# materializer reports it, and not a second look at the tree (#480).
_next_steps(names, collisions)
_finish_by_hand(not verified, refused, topic, unset)
# materializer reports it, and not a second look at the tree (#480);
# under `--local-remote-dir` no pull request is opened, so NEXT names
# the split's branch, and the repository it is pushed to (#523).
_next_steps(names, collisions,
(branch, urls["assembly"]) if local else None)
_finish_by_hand(not verified, refused, topic, unset, opened=opened,
carried=carried)
if verified:
# A verification mismatch outranks a refused pull request and a
# missing topic.
Expand Down Expand Up @@ -9229,10 +9282,25 @@ def _left_by_the_split(source: Source, urls: dict, names: dict,
SHAPE_MAKEFILE = "Makefile"


def _next_steps(names: dict, collisions: list) -> None:
def _next_steps(names: dict, collisions: list,
split: tuple[str, str] | None = None) -> None:
"""What a person does once the split is verified AND its pull request is
open. Not said where there is none to review: after a refused `gh pr
create`, the command to open it is the next step (#304).
open, or none was tried. Not said after a refused `gh pr create`: there
the command to open it is the next step (#304).

`split` is `(branch, url)`, the split's branch and the repository it is
pushed to, where no pull request was tried: under `--local-remote-dir`,
where that repository is the source itself. NEXT said "review the pull
request" there as well, over none (#523, N2), and named a clone of the
default branch, where the split is not. There it names the split on its
branch, and a clone of that branch from that repository, each value
quoted for `sh` as `_branch_deletion` quotes it, so the line pastes as
printed. The clone carries `FILE_PROTOCOL`: the split's `.gitmodules`
names each leg by its plain path under `--local-remote-dir`, and git
clones a submodule over the `file` transport only where it is told to,
so without it both leg clones die `transport 'file' not allowed`, as
pasting the line did in #582's review. Without `split`, the block is
what it was, byte for byte.

`collisions` is each `(intended, actual)` the materializer wrote beside a
file the source kept: where the shape's files WENT, and not a second
Expand All @@ -9255,9 +9323,18 @@ def _next_steps(names: dict, collisions: list) -> None:
bootstrap = ("" if script else
"\n python3 scripts/bootstrap.py" if makefile else
"\n make bootstrap")
print(f"\nNEXT: review the pull request on {names['assembly']}, then\n"
f" git clone --recurse-submodules <{names['assembly']} url>"
f"{bootstrap}")
if split is None:
review = f"the pull request on {names['assembly']}"
clone = ("git clone --recurse-submodules "
f"<{names['assembly']} url>")
else:
branch, url = split
review = (f"the split on its branch in {names['assembly']} (no pull "
"request is opened under --local-remote-dir)")
clone = " ".join(["git", *FILE_PROTOCOL, "clone",
"--recurse-submodules", "-b", shlex.quote(branch),
shlex.quote(url)])
print(f"\nNEXT: review {review}, then\n {clone}{bootstrap}")
if script:
kept = [path for path in (SHAPE_MAKEFILE, BOOTSTRAP)
if path in aside]
Expand Down
Loading
Loading