From 570abd8d5c2529a886e03c5db80a80e2c2da9b04 Mon Sep 17 00:00:00 2001 From: Noisemaker111 <139656120+Noisemaker111@users.noreply.github.com> Date: Thu, 10 Sep 2026 11:53:30 -0400 Subject: [PATCH] Reserve main merges for Jon and prepare maintainer release notes --- .github/workflows/agent-checks.yml | 3 ++ AGENTS.md | 32 +++++++++++----- scripts/check_release.py | 25 ++++++++++++ scripts/integrate_agents.py | 23 ++++++++--- scripts/prepare_release.py | 61 ++++++++++++++++++++++++++++++ 5 files changed, 128 insertions(+), 16 deletions(-) create mode 100644 scripts/check_release.py create mode 100644 scripts/prepare_release.py diff --git a/.github/workflows/agent-checks.yml b/.github/workflows/agent-checks.yml index 66d5d91..58fdce6 100644 --- a/.github/workflows/agent-checks.yml +++ b/.github/workflows/agent-checks.yml @@ -2,6 +2,7 @@ name: Agent checks on: pull_request: branches: [agents, main] + types: [opened, synchronize, reopened, ready_for_review, edited] push: branches: [agents] permissions: @@ -27,3 +28,5 @@ jobs: run: python -m unittest discover -s experiments/command_specialist -p test_bindings.py -v - name: Native and PowerShell contract invariants run: python -m unittest discover -s experiments/command_specialist -p test_contract.py -v + - name: Frozen main candidate and patch notes + run: python scripts/check_release.py diff --git a/AGENTS.md b/AGENTS.md index 6477e37..3c8ca2f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,8 +4,11 @@ branch. Jon authorized agents-branch automation on September 10, 2026: agents may prepare owned changes, verify them, open ready PRs against `agents`, and merge them when the gates below pass. This replaces per-PR human merge requests for `agents`. -Merging into `main`, publication, deployments, model/weight changes, and production -data changes still require explicit approval for the concrete batch. +Only Jon personally merges release PRs into `main`. Agents must never merge main, +enable its auto-merge, or push directly to it, even after chat approval. Publication, +deployments, model/weight changes, and production data changes need separate approval. +This agents automation applies to every agent acting for a maintainer with live +repository write, maintain, or admin permission; no particular model/host is privileged. Start from fetched `agents` in an owned worktree. Preserve other sessions and reuse existing PRs for the same work. Run commands from the repository root with Python @@ -27,8 +30,11 @@ into both branches and pushes to `agents`. It runs on a hosted Windows runner wi read-only permissions and no persisted checkout credentials. PR code never receives a privileged merge token. `agents` requires this GitHub Actions check and an up-to-date PR, prevents force pushes/deletion, and applies protection to administrators. No approving review is -required for `agents`. Main protection is pending Jon's choice of GitHub review -enforcement versus explicit chat approval; do not claim it is server-enforced. +required for `agents`. Main requires the same CI/PR gate; Jon performs the merge +himself. A separate approving reviewer is not required because Jon authors these +PRs through the same account. The platform cannot distinguish Jon from an agent +using his credentials; the human-only merge rule is enforced by this workflow and +the coordinator's unconditional refusal of main. The local coordinator is `scripts/integrate_agents.py`. After reviewing the diff, verifying the current head locally, and waiting for CI, run: @@ -37,14 +43,19 @@ verifying the current head locally, and waiting for CI, run: python scripts/integrate_agents.py --pr --verified-head ``` -It accepts only ready same-repository PRs authored by Noisemaker111 targeting -`agents`; checks the exact head, workflow identity/result, and mergeability; and +It accepts only ready same-repository PRs targeting `agents` when both the PR author +and the acting account have live write/maintain/admin permission; checks the exact head, workflow identity/result, and mergeability; and merges with GitHub's expected-head condition. It refuses `main`. `--check-only` checks eligibility without merging. Invoke it from a trusted checkout; passing a head is the agent's attestation of actual local verification, not proof supplied -by CI. This is agent-operated automation, not an installed unattended scheduler. +by CI. Every maintainer's agent uses this same automatic integration path; it does not +require Jon to run the command. It is agent-operated, not an unattended scheduler. The agent continues through merge and merged-revision verification without asking -Jon for routine agents integration approval. +Jon for routine agents integration approval. Each successful merge automatically +runs `scripts/prepare_release.py`: it generates patch notes from merged change +titles and opens a frozen main release PR. An existing open release batch is +preserved; later agents changes wait for the next batch. Release preparation may +be resumed with `python scripts/prepare_release.py`; this never merges main. This repository is a local skill/CLI, with no hosted frontend, backend, database, queue, production service, or deployment trigger configured here. Dev validation @@ -57,8 +68,9 @@ reporting dev verification. For stable promotion, branch a frozen release candidate from a verified agents revision and open a ready PR into `main`. Include the full diff, user-facing notes, verification evidence, and rollback target (the prior main revision). Later agents -changes must not join that candidate silently. Ask Jon to approve that concrete -batch before merging. The agents GitHub PR/check gates are enforced, but this account is shared +changes must not join that candidate silently. CI checks the candidate identity +and generated patch notes via `scripts/check_release.py`, in addition to core +checks. Ask Jon to merge the CI-green release PR himself; agents stop before main. The agents GitHub PR/check gates are enforced, but this account is shared by Jon and agents: human authorization is a workflow rule, not an independently verified GitHub reviewer identity. The coordinator cannot merge stable. No package or runtime deployment is implied by either merge. diff --git a/scripts/check_release.py b/scripts/check_release.py new file mode 100644 index 0000000..6f12002 --- /dev/null +++ b/scripts/check_release.py @@ -0,0 +1,25 @@ +"""Validate that a main PR is an explicitly pinned release with patch notes.""" +import json +import os +from pathlib import Path +import re + + +def validate(pr): + if pr['base']['ref'] != 'main': + return + if not pr['head']['ref'].startswith('release/agents-'): + raise ValueError('Main accepts a frozen agents release candidate; prepare its release PR') + marker = f"" + body = pr.get('body') or '' + if marker not in body or '## Patch notes' not in body or not re.search(r'^- .+', body, re.MULTILINE): + raise ValueError('Patch notes and exact candidate identity must be present in the release PR') + if not re.search(r'', body): + raise ValueError('Release must record the prior stable source') + + +if __name__ == '__main__': + event = json.loads(Path(os.environ['GITHUB_EVENT_PATH']).read_text(encoding='utf-8')) + if 'pull_request' in event: + validate(event['pull_request']) + print('Release candidate metadata checked; only Jon may merge main.') diff --git a/scripts/integrate_agents.py b/scripts/integrate_agents.py index 1a25f41..a411508 100644 --- a/scripts/integrate_agents.py +++ b/scripts/integrate_agents.py @@ -1,4 +1,4 @@ -"""Integrate a locally verified, CI-green owner PR into agents; never main. +"""Integrate a locally verified, CI-green maintainer PR into agents; never main. Run from a trusted agents checkout after inspecting the diff and completing the real user-operation check. No PR code is downloaded or executed by this tool. @@ -9,7 +9,7 @@ import subprocess REPO = "Noisemaker111/shell-forensics" -OWNER = "Noisemaker111" +MAINTAINER_PERMISSIONS = {"write", "maintain", "admin"} def gh(*args): @@ -17,11 +17,17 @@ def gh(*args): return json.loads(result.stdout) if result.stdout.strip() else None +def require_maintainer(login): + permission = gh("api", f"repos/{REPO}/collaborators/{login}/permission")["permission"] + if permission not in MAINTAINER_PERMISSIONS: + raise ValueError(f"{login} does not have repository write/maintain/admin permission") + + def eligible(pr, verified_head): if pr["baseRefName"] != "agents": - raise ValueError("Only agents PRs may be integrated; main needs Jon's explicit batch approval") - if pr["state"] != "OPEN" or pr["isDraft"] or pr["isCrossRepository"] or pr["author"]["login"].lower() != OWNER.lower(): - raise ValueError("Require an open, ready, same-repository owner PR") + raise ValueError("Only agents PRs may be integrated; only Jon may perform the main merge") + if pr["state"] != "OPEN" or pr["isDraft"] or pr["isCrossRepository"]: + raise ValueError("Require an open, ready, same-repository maintainer PR") if pr["headRefOid"] != verified_head: raise ValueError("Head changed or was not locally verified; recheck the actual user operation") if pr["mergeable"] != "MERGEABLE" or pr["mergeStateStatus"] != "CLEAN": @@ -44,6 +50,8 @@ def main(): fields = "state,isDraft,isCrossRepository,author,baseRefName,headRefOid,mergeable,mergeStateStatus,statusCheckRollup" pr = gh("pr", "view", str(args.pr), "--repo", REPO, "--json", fields) checks = eligible(pr, args.verified_head) + require_maintainer(pr["author"]["login"]) + require_maintainer(gh("api", "user")["login"]) for check in checks: match = re.fullmatch(r"https://github\.com/Noisemaker111/shell-forensics/actions/runs/(\d+)/job/\d+", check.get("detailsUrl", "")) if not match: @@ -62,7 +70,10 @@ def main(): receipt = gh("pr", "view", str(args.pr), "--repo", REPO, "--json", "state,baseRefName,mergeCommit,url") if receipt["state"] != "MERGED" or receipt["baseRefName"] != "agents": raise RuntimeError("Merge receipt did not confirm agents integration") - print(json.dumps(receipt)) + print(json.dumps(receipt), flush=True) + # Prepare the human's next release batch; this helper never merges main. + from prepare_release import prepare + print(json.dumps(prepare(receipt["mergeCommit"]["oid"]))) if __name__ == "__main__": diff --git a/scripts/prepare_release.py b/scripts/prepare_release.py new file mode 100644 index 0000000..70a7616 --- /dev/null +++ b/scripts/prepare_release.py @@ -0,0 +1,61 @@ +"""Generate patch notes and a frozen release PR; only Jon merges main.""" +import argparse +import json +from pathlib import Path +import re +import subprocess + +REPO = "Noisemaker111/shell-forensics" + + +def command(*args): + return subprocess.run(args, check=True, capture_output=True, text=True, encoding="utf-8", timeout=60).stdout.strip() + + +def prepare(candidate=None): + command("git", "fetch", "origin", "agents", "main") + candidate = candidate or command("git", "rev-parse", "origin/agents") + if not re.fullmatch(r"[0-9a-f]{40}", candidate): + raise ValueError("candidate must be a full commit ID") + command("git", "merge-base", "--is-ancestor", candidate, "origin/agents") + stable = command("git", "rev-parse", "origin/main") + command("git", "merge-base", "--is-ancestor", stable, candidate) + if not command("git", "diff", "--name-only", stable, candidate): + return {"release": "no changes"} + existing = json.loads(command("gh", "pr", "list", "--repo", REPO, "--base", "main", "--state", "open", "--json", "headRefName,url")) + releases = [p for p in existing if p["headRefName"].startswith("release/agents-")] + if releases: + return {"release": "existing batch preserved for Jon", "url": releases[0]["url"]} + subjects = command("git", "log", "--first-parent", "--reverse", "--format=%s", f"{stable}..{candidate}").splitlines() + if not subjects: + raise ValueError("No release changes were found") + notes = (f"\n\n" + "## Patch notes\n\n" + "\n".join(f"- {subject}" for subject in subjects) + + f"\n\n[Full release diff](https://github.com/{REPO}/compare/{stable}...{candidate})\n\n" + "## Merge and verification\n\nOnly Jon merges this PR into main. Agents must not enable auto-merge or perform the stable merge. " + "This candidate is frozen; later agents changes belong to a later batch.\n\n" + "Required CI runs the Windows binding/contract checks, syntax validation, and frozen-candidate metadata check. " + "Review the candidate's included PRs for their affected-user-operation evidence; hosted CI does not run the private model. " + "These patch notes are generated from merged change titles and do not imply additional performance or deployment validation.\n\n" + f"Previous stable source / rollback target: `{stable}`. No deployment, package publication, or data migration is performed by this helper.\n") + out = Path("work/releases") / candidate + out.mkdir(parents=True, exist_ok=True) + body = out / "patch-notes.md" + body.write_text(notes, encoding="utf-8") + branch = f"release/agents-{candidate[:12]}" + remote = command("git", "ls-remote", "origin", f"refs/heads/{branch}") + if remote and remote.split()[0] != candidate: + raise ValueError("Release branch already points elsewhere; refusing to move it") + if not remote: + command("git", "push", "origin", f"{candidate}:refs/heads/{branch}") + url = command("gh", "pr", "create", "--repo", REPO, "--base", "main", "--head", branch, + "--title", "Release verified agents changes to main", "--body-file", str(body)) + receipt = {"release": "ready for Jon after CI", "url": url, "candidate": candidate, "notes": str(body.resolve())} + (out / "receipt.json").write_text(json.dumps(receipt, indent=2), encoding="utf-8") + return receipt + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--candidate") + print(json.dumps(prepare(parser.parse_args().candidate)))