From a27a85a7dffd5c148b56047eec6478582691578f Mon Sep 17 00:00:00 2001 From: GitHub Copilot <223556219+Copilot@users.noreply.github.com> Date: Sat, 28 Feb 2026 04:58:42 +0000 Subject: [PATCH 1/8] Add pre-push ancestry guard for PR branch updates Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../gh-aw-fragments/safe-output-push-to-pr.md | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 github/workflows/gh-aw-fragments/safe-output-push-to-pr.md diff --git a/github/workflows/gh-aw-fragments/safe-output-push-to-pr.md b/github/workflows/gh-aw-fragments/safe-output-push-to-pr.md new file mode 100644 index 00000000..f1c18b95 --- /dev/null +++ b/github/workflows/gh-aw-fragments/safe-output-push-to-pr.md @@ -0,0 +1,72 @@ +--- +safe-inputs: + ready-to-make-pr: + description: "Run the PR readiness checklist before creating or updating a PR" + py: | + import os, json, subprocess + def find(*paths): + return next((p for p in paths if os.path.isfile(p)), None) + def run(cmd): + try: + return subprocess.run(cmd, capture_output=True, text=True, timeout=60) + except subprocess.TimeoutExpired: + return subprocess.CompletedProcess(cmd, 1, stdout='', stderr='diff timed out') + contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') + pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') + # Generate diff of all local changes vs upstream for self-review + # Try --merge-base (committed+staged+unstaged vs upstream), fall back to + # @{upstream} 2-dot (committed only), then HEAD (uncommitted only) + diff_text = '' + for diff_cmd in [ + ['git', 'diff', '--merge-base', '@{upstream}'], + ['git', 'diff', '@{upstream}'], + ['git', 'diff', 'HEAD'], + ]: + result = run(diff_cmd) + if result.stdout.strip(): + diff_text = result.stdout.strip() + break + stat_text = '' + for stat_cmd in [ + ['git', 'diff', '--stat', '--merge-base', '@{upstream}'], + ['git', 'diff', '--stat', '@{upstream}'], + ['git', 'diff', '--stat', 'HEAD'], + ]: + result = run(stat_cmd) + if result.stdout.strip(): + stat_text = result.stdout.strip() + break + os.makedirs('/tmp/self-review', exist_ok=True) + with open('/tmp/self-review/diff.patch', 'w') as f: + f.write(diff_text) + with open('/tmp/self-review/stat.txt', 'w') as f: + f.write(stat_text) + diff_line_count = len(diff_text.splitlines()) + checklist = [] + if contributing: checklist.append(f'Review the contributing guide ({contributing}) before opening or updating a PR.') + if pr_template: checklist.append(f'Follow the PR template ({pr_template}) for title, description, and validation notes.') + checklist.append('Confirm the requested task is fully completed and validated before creating or pushing PR changes.') + if diff_line_count > 0: + checklist.append(f'A diff of your unpushed changes ({diff_line_count} lines) has been saved to `/tmp/self-review/diff.patch` (full diff) and `/tmp/self-review/stat.txt` (summary). Spawn a `code-review` sub-agent via `runSubagent` to review the diff against the codebase. Tell it to read `/tmp/self-review/diff.patch` and the relevant source files, and look for bugs, logic errors, missed edge cases, and style issues. If the sub-agent finds legitimate issues, fix them, commit, and call `ready_to_make_pr` again to regenerate the diff before proceeding.') + print(json.dumps({'status': 'ok', 'checklist': checklist, 'contributing_guide': contributing, 'pr_template': pr_template, 'diff_line_count': diff_line_count})) +safe-outputs: + push-to-pull-request-branch: + github-token-for-extra-empty-commit: ${{ secrets.EXTRA_COMMIT_GITHUB_TOKEN }} +--- + +Before calling `push_to_pull_request_branch`, call `ready_to_make_pr` and apply its checklist. + +## push-to-pull-request-branch Limitations + +- **Patch size**: Max ~10 MB (10,240 KB). Keep changes focused — very large refactors may exceed this. +- **Fork PRs**: Cannot push to fork PR branches. Check via `pull_request_read` with method `get` whether the PR head repo differs from the base repo. If it's a fork, explain that you cannot push and suggest the author apply changes themselves. +- **Committed changes required**: You must have locally committed changes before calling push. Uncommitted or staged-only changes will fail. +- **Branch**: Pushes to the PR's head branch. The workspace must have the PR branch checked out. +- **No history rewrites before push**: Read the original PR head SHA from `/tmp/pr-context/pr.json` and run `git merge-base --is-ancestor "" HEAD` before calling push. If this fails, do not call `push_to_pull_request_branch`; explain that local history was rewritten (for example by rebase/reset/cherry-pick flows) and re-apply the needed changes as direct file edits on top of the existing PR head. +- You may not submit code that modifies files in `.github/workflows/`. Doing so will cause the submission to be rejected. If asked to modify workflow files, propose the change in a copy placed in a `github/` folder (without the leading period) and note in the PR that the file needs to be relocated by someone with workflow write access. + +Trying to resolve merge conflicts? Do NOT create merge commits (commits with multiple parents) — `push_to_pull_request_branch` uses `git format-patch` which breaks on merge commits. This means: no `git merge`, no `git rebase`, no `git commit-tree` with multiple `-p` flags. Instead: +1. Use `git merge-tree` or `git diff` to compare the conflicting files between this PR branch and the PR base branch (read from `/tmp/pr-context/pr.json`) +2. Edit the files directly to incorporate the changes from the base branch +3. Commit the changes as regular (single-parent) commits +4. Use push_to_pull_request_branch to push From 85fe668709b1d5901c7b7ccae36d500bf9b42e01 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Feb 2026 00:09:34 -0600 Subject: [PATCH 2/8] Move pre-push ancestry guard to .github/workflows (activate it) (#469) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: strawgate <6384545+strawgate@users.noreply.github.com> --- .../gh-aw-fragments/safe-output-push-to-pr.md | 1 + .../gh-aw-fragments/safe-output-push-to-pr.md | 72 ------------------- 2 files changed, 1 insertion(+), 72 deletions(-) delete mode 100644 github/workflows/gh-aw-fragments/safe-output-push-to-pr.md diff --git a/.github/workflows/gh-aw-fragments/safe-output-push-to-pr.md b/.github/workflows/gh-aw-fragments/safe-output-push-to-pr.md index cb144c51..f1c18b95 100644 --- a/.github/workflows/gh-aw-fragments/safe-output-push-to-pr.md +++ b/.github/workflows/gh-aw-fragments/safe-output-push-to-pr.md @@ -62,6 +62,7 @@ Before calling `push_to_pull_request_branch`, call `ready_to_make_pr` and apply - **Fork PRs**: Cannot push to fork PR branches. Check via `pull_request_read` with method `get` whether the PR head repo differs from the base repo. If it's a fork, explain that you cannot push and suggest the author apply changes themselves. - **Committed changes required**: You must have locally committed changes before calling push. Uncommitted or staged-only changes will fail. - **Branch**: Pushes to the PR's head branch. The workspace must have the PR branch checked out. +- **No history rewrites before push**: Read the original PR head SHA from `/tmp/pr-context/pr.json` and run `git merge-base --is-ancestor "" HEAD` before calling push. If this fails, do not call `push_to_pull_request_branch`; explain that local history was rewritten (for example by rebase/reset/cherry-pick flows) and re-apply the needed changes as direct file edits on top of the existing PR head. - You may not submit code that modifies files in `.github/workflows/`. Doing so will cause the submission to be rejected. If asked to modify workflow files, propose the change in a copy placed in a `github/` folder (without the leading period) and note in the PR that the file needs to be relocated by someone with workflow write access. Trying to resolve merge conflicts? Do NOT create merge commits (commits with multiple parents) — `push_to_pull_request_branch` uses `git format-patch` which breaks on merge commits. This means: no `git merge`, no `git rebase`, no `git commit-tree` with multiple `-p` flags. Instead: diff --git a/github/workflows/gh-aw-fragments/safe-output-push-to-pr.md b/github/workflows/gh-aw-fragments/safe-output-push-to-pr.md deleted file mode 100644 index f1c18b95..00000000 --- a/github/workflows/gh-aw-fragments/safe-output-push-to-pr.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -safe-inputs: - ready-to-make-pr: - description: "Run the PR readiness checklist before creating or updating a PR" - py: | - import os, json, subprocess - def find(*paths): - return next((p for p in paths if os.path.isfile(p)), None) - def run(cmd): - try: - return subprocess.run(cmd, capture_output=True, text=True, timeout=60) - except subprocess.TimeoutExpired: - return subprocess.CompletedProcess(cmd, 1, stdout='', stderr='diff timed out') - contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') - pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') - # Generate diff of all local changes vs upstream for self-review - # Try --merge-base (committed+staged+unstaged vs upstream), fall back to - # @{upstream} 2-dot (committed only), then HEAD (uncommitted only) - diff_text = '' - for diff_cmd in [ - ['git', 'diff', '--merge-base', '@{upstream}'], - ['git', 'diff', '@{upstream}'], - ['git', 'diff', 'HEAD'], - ]: - result = run(diff_cmd) - if result.stdout.strip(): - diff_text = result.stdout.strip() - break - stat_text = '' - for stat_cmd in [ - ['git', 'diff', '--stat', '--merge-base', '@{upstream}'], - ['git', 'diff', '--stat', '@{upstream}'], - ['git', 'diff', '--stat', 'HEAD'], - ]: - result = run(stat_cmd) - if result.stdout.strip(): - stat_text = result.stdout.strip() - break - os.makedirs('/tmp/self-review', exist_ok=True) - with open('/tmp/self-review/diff.patch', 'w') as f: - f.write(diff_text) - with open('/tmp/self-review/stat.txt', 'w') as f: - f.write(stat_text) - diff_line_count = len(diff_text.splitlines()) - checklist = [] - if contributing: checklist.append(f'Review the contributing guide ({contributing}) before opening or updating a PR.') - if pr_template: checklist.append(f'Follow the PR template ({pr_template}) for title, description, and validation notes.') - checklist.append('Confirm the requested task is fully completed and validated before creating or pushing PR changes.') - if diff_line_count > 0: - checklist.append(f'A diff of your unpushed changes ({diff_line_count} lines) has been saved to `/tmp/self-review/diff.patch` (full diff) and `/tmp/self-review/stat.txt` (summary). Spawn a `code-review` sub-agent via `runSubagent` to review the diff against the codebase. Tell it to read `/tmp/self-review/diff.patch` and the relevant source files, and look for bugs, logic errors, missed edge cases, and style issues. If the sub-agent finds legitimate issues, fix them, commit, and call `ready_to_make_pr` again to regenerate the diff before proceeding.') - print(json.dumps({'status': 'ok', 'checklist': checklist, 'contributing_guide': contributing, 'pr_template': pr_template, 'diff_line_count': diff_line_count})) -safe-outputs: - push-to-pull-request-branch: - github-token-for-extra-empty-commit: ${{ secrets.EXTRA_COMMIT_GITHUB_TOKEN }} ---- - -Before calling `push_to_pull_request_branch`, call `ready_to_make_pr` and apply its checklist. - -## push-to-pull-request-branch Limitations - -- **Patch size**: Max ~10 MB (10,240 KB). Keep changes focused — very large refactors may exceed this. -- **Fork PRs**: Cannot push to fork PR branches. Check via `pull_request_read` with method `get` whether the PR head repo differs from the base repo. If it's a fork, explain that you cannot push and suggest the author apply changes themselves. -- **Committed changes required**: You must have locally committed changes before calling push. Uncommitted or staged-only changes will fail. -- **Branch**: Pushes to the PR's head branch. The workspace must have the PR branch checked out. -- **No history rewrites before push**: Read the original PR head SHA from `/tmp/pr-context/pr.json` and run `git merge-base --is-ancestor "" HEAD` before calling push. If this fails, do not call `push_to_pull_request_branch`; explain that local history was rewritten (for example by rebase/reset/cherry-pick flows) and re-apply the needed changes as direct file edits on top of the existing PR head. -- You may not submit code that modifies files in `.github/workflows/`. Doing so will cause the submission to be rejected. If asked to modify workflow files, propose the change in a copy placed in a `github/` folder (without the leading period) and note in the PR that the file needs to be relocated by someone with workflow write access. - -Trying to resolve merge conflicts? Do NOT create merge commits (commits with multiple parents) — `push_to_pull_request_branch` uses `git format-patch` which breaks on merge commits. This means: no `git merge`, no `git rebase`, no `git commit-tree` with multiple `-p` flags. Instead: -1. Use `git merge-tree` or `git diff` to compare the conflicting files between this PR branch and the PR base branch (read from `/tmp/pr-context/pr.json`) -2. Edit the files directly to incorporate the changes from the base branch -3. Commit the changes as regular (single-parent) commits -4. Use push_to_pull_request_branch to push From af0b7ebf24ad58fb48a8a19e8b2fe123a7899efb Mon Sep 17 00:00:00 2001 From: William Easton Date: Sat, 28 Feb 2026 00:41:21 -0600 Subject: [PATCH 3/8] Add pre-push guards to ready_to_make_pr safe-input Move the ancestry and merge commit checks from prompt-based instructions into the ready_to_make_pr Python script so they are enforced automatically. When the agent calls ready_to_make_pr, the script now: - push-to-pr: checks that the original PR head SHA (from pr.json) is an ancestor of HEAD (detects rebase/reset) and that no merge commits exist since the PR head - create-pr: checks that no merge commits exist since the upstream fork point Both return a status:error JSON with actionable fix instructions. Also adds headRefOid to the pr-context gh pr view fields so the SHA is available in pr.json. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/downstream-users.lock.yml | 18 +++++++++++++- .../workflows/gh-aw-bug-exterminator.lock.yml | 18 +++++++++++++- .../gh-aw-code-duplication-fixer.lock.yml | 18 +++++++++++++- .../workflows/gh-aw-code-simplifier.lock.yml | 18 +++++++++++++- .../workflows/gh-aw-fragments/pr-context.md | 4 ++-- .../gh-aw-fragments/safe-output-create-pr.md | 16 +++++++++++++ .../gh-aw-fragments/safe-output-push-to-pr.md | 21 +++++++++++++++- .github/workflows/gh-aw-issue-fixer.lock.yml | 18 +++++++++++++- ...gh-aw-mention-in-issue-no-sandbox.lock.yml | 18 +++++++++++++- .../workflows/gh-aw-mention-in-issue.lock.yml | 18 +++++++++++++- .../gh-aw-mention-in-pr-by-id.lock.yml | 24 +++++++++++++++++-- .../gh-aw-mention-in-pr-no-sandbox.lock.yml | 24 +++++++++++++++++-- .../workflows/gh-aw-mention-in-pr.lock.yml | 24 +++++++++++++++++-- .../gh-aw-newbie-contributor-fixer.lock.yml | 18 +++++++++++++- .../workflows/gh-aw-pr-actions-fixer.lock.yml | 22 ++++++++++++++++- .../gh-aw-pr-review-addresser.lock.yml | 24 +++++++++++++++++-- .github/workflows/gh-aw-pr-review.lock.yml | 4 ++-- .../workflows/gh-aw-release-update.lock.yml | 18 +++++++++++++- .../workflows/gh-aw-scheduled-fix.lock.yml | 18 +++++++++++++- .../gh-aw-small-problem-fixer.lock.yml | 18 +++++++++++++- .../workflows/gh-aw-test-improvement.lock.yml | 18 +++++++++++++- .../workflows/gh-aw-test-improver.lock.yml | 18 +++++++++++++- .../workflows/gh-aw-text-beautifier.lock.yml | 18 +++++++++++++- 23 files changed, 387 insertions(+), 28 deletions(-) diff --git a/.github/workflows/downstream-users.lock.yml b/.github/workflows/downstream-users.lock.yml index 8a720acb..b5115bd2 100644 --- a/.github/workflows/downstream-users.lock.yml +++ b/.github/workflows/downstream-users.lock.yml @@ -36,7 +36,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"2af4e8b8cdb1ae88a3f4c13292b8b55bca643a5177cbcba3f01564342aa90e75"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"75a0c54b445845fa523a41bd46bdb98c85e1a1a5028213eca1d7a8c37d0f6a5b"} name: "Internal: Downstream Users" "on": @@ -883,6 +883,22 @@ jobs: return subprocess.run(cmd, capture_output=True, text=True, timeout=60) except subprocess.TimeoutExpired: return subprocess.CompletedProcess(cmd, 1, stdout='', stderr='diff timed out') + + # --- Guard: detect merge commits --- + # Find the fork point with the upstream branch to scope the check + upstream_sha = '' + for ref in ['@{upstream}', 'origin/HEAD', 'origin/main']: + r = run(['git', 'merge-base', 'HEAD', ref]) + if r.returncode == 0 and r.stdout.strip(): + upstream_sha = r.stdout.strip() + break + if upstream_sha: + log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) + merge_shas = log.stdout.strip() + if merge_shas: + print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) + raise SystemExit(0) + contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') # Generate diff of all local changes vs upstream for self-review diff --git a/.github/workflows/gh-aw-bug-exterminator.lock.yml b/.github/workflows/gh-aw-bug-exterminator.lock.yml index c399b6ce..eb668fa7 100644 --- a/.github/workflows/gh-aw-bug-exterminator.lock.yml +++ b/.github/workflows/gh-aw-bug-exterminator.lock.yml @@ -37,7 +37,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"218198d7daeb5075e026cb833b2fb8d909f16ed8457059e34022058af9743643"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"56ffeeeb47252fb1cc3a8b75178e9c6f97a33a7f6fb82f53dd2ce084d85df4d5"} name: "Gh Aw Bug Exterminator" "on": @@ -932,6 +932,22 @@ jobs: return subprocess.run(cmd, capture_output=True, text=True, timeout=60) except subprocess.TimeoutExpired: return subprocess.CompletedProcess(cmd, 1, stdout='', stderr='diff timed out') + + # --- Guard: detect merge commits --- + # Find the fork point with the upstream branch to scope the check + upstream_sha = '' + for ref in ['@{upstream}', 'origin/HEAD', 'origin/main']: + r = run(['git', 'merge-base', 'HEAD', ref]) + if r.returncode == 0 and r.stdout.strip(): + upstream_sha = r.stdout.strip() + break + if upstream_sha: + log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) + merge_shas = log.stdout.strip() + if merge_shas: + print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) + raise SystemExit(0) + contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') # Generate diff of all local changes vs upstream for self-review diff --git a/.github/workflows/gh-aw-code-duplication-fixer.lock.yml b/.github/workflows/gh-aw-code-duplication-fixer.lock.yml index 6e6b11ee..febe1257 100644 --- a/.github/workflows/gh-aw-code-duplication-fixer.lock.yml +++ b/.github/workflows/gh-aw-code-duplication-fixer.lock.yml @@ -37,7 +37,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"20c8f1a7ddd18d5b9e5596b052662c695ce9e398012cb5d02b374481e929efc3"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"f4ca568e3a2a661cd29b5a5f4720abec9b7453950ecaeb890f9fe22c464b7a5d"} name: "Code Duplication Fixer" "on": @@ -934,6 +934,22 @@ jobs: return subprocess.run(cmd, capture_output=True, text=True, timeout=60) except subprocess.TimeoutExpired: return subprocess.CompletedProcess(cmd, 1, stdout='', stderr='diff timed out') + + # --- Guard: detect merge commits --- + # Find the fork point with the upstream branch to scope the check + upstream_sha = '' + for ref in ['@{upstream}', 'origin/HEAD', 'origin/main']: + r = run(['git', 'merge-base', 'HEAD', ref]) + if r.returncode == 0 and r.stdout.strip(): + upstream_sha = r.stdout.strip() + break + if upstream_sha: + log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) + merge_shas = log.stdout.strip() + if merge_shas: + print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) + raise SystemExit(0) + contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') # Generate diff of all local changes vs upstream for self-review diff --git a/.github/workflows/gh-aw-code-simplifier.lock.yml b/.github/workflows/gh-aw-code-simplifier.lock.yml index 93fb4944..d5230ab3 100644 --- a/.github/workflows/gh-aw-code-simplifier.lock.yml +++ b/.github/workflows/gh-aw-code-simplifier.lock.yml @@ -37,7 +37,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"1dd217c27d801a10753f45e835ebf26461c5cdb7effaf0d738a18a46c10ad048"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"95b8c433cc8d86f8ac6b7d628f2df4d4295627e128cbc061b01b7ae458a755c9"} name: "Code Simplifier" "on": @@ -949,6 +949,22 @@ jobs: return subprocess.run(cmd, capture_output=True, text=True, timeout=60) except subprocess.TimeoutExpired: return subprocess.CompletedProcess(cmd, 1, stdout='', stderr='diff timed out') + + # --- Guard: detect merge commits --- + # Find the fork point with the upstream branch to scope the check + upstream_sha = '' + for ref in ['@{upstream}', 'origin/HEAD', 'origin/main']: + r = run(['git', 'merge-base', 'HEAD', ref]) + if r.returncode == 0 and r.stdout.strip(): + upstream_sha = r.stdout.strip() + break + if upstream_sha: + log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) + merge_shas = log.stdout.strip() + if merge_shas: + print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) + raise SystemExit(0) + contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') # Generate diff of all local changes vs upstream for self-review diff --git a/.github/workflows/gh-aw-fragments/pr-context.md b/.github/workflows/gh-aw-fragments/pr-context.md index a859c20b..31f01150 100644 --- a/.github/workflows/gh-aw-fragments/pr-context.md +++ b/.github/workflows/gh-aw-fragments/pr-context.md @@ -9,7 +9,7 @@ steps: mkdir -p /tmp/pr-context # PR metadata - gh pr view "$PR_NUMBER" --json title,body,author,baseRefName,headRefName,url \ + gh pr view "$PR_NUMBER" --json title,body,author,baseRefName,headRefName,headRefOid,url \ > /tmp/pr-context/pr.json # Full diff @@ -109,7 +109,7 @@ steps: | File | Description | | --- | --- | - | `pr.json` | PR metadata — title, body, author, base/head branches, URL | + | `pr.json` | PR metadata — title, body, author, base/head branches, head commit SHA (`headRefOid`), URL | | `pr.diff` | Full unified diff of all changes | | `files.json` | Changed files array — each entry has `filename`, `status`, `additions`, `deletions`, `patch` | | `diffs/.diff` | Per-file diffs — one file per changed file, mirroring the repo path under `diffs/` | diff --git a/.github/workflows/gh-aw-fragments/safe-output-create-pr.md b/.github/workflows/gh-aw-fragments/safe-output-create-pr.md index ce43e47d..b454eb23 100644 --- a/.github/workflows/gh-aw-fragments/safe-output-create-pr.md +++ b/.github/workflows/gh-aw-fragments/safe-output-create-pr.md @@ -11,6 +11,22 @@ safe-inputs: return subprocess.run(cmd, capture_output=True, text=True, timeout=60) except subprocess.TimeoutExpired: return subprocess.CompletedProcess(cmd, 1, stdout='', stderr='diff timed out') + + # --- Guard: detect merge commits --- + # Find the fork point with the upstream branch to scope the check + upstream_sha = '' + for ref in ['@{upstream}', 'origin/HEAD', 'origin/main']: + r = run(['git', 'merge-base', 'HEAD', ref]) + if r.returncode == 0 and r.stdout.strip(): + upstream_sha = r.stdout.strip() + break + if upstream_sha: + log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) + merge_shas = log.stdout.strip() + if merge_shas: + print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) + raise SystemExit(0) + contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') # Generate diff of all local changes vs upstream for self-review diff --git a/.github/workflows/gh-aw-fragments/safe-output-push-to-pr.md b/.github/workflows/gh-aw-fragments/safe-output-push-to-pr.md index f1c18b95..efae04dd 100644 --- a/.github/workflows/gh-aw-fragments/safe-output-push-to-pr.md +++ b/.github/workflows/gh-aw-fragments/safe-output-push-to-pr.md @@ -11,6 +11,26 @@ safe-inputs: return subprocess.run(cmd, capture_output=True, text=True, timeout=60) except subprocess.TimeoutExpired: return subprocess.CompletedProcess(cmd, 1, stdout='', stderr='diff timed out') + + # --- Guard: detect history rewrites and merge commits --- + pr_json_path = '/tmp/pr-context/pr.json' + if os.path.isfile(pr_json_path): + with open(pr_json_path) as f: + pr_data = json.load(f) + pr_head_sha = pr_data.get('headRefOid', '') + if pr_head_sha: + # Check 1: PR head must be an ancestor of HEAD (no rebase/reset) + anc = run(['git', 'merge-base', '--is-ancestor', pr_head_sha, 'HEAD']) + if anc.returncode != 0: + print(json.dumps({'status': 'error', 'error': f'History rewrite detected: the original PR head ({pr_head_sha[:12]}) is not an ancestor of HEAD. This means git rebase, reset, or cherry-pick rewrote history. push_to_pull_request_branch will fail. Fix: reset to the PR head with `git checkout {pr_head_sha}`, re-apply your changes as direct file edits, and commit as regular commits.'})) + raise SystemExit(0) + # Check 2: no merge commits (multiple parents) since PR head + log = run(['git', 'rev-list', '--min-parents=2', f'{pr_head_sha}..HEAD']) + merge_shas = log.stdout.strip() + if merge_shas: + print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... push_to_pull_request_branch uses git format-patch which breaks on merge commits. Fix: reset to the PR head with `git checkout {pr_head_sha}`, re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags), and commit as regular single-parent commits.'})) + raise SystemExit(0) + contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') # Generate diff of all local changes vs upstream for self-review @@ -62,7 +82,6 @@ Before calling `push_to_pull_request_branch`, call `ready_to_make_pr` and apply - **Fork PRs**: Cannot push to fork PR branches. Check via `pull_request_read` with method `get` whether the PR head repo differs from the base repo. If it's a fork, explain that you cannot push and suggest the author apply changes themselves. - **Committed changes required**: You must have locally committed changes before calling push. Uncommitted or staged-only changes will fail. - **Branch**: Pushes to the PR's head branch. The workspace must have the PR branch checked out. -- **No history rewrites before push**: Read the original PR head SHA from `/tmp/pr-context/pr.json` and run `git merge-base --is-ancestor "" HEAD` before calling push. If this fails, do not call `push_to_pull_request_branch`; explain that local history was rewritten (for example by rebase/reset/cherry-pick flows) and re-apply the needed changes as direct file edits on top of the existing PR head. - You may not submit code that modifies files in `.github/workflows/`. Doing so will cause the submission to be rejected. If asked to modify workflow files, propose the change in a copy placed in a `github/` folder (without the leading period) and note in the PR that the file needs to be relocated by someone with workflow write access. Trying to resolve merge conflicts? Do NOT create merge commits (commits with multiple parents) — `push_to_pull_request_branch` uses `git format-patch` which breaks on merge commits. This means: no `git merge`, no `git rebase`, no `git commit-tree` with multiple `-p` flags. Instead: diff --git a/.github/workflows/gh-aw-issue-fixer.lock.yml b/.github/workflows/gh-aw-issue-fixer.lock.yml index 02ac88fd..5eabfd21 100644 --- a/.github/workflows/gh-aw-issue-fixer.lock.yml +++ b/.github/workflows/gh-aw-issue-fixer.lock.yml @@ -38,7 +38,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"38f1a8915f4a710c85bb4bb481fcd8f5a16fc304c1ec8975935d366c2eac8f04"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"548c2451749f5e4866f4cebb640738e071869e9415bddc55449bca0dc09b9d25"} name: "Issue Fixer" "on": @@ -982,6 +982,22 @@ jobs: return subprocess.run(cmd, capture_output=True, text=True, timeout=60) except subprocess.TimeoutExpired: return subprocess.CompletedProcess(cmd, 1, stdout='', stderr='diff timed out') + + # --- Guard: detect merge commits --- + # Find the fork point with the upstream branch to scope the check + upstream_sha = '' + for ref in ['@{upstream}', 'origin/HEAD', 'origin/main']: + r = run(['git', 'merge-base', 'HEAD', ref]) + if r.returncode == 0 and r.stdout.strip(): + upstream_sha = r.stdout.strip() + break + if upstream_sha: + log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) + merge_shas = log.stdout.strip() + if merge_shas: + print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) + raise SystemExit(0) + contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') # Generate diff of all local changes vs upstream for self-review diff --git a/.github/workflows/gh-aw-mention-in-issue-no-sandbox.lock.yml b/.github/workflows/gh-aw-mention-in-issue-no-sandbox.lock.yml index 6cb5a8df..1d8ad6d5 100644 --- a/.github/workflows/gh-aw-mention-in-issue-no-sandbox.lock.yml +++ b/.github/workflows/gh-aw-mention-in-issue-no-sandbox.lock.yml @@ -39,7 +39,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"5bfcb667b2eba53298d9d9cca9697fe53c865a0630c15bd0e9a4369b6d1f1731"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"a0312c9ee73a9ed2f28198390731953134a165619ff73028509b16f7487f156f"} name: "Mention in Issue (no sandbox)" "on": @@ -1070,6 +1070,22 @@ jobs: return subprocess.run(cmd, capture_output=True, text=True, timeout=60) except subprocess.TimeoutExpired: return subprocess.CompletedProcess(cmd, 1, stdout='', stderr='diff timed out') + + # --- Guard: detect merge commits --- + # Find the fork point with the upstream branch to scope the check + upstream_sha = '' + for ref in ['@{upstream}', 'origin/HEAD', 'origin/main']: + r = run(['git', 'merge-base', 'HEAD', ref]) + if r.returncode == 0 and r.stdout.strip(): + upstream_sha = r.stdout.strip() + break + if upstream_sha: + log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) + merge_shas = log.stdout.strip() + if merge_shas: + print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) + raise SystemExit(0) + contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') # Generate diff of all local changes vs upstream for self-review diff --git a/.github/workflows/gh-aw-mention-in-issue.lock.yml b/.github/workflows/gh-aw-mention-in-issue.lock.yml index 575e2462..ba30d73a 100644 --- a/.github/workflows/gh-aw-mention-in-issue.lock.yml +++ b/.github/workflows/gh-aw-mention-in-issue.lock.yml @@ -39,7 +39,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"c8178043454e97abbcd7fd7885e3f0144c1ae947263f00ff78296bfc7792e375"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"31bfa189e40c08f467f697aee13817bdb6a3ff1b1a6bbd6f85ffe1e78e0d1b39"} name: "Mention in Issue" "on": @@ -1074,6 +1074,22 @@ jobs: return subprocess.run(cmd, capture_output=True, text=True, timeout=60) except subprocess.TimeoutExpired: return subprocess.CompletedProcess(cmd, 1, stdout='', stderr='diff timed out') + + # --- Guard: detect merge commits --- + # Find the fork point with the upstream branch to scope the check + upstream_sha = '' + for ref in ['@{upstream}', 'origin/HEAD', 'origin/main']: + r = run(['git', 'merge-base', 'HEAD', ref]) + if r.returncode == 0 and r.stdout.strip(): + upstream_sha = r.stdout.strip() + break + if upstream_sha: + log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) + merge_shas = log.stdout.strip() + if merge_shas: + print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) + raise SystemExit(0) + contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') # Generate diff of all local changes vs upstream for self-review diff --git a/.github/workflows/gh-aw-mention-in-pr-by-id.lock.yml b/.github/workflows/gh-aw-mention-in-pr-by-id.lock.yml index 16d73ca7..e3726a86 100644 --- a/.github/workflows/gh-aw-mention-in-pr-by-id.lock.yml +++ b/.github/workflows/gh-aw-mention-in-pr-by-id.lock.yml @@ -43,7 +43,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"f043fc0691132592cc78f7aa21ce5968eddce5733a24562b6dd4ee676e32744c"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"d3fd483a5228aa1877ae86b4bc4290440568738e5c08e318a9f37a78cf46d4d7"} name: "Mention in PR by ID" "on": @@ -652,7 +652,7 @@ jobs: GH_TOKEN: ${{ github.token }} PR_NUMBER: ${{ github.event.pull_request.number || inputs.target-pr-number || github.event.issue.number }} name: Fetch PR context to disk - run: "set -euo pipefail\nmkdir -p /tmp/pr-context\n\n# PR metadata\ngh pr view \"$PR_NUMBER\" --json title,body,author,baseRefName,headRefName,url \\\n > /tmp/pr-context/pr.json\n\n# Full diff\nif ! gh pr diff \"$PR_NUMBER\" > /tmp/pr-context/pr.diff; then\n echo \"::warning::Failed to fetch full PR diff; per-file diffs from files.json are still available.\"\n : > /tmp/pr-context/pr.diff\nfi\n\n# Changed files list (--paginate may output concatenated arrays; jq -s 'add' merges them)\ngh api \"repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/files\" --paginate \\\n | jq -s 'add // []' > /tmp/pr-context/files.json\n\n# Per-file diffs\njq -c '.[]' /tmp/pr-context/files.json | while IFS= read -r entry; do\n filename=$(echo \"$entry\" | jq -r '.filename')\n mkdir -p \"/tmp/pr-context/diffs/$(dirname \"$filename\")\"\n echo \"$entry\" | jq -r '.patch // empty' > \"/tmp/pr-context/diffs/${filename}.diff\"\ndone\n\n# File orderings for sub-agent review (3 strategies)\njq -r '[.[] | .filename] | sort | .[]' /tmp/pr-context/files.json \\\n > /tmp/pr-context/file_order_az.txt\njq -r '[.[] | .filename] | sort | reverse | .[]' /tmp/pr-context/files.json \\\n > /tmp/pr-context/file_order_za.txt\njq -r '[.[] | {filename, size: ((.additions // 0) + (.deletions // 0))}] | sort_by(-.size) | .[].filename' /tmp/pr-context/files.json \\\n > /tmp/pr-context/file_order_largest.txt\n\n# Existing reviews\ngh api \"repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/reviews\" --paginate \\\n | jq -s 'add // []' > /tmp/pr-context/reviews.json\n\n# Review threads with resolution status (GraphQL — REST lacks isResolved/isOutdated)\ngh api graphql --paginate -f query='\n query($owner: String!, $repo: String!, $number: Int!, $endCursor: String) {\n repository(owner: $owner, name: $repo) {\n pullRequest(number: $number) {\n reviewThreads(first: 100, after: $endCursor) {\n pageInfo { hasNextPage endCursor }\n nodes {\n id\n isResolved\n isOutdated\n isCollapsed\n path\n line\n startLine\n comments(first: 100) {\n nodes {\n id\n databaseId\n body\n author { login }\n createdAt\n }\n }\n }\n }\n }\n }\n }\n' -F owner=\"${GITHUB_REPOSITORY%/*}\" -F repo=\"${GITHUB_REPOSITORY#*/}\" -F \"number=$PR_NUMBER\" \\\n --jq '.data.repository.pullRequest.reviewThreads.nodes' \\\n | jq -s 'add // []' > /tmp/pr-context/review_comments.json\n\n# Per-file review threads (mirrors diffs/ structure)\njq -c '.[]' /tmp/pr-context/review_comments.json | while IFS= read -r thread; do\n filepath=$(echo \"$thread\" | jq -r '.path // empty')\n [ -z \"$filepath\" ] && continue\n mkdir -p \"/tmp/pr-context/threads/$(dirname \"$filepath\")\"\n echo \"$thread\" >> \"/tmp/pr-context/threads/${filepath}.jsonl\"\ndone\n# Convert per-file JSONL to proper JSON arrays\nmkdir -p /tmp/pr-context/threads\nfind /tmp/pr-context/threads -name '*.jsonl' 2>/dev/null | while IFS= read -r jsonl; do\n jq -s '.' \"$jsonl\" > \"${jsonl%.jsonl}.json\"\n rm \"$jsonl\"\ndone\n\n# PR discussion comments\ngh api \"repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments\" --paginate \\\n | jq -s 'add // []' > /tmp/pr-context/comments.json\n\n# Linked issues\njq -r '.body // \"\"' /tmp/pr-context/pr.json 2>/dev/null \\\n | grep -oiE '(fixes|closes|resolves)\\s+#[0-9]+' \\\n | grep -oE '[0-9]+$' \\\n | sort -u \\\n | while read -r issue; do\n gh api \"repos/$GITHUB_REPOSITORY/issues/$issue\" > \"/tmp/pr-context/issue-${issue}.json\" || true\n done || true\n\n# Write manifest\ncat > /tmp/pr-context/README.md << 'MANIFEST'\n# PR Context\n\nPre-fetched PR data. All files are in `/tmp/pr-context/`.\n\n| File | Description |\n| --- | --- |\n| `pr.json` | PR metadata — title, body, author, base/head branches, URL |\n| `pr.diff` | Full unified diff of all changes |\n| `files.json` | Changed files array — each entry has `filename`, `status`, `additions`, `deletions`, `patch` |\n| `diffs/.diff` | Per-file diffs — one file per changed file, mirroring the repo path under `diffs/` |\n| `file_order_az.txt` | Changed files sorted alphabetically (A→Z), one filename per line |\n| `file_order_za.txt` | Changed files sorted reverse-alphabetically (Z→A), one filename per line |\n| `file_order_largest.txt` | Changed files sorted by diff size descending (largest first), one filename per line |\n| `reviews.json` | Prior review submissions — author, state (APPROVED/CHANGES_REQUESTED/COMMENTED), body |\n| `review_comments.json` | All review threads (GraphQL) — each thread has `id` (node ID for resolving), `isResolved`, `isOutdated`, `path`, `line`, and nested `comments` with `id`, `databaseId` (numeric REST ID for replies), body/author |\n| `threads/.json` | Per-file review threads — one file per changed file with existing threads, mirroring the repo path under `threads/` |\n| `comments.json` | PR discussion comments (not inline) |\n| `issue-{N}.json` | Linked issue details (one file per linked issue, if any) |\n| `agents.md` | Repository conventions from `generate_agents_md` (if written by agent) |\n| `review-instructions.md` | Review instructions, criteria, and calibration examples (if written by review-process fragment) |\nMANIFEST\n\necho \"PR context written to /tmp/pr-context/\"\nls -la /tmp/pr-context/" + run: "set -euo pipefail\nmkdir -p /tmp/pr-context\n\n# PR metadata\ngh pr view \"$PR_NUMBER\" --json title,body,author,baseRefName,headRefName,headRefOid,url \\\n > /tmp/pr-context/pr.json\n\n# Full diff\nif ! gh pr diff \"$PR_NUMBER\" > /tmp/pr-context/pr.diff; then\n echo \"::warning::Failed to fetch full PR diff; per-file diffs from files.json are still available.\"\n : > /tmp/pr-context/pr.diff\nfi\n\n# Changed files list (--paginate may output concatenated arrays; jq -s 'add' merges them)\ngh api \"repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/files\" --paginate \\\n | jq -s 'add // []' > /tmp/pr-context/files.json\n\n# Per-file diffs\njq -c '.[]' /tmp/pr-context/files.json | while IFS= read -r entry; do\n filename=$(echo \"$entry\" | jq -r '.filename')\n mkdir -p \"/tmp/pr-context/diffs/$(dirname \"$filename\")\"\n echo \"$entry\" | jq -r '.patch // empty' > \"/tmp/pr-context/diffs/${filename}.diff\"\ndone\n\n# File orderings for sub-agent review (3 strategies)\njq -r '[.[] | .filename] | sort | .[]' /tmp/pr-context/files.json \\\n > /tmp/pr-context/file_order_az.txt\njq -r '[.[] | .filename] | sort | reverse | .[]' /tmp/pr-context/files.json \\\n > /tmp/pr-context/file_order_za.txt\njq -r '[.[] | {filename, size: ((.additions // 0) + (.deletions // 0))}] | sort_by(-.size) | .[].filename' /tmp/pr-context/files.json \\\n > /tmp/pr-context/file_order_largest.txt\n\n# Existing reviews\ngh api \"repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/reviews\" --paginate \\\n | jq -s 'add // []' > /tmp/pr-context/reviews.json\n\n# Review threads with resolution status (GraphQL — REST lacks isResolved/isOutdated)\ngh api graphql --paginate -f query='\n query($owner: String!, $repo: String!, $number: Int!, $endCursor: String) {\n repository(owner: $owner, name: $repo) {\n pullRequest(number: $number) {\n reviewThreads(first: 100, after: $endCursor) {\n pageInfo { hasNextPage endCursor }\n nodes {\n id\n isResolved\n isOutdated\n isCollapsed\n path\n line\n startLine\n comments(first: 100) {\n nodes {\n id\n databaseId\n body\n author { login }\n createdAt\n }\n }\n }\n }\n }\n }\n }\n' -F owner=\"${GITHUB_REPOSITORY%/*}\" -F repo=\"${GITHUB_REPOSITORY#*/}\" -F \"number=$PR_NUMBER\" \\\n --jq '.data.repository.pullRequest.reviewThreads.nodes' \\\n | jq -s 'add // []' > /tmp/pr-context/review_comments.json\n\n# Per-file review threads (mirrors diffs/ structure)\njq -c '.[]' /tmp/pr-context/review_comments.json | while IFS= read -r thread; do\n filepath=$(echo \"$thread\" | jq -r '.path // empty')\n [ -z \"$filepath\" ] && continue\n mkdir -p \"/tmp/pr-context/threads/$(dirname \"$filepath\")\"\n echo \"$thread\" >> \"/tmp/pr-context/threads/${filepath}.jsonl\"\ndone\n# Convert per-file JSONL to proper JSON arrays\nmkdir -p /tmp/pr-context/threads\nfind /tmp/pr-context/threads -name '*.jsonl' 2>/dev/null | while IFS= read -r jsonl; do\n jq -s '.' \"$jsonl\" > \"${jsonl%.jsonl}.json\"\n rm \"$jsonl\"\ndone\n\n# PR discussion comments\ngh api \"repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments\" --paginate \\\n | jq -s 'add // []' > /tmp/pr-context/comments.json\n\n# Linked issues\njq -r '.body // \"\"' /tmp/pr-context/pr.json 2>/dev/null \\\n | grep -oiE '(fixes|closes|resolves)\\s+#[0-9]+' \\\n | grep -oE '[0-9]+$' \\\n | sort -u \\\n | while read -r issue; do\n gh api \"repos/$GITHUB_REPOSITORY/issues/$issue\" > \"/tmp/pr-context/issue-${issue}.json\" || true\n done || true\n\n# Write manifest\ncat > /tmp/pr-context/README.md << 'MANIFEST'\n# PR Context\n\nPre-fetched PR data. All files are in `/tmp/pr-context/`.\n\n| File | Description |\n| --- | --- |\n| `pr.json` | PR metadata — title, body, author, base/head branches, head commit SHA (`headRefOid`), URL |\n| `pr.diff` | Full unified diff of all changes |\n| `files.json` | Changed files array — each entry has `filename`, `status`, `additions`, `deletions`, `patch` |\n| `diffs/.diff` | Per-file diffs — one file per changed file, mirroring the repo path under `diffs/` |\n| `file_order_az.txt` | Changed files sorted alphabetically (A→Z), one filename per line |\n| `file_order_za.txt` | Changed files sorted reverse-alphabetically (Z→A), one filename per line |\n| `file_order_largest.txt` | Changed files sorted by diff size descending (largest first), one filename per line |\n| `reviews.json` | Prior review submissions — author, state (APPROVED/CHANGES_REQUESTED/COMMENTED), body |\n| `review_comments.json` | All review threads (GraphQL) — each thread has `id` (node ID for resolving), `isResolved`, `isOutdated`, `path`, `line`, and nested `comments` with `id`, `databaseId` (numeric REST ID for replies), body/author |\n| `threads/.json` | Per-file review threads — one file per changed file with existing threads, mirroring the repo path under `threads/` |\n| `comments.json` | PR discussion comments (not inline) |\n| `issue-{N}.json` | Linked issue details (one file per linked issue, if any) |\n| `agents.md` | Repository conventions from `generate_agents_md` (if written by agent) |\n| `review-instructions.md` | Review instructions, criteria, and calibration examples (if written by review-process fragment) |\nMANIFEST\n\necho \"PR context written to /tmp/pr-context/\"\nls -la /tmp/pr-context/" - name: Write review instructions to disk run: "mkdir -p /tmp/pr-context\ncat > /tmp/pr-context/review-instructions.md << 'REVIEW_EOF'\n# Review Instructions for Sub-agents\n\nYou are a code review sub-agent. Read these instructions, then review the PR files in the order provided in your prompt.\n\n## Context\n\nBefore reviewing files, read these to understand the PR:\n\n1. `/tmp/pr-context/pr.json` — PR title, description, author, and branches. Understand what the PR is trying to accomplish.\n2. `/tmp/pr-context/agents.md` — Repository coding conventions and guidelines (if it exists).\n3. `/tmp/pr-context/review_comments.json` — Existing review threads. Note which files already have threads so you don't duplicate.\n4. `/tmp/pr-context/issue-*.json` — Linked issue details (if any). Understand the motivation and acceptance criteria.\n\n## Process\n\nReview the PR diff file by file in your assigned order. For each changed file:\n\n1. **Read the diff** for this file from `/tmp/pr-context/diffs/.diff` to understand what changed. If the diff is empty or truncated (e.g., binary files or very large changes), fall back to reading the full file from the workspace and comparing against context.\n2. **Read the full file from the workspace.** The PR branch is checked out locally — open the file directly to get complete contents with line numbers.\n3. **Check existing threads** for this file from `/tmp/pr-context/threads/.json` (if it exists). Skip issues that are already under discussion — each thread has `isResolved` and `isOutdated` fields.\n4. **Identify potential issues** matching the review criteria below.\n5. **Quick-check each issue** before including it:\n - What specific code pattern or change triggers this concern?\n - Is there an obvious guard, handler, or mitigation visible in the immediate context?\n - Can you describe a concrete failure scenario (the `evidence` field)? If you cannot articulate what specific input or state triggers the problem, drop the finding.\n - If the issue is clearly handled, skip it. If you're unsure, include it — the parent will verify.\n6. **Add to your findings list.** Do NOT leave inline comments — you don't have that tool. Return findings in this format:\n\n```\n- file: path/to/file\n line: 42\n severity: HIGH\n title: Brief title\n description: What the issue is and why it matters\n evidence: The specific code pattern and failure scenario\n suggestion: corrected code here (optional — only if you can provide a concrete fix)\n```\n\n**Review every file in your assigned order.** Files reviewed earlier get more attention, which is why different sub-agents use different orderings.\n\n**Check existing threads** — per-file threads are at `/tmp/pr-context/threads/.json` (step 3 above). The full list is at `/tmp/pr-context/review_comments.json`. Do not flag issues that are already under discussion (resolved or unresolved). For outdated threads, only re-flag if the issue still applies to the current diff.\n\n**Return your full findings list** when done, or an empty list if no issues were found.\n\n## Review Criteria\n\nFocus on these categories in priority order:\n\n1. Security vulnerabilities (injection, XSS, auth bypass, secrets exposure)\n2. Logic bugs that could cause runtime failures or incorrect behavior\n3. Data integrity issues (race conditions, missing transactions, corruption risk)\n4. Performance bottlenecks (N+1 queries, memory leaks, blocking operations)\n5. Error handling gaps (unhandled exceptions, missing validation)\n6. Breaking changes to public APIs without migration path\n7. Missing or incorrect test coverage for critical paths\n\n## What NOT to Flag\n\nOnly review the diff — do not flag issues in unchanged code, pre-existing problems not introduced by this PR, or style preferences handled by linters or formatters.\n\n**Common false positives** — these patterns look like issues but usually aren't. Before flagging anything in these categories, confirm the problem is real by reading the surrounding code:\n\n- **Security — input already sanitized:** Don't flag injection or XSS risks when inputs are sanitized upstream, parameterized queries are used, or the framework auto-escapes output.\n- **Null/undefined — guarded elsewhere:** Don't flag potential null dereferences if the value is guaranteed by a type guard, assertion, schema validation, or upstream null check.\n- **Error handling — handled at a different layer:** Don't flag missing try/catch if the caller, middleware, or framework catches and handles the error (e.g., Express error middleware, React error boundaries).\n- **Performance — theoretical, not practical:** Don't flag algorithmic complexity (e.g., O(n^2)) unless N is demonstrably large enough to matter in the actual usage context. \"This could be slow\" without evidence is not actionable.\n- **Validation — exists at another layer:** Don't flag missing input validation if it's handled by an API gateway, middleware, schema validator, or type system.\n- **Test coverage — trivial or generated code:** Don't flag missing tests for trivial getters/setters, auto-generated code, or simple delegation methods.\n- **Style or naming — not in coding guidelines:** Don't flag naming conventions or code style unless they violate the repository's documented coding guidelines (from `generate_agents_md` or CONTRIBUTING docs).\n\n**Existing review threads** — check BEFORE flagging any issue:\n\n- **Resolved with reviewer reply** (e.g. \"This is intentional\") — reviewer's decision is final. Do NOT re-flag.\n- **Resolved without reply** — author likely fixed it. Do NOT re-raise unless the fix introduced a new problem.\n- **Unresolved** — already flagged. Do NOT duplicate.\n- **Outdated** — only re-flag if the issue still applies to the current diff.\n\nWhen in doubt, do not duplicate. Redundant comments erode trust.\n\nFinding no issues is a valid and valuable outcome. An empty findings list is better than findings that waste the author's time or erode trust. Do not manufacture findings to justify your review — if the code is sound, return an empty list.\n\n## Severity Classification\n\nDetermine severity AFTER investigating the issue, not before. First identify the problem and trace through the code, then assign a severity based on the evidence you found.\n\n- 🔴 CRITICAL — Must fix before merge (security vulnerabilities, data corruption, production-breaking bugs)\n- 🟠 HIGH — Should fix before merge (logic errors, missing validation, significant performance issues)\n- 🟡 MEDIUM — Address soon, non-blocking (error handling gaps, suboptimal patterns, missing edge cases)\n- ⚪ LOW — Author discretion (minor improvements, documentation gaps)\n- 💬 NITPICK — Truly optional (stylistic preferences, alternative approaches)\n\n## Review Intensity\n\nThe review intensity is `${{ inputs.intensity || 'balanced' }}`.\n\n- **conservative**: High evidence bar. Only flag when you can demonstrate a concrete failure scenario. If you can construct a reasonable counterargument, do not flag. Approval with zero findings is the expected outcome for most PRs.\n- **balanced**: Standard evidence bar. Flag when you can point to specific code that would fail. If the issue is ambiguous, lean toward not flagging.\n- **aggressive**: Lower evidence bar. Flag when evidence exists even if the failure scenario is not fully confirmed. Improvement suggestions welcome but must cite specific code.\n\n## Calibration Examples\n\nUse these examples to calibrate your judgment. Each pair shows a real issue and a similar-looking pattern that is NOT an issue.\n\n### Example 1: Null/Undefined Access\n\n**True positive — flag this:**\n\n```js\n// PR adds this handler\napp.get('/user/:id', async (req, res) => {\n const user = await db.findUser(req.params.id);\n res.json({ name: user.name, email: user.email });\n});\n```\n\nWhy flag: `db.findUser()` can return `null` when no user matches the ID. Accessing `user.name` will throw a TypeError at runtime. No upstream guard exists — the route handler is the entry point.\n\n**False positive — do NOT flag this:**\n\n```ts\n// PR adds this line inside an existing function\nconst settings = user.getSettings();\n```\n\nWhy skip: Reading the full file reveals `user` is typed as `User` (not `User | null`), and the calling function only runs after `authenticateUser()` middleware which guarantees a valid user object. The null case is handled at a different layer.\n\n### Example 2: SQL Injection\n\n**True positive — flag this:**\n\n```python\n# PR adds this query\ncursor.execute(f\"SELECT * FROM orders WHERE customer_id = '{customer_id}'\")\n```\n\nWhy flag: String interpolation in a SQL query with user-controlled input (`customer_id` comes from the request). No parameterization or sanitization anywhere in the call chain.\n\n**False positive — do NOT flag this:**\n\n```python\n# PR adds this query\ncursor.execute(f\"SELECT * FROM orders WHERE status = '{OrderStatus.PENDING.value}'\")\n```\n\nWhy skip: The interpolated value is a hardcoded enum constant (`OrderStatus.PENDING`), not user input. There is no injection vector.\n\n### Example 3: Borderline — Do NOT Flag\n\n```go\n// PR adds this function\nfunc processItems(items []Item) []Result {\n results := make([]Result, 0)\n for _, item := range items {\n for _, tag := range item.Tags {\n results = append(results, process(item, tag))\n }\n }\n return results\n}\n```\n\nThis looks like an O(n*m) performance concern. But without evidence that `items` or `Tags` are large in practice, this is speculative. The function processes a bounded dataset (items from a single user request). Do not flag theoretical performance issues without evidence of real-world impact.\nREVIEW_EOF" - env: @@ -1230,6 +1230,26 @@ jobs: return subprocess.run(cmd, capture_output=True, text=True, timeout=60) except subprocess.TimeoutExpired: return subprocess.CompletedProcess(cmd, 1, stdout='', stderr='diff timed out') + + # --- Guard: detect history rewrites and merge commits --- + pr_json_path = '/tmp/pr-context/pr.json' + if os.path.isfile(pr_json_path): + with open(pr_json_path) as f: + pr_data = json.load(f) + pr_head_sha = pr_data.get('headRefOid', '') + if pr_head_sha: + # Check 1: PR head must be an ancestor of HEAD (no rebase/reset) + anc = run(['git', 'merge-base', '--is-ancestor', pr_head_sha, 'HEAD']) + if anc.returncode != 0: + print(json.dumps({'status': 'error', 'error': f'History rewrite detected: the original PR head ({pr_head_sha[:12]}) is not an ancestor of HEAD. This means git rebase, reset, or cherry-pick rewrote history. push_to_pull_request_branch will fail. Fix: reset to the PR head with `git checkout {pr_head_sha}`, re-apply your changes as direct file edits, and commit as regular commits.'})) + raise SystemExit(0) + # Check 2: no merge commits (multiple parents) since PR head + log = run(['git', 'rev-list', '--min-parents=2', f'{pr_head_sha}..HEAD']) + merge_shas = log.stdout.strip() + if merge_shas: + print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... push_to_pull_request_branch uses git format-patch which breaks on merge commits. Fix: reset to the PR head with `git checkout {pr_head_sha}`, re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags), and commit as regular single-parent commits.'})) + raise SystemExit(0) + contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') # Generate diff of all local changes vs upstream for self-review diff --git a/.github/workflows/gh-aw-mention-in-pr-no-sandbox.lock.yml b/.github/workflows/gh-aw-mention-in-pr-no-sandbox.lock.yml index 4a37f85e..b52ed1f6 100644 --- a/.github/workflows/gh-aw-mention-in-pr-no-sandbox.lock.yml +++ b/.github/workflows/gh-aw-mention-in-pr-no-sandbox.lock.yml @@ -44,7 +44,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"764aa68cced612fafebc99df7d93376341b5031834e5c0e6363a66671047ebef"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"fce25679276da6f63524ad7cb35eae8207001cf0f96e8312f33bd460276d7847"} name: "Mention in PR (no sandbox)" "on": @@ -726,7 +726,7 @@ jobs: GH_TOKEN: ${{ github.token }} PR_NUMBER: ${{ github.event.pull_request.number || inputs.target-pr-number || github.event.issue.number }} name: Fetch PR context to disk - run: "set -euo pipefail\nmkdir -p /tmp/pr-context\n\n# PR metadata\ngh pr view \"$PR_NUMBER\" --json title,body,author,baseRefName,headRefName,url \\\n > /tmp/pr-context/pr.json\n\n# Full diff\nif ! gh pr diff \"$PR_NUMBER\" > /tmp/pr-context/pr.diff; then\n echo \"::warning::Failed to fetch full PR diff; per-file diffs from files.json are still available.\"\n : > /tmp/pr-context/pr.diff\nfi\n\n# Changed files list (--paginate may output concatenated arrays; jq -s 'add' merges them)\ngh api \"repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/files\" --paginate \\\n | jq -s 'add // []' > /tmp/pr-context/files.json\n\n# Per-file diffs\njq -c '.[]' /tmp/pr-context/files.json | while IFS= read -r entry; do\n filename=$(echo \"$entry\" | jq -r '.filename')\n mkdir -p \"/tmp/pr-context/diffs/$(dirname \"$filename\")\"\n echo \"$entry\" | jq -r '.patch // empty' > \"/tmp/pr-context/diffs/${filename}.diff\"\ndone\n\n# File orderings for sub-agent review (3 strategies)\njq -r '[.[] | .filename] | sort | .[]' /tmp/pr-context/files.json \\\n > /tmp/pr-context/file_order_az.txt\njq -r '[.[] | .filename] | sort | reverse | .[]' /tmp/pr-context/files.json \\\n > /tmp/pr-context/file_order_za.txt\njq -r '[.[] | {filename, size: ((.additions // 0) + (.deletions // 0))}] | sort_by(-.size) | .[].filename' /tmp/pr-context/files.json \\\n > /tmp/pr-context/file_order_largest.txt\n\n# Existing reviews\ngh api \"repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/reviews\" --paginate \\\n | jq -s 'add // []' > /tmp/pr-context/reviews.json\n\n# Review threads with resolution status (GraphQL — REST lacks isResolved/isOutdated)\ngh api graphql --paginate -f query='\n query($owner: String!, $repo: String!, $number: Int!, $endCursor: String) {\n repository(owner: $owner, name: $repo) {\n pullRequest(number: $number) {\n reviewThreads(first: 100, after: $endCursor) {\n pageInfo { hasNextPage endCursor }\n nodes {\n id\n isResolved\n isOutdated\n isCollapsed\n path\n line\n startLine\n comments(first: 100) {\n nodes {\n id\n databaseId\n body\n author { login }\n createdAt\n }\n }\n }\n }\n }\n }\n }\n' -F owner=\"${GITHUB_REPOSITORY%/*}\" -F repo=\"${GITHUB_REPOSITORY#*/}\" -F \"number=$PR_NUMBER\" \\\n --jq '.data.repository.pullRequest.reviewThreads.nodes' \\\n | jq -s 'add // []' > /tmp/pr-context/review_comments.json\n\n# Per-file review threads (mirrors diffs/ structure)\njq -c '.[]' /tmp/pr-context/review_comments.json | while IFS= read -r thread; do\n filepath=$(echo \"$thread\" | jq -r '.path // empty')\n [ -z \"$filepath\" ] && continue\n mkdir -p \"/tmp/pr-context/threads/$(dirname \"$filepath\")\"\n echo \"$thread\" >> \"/tmp/pr-context/threads/${filepath}.jsonl\"\ndone\n# Convert per-file JSONL to proper JSON arrays\nmkdir -p /tmp/pr-context/threads\nfind /tmp/pr-context/threads -name '*.jsonl' 2>/dev/null | while IFS= read -r jsonl; do\n jq -s '.' \"$jsonl\" > \"${jsonl%.jsonl}.json\"\n rm \"$jsonl\"\ndone\n\n# PR discussion comments\ngh api \"repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments\" --paginate \\\n | jq -s 'add // []' > /tmp/pr-context/comments.json\n\n# Linked issues\njq -r '.body // \"\"' /tmp/pr-context/pr.json 2>/dev/null \\\n | grep -oiE '(fixes|closes|resolves)\\s+#[0-9]+' \\\n | grep -oE '[0-9]+$' \\\n | sort -u \\\n | while read -r issue; do\n gh api \"repos/$GITHUB_REPOSITORY/issues/$issue\" > \"/tmp/pr-context/issue-${issue}.json\" || true\n done || true\n\n# Write manifest\ncat > /tmp/pr-context/README.md << 'MANIFEST'\n# PR Context\n\nPre-fetched PR data. All files are in `/tmp/pr-context/`.\n\n| File | Description |\n| --- | --- |\n| `pr.json` | PR metadata — title, body, author, base/head branches, URL |\n| `pr.diff` | Full unified diff of all changes |\n| `files.json` | Changed files array — each entry has `filename`, `status`, `additions`, `deletions`, `patch` |\n| `diffs/.diff` | Per-file diffs — one file per changed file, mirroring the repo path under `diffs/` |\n| `file_order_az.txt` | Changed files sorted alphabetically (A→Z), one filename per line |\n| `file_order_za.txt` | Changed files sorted reverse-alphabetically (Z→A), one filename per line |\n| `file_order_largest.txt` | Changed files sorted by diff size descending (largest first), one filename per line |\n| `reviews.json` | Prior review submissions — author, state (APPROVED/CHANGES_REQUESTED/COMMENTED), body |\n| `review_comments.json` | All review threads (GraphQL) — each thread has `id` (node ID for resolving), `isResolved`, `isOutdated`, `path`, `line`, and nested `comments` with `id`, `databaseId` (numeric REST ID for replies), body/author |\n| `threads/.json` | Per-file review threads — one file per changed file with existing threads, mirroring the repo path under `threads/` |\n| `comments.json` | PR discussion comments (not inline) |\n| `issue-{N}.json` | Linked issue details (one file per linked issue, if any) |\n| `agents.md` | Repository conventions from `generate_agents_md` (if written by agent) |\n| `review-instructions.md` | Review instructions, criteria, and calibration examples (if written by review-process fragment) |\nMANIFEST\n\necho \"PR context written to /tmp/pr-context/\"\nls -la /tmp/pr-context/" + run: "set -euo pipefail\nmkdir -p /tmp/pr-context\n\n# PR metadata\ngh pr view \"$PR_NUMBER\" --json title,body,author,baseRefName,headRefName,headRefOid,url \\\n > /tmp/pr-context/pr.json\n\n# Full diff\nif ! gh pr diff \"$PR_NUMBER\" > /tmp/pr-context/pr.diff; then\n echo \"::warning::Failed to fetch full PR diff; per-file diffs from files.json are still available.\"\n : > /tmp/pr-context/pr.diff\nfi\n\n# Changed files list (--paginate may output concatenated arrays; jq -s 'add' merges them)\ngh api \"repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/files\" --paginate \\\n | jq -s 'add // []' > /tmp/pr-context/files.json\n\n# Per-file diffs\njq -c '.[]' /tmp/pr-context/files.json | while IFS= read -r entry; do\n filename=$(echo \"$entry\" | jq -r '.filename')\n mkdir -p \"/tmp/pr-context/diffs/$(dirname \"$filename\")\"\n echo \"$entry\" | jq -r '.patch // empty' > \"/tmp/pr-context/diffs/${filename}.diff\"\ndone\n\n# File orderings for sub-agent review (3 strategies)\njq -r '[.[] | .filename] | sort | .[]' /tmp/pr-context/files.json \\\n > /tmp/pr-context/file_order_az.txt\njq -r '[.[] | .filename] | sort | reverse | .[]' /tmp/pr-context/files.json \\\n > /tmp/pr-context/file_order_za.txt\njq -r '[.[] | {filename, size: ((.additions // 0) + (.deletions // 0))}] | sort_by(-.size) | .[].filename' /tmp/pr-context/files.json \\\n > /tmp/pr-context/file_order_largest.txt\n\n# Existing reviews\ngh api \"repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/reviews\" --paginate \\\n | jq -s 'add // []' > /tmp/pr-context/reviews.json\n\n# Review threads with resolution status (GraphQL — REST lacks isResolved/isOutdated)\ngh api graphql --paginate -f query='\n query($owner: String!, $repo: String!, $number: Int!, $endCursor: String) {\n repository(owner: $owner, name: $repo) {\n pullRequest(number: $number) {\n reviewThreads(first: 100, after: $endCursor) {\n pageInfo { hasNextPage endCursor }\n nodes {\n id\n isResolved\n isOutdated\n isCollapsed\n path\n line\n startLine\n comments(first: 100) {\n nodes {\n id\n databaseId\n body\n author { login }\n createdAt\n }\n }\n }\n }\n }\n }\n }\n' -F owner=\"${GITHUB_REPOSITORY%/*}\" -F repo=\"${GITHUB_REPOSITORY#*/}\" -F \"number=$PR_NUMBER\" \\\n --jq '.data.repository.pullRequest.reviewThreads.nodes' \\\n | jq -s 'add // []' > /tmp/pr-context/review_comments.json\n\n# Per-file review threads (mirrors diffs/ structure)\njq -c '.[]' /tmp/pr-context/review_comments.json | while IFS= read -r thread; do\n filepath=$(echo \"$thread\" | jq -r '.path // empty')\n [ -z \"$filepath\" ] && continue\n mkdir -p \"/tmp/pr-context/threads/$(dirname \"$filepath\")\"\n echo \"$thread\" >> \"/tmp/pr-context/threads/${filepath}.jsonl\"\ndone\n# Convert per-file JSONL to proper JSON arrays\nmkdir -p /tmp/pr-context/threads\nfind /tmp/pr-context/threads -name '*.jsonl' 2>/dev/null | while IFS= read -r jsonl; do\n jq -s '.' \"$jsonl\" > \"${jsonl%.jsonl}.json\"\n rm \"$jsonl\"\ndone\n\n# PR discussion comments\ngh api \"repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments\" --paginate \\\n | jq -s 'add // []' > /tmp/pr-context/comments.json\n\n# Linked issues\njq -r '.body // \"\"' /tmp/pr-context/pr.json 2>/dev/null \\\n | grep -oiE '(fixes|closes|resolves)\\s+#[0-9]+' \\\n | grep -oE '[0-9]+$' \\\n | sort -u \\\n | while read -r issue; do\n gh api \"repos/$GITHUB_REPOSITORY/issues/$issue\" > \"/tmp/pr-context/issue-${issue}.json\" || true\n done || true\n\n# Write manifest\ncat > /tmp/pr-context/README.md << 'MANIFEST'\n# PR Context\n\nPre-fetched PR data. All files are in `/tmp/pr-context/`.\n\n| File | Description |\n| --- | --- |\n| `pr.json` | PR metadata — title, body, author, base/head branches, head commit SHA (`headRefOid`), URL |\n| `pr.diff` | Full unified diff of all changes |\n| `files.json` | Changed files array — each entry has `filename`, `status`, `additions`, `deletions`, `patch` |\n| `diffs/.diff` | Per-file diffs — one file per changed file, mirroring the repo path under `diffs/` |\n| `file_order_az.txt` | Changed files sorted alphabetically (A→Z), one filename per line |\n| `file_order_za.txt` | Changed files sorted reverse-alphabetically (Z→A), one filename per line |\n| `file_order_largest.txt` | Changed files sorted by diff size descending (largest first), one filename per line |\n| `reviews.json` | Prior review submissions — author, state (APPROVED/CHANGES_REQUESTED/COMMENTED), body |\n| `review_comments.json` | All review threads (GraphQL) — each thread has `id` (node ID for resolving), `isResolved`, `isOutdated`, `path`, `line`, and nested `comments` with `id`, `databaseId` (numeric REST ID for replies), body/author |\n| `threads/.json` | Per-file review threads — one file per changed file with existing threads, mirroring the repo path under `threads/` |\n| `comments.json` | PR discussion comments (not inline) |\n| `issue-{N}.json` | Linked issue details (one file per linked issue, if any) |\n| `agents.md` | Repository conventions from `generate_agents_md` (if written by agent) |\n| `review-instructions.md` | Review instructions, criteria, and calibration examples (if written by review-process fragment) |\nMANIFEST\n\necho \"PR context written to /tmp/pr-context/\"\nls -la /tmp/pr-context/" - name: Write review instructions to disk run: "mkdir -p /tmp/pr-context\ncat > /tmp/pr-context/review-instructions.md << 'REVIEW_EOF'\n# Review Instructions for Sub-agents\n\nYou are a code review sub-agent. Read these instructions, then review the PR files in the order provided in your prompt.\n\n## Context\n\nBefore reviewing files, read these to understand the PR:\n\n1. `/tmp/pr-context/pr.json` — PR title, description, author, and branches. Understand what the PR is trying to accomplish.\n2. `/tmp/pr-context/agents.md` — Repository coding conventions and guidelines (if it exists).\n3. `/tmp/pr-context/review_comments.json` — Existing review threads. Note which files already have threads so you don't duplicate.\n4. `/tmp/pr-context/issue-*.json` — Linked issue details (if any). Understand the motivation and acceptance criteria.\n\n## Process\n\nReview the PR diff file by file in your assigned order. For each changed file:\n\n1. **Read the diff** for this file from `/tmp/pr-context/diffs/.diff` to understand what changed. If the diff is empty or truncated (e.g., binary files or very large changes), fall back to reading the full file from the workspace and comparing against context.\n2. **Read the full file from the workspace.** The PR branch is checked out locally — open the file directly to get complete contents with line numbers.\n3. **Check existing threads** for this file from `/tmp/pr-context/threads/.json` (if it exists). Skip issues that are already under discussion — each thread has `isResolved` and `isOutdated` fields.\n4. **Identify potential issues** matching the review criteria below.\n5. **Quick-check each issue** before including it:\n - What specific code pattern or change triggers this concern?\n - Is there an obvious guard, handler, or mitigation visible in the immediate context?\n - Can you describe a concrete failure scenario (the `evidence` field)? If you cannot articulate what specific input or state triggers the problem, drop the finding.\n - If the issue is clearly handled, skip it. If you're unsure, include it — the parent will verify.\n6. **Add to your findings list.** Do NOT leave inline comments — you don't have that tool. Return findings in this format:\n\n```\n- file: path/to/file\n line: 42\n severity: HIGH\n title: Brief title\n description: What the issue is and why it matters\n evidence: The specific code pattern and failure scenario\n suggestion: corrected code here (optional — only if you can provide a concrete fix)\n```\n\n**Review every file in your assigned order.** Files reviewed earlier get more attention, which is why different sub-agents use different orderings.\n\n**Check existing threads** — per-file threads are at `/tmp/pr-context/threads/.json` (step 3 above). The full list is at `/tmp/pr-context/review_comments.json`. Do not flag issues that are already under discussion (resolved or unresolved). For outdated threads, only re-flag if the issue still applies to the current diff.\n\n**Return your full findings list** when done, or an empty list if no issues were found.\n\n## Review Criteria\n\nFocus on these categories in priority order:\n\n1. Security vulnerabilities (injection, XSS, auth bypass, secrets exposure)\n2. Logic bugs that could cause runtime failures or incorrect behavior\n3. Data integrity issues (race conditions, missing transactions, corruption risk)\n4. Performance bottlenecks (N+1 queries, memory leaks, blocking operations)\n5. Error handling gaps (unhandled exceptions, missing validation)\n6. Breaking changes to public APIs without migration path\n7. Missing or incorrect test coverage for critical paths\n\n## What NOT to Flag\n\nOnly review the diff — do not flag issues in unchanged code, pre-existing problems not introduced by this PR, or style preferences handled by linters or formatters.\n\n**Common false positives** — these patterns look like issues but usually aren't. Before flagging anything in these categories, confirm the problem is real by reading the surrounding code:\n\n- **Security — input already sanitized:** Don't flag injection or XSS risks when inputs are sanitized upstream, parameterized queries are used, or the framework auto-escapes output.\n- **Null/undefined — guarded elsewhere:** Don't flag potential null dereferences if the value is guaranteed by a type guard, assertion, schema validation, or upstream null check.\n- **Error handling — handled at a different layer:** Don't flag missing try/catch if the caller, middleware, or framework catches and handles the error (e.g., Express error middleware, React error boundaries).\n- **Performance — theoretical, not practical:** Don't flag algorithmic complexity (e.g., O(n^2)) unless N is demonstrably large enough to matter in the actual usage context. \"This could be slow\" without evidence is not actionable.\n- **Validation — exists at another layer:** Don't flag missing input validation if it's handled by an API gateway, middleware, schema validator, or type system.\n- **Test coverage — trivial or generated code:** Don't flag missing tests for trivial getters/setters, auto-generated code, or simple delegation methods.\n- **Style or naming — not in coding guidelines:** Don't flag naming conventions or code style unless they violate the repository's documented coding guidelines (from `generate_agents_md` or CONTRIBUTING docs).\n\n**Existing review threads** — check BEFORE flagging any issue:\n\n- **Resolved with reviewer reply** (e.g. \"This is intentional\") — reviewer's decision is final. Do NOT re-flag.\n- **Resolved without reply** — author likely fixed it. Do NOT re-raise unless the fix introduced a new problem.\n- **Unresolved** — already flagged. Do NOT duplicate.\n- **Outdated** — only re-flag if the issue still applies to the current diff.\n\nWhen in doubt, do not duplicate. Redundant comments erode trust.\n\nFinding no issues is a valid and valuable outcome. An empty findings list is better than findings that waste the author's time or erode trust. Do not manufacture findings to justify your review — if the code is sound, return an empty list.\n\n## Severity Classification\n\nDetermine severity AFTER investigating the issue, not before. First identify the problem and trace through the code, then assign a severity based on the evidence you found.\n\n- 🔴 CRITICAL — Must fix before merge (security vulnerabilities, data corruption, production-breaking bugs)\n- 🟠 HIGH — Should fix before merge (logic errors, missing validation, significant performance issues)\n- 🟡 MEDIUM — Address soon, non-blocking (error handling gaps, suboptimal patterns, missing edge cases)\n- ⚪ LOW — Author discretion (minor improvements, documentation gaps)\n- 💬 NITPICK — Truly optional (stylistic preferences, alternative approaches)\n\n## Review Intensity\n\nThe review intensity is `${{ inputs.intensity || 'balanced' }}`.\n\n- **conservative**: High evidence bar. Only flag when you can demonstrate a concrete failure scenario. If you can construct a reasonable counterargument, do not flag. Approval with zero findings is the expected outcome for most PRs.\n- **balanced**: Standard evidence bar. Flag when you can point to specific code that would fail. If the issue is ambiguous, lean toward not flagging.\n- **aggressive**: Lower evidence bar. Flag when evidence exists even if the failure scenario is not fully confirmed. Improvement suggestions welcome but must cite specific code.\n\n## Calibration Examples\n\nUse these examples to calibrate your judgment. Each pair shows a real issue and a similar-looking pattern that is NOT an issue.\n\n### Example 1: Null/Undefined Access\n\n**True positive — flag this:**\n\n```js\n// PR adds this handler\napp.get('/user/:id', async (req, res) => {\n const user = await db.findUser(req.params.id);\n res.json({ name: user.name, email: user.email });\n});\n```\n\nWhy flag: `db.findUser()` can return `null` when no user matches the ID. Accessing `user.name` will throw a TypeError at runtime. No upstream guard exists — the route handler is the entry point.\n\n**False positive — do NOT flag this:**\n\n```ts\n// PR adds this line inside an existing function\nconst settings = user.getSettings();\n```\n\nWhy skip: Reading the full file reveals `user` is typed as `User` (not `User | null`), and the calling function only runs after `authenticateUser()` middleware which guarantees a valid user object. The null case is handled at a different layer.\n\n### Example 2: SQL Injection\n\n**True positive — flag this:**\n\n```python\n# PR adds this query\ncursor.execute(f\"SELECT * FROM orders WHERE customer_id = '{customer_id}'\")\n```\n\nWhy flag: String interpolation in a SQL query with user-controlled input (`customer_id` comes from the request). No parameterization or sanitization anywhere in the call chain.\n\n**False positive — do NOT flag this:**\n\n```python\n# PR adds this query\ncursor.execute(f\"SELECT * FROM orders WHERE status = '{OrderStatus.PENDING.value}'\")\n```\n\nWhy skip: The interpolated value is a hardcoded enum constant (`OrderStatus.PENDING`), not user input. There is no injection vector.\n\n### Example 3: Borderline — Do NOT Flag\n\n```go\n// PR adds this function\nfunc processItems(items []Item) []Result {\n results := make([]Result, 0)\n for _, item := range items {\n for _, tag := range item.Tags {\n results = append(results, process(item, tag))\n }\n }\n return results\n}\n```\n\nThis looks like an O(n*m) performance concern. But without evidence that `items` or `Tags` are large in practice, this is speculative. The function processes a bounded dataset (items from a single user request). Do not flag theoretical performance issues without evidence of real-world impact.\nREVIEW_EOF" - env: @@ -1334,6 +1334,26 @@ jobs: return subprocess.run(cmd, capture_output=True, text=True, timeout=60) except subprocess.TimeoutExpired: return subprocess.CompletedProcess(cmd, 1, stdout='', stderr='diff timed out') + + # --- Guard: detect history rewrites and merge commits --- + pr_json_path = '/tmp/pr-context/pr.json' + if os.path.isfile(pr_json_path): + with open(pr_json_path) as f: + pr_data = json.load(f) + pr_head_sha = pr_data.get('headRefOid', '') + if pr_head_sha: + # Check 1: PR head must be an ancestor of HEAD (no rebase/reset) + anc = run(['git', 'merge-base', '--is-ancestor', pr_head_sha, 'HEAD']) + if anc.returncode != 0: + print(json.dumps({'status': 'error', 'error': f'History rewrite detected: the original PR head ({pr_head_sha[:12]}) is not an ancestor of HEAD. This means git rebase, reset, or cherry-pick rewrote history. push_to_pull_request_branch will fail. Fix: reset to the PR head with `git checkout {pr_head_sha}`, re-apply your changes as direct file edits, and commit as regular commits.'})) + raise SystemExit(0) + # Check 2: no merge commits (multiple parents) since PR head + log = run(['git', 'rev-list', '--min-parents=2', f'{pr_head_sha}..HEAD']) + merge_shas = log.stdout.strip() + if merge_shas: + print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... push_to_pull_request_branch uses git format-patch which breaks on merge commits. Fix: reset to the PR head with `git checkout {pr_head_sha}`, re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags), and commit as regular single-parent commits.'})) + raise SystemExit(0) + contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') # Generate diff of all local changes vs upstream for self-review diff --git a/.github/workflows/gh-aw-mention-in-pr.lock.yml b/.github/workflows/gh-aw-mention-in-pr.lock.yml index 9c9f8c0d..2f4731ff 100644 --- a/.github/workflows/gh-aw-mention-in-pr.lock.yml +++ b/.github/workflows/gh-aw-mention-in-pr.lock.yml @@ -44,7 +44,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"cda6a28c3dcf5a152d2329e660999407d7f6c601c5d0797350b7783ddd4ac4b7"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"7e403799ef20bf252adf0e73e990c12389d8b8a5f4636463729d18868fcb283d"} name: "Mention in PR" "on": @@ -739,7 +739,7 @@ jobs: GH_TOKEN: ${{ github.token }} PR_NUMBER: ${{ github.event.pull_request.number || inputs.target-pr-number || github.event.issue.number }} name: Fetch PR context to disk - run: "set -euo pipefail\nmkdir -p /tmp/pr-context\n\n# PR metadata\ngh pr view \"$PR_NUMBER\" --json title,body,author,baseRefName,headRefName,url \\\n > /tmp/pr-context/pr.json\n\n# Full diff\nif ! gh pr diff \"$PR_NUMBER\" > /tmp/pr-context/pr.diff; then\n echo \"::warning::Failed to fetch full PR diff; per-file diffs from files.json are still available.\"\n : > /tmp/pr-context/pr.diff\nfi\n\n# Changed files list (--paginate may output concatenated arrays; jq -s 'add' merges them)\ngh api \"repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/files\" --paginate \\\n | jq -s 'add // []' > /tmp/pr-context/files.json\n\n# Per-file diffs\njq -c '.[]' /tmp/pr-context/files.json | while IFS= read -r entry; do\n filename=$(echo \"$entry\" | jq -r '.filename')\n mkdir -p \"/tmp/pr-context/diffs/$(dirname \"$filename\")\"\n echo \"$entry\" | jq -r '.patch // empty' > \"/tmp/pr-context/diffs/${filename}.diff\"\ndone\n\n# File orderings for sub-agent review (3 strategies)\njq -r '[.[] | .filename] | sort | .[]' /tmp/pr-context/files.json \\\n > /tmp/pr-context/file_order_az.txt\njq -r '[.[] | .filename] | sort | reverse | .[]' /tmp/pr-context/files.json \\\n > /tmp/pr-context/file_order_za.txt\njq -r '[.[] | {filename, size: ((.additions // 0) + (.deletions // 0))}] | sort_by(-.size) | .[].filename' /tmp/pr-context/files.json \\\n > /tmp/pr-context/file_order_largest.txt\n\n# Existing reviews\ngh api \"repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/reviews\" --paginate \\\n | jq -s 'add // []' > /tmp/pr-context/reviews.json\n\n# Review threads with resolution status (GraphQL — REST lacks isResolved/isOutdated)\ngh api graphql --paginate -f query='\n query($owner: String!, $repo: String!, $number: Int!, $endCursor: String) {\n repository(owner: $owner, name: $repo) {\n pullRequest(number: $number) {\n reviewThreads(first: 100, after: $endCursor) {\n pageInfo { hasNextPage endCursor }\n nodes {\n id\n isResolved\n isOutdated\n isCollapsed\n path\n line\n startLine\n comments(first: 100) {\n nodes {\n id\n databaseId\n body\n author { login }\n createdAt\n }\n }\n }\n }\n }\n }\n }\n' -F owner=\"${GITHUB_REPOSITORY%/*}\" -F repo=\"${GITHUB_REPOSITORY#*/}\" -F \"number=$PR_NUMBER\" \\\n --jq '.data.repository.pullRequest.reviewThreads.nodes' \\\n | jq -s 'add // []' > /tmp/pr-context/review_comments.json\n\n# Per-file review threads (mirrors diffs/ structure)\njq -c '.[]' /tmp/pr-context/review_comments.json | while IFS= read -r thread; do\n filepath=$(echo \"$thread\" | jq -r '.path // empty')\n [ -z \"$filepath\" ] && continue\n mkdir -p \"/tmp/pr-context/threads/$(dirname \"$filepath\")\"\n echo \"$thread\" >> \"/tmp/pr-context/threads/${filepath}.jsonl\"\ndone\n# Convert per-file JSONL to proper JSON arrays\nmkdir -p /tmp/pr-context/threads\nfind /tmp/pr-context/threads -name '*.jsonl' 2>/dev/null | while IFS= read -r jsonl; do\n jq -s '.' \"$jsonl\" > \"${jsonl%.jsonl}.json\"\n rm \"$jsonl\"\ndone\n\n# PR discussion comments\ngh api \"repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments\" --paginate \\\n | jq -s 'add // []' > /tmp/pr-context/comments.json\n\n# Linked issues\njq -r '.body // \"\"' /tmp/pr-context/pr.json 2>/dev/null \\\n | grep -oiE '(fixes|closes|resolves)\\s+#[0-9]+' \\\n | grep -oE '[0-9]+$' \\\n | sort -u \\\n | while read -r issue; do\n gh api \"repos/$GITHUB_REPOSITORY/issues/$issue\" > \"/tmp/pr-context/issue-${issue}.json\" || true\n done || true\n\n# Write manifest\ncat > /tmp/pr-context/README.md << 'MANIFEST'\n# PR Context\n\nPre-fetched PR data. All files are in `/tmp/pr-context/`.\n\n| File | Description |\n| --- | --- |\n| `pr.json` | PR metadata — title, body, author, base/head branches, URL |\n| `pr.diff` | Full unified diff of all changes |\n| `files.json` | Changed files array — each entry has `filename`, `status`, `additions`, `deletions`, `patch` |\n| `diffs/.diff` | Per-file diffs — one file per changed file, mirroring the repo path under `diffs/` |\n| `file_order_az.txt` | Changed files sorted alphabetically (A→Z), one filename per line |\n| `file_order_za.txt` | Changed files sorted reverse-alphabetically (Z→A), one filename per line |\n| `file_order_largest.txt` | Changed files sorted by diff size descending (largest first), one filename per line |\n| `reviews.json` | Prior review submissions — author, state (APPROVED/CHANGES_REQUESTED/COMMENTED), body |\n| `review_comments.json` | All review threads (GraphQL) — each thread has `id` (node ID for resolving), `isResolved`, `isOutdated`, `path`, `line`, and nested `comments` with `id`, `databaseId` (numeric REST ID for replies), body/author |\n| `threads/.json` | Per-file review threads — one file per changed file with existing threads, mirroring the repo path under `threads/` |\n| `comments.json` | PR discussion comments (not inline) |\n| `issue-{N}.json` | Linked issue details (one file per linked issue, if any) |\n| `agents.md` | Repository conventions from `generate_agents_md` (if written by agent) |\n| `review-instructions.md` | Review instructions, criteria, and calibration examples (if written by review-process fragment) |\nMANIFEST\n\necho \"PR context written to /tmp/pr-context/\"\nls -la /tmp/pr-context/" + run: "set -euo pipefail\nmkdir -p /tmp/pr-context\n\n# PR metadata\ngh pr view \"$PR_NUMBER\" --json title,body,author,baseRefName,headRefName,headRefOid,url \\\n > /tmp/pr-context/pr.json\n\n# Full diff\nif ! gh pr diff \"$PR_NUMBER\" > /tmp/pr-context/pr.diff; then\n echo \"::warning::Failed to fetch full PR diff; per-file diffs from files.json are still available.\"\n : > /tmp/pr-context/pr.diff\nfi\n\n# Changed files list (--paginate may output concatenated arrays; jq -s 'add' merges them)\ngh api \"repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/files\" --paginate \\\n | jq -s 'add // []' > /tmp/pr-context/files.json\n\n# Per-file diffs\njq -c '.[]' /tmp/pr-context/files.json | while IFS= read -r entry; do\n filename=$(echo \"$entry\" | jq -r '.filename')\n mkdir -p \"/tmp/pr-context/diffs/$(dirname \"$filename\")\"\n echo \"$entry\" | jq -r '.patch // empty' > \"/tmp/pr-context/diffs/${filename}.diff\"\ndone\n\n# File orderings for sub-agent review (3 strategies)\njq -r '[.[] | .filename] | sort | .[]' /tmp/pr-context/files.json \\\n > /tmp/pr-context/file_order_az.txt\njq -r '[.[] | .filename] | sort | reverse | .[]' /tmp/pr-context/files.json \\\n > /tmp/pr-context/file_order_za.txt\njq -r '[.[] | {filename, size: ((.additions // 0) + (.deletions // 0))}] | sort_by(-.size) | .[].filename' /tmp/pr-context/files.json \\\n > /tmp/pr-context/file_order_largest.txt\n\n# Existing reviews\ngh api \"repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/reviews\" --paginate \\\n | jq -s 'add // []' > /tmp/pr-context/reviews.json\n\n# Review threads with resolution status (GraphQL — REST lacks isResolved/isOutdated)\ngh api graphql --paginate -f query='\n query($owner: String!, $repo: String!, $number: Int!, $endCursor: String) {\n repository(owner: $owner, name: $repo) {\n pullRequest(number: $number) {\n reviewThreads(first: 100, after: $endCursor) {\n pageInfo { hasNextPage endCursor }\n nodes {\n id\n isResolved\n isOutdated\n isCollapsed\n path\n line\n startLine\n comments(first: 100) {\n nodes {\n id\n databaseId\n body\n author { login }\n createdAt\n }\n }\n }\n }\n }\n }\n }\n' -F owner=\"${GITHUB_REPOSITORY%/*}\" -F repo=\"${GITHUB_REPOSITORY#*/}\" -F \"number=$PR_NUMBER\" \\\n --jq '.data.repository.pullRequest.reviewThreads.nodes' \\\n | jq -s 'add // []' > /tmp/pr-context/review_comments.json\n\n# Per-file review threads (mirrors diffs/ structure)\njq -c '.[]' /tmp/pr-context/review_comments.json | while IFS= read -r thread; do\n filepath=$(echo \"$thread\" | jq -r '.path // empty')\n [ -z \"$filepath\" ] && continue\n mkdir -p \"/tmp/pr-context/threads/$(dirname \"$filepath\")\"\n echo \"$thread\" >> \"/tmp/pr-context/threads/${filepath}.jsonl\"\ndone\n# Convert per-file JSONL to proper JSON arrays\nmkdir -p /tmp/pr-context/threads\nfind /tmp/pr-context/threads -name '*.jsonl' 2>/dev/null | while IFS= read -r jsonl; do\n jq -s '.' \"$jsonl\" > \"${jsonl%.jsonl}.json\"\n rm \"$jsonl\"\ndone\n\n# PR discussion comments\ngh api \"repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments\" --paginate \\\n | jq -s 'add // []' > /tmp/pr-context/comments.json\n\n# Linked issues\njq -r '.body // \"\"' /tmp/pr-context/pr.json 2>/dev/null \\\n | grep -oiE '(fixes|closes|resolves)\\s+#[0-9]+' \\\n | grep -oE '[0-9]+$' \\\n | sort -u \\\n | while read -r issue; do\n gh api \"repos/$GITHUB_REPOSITORY/issues/$issue\" > \"/tmp/pr-context/issue-${issue}.json\" || true\n done || true\n\n# Write manifest\ncat > /tmp/pr-context/README.md << 'MANIFEST'\n# PR Context\n\nPre-fetched PR data. All files are in `/tmp/pr-context/`.\n\n| File | Description |\n| --- | --- |\n| `pr.json` | PR metadata — title, body, author, base/head branches, head commit SHA (`headRefOid`), URL |\n| `pr.diff` | Full unified diff of all changes |\n| `files.json` | Changed files array — each entry has `filename`, `status`, `additions`, `deletions`, `patch` |\n| `diffs/.diff` | Per-file diffs — one file per changed file, mirroring the repo path under `diffs/` |\n| `file_order_az.txt` | Changed files sorted alphabetically (A→Z), one filename per line |\n| `file_order_za.txt` | Changed files sorted reverse-alphabetically (Z→A), one filename per line |\n| `file_order_largest.txt` | Changed files sorted by diff size descending (largest first), one filename per line |\n| `reviews.json` | Prior review submissions — author, state (APPROVED/CHANGES_REQUESTED/COMMENTED), body |\n| `review_comments.json` | All review threads (GraphQL) — each thread has `id` (node ID for resolving), `isResolved`, `isOutdated`, `path`, `line`, and nested `comments` with `id`, `databaseId` (numeric REST ID for replies), body/author |\n| `threads/.json` | Per-file review threads — one file per changed file with existing threads, mirroring the repo path under `threads/` |\n| `comments.json` | PR discussion comments (not inline) |\n| `issue-{N}.json` | Linked issue details (one file per linked issue, if any) |\n| `agents.md` | Repository conventions from `generate_agents_md` (if written by agent) |\n| `review-instructions.md` | Review instructions, criteria, and calibration examples (if written by review-process fragment) |\nMANIFEST\n\necho \"PR context written to /tmp/pr-context/\"\nls -la /tmp/pr-context/" - name: Write review instructions to disk run: "mkdir -p /tmp/pr-context\ncat > /tmp/pr-context/review-instructions.md << 'REVIEW_EOF'\n# Review Instructions for Sub-agents\n\nYou are a code review sub-agent. Read these instructions, then review the PR files in the order provided in your prompt.\n\n## Context\n\nBefore reviewing files, read these to understand the PR:\n\n1. `/tmp/pr-context/pr.json` — PR title, description, author, and branches. Understand what the PR is trying to accomplish.\n2. `/tmp/pr-context/agents.md` — Repository coding conventions and guidelines (if it exists).\n3. `/tmp/pr-context/review_comments.json` — Existing review threads. Note which files already have threads so you don't duplicate.\n4. `/tmp/pr-context/issue-*.json` — Linked issue details (if any). Understand the motivation and acceptance criteria.\n\n## Process\n\nReview the PR diff file by file in your assigned order. For each changed file:\n\n1. **Read the diff** for this file from `/tmp/pr-context/diffs/.diff` to understand what changed. If the diff is empty or truncated (e.g., binary files or very large changes), fall back to reading the full file from the workspace and comparing against context.\n2. **Read the full file from the workspace.** The PR branch is checked out locally — open the file directly to get complete contents with line numbers.\n3. **Check existing threads** for this file from `/tmp/pr-context/threads/.json` (if it exists). Skip issues that are already under discussion — each thread has `isResolved` and `isOutdated` fields.\n4. **Identify potential issues** matching the review criteria below.\n5. **Quick-check each issue** before including it:\n - What specific code pattern or change triggers this concern?\n - Is there an obvious guard, handler, or mitigation visible in the immediate context?\n - Can you describe a concrete failure scenario (the `evidence` field)? If you cannot articulate what specific input or state triggers the problem, drop the finding.\n - If the issue is clearly handled, skip it. If you're unsure, include it — the parent will verify.\n6. **Add to your findings list.** Do NOT leave inline comments — you don't have that tool. Return findings in this format:\n\n```\n- file: path/to/file\n line: 42\n severity: HIGH\n title: Brief title\n description: What the issue is and why it matters\n evidence: The specific code pattern and failure scenario\n suggestion: corrected code here (optional — only if you can provide a concrete fix)\n```\n\n**Review every file in your assigned order.** Files reviewed earlier get more attention, which is why different sub-agents use different orderings.\n\n**Check existing threads** — per-file threads are at `/tmp/pr-context/threads/.json` (step 3 above). The full list is at `/tmp/pr-context/review_comments.json`. Do not flag issues that are already under discussion (resolved or unresolved). For outdated threads, only re-flag if the issue still applies to the current diff.\n\n**Return your full findings list** when done, or an empty list if no issues were found.\n\n## Review Criteria\n\nFocus on these categories in priority order:\n\n1. Security vulnerabilities (injection, XSS, auth bypass, secrets exposure)\n2. Logic bugs that could cause runtime failures or incorrect behavior\n3. Data integrity issues (race conditions, missing transactions, corruption risk)\n4. Performance bottlenecks (N+1 queries, memory leaks, blocking operations)\n5. Error handling gaps (unhandled exceptions, missing validation)\n6. Breaking changes to public APIs without migration path\n7. Missing or incorrect test coverage for critical paths\n\n## What NOT to Flag\n\nOnly review the diff — do not flag issues in unchanged code, pre-existing problems not introduced by this PR, or style preferences handled by linters or formatters.\n\n**Common false positives** — these patterns look like issues but usually aren't. Before flagging anything in these categories, confirm the problem is real by reading the surrounding code:\n\n- **Security — input already sanitized:** Don't flag injection or XSS risks when inputs are sanitized upstream, parameterized queries are used, or the framework auto-escapes output.\n- **Null/undefined — guarded elsewhere:** Don't flag potential null dereferences if the value is guaranteed by a type guard, assertion, schema validation, or upstream null check.\n- **Error handling — handled at a different layer:** Don't flag missing try/catch if the caller, middleware, or framework catches and handles the error (e.g., Express error middleware, React error boundaries).\n- **Performance — theoretical, not practical:** Don't flag algorithmic complexity (e.g., O(n^2)) unless N is demonstrably large enough to matter in the actual usage context. \"This could be slow\" without evidence is not actionable.\n- **Validation — exists at another layer:** Don't flag missing input validation if it's handled by an API gateway, middleware, schema validator, or type system.\n- **Test coverage — trivial or generated code:** Don't flag missing tests for trivial getters/setters, auto-generated code, or simple delegation methods.\n- **Style or naming — not in coding guidelines:** Don't flag naming conventions or code style unless they violate the repository's documented coding guidelines (from `generate_agents_md` or CONTRIBUTING docs).\n\n**Existing review threads** — check BEFORE flagging any issue:\n\n- **Resolved with reviewer reply** (e.g. \"This is intentional\") — reviewer's decision is final. Do NOT re-flag.\n- **Resolved without reply** — author likely fixed it. Do NOT re-raise unless the fix introduced a new problem.\n- **Unresolved** — already flagged. Do NOT duplicate.\n- **Outdated** — only re-flag if the issue still applies to the current diff.\n\nWhen in doubt, do not duplicate. Redundant comments erode trust.\n\nFinding no issues is a valid and valuable outcome. An empty findings list is better than findings that waste the author's time or erode trust. Do not manufacture findings to justify your review — if the code is sound, return an empty list.\n\n## Severity Classification\n\nDetermine severity AFTER investigating the issue, not before. First identify the problem and trace through the code, then assign a severity based on the evidence you found.\n\n- 🔴 CRITICAL — Must fix before merge (security vulnerabilities, data corruption, production-breaking bugs)\n- 🟠 HIGH — Should fix before merge (logic errors, missing validation, significant performance issues)\n- 🟡 MEDIUM — Address soon, non-blocking (error handling gaps, suboptimal patterns, missing edge cases)\n- ⚪ LOW — Author discretion (minor improvements, documentation gaps)\n- 💬 NITPICK — Truly optional (stylistic preferences, alternative approaches)\n\n## Review Intensity\n\nThe review intensity is `${{ inputs.intensity || 'balanced' }}`.\n\n- **conservative**: High evidence bar. Only flag when you can demonstrate a concrete failure scenario. If you can construct a reasonable counterargument, do not flag. Approval with zero findings is the expected outcome for most PRs.\n- **balanced**: Standard evidence bar. Flag when you can point to specific code that would fail. If the issue is ambiguous, lean toward not flagging.\n- **aggressive**: Lower evidence bar. Flag when evidence exists even if the failure scenario is not fully confirmed. Improvement suggestions welcome but must cite specific code.\n\n## Calibration Examples\n\nUse these examples to calibrate your judgment. Each pair shows a real issue and a similar-looking pattern that is NOT an issue.\n\n### Example 1: Null/Undefined Access\n\n**True positive — flag this:**\n\n```js\n// PR adds this handler\napp.get('/user/:id', async (req, res) => {\n const user = await db.findUser(req.params.id);\n res.json({ name: user.name, email: user.email });\n});\n```\n\nWhy flag: `db.findUser()` can return `null` when no user matches the ID. Accessing `user.name` will throw a TypeError at runtime. No upstream guard exists — the route handler is the entry point.\n\n**False positive — do NOT flag this:**\n\n```ts\n// PR adds this line inside an existing function\nconst settings = user.getSettings();\n```\n\nWhy skip: Reading the full file reveals `user` is typed as `User` (not `User | null`), and the calling function only runs after `authenticateUser()` middleware which guarantees a valid user object. The null case is handled at a different layer.\n\n### Example 2: SQL Injection\n\n**True positive — flag this:**\n\n```python\n# PR adds this query\ncursor.execute(f\"SELECT * FROM orders WHERE customer_id = '{customer_id}'\")\n```\n\nWhy flag: String interpolation in a SQL query with user-controlled input (`customer_id` comes from the request). No parameterization or sanitization anywhere in the call chain.\n\n**False positive — do NOT flag this:**\n\n```python\n# PR adds this query\ncursor.execute(f\"SELECT * FROM orders WHERE status = '{OrderStatus.PENDING.value}'\")\n```\n\nWhy skip: The interpolated value is a hardcoded enum constant (`OrderStatus.PENDING`), not user input. There is no injection vector.\n\n### Example 3: Borderline — Do NOT Flag\n\n```go\n// PR adds this function\nfunc processItems(items []Item) []Result {\n results := make([]Result, 0)\n for _, item := range items {\n for _, tag := range item.Tags {\n results = append(results, process(item, tag))\n }\n }\n return results\n}\n```\n\nThis looks like an O(n*m) performance concern. But without evidence that `items` or `Tags` are large in practice, this is speculative. The function processes a bounded dataset (items from a single user request). Do not flag theoretical performance issues without evidence of real-world impact.\nREVIEW_EOF" - env: @@ -1349,6 +1349,26 @@ jobs: return subprocess.run(cmd, capture_output=True, text=True, timeout=60) except subprocess.TimeoutExpired: return subprocess.CompletedProcess(cmd, 1, stdout='', stderr='diff timed out') + + # --- Guard: detect history rewrites and merge commits --- + pr_json_path = '/tmp/pr-context/pr.json' + if os.path.isfile(pr_json_path): + with open(pr_json_path) as f: + pr_data = json.load(f) + pr_head_sha = pr_data.get('headRefOid', '') + if pr_head_sha: + # Check 1: PR head must be an ancestor of HEAD (no rebase/reset) + anc = run(['git', 'merge-base', '--is-ancestor', pr_head_sha, 'HEAD']) + if anc.returncode != 0: + print(json.dumps({'status': 'error', 'error': f'History rewrite detected: the original PR head ({pr_head_sha[:12]}) is not an ancestor of HEAD. This means git rebase, reset, or cherry-pick rewrote history. push_to_pull_request_branch will fail. Fix: reset to the PR head with `git checkout {pr_head_sha}`, re-apply your changes as direct file edits, and commit as regular commits.'})) + raise SystemExit(0) + # Check 2: no merge commits (multiple parents) since PR head + log = run(['git', 'rev-list', '--min-parents=2', f'{pr_head_sha}..HEAD']) + merge_shas = log.stdout.strip() + if merge_shas: + print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... push_to_pull_request_branch uses git format-patch which breaks on merge commits. Fix: reset to the PR head with `git checkout {pr_head_sha}`, re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags), and commit as regular single-parent commits.'})) + raise SystemExit(0) + contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') # Generate diff of all local changes vs upstream for self-review diff --git a/.github/workflows/gh-aw-newbie-contributor-fixer.lock.yml b/.github/workflows/gh-aw-newbie-contributor-fixer.lock.yml index 4e43221c..b22cf289 100644 --- a/.github/workflows/gh-aw-newbie-contributor-fixer.lock.yml +++ b/.github/workflows/gh-aw-newbie-contributor-fixer.lock.yml @@ -37,7 +37,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"024dac322c2a829d3934945b47e8d4fee6d109bc488ace55b42d8fbbc62c3c58"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"db46c91fd6bebb36c863eac3028f32f6a6addada7f803e5fd91a6b6597cc013e"} name: "Newbie Contributor Fixer" "on": @@ -935,6 +935,22 @@ jobs: return subprocess.run(cmd, capture_output=True, text=True, timeout=60) except subprocess.TimeoutExpired: return subprocess.CompletedProcess(cmd, 1, stdout='', stderr='diff timed out') + + # --- Guard: detect merge commits --- + # Find the fork point with the upstream branch to scope the check + upstream_sha = '' + for ref in ['@{upstream}', 'origin/HEAD', 'origin/main']: + r = run(['git', 'merge-base', 'HEAD', ref]) + if r.returncode == 0 and r.stdout.strip(): + upstream_sha = r.stdout.strip() + break + if upstream_sha: + log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) + merge_shas = log.stdout.strip() + if merge_shas: + print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) + raise SystemExit(0) + contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') # Generate diff of all local changes vs upstream for self-review diff --git a/.github/workflows/gh-aw-pr-actions-fixer.lock.yml b/.github/workflows/gh-aw-pr-actions-fixer.lock.yml index 08986bb1..65922a9b 100644 --- a/.github/workflows/gh-aw-pr-actions-fixer.lock.yml +++ b/.github/workflows/gh-aw-pr-actions-fixer.lock.yml @@ -37,7 +37,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"e490edb6d9a4e54d16cbbef444ea346179e20b8ad15ccb211e1edb6efc039de2"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"706d5729d0afe6acf1a5efc3d1a5e5c125ff4a4b9b0000fe5bd6d4cd6d6ffa16"} name: "PR Actions Fixer" "on": @@ -948,6 +948,26 @@ jobs: return subprocess.run(cmd, capture_output=True, text=True, timeout=60) except subprocess.TimeoutExpired: return subprocess.CompletedProcess(cmd, 1, stdout='', stderr='diff timed out') + + # --- Guard: detect history rewrites and merge commits --- + pr_json_path = '/tmp/pr-context/pr.json' + if os.path.isfile(pr_json_path): + with open(pr_json_path) as f: + pr_data = json.load(f) + pr_head_sha = pr_data.get('headRefOid', '') + if pr_head_sha: + # Check 1: PR head must be an ancestor of HEAD (no rebase/reset) + anc = run(['git', 'merge-base', '--is-ancestor', pr_head_sha, 'HEAD']) + if anc.returncode != 0: + print(json.dumps({'status': 'error', 'error': f'History rewrite detected: the original PR head ({pr_head_sha[:12]}) is not an ancestor of HEAD. This means git rebase, reset, or cherry-pick rewrote history. push_to_pull_request_branch will fail. Fix: reset to the PR head with `git checkout {pr_head_sha}`, re-apply your changes as direct file edits, and commit as regular commits.'})) + raise SystemExit(0) + # Check 2: no merge commits (multiple parents) since PR head + log = run(['git', 'rev-list', '--min-parents=2', f'{pr_head_sha}..HEAD']) + merge_shas = log.stdout.strip() + if merge_shas: + print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... push_to_pull_request_branch uses git format-patch which breaks on merge commits. Fix: reset to the PR head with `git checkout {pr_head_sha}`, re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags), and commit as regular single-parent commits.'})) + raise SystemExit(0) + contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') # Generate diff of all local changes vs upstream for self-review diff --git a/.github/workflows/gh-aw-pr-review-addresser.lock.yml b/.github/workflows/gh-aw-pr-review-addresser.lock.yml index 8725bee3..6a2c2ba8 100644 --- a/.github/workflows/gh-aw-pr-review-addresser.lock.yml +++ b/.github/workflows/gh-aw-pr-review-addresser.lock.yml @@ -40,7 +40,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"4f1ee1257f4864d55ec2da7ea89e4f721fd045e23e88ff58a32ed227cf00ba16"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"05f95a5583fe87b419ce91babee4470e61dc8e38b690f8cfb0d327576c6bdd46"} name: "PR Review Addresser" "on": @@ -569,7 +569,7 @@ jobs: GH_TOKEN: ${{ github.token }} PR_NUMBER: ${{ github.event.pull_request.number || inputs.target-pr-number || github.event.issue.number }} name: Fetch PR context to disk - run: "set -euo pipefail\nmkdir -p /tmp/pr-context\n\n# PR metadata\ngh pr view \"$PR_NUMBER\" --json title,body,author,baseRefName,headRefName,url \\\n > /tmp/pr-context/pr.json\n\n# Full diff\nif ! gh pr diff \"$PR_NUMBER\" > /tmp/pr-context/pr.diff; then\n echo \"::warning::Failed to fetch full PR diff; per-file diffs from files.json are still available.\"\n : > /tmp/pr-context/pr.diff\nfi\n\n# Changed files list (--paginate may output concatenated arrays; jq -s 'add' merges them)\ngh api \"repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/files\" --paginate \\\n | jq -s 'add // []' > /tmp/pr-context/files.json\n\n# Per-file diffs\njq -c '.[]' /tmp/pr-context/files.json | while IFS= read -r entry; do\n filename=$(echo \"$entry\" | jq -r '.filename')\n mkdir -p \"/tmp/pr-context/diffs/$(dirname \"$filename\")\"\n echo \"$entry\" | jq -r '.patch // empty' > \"/tmp/pr-context/diffs/${filename}.diff\"\ndone\n\n# File orderings for sub-agent review (3 strategies)\njq -r '[.[] | .filename] | sort | .[]' /tmp/pr-context/files.json \\\n > /tmp/pr-context/file_order_az.txt\njq -r '[.[] | .filename] | sort | reverse | .[]' /tmp/pr-context/files.json \\\n > /tmp/pr-context/file_order_za.txt\njq -r '[.[] | {filename, size: ((.additions // 0) + (.deletions // 0))}] | sort_by(-.size) | .[].filename' /tmp/pr-context/files.json \\\n > /tmp/pr-context/file_order_largest.txt\n\n# Existing reviews\ngh api \"repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/reviews\" --paginate \\\n | jq -s 'add // []' > /tmp/pr-context/reviews.json\n\n# Review threads with resolution status (GraphQL — REST lacks isResolved/isOutdated)\ngh api graphql --paginate -f query='\n query($owner: String!, $repo: String!, $number: Int!, $endCursor: String) {\n repository(owner: $owner, name: $repo) {\n pullRequest(number: $number) {\n reviewThreads(first: 100, after: $endCursor) {\n pageInfo { hasNextPage endCursor }\n nodes {\n id\n isResolved\n isOutdated\n isCollapsed\n path\n line\n startLine\n comments(first: 100) {\n nodes {\n id\n databaseId\n body\n author { login }\n createdAt\n }\n }\n }\n }\n }\n }\n }\n' -F owner=\"${GITHUB_REPOSITORY%/*}\" -F repo=\"${GITHUB_REPOSITORY#*/}\" -F \"number=$PR_NUMBER\" \\\n --jq '.data.repository.pullRequest.reviewThreads.nodes' \\\n | jq -s 'add // []' > /tmp/pr-context/review_comments.json\n\n# Per-file review threads (mirrors diffs/ structure)\njq -c '.[]' /tmp/pr-context/review_comments.json | while IFS= read -r thread; do\n filepath=$(echo \"$thread\" | jq -r '.path // empty')\n [ -z \"$filepath\" ] && continue\n mkdir -p \"/tmp/pr-context/threads/$(dirname \"$filepath\")\"\n echo \"$thread\" >> \"/tmp/pr-context/threads/${filepath}.jsonl\"\ndone\n# Convert per-file JSONL to proper JSON arrays\nmkdir -p /tmp/pr-context/threads\nfind /tmp/pr-context/threads -name '*.jsonl' 2>/dev/null | while IFS= read -r jsonl; do\n jq -s '.' \"$jsonl\" > \"${jsonl%.jsonl}.json\"\n rm \"$jsonl\"\ndone\n\n# PR discussion comments\ngh api \"repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments\" --paginate \\\n | jq -s 'add // []' > /tmp/pr-context/comments.json\n\n# Linked issues\njq -r '.body // \"\"' /tmp/pr-context/pr.json 2>/dev/null \\\n | grep -oiE '(fixes|closes|resolves)\\s+#[0-9]+' \\\n | grep -oE '[0-9]+$' \\\n | sort -u \\\n | while read -r issue; do\n gh api \"repos/$GITHUB_REPOSITORY/issues/$issue\" > \"/tmp/pr-context/issue-${issue}.json\" || true\n done || true\n\n# Write manifest\ncat > /tmp/pr-context/README.md << 'MANIFEST'\n# PR Context\n\nPre-fetched PR data. All files are in `/tmp/pr-context/`.\n\n| File | Description |\n| --- | --- |\n| `pr.json` | PR metadata — title, body, author, base/head branches, URL |\n| `pr.diff` | Full unified diff of all changes |\n| `files.json` | Changed files array — each entry has `filename`, `status`, `additions`, `deletions`, `patch` |\n| `diffs/.diff` | Per-file diffs — one file per changed file, mirroring the repo path under `diffs/` |\n| `file_order_az.txt` | Changed files sorted alphabetically (A→Z), one filename per line |\n| `file_order_za.txt` | Changed files sorted reverse-alphabetically (Z→A), one filename per line |\n| `file_order_largest.txt` | Changed files sorted by diff size descending (largest first), one filename per line |\n| `reviews.json` | Prior review submissions — author, state (APPROVED/CHANGES_REQUESTED/COMMENTED), body |\n| `review_comments.json` | All review threads (GraphQL) — each thread has `id` (node ID for resolving), `isResolved`, `isOutdated`, `path`, `line`, and nested `comments` with `id`, `databaseId` (numeric REST ID for replies), body/author |\n| `threads/.json` | Per-file review threads — one file per changed file with existing threads, mirroring the repo path under `threads/` |\n| `comments.json` | PR discussion comments (not inline) |\n| `issue-{N}.json` | Linked issue details (one file per linked issue, if any) |\n| `agents.md` | Repository conventions from `generate_agents_md` (if written by agent) |\n| `review-instructions.md` | Review instructions, criteria, and calibration examples (if written by review-process fragment) |\nMANIFEST\n\necho \"PR context written to /tmp/pr-context/\"\nls -la /tmp/pr-context/" + run: "set -euo pipefail\nmkdir -p /tmp/pr-context\n\n# PR metadata\ngh pr view \"$PR_NUMBER\" --json title,body,author,baseRefName,headRefName,headRefOid,url \\\n > /tmp/pr-context/pr.json\n\n# Full diff\nif ! gh pr diff \"$PR_NUMBER\" > /tmp/pr-context/pr.diff; then\n echo \"::warning::Failed to fetch full PR diff; per-file diffs from files.json are still available.\"\n : > /tmp/pr-context/pr.diff\nfi\n\n# Changed files list (--paginate may output concatenated arrays; jq -s 'add' merges them)\ngh api \"repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/files\" --paginate \\\n | jq -s 'add // []' > /tmp/pr-context/files.json\n\n# Per-file diffs\njq -c '.[]' /tmp/pr-context/files.json | while IFS= read -r entry; do\n filename=$(echo \"$entry\" | jq -r '.filename')\n mkdir -p \"/tmp/pr-context/diffs/$(dirname \"$filename\")\"\n echo \"$entry\" | jq -r '.patch // empty' > \"/tmp/pr-context/diffs/${filename}.diff\"\ndone\n\n# File orderings for sub-agent review (3 strategies)\njq -r '[.[] | .filename] | sort | .[]' /tmp/pr-context/files.json \\\n > /tmp/pr-context/file_order_az.txt\njq -r '[.[] | .filename] | sort | reverse | .[]' /tmp/pr-context/files.json \\\n > /tmp/pr-context/file_order_za.txt\njq -r '[.[] | {filename, size: ((.additions // 0) + (.deletions // 0))}] | sort_by(-.size) | .[].filename' /tmp/pr-context/files.json \\\n > /tmp/pr-context/file_order_largest.txt\n\n# Existing reviews\ngh api \"repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/reviews\" --paginate \\\n | jq -s 'add // []' > /tmp/pr-context/reviews.json\n\n# Review threads with resolution status (GraphQL — REST lacks isResolved/isOutdated)\ngh api graphql --paginate -f query='\n query($owner: String!, $repo: String!, $number: Int!, $endCursor: String) {\n repository(owner: $owner, name: $repo) {\n pullRequest(number: $number) {\n reviewThreads(first: 100, after: $endCursor) {\n pageInfo { hasNextPage endCursor }\n nodes {\n id\n isResolved\n isOutdated\n isCollapsed\n path\n line\n startLine\n comments(first: 100) {\n nodes {\n id\n databaseId\n body\n author { login }\n createdAt\n }\n }\n }\n }\n }\n }\n }\n' -F owner=\"${GITHUB_REPOSITORY%/*}\" -F repo=\"${GITHUB_REPOSITORY#*/}\" -F \"number=$PR_NUMBER\" \\\n --jq '.data.repository.pullRequest.reviewThreads.nodes' \\\n | jq -s 'add // []' > /tmp/pr-context/review_comments.json\n\n# Per-file review threads (mirrors diffs/ structure)\njq -c '.[]' /tmp/pr-context/review_comments.json | while IFS= read -r thread; do\n filepath=$(echo \"$thread\" | jq -r '.path // empty')\n [ -z \"$filepath\" ] && continue\n mkdir -p \"/tmp/pr-context/threads/$(dirname \"$filepath\")\"\n echo \"$thread\" >> \"/tmp/pr-context/threads/${filepath}.jsonl\"\ndone\n# Convert per-file JSONL to proper JSON arrays\nmkdir -p /tmp/pr-context/threads\nfind /tmp/pr-context/threads -name '*.jsonl' 2>/dev/null | while IFS= read -r jsonl; do\n jq -s '.' \"$jsonl\" > \"${jsonl%.jsonl}.json\"\n rm \"$jsonl\"\ndone\n\n# PR discussion comments\ngh api \"repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments\" --paginate \\\n | jq -s 'add // []' > /tmp/pr-context/comments.json\n\n# Linked issues\njq -r '.body // \"\"' /tmp/pr-context/pr.json 2>/dev/null \\\n | grep -oiE '(fixes|closes|resolves)\\s+#[0-9]+' \\\n | grep -oE '[0-9]+$' \\\n | sort -u \\\n | while read -r issue; do\n gh api \"repos/$GITHUB_REPOSITORY/issues/$issue\" > \"/tmp/pr-context/issue-${issue}.json\" || true\n done || true\n\n# Write manifest\ncat > /tmp/pr-context/README.md << 'MANIFEST'\n# PR Context\n\nPre-fetched PR data. All files are in `/tmp/pr-context/`.\n\n| File | Description |\n| --- | --- |\n| `pr.json` | PR metadata — title, body, author, base/head branches, head commit SHA (`headRefOid`), URL |\n| `pr.diff` | Full unified diff of all changes |\n| `files.json` | Changed files array — each entry has `filename`, `status`, `additions`, `deletions`, `patch` |\n| `diffs/.diff` | Per-file diffs — one file per changed file, mirroring the repo path under `diffs/` |\n| `file_order_az.txt` | Changed files sorted alphabetically (A→Z), one filename per line |\n| `file_order_za.txt` | Changed files sorted reverse-alphabetically (Z→A), one filename per line |\n| `file_order_largest.txt` | Changed files sorted by diff size descending (largest first), one filename per line |\n| `reviews.json` | Prior review submissions — author, state (APPROVED/CHANGES_REQUESTED/COMMENTED), body |\n| `review_comments.json` | All review threads (GraphQL) — each thread has `id` (node ID for resolving), `isResolved`, `isOutdated`, `path`, `line`, and nested `comments` with `id`, `databaseId` (numeric REST ID for replies), body/author |\n| `threads/.json` | Per-file review threads — one file per changed file with existing threads, mirroring the repo path under `threads/` |\n| `comments.json` | PR discussion comments (not inline) |\n| `issue-{N}.json` | Linked issue details (one file per linked issue, if any) |\n| `agents.md` | Repository conventions from `generate_agents_md` (if written by agent) |\n| `review-instructions.md` | Review instructions, criteria, and calibration examples (if written by review-process fragment) |\nMANIFEST\n\necho \"PR context written to /tmp/pr-context/\"\nls -la /tmp/pr-context/" - env: GITHUB_TOKEN: ${{ github.token }} REPO_NAME: ${{ github.repository }} @@ -1057,6 +1057,26 @@ jobs: return subprocess.run(cmd, capture_output=True, text=True, timeout=60) except subprocess.TimeoutExpired: return subprocess.CompletedProcess(cmd, 1, stdout='', stderr='diff timed out') + + # --- Guard: detect history rewrites and merge commits --- + pr_json_path = '/tmp/pr-context/pr.json' + if os.path.isfile(pr_json_path): + with open(pr_json_path) as f: + pr_data = json.load(f) + pr_head_sha = pr_data.get('headRefOid', '') + if pr_head_sha: + # Check 1: PR head must be an ancestor of HEAD (no rebase/reset) + anc = run(['git', 'merge-base', '--is-ancestor', pr_head_sha, 'HEAD']) + if anc.returncode != 0: + print(json.dumps({'status': 'error', 'error': f'History rewrite detected: the original PR head ({pr_head_sha[:12]}) is not an ancestor of HEAD. This means git rebase, reset, or cherry-pick rewrote history. push_to_pull_request_branch will fail. Fix: reset to the PR head with `git checkout {pr_head_sha}`, re-apply your changes as direct file edits, and commit as regular commits.'})) + raise SystemExit(0) + # Check 2: no merge commits (multiple parents) since PR head + log = run(['git', 'rev-list', '--min-parents=2', f'{pr_head_sha}..HEAD']) + merge_shas = log.stdout.strip() + if merge_shas: + print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... push_to_pull_request_branch uses git format-patch which breaks on merge commits. Fix: reset to the PR head with `git checkout {pr_head_sha}`, re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags), and commit as regular single-parent commits.'})) + raise SystemExit(0) + contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') # Generate diff of all local changes vs upstream for self-review diff --git a/.github/workflows/gh-aw-pr-review.lock.yml b/.github/workflows/gh-aw-pr-review.lock.yml index ebdffd12..096dcb9b 100644 --- a/.github/workflows/gh-aw-pr-review.lock.yml +++ b/.github/workflows/gh-aw-pr-review.lock.yml @@ -40,7 +40,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"1b2161458056b26894b5284f34bbf728738acc188f845d353b31224304013428"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"68a777412c77ebdcfffccbeeb3b012b97cf87e39d9dc272ed9536f3be1993cf4"} name: "PR Review" "on": @@ -685,7 +685,7 @@ jobs: GH_TOKEN: ${{ github.token }} PR_NUMBER: ${{ github.event.pull_request.number || inputs.target-pr-number || github.event.issue.number }} name: Fetch PR context to disk - run: "set -euo pipefail\nmkdir -p /tmp/pr-context\n\n# PR metadata\ngh pr view \"$PR_NUMBER\" --json title,body,author,baseRefName,headRefName,url \\\n > /tmp/pr-context/pr.json\n\n# Full diff\nif ! gh pr diff \"$PR_NUMBER\" > /tmp/pr-context/pr.diff; then\n echo \"::warning::Failed to fetch full PR diff; per-file diffs from files.json are still available.\"\n : > /tmp/pr-context/pr.diff\nfi\n\n# Changed files list (--paginate may output concatenated arrays; jq -s 'add' merges them)\ngh api \"repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/files\" --paginate \\\n | jq -s 'add // []' > /tmp/pr-context/files.json\n\n# Per-file diffs\njq -c '.[]' /tmp/pr-context/files.json | while IFS= read -r entry; do\n filename=$(echo \"$entry\" | jq -r '.filename')\n mkdir -p \"/tmp/pr-context/diffs/$(dirname \"$filename\")\"\n echo \"$entry\" | jq -r '.patch // empty' > \"/tmp/pr-context/diffs/${filename}.diff\"\ndone\n\n# File orderings for sub-agent review (3 strategies)\njq -r '[.[] | .filename] | sort | .[]' /tmp/pr-context/files.json \\\n > /tmp/pr-context/file_order_az.txt\njq -r '[.[] | .filename] | sort | reverse | .[]' /tmp/pr-context/files.json \\\n > /tmp/pr-context/file_order_za.txt\njq -r '[.[] | {filename, size: ((.additions // 0) + (.deletions // 0))}] | sort_by(-.size) | .[].filename' /tmp/pr-context/files.json \\\n > /tmp/pr-context/file_order_largest.txt\n\n# Existing reviews\ngh api \"repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/reviews\" --paginate \\\n | jq -s 'add // []' > /tmp/pr-context/reviews.json\n\n# Review threads with resolution status (GraphQL — REST lacks isResolved/isOutdated)\ngh api graphql --paginate -f query='\n query($owner: String!, $repo: String!, $number: Int!, $endCursor: String) {\n repository(owner: $owner, name: $repo) {\n pullRequest(number: $number) {\n reviewThreads(first: 100, after: $endCursor) {\n pageInfo { hasNextPage endCursor }\n nodes {\n id\n isResolved\n isOutdated\n isCollapsed\n path\n line\n startLine\n comments(first: 100) {\n nodes {\n id\n databaseId\n body\n author { login }\n createdAt\n }\n }\n }\n }\n }\n }\n }\n' -F owner=\"${GITHUB_REPOSITORY%/*}\" -F repo=\"${GITHUB_REPOSITORY#*/}\" -F \"number=$PR_NUMBER\" \\\n --jq '.data.repository.pullRequest.reviewThreads.nodes' \\\n | jq -s 'add // []' > /tmp/pr-context/review_comments.json\n\n# Per-file review threads (mirrors diffs/ structure)\njq -c '.[]' /tmp/pr-context/review_comments.json | while IFS= read -r thread; do\n filepath=$(echo \"$thread\" | jq -r '.path // empty')\n [ -z \"$filepath\" ] && continue\n mkdir -p \"/tmp/pr-context/threads/$(dirname \"$filepath\")\"\n echo \"$thread\" >> \"/tmp/pr-context/threads/${filepath}.jsonl\"\ndone\n# Convert per-file JSONL to proper JSON arrays\nmkdir -p /tmp/pr-context/threads\nfind /tmp/pr-context/threads -name '*.jsonl' 2>/dev/null | while IFS= read -r jsonl; do\n jq -s '.' \"$jsonl\" > \"${jsonl%.jsonl}.json\"\n rm \"$jsonl\"\ndone\n\n# PR discussion comments\ngh api \"repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments\" --paginate \\\n | jq -s 'add // []' > /tmp/pr-context/comments.json\n\n# Linked issues\njq -r '.body // \"\"' /tmp/pr-context/pr.json 2>/dev/null \\\n | grep -oiE '(fixes|closes|resolves)\\s+#[0-9]+' \\\n | grep -oE '[0-9]+$' \\\n | sort -u \\\n | while read -r issue; do\n gh api \"repos/$GITHUB_REPOSITORY/issues/$issue\" > \"/tmp/pr-context/issue-${issue}.json\" || true\n done || true\n\n# Write manifest\ncat > /tmp/pr-context/README.md << 'MANIFEST'\n# PR Context\n\nPre-fetched PR data. All files are in `/tmp/pr-context/`.\n\n| File | Description |\n| --- | --- |\n| `pr.json` | PR metadata — title, body, author, base/head branches, URL |\n| `pr.diff` | Full unified diff of all changes |\n| `files.json` | Changed files array — each entry has `filename`, `status`, `additions`, `deletions`, `patch` |\n| `diffs/.diff` | Per-file diffs — one file per changed file, mirroring the repo path under `diffs/` |\n| `file_order_az.txt` | Changed files sorted alphabetically (A→Z), one filename per line |\n| `file_order_za.txt` | Changed files sorted reverse-alphabetically (Z→A), one filename per line |\n| `file_order_largest.txt` | Changed files sorted by diff size descending (largest first), one filename per line |\n| `reviews.json` | Prior review submissions — author, state (APPROVED/CHANGES_REQUESTED/COMMENTED), body |\n| `review_comments.json` | All review threads (GraphQL) — each thread has `id` (node ID for resolving), `isResolved`, `isOutdated`, `path`, `line`, and nested `comments` with `id`, `databaseId` (numeric REST ID for replies), body/author |\n| `threads/.json` | Per-file review threads — one file per changed file with existing threads, mirroring the repo path under `threads/` |\n| `comments.json` | PR discussion comments (not inline) |\n| `issue-{N}.json` | Linked issue details (one file per linked issue, if any) |\n| `agents.md` | Repository conventions from `generate_agents_md` (if written by agent) |\n| `review-instructions.md` | Review instructions, criteria, and calibration examples (if written by review-process fragment) |\nMANIFEST\n\necho \"PR context written to /tmp/pr-context/\"\nls -la /tmp/pr-context/" + run: "set -euo pipefail\nmkdir -p /tmp/pr-context\n\n# PR metadata\ngh pr view \"$PR_NUMBER\" --json title,body,author,baseRefName,headRefName,headRefOid,url \\\n > /tmp/pr-context/pr.json\n\n# Full diff\nif ! gh pr diff \"$PR_NUMBER\" > /tmp/pr-context/pr.diff; then\n echo \"::warning::Failed to fetch full PR diff; per-file diffs from files.json are still available.\"\n : > /tmp/pr-context/pr.diff\nfi\n\n# Changed files list (--paginate may output concatenated arrays; jq -s 'add' merges them)\ngh api \"repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/files\" --paginate \\\n | jq -s 'add // []' > /tmp/pr-context/files.json\n\n# Per-file diffs\njq -c '.[]' /tmp/pr-context/files.json | while IFS= read -r entry; do\n filename=$(echo \"$entry\" | jq -r '.filename')\n mkdir -p \"/tmp/pr-context/diffs/$(dirname \"$filename\")\"\n echo \"$entry\" | jq -r '.patch // empty' > \"/tmp/pr-context/diffs/${filename}.diff\"\ndone\n\n# File orderings for sub-agent review (3 strategies)\njq -r '[.[] | .filename] | sort | .[]' /tmp/pr-context/files.json \\\n > /tmp/pr-context/file_order_az.txt\njq -r '[.[] | .filename] | sort | reverse | .[]' /tmp/pr-context/files.json \\\n > /tmp/pr-context/file_order_za.txt\njq -r '[.[] | {filename, size: ((.additions // 0) + (.deletions // 0))}] | sort_by(-.size) | .[].filename' /tmp/pr-context/files.json \\\n > /tmp/pr-context/file_order_largest.txt\n\n# Existing reviews\ngh api \"repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/reviews\" --paginate \\\n | jq -s 'add // []' > /tmp/pr-context/reviews.json\n\n# Review threads with resolution status (GraphQL — REST lacks isResolved/isOutdated)\ngh api graphql --paginate -f query='\n query($owner: String!, $repo: String!, $number: Int!, $endCursor: String) {\n repository(owner: $owner, name: $repo) {\n pullRequest(number: $number) {\n reviewThreads(first: 100, after: $endCursor) {\n pageInfo { hasNextPage endCursor }\n nodes {\n id\n isResolved\n isOutdated\n isCollapsed\n path\n line\n startLine\n comments(first: 100) {\n nodes {\n id\n databaseId\n body\n author { login }\n createdAt\n }\n }\n }\n }\n }\n }\n }\n' -F owner=\"${GITHUB_REPOSITORY%/*}\" -F repo=\"${GITHUB_REPOSITORY#*/}\" -F \"number=$PR_NUMBER\" \\\n --jq '.data.repository.pullRequest.reviewThreads.nodes' \\\n | jq -s 'add // []' > /tmp/pr-context/review_comments.json\n\n# Per-file review threads (mirrors diffs/ structure)\njq -c '.[]' /tmp/pr-context/review_comments.json | while IFS= read -r thread; do\n filepath=$(echo \"$thread\" | jq -r '.path // empty')\n [ -z \"$filepath\" ] && continue\n mkdir -p \"/tmp/pr-context/threads/$(dirname \"$filepath\")\"\n echo \"$thread\" >> \"/tmp/pr-context/threads/${filepath}.jsonl\"\ndone\n# Convert per-file JSONL to proper JSON arrays\nmkdir -p /tmp/pr-context/threads\nfind /tmp/pr-context/threads -name '*.jsonl' 2>/dev/null | while IFS= read -r jsonl; do\n jq -s '.' \"$jsonl\" > \"${jsonl%.jsonl}.json\"\n rm \"$jsonl\"\ndone\n\n# PR discussion comments\ngh api \"repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments\" --paginate \\\n | jq -s 'add // []' > /tmp/pr-context/comments.json\n\n# Linked issues\njq -r '.body // \"\"' /tmp/pr-context/pr.json 2>/dev/null \\\n | grep -oiE '(fixes|closes|resolves)\\s+#[0-9]+' \\\n | grep -oE '[0-9]+$' \\\n | sort -u \\\n | while read -r issue; do\n gh api \"repos/$GITHUB_REPOSITORY/issues/$issue\" > \"/tmp/pr-context/issue-${issue}.json\" || true\n done || true\n\n# Write manifest\ncat > /tmp/pr-context/README.md << 'MANIFEST'\n# PR Context\n\nPre-fetched PR data. All files are in `/tmp/pr-context/`.\n\n| File | Description |\n| --- | --- |\n| `pr.json` | PR metadata — title, body, author, base/head branches, head commit SHA (`headRefOid`), URL |\n| `pr.diff` | Full unified diff of all changes |\n| `files.json` | Changed files array — each entry has `filename`, `status`, `additions`, `deletions`, `patch` |\n| `diffs/.diff` | Per-file diffs — one file per changed file, mirroring the repo path under `diffs/` |\n| `file_order_az.txt` | Changed files sorted alphabetically (A→Z), one filename per line |\n| `file_order_za.txt` | Changed files sorted reverse-alphabetically (Z→A), one filename per line |\n| `file_order_largest.txt` | Changed files sorted by diff size descending (largest first), one filename per line |\n| `reviews.json` | Prior review submissions — author, state (APPROVED/CHANGES_REQUESTED/COMMENTED), body |\n| `review_comments.json` | All review threads (GraphQL) — each thread has `id` (node ID for resolving), `isResolved`, `isOutdated`, `path`, `line`, and nested `comments` with `id`, `databaseId` (numeric REST ID for replies), body/author |\n| `threads/.json` | Per-file review threads — one file per changed file with existing threads, mirroring the repo path under `threads/` |\n| `comments.json` | PR discussion comments (not inline) |\n| `issue-{N}.json` | Linked issue details (one file per linked issue, if any) |\n| `agents.md` | Repository conventions from `generate_agents_md` (if written by agent) |\n| `review-instructions.md` | Review instructions, criteria, and calibration examples (if written by review-process fragment) |\nMANIFEST\n\necho \"PR context written to /tmp/pr-context/\"\nls -la /tmp/pr-context/" - name: Write review instructions to disk run: "mkdir -p /tmp/pr-context\ncat > /tmp/pr-context/review-instructions.md << 'REVIEW_EOF'\n# Review Instructions for Sub-agents\n\nYou are a code review sub-agent. Read these instructions, then review the PR files in the order provided in your prompt.\n\n## Context\n\nBefore reviewing files, read these to understand the PR:\n\n1. `/tmp/pr-context/pr.json` — PR title, description, author, and branches. Understand what the PR is trying to accomplish.\n2. `/tmp/pr-context/agents.md` — Repository coding conventions and guidelines (if it exists).\n3. `/tmp/pr-context/review_comments.json` — Existing review threads. Note which files already have threads so you don't duplicate.\n4. `/tmp/pr-context/issue-*.json` — Linked issue details (if any). Understand the motivation and acceptance criteria.\n\n## Process\n\nReview the PR diff file by file in your assigned order. For each changed file:\n\n1. **Read the diff** for this file from `/tmp/pr-context/diffs/.diff` to understand what changed. If the diff is empty or truncated (e.g., binary files or very large changes), fall back to reading the full file from the workspace and comparing against context.\n2. **Read the full file from the workspace.** The PR branch is checked out locally — open the file directly to get complete contents with line numbers.\n3. **Check existing threads** for this file from `/tmp/pr-context/threads/.json` (if it exists). Skip issues that are already under discussion — each thread has `isResolved` and `isOutdated` fields.\n4. **Identify potential issues** matching the review criteria below.\n5. **Quick-check each issue** before including it:\n - What specific code pattern or change triggers this concern?\n - Is there an obvious guard, handler, or mitigation visible in the immediate context?\n - Can you describe a concrete failure scenario (the `evidence` field)? If you cannot articulate what specific input or state triggers the problem, drop the finding.\n - If the issue is clearly handled, skip it. If you're unsure, include it — the parent will verify.\n6. **Add to your findings list.** Do NOT leave inline comments — you don't have that tool. Return findings in this format:\n\n```\n- file: path/to/file\n line: 42\n severity: HIGH\n title: Brief title\n description: What the issue is and why it matters\n evidence: The specific code pattern and failure scenario\n suggestion: corrected code here (optional — only if you can provide a concrete fix)\n```\n\n**Review every file in your assigned order.** Files reviewed earlier get more attention, which is why different sub-agents use different orderings.\n\n**Check existing threads** — per-file threads are at `/tmp/pr-context/threads/.json` (step 3 above). The full list is at `/tmp/pr-context/review_comments.json`. Do not flag issues that are already under discussion (resolved or unresolved). For outdated threads, only re-flag if the issue still applies to the current diff.\n\n**Return your full findings list** when done, or an empty list if no issues were found.\n\n## Review Criteria\n\nFocus on these categories in priority order:\n\n1. Security vulnerabilities (injection, XSS, auth bypass, secrets exposure)\n2. Logic bugs that could cause runtime failures or incorrect behavior\n3. Data integrity issues (race conditions, missing transactions, corruption risk)\n4. Performance bottlenecks (N+1 queries, memory leaks, blocking operations)\n5. Error handling gaps (unhandled exceptions, missing validation)\n6. Breaking changes to public APIs without migration path\n7. Missing or incorrect test coverage for critical paths\n\n## What NOT to Flag\n\nOnly review the diff — do not flag issues in unchanged code, pre-existing problems not introduced by this PR, or style preferences handled by linters or formatters.\n\n**Common false positives** — these patterns look like issues but usually aren't. Before flagging anything in these categories, confirm the problem is real by reading the surrounding code:\n\n- **Security — input already sanitized:** Don't flag injection or XSS risks when inputs are sanitized upstream, parameterized queries are used, or the framework auto-escapes output.\n- **Null/undefined — guarded elsewhere:** Don't flag potential null dereferences if the value is guaranteed by a type guard, assertion, schema validation, or upstream null check.\n- **Error handling — handled at a different layer:** Don't flag missing try/catch if the caller, middleware, or framework catches and handles the error (e.g., Express error middleware, React error boundaries).\n- **Performance — theoretical, not practical:** Don't flag algorithmic complexity (e.g., O(n^2)) unless N is demonstrably large enough to matter in the actual usage context. \"This could be slow\" without evidence is not actionable.\n- **Validation — exists at another layer:** Don't flag missing input validation if it's handled by an API gateway, middleware, schema validator, or type system.\n- **Test coverage — trivial or generated code:** Don't flag missing tests for trivial getters/setters, auto-generated code, or simple delegation methods.\n- **Style or naming — not in coding guidelines:** Don't flag naming conventions or code style unless they violate the repository's documented coding guidelines (from `generate_agents_md` or CONTRIBUTING docs).\n\n**Existing review threads** — check BEFORE flagging any issue:\n\n- **Resolved with reviewer reply** (e.g. \"This is intentional\") — reviewer's decision is final. Do NOT re-flag.\n- **Resolved without reply** — author likely fixed it. Do NOT re-raise unless the fix introduced a new problem.\n- **Unresolved** — already flagged. Do NOT duplicate.\n- **Outdated** — only re-flag if the issue still applies to the current diff.\n\nWhen in doubt, do not duplicate. Redundant comments erode trust.\n\nFinding no issues is a valid and valuable outcome. An empty findings list is better than findings that waste the author's time or erode trust. Do not manufacture findings to justify your review — if the code is sound, return an empty list.\n\n## Severity Classification\n\nDetermine severity AFTER investigating the issue, not before. First identify the problem and trace through the code, then assign a severity based on the evidence you found.\n\n- 🔴 CRITICAL — Must fix before merge (security vulnerabilities, data corruption, production-breaking bugs)\n- 🟠 HIGH — Should fix before merge (logic errors, missing validation, significant performance issues)\n- 🟡 MEDIUM — Address soon, non-blocking (error handling gaps, suboptimal patterns, missing edge cases)\n- ⚪ LOW — Author discretion (minor improvements, documentation gaps)\n- 💬 NITPICK — Truly optional (stylistic preferences, alternative approaches)\n\n## Review Intensity\n\nThe review intensity is `${{ inputs.intensity || 'balanced' }}`.\n\n- **conservative**: High evidence bar. Only flag when you can demonstrate a concrete failure scenario. If you can construct a reasonable counterargument, do not flag. Approval with zero findings is the expected outcome for most PRs.\n- **balanced**: Standard evidence bar. Flag when you can point to specific code that would fail. If the issue is ambiguous, lean toward not flagging.\n- **aggressive**: Lower evidence bar. Flag when evidence exists even if the failure scenario is not fully confirmed. Improvement suggestions welcome but must cite specific code.\n\n## Calibration Examples\n\nUse these examples to calibrate your judgment. Each pair shows a real issue and a similar-looking pattern that is NOT an issue.\n\n### Example 1: Null/Undefined Access\n\n**True positive — flag this:**\n\n```js\n// PR adds this handler\napp.get('/user/:id', async (req, res) => {\n const user = await db.findUser(req.params.id);\n res.json({ name: user.name, email: user.email });\n});\n```\n\nWhy flag: `db.findUser()` can return `null` when no user matches the ID. Accessing `user.name` will throw a TypeError at runtime. No upstream guard exists — the route handler is the entry point.\n\n**False positive — do NOT flag this:**\n\n```ts\n// PR adds this line inside an existing function\nconst settings = user.getSettings();\n```\n\nWhy skip: Reading the full file reveals `user` is typed as `User` (not `User | null`), and the calling function only runs after `authenticateUser()` middleware which guarantees a valid user object. The null case is handled at a different layer.\n\n### Example 2: SQL Injection\n\n**True positive — flag this:**\n\n```python\n# PR adds this query\ncursor.execute(f\"SELECT * FROM orders WHERE customer_id = '{customer_id}'\")\n```\n\nWhy flag: String interpolation in a SQL query with user-controlled input (`customer_id` comes from the request). No parameterization or sanitization anywhere in the call chain.\n\n**False positive — do NOT flag this:**\n\n```python\n# PR adds this query\ncursor.execute(f\"SELECT * FROM orders WHERE status = '{OrderStatus.PENDING.value}'\")\n```\n\nWhy skip: The interpolated value is a hardcoded enum constant (`OrderStatus.PENDING`), not user input. There is no injection vector.\n\n### Example 3: Borderline — Do NOT Flag\n\n```go\n// PR adds this function\nfunc processItems(items []Item) []Result {\n results := make([]Result, 0)\n for _, item := range items {\n for _, tag := range item.Tags {\n results = append(results, process(item, tag))\n }\n }\n return results\n}\n```\n\nThis looks like an O(n*m) performance concern. But without evidence that `items` or `Tags` are large in practice, this is speculative. The function processes a bounded dataset (items from a single user request). Do not flag theoretical performance issues without evidence of real-world impact.\nREVIEW_EOF" - env: diff --git a/.github/workflows/gh-aw-release-update.lock.yml b/.github/workflows/gh-aw-release-update.lock.yml index e69b541c..b90b01db 100644 --- a/.github/workflows/gh-aw-release-update.lock.yml +++ b/.github/workflows/gh-aw-release-update.lock.yml @@ -36,7 +36,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"540fed2332d999fe5901bc59a882c439fe966cffe03b7910e56f993df13555e6"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"ebd49d31f4bcb3fdefafdfc0ed73041d51c518b88126ead778c1eecf8b18598b"} name: "Release Update Check" "on": @@ -903,6 +903,22 @@ jobs: return subprocess.run(cmd, capture_output=True, text=True, timeout=60) except subprocess.TimeoutExpired: return subprocess.CompletedProcess(cmd, 1, stdout='', stderr='diff timed out') + + # --- Guard: detect merge commits --- + # Find the fork point with the upstream branch to scope the check + upstream_sha = '' + for ref in ['@{upstream}', 'origin/HEAD', 'origin/main']: + r = run(['git', 'merge-base', 'HEAD', ref]) + if r.returncode == 0 and r.stdout.strip(): + upstream_sha = r.stdout.strip() + break + if upstream_sha: + log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) + merge_shas = log.stdout.strip() + if merge_shas: + print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) + raise SystemExit(0) + contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') # Generate diff of all local changes vs upstream for self-review diff --git a/.github/workflows/gh-aw-scheduled-fix.lock.yml b/.github/workflows/gh-aw-scheduled-fix.lock.yml index 8becb0c8..e1120bb1 100644 --- a/.github/workflows/gh-aw-scheduled-fix.lock.yml +++ b/.github/workflows/gh-aw-scheduled-fix.lock.yml @@ -37,7 +37,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"18eac1fd19c9aa09b0f306acbbc1b3a08d4f214f087b25be62fb8b5b8a947e7d"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"a690c02c6c714a6cf2f8f98decf62577aecd3cfe3ebbf77bfb3ef092ce5b83d2"} name: "Scheduled Fix" "on": @@ -958,6 +958,22 @@ jobs: return subprocess.run(cmd, capture_output=True, text=True, timeout=60) except subprocess.TimeoutExpired: return subprocess.CompletedProcess(cmd, 1, stdout='', stderr='diff timed out') + + # --- Guard: detect merge commits --- + # Find the fork point with the upstream branch to scope the check + upstream_sha = '' + for ref in ['@{upstream}', 'origin/HEAD', 'origin/main']: + r = run(['git', 'merge-base', 'HEAD', ref]) + if r.returncode == 0 and r.stdout.strip(): + upstream_sha = r.stdout.strip() + break + if upstream_sha: + log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) + merge_shas = log.stdout.strip() + if merge_shas: + print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) + raise SystemExit(0) + contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') # Generate diff of all local changes vs upstream for self-review diff --git a/.github/workflows/gh-aw-small-problem-fixer.lock.yml b/.github/workflows/gh-aw-small-problem-fixer.lock.yml index 08806e21..3956a359 100644 --- a/.github/workflows/gh-aw-small-problem-fixer.lock.yml +++ b/.github/workflows/gh-aw-small-problem-fixer.lock.yml @@ -37,7 +37,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"bba6645ad30ad38083fb374055d6952f84711b2304ebe53704bb5f00a7043069"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"6e0916ffbc2999265e625637fcf342d769789c55f2242ec968fcc1985df5daf8"} name: "Small Problem Fixer" "on": @@ -986,6 +986,22 @@ jobs: return subprocess.run(cmd, capture_output=True, text=True, timeout=60) except subprocess.TimeoutExpired: return subprocess.CompletedProcess(cmd, 1, stdout='', stderr='diff timed out') + + # --- Guard: detect merge commits --- + # Find the fork point with the upstream branch to scope the check + upstream_sha = '' + for ref in ['@{upstream}', 'origin/HEAD', 'origin/main']: + r = run(['git', 'merge-base', 'HEAD', ref]) + if r.returncode == 0 and r.stdout.strip(): + upstream_sha = r.stdout.strip() + break + if upstream_sha: + log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) + merge_shas = log.stdout.strip() + if merge_shas: + print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) + raise SystemExit(0) + contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') # Generate diff of all local changes vs upstream for self-review diff --git a/.github/workflows/gh-aw-test-improvement.lock.yml b/.github/workflows/gh-aw-test-improvement.lock.yml index f8296b42..6b2dfd7c 100644 --- a/.github/workflows/gh-aw-test-improvement.lock.yml +++ b/.github/workflows/gh-aw-test-improvement.lock.yml @@ -41,7 +41,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"08d168e02162376fdd5be78c4b6cfa1f910732fad86deb9e101fab2c9271416d"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"9d6fcf1b2ca7c07f32f39938d2513e478c6e9a41cc283d763d9d05ac6779c820"} name: "Test Improver" "on": @@ -946,6 +946,22 @@ jobs: return subprocess.run(cmd, capture_output=True, text=True, timeout=60) except subprocess.TimeoutExpired: return subprocess.CompletedProcess(cmd, 1, stdout='', stderr='diff timed out') + + # --- Guard: detect merge commits --- + # Find the fork point with the upstream branch to scope the check + upstream_sha = '' + for ref in ['@{upstream}', 'origin/HEAD', 'origin/main']: + r = run(['git', 'merge-base', 'HEAD', ref]) + if r.returncode == 0 and r.stdout.strip(): + upstream_sha = r.stdout.strip() + break + if upstream_sha: + log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) + merge_shas = log.stdout.strip() + if merge_shas: + print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) + raise SystemExit(0) + contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') # Generate diff of all local changes vs upstream for self-review diff --git a/.github/workflows/gh-aw-test-improver.lock.yml b/.github/workflows/gh-aw-test-improver.lock.yml index 0106c706..ecfea747 100644 --- a/.github/workflows/gh-aw-test-improver.lock.yml +++ b/.github/workflows/gh-aw-test-improver.lock.yml @@ -36,7 +36,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"08d168e02162376fdd5be78c4b6cfa1f910732fad86deb9e101fab2c9271416d"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"9d6fcf1b2ca7c07f32f39938d2513e478c6e9a41cc283d763d9d05ac6779c820"} name: "Test Improver" "on": @@ -941,6 +941,22 @@ jobs: return subprocess.run(cmd, capture_output=True, text=True, timeout=60) except subprocess.TimeoutExpired: return subprocess.CompletedProcess(cmd, 1, stdout='', stderr='diff timed out') + + # --- Guard: detect merge commits --- + # Find the fork point with the upstream branch to scope the check + upstream_sha = '' + for ref in ['@{upstream}', 'origin/HEAD', 'origin/main']: + r = run(['git', 'merge-base', 'HEAD', ref]) + if r.returncode == 0 and r.stdout.strip(): + upstream_sha = r.stdout.strip() + break + if upstream_sha: + log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) + merge_shas = log.stdout.strip() + if merge_shas: + print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) + raise SystemExit(0) + contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') # Generate diff of all local changes vs upstream for self-review diff --git a/.github/workflows/gh-aw-text-beautifier.lock.yml b/.github/workflows/gh-aw-text-beautifier.lock.yml index 445d7740..9a90a53b 100644 --- a/.github/workflows/gh-aw-text-beautifier.lock.yml +++ b/.github/workflows/gh-aw-text-beautifier.lock.yml @@ -38,7 +38,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"8276eb97a80c0abd997c5d568c174ee440b4fa4624945e95e0e21aa0a1bb3f37"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"b12f8da03f4f92ec82e6567f0492a9a5289486ab806edc78692288d2472197f8"} name: "Text Beautifier" "on": @@ -943,6 +943,22 @@ jobs: return subprocess.run(cmd, capture_output=True, text=True, timeout=60) except subprocess.TimeoutExpired: return subprocess.CompletedProcess(cmd, 1, stdout='', stderr='diff timed out') + + # --- Guard: detect merge commits --- + # Find the fork point with the upstream branch to scope the check + upstream_sha = '' + for ref in ['@{upstream}', 'origin/HEAD', 'origin/main']: + r = run(['git', 'merge-base', 'HEAD', ref]) + if r.returncode == 0 and r.stdout.strip(): + upstream_sha = r.stdout.strip() + break + if upstream_sha: + log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) + merge_shas = log.stdout.strip() + if merge_shas: + print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) + raise SystemExit(0) + contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') # Generate diff of all local changes vs upstream for self-review From 6e583d4136dc7e7edcb3a3066e7b53baa65b3a9e Mon Sep 17 00:00:00 2001 From: William Easton Date: Sat, 28 Feb 2026 00:52:59 -0600 Subject: [PATCH 4/8] Fix pytest failures and add guard tests - Remove `---` from Python comments in safe-input fragments that broke the test's YAML frontmatter extraction (split on `---`) - Update test_fragments_have_identical_py to test_fragments_share_common_logic since push and create fragments now have different guard sections - Add TestPushGuards: ancestry rewrite detection, merge commit detection, no-pr-json passthrough, normal commit passthrough - Add TestCreateGuards: merge commit detection, normal commit passthrough - Recompile lock files after merging origin/main Co-Authored-By: Claude Opus 4.6 --- .github/workflows/downstream-users.lock.yml | 4 +- .../workflows/gh-aw-bug-exterminator.lock.yml | 4 +- .../gh-aw-code-duplication-fixer.lock.yml | 4 +- .../workflows/gh-aw-code-simplifier.lock.yml | 4 +- .../gh-aw-fragments/safe-output-create-pr.md | 2 +- .../gh-aw-fragments/safe-output-push-to-pr.md | 2 +- .github/workflows/gh-aw-issue-fixer.lock.yml | 4 +- ...gh-aw-mention-in-issue-no-sandbox.lock.yml | 4 +- .../workflows/gh-aw-mention-in-issue.lock.yml | 4 +- .../gh-aw-mention-in-pr-by-id.lock.yml | 4 +- .../gh-aw-mention-in-pr-no-sandbox.lock.yml | 4 +- .../workflows/gh-aw-mention-in-pr.lock.yml | 24 ++- .../gh-aw-newbie-contributor-fixer.lock.yml | 4 +- .../workflows/gh-aw-pr-actions-fixer.lock.yml | 4 +- .../gh-aw-pr-review-addresser.lock.yml | 4 +- .../workflows/gh-aw-release-update.lock.yml | 4 +- .../workflows/gh-aw-scheduled-fix.lock.yml | 4 +- .../gh-aw-small-problem-fixer.lock.yml | 4 +- .../workflows/gh-aw-test-improvement.lock.yml | 4 +- .../workflows/gh-aw-test-improver.lock.yml | 4 +- .../workflows/gh-aw-text-beautifier.lock.yml | 4 +- tests/test_safe_input_ready_to_make_pr.py | 166 +++++++++++++++++- 22 files changed, 223 insertions(+), 43 deletions(-) diff --git a/.github/workflows/downstream-users.lock.yml b/.github/workflows/downstream-users.lock.yml index b5115bd2..0cbbbb11 100644 --- a/.github/workflows/downstream-users.lock.yml +++ b/.github/workflows/downstream-users.lock.yml @@ -36,7 +36,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"75a0c54b445845fa523a41bd46bdb98c85e1a1a5028213eca1d7a8c37d0f6a5b"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"ca0c06fcccf3ad9313ebe550cac79cc41020061ac13e5693312e79cfc0a1ff0d"} name: "Internal: Downstream Users" "on": @@ -884,7 +884,7 @@ jobs: except subprocess.TimeoutExpired: return subprocess.CompletedProcess(cmd, 1, stdout='', stderr='diff timed out') - # --- Guard: detect merge commits --- + # Guard: detect merge commits # Find the fork point with the upstream branch to scope the check upstream_sha = '' for ref in ['@{upstream}', 'origin/HEAD', 'origin/main']: diff --git a/.github/workflows/gh-aw-bug-exterminator.lock.yml b/.github/workflows/gh-aw-bug-exterminator.lock.yml index eb668fa7..8b5e4e15 100644 --- a/.github/workflows/gh-aw-bug-exterminator.lock.yml +++ b/.github/workflows/gh-aw-bug-exterminator.lock.yml @@ -37,7 +37,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"56ffeeeb47252fb1cc3a8b75178e9c6f97a33a7f6fb82f53dd2ce084d85df4d5"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"a2b480df98fe924dccae1ea22c4330d416d7cbd9bbfb9367ba1df6cedb576511"} name: "Gh Aw Bug Exterminator" "on": @@ -933,7 +933,7 @@ jobs: except subprocess.TimeoutExpired: return subprocess.CompletedProcess(cmd, 1, stdout='', stderr='diff timed out') - # --- Guard: detect merge commits --- + # Guard: detect merge commits # Find the fork point with the upstream branch to scope the check upstream_sha = '' for ref in ['@{upstream}', 'origin/HEAD', 'origin/main']: diff --git a/.github/workflows/gh-aw-code-duplication-fixer.lock.yml b/.github/workflows/gh-aw-code-duplication-fixer.lock.yml index febe1257..d1627e7b 100644 --- a/.github/workflows/gh-aw-code-duplication-fixer.lock.yml +++ b/.github/workflows/gh-aw-code-duplication-fixer.lock.yml @@ -37,7 +37,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"f4ca568e3a2a661cd29b5a5f4720abec9b7453950ecaeb890f9fe22c464b7a5d"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"9a2d9b699fd61c41d444bc9e64726b99bc00fa33a8c8536b4478307ee0f8ede9"} name: "Code Duplication Fixer" "on": @@ -935,7 +935,7 @@ jobs: except subprocess.TimeoutExpired: return subprocess.CompletedProcess(cmd, 1, stdout='', stderr='diff timed out') - # --- Guard: detect merge commits --- + # Guard: detect merge commits # Find the fork point with the upstream branch to scope the check upstream_sha = '' for ref in ['@{upstream}', 'origin/HEAD', 'origin/main']: diff --git a/.github/workflows/gh-aw-code-simplifier.lock.yml b/.github/workflows/gh-aw-code-simplifier.lock.yml index d5230ab3..cb246cd7 100644 --- a/.github/workflows/gh-aw-code-simplifier.lock.yml +++ b/.github/workflows/gh-aw-code-simplifier.lock.yml @@ -37,7 +37,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"95b8c433cc8d86f8ac6b7d628f2df4d4295627e128cbc061b01b7ae458a755c9"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"4e1adfae148ee8b5f62112a63e914f9ca815233d1dd0ff515f563e2e1d90a44c"} name: "Code Simplifier" "on": @@ -950,7 +950,7 @@ jobs: except subprocess.TimeoutExpired: return subprocess.CompletedProcess(cmd, 1, stdout='', stderr='diff timed out') - # --- Guard: detect merge commits --- + # Guard: detect merge commits # Find the fork point with the upstream branch to scope the check upstream_sha = '' for ref in ['@{upstream}', 'origin/HEAD', 'origin/main']: diff --git a/.github/workflows/gh-aw-fragments/safe-output-create-pr.md b/.github/workflows/gh-aw-fragments/safe-output-create-pr.md index b454eb23..726be60f 100644 --- a/.github/workflows/gh-aw-fragments/safe-output-create-pr.md +++ b/.github/workflows/gh-aw-fragments/safe-output-create-pr.md @@ -12,7 +12,7 @@ safe-inputs: except subprocess.TimeoutExpired: return subprocess.CompletedProcess(cmd, 1, stdout='', stderr='diff timed out') - # --- Guard: detect merge commits --- + # Guard: detect merge commits # Find the fork point with the upstream branch to scope the check upstream_sha = '' for ref in ['@{upstream}', 'origin/HEAD', 'origin/main']: diff --git a/.github/workflows/gh-aw-fragments/safe-output-push-to-pr.md b/.github/workflows/gh-aw-fragments/safe-output-push-to-pr.md index efae04dd..3c79e6a3 100644 --- a/.github/workflows/gh-aw-fragments/safe-output-push-to-pr.md +++ b/.github/workflows/gh-aw-fragments/safe-output-push-to-pr.md @@ -12,7 +12,7 @@ safe-inputs: except subprocess.TimeoutExpired: return subprocess.CompletedProcess(cmd, 1, stdout='', stderr='diff timed out') - # --- Guard: detect history rewrites and merge commits --- + # Guard: detect history rewrites and merge commits pr_json_path = '/tmp/pr-context/pr.json' if os.path.isfile(pr_json_path): with open(pr_json_path) as f: diff --git a/.github/workflows/gh-aw-issue-fixer.lock.yml b/.github/workflows/gh-aw-issue-fixer.lock.yml index 5eabfd21..d26fac18 100644 --- a/.github/workflows/gh-aw-issue-fixer.lock.yml +++ b/.github/workflows/gh-aw-issue-fixer.lock.yml @@ -38,7 +38,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"548c2451749f5e4866f4cebb640738e071869e9415bddc55449bca0dc09b9d25"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"26711f1b6d616d268f730a5feee132792f43226b50595f2034a43814d6ca16e7"} name: "Issue Fixer" "on": @@ -983,7 +983,7 @@ jobs: except subprocess.TimeoutExpired: return subprocess.CompletedProcess(cmd, 1, stdout='', stderr='diff timed out') - # --- Guard: detect merge commits --- + # Guard: detect merge commits # Find the fork point with the upstream branch to scope the check upstream_sha = '' for ref in ['@{upstream}', 'origin/HEAD', 'origin/main']: diff --git a/.github/workflows/gh-aw-mention-in-issue-no-sandbox.lock.yml b/.github/workflows/gh-aw-mention-in-issue-no-sandbox.lock.yml index 1d8ad6d5..288653ab 100644 --- a/.github/workflows/gh-aw-mention-in-issue-no-sandbox.lock.yml +++ b/.github/workflows/gh-aw-mention-in-issue-no-sandbox.lock.yml @@ -39,7 +39,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"a0312c9ee73a9ed2f28198390731953134a165619ff73028509b16f7487f156f"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"cefaf06cce0d7fdb9c2c54a5837643bd723e1e065c7921c9a96d724f2f7b1d2b"} name: "Mention in Issue (no sandbox)" "on": @@ -1071,7 +1071,7 @@ jobs: except subprocess.TimeoutExpired: return subprocess.CompletedProcess(cmd, 1, stdout='', stderr='diff timed out') - # --- Guard: detect merge commits --- + # Guard: detect merge commits # Find the fork point with the upstream branch to scope the check upstream_sha = '' for ref in ['@{upstream}', 'origin/HEAD', 'origin/main']: diff --git a/.github/workflows/gh-aw-mention-in-issue.lock.yml b/.github/workflows/gh-aw-mention-in-issue.lock.yml index ba30d73a..86de06d2 100644 --- a/.github/workflows/gh-aw-mention-in-issue.lock.yml +++ b/.github/workflows/gh-aw-mention-in-issue.lock.yml @@ -39,7 +39,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"31bfa189e40c08f467f697aee13817bdb6a3ff1b1a6bbd6f85ffe1e78e0d1b39"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"4b0cc70027a93055b66eeaba3d1b8f77ac40c1f855d16fc3be9ba71fb5b4caea"} name: "Mention in Issue" "on": @@ -1075,7 +1075,7 @@ jobs: except subprocess.TimeoutExpired: return subprocess.CompletedProcess(cmd, 1, stdout='', stderr='diff timed out') - # --- Guard: detect merge commits --- + # Guard: detect merge commits # Find the fork point with the upstream branch to scope the check upstream_sha = '' for ref in ['@{upstream}', 'origin/HEAD', 'origin/main']: diff --git a/.github/workflows/gh-aw-mention-in-pr-by-id.lock.yml b/.github/workflows/gh-aw-mention-in-pr-by-id.lock.yml index e3726a86..82a4adca 100644 --- a/.github/workflows/gh-aw-mention-in-pr-by-id.lock.yml +++ b/.github/workflows/gh-aw-mention-in-pr-by-id.lock.yml @@ -43,7 +43,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"d3fd483a5228aa1877ae86b4bc4290440568738e5c08e318a9f37a78cf46d4d7"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"7dca61f11e2522195822f429e26d90495c98b0658654dd5d14fc0e8493d878c1"} name: "Mention in PR by ID" "on": @@ -1231,7 +1231,7 @@ jobs: except subprocess.TimeoutExpired: return subprocess.CompletedProcess(cmd, 1, stdout='', stderr='diff timed out') - # --- Guard: detect history rewrites and merge commits --- + # Guard: detect history rewrites and merge commits pr_json_path = '/tmp/pr-context/pr.json' if os.path.isfile(pr_json_path): with open(pr_json_path) as f: diff --git a/.github/workflows/gh-aw-mention-in-pr-no-sandbox.lock.yml b/.github/workflows/gh-aw-mention-in-pr-no-sandbox.lock.yml index b52ed1f6..9e261a0e 100644 --- a/.github/workflows/gh-aw-mention-in-pr-no-sandbox.lock.yml +++ b/.github/workflows/gh-aw-mention-in-pr-no-sandbox.lock.yml @@ -44,7 +44,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"fce25679276da6f63524ad7cb35eae8207001cf0f96e8312f33bd460276d7847"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"c29c75e4f028a88a635b714d38c1b9181e3205551aa2e8ed503d30a318817d1f"} name: "Mention in PR (no sandbox)" "on": @@ -1335,7 +1335,7 @@ jobs: except subprocess.TimeoutExpired: return subprocess.CompletedProcess(cmd, 1, stdout='', stderr='diff timed out') - # --- Guard: detect history rewrites and merge commits --- + # Guard: detect history rewrites and merge commits pr_json_path = '/tmp/pr-context/pr.json' if os.path.isfile(pr_json_path): with open(pr_json_path) as f: diff --git a/.github/workflows/gh-aw-mention-in-pr.lock.yml b/.github/workflows/gh-aw-mention-in-pr.lock.yml index 667ca3cf..93fbd4df 100644 --- a/.github/workflows/gh-aw-mention-in-pr.lock.yml +++ b/.github/workflows/gh-aw-mention-in-pr.lock.yml @@ -44,7 +44,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"6c993989c5542e01f4c2ab3a0f4de5322c78a0a29f2a93f87ca68fb54610077f"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"7a4f68db703c96b3a51a88db89ef4369b7dd562d2787954f2878c7f878d422a8"} name: "Mention in PR" "on": @@ -758,7 +758,7 @@ jobs: GH_TOKEN: ${{ github.token }} PR_NUMBER: ${{ github.event.pull_request.number || inputs.target-pr-number || github.event.issue.number }} name: Fetch PR context to disk - run: "set -euo pipefail\nmkdir -p /tmp/pr-context\n\n# PR metadata\ngh pr view \"$PR_NUMBER\" --json title,body,author,baseRefName,headRefName,url \\\n > /tmp/pr-context/pr.json\n\n# Full diff\nif ! gh pr diff \"$PR_NUMBER\" > /tmp/pr-context/pr.diff; then\n echo \"::warning::Failed to fetch full PR diff; per-file diffs from files.json are still available.\"\n : > /tmp/pr-context/pr.diff\nfi\n\n# Changed files list (--paginate may output concatenated arrays; jq -s 'add' merges them)\ngh api \"repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/files\" --paginate \\\n | jq -s 'add // []' > /tmp/pr-context/files.json\n\n# Per-file diffs\njq -c '.[]' /tmp/pr-context/files.json | while IFS= read -r entry; do\n filename=$(echo \"$entry\" | jq -r '.filename')\n mkdir -p \"/tmp/pr-context/diffs/$(dirname \"$filename\")\"\n echo \"$entry\" | jq -r '.patch // empty' > \"/tmp/pr-context/diffs/${filename}.diff\"\ndone\n\n# File orderings for sub-agent review (3 strategies)\njq -r '[.[] | .filename] | sort | .[]' /tmp/pr-context/files.json \\\n > /tmp/pr-context/file_order_az.txt\njq -r '[.[] | .filename] | sort | reverse | .[]' /tmp/pr-context/files.json \\\n > /tmp/pr-context/file_order_za.txt\njq -r '[.[] | {filename, size: ((.additions // 0) + (.deletions // 0))}] | sort_by(-.size) | .[].filename' /tmp/pr-context/files.json \\\n > /tmp/pr-context/file_order_largest.txt\n\n# Existing reviews\ngh api \"repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/reviews\" --paginate \\\n | jq -s 'add // []' > /tmp/pr-context/reviews.json\n\n# Review threads with resolution status (GraphQL — REST lacks isResolved/isOutdated)\ngh api graphql --paginate -f query='\n query($owner: String!, $repo: String!, $number: Int!, $endCursor: String) {\n repository(owner: $owner, name: $repo) {\n pullRequest(number: $number) {\n reviewThreads(first: 100, after: $endCursor) {\n pageInfo { hasNextPage endCursor }\n nodes {\n id\n isResolved\n isOutdated\n isCollapsed\n path\n line\n startLine\n comments(first: 100) {\n nodes {\n id\n databaseId\n body\n author { login }\n createdAt\n }\n }\n }\n }\n }\n }\n }\n' -F owner=\"${GITHUB_REPOSITORY%/*}\" -F repo=\"${GITHUB_REPOSITORY#*/}\" -F \"number=$PR_NUMBER\" \\\n --jq '.data.repository.pullRequest.reviewThreads.nodes' \\\n | jq -s 'add // []' > /tmp/pr-context/review_comments.json\n\n# Per-file review threads (mirrors diffs/ structure)\njq -c '.[]' /tmp/pr-context/review_comments.json | while IFS= read -r thread; do\n filepath=$(echo \"$thread\" | jq -r '.path // empty')\n [ -z \"$filepath\" ] && continue\n mkdir -p \"/tmp/pr-context/threads/$(dirname \"$filepath\")\"\n echo \"$thread\" >> \"/tmp/pr-context/threads/${filepath}.jsonl\"\ndone\n# Convert per-file JSONL to proper JSON arrays\nmkdir -p /tmp/pr-context/threads\nfind /tmp/pr-context/threads -name '*.jsonl' 2>/dev/null | while IFS= read -r jsonl; do\n jq -s '.' \"$jsonl\" > \"${jsonl%.jsonl}.json\"\n rm \"$jsonl\"\ndone\n\n# PR discussion comments\ngh api \"repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments\" --paginate \\\n | jq -s 'add // []' > /tmp/pr-context/comments.json\n\n# Linked issues\njq -r '.body // \"\"' /tmp/pr-context/pr.json 2>/dev/null \\\n | grep -oiE '(fixes|closes|resolves)\\s+#[0-9]+' \\\n | grep -oE '[0-9]+$' \\\n | sort -u \\\n | while read -r issue; do\n gh api \"repos/$GITHUB_REPOSITORY/issues/$issue\" > \"/tmp/pr-context/issue-${issue}.json\" || true\n done || true\n\n# Write manifest\ncat > /tmp/pr-context/README.md << 'MANIFEST'\n# PR Context\n\nPre-fetched PR data. All files are in `/tmp/pr-context/`.\n\n| File | Description |\n| --- | --- |\n| `pr.json` | PR metadata — title, body, author, base/head branches, URL |\n| `pr.diff` | Full unified diff of all changes |\n| `files.json` | Changed files array — each entry has `filename`, `status`, `additions`, `deletions`, `patch` |\n| `diffs/.diff` | Per-file diffs — one file per changed file, mirroring the repo path under `diffs/` |\n| `file_order_az.txt` | Changed files sorted alphabetically (A→Z), one filename per line |\n| `file_order_za.txt` | Changed files sorted reverse-alphabetically (Z→A), one filename per line |\n| `file_order_largest.txt` | Changed files sorted by diff size descending (largest first), one filename per line |\n| `reviews.json` | Prior review submissions — author, state (APPROVED/CHANGES_REQUESTED/COMMENTED), body |\n| `review_comments.json` | All review threads (GraphQL) — each thread has `id` (node ID for resolving), `isResolved`, `isOutdated`, `path`, `line`, and nested `comments` with `id`, `databaseId` (numeric REST ID for replies), body/author |\n| `threads/.json` | Per-file review threads — one file per changed file with existing threads, mirroring the repo path under `threads/` |\n| `comments.json` | PR discussion comments (not inline) |\n| `issue-{N}.json` | Linked issue details (one file per linked issue, if any) |\n| `agents.md` | Repository conventions from `generate_agents_md` (if written by agent) |\n| `review-instructions.md` | Review instructions, criteria, and calibration examples (if written by review-process fragment) |\nMANIFEST\n\necho \"PR context written to /tmp/pr-context/\"\nls -la /tmp/pr-context/" + run: "set -euo pipefail\nmkdir -p /tmp/pr-context\n\n# PR metadata\ngh pr view \"$PR_NUMBER\" --json title,body,author,baseRefName,headRefName,headRefOid,url \\\n > /tmp/pr-context/pr.json\n\n# Full diff\nif ! gh pr diff \"$PR_NUMBER\" > /tmp/pr-context/pr.diff; then\n echo \"::warning::Failed to fetch full PR diff; per-file diffs from files.json are still available.\"\n : > /tmp/pr-context/pr.diff\nfi\n\n# Changed files list (--paginate may output concatenated arrays; jq -s 'add' merges them)\ngh api \"repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/files\" --paginate \\\n | jq -s 'add // []' > /tmp/pr-context/files.json\n\n# Per-file diffs\njq -c '.[]' /tmp/pr-context/files.json | while IFS= read -r entry; do\n filename=$(echo \"$entry\" | jq -r '.filename')\n mkdir -p \"/tmp/pr-context/diffs/$(dirname \"$filename\")\"\n echo \"$entry\" | jq -r '.patch // empty' > \"/tmp/pr-context/diffs/${filename}.diff\"\ndone\n\n# File orderings for sub-agent review (3 strategies)\njq -r '[.[] | .filename] | sort | .[]' /tmp/pr-context/files.json \\\n > /tmp/pr-context/file_order_az.txt\njq -r '[.[] | .filename] | sort | reverse | .[]' /tmp/pr-context/files.json \\\n > /tmp/pr-context/file_order_za.txt\njq -r '[.[] | {filename, size: ((.additions // 0) + (.deletions // 0))}] | sort_by(-.size) | .[].filename' /tmp/pr-context/files.json \\\n > /tmp/pr-context/file_order_largest.txt\n\n# Existing reviews\ngh api \"repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/reviews\" --paginate \\\n | jq -s 'add // []' > /tmp/pr-context/reviews.json\n\n# Review threads with resolution status (GraphQL — REST lacks isResolved/isOutdated)\ngh api graphql --paginate -f query='\n query($owner: String!, $repo: String!, $number: Int!, $endCursor: String) {\n repository(owner: $owner, name: $repo) {\n pullRequest(number: $number) {\n reviewThreads(first: 100, after: $endCursor) {\n pageInfo { hasNextPage endCursor }\n nodes {\n id\n isResolved\n isOutdated\n isCollapsed\n path\n line\n startLine\n comments(first: 100) {\n nodes {\n id\n databaseId\n body\n author { login }\n createdAt\n }\n }\n }\n }\n }\n }\n }\n' -F owner=\"${GITHUB_REPOSITORY%/*}\" -F repo=\"${GITHUB_REPOSITORY#*/}\" -F \"number=$PR_NUMBER\" \\\n --jq '.data.repository.pullRequest.reviewThreads.nodes' \\\n | jq -s 'add // []' > /tmp/pr-context/review_comments.json\n\n# Per-file review threads (mirrors diffs/ structure)\njq -c '.[]' /tmp/pr-context/review_comments.json | while IFS= read -r thread; do\n filepath=$(echo \"$thread\" | jq -r '.path // empty')\n [ -z \"$filepath\" ] && continue\n mkdir -p \"/tmp/pr-context/threads/$(dirname \"$filepath\")\"\n echo \"$thread\" >> \"/tmp/pr-context/threads/${filepath}.jsonl\"\ndone\n# Convert per-file JSONL to proper JSON arrays\nmkdir -p /tmp/pr-context/threads\nfind /tmp/pr-context/threads -name '*.jsonl' 2>/dev/null | while IFS= read -r jsonl; do\n jq -s '.' \"$jsonl\" > \"${jsonl%.jsonl}.json\"\n rm \"$jsonl\"\ndone\n\n# PR discussion comments\ngh api \"repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments\" --paginate \\\n | jq -s 'add // []' > /tmp/pr-context/comments.json\n\n# Linked issues\njq -r '.body // \"\"' /tmp/pr-context/pr.json 2>/dev/null \\\n | grep -oiE '(fixes|closes|resolves)\\s+#[0-9]+' \\\n | grep -oE '[0-9]+$' \\\n | sort -u \\\n | while read -r issue; do\n gh api \"repos/$GITHUB_REPOSITORY/issues/$issue\" > \"/tmp/pr-context/issue-${issue}.json\" || true\n done || true\n\n# Write manifest\ncat > /tmp/pr-context/README.md << 'MANIFEST'\n# PR Context\n\nPre-fetched PR data. All files are in `/tmp/pr-context/`.\n\n| File | Description |\n| --- | --- |\n| `pr.json` | PR metadata — title, body, author, base/head branches, head commit SHA (`headRefOid`), URL |\n| `pr.diff` | Full unified diff of all changes |\n| `files.json` | Changed files array — each entry has `filename`, `status`, `additions`, `deletions`, `patch` |\n| `diffs/.diff` | Per-file diffs — one file per changed file, mirroring the repo path under `diffs/` |\n| `file_order_az.txt` | Changed files sorted alphabetically (A→Z), one filename per line |\n| `file_order_za.txt` | Changed files sorted reverse-alphabetically (Z→A), one filename per line |\n| `file_order_largest.txt` | Changed files sorted by diff size descending (largest first), one filename per line |\n| `reviews.json` | Prior review submissions — author, state (APPROVED/CHANGES_REQUESTED/COMMENTED), body |\n| `review_comments.json` | All review threads (GraphQL) — each thread has `id` (node ID for resolving), `isResolved`, `isOutdated`, `path`, `line`, and nested `comments` with `id`, `databaseId` (numeric REST ID for replies), body/author |\n| `threads/.json` | Per-file review threads — one file per changed file with existing threads, mirroring the repo path under `threads/` |\n| `comments.json` | PR discussion comments (not inline) |\n| `issue-{N}.json` | Linked issue details (one file per linked issue, if any) |\n| `agents.md` | Repository conventions from `generate_agents_md` (if written by agent) |\n| `review-instructions.md` | Review instructions, criteria, and calibration examples (if written by review-process fragment) |\nMANIFEST\n\necho \"PR context written to /tmp/pr-context/\"\nls -la /tmp/pr-context/" - name: Write review instructions to disk run: "mkdir -p /tmp/pr-context\ncat > /tmp/pr-context/review-instructions.md << 'REVIEW_EOF'\n# Review Instructions for Sub-agents\n\nYou are a code review sub-agent. Read these instructions, then review the PR files in the order provided in your prompt.\n\n## Context\n\nBefore reviewing files, read these to understand the PR:\n\n1. `/tmp/pr-context/pr.json` — PR title, description, author, and branches. Understand what the PR is trying to accomplish.\n2. `/tmp/pr-context/agents.md` — Repository coding conventions and guidelines (if it exists).\n3. `/tmp/pr-context/review_comments.json` — Existing review threads. Note which files already have threads so you don't duplicate.\n4. `/tmp/pr-context/issue-*.json` — Linked issue details (if any). Understand the motivation and acceptance criteria.\n\n## Process\n\nReview the PR diff file by file in your assigned order. For each changed file:\n\n1. **Read the diff** for this file from `/tmp/pr-context/diffs/.diff` to understand what changed. If the diff is empty or truncated (e.g., binary files or very large changes), fall back to reading the full file from the workspace and comparing against context.\n2. **Read the full file from the workspace.** The PR branch is checked out locally — open the file directly to get complete contents with line numbers.\n3. **Check existing threads** for this file from `/tmp/pr-context/threads/.json` (if it exists). Skip issues that are already under discussion — each thread has `isResolved` and `isOutdated` fields.\n4. **Identify potential issues** matching the review criteria below.\n5. **Quick-check each issue** before including it:\n - What specific code pattern or change triggers this concern?\n - Is there an obvious guard, handler, or mitigation visible in the immediate context?\n - Can you describe a concrete failure scenario (the `evidence` field)? If you cannot articulate what specific input or state triggers the problem, drop the finding.\n - If the issue is clearly handled, skip it. If you're unsure, include it — the parent will verify.\n6. **Add to your findings list.** Do NOT leave inline comments — you don't have that tool. Return findings in this format:\n\n```\n- file: path/to/file\n line: 42\n severity: HIGH\n title: Brief title\n description: What the issue is and why it matters\n evidence: The specific code pattern and failure scenario\n suggestion: corrected code here (optional — only if you can provide a concrete fix)\n```\n\n**Review every file in your assigned order.** Files reviewed earlier get more attention, which is why different sub-agents use different orderings.\n\n**Check existing threads** — per-file threads are at `/tmp/pr-context/threads/.json` (step 3 above). The full list is at `/tmp/pr-context/review_comments.json`. Do not flag issues that are already under discussion (resolved or unresolved). For outdated threads, only re-flag if the issue still applies to the current diff.\n\n**Return your full findings list** when done, or an empty list if no issues were found.\n\n## Review Criteria\n\nFocus on these categories in priority order:\n\n1. Security vulnerabilities (injection, XSS, auth bypass, secrets exposure)\n2. Logic bugs that could cause runtime failures or incorrect behavior\n3. Data integrity issues (race conditions, missing transactions, corruption risk)\n4. Performance bottlenecks (N+1 queries, memory leaks, blocking operations)\n5. Error handling gaps (unhandled exceptions, missing validation)\n6. Breaking changes to public APIs without migration path\n7. Missing or incorrect test coverage for critical paths\n\n## What NOT to Flag\n\nOnly review the diff — do not flag issues in unchanged code, pre-existing problems not introduced by this PR, or style preferences handled by linters or formatters.\n\n**Common false positives** — these patterns look like issues but usually aren't. Before flagging anything in these categories, confirm the problem is real by reading the surrounding code:\n\n- **Security — input already sanitized:** Don't flag injection or XSS risks when inputs are sanitized upstream, parameterized queries are used, or the framework auto-escapes output.\n- **Null/undefined — guarded elsewhere:** Don't flag potential null dereferences if the value is guaranteed by a type guard, assertion, schema validation, or upstream null check.\n- **Error handling — handled at a different layer:** Don't flag missing try/catch if the caller, middleware, or framework catches and handles the error (e.g., Express error middleware, React error boundaries).\n- **Performance — theoretical, not practical:** Don't flag algorithmic complexity (e.g., O(n^2)) unless N is demonstrably large enough to matter in the actual usage context. \"This could be slow\" without evidence is not actionable.\n- **Validation — exists at another layer:** Don't flag missing input validation if it's handled by an API gateway, middleware, schema validator, or type system.\n- **Test coverage — trivial or generated code:** Don't flag missing tests for trivial getters/setters, auto-generated code, or simple delegation methods.\n- **Style or naming — not in coding guidelines:** Don't flag naming conventions or code style unless they violate the repository's documented coding guidelines (from `generate_agents_md` or CONTRIBUTING docs).\n\n**Existing review threads** — check BEFORE flagging any issue:\n\n- **Resolved with reviewer reply** (e.g. \"This is intentional\") — reviewer's decision is final. Do NOT re-flag.\n- **Resolved without reply** — author likely fixed it. Do NOT re-raise unless the fix introduced a new problem.\n- **Unresolved** — already flagged. Do NOT duplicate.\n- **Outdated** — only re-flag if the issue still applies to the current diff.\n\nWhen in doubt, do not duplicate. Redundant comments erode trust.\n\nFinding no issues is a valid and valuable outcome. An empty findings list is better than findings that waste the author's time or erode trust. Do not manufacture findings to justify your review — if the code is sound, return an empty list.\n\n## Severity Classification\n\nDetermine severity AFTER investigating the issue, not before. First identify the problem and trace through the code, then assign a severity based on the evidence you found.\n\n- 🔴 CRITICAL — Must fix before merge (security vulnerabilities, data corruption, production-breaking bugs)\n- 🟠 HIGH — Should fix before merge (logic errors, missing validation, significant performance issues)\n- 🟡 MEDIUM — Address soon, non-blocking (error handling gaps, suboptimal patterns, missing edge cases)\n- ⚪ LOW — Author discretion (minor improvements, documentation gaps)\n- 💬 NITPICK — Truly optional (stylistic preferences, alternative approaches)\n\n## Review Intensity\n\nThe review intensity is `${{ inputs.intensity || 'balanced' }}`.\n\n- **conservative**: High evidence bar. Only flag when you can demonstrate a concrete failure scenario. If you can construct a reasonable counterargument, do not flag. Approval with zero findings is the expected outcome for most PRs.\n- **balanced**: Standard evidence bar. Flag when you can point to specific code that would fail. If the issue is ambiguous, lean toward not flagging.\n- **aggressive**: Lower evidence bar. Flag when evidence exists even if the failure scenario is not fully confirmed. Improvement suggestions welcome but must cite specific code.\n\n## Calibration Examples\n\nUse these examples to calibrate your judgment. Each pair shows a real issue and a similar-looking pattern that is NOT an issue.\n\n### Example 1: Null/Undefined Access\n\n**True positive — flag this:**\n\n```js\n// PR adds this handler\napp.get('/user/:id', async (req, res) => {\n const user = await db.findUser(req.params.id);\n res.json({ name: user.name, email: user.email });\n});\n```\n\nWhy flag: `db.findUser()` can return `null` when no user matches the ID. Accessing `user.name` will throw a TypeError at runtime. No upstream guard exists — the route handler is the entry point.\n\n**False positive — do NOT flag this:**\n\n```ts\n// PR adds this line inside an existing function\nconst settings = user.getSettings();\n```\n\nWhy skip: Reading the full file reveals `user` is typed as `User` (not `User | null`), and the calling function only runs after `authenticateUser()` middleware which guarantees a valid user object. The null case is handled at a different layer.\n\n### Example 2: SQL Injection\n\n**True positive — flag this:**\n\n```python\n# PR adds this query\ncursor.execute(f\"SELECT * FROM orders WHERE customer_id = '{customer_id}'\")\n```\n\nWhy flag: String interpolation in a SQL query with user-controlled input (`customer_id` comes from the request). No parameterization or sanitization anywhere in the call chain.\n\n**False positive — do NOT flag this:**\n\n```python\n# PR adds this query\ncursor.execute(f\"SELECT * FROM orders WHERE status = '{OrderStatus.PENDING.value}'\")\n```\n\nWhy skip: The interpolated value is a hardcoded enum constant (`OrderStatus.PENDING`), not user input. There is no injection vector.\n\n### Example 3: Borderline — Do NOT Flag\n\n```go\n// PR adds this function\nfunc processItems(items []Item) []Result {\n results := make([]Result, 0)\n for _, item := range items {\n for _, tag := range item.Tags {\n results = append(results, process(item, tag))\n }\n }\n return results\n}\n```\n\nThis looks like an O(n*m) performance concern. But without evidence that `items` or `Tags` are large in practice, this is speculative. The function processes a bounded dataset (items from a single user request). Do not flag theoretical performance issues without evidence of real-world impact.\nREVIEW_EOF" - env: @@ -1368,6 +1368,26 @@ jobs: return subprocess.run(cmd, capture_output=True, text=True, timeout=60) except subprocess.TimeoutExpired: return subprocess.CompletedProcess(cmd, 1, stdout='', stderr='diff timed out') + + # Guard: detect history rewrites and merge commits + pr_json_path = '/tmp/pr-context/pr.json' + if os.path.isfile(pr_json_path): + with open(pr_json_path) as f: + pr_data = json.load(f) + pr_head_sha = pr_data.get('headRefOid', '') + if pr_head_sha: + # Check 1: PR head must be an ancestor of HEAD (no rebase/reset) + anc = run(['git', 'merge-base', '--is-ancestor', pr_head_sha, 'HEAD']) + if anc.returncode != 0: + print(json.dumps({'status': 'error', 'error': f'History rewrite detected: the original PR head ({pr_head_sha[:12]}) is not an ancestor of HEAD. This means git rebase, reset, or cherry-pick rewrote history. push_to_pull_request_branch will fail. Fix: reset to the PR head with `git checkout {pr_head_sha}`, re-apply your changes as direct file edits, and commit as regular commits.'})) + raise SystemExit(0) + # Check 2: no merge commits (multiple parents) since PR head + log = run(['git', 'rev-list', '--min-parents=2', f'{pr_head_sha}..HEAD']) + merge_shas = log.stdout.strip() + if merge_shas: + print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... push_to_pull_request_branch uses git format-patch which breaks on merge commits. Fix: reset to the PR head with `git checkout {pr_head_sha}`, re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags), and commit as regular single-parent commits.'})) + raise SystemExit(0) + contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') # Generate diff of all local changes vs upstream for self-review diff --git a/.github/workflows/gh-aw-newbie-contributor-fixer.lock.yml b/.github/workflows/gh-aw-newbie-contributor-fixer.lock.yml index b22cf289..8c0d96b0 100644 --- a/.github/workflows/gh-aw-newbie-contributor-fixer.lock.yml +++ b/.github/workflows/gh-aw-newbie-contributor-fixer.lock.yml @@ -37,7 +37,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"db46c91fd6bebb36c863eac3028f32f6a6addada7f803e5fd91a6b6597cc013e"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"642beb3d9533c980d9ca0a9b8898c3559e40aaf86aefaf43f93fd4f952af3350"} name: "Newbie Contributor Fixer" "on": @@ -936,7 +936,7 @@ jobs: except subprocess.TimeoutExpired: return subprocess.CompletedProcess(cmd, 1, stdout='', stderr='diff timed out') - # --- Guard: detect merge commits --- + # Guard: detect merge commits # Find the fork point with the upstream branch to scope the check upstream_sha = '' for ref in ['@{upstream}', 'origin/HEAD', 'origin/main']: diff --git a/.github/workflows/gh-aw-pr-actions-fixer.lock.yml b/.github/workflows/gh-aw-pr-actions-fixer.lock.yml index 65922a9b..ab52f74d 100644 --- a/.github/workflows/gh-aw-pr-actions-fixer.lock.yml +++ b/.github/workflows/gh-aw-pr-actions-fixer.lock.yml @@ -37,7 +37,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"706d5729d0afe6acf1a5efc3d1a5e5c125ff4a4b9b0000fe5bd6d4cd6d6ffa16"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"47dd01faf769b21aa06f536959bc654b370dbac9708994d87022ffbf87e494e9"} name: "PR Actions Fixer" "on": @@ -949,7 +949,7 @@ jobs: except subprocess.TimeoutExpired: return subprocess.CompletedProcess(cmd, 1, stdout='', stderr='diff timed out') - # --- Guard: detect history rewrites and merge commits --- + # Guard: detect history rewrites and merge commits pr_json_path = '/tmp/pr-context/pr.json' if os.path.isfile(pr_json_path): with open(pr_json_path) as f: diff --git a/.github/workflows/gh-aw-pr-review-addresser.lock.yml b/.github/workflows/gh-aw-pr-review-addresser.lock.yml index 6a2c2ba8..3f379783 100644 --- a/.github/workflows/gh-aw-pr-review-addresser.lock.yml +++ b/.github/workflows/gh-aw-pr-review-addresser.lock.yml @@ -40,7 +40,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"05f95a5583fe87b419ce91babee4470e61dc8e38b690f8cfb0d327576c6bdd46"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"3948810d4d9b4b8eccb8d7fa41a55369ed66b59821f3193038f5f1b4e14bf1ac"} name: "PR Review Addresser" "on": @@ -1058,7 +1058,7 @@ jobs: except subprocess.TimeoutExpired: return subprocess.CompletedProcess(cmd, 1, stdout='', stderr='diff timed out') - # --- Guard: detect history rewrites and merge commits --- + # Guard: detect history rewrites and merge commits pr_json_path = '/tmp/pr-context/pr.json' if os.path.isfile(pr_json_path): with open(pr_json_path) as f: diff --git a/.github/workflows/gh-aw-release-update.lock.yml b/.github/workflows/gh-aw-release-update.lock.yml index b90b01db..4d02b475 100644 --- a/.github/workflows/gh-aw-release-update.lock.yml +++ b/.github/workflows/gh-aw-release-update.lock.yml @@ -36,7 +36,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"ebd49d31f4bcb3fdefafdfc0ed73041d51c518b88126ead778c1eecf8b18598b"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"c29059ccd4059710b3ec50c26042207612a2ba956395ddb4fd7bfd051a404f9a"} name: "Release Update Check" "on": @@ -904,7 +904,7 @@ jobs: except subprocess.TimeoutExpired: return subprocess.CompletedProcess(cmd, 1, stdout='', stderr='diff timed out') - # --- Guard: detect merge commits --- + # Guard: detect merge commits # Find the fork point with the upstream branch to scope the check upstream_sha = '' for ref in ['@{upstream}', 'origin/HEAD', 'origin/main']: diff --git a/.github/workflows/gh-aw-scheduled-fix.lock.yml b/.github/workflows/gh-aw-scheduled-fix.lock.yml index e1120bb1..3287ddf7 100644 --- a/.github/workflows/gh-aw-scheduled-fix.lock.yml +++ b/.github/workflows/gh-aw-scheduled-fix.lock.yml @@ -37,7 +37,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"a690c02c6c714a6cf2f8f98decf62577aecd3cfe3ebbf77bfb3ef092ce5b83d2"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"f59e0e683d81777293efe24ff400ef10f4d3180df1bada82315ab80420c478cf"} name: "Scheduled Fix" "on": @@ -959,7 +959,7 @@ jobs: except subprocess.TimeoutExpired: return subprocess.CompletedProcess(cmd, 1, stdout='', stderr='diff timed out') - # --- Guard: detect merge commits --- + # Guard: detect merge commits # Find the fork point with the upstream branch to scope the check upstream_sha = '' for ref in ['@{upstream}', 'origin/HEAD', 'origin/main']: diff --git a/.github/workflows/gh-aw-small-problem-fixer.lock.yml b/.github/workflows/gh-aw-small-problem-fixer.lock.yml index 3956a359..1d4b085c 100644 --- a/.github/workflows/gh-aw-small-problem-fixer.lock.yml +++ b/.github/workflows/gh-aw-small-problem-fixer.lock.yml @@ -37,7 +37,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"6e0916ffbc2999265e625637fcf342d769789c55f2242ec968fcc1985df5daf8"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"3c72786f58010a2b372bc7eb8555c6b006470f521f73bf045a3e9c067682e48f"} name: "Small Problem Fixer" "on": @@ -987,7 +987,7 @@ jobs: except subprocess.TimeoutExpired: return subprocess.CompletedProcess(cmd, 1, stdout='', stderr='diff timed out') - # --- Guard: detect merge commits --- + # Guard: detect merge commits # Find the fork point with the upstream branch to scope the check upstream_sha = '' for ref in ['@{upstream}', 'origin/HEAD', 'origin/main']: diff --git a/.github/workflows/gh-aw-test-improvement.lock.yml b/.github/workflows/gh-aw-test-improvement.lock.yml index 6b2dfd7c..69883cc1 100644 --- a/.github/workflows/gh-aw-test-improvement.lock.yml +++ b/.github/workflows/gh-aw-test-improvement.lock.yml @@ -41,7 +41,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"9d6fcf1b2ca7c07f32f39938d2513e478c6e9a41cc283d763d9d05ac6779c820"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"9395f2a717f21eed792a960e30c641e556409154598e012581820ff4ddcb5659"} name: "Test Improver" "on": @@ -947,7 +947,7 @@ jobs: except subprocess.TimeoutExpired: return subprocess.CompletedProcess(cmd, 1, stdout='', stderr='diff timed out') - # --- Guard: detect merge commits --- + # Guard: detect merge commits # Find the fork point with the upstream branch to scope the check upstream_sha = '' for ref in ['@{upstream}', 'origin/HEAD', 'origin/main']: diff --git a/.github/workflows/gh-aw-test-improver.lock.yml b/.github/workflows/gh-aw-test-improver.lock.yml index ecfea747..aa091d8e 100644 --- a/.github/workflows/gh-aw-test-improver.lock.yml +++ b/.github/workflows/gh-aw-test-improver.lock.yml @@ -36,7 +36,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"9d6fcf1b2ca7c07f32f39938d2513e478c6e9a41cc283d763d9d05ac6779c820"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"9395f2a717f21eed792a960e30c641e556409154598e012581820ff4ddcb5659"} name: "Test Improver" "on": @@ -942,7 +942,7 @@ jobs: except subprocess.TimeoutExpired: return subprocess.CompletedProcess(cmd, 1, stdout='', stderr='diff timed out') - # --- Guard: detect merge commits --- + # Guard: detect merge commits # Find the fork point with the upstream branch to scope the check upstream_sha = '' for ref in ['@{upstream}', 'origin/HEAD', 'origin/main']: diff --git a/.github/workflows/gh-aw-text-beautifier.lock.yml b/.github/workflows/gh-aw-text-beautifier.lock.yml index 9a90a53b..fce350bc 100644 --- a/.github/workflows/gh-aw-text-beautifier.lock.yml +++ b/.github/workflows/gh-aw-text-beautifier.lock.yml @@ -38,7 +38,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"b12f8da03f4f92ec82e6567f0492a9a5289486ab806edc78692288d2472197f8"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"5553c98188b98ce05aca8133aa9b11f130b4bfc3fbeb117c444da5ce65543c91"} name: "Text Beautifier" "on": @@ -944,7 +944,7 @@ jobs: except subprocess.TimeoutExpired: return subprocess.CompletedProcess(cmd, 1, stdout='', stderr='diff timed out') - # --- Guard: detect merge commits --- + # Guard: detect merge commits # Find the fork point with the upstream branch to scope the check upstream_sha = '' for ref in ['@{upstream}', 'origin/HEAD', 'origin/main']: diff --git a/tests/test_safe_input_ready_to_make_pr.py b/tests/test_safe_input_ready_to_make_pr.py index ee275ef5..1c91abd2 100644 --- a/tests/test_safe_input_ready_to_make_pr.py +++ b/tests/test_safe_input_ready_to_make_pr.py @@ -117,11 +117,18 @@ def test_create_extract(self): assert "import" in code assert "json.dumps" in code - def test_fragments_have_identical_py(self): - """The two fragments should have identical Python logic.""" + def test_fragments_share_common_logic(self): + """Both fragments should share the same diff/checklist logic (tail portion).""" push_code = extract_py_block(PUSH_FRAGMENT) create_code = extract_py_block(CREATE_FRAGMENT) - assert push_code == create_code + # The push fragment has an extra ancestry check; both share the + # contributing/diff/checklist logic from "contributing = find(...)" onward. + common_marker = "contributing = find(" + assert common_marker in push_code + assert common_marker in create_code + push_tail = push_code[push_code.index(common_marker):] + create_tail = create_code[create_code.index(common_marker):] + assert push_tail == create_tail # --------------------------------------------------------------------------- @@ -368,3 +375,156 @@ def test_diff_line_count_in_checklist(self, py_code, tmp_path): assert count > 0 # The line count should appear in the checklist text assert f"({count} lines)" in " ".join(output["checklist"]) + + +# --------------------------------------------------------------------------- +# Push fragment: history rewrite and merge commit guards +# --------------------------------------------------------------------------- + + +def _get_head_sha(repo: Path) -> str: + """Get the current HEAD commit SHA.""" + result = subprocess.run( + ["git", "rev-parse", "HEAD"], + cwd=str(repo), + capture_output=True, + text=True, + check=True, + ) + return result.stdout.strip() + + +def _write_pr_json(head_sha: str) -> None: + """Write a minimal pr.json with the given headRefOid.""" + os.makedirs("/tmp/pr-context", exist_ok=True) + with open("/tmp/pr-context/pr.json", "w") as f: + json.dump({"headRefOid": head_sha}, f) + + +def _cleanup_pr_json() -> None: + """Remove pr.json so it doesn't leak between tests.""" + try: + os.remove("/tmp/pr-context/pr.json") + except FileNotFoundError: + pass + + +class TestPushGuards: + """Test the ancestry and merge-commit guards in the push fragment.""" + + @pytest.fixture + def py_code(self): + return extract_py_block(PUSH_FRAGMENT) + + @pytest.fixture(autouse=True) + def cleanup(self): + yield + _cleanup_pr_json() + + def test_no_pr_json_passes(self, py_code, tmp_path): + """Without pr.json the guard is skipped — should succeed.""" + _cleanup_pr_json() + repo = make_git_repo(tmp_path, with_upstream=True) + output = run_py_in_repo(py_code, str(repo)) + assert output["status"] == "ok" + + def test_normal_commit_passes(self, py_code, tmp_path): + """A regular commit on top of the PR head should pass.""" + repo = make_git_repo(tmp_path, with_upstream=True) + head_sha = _get_head_sha(repo) + _write_pr_json(head_sha) + + # Add a normal commit + (repo / "new.txt").write_text("new\n") + subprocess.run(["git", "add", "new.txt"], cwd=str(repo), check=True, capture_output=True) + subprocess.run(["git", "commit", "-m", "add new"], cwd=str(repo), check=True, capture_output=True) + + output = run_py_in_repo(py_code, str(repo)) + assert output["status"] == "ok" + + def test_history_rewrite_detected(self, py_code, tmp_path): + """If HEAD diverges from PR head (rebase), guard should error.""" + repo = make_git_repo(tmp_path, with_upstream=True) + + # Record the initial head + initial_sha = _get_head_sha(repo) + + # Make a second commit, then record that as PR head + (repo / "a.txt").write_text("a\n") + subprocess.run(["git", "add", "a.txt"], cwd=str(repo), check=True, capture_output=True) + subprocess.run(["git", "commit", "-m", "second"], cwd=str(repo), check=True, capture_output=True) + pr_head = _get_head_sha(repo) + + # Now reset back to initial — simulates a rebase that dropped the PR head + subprocess.run(["git", "reset", "--hard", initial_sha], cwd=str(repo), check=True, capture_output=True) + (repo / "b.txt").write_text("b\n") + subprocess.run(["git", "add", "b.txt"], cwd=str(repo), check=True, capture_output=True) + subprocess.run(["git", "commit", "-m", "diverged"], cwd=str(repo), check=True, capture_output=True) + + _write_pr_json(pr_head) + output = run_py_in_repo(py_code, str(repo)) + assert output["status"] == "error" + assert "History rewrite" in output["error"] + + def test_merge_commit_detected(self, py_code, tmp_path): + """A merge commit after the PR head should be detected.""" + repo = make_git_repo(tmp_path, with_upstream=True) + pr_head = _get_head_sha(repo) + _write_pr_json(pr_head) + + # Create a side branch and merge it — produces a merge commit + subprocess.run(["git", "checkout", "-b", "side"], cwd=str(repo), check=True, capture_output=True) + (repo / "side.txt").write_text("side\n") + subprocess.run(["git", "add", "side.txt"], cwd=str(repo), check=True, capture_output=True) + subprocess.run(["git", "commit", "-m", "side"], cwd=str(repo), check=True, capture_output=True) + subprocess.run(["git", "checkout", "main"], cwd=str(repo), check=True, capture_output=True) + (repo / "main2.txt").write_text("main2\n") + subprocess.run(["git", "add", "main2.txt"], cwd=str(repo), check=True, capture_output=True) + subprocess.run(["git", "commit", "-m", "main2"], cwd=str(repo), check=True, capture_output=True) + subprocess.run(["git", "merge", "side", "--no-edit"], cwd=str(repo), check=True, capture_output=True) + + output = run_py_in_repo(py_code, str(repo)) + assert output["status"] == "error" + assert "Merge commit" in output["error"] + + +# --------------------------------------------------------------------------- +# Create fragment: merge commit guard +# --------------------------------------------------------------------------- + + +class TestCreateGuards: + """Test the merge-commit guard in the create fragment.""" + + @pytest.fixture + def py_code(self): + return extract_py_block(CREATE_FRAGMENT) + + def test_normal_commit_passes(self, py_code, tmp_path): + """Regular commits should pass the create guard.""" + repo = make_git_repo(tmp_path, with_upstream=True) + (repo / "new.txt").write_text("new\n") + subprocess.run(["git", "add", "new.txt"], cwd=str(repo), check=True, capture_output=True) + subprocess.run(["git", "commit", "-m", "add new"], cwd=str(repo), check=True, capture_output=True) + + output = run_py_in_repo(py_code, str(repo)) + assert output["status"] == "ok" + + def test_merge_commit_detected(self, py_code, tmp_path): + """A merge commit should be detected by the create guard.""" + repo = make_git_repo(tmp_path, with_upstream=True) + + # Create a side branch and merge it + subprocess.run(["git", "checkout", "-b", "side"], cwd=str(repo), check=True, capture_output=True) + (repo / "side.txt").write_text("side\n") + subprocess.run(["git", "add", "side.txt"], cwd=str(repo), check=True, capture_output=True) + subprocess.run(["git", "commit", "-m", "side"], cwd=str(repo), check=True, capture_output=True) + subprocess.run(["git", "checkout", "main"], cwd=str(repo), check=True, capture_output=True) + (repo / "main2.txt").write_text("main2\n") + subprocess.run(["git", "add", "main2.txt"], cwd=str(repo), check=True, capture_output=True) + subprocess.run(["git", "commit", "-m", "main2"], cwd=str(repo), check=True, capture_output=True) + subprocess.run(["git", "merge", "side", "--no-edit"], cwd=str(repo), check=True, capture_output=True) + + output = run_py_in_repo(py_code, str(repo)) + assert output["status"] == "error" + assert "Merge commit" in output["error"] From 4eee5c12666de8c2d42bcb4f628787fb1c19d9fc Mon Sep 17 00:00:00 2001 From: William Easton Date: Sat, 28 Feb 2026 00:55:26 -0600 Subject: [PATCH 5/8] Address PR feedback: fail-closed guard and fix detached HEAD guidance - Create fragment: fail closed when upstream fork point cannot be resolved instead of silently skipping the merge-commit guard - Push fragment: replace `git checkout {sha}` (detaches HEAD) with `git reset --hard {sha}` in recovery guidance to keep branch checked out - Add test for create fragment's no-upstream fail-closed behavior Co-Authored-By: Claude Opus 4.6 --- .github/workflows/downstream-users.lock.yml | 16 +++++++++------- .../workflows/gh-aw-bug-exterminator.lock.yml | 16 +++++++++------- .../gh-aw-code-duplication-fixer.lock.yml | 16 +++++++++------- .github/workflows/gh-aw-code-simplifier.lock.yml | 16 +++++++++------- .../gh-aw-fragments/safe-output-create-pr.md | 14 ++++++++------ .../gh-aw-fragments/safe-output-push-to-pr.md | 4 ++-- .github/workflows/gh-aw-issue-fixer.lock.yml | 16 +++++++++------- .../gh-aw-mention-in-issue-no-sandbox.lock.yml | 16 +++++++++------- .../workflows/gh-aw-mention-in-issue.lock.yml | 16 +++++++++------- .../workflows/gh-aw-mention-in-pr-by-id.lock.yml | 6 +++--- .../gh-aw-mention-in-pr-no-sandbox.lock.yml | 6 +++--- .github/workflows/gh-aw-mention-in-pr.lock.yml | 6 +++--- .../gh-aw-newbie-contributor-fixer.lock.yml | 16 +++++++++------- .../workflows/gh-aw-pr-actions-fixer.lock.yml | 6 +++--- .../workflows/gh-aw-pr-review-addresser.lock.yml | 6 +++--- .github/workflows/gh-aw-release-update.lock.yml | 16 +++++++++------- .github/workflows/gh-aw-scheduled-fix.lock.yml | 16 +++++++++------- .../workflows/gh-aw-small-problem-fixer.lock.yml | 16 +++++++++------- .../workflows/gh-aw-test-improvement.lock.yml | 16 +++++++++------- .github/workflows/gh-aw-test-improver.lock.yml | 16 +++++++++------- .github/workflows/gh-aw-text-beautifier.lock.yml | 16 +++++++++------- tests/test_safe_input_ready_to_make_pr.py | 8 ++++++++ 22 files changed, 159 insertions(+), 121 deletions(-) diff --git a/.github/workflows/downstream-users.lock.yml b/.github/workflows/downstream-users.lock.yml index 0cbbbb11..f3efc1a6 100644 --- a/.github/workflows/downstream-users.lock.yml +++ b/.github/workflows/downstream-users.lock.yml @@ -36,7 +36,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"ca0c06fcccf3ad9313ebe550cac79cc41020061ac13e5693312e79cfc0a1ff0d"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"a51ecaef1406bb447225a0485fab727a0691862a287cae8be02c56ebf541165f"} name: "Internal: Downstream Users" "on": @@ -892,12 +892,14 @@ jobs: if r.returncode == 0 and r.stdout.strip(): upstream_sha = r.stdout.strip() break - if upstream_sha: - log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) - merge_shas = log.stdout.strip() - if merge_shas: - print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) - raise SystemExit(0) + if not upstream_sha: + print(json.dumps({'status': 'error', 'error': 'Unable to determine upstream fork point for merge-commit validation. Fix: ensure remotes are fetched and a tracking branch is set (e.g., `git branch --set-upstream-to origin/`), then rerun ready_to_make_pr.'})) + raise SystemExit(0) + log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) + merge_shas = log.stdout.strip() + if merge_shas: + print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) + raise SystemExit(0) contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') diff --git a/.github/workflows/gh-aw-bug-exterminator.lock.yml b/.github/workflows/gh-aw-bug-exterminator.lock.yml index 8b5e4e15..91858164 100644 --- a/.github/workflows/gh-aw-bug-exterminator.lock.yml +++ b/.github/workflows/gh-aw-bug-exterminator.lock.yml @@ -37,7 +37,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"a2b480df98fe924dccae1ea22c4330d416d7cbd9bbfb9367ba1df6cedb576511"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"fdfcf0812db132fd3ec7de0417bdda17c2c5fa0df0fa49ef6d5d1e4468f10a8b"} name: "Gh Aw Bug Exterminator" "on": @@ -941,12 +941,14 @@ jobs: if r.returncode == 0 and r.stdout.strip(): upstream_sha = r.stdout.strip() break - if upstream_sha: - log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) - merge_shas = log.stdout.strip() - if merge_shas: - print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) - raise SystemExit(0) + if not upstream_sha: + print(json.dumps({'status': 'error', 'error': 'Unable to determine upstream fork point for merge-commit validation. Fix: ensure remotes are fetched and a tracking branch is set (e.g., `git branch --set-upstream-to origin/`), then rerun ready_to_make_pr.'})) + raise SystemExit(0) + log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) + merge_shas = log.stdout.strip() + if merge_shas: + print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) + raise SystemExit(0) contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') diff --git a/.github/workflows/gh-aw-code-duplication-fixer.lock.yml b/.github/workflows/gh-aw-code-duplication-fixer.lock.yml index d1627e7b..3bb40c7d 100644 --- a/.github/workflows/gh-aw-code-duplication-fixer.lock.yml +++ b/.github/workflows/gh-aw-code-duplication-fixer.lock.yml @@ -37,7 +37,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"9a2d9b699fd61c41d444bc9e64726b99bc00fa33a8c8536b4478307ee0f8ede9"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"0d2a281546b567ab8b5f6aa1c4cbc618f2207f224efe3285fea2b8ec3de66e4f"} name: "Code Duplication Fixer" "on": @@ -943,12 +943,14 @@ jobs: if r.returncode == 0 and r.stdout.strip(): upstream_sha = r.stdout.strip() break - if upstream_sha: - log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) - merge_shas = log.stdout.strip() - if merge_shas: - print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) - raise SystemExit(0) + if not upstream_sha: + print(json.dumps({'status': 'error', 'error': 'Unable to determine upstream fork point for merge-commit validation. Fix: ensure remotes are fetched and a tracking branch is set (e.g., `git branch --set-upstream-to origin/`), then rerun ready_to_make_pr.'})) + raise SystemExit(0) + log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) + merge_shas = log.stdout.strip() + if merge_shas: + print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) + raise SystemExit(0) contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') diff --git a/.github/workflows/gh-aw-code-simplifier.lock.yml b/.github/workflows/gh-aw-code-simplifier.lock.yml index cb246cd7..be27c33d 100644 --- a/.github/workflows/gh-aw-code-simplifier.lock.yml +++ b/.github/workflows/gh-aw-code-simplifier.lock.yml @@ -37,7 +37,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"4e1adfae148ee8b5f62112a63e914f9ca815233d1dd0ff515f563e2e1d90a44c"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"8b1da47596ac97bcd9a3ebe205dfc30dce8c2d659baec8bf7f07024f9207be2d"} name: "Code Simplifier" "on": @@ -958,12 +958,14 @@ jobs: if r.returncode == 0 and r.stdout.strip(): upstream_sha = r.stdout.strip() break - if upstream_sha: - log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) - merge_shas = log.stdout.strip() - if merge_shas: - print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) - raise SystemExit(0) + if not upstream_sha: + print(json.dumps({'status': 'error', 'error': 'Unable to determine upstream fork point for merge-commit validation. Fix: ensure remotes are fetched and a tracking branch is set (e.g., `git branch --set-upstream-to origin/`), then rerun ready_to_make_pr.'})) + raise SystemExit(0) + log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) + merge_shas = log.stdout.strip() + if merge_shas: + print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) + raise SystemExit(0) contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') diff --git a/.github/workflows/gh-aw-fragments/safe-output-create-pr.md b/.github/workflows/gh-aw-fragments/safe-output-create-pr.md index 726be60f..825d75de 100644 --- a/.github/workflows/gh-aw-fragments/safe-output-create-pr.md +++ b/.github/workflows/gh-aw-fragments/safe-output-create-pr.md @@ -20,12 +20,14 @@ safe-inputs: if r.returncode == 0 and r.stdout.strip(): upstream_sha = r.stdout.strip() break - if upstream_sha: - log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) - merge_shas = log.stdout.strip() - if merge_shas: - print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) - raise SystemExit(0) + if not upstream_sha: + print(json.dumps({'status': 'error', 'error': 'Unable to determine upstream fork point for merge-commit validation. Fix: ensure remotes are fetched and a tracking branch is set (e.g., `git branch --set-upstream-to origin/`), then rerun ready_to_make_pr.'})) + raise SystemExit(0) + log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) + merge_shas = log.stdout.strip() + if merge_shas: + print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) + raise SystemExit(0) contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') diff --git a/.github/workflows/gh-aw-fragments/safe-output-push-to-pr.md b/.github/workflows/gh-aw-fragments/safe-output-push-to-pr.md index 3c79e6a3..9333da1a 100644 --- a/.github/workflows/gh-aw-fragments/safe-output-push-to-pr.md +++ b/.github/workflows/gh-aw-fragments/safe-output-push-to-pr.md @@ -22,13 +22,13 @@ safe-inputs: # Check 1: PR head must be an ancestor of HEAD (no rebase/reset) anc = run(['git', 'merge-base', '--is-ancestor', pr_head_sha, 'HEAD']) if anc.returncode != 0: - print(json.dumps({'status': 'error', 'error': f'History rewrite detected: the original PR head ({pr_head_sha[:12]}) is not an ancestor of HEAD. This means git rebase, reset, or cherry-pick rewrote history. push_to_pull_request_branch will fail. Fix: reset to the PR head with `git checkout {pr_head_sha}`, re-apply your changes as direct file edits, and commit as regular commits.'})) + print(json.dumps({'status': 'error', 'error': f'History rewrite detected: the original PR head ({pr_head_sha[:12]}) is not an ancestor of HEAD. This means git rebase, reset, or cherry-pick rewrote history. push_to_pull_request_branch will fail. Fix: run `git reset --hard {pr_head_sha}` to restore the PR branch to its original head, then re-apply your changes as direct file edits and commit as regular commits.'})) raise SystemExit(0) # Check 2: no merge commits (multiple parents) since PR head log = run(['git', 'rev-list', '--min-parents=2', f'{pr_head_sha}..HEAD']) merge_shas = log.stdout.strip() if merge_shas: - print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... push_to_pull_request_branch uses git format-patch which breaks on merge commits. Fix: reset to the PR head with `git checkout {pr_head_sha}`, re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags), and commit as regular single-parent commits.'})) + print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... push_to_pull_request_branch uses git format-patch which breaks on merge commits. Fix: run `git reset --hard {pr_head_sha}` to restore the PR branch, then re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) raise SystemExit(0) contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') diff --git a/.github/workflows/gh-aw-issue-fixer.lock.yml b/.github/workflows/gh-aw-issue-fixer.lock.yml index d26fac18..5e32d49d 100644 --- a/.github/workflows/gh-aw-issue-fixer.lock.yml +++ b/.github/workflows/gh-aw-issue-fixer.lock.yml @@ -38,7 +38,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"26711f1b6d616d268f730a5feee132792f43226b50595f2034a43814d6ca16e7"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"42faade16be070194b046b883c2b2f38fa7b160b49c3c062a4440dea6b729f1a"} name: "Issue Fixer" "on": @@ -991,12 +991,14 @@ jobs: if r.returncode == 0 and r.stdout.strip(): upstream_sha = r.stdout.strip() break - if upstream_sha: - log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) - merge_shas = log.stdout.strip() - if merge_shas: - print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) - raise SystemExit(0) + if not upstream_sha: + print(json.dumps({'status': 'error', 'error': 'Unable to determine upstream fork point for merge-commit validation. Fix: ensure remotes are fetched and a tracking branch is set (e.g., `git branch --set-upstream-to origin/`), then rerun ready_to_make_pr.'})) + raise SystemExit(0) + log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) + merge_shas = log.stdout.strip() + if merge_shas: + print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) + raise SystemExit(0) contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') diff --git a/.github/workflows/gh-aw-mention-in-issue-no-sandbox.lock.yml b/.github/workflows/gh-aw-mention-in-issue-no-sandbox.lock.yml index 288653ab..e1c512bf 100644 --- a/.github/workflows/gh-aw-mention-in-issue-no-sandbox.lock.yml +++ b/.github/workflows/gh-aw-mention-in-issue-no-sandbox.lock.yml @@ -39,7 +39,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"cefaf06cce0d7fdb9c2c54a5837643bd723e1e065c7921c9a96d724f2f7b1d2b"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"6260c974a1039de55e69600a78900b56f20518c98f3a7233f68ab71abc765ff7"} name: "Mention in Issue (no sandbox)" "on": @@ -1079,12 +1079,14 @@ jobs: if r.returncode == 0 and r.stdout.strip(): upstream_sha = r.stdout.strip() break - if upstream_sha: - log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) - merge_shas = log.stdout.strip() - if merge_shas: - print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) - raise SystemExit(0) + if not upstream_sha: + print(json.dumps({'status': 'error', 'error': 'Unable to determine upstream fork point for merge-commit validation. Fix: ensure remotes are fetched and a tracking branch is set (e.g., `git branch --set-upstream-to origin/`), then rerun ready_to_make_pr.'})) + raise SystemExit(0) + log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) + merge_shas = log.stdout.strip() + if merge_shas: + print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) + raise SystemExit(0) contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') diff --git a/.github/workflows/gh-aw-mention-in-issue.lock.yml b/.github/workflows/gh-aw-mention-in-issue.lock.yml index 86de06d2..a2df222b 100644 --- a/.github/workflows/gh-aw-mention-in-issue.lock.yml +++ b/.github/workflows/gh-aw-mention-in-issue.lock.yml @@ -39,7 +39,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"4b0cc70027a93055b66eeaba3d1b8f77ac40c1f855d16fc3be9ba71fb5b4caea"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"a12d6e3fd649bf1f5b3f2e87c73b47e239f61dd2ab38981e95e282fe0e86cde9"} name: "Mention in Issue" "on": @@ -1083,12 +1083,14 @@ jobs: if r.returncode == 0 and r.stdout.strip(): upstream_sha = r.stdout.strip() break - if upstream_sha: - log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) - merge_shas = log.stdout.strip() - if merge_shas: - print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) - raise SystemExit(0) + if not upstream_sha: + print(json.dumps({'status': 'error', 'error': 'Unable to determine upstream fork point for merge-commit validation. Fix: ensure remotes are fetched and a tracking branch is set (e.g., `git branch --set-upstream-to origin/`), then rerun ready_to_make_pr.'})) + raise SystemExit(0) + log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) + merge_shas = log.stdout.strip() + if merge_shas: + print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) + raise SystemExit(0) contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') diff --git a/.github/workflows/gh-aw-mention-in-pr-by-id.lock.yml b/.github/workflows/gh-aw-mention-in-pr-by-id.lock.yml index 82a4adca..e67f91ed 100644 --- a/.github/workflows/gh-aw-mention-in-pr-by-id.lock.yml +++ b/.github/workflows/gh-aw-mention-in-pr-by-id.lock.yml @@ -43,7 +43,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"7dca61f11e2522195822f429e26d90495c98b0658654dd5d14fc0e8493d878c1"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"9868b9919a971c567cd0b39b22d8078c606934a5700d201da5c42785a827b629"} name: "Mention in PR by ID" "on": @@ -1241,13 +1241,13 @@ jobs: # Check 1: PR head must be an ancestor of HEAD (no rebase/reset) anc = run(['git', 'merge-base', '--is-ancestor', pr_head_sha, 'HEAD']) if anc.returncode != 0: - print(json.dumps({'status': 'error', 'error': f'History rewrite detected: the original PR head ({pr_head_sha[:12]}) is not an ancestor of HEAD. This means git rebase, reset, or cherry-pick rewrote history. push_to_pull_request_branch will fail. Fix: reset to the PR head with `git checkout {pr_head_sha}`, re-apply your changes as direct file edits, and commit as regular commits.'})) + print(json.dumps({'status': 'error', 'error': f'History rewrite detected: the original PR head ({pr_head_sha[:12]}) is not an ancestor of HEAD. This means git rebase, reset, or cherry-pick rewrote history. push_to_pull_request_branch will fail. Fix: run `git reset --hard {pr_head_sha}` to restore the PR branch to its original head, then re-apply your changes as direct file edits and commit as regular commits.'})) raise SystemExit(0) # Check 2: no merge commits (multiple parents) since PR head log = run(['git', 'rev-list', '--min-parents=2', f'{pr_head_sha}..HEAD']) merge_shas = log.stdout.strip() if merge_shas: - print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... push_to_pull_request_branch uses git format-patch which breaks on merge commits. Fix: reset to the PR head with `git checkout {pr_head_sha}`, re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags), and commit as regular single-parent commits.'})) + print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... push_to_pull_request_branch uses git format-patch which breaks on merge commits. Fix: run `git reset --hard {pr_head_sha}` to restore the PR branch, then re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) raise SystemExit(0) contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') diff --git a/.github/workflows/gh-aw-mention-in-pr-no-sandbox.lock.yml b/.github/workflows/gh-aw-mention-in-pr-no-sandbox.lock.yml index 9e261a0e..7751b608 100644 --- a/.github/workflows/gh-aw-mention-in-pr-no-sandbox.lock.yml +++ b/.github/workflows/gh-aw-mention-in-pr-no-sandbox.lock.yml @@ -44,7 +44,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"c29c75e4f028a88a635b714d38c1b9181e3205551aa2e8ed503d30a318817d1f"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"a1f9d857d28af190c32f118a2b08d94f982282616ce43931e4d363bcc66d8163"} name: "Mention in PR (no sandbox)" "on": @@ -1345,13 +1345,13 @@ jobs: # Check 1: PR head must be an ancestor of HEAD (no rebase/reset) anc = run(['git', 'merge-base', '--is-ancestor', pr_head_sha, 'HEAD']) if anc.returncode != 0: - print(json.dumps({'status': 'error', 'error': f'History rewrite detected: the original PR head ({pr_head_sha[:12]}) is not an ancestor of HEAD. This means git rebase, reset, or cherry-pick rewrote history. push_to_pull_request_branch will fail. Fix: reset to the PR head with `git checkout {pr_head_sha}`, re-apply your changes as direct file edits, and commit as regular commits.'})) + print(json.dumps({'status': 'error', 'error': f'History rewrite detected: the original PR head ({pr_head_sha[:12]}) is not an ancestor of HEAD. This means git rebase, reset, or cherry-pick rewrote history. push_to_pull_request_branch will fail. Fix: run `git reset --hard {pr_head_sha}` to restore the PR branch to its original head, then re-apply your changes as direct file edits and commit as regular commits.'})) raise SystemExit(0) # Check 2: no merge commits (multiple parents) since PR head log = run(['git', 'rev-list', '--min-parents=2', f'{pr_head_sha}..HEAD']) merge_shas = log.stdout.strip() if merge_shas: - print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... push_to_pull_request_branch uses git format-patch which breaks on merge commits. Fix: reset to the PR head with `git checkout {pr_head_sha}`, re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags), and commit as regular single-parent commits.'})) + print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... push_to_pull_request_branch uses git format-patch which breaks on merge commits. Fix: run `git reset --hard {pr_head_sha}` to restore the PR branch, then re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) raise SystemExit(0) contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') diff --git a/.github/workflows/gh-aw-mention-in-pr.lock.yml b/.github/workflows/gh-aw-mention-in-pr.lock.yml index 93fbd4df..ec104974 100644 --- a/.github/workflows/gh-aw-mention-in-pr.lock.yml +++ b/.github/workflows/gh-aw-mention-in-pr.lock.yml @@ -44,7 +44,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"7a4f68db703c96b3a51a88db89ef4369b7dd562d2787954f2878c7f878d422a8"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"f186b4ca75c18bfe67ac78e8786f59f6e0bd306eff2bb99b62d17bd096df7239"} name: "Mention in PR" "on": @@ -1379,13 +1379,13 @@ jobs: # Check 1: PR head must be an ancestor of HEAD (no rebase/reset) anc = run(['git', 'merge-base', '--is-ancestor', pr_head_sha, 'HEAD']) if anc.returncode != 0: - print(json.dumps({'status': 'error', 'error': f'History rewrite detected: the original PR head ({pr_head_sha[:12]}) is not an ancestor of HEAD. This means git rebase, reset, or cherry-pick rewrote history. push_to_pull_request_branch will fail. Fix: reset to the PR head with `git checkout {pr_head_sha}`, re-apply your changes as direct file edits, and commit as regular commits.'})) + print(json.dumps({'status': 'error', 'error': f'History rewrite detected: the original PR head ({pr_head_sha[:12]}) is not an ancestor of HEAD. This means git rebase, reset, or cherry-pick rewrote history. push_to_pull_request_branch will fail. Fix: run `git reset --hard {pr_head_sha}` to restore the PR branch to its original head, then re-apply your changes as direct file edits and commit as regular commits.'})) raise SystemExit(0) # Check 2: no merge commits (multiple parents) since PR head log = run(['git', 'rev-list', '--min-parents=2', f'{pr_head_sha}..HEAD']) merge_shas = log.stdout.strip() if merge_shas: - print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... push_to_pull_request_branch uses git format-patch which breaks on merge commits. Fix: reset to the PR head with `git checkout {pr_head_sha}`, re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags), and commit as regular single-parent commits.'})) + print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... push_to_pull_request_branch uses git format-patch which breaks on merge commits. Fix: run `git reset --hard {pr_head_sha}` to restore the PR branch, then re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) raise SystemExit(0) contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') diff --git a/.github/workflows/gh-aw-newbie-contributor-fixer.lock.yml b/.github/workflows/gh-aw-newbie-contributor-fixer.lock.yml index 8c0d96b0..b886b92b 100644 --- a/.github/workflows/gh-aw-newbie-contributor-fixer.lock.yml +++ b/.github/workflows/gh-aw-newbie-contributor-fixer.lock.yml @@ -37,7 +37,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"642beb3d9533c980d9ca0a9b8898c3559e40aaf86aefaf43f93fd4f952af3350"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"7517579423339f29e0169914dbc406aab3cfc50a49a2f67ba959c1c8dc5476df"} name: "Newbie Contributor Fixer" "on": @@ -944,12 +944,14 @@ jobs: if r.returncode == 0 and r.stdout.strip(): upstream_sha = r.stdout.strip() break - if upstream_sha: - log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) - merge_shas = log.stdout.strip() - if merge_shas: - print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) - raise SystemExit(0) + if not upstream_sha: + print(json.dumps({'status': 'error', 'error': 'Unable to determine upstream fork point for merge-commit validation. Fix: ensure remotes are fetched and a tracking branch is set (e.g., `git branch --set-upstream-to origin/`), then rerun ready_to_make_pr.'})) + raise SystemExit(0) + log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) + merge_shas = log.stdout.strip() + if merge_shas: + print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) + raise SystemExit(0) contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') diff --git a/.github/workflows/gh-aw-pr-actions-fixer.lock.yml b/.github/workflows/gh-aw-pr-actions-fixer.lock.yml index ab52f74d..2c5fc172 100644 --- a/.github/workflows/gh-aw-pr-actions-fixer.lock.yml +++ b/.github/workflows/gh-aw-pr-actions-fixer.lock.yml @@ -37,7 +37,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"47dd01faf769b21aa06f536959bc654b370dbac9708994d87022ffbf87e494e9"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"e164adebd7b6780782d25d56627788d045541598c49ab616e750ba0a5a9e00a4"} name: "PR Actions Fixer" "on": @@ -959,13 +959,13 @@ jobs: # Check 1: PR head must be an ancestor of HEAD (no rebase/reset) anc = run(['git', 'merge-base', '--is-ancestor', pr_head_sha, 'HEAD']) if anc.returncode != 0: - print(json.dumps({'status': 'error', 'error': f'History rewrite detected: the original PR head ({pr_head_sha[:12]}) is not an ancestor of HEAD. This means git rebase, reset, or cherry-pick rewrote history. push_to_pull_request_branch will fail. Fix: reset to the PR head with `git checkout {pr_head_sha}`, re-apply your changes as direct file edits, and commit as regular commits.'})) + print(json.dumps({'status': 'error', 'error': f'History rewrite detected: the original PR head ({pr_head_sha[:12]}) is not an ancestor of HEAD. This means git rebase, reset, or cherry-pick rewrote history. push_to_pull_request_branch will fail. Fix: run `git reset --hard {pr_head_sha}` to restore the PR branch to its original head, then re-apply your changes as direct file edits and commit as regular commits.'})) raise SystemExit(0) # Check 2: no merge commits (multiple parents) since PR head log = run(['git', 'rev-list', '--min-parents=2', f'{pr_head_sha}..HEAD']) merge_shas = log.stdout.strip() if merge_shas: - print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... push_to_pull_request_branch uses git format-patch which breaks on merge commits. Fix: reset to the PR head with `git checkout {pr_head_sha}`, re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags), and commit as regular single-parent commits.'})) + print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... push_to_pull_request_branch uses git format-patch which breaks on merge commits. Fix: run `git reset --hard {pr_head_sha}` to restore the PR branch, then re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) raise SystemExit(0) contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') diff --git a/.github/workflows/gh-aw-pr-review-addresser.lock.yml b/.github/workflows/gh-aw-pr-review-addresser.lock.yml index 3f379783..01fc2feb 100644 --- a/.github/workflows/gh-aw-pr-review-addresser.lock.yml +++ b/.github/workflows/gh-aw-pr-review-addresser.lock.yml @@ -40,7 +40,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"3948810d4d9b4b8eccb8d7fa41a55369ed66b59821f3193038f5f1b4e14bf1ac"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"51c25bf2dc5e54ece7260a53347759424d68d373b43adad767dc85577955ef2f"} name: "PR Review Addresser" "on": @@ -1068,13 +1068,13 @@ jobs: # Check 1: PR head must be an ancestor of HEAD (no rebase/reset) anc = run(['git', 'merge-base', '--is-ancestor', pr_head_sha, 'HEAD']) if anc.returncode != 0: - print(json.dumps({'status': 'error', 'error': f'History rewrite detected: the original PR head ({pr_head_sha[:12]}) is not an ancestor of HEAD. This means git rebase, reset, or cherry-pick rewrote history. push_to_pull_request_branch will fail. Fix: reset to the PR head with `git checkout {pr_head_sha}`, re-apply your changes as direct file edits, and commit as regular commits.'})) + print(json.dumps({'status': 'error', 'error': f'History rewrite detected: the original PR head ({pr_head_sha[:12]}) is not an ancestor of HEAD. This means git rebase, reset, or cherry-pick rewrote history. push_to_pull_request_branch will fail. Fix: run `git reset --hard {pr_head_sha}` to restore the PR branch to its original head, then re-apply your changes as direct file edits and commit as regular commits.'})) raise SystemExit(0) # Check 2: no merge commits (multiple parents) since PR head log = run(['git', 'rev-list', '--min-parents=2', f'{pr_head_sha}..HEAD']) merge_shas = log.stdout.strip() if merge_shas: - print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... push_to_pull_request_branch uses git format-patch which breaks on merge commits. Fix: reset to the PR head with `git checkout {pr_head_sha}`, re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags), and commit as regular single-parent commits.'})) + print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... push_to_pull_request_branch uses git format-patch which breaks on merge commits. Fix: run `git reset --hard {pr_head_sha}` to restore the PR branch, then re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) raise SystemExit(0) contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') diff --git a/.github/workflows/gh-aw-release-update.lock.yml b/.github/workflows/gh-aw-release-update.lock.yml index 4d02b475..729da88d 100644 --- a/.github/workflows/gh-aw-release-update.lock.yml +++ b/.github/workflows/gh-aw-release-update.lock.yml @@ -36,7 +36,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"c29059ccd4059710b3ec50c26042207612a2ba956395ddb4fd7bfd051a404f9a"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"e8413d8f5f30f529f10f73c0272523416e066abd3c8a93fcbcfc153681b8c0f3"} name: "Release Update Check" "on": @@ -912,12 +912,14 @@ jobs: if r.returncode == 0 and r.stdout.strip(): upstream_sha = r.stdout.strip() break - if upstream_sha: - log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) - merge_shas = log.stdout.strip() - if merge_shas: - print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) - raise SystemExit(0) + if not upstream_sha: + print(json.dumps({'status': 'error', 'error': 'Unable to determine upstream fork point for merge-commit validation. Fix: ensure remotes are fetched and a tracking branch is set (e.g., `git branch --set-upstream-to origin/`), then rerun ready_to_make_pr.'})) + raise SystemExit(0) + log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) + merge_shas = log.stdout.strip() + if merge_shas: + print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) + raise SystemExit(0) contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') diff --git a/.github/workflows/gh-aw-scheduled-fix.lock.yml b/.github/workflows/gh-aw-scheduled-fix.lock.yml index 3287ddf7..91eb8c4b 100644 --- a/.github/workflows/gh-aw-scheduled-fix.lock.yml +++ b/.github/workflows/gh-aw-scheduled-fix.lock.yml @@ -37,7 +37,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"f59e0e683d81777293efe24ff400ef10f4d3180df1bada82315ab80420c478cf"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"729668b1b3f51805d888df2b61ca2fe108a24b0f61eaf83ac68d75c8f4073200"} name: "Scheduled Fix" "on": @@ -967,12 +967,14 @@ jobs: if r.returncode == 0 and r.stdout.strip(): upstream_sha = r.stdout.strip() break - if upstream_sha: - log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) - merge_shas = log.stdout.strip() - if merge_shas: - print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) - raise SystemExit(0) + if not upstream_sha: + print(json.dumps({'status': 'error', 'error': 'Unable to determine upstream fork point for merge-commit validation. Fix: ensure remotes are fetched and a tracking branch is set (e.g., `git branch --set-upstream-to origin/`), then rerun ready_to_make_pr.'})) + raise SystemExit(0) + log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) + merge_shas = log.stdout.strip() + if merge_shas: + print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) + raise SystemExit(0) contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') diff --git a/.github/workflows/gh-aw-small-problem-fixer.lock.yml b/.github/workflows/gh-aw-small-problem-fixer.lock.yml index 1d4b085c..f9dc2bbf 100644 --- a/.github/workflows/gh-aw-small-problem-fixer.lock.yml +++ b/.github/workflows/gh-aw-small-problem-fixer.lock.yml @@ -37,7 +37,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"3c72786f58010a2b372bc7eb8555c6b006470f521f73bf045a3e9c067682e48f"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"17d0b1e78e19ecad5182a120aeffc9726e3aadcd5a42137f4bb24bfa388f103f"} name: "Small Problem Fixer" "on": @@ -995,12 +995,14 @@ jobs: if r.returncode == 0 and r.stdout.strip(): upstream_sha = r.stdout.strip() break - if upstream_sha: - log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) - merge_shas = log.stdout.strip() - if merge_shas: - print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) - raise SystemExit(0) + if not upstream_sha: + print(json.dumps({'status': 'error', 'error': 'Unable to determine upstream fork point for merge-commit validation. Fix: ensure remotes are fetched and a tracking branch is set (e.g., `git branch --set-upstream-to origin/`), then rerun ready_to_make_pr.'})) + raise SystemExit(0) + log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) + merge_shas = log.stdout.strip() + if merge_shas: + print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) + raise SystemExit(0) contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') diff --git a/.github/workflows/gh-aw-test-improvement.lock.yml b/.github/workflows/gh-aw-test-improvement.lock.yml index 69883cc1..9efbd71b 100644 --- a/.github/workflows/gh-aw-test-improvement.lock.yml +++ b/.github/workflows/gh-aw-test-improvement.lock.yml @@ -41,7 +41,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"9395f2a717f21eed792a960e30c641e556409154598e012581820ff4ddcb5659"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"399380cbad5e18f4fe5836e1df05733b9b25bbf6c8e1571d4ab4afb3a494a6f9"} name: "Test Improver" "on": @@ -955,12 +955,14 @@ jobs: if r.returncode == 0 and r.stdout.strip(): upstream_sha = r.stdout.strip() break - if upstream_sha: - log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) - merge_shas = log.stdout.strip() - if merge_shas: - print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) - raise SystemExit(0) + if not upstream_sha: + print(json.dumps({'status': 'error', 'error': 'Unable to determine upstream fork point for merge-commit validation. Fix: ensure remotes are fetched and a tracking branch is set (e.g., `git branch --set-upstream-to origin/`), then rerun ready_to_make_pr.'})) + raise SystemExit(0) + log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) + merge_shas = log.stdout.strip() + if merge_shas: + print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) + raise SystemExit(0) contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') diff --git a/.github/workflows/gh-aw-test-improver.lock.yml b/.github/workflows/gh-aw-test-improver.lock.yml index aa091d8e..e4f320d8 100644 --- a/.github/workflows/gh-aw-test-improver.lock.yml +++ b/.github/workflows/gh-aw-test-improver.lock.yml @@ -36,7 +36,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"9395f2a717f21eed792a960e30c641e556409154598e012581820ff4ddcb5659"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"399380cbad5e18f4fe5836e1df05733b9b25bbf6c8e1571d4ab4afb3a494a6f9"} name: "Test Improver" "on": @@ -950,12 +950,14 @@ jobs: if r.returncode == 0 and r.stdout.strip(): upstream_sha = r.stdout.strip() break - if upstream_sha: - log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) - merge_shas = log.stdout.strip() - if merge_shas: - print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) - raise SystemExit(0) + if not upstream_sha: + print(json.dumps({'status': 'error', 'error': 'Unable to determine upstream fork point for merge-commit validation. Fix: ensure remotes are fetched and a tracking branch is set (e.g., `git branch --set-upstream-to origin/`), then rerun ready_to_make_pr.'})) + raise SystemExit(0) + log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) + merge_shas = log.stdout.strip() + if merge_shas: + print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) + raise SystemExit(0) contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') diff --git a/.github/workflows/gh-aw-text-beautifier.lock.yml b/.github/workflows/gh-aw-text-beautifier.lock.yml index fce350bc..c799ffa6 100644 --- a/.github/workflows/gh-aw-text-beautifier.lock.yml +++ b/.github/workflows/gh-aw-text-beautifier.lock.yml @@ -38,7 +38,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"5553c98188b98ce05aca8133aa9b11f130b4bfc3fbeb117c444da5ce65543c91"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"f78088e1c8e96dba6189d8221a8090c823e8db499d8f55c352350a1875ebea81"} name: "Text Beautifier" "on": @@ -952,12 +952,14 @@ jobs: if r.returncode == 0 and r.stdout.strip(): upstream_sha = r.stdout.strip() break - if upstream_sha: - log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) - merge_shas = log.stdout.strip() - if merge_shas: - print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) - raise SystemExit(0) + if not upstream_sha: + print(json.dumps({'status': 'error', 'error': 'Unable to determine upstream fork point for merge-commit validation. Fix: ensure remotes are fetched and a tracking branch is set (e.g., `git branch --set-upstream-to origin/`), then rerun ready_to_make_pr.'})) + raise SystemExit(0) + log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) + merge_shas = log.stdout.strip() + if merge_shas: + print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) + raise SystemExit(0) contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') diff --git a/tests/test_safe_input_ready_to_make_pr.py b/tests/test_safe_input_ready_to_make_pr.py index 1c91abd2..db96bd2a 100644 --- a/tests/test_safe_input_ready_to_make_pr.py +++ b/tests/test_safe_input_ready_to_make_pr.py @@ -528,3 +528,11 @@ def test_merge_commit_detected(self, py_code, tmp_path): output = run_py_in_repo(py_code, str(repo)) assert output["status"] == "error" assert "Merge commit" in output["error"] + + def test_no_upstream_fails_closed(self, py_code, tmp_path): + """Without an upstream ref, the create guard should fail closed.""" + repo = make_git_repo(tmp_path, with_upstream=False) + + output = run_py_in_repo(py_code, str(repo)) + assert output["status"] == "error" + assert "upstream" in output["error"].lower() From c5e31af50652455d49ed539a0bb492b0011fc0a0 Mon Sep 17 00:00:00 2001 From: William Easton Date: Sat, 28 Feb 2026 00:59:03 -0600 Subject: [PATCH 6/8] Fix misleading diff comment and clarify merge conflict guidance - Fix comment describing diff fallback chain: `git diff @{upstream}` includes working tree changes, not "committed only" - Replace vague `git merge-tree or git diff` with specific `git diff HEAD...origin/` for merge conflict resolution Co-Authored-By: Claude Opus 4.6 --- .github/workflows/downstream-users.lock.yml | 6 +++--- .github/workflows/gh-aw-bug-exterminator.lock.yml | 6 +++--- .github/workflows/gh-aw-code-duplication-fixer.lock.yml | 6 +++--- .github/workflows/gh-aw-code-simplifier.lock.yml | 6 +++--- .../workflows/gh-aw-fragments/safe-output-create-pr.md | 4 ++-- .../workflows/gh-aw-fragments/safe-output-push-to-pr.md | 6 +++--- .github/workflows/gh-aw-issue-fixer.lock.yml | 6 +++--- .../workflows/gh-aw-mention-in-issue-no-sandbox.lock.yml | 6 +++--- .github/workflows/gh-aw-mention-in-issue.lock.yml | 6 +++--- .github/workflows/gh-aw-mention-in-pr-by-id.lock.yml | 8 ++++---- .github/workflows/gh-aw-mention-in-pr-no-sandbox.lock.yml | 8 ++++---- .github/workflows/gh-aw-mention-in-pr.lock.yml | 8 ++++---- .github/workflows/gh-aw-newbie-contributor-fixer.lock.yml | 6 +++--- .github/workflows/gh-aw-pr-actions-fixer.lock.yml | 8 ++++---- .github/workflows/gh-aw-pr-review-addresser.lock.yml | 8 ++++---- .github/workflows/gh-aw-release-update.lock.yml | 6 +++--- .github/workflows/gh-aw-scheduled-fix.lock.yml | 6 +++--- .github/workflows/gh-aw-small-problem-fixer.lock.yml | 6 +++--- .github/workflows/gh-aw-test-improvement.lock.yml | 6 +++--- .github/workflows/gh-aw-test-improver.lock.yml | 6 +++--- .github/workflows/gh-aw-text-beautifier.lock.yml | 6 +++--- 21 files changed, 67 insertions(+), 67 deletions(-) diff --git a/.github/workflows/downstream-users.lock.yml b/.github/workflows/downstream-users.lock.yml index f3efc1a6..4447fa73 100644 --- a/.github/workflows/downstream-users.lock.yml +++ b/.github/workflows/downstream-users.lock.yml @@ -36,7 +36,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"a51ecaef1406bb447225a0485fab727a0691862a287cae8be02c56ebf541165f"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"e43d8a296eb8fddc80fad9fd24bbc8089c75c17d7adefda9da8f6666ff2c8a2e"} name: "Internal: Downstream Users" "on": @@ -904,8 +904,8 @@ jobs: contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') # Generate diff of all local changes vs upstream for self-review - # Try --merge-base (committed+staged+unstaged vs upstream), fall back to - # @{upstream} 2-dot (committed only), then HEAD (uncommitted only) + # Try --merge-base (vs common ancestor), fall back to + # @{upstream} 2-dot (vs upstream tip), then HEAD (uncommitted only) diff_text = '' for diff_cmd in [ ['git', 'diff', '--merge-base', '@{upstream}'], diff --git a/.github/workflows/gh-aw-bug-exterminator.lock.yml b/.github/workflows/gh-aw-bug-exterminator.lock.yml index 91858164..3aa7fc5a 100644 --- a/.github/workflows/gh-aw-bug-exterminator.lock.yml +++ b/.github/workflows/gh-aw-bug-exterminator.lock.yml @@ -37,7 +37,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"fdfcf0812db132fd3ec7de0417bdda17c2c5fa0df0fa49ef6d5d1e4468f10a8b"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"81fe7c2b632df6afa9b55605616b2e97e6d6a271416f5da65847a7a3ba83b417"} name: "Gh Aw Bug Exterminator" "on": @@ -953,8 +953,8 @@ jobs: contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') # Generate diff of all local changes vs upstream for self-review - # Try --merge-base (committed+staged+unstaged vs upstream), fall back to - # @{upstream} 2-dot (committed only), then HEAD (uncommitted only) + # Try --merge-base (vs common ancestor), fall back to + # @{upstream} 2-dot (vs upstream tip), then HEAD (uncommitted only) diff_text = '' for diff_cmd in [ ['git', 'diff', '--merge-base', '@{upstream}'], diff --git a/.github/workflows/gh-aw-code-duplication-fixer.lock.yml b/.github/workflows/gh-aw-code-duplication-fixer.lock.yml index 3bb40c7d..6fa283a5 100644 --- a/.github/workflows/gh-aw-code-duplication-fixer.lock.yml +++ b/.github/workflows/gh-aw-code-duplication-fixer.lock.yml @@ -37,7 +37,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"0d2a281546b567ab8b5f6aa1c4cbc618f2207f224efe3285fea2b8ec3de66e4f"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"c0e75e19a630a4b22508bf3724d337c366ba4a457841c1db13f8af3bf46f4318"} name: "Code Duplication Fixer" "on": @@ -955,8 +955,8 @@ jobs: contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') # Generate diff of all local changes vs upstream for self-review - # Try --merge-base (committed+staged+unstaged vs upstream), fall back to - # @{upstream} 2-dot (committed only), then HEAD (uncommitted only) + # Try --merge-base (vs common ancestor), fall back to + # @{upstream} 2-dot (vs upstream tip), then HEAD (uncommitted only) diff_text = '' for diff_cmd in [ ['git', 'diff', '--merge-base', '@{upstream}'], diff --git a/.github/workflows/gh-aw-code-simplifier.lock.yml b/.github/workflows/gh-aw-code-simplifier.lock.yml index be27c33d..001565b3 100644 --- a/.github/workflows/gh-aw-code-simplifier.lock.yml +++ b/.github/workflows/gh-aw-code-simplifier.lock.yml @@ -37,7 +37,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"8b1da47596ac97bcd9a3ebe205dfc30dce8c2d659baec8bf7f07024f9207be2d"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"adeab66764152c1e72be562cc1b7e81be1c6b8950fc193334e3911110f3cfced"} name: "Code Simplifier" "on": @@ -970,8 +970,8 @@ jobs: contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') # Generate diff of all local changes vs upstream for self-review - # Try --merge-base (committed+staged+unstaged vs upstream), fall back to - # @{upstream} 2-dot (committed only), then HEAD (uncommitted only) + # Try --merge-base (vs common ancestor), fall back to + # @{upstream} 2-dot (vs upstream tip), then HEAD (uncommitted only) diff_text = '' for diff_cmd in [ ['git', 'diff', '--merge-base', '@{upstream}'], diff --git a/.github/workflows/gh-aw-fragments/safe-output-create-pr.md b/.github/workflows/gh-aw-fragments/safe-output-create-pr.md index 825d75de..7352e41d 100644 --- a/.github/workflows/gh-aw-fragments/safe-output-create-pr.md +++ b/.github/workflows/gh-aw-fragments/safe-output-create-pr.md @@ -32,8 +32,8 @@ safe-inputs: contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') # Generate diff of all local changes vs upstream for self-review - # Try --merge-base (committed+staged+unstaged vs upstream), fall back to - # @{upstream} 2-dot (committed only), then HEAD (uncommitted only) + # Try --merge-base (vs common ancestor), fall back to + # @{upstream} 2-dot (vs upstream tip), then HEAD (uncommitted only) diff_text = '' for diff_cmd in [ ['git', 'diff', '--merge-base', '@{upstream}'], diff --git a/.github/workflows/gh-aw-fragments/safe-output-push-to-pr.md b/.github/workflows/gh-aw-fragments/safe-output-push-to-pr.md index 9333da1a..226c2da6 100644 --- a/.github/workflows/gh-aw-fragments/safe-output-push-to-pr.md +++ b/.github/workflows/gh-aw-fragments/safe-output-push-to-pr.md @@ -34,8 +34,8 @@ safe-inputs: contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') # Generate diff of all local changes vs upstream for self-review - # Try --merge-base (committed+staged+unstaged vs upstream), fall back to - # @{upstream} 2-dot (committed only), then HEAD (uncommitted only) + # Try --merge-base (vs common ancestor), fall back to + # @{upstream} 2-dot (vs upstream tip), then HEAD (uncommitted only) diff_text = '' for diff_cmd in [ ['git', 'diff', '--merge-base', '@{upstream}'], @@ -85,7 +85,7 @@ Before calling `push_to_pull_request_branch`, call `ready_to_make_pr` and apply - You may not submit code that modifies files in `.github/workflows/`. Doing so will cause the submission to be rejected. If asked to modify workflow files, propose the change in a copy placed in a `github/` folder (without the leading period) and note in the PR that the file needs to be relocated by someone with workflow write access. Trying to resolve merge conflicts? Do NOT create merge commits (commits with multiple parents) — `push_to_pull_request_branch` uses `git format-patch` which breaks on merge commits. This means: no `git merge`, no `git rebase`, no `git commit-tree` with multiple `-p` flags. Instead: -1. Use `git merge-tree` or `git diff` to compare the conflicting files between this PR branch and the PR base branch (read from `/tmp/pr-context/pr.json`) +1. Use `git diff HEAD...origin/` (base branch from `/tmp/pr-context/pr.json` field `baseRefName`) to see what the base branch changed in the conflicting files 2. Edit the files directly to incorporate the changes from the base branch 3. Commit the changes as regular (single-parent) commits 4. Use push_to_pull_request_branch to push diff --git a/.github/workflows/gh-aw-issue-fixer.lock.yml b/.github/workflows/gh-aw-issue-fixer.lock.yml index 5e32d49d..6b21e27c 100644 --- a/.github/workflows/gh-aw-issue-fixer.lock.yml +++ b/.github/workflows/gh-aw-issue-fixer.lock.yml @@ -38,7 +38,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"42faade16be070194b046b883c2b2f38fa7b160b49c3c062a4440dea6b729f1a"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"d611f067d7728287548052bb2e8033d40067f412f234ea5574773aa951c4a0d2"} name: "Issue Fixer" "on": @@ -1003,8 +1003,8 @@ jobs: contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') # Generate diff of all local changes vs upstream for self-review - # Try --merge-base (committed+staged+unstaged vs upstream), fall back to - # @{upstream} 2-dot (committed only), then HEAD (uncommitted only) + # Try --merge-base (vs common ancestor), fall back to + # @{upstream} 2-dot (vs upstream tip), then HEAD (uncommitted only) diff_text = '' for diff_cmd in [ ['git', 'diff', '--merge-base', '@{upstream}'], diff --git a/.github/workflows/gh-aw-mention-in-issue-no-sandbox.lock.yml b/.github/workflows/gh-aw-mention-in-issue-no-sandbox.lock.yml index e1c512bf..c7f88a01 100644 --- a/.github/workflows/gh-aw-mention-in-issue-no-sandbox.lock.yml +++ b/.github/workflows/gh-aw-mention-in-issue-no-sandbox.lock.yml @@ -39,7 +39,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"6260c974a1039de55e69600a78900b56f20518c98f3a7233f68ab71abc765ff7"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"b01c2e024e7984cea8195e68d34a62ea3c0bed11d3227863b176229287966825"} name: "Mention in Issue (no sandbox)" "on": @@ -1091,8 +1091,8 @@ jobs: contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') # Generate diff of all local changes vs upstream for self-review - # Try --merge-base (committed+staged+unstaged vs upstream), fall back to - # @{upstream} 2-dot (committed only), then HEAD (uncommitted only) + # Try --merge-base (vs common ancestor), fall back to + # @{upstream} 2-dot (vs upstream tip), then HEAD (uncommitted only) diff_text = '' for diff_cmd in [ ['git', 'diff', '--merge-base', '@{upstream}'], diff --git a/.github/workflows/gh-aw-mention-in-issue.lock.yml b/.github/workflows/gh-aw-mention-in-issue.lock.yml index a2df222b..d854e614 100644 --- a/.github/workflows/gh-aw-mention-in-issue.lock.yml +++ b/.github/workflows/gh-aw-mention-in-issue.lock.yml @@ -39,7 +39,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"a12d6e3fd649bf1f5b3f2e87c73b47e239f61dd2ab38981e95e282fe0e86cde9"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"13ba73c6ccffccc91d6878e8fd94697098aaa96b76585a5ab7d70278341dcc46"} name: "Mention in Issue" "on": @@ -1095,8 +1095,8 @@ jobs: contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') # Generate diff of all local changes vs upstream for self-review - # Try --merge-base (committed+staged+unstaged vs upstream), fall back to - # @{upstream} 2-dot (committed only), then HEAD (uncommitted only) + # Try --merge-base (vs common ancestor), fall back to + # @{upstream} 2-dot (vs upstream tip), then HEAD (uncommitted only) diff_text = '' for diff_cmd in [ ['git', 'diff', '--merge-base', '@{upstream}'], diff --git a/.github/workflows/gh-aw-mention-in-pr-by-id.lock.yml b/.github/workflows/gh-aw-mention-in-pr-by-id.lock.yml index e67f91ed..cd2a95e3 100644 --- a/.github/workflows/gh-aw-mention-in-pr-by-id.lock.yml +++ b/.github/workflows/gh-aw-mention-in-pr-by-id.lock.yml @@ -43,7 +43,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"9868b9919a971c567cd0b39b22d8078c606934a5700d201da5c42785a827b629"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"15f053fec1502ce4d5b0586f9cb578e6b0101b9d2813851a2eaa1042e25d5d59"} name: "Mention in PR by ID" "on": @@ -437,7 +437,7 @@ jobs: - You may not submit code that modifies files in `.github/workflows/`. Doing so will cause the submission to be rejected. If asked to modify workflow files, propose the change in a copy placed in a `github/` folder (without the leading period) and note in the PR that the file needs to be relocated by someone with workflow write access. Trying to resolve merge conflicts? Do NOT create merge commits (commits with multiple parents) — `push_to_pull_request_branch` uses `git format-patch` which breaks on merge commits. This means: no `git merge`, no `git rebase`, no `git commit-tree` with multiple `-p` flags. Instead: - 1. Use `git merge-tree` or `git diff` to compare the conflicting files between this PR branch and the PR base branch (read from `/tmp/pr-context/pr.json`) + 1. Use `git diff HEAD...origin/` (base branch from `/tmp/pr-context/pr.json` field `baseRefName`) to see what the base branch changed in the conflicting files 2. Edit the files directly to incorporate the changes from the base branch 3. Commit the changes as regular (single-parent) commits 4. Use push_to_pull_request_branch to push @@ -1253,8 +1253,8 @@ jobs: contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') # Generate diff of all local changes vs upstream for self-review - # Try --merge-base (committed+staged+unstaged vs upstream), fall back to - # @{upstream} 2-dot (committed only), then HEAD (uncommitted only) + # Try --merge-base (vs common ancestor), fall back to + # @{upstream} 2-dot (vs upstream tip), then HEAD (uncommitted only) diff_text = '' for diff_cmd in [ ['git', 'diff', '--merge-base', '@{upstream}'], diff --git a/.github/workflows/gh-aw-mention-in-pr-no-sandbox.lock.yml b/.github/workflows/gh-aw-mention-in-pr-no-sandbox.lock.yml index 7751b608..bb2c44a0 100644 --- a/.github/workflows/gh-aw-mention-in-pr-no-sandbox.lock.yml +++ b/.github/workflows/gh-aw-mention-in-pr-no-sandbox.lock.yml @@ -44,7 +44,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"a1f9d857d28af190c32f118a2b08d94f982282616ce43931e4d363bcc66d8163"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"fd9dbb36c623bbd3a7915d705999722975a8a43cc6bd7b2b1a23996593caaf34"} name: "Mention in PR (no sandbox)" "on": @@ -453,7 +453,7 @@ jobs: - You may not submit code that modifies files in `.github/workflows/`. Doing so will cause the submission to be rejected. If asked to modify workflow files, propose the change in a copy placed in a `github/` folder (without the leading period) and note in the PR that the file needs to be relocated by someone with workflow write access. Trying to resolve merge conflicts? Do NOT create merge commits (commits with multiple parents) — `push_to_pull_request_branch` uses `git format-patch` which breaks on merge commits. This means: no `git merge`, no `git rebase`, no `git commit-tree` with multiple `-p` flags. Instead: - 1. Use `git merge-tree` or `git diff` to compare the conflicting files between this PR branch and the PR base branch (read from `/tmp/pr-context/pr.json`) + 1. Use `git diff HEAD...origin/` (base branch from `/tmp/pr-context/pr.json` field `baseRefName`) to see what the base branch changed in the conflicting files 2. Edit the files directly to incorporate the changes from the base branch 3. Commit the changes as regular (single-parent) commits 4. Use push_to_pull_request_branch to push @@ -1357,8 +1357,8 @@ jobs: contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') # Generate diff of all local changes vs upstream for self-review - # Try --merge-base (committed+staged+unstaged vs upstream), fall back to - # @{upstream} 2-dot (committed only), then HEAD (uncommitted only) + # Try --merge-base (vs common ancestor), fall back to + # @{upstream} 2-dot (vs upstream tip), then HEAD (uncommitted only) diff_text = '' for diff_cmd in [ ['git', 'diff', '--merge-base', '@{upstream}'], diff --git a/.github/workflows/gh-aw-mention-in-pr.lock.yml b/.github/workflows/gh-aw-mention-in-pr.lock.yml index ec104974..52d2c2b4 100644 --- a/.github/workflows/gh-aw-mention-in-pr.lock.yml +++ b/.github/workflows/gh-aw-mention-in-pr.lock.yml @@ -44,7 +44,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"f186b4ca75c18bfe67ac78e8786f59f6e0bd306eff2bb99b62d17bd096df7239"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"c2332cbb0df4ea3be17b0b4bb2b4d822ed265bdcb62b3fb0f9de3afd1772dc56"} name: "Mention in PR" "on": @@ -465,7 +465,7 @@ jobs: - You may not submit code that modifies files in `.github/workflows/`. Doing so will cause the submission to be rejected. If asked to modify workflow files, propose the change in a copy placed in a `github/` folder (without the leading period) and note in the PR that the file needs to be relocated by someone with workflow write access. Trying to resolve merge conflicts? Do NOT create merge commits (commits with multiple parents) — `push_to_pull_request_branch` uses `git format-patch` which breaks on merge commits. This means: no `git merge`, no `git rebase`, no `git commit-tree` with multiple `-p` flags. Instead: - 1. Use `git merge-tree` or `git diff` to compare the conflicting files between this PR branch and the PR base branch (read from `/tmp/pr-context/pr.json`) + 1. Use `git diff HEAD...origin/` (base branch from `/tmp/pr-context/pr.json` field `baseRefName`) to see what the base branch changed in the conflicting files 2. Edit the files directly to incorporate the changes from the base branch 3. Commit the changes as regular (single-parent) commits 4. Use push_to_pull_request_branch to push @@ -1391,8 +1391,8 @@ jobs: contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') # Generate diff of all local changes vs upstream for self-review - # Try --merge-base (committed+staged+unstaged vs upstream), fall back to - # @{upstream} 2-dot (committed only), then HEAD (uncommitted only) + # Try --merge-base (vs common ancestor), fall back to + # @{upstream} 2-dot (vs upstream tip), then HEAD (uncommitted only) diff_text = '' for diff_cmd in [ ['git', 'diff', '--merge-base', '@{upstream}'], diff --git a/.github/workflows/gh-aw-newbie-contributor-fixer.lock.yml b/.github/workflows/gh-aw-newbie-contributor-fixer.lock.yml index b886b92b..458fd1db 100644 --- a/.github/workflows/gh-aw-newbie-contributor-fixer.lock.yml +++ b/.github/workflows/gh-aw-newbie-contributor-fixer.lock.yml @@ -37,7 +37,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"7517579423339f29e0169914dbc406aab3cfc50a49a2f67ba959c1c8dc5476df"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"8448aa6cc23717e296338fa41efae3c393bdfc78463ef7b722694313034024a1"} name: "Newbie Contributor Fixer" "on": @@ -956,8 +956,8 @@ jobs: contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') # Generate diff of all local changes vs upstream for self-review - # Try --merge-base (committed+staged+unstaged vs upstream), fall back to - # @{upstream} 2-dot (committed only), then HEAD (uncommitted only) + # Try --merge-base (vs common ancestor), fall back to + # @{upstream} 2-dot (vs upstream tip), then HEAD (uncommitted only) diff_text = '' for diff_cmd in [ ['git', 'diff', '--merge-base', '@{upstream}'], diff --git a/.github/workflows/gh-aw-pr-actions-fixer.lock.yml b/.github/workflows/gh-aw-pr-actions-fixer.lock.yml index 2c5fc172..30348457 100644 --- a/.github/workflows/gh-aw-pr-actions-fixer.lock.yml +++ b/.github/workflows/gh-aw-pr-actions-fixer.lock.yml @@ -37,7 +37,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"e164adebd7b6780782d25d56627788d045541598c49ab616e750ba0a5a9e00a4"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"e7d11d67614de5d003a7179517eb81006450c07b5dc97782908d03ab9388b5e1"} name: "PR Actions Fixer" "on": @@ -298,7 +298,7 @@ jobs: - You may not submit code that modifies files in `.github/workflows/`. Doing so will cause the submission to be rejected. If asked to modify workflow files, propose the change in a copy placed in a `github/` folder (without the leading period) and note in the PR that the file needs to be relocated by someone with workflow write access. Trying to resolve merge conflicts? Do NOT create merge commits (commits with multiple parents) — `push_to_pull_request_branch` uses `git format-patch` which breaks on merge commits. This means: no `git merge`, no `git rebase`, no `git commit-tree` with multiple `-p` flags. Instead: - 1. Use `git merge-tree` or `git diff` to compare the conflicting files between this PR branch and the PR base branch (read from `/tmp/pr-context/pr.json`) + 1. Use `git diff HEAD...origin/` (base branch from `/tmp/pr-context/pr.json` field `baseRefName`) to see what the base branch changed in the conflicting files 2. Edit the files directly to incorporate the changes from the base branch 3. Commit the changes as regular (single-parent) commits 4. Use push_to_pull_request_branch to push @@ -971,8 +971,8 @@ jobs: contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') # Generate diff of all local changes vs upstream for self-review - # Try --merge-base (committed+staged+unstaged vs upstream), fall back to - # @{upstream} 2-dot (committed only), then HEAD (uncommitted only) + # Try --merge-base (vs common ancestor), fall back to + # @{upstream} 2-dot (vs upstream tip), then HEAD (uncommitted only) diff_text = '' for diff_cmd in [ ['git', 'diff', '--merge-base', '@{upstream}'], diff --git a/.github/workflows/gh-aw-pr-review-addresser.lock.yml b/.github/workflows/gh-aw-pr-review-addresser.lock.yml index 01fc2feb..ae88c49d 100644 --- a/.github/workflows/gh-aw-pr-review-addresser.lock.yml +++ b/.github/workflows/gh-aw-pr-review-addresser.lock.yml @@ -40,7 +40,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"51c25bf2dc5e54ece7260a53347759424d68d373b43adad767dc85577955ef2f"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"3a80e65ab7a75a65ff8e36d901a631fe39aded592e78ecc532d367a3b6158280"} name: "PR Review Addresser" "on": @@ -309,7 +309,7 @@ jobs: - You may not submit code that modifies files in `.github/workflows/`. Doing so will cause the submission to be rejected. If asked to modify workflow files, propose the change in a copy placed in a `github/` folder (without the leading period) and note in the PR that the file needs to be relocated by someone with workflow write access. Trying to resolve merge conflicts? Do NOT create merge commits (commits with multiple parents) — `push_to_pull_request_branch` uses `git format-patch` which breaks on merge commits. This means: no `git merge`, no `git rebase`, no `git commit-tree` with multiple `-p` flags. Instead: - 1. Use `git merge-tree` or `git diff` to compare the conflicting files between this PR branch and the PR base branch (read from `/tmp/pr-context/pr.json`) + 1. Use `git diff HEAD...origin/` (base branch from `/tmp/pr-context/pr.json` field `baseRefName`) to see what the base branch changed in the conflicting files 2. Edit the files directly to incorporate the changes from the base branch 3. Commit the changes as regular (single-parent) commits 4. Use push_to_pull_request_branch to push @@ -1080,8 +1080,8 @@ jobs: contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') # Generate diff of all local changes vs upstream for self-review - # Try --merge-base (committed+staged+unstaged vs upstream), fall back to - # @{upstream} 2-dot (committed only), then HEAD (uncommitted only) + # Try --merge-base (vs common ancestor), fall back to + # @{upstream} 2-dot (vs upstream tip), then HEAD (uncommitted only) diff_text = '' for diff_cmd in [ ['git', 'diff', '--merge-base', '@{upstream}'], diff --git a/.github/workflows/gh-aw-release-update.lock.yml b/.github/workflows/gh-aw-release-update.lock.yml index 729da88d..d67c20d8 100644 --- a/.github/workflows/gh-aw-release-update.lock.yml +++ b/.github/workflows/gh-aw-release-update.lock.yml @@ -36,7 +36,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"e8413d8f5f30f529f10f73c0272523416e066abd3c8a93fcbcfc153681b8c0f3"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"fa74631a4fe4318f4a15809c2d56490e0f1ad6b4e7a52cb864b0db7c281669f3"} name: "Release Update Check" "on": @@ -924,8 +924,8 @@ jobs: contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') # Generate diff of all local changes vs upstream for self-review - # Try --merge-base (committed+staged+unstaged vs upstream), fall back to - # @{upstream} 2-dot (committed only), then HEAD (uncommitted only) + # Try --merge-base (vs common ancestor), fall back to + # @{upstream} 2-dot (vs upstream tip), then HEAD (uncommitted only) diff_text = '' for diff_cmd in [ ['git', 'diff', '--merge-base', '@{upstream}'], diff --git a/.github/workflows/gh-aw-scheduled-fix.lock.yml b/.github/workflows/gh-aw-scheduled-fix.lock.yml index 91eb8c4b..0b9d50be 100644 --- a/.github/workflows/gh-aw-scheduled-fix.lock.yml +++ b/.github/workflows/gh-aw-scheduled-fix.lock.yml @@ -37,7 +37,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"729668b1b3f51805d888df2b61ca2fe108a24b0f61eaf83ac68d75c8f4073200"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"c8e403a480343146d8075e2ccc857faefa7cd5ac0c08b757e798ed47f4ce1554"} name: "Scheduled Fix" "on": @@ -979,8 +979,8 @@ jobs: contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') # Generate diff of all local changes vs upstream for self-review - # Try --merge-base (committed+staged+unstaged vs upstream), fall back to - # @{upstream} 2-dot (committed only), then HEAD (uncommitted only) + # Try --merge-base (vs common ancestor), fall back to + # @{upstream} 2-dot (vs upstream tip), then HEAD (uncommitted only) diff_text = '' for diff_cmd in [ ['git', 'diff', '--merge-base', '@{upstream}'], diff --git a/.github/workflows/gh-aw-small-problem-fixer.lock.yml b/.github/workflows/gh-aw-small-problem-fixer.lock.yml index f9dc2bbf..d612314d 100644 --- a/.github/workflows/gh-aw-small-problem-fixer.lock.yml +++ b/.github/workflows/gh-aw-small-problem-fixer.lock.yml @@ -37,7 +37,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"17d0b1e78e19ecad5182a120aeffc9726e3aadcd5a42137f4bb24bfa388f103f"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"f15637cf54b9d879ea7bd3ee5207c0e7e7143645f779711e33328b5a5d1ac746"} name: "Small Problem Fixer" "on": @@ -1007,8 +1007,8 @@ jobs: contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') # Generate diff of all local changes vs upstream for self-review - # Try --merge-base (committed+staged+unstaged vs upstream), fall back to - # @{upstream} 2-dot (committed only), then HEAD (uncommitted only) + # Try --merge-base (vs common ancestor), fall back to + # @{upstream} 2-dot (vs upstream tip), then HEAD (uncommitted only) diff_text = '' for diff_cmd in [ ['git', 'diff', '--merge-base', '@{upstream}'], diff --git a/.github/workflows/gh-aw-test-improvement.lock.yml b/.github/workflows/gh-aw-test-improvement.lock.yml index 9efbd71b..08929967 100644 --- a/.github/workflows/gh-aw-test-improvement.lock.yml +++ b/.github/workflows/gh-aw-test-improvement.lock.yml @@ -41,7 +41,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"399380cbad5e18f4fe5836e1df05733b9b25bbf6c8e1571d4ab4afb3a494a6f9"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"720fbeefb1cdfbab657d459a504b39d5bc7bd419d6b6214874f323e9a834afed"} name: "Test Improver" "on": @@ -967,8 +967,8 @@ jobs: contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') # Generate diff of all local changes vs upstream for self-review - # Try --merge-base (committed+staged+unstaged vs upstream), fall back to - # @{upstream} 2-dot (committed only), then HEAD (uncommitted only) + # Try --merge-base (vs common ancestor), fall back to + # @{upstream} 2-dot (vs upstream tip), then HEAD (uncommitted only) diff_text = '' for diff_cmd in [ ['git', 'diff', '--merge-base', '@{upstream}'], diff --git a/.github/workflows/gh-aw-test-improver.lock.yml b/.github/workflows/gh-aw-test-improver.lock.yml index e4f320d8..7700ae13 100644 --- a/.github/workflows/gh-aw-test-improver.lock.yml +++ b/.github/workflows/gh-aw-test-improver.lock.yml @@ -36,7 +36,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"399380cbad5e18f4fe5836e1df05733b9b25bbf6c8e1571d4ab4afb3a494a6f9"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"720fbeefb1cdfbab657d459a504b39d5bc7bd419d6b6214874f323e9a834afed"} name: "Test Improver" "on": @@ -962,8 +962,8 @@ jobs: contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') # Generate diff of all local changes vs upstream for self-review - # Try --merge-base (committed+staged+unstaged vs upstream), fall back to - # @{upstream} 2-dot (committed only), then HEAD (uncommitted only) + # Try --merge-base (vs common ancestor), fall back to + # @{upstream} 2-dot (vs upstream tip), then HEAD (uncommitted only) diff_text = '' for diff_cmd in [ ['git', 'diff', '--merge-base', '@{upstream}'], diff --git a/.github/workflows/gh-aw-text-beautifier.lock.yml b/.github/workflows/gh-aw-text-beautifier.lock.yml index c799ffa6..bf2b0045 100644 --- a/.github/workflows/gh-aw-text-beautifier.lock.yml +++ b/.github/workflows/gh-aw-text-beautifier.lock.yml @@ -38,7 +38,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"f78088e1c8e96dba6189d8221a8090c823e8db499d8f55c352350a1875ebea81"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"2c23a606f6c9943ca7855f938da18eb419deb3009ec006a572d96c01241d8a83"} name: "Text Beautifier" "on": @@ -964,8 +964,8 @@ jobs: contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') # Generate diff of all local changes vs upstream for self-review - # Try --merge-base (committed+staged+unstaged vs upstream), fall back to - # @{upstream} 2-dot (committed only), then HEAD (uncommitted only) + # Try --merge-base (vs common ancestor), fall back to + # @{upstream} 2-dot (vs upstream tip), then HEAD (uncommitted only) diff_text = '' for diff_cmd in [ ['git', 'diff', '--merge-base', '@{upstream}'], From 47e1d3bbccd9b3a137269d05c655ebeda235385a Mon Sep 17 00:00:00 2001 From: William Easton Date: Sat, 28 Feb 2026 01:04:20 -0600 Subject: [PATCH 7/8] detect invalid pushes --- .github/workflows/downstream-users.lock.yml | 5 +++- .../workflows/gh-aw-bug-exterminator.lock.yml | 5 +++- .../gh-aw-code-duplication-fixer.lock.yml | 5 +++- .../workflows/gh-aw-code-simplifier.lock.yml | 5 +++- .../gh-aw-fragments/safe-output-create-pr.md | 3 +++ .../gh-aw-fragments/safe-output-push-to-pr.md | 13 +++++---- .github/workflows/gh-aw-issue-fixer.lock.yml | 5 +++- ...gh-aw-mention-in-issue-no-sandbox.lock.yml | 5 +++- .../workflows/gh-aw-mention-in-issue.lock.yml | 5 +++- .../gh-aw-mention-in-pr-by-id.lock.yml | 27 ++++++++++--------- .../gh-aw-mention-in-pr-no-sandbox.lock.yml | 27 ++++++++++--------- .../workflows/gh-aw-mention-in-pr.lock.yml | 27 ++++++++++--------- .../gh-aw-newbie-contributor-fixer.lock.yml | 5 +++- .../workflows/gh-aw-pr-actions-fixer.lock.yml | 27 ++++++++++--------- .../gh-aw-pr-review-addresser.lock.yml | 27 ++++++++++--------- .../workflows/gh-aw-release-update.lock.yml | 5 +++- .../workflows/gh-aw-scheduled-fix.lock.yml | 5 +++- .../gh-aw-small-problem-fixer.lock.yml | 5 +++- .../workflows/gh-aw-test-improvement.lock.yml | 5 +++- .../workflows/gh-aw-test-improver.lock.yml | 5 +++- .../workflows/gh-aw-text-beautifier.lock.yml | 5 +++- tests/test_safe_input_ready_to_make_pr.py | 21 +++++++-------- 22 files changed, 152 insertions(+), 90 deletions(-) diff --git a/.github/workflows/downstream-users.lock.yml b/.github/workflows/downstream-users.lock.yml index 4447fa73..003bf2cd 100644 --- a/.github/workflows/downstream-users.lock.yml +++ b/.github/workflows/downstream-users.lock.yml @@ -36,7 +36,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"e43d8a296eb8fddc80fad9fd24bbc8089c75c17d7adefda9da8f6666ff2c8a2e"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"b309db9c1ea539cc8ed7a88a26d66e9f35d4d455b1b16322f422bd4098679c9b"} name: "Internal: Downstream Users" "on": @@ -896,6 +896,9 @@ jobs: print(json.dumps({'status': 'error', 'error': 'Unable to determine upstream fork point for merge-commit validation. Fix: ensure remotes are fetched and a tracking branch is set (e.g., `git branch --set-upstream-to origin/`), then rerun ready_to_make_pr.'})) raise SystemExit(0) log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) + if log.returncode != 0: + print(json.dumps({'status': 'error', 'error': f'Failed to check for merge commits (git rev-list exited {log.returncode}): {log.stderr.strip()}. Cannot verify commit history is safe for PR creation.'})) + raise SystemExit(0) merge_shas = log.stdout.strip() if merge_shas: print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) diff --git a/.github/workflows/gh-aw-bug-exterminator.lock.yml b/.github/workflows/gh-aw-bug-exterminator.lock.yml index 3aa7fc5a..bcfe4857 100644 --- a/.github/workflows/gh-aw-bug-exterminator.lock.yml +++ b/.github/workflows/gh-aw-bug-exterminator.lock.yml @@ -37,7 +37,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"81fe7c2b632df6afa9b55605616b2e97e6d6a271416f5da65847a7a3ba83b417"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"48a3e323229dc2b434ab6c1d1f6a1f2ec4aa544cd16acfd50b0eedfea4ccbcc2"} name: "Gh Aw Bug Exterminator" "on": @@ -945,6 +945,9 @@ jobs: print(json.dumps({'status': 'error', 'error': 'Unable to determine upstream fork point for merge-commit validation. Fix: ensure remotes are fetched and a tracking branch is set (e.g., `git branch --set-upstream-to origin/`), then rerun ready_to_make_pr.'})) raise SystemExit(0) log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) + if log.returncode != 0: + print(json.dumps({'status': 'error', 'error': f'Failed to check for merge commits (git rev-list exited {log.returncode}): {log.stderr.strip()}. Cannot verify commit history is safe for PR creation.'})) + raise SystemExit(0) merge_shas = log.stdout.strip() if merge_shas: print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) diff --git a/.github/workflows/gh-aw-code-duplication-fixer.lock.yml b/.github/workflows/gh-aw-code-duplication-fixer.lock.yml index 6fa283a5..46506829 100644 --- a/.github/workflows/gh-aw-code-duplication-fixer.lock.yml +++ b/.github/workflows/gh-aw-code-duplication-fixer.lock.yml @@ -37,7 +37,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"c0e75e19a630a4b22508bf3724d337c366ba4a457841c1db13f8af3bf46f4318"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"35511b1f2b8cca89490cb19ff4b59e8318373c5edd9d4e390682d589ef35fd3e"} name: "Code Duplication Fixer" "on": @@ -947,6 +947,9 @@ jobs: print(json.dumps({'status': 'error', 'error': 'Unable to determine upstream fork point for merge-commit validation. Fix: ensure remotes are fetched and a tracking branch is set (e.g., `git branch --set-upstream-to origin/`), then rerun ready_to_make_pr.'})) raise SystemExit(0) log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) + if log.returncode != 0: + print(json.dumps({'status': 'error', 'error': f'Failed to check for merge commits (git rev-list exited {log.returncode}): {log.stderr.strip()}. Cannot verify commit history is safe for PR creation.'})) + raise SystemExit(0) merge_shas = log.stdout.strip() if merge_shas: print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) diff --git a/.github/workflows/gh-aw-code-simplifier.lock.yml b/.github/workflows/gh-aw-code-simplifier.lock.yml index 001565b3..14e3d9f3 100644 --- a/.github/workflows/gh-aw-code-simplifier.lock.yml +++ b/.github/workflows/gh-aw-code-simplifier.lock.yml @@ -37,7 +37,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"adeab66764152c1e72be562cc1b7e81be1c6b8950fc193334e3911110f3cfced"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"6138d73dad21c897fd905116ef5d1ca73d766e44210d2eec4392d076babbace0"} name: "Code Simplifier" "on": @@ -962,6 +962,9 @@ jobs: print(json.dumps({'status': 'error', 'error': 'Unable to determine upstream fork point for merge-commit validation. Fix: ensure remotes are fetched and a tracking branch is set (e.g., `git branch --set-upstream-to origin/`), then rerun ready_to_make_pr.'})) raise SystemExit(0) log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) + if log.returncode != 0: + print(json.dumps({'status': 'error', 'error': f'Failed to check for merge commits (git rev-list exited {log.returncode}): {log.stderr.strip()}. Cannot verify commit history is safe for PR creation.'})) + raise SystemExit(0) merge_shas = log.stdout.strip() if merge_shas: print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) diff --git a/.github/workflows/gh-aw-fragments/safe-output-create-pr.md b/.github/workflows/gh-aw-fragments/safe-output-create-pr.md index 7352e41d..771e2401 100644 --- a/.github/workflows/gh-aw-fragments/safe-output-create-pr.md +++ b/.github/workflows/gh-aw-fragments/safe-output-create-pr.md @@ -24,6 +24,9 @@ safe-inputs: print(json.dumps({'status': 'error', 'error': 'Unable to determine upstream fork point for merge-commit validation. Fix: ensure remotes are fetched and a tracking branch is set (e.g., `git branch --set-upstream-to origin/`), then rerun ready_to_make_pr.'})) raise SystemExit(0) log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) + if log.returncode != 0: + print(json.dumps({'status': 'error', 'error': f'Failed to check for merge commits (git rev-list exited {log.returncode}): {log.stderr.strip()}. Cannot verify commit history is safe for PR creation.'})) + raise SystemExit(0) merge_shas = log.stdout.strip() if merge_shas: print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) diff --git a/.github/workflows/gh-aw-fragments/safe-output-push-to-pr.md b/.github/workflows/gh-aw-fragments/safe-output-push-to-pr.md index 226c2da6..eb36a774 100644 --- a/.github/workflows/gh-aw-fragments/safe-output-push-to-pr.md +++ b/.github/workflows/gh-aw-fragments/safe-output-push-to-pr.md @@ -1,7 +1,7 @@ --- safe-inputs: - ready-to-make-pr: - description: "Run the PR readiness checklist before creating or updating a PR" + ready-to-push-to-pr: + description: "Run the PR readiness checklist before pushing to a PR" py: | import os, json, subprocess def find(*paths): @@ -26,6 +26,9 @@ safe-inputs: raise SystemExit(0) # Check 2: no merge commits (multiple parents) since PR head log = run(['git', 'rev-list', '--min-parents=2', f'{pr_head_sha}..HEAD']) + if log.returncode != 0: + print(json.dumps({'status': 'error', 'error': f'Failed to check for merge commits (git rev-list exited {log.returncode}): {log.stderr.strip()}. Cannot verify commit history is safe for push.'})) + raise SystemExit(0) merge_shas = log.stdout.strip() if merge_shas: print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... push_to_pull_request_branch uses git format-patch which breaks on merge commits. Fix: run `git reset --hard {pr_head_sha}` to restore the PR branch, then re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) @@ -67,14 +70,14 @@ safe-inputs: if pr_template: checklist.append(f'Follow the PR template ({pr_template}) for title, description, and validation notes.') checklist.append('Confirm the requested task is fully completed and validated before creating or pushing PR changes.') if diff_line_count > 0: - checklist.append(f'A diff of your unpushed changes ({diff_line_count} lines) has been saved to `/tmp/self-review/diff.patch` (full diff) and `/tmp/self-review/stat.txt` (summary). Spawn a `code-review` sub-agent via `runSubagent` to review the diff against the codebase. Tell it to read `/tmp/self-review/diff.patch` and the relevant source files, and look for bugs, logic errors, missed edge cases, and style issues. If the sub-agent finds legitimate issues, fix them, commit, and call `ready_to_make_pr` again to regenerate the diff before proceeding.') + checklist.append(f'A diff of your unpushed changes ({diff_line_count} lines) has been saved to `/tmp/self-review/diff.patch` (full diff) and `/tmp/self-review/stat.txt` (summary). Spawn a `code-review` sub-agent via `runSubagent` to review the diff against the codebase. Tell it to read `/tmp/self-review/diff.patch` and the relevant source files, and look for bugs, logic errors, missed edge cases, and style issues. If the sub-agent finds legitimate issues, fix them, commit, and call `ready_to_push_to_pr` again to regenerate the diff before proceeding.') print(json.dumps({'status': 'ok', 'checklist': checklist, 'contributing_guide': contributing, 'pr_template': pr_template, 'diff_line_count': diff_line_count})) safe-outputs: push-to-pull-request-branch: github-token-for-extra-empty-commit: ${{ secrets.EXTRA_COMMIT_GITHUB_TOKEN }} --- -Before calling `push_to_pull_request_branch`, call `ready_to_make_pr` and apply its checklist. +Before calling `push_to_pull_request_branch`, call `ready_to_push_to_pr` and apply its checklist. ## push-to-pull-request-branch Limitations @@ -88,4 +91,4 @@ Trying to resolve merge conflicts? Do NOT create merge commits (commits with mul 1. Use `git diff HEAD...origin/` (base branch from `/tmp/pr-context/pr.json` field `baseRefName`) to see what the base branch changed in the conflicting files 2. Edit the files directly to incorporate the changes from the base branch 3. Commit the changes as regular (single-parent) commits -4. Use push_to_pull_request_branch to push +4. Once you are done with all of your changes on this branch, call `ready_to_push_to_pr` and then `push_to_pull_request_branch` to push diff --git a/.github/workflows/gh-aw-issue-fixer.lock.yml b/.github/workflows/gh-aw-issue-fixer.lock.yml index 6b21e27c..ce193984 100644 --- a/.github/workflows/gh-aw-issue-fixer.lock.yml +++ b/.github/workflows/gh-aw-issue-fixer.lock.yml @@ -38,7 +38,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"d611f067d7728287548052bb2e8033d40067f412f234ea5574773aa951c4a0d2"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"19f326850a01fe8800486e3839057f314a1a2ff0afb9b9b3196aff2a3d7b5d92"} name: "Issue Fixer" "on": @@ -995,6 +995,9 @@ jobs: print(json.dumps({'status': 'error', 'error': 'Unable to determine upstream fork point for merge-commit validation. Fix: ensure remotes are fetched and a tracking branch is set (e.g., `git branch --set-upstream-to origin/`), then rerun ready_to_make_pr.'})) raise SystemExit(0) log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) + if log.returncode != 0: + print(json.dumps({'status': 'error', 'error': f'Failed to check for merge commits (git rev-list exited {log.returncode}): {log.stderr.strip()}. Cannot verify commit history is safe for PR creation.'})) + raise SystemExit(0) merge_shas = log.stdout.strip() if merge_shas: print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) diff --git a/.github/workflows/gh-aw-mention-in-issue-no-sandbox.lock.yml b/.github/workflows/gh-aw-mention-in-issue-no-sandbox.lock.yml index c7f88a01..c2e0d5f7 100644 --- a/.github/workflows/gh-aw-mention-in-issue-no-sandbox.lock.yml +++ b/.github/workflows/gh-aw-mention-in-issue-no-sandbox.lock.yml @@ -39,7 +39,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"b01c2e024e7984cea8195e68d34a62ea3c0bed11d3227863b176229287966825"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"0b87d308d3e15d36ce014f6c26b37a29bcc8f7f787c92864df432fa97e3f985a"} name: "Mention in Issue (no sandbox)" "on": @@ -1083,6 +1083,9 @@ jobs: print(json.dumps({'status': 'error', 'error': 'Unable to determine upstream fork point for merge-commit validation. Fix: ensure remotes are fetched and a tracking branch is set (e.g., `git branch --set-upstream-to origin/`), then rerun ready_to_make_pr.'})) raise SystemExit(0) log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) + if log.returncode != 0: + print(json.dumps({'status': 'error', 'error': f'Failed to check for merge commits (git rev-list exited {log.returncode}): {log.stderr.strip()}. Cannot verify commit history is safe for PR creation.'})) + raise SystemExit(0) merge_shas = log.stdout.strip() if merge_shas: print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) diff --git a/.github/workflows/gh-aw-mention-in-issue.lock.yml b/.github/workflows/gh-aw-mention-in-issue.lock.yml index d854e614..69e3aa89 100644 --- a/.github/workflows/gh-aw-mention-in-issue.lock.yml +++ b/.github/workflows/gh-aw-mention-in-issue.lock.yml @@ -39,7 +39,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"13ba73c6ccffccc91d6878e8fd94697098aaa96b76585a5ab7d70278341dcc46"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"9e57139f1dd35552a2b68d05aaba3ccc129668cb5ea581f763fd102f15f4eff2"} name: "Mention in Issue" "on": @@ -1087,6 +1087,9 @@ jobs: print(json.dumps({'status': 'error', 'error': 'Unable to determine upstream fork point for merge-commit validation. Fix: ensure remotes are fetched and a tracking branch is set (e.g., `git branch --set-upstream-to origin/`), then rerun ready_to_make_pr.'})) raise SystemExit(0) log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) + if log.returncode != 0: + print(json.dumps({'status': 'error', 'error': f'Failed to check for merge commits (git rev-list exited {log.returncode}): {log.stderr.strip()}. Cannot verify commit history is safe for PR creation.'})) + raise SystemExit(0) merge_shas = log.stdout.strip() if merge_shas: print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) diff --git a/.github/workflows/gh-aw-mention-in-pr-by-id.lock.yml b/.github/workflows/gh-aw-mention-in-pr-by-id.lock.yml index cd2a95e3..f4fa5236 100644 --- a/.github/workflows/gh-aw-mention-in-pr-by-id.lock.yml +++ b/.github/workflows/gh-aw-mention-in-pr-by-id.lock.yml @@ -43,7 +43,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"15f053fec1502ce4d5b0586f9cb578e6b0101b9d2813851a2eaa1042e25d5d59"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"8eaf0ee006b4e1a7ed22d3c143762af9f6f68663ee2d39b0826a601d7d9f06ce"} name: "Mention in PR by ID" "on": @@ -426,7 +426,7 @@ jobs: **Do NOT** describe what the PR does, list the files you reviewed, summarize inline comments, or restate prior review feedback. The PR author already knows what their PR does. Your inline comments already contain all the detail. The review body exists solely to communicate the approve/request-changes decision and important/critical feedback that cannot be covered in inline comments. GH_AW_PROMPT_EOF cat << 'GH_AW_PROMPT_EOF' - Before calling `push_to_pull_request_branch`, call `ready_to_make_pr` and apply its checklist. + Before calling `push_to_pull_request_branch`, call `ready_to_push_to_pr` and apply its checklist. ## push-to-pull-request-branch Limitations @@ -440,7 +440,7 @@ jobs: 1. Use `git diff HEAD...origin/` (base branch from `/tmp/pr-context/pr.json` field `baseRefName`) to see what the base branch changed in the conflicting files 2. Edit the files directly to incorporate the changes from the base branch 3. Commit the changes as regular (single-parent) commits - 4. Use push_to_pull_request_branch to push + 4. Once you are done with all of your changes on this branch, call `ready_to_push_to_pr` and then `push_to_pull_request_branch` to push GH_AW_PROMPT_EOF cat << 'GH_AW_PROMPT_EOF' ## resolve-pull-request-review-thread Limitations @@ -1175,13 +1175,13 @@ jobs: "logDir": "/opt/gh-aw/safe-inputs/logs", "tools": [ { - "name": "ready-to-make-pr", - "description": "Run the PR readiness checklist before creating or updating a PR", + "name": "ready-to-push-to-pr", + "description": "Run the PR readiness checklist before pushing to a PR", "inputSchema": { "properties": {}, "type": "object" }, - "handler": "ready-to-make-pr.py", + "handler": "ready-to-push-to-pr.py", "timeout": 60 } ] @@ -1206,10 +1206,10 @@ jobs: - name: Setup Safe Inputs Tool Files run: | - cat > /opt/gh-aw/safe-inputs/ready-to-make-pr.py << 'GH_AW_SAFE_INPUTS_PY_READY-TO-MAKE-PR_EOF' + cat > /opt/gh-aw/safe-inputs/ready-to-push-to-pr.py << 'GH_AW_SAFE_INPUTS_PY_READY-TO-PUSH-TO-PR_EOF' #!/usr/bin/env python3 - # Auto-generated safe-input tool: ready-to-make-pr - # Run the PR readiness checklist before creating or updating a PR + # Auto-generated safe-input tool: ready-to-push-to-pr + # Run the PR readiness checklist before pushing to a PR import json import os @@ -1245,6 +1245,9 @@ jobs: raise SystemExit(0) # Check 2: no merge commits (multiple parents) since PR head log = run(['git', 'rev-list', '--min-parents=2', f'{pr_head_sha}..HEAD']) + if log.returncode != 0: + print(json.dumps({'status': 'error', 'error': f'Failed to check for merge commits (git rev-list exited {log.returncode}): {log.stderr.strip()}. Cannot verify commit history is safe for push.'})) + raise SystemExit(0) merge_shas = log.stdout.strip() if merge_shas: print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... push_to_pull_request_branch uses git format-patch which breaks on merge commits. Fix: run `git reset --hard {pr_head_sha}` to restore the PR branch, then re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) @@ -1286,12 +1289,12 @@ jobs: if pr_template: checklist.append(f'Follow the PR template ({pr_template}) for title, description, and validation notes.') checklist.append('Confirm the requested task is fully completed and validated before creating or pushing PR changes.') if diff_line_count > 0: - checklist.append(f'A diff of your unpushed changes ({diff_line_count} lines) has been saved to `/tmp/self-review/diff.patch` (full diff) and `/tmp/self-review/stat.txt` (summary). Spawn a `code-review` sub-agent via `runSubagent` to review the diff against the codebase. Tell it to read `/tmp/self-review/diff.patch` and the relevant source files, and look for bugs, logic errors, missed edge cases, and style issues. If the sub-agent finds legitimate issues, fix them, commit, and call `ready_to_make_pr` again to regenerate the diff before proceeding.') + checklist.append(f'A diff of your unpushed changes ({diff_line_count} lines) has been saved to `/tmp/self-review/diff.patch` (full diff) and `/tmp/self-review/stat.txt` (summary). Spawn a `code-review` sub-agent via `runSubagent` to review the diff against the codebase. Tell it to read `/tmp/self-review/diff.patch` and the relevant source files, and look for bugs, logic errors, missed edge cases, and style issues. If the sub-agent finds legitimate issues, fix them, commit, and call `ready_to_push_to_pr` again to regenerate the diff before proceeding.') print(json.dumps({'status': 'ok', 'checklist': checklist, 'contributing_guide': contributing, 'pr_template': pr_template, 'diff_line_count': diff_line_count})) - GH_AW_SAFE_INPUTS_PY_READY-TO-MAKE-PR_EOF - chmod +x /opt/gh-aw/safe-inputs/ready-to-make-pr.py + GH_AW_SAFE_INPUTS_PY_READY-TO-PUSH-TO-PR_EOF + chmod +x /opt/gh-aw/safe-inputs/ready-to-push-to-pr.py - name: Generate Safe Inputs MCP Server Config id: safe-inputs-config diff --git a/.github/workflows/gh-aw-mention-in-pr-no-sandbox.lock.yml b/.github/workflows/gh-aw-mention-in-pr-no-sandbox.lock.yml index bb2c44a0..0e971bf5 100644 --- a/.github/workflows/gh-aw-mention-in-pr-no-sandbox.lock.yml +++ b/.github/workflows/gh-aw-mention-in-pr-no-sandbox.lock.yml @@ -44,7 +44,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"fd9dbb36c623bbd3a7915d705999722975a8a43cc6bd7b2b1a23996593caaf34"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"37eed9452f2bf3443bda29742805f3e384dcd059772d5238c9aa7c0737a31c4a"} name: "Mention in PR (no sandbox)" "on": @@ -442,7 +442,7 @@ jobs: **Do NOT** describe what the PR does, list the files you reviewed, summarize inline comments, or restate prior review feedback. The PR author already knows what their PR does. Your inline comments already contain all the detail. The review body exists solely to communicate the approve/request-changes decision and important/critical feedback that cannot be covered in inline comments. GH_AW_PROMPT_EOF cat << 'GH_AW_PROMPT_EOF' - Before calling `push_to_pull_request_branch`, call `ready_to_make_pr` and apply its checklist. + Before calling `push_to_pull_request_branch`, call `ready_to_push_to_pr` and apply its checklist. ## push-to-pull-request-branch Limitations @@ -456,7 +456,7 @@ jobs: 1. Use `git diff HEAD...origin/` (base branch from `/tmp/pr-context/pr.json` field `baseRefName`) to see what the base branch changed in the conflicting files 2. Edit the files directly to incorporate the changes from the base branch 3. Commit the changes as regular (single-parent) commits - 4. Use push_to_pull_request_branch to push + 4. Once you are done with all of your changes on this branch, call `ready_to_push_to_pr` and then `push_to_pull_request_branch` to push GH_AW_PROMPT_EOF cat << 'GH_AW_PROMPT_EOF' ## resolve-pull-request-review-thread Limitations @@ -1279,13 +1279,13 @@ jobs: "logDir": "/opt/gh-aw/safe-inputs/logs", "tools": [ { - "name": "ready-to-make-pr", - "description": "Run the PR readiness checklist before creating or updating a PR", + "name": "ready-to-push-to-pr", + "description": "Run the PR readiness checklist before pushing to a PR", "inputSchema": { "properties": {}, "type": "object" }, - "handler": "ready-to-make-pr.py", + "handler": "ready-to-push-to-pr.py", "timeout": 60 } ] @@ -1310,10 +1310,10 @@ jobs: - name: Setup Safe Inputs Tool Files run: | - cat > /opt/gh-aw/safe-inputs/ready-to-make-pr.py << 'GH_AW_SAFE_INPUTS_PY_READY-TO-MAKE-PR_EOF' + cat > /opt/gh-aw/safe-inputs/ready-to-push-to-pr.py << 'GH_AW_SAFE_INPUTS_PY_READY-TO-PUSH-TO-PR_EOF' #!/usr/bin/env python3 - # Auto-generated safe-input tool: ready-to-make-pr - # Run the PR readiness checklist before creating or updating a PR + # Auto-generated safe-input tool: ready-to-push-to-pr + # Run the PR readiness checklist before pushing to a PR import json import os @@ -1349,6 +1349,9 @@ jobs: raise SystemExit(0) # Check 2: no merge commits (multiple parents) since PR head log = run(['git', 'rev-list', '--min-parents=2', f'{pr_head_sha}..HEAD']) + if log.returncode != 0: + print(json.dumps({'status': 'error', 'error': f'Failed to check for merge commits (git rev-list exited {log.returncode}): {log.stderr.strip()}. Cannot verify commit history is safe for push.'})) + raise SystemExit(0) merge_shas = log.stdout.strip() if merge_shas: print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... push_to_pull_request_branch uses git format-patch which breaks on merge commits. Fix: run `git reset --hard {pr_head_sha}` to restore the PR branch, then re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) @@ -1390,12 +1393,12 @@ jobs: if pr_template: checklist.append(f'Follow the PR template ({pr_template}) for title, description, and validation notes.') checklist.append('Confirm the requested task is fully completed and validated before creating or pushing PR changes.') if diff_line_count > 0: - checklist.append(f'A diff of your unpushed changes ({diff_line_count} lines) has been saved to `/tmp/self-review/diff.patch` (full diff) and `/tmp/self-review/stat.txt` (summary). Spawn a `code-review` sub-agent via `runSubagent` to review the diff against the codebase. Tell it to read `/tmp/self-review/diff.patch` and the relevant source files, and look for bugs, logic errors, missed edge cases, and style issues. If the sub-agent finds legitimate issues, fix them, commit, and call `ready_to_make_pr` again to regenerate the diff before proceeding.') + checklist.append(f'A diff of your unpushed changes ({diff_line_count} lines) has been saved to `/tmp/self-review/diff.patch` (full diff) and `/tmp/self-review/stat.txt` (summary). Spawn a `code-review` sub-agent via `runSubagent` to review the diff against the codebase. Tell it to read `/tmp/self-review/diff.patch` and the relevant source files, and look for bugs, logic errors, missed edge cases, and style issues. If the sub-agent finds legitimate issues, fix them, commit, and call `ready_to_push_to_pr` again to regenerate the diff before proceeding.') print(json.dumps({'status': 'ok', 'checklist': checklist, 'contributing_guide': contributing, 'pr_template': pr_template, 'diff_line_count': diff_line_count})) - GH_AW_SAFE_INPUTS_PY_READY-TO-MAKE-PR_EOF - chmod +x /opt/gh-aw/safe-inputs/ready-to-make-pr.py + GH_AW_SAFE_INPUTS_PY_READY-TO-PUSH-TO-PR_EOF + chmod +x /opt/gh-aw/safe-inputs/ready-to-push-to-pr.py - name: Generate Safe Inputs MCP Server Config id: safe-inputs-config diff --git a/.github/workflows/gh-aw-mention-in-pr.lock.yml b/.github/workflows/gh-aw-mention-in-pr.lock.yml index 52d2c2b4..cb3c7f5a 100644 --- a/.github/workflows/gh-aw-mention-in-pr.lock.yml +++ b/.github/workflows/gh-aw-mention-in-pr.lock.yml @@ -44,7 +44,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"c2332cbb0df4ea3be17b0b4bb2b4d822ed265bdcb62b3fb0f9de3afd1772dc56"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"3e2208ff26fa33bb18399c3abb9067fe36fae6c3b0bd59650e6973ee1ba67b99"} name: "Mention in PR" "on": @@ -454,7 +454,7 @@ jobs: **Do NOT** describe what the PR does, list the files you reviewed, summarize inline comments, or restate prior review feedback. The PR author already knows what their PR does. Your inline comments already contain all the detail. The review body exists solely to communicate the approve/request-changes decision and important/critical feedback that cannot be covered in inline comments. GH_AW_PROMPT_EOF cat << 'GH_AW_PROMPT_EOF' - Before calling `push_to_pull_request_branch`, call `ready_to_make_pr` and apply its checklist. + Before calling `push_to_pull_request_branch`, call `ready_to_push_to_pr` and apply its checklist. ## push-to-pull-request-branch Limitations @@ -468,7 +468,7 @@ jobs: 1. Use `git diff HEAD...origin/` (base branch from `/tmp/pr-context/pr.json` field `baseRefName`) to see what the base branch changed in the conflicting files 2. Edit the files directly to incorporate the changes from the base branch 3. Commit the changes as regular (single-parent) commits - 4. Use push_to_pull_request_branch to push + 4. Once you are done with all of your changes on this branch, call `ready_to_push_to_pr` and then `push_to_pull_request_branch` to push GH_AW_PROMPT_EOF cat << 'GH_AW_PROMPT_EOF' ## resolve-pull-request-review-thread Limitations @@ -1313,13 +1313,13 @@ jobs: "logDir": "/opt/gh-aw/safe-inputs/logs", "tools": [ { - "name": "ready-to-make-pr", - "description": "Run the PR readiness checklist before creating or updating a PR", + "name": "ready-to-push-to-pr", + "description": "Run the PR readiness checklist before pushing to a PR", "inputSchema": { "properties": {}, "type": "object" }, - "handler": "ready-to-make-pr.py", + "handler": "ready-to-push-to-pr.py", "timeout": 60 } ] @@ -1344,10 +1344,10 @@ jobs: - name: Setup Safe Inputs Tool Files run: | - cat > /opt/gh-aw/safe-inputs/ready-to-make-pr.py << 'GH_AW_SAFE_INPUTS_PY_READY-TO-MAKE-PR_EOF' + cat > /opt/gh-aw/safe-inputs/ready-to-push-to-pr.py << 'GH_AW_SAFE_INPUTS_PY_READY-TO-PUSH-TO-PR_EOF' #!/usr/bin/env python3 - # Auto-generated safe-input tool: ready-to-make-pr - # Run the PR readiness checklist before creating or updating a PR + # Auto-generated safe-input tool: ready-to-push-to-pr + # Run the PR readiness checklist before pushing to a PR import json import os @@ -1383,6 +1383,9 @@ jobs: raise SystemExit(0) # Check 2: no merge commits (multiple parents) since PR head log = run(['git', 'rev-list', '--min-parents=2', f'{pr_head_sha}..HEAD']) + if log.returncode != 0: + print(json.dumps({'status': 'error', 'error': f'Failed to check for merge commits (git rev-list exited {log.returncode}): {log.stderr.strip()}. Cannot verify commit history is safe for push.'})) + raise SystemExit(0) merge_shas = log.stdout.strip() if merge_shas: print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... push_to_pull_request_branch uses git format-patch which breaks on merge commits. Fix: run `git reset --hard {pr_head_sha}` to restore the PR branch, then re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) @@ -1424,12 +1427,12 @@ jobs: if pr_template: checklist.append(f'Follow the PR template ({pr_template}) for title, description, and validation notes.') checklist.append('Confirm the requested task is fully completed and validated before creating or pushing PR changes.') if diff_line_count > 0: - checklist.append(f'A diff of your unpushed changes ({diff_line_count} lines) has been saved to `/tmp/self-review/diff.patch` (full diff) and `/tmp/self-review/stat.txt` (summary). Spawn a `code-review` sub-agent via `runSubagent` to review the diff against the codebase. Tell it to read `/tmp/self-review/diff.patch` and the relevant source files, and look for bugs, logic errors, missed edge cases, and style issues. If the sub-agent finds legitimate issues, fix them, commit, and call `ready_to_make_pr` again to regenerate the diff before proceeding.') + checklist.append(f'A diff of your unpushed changes ({diff_line_count} lines) has been saved to `/tmp/self-review/diff.patch` (full diff) and `/tmp/self-review/stat.txt` (summary). Spawn a `code-review` sub-agent via `runSubagent` to review the diff against the codebase. Tell it to read `/tmp/self-review/diff.patch` and the relevant source files, and look for bugs, logic errors, missed edge cases, and style issues. If the sub-agent finds legitimate issues, fix them, commit, and call `ready_to_push_to_pr` again to regenerate the diff before proceeding.') print(json.dumps({'status': 'ok', 'checklist': checklist, 'contributing_guide': contributing, 'pr_template': pr_template, 'diff_line_count': diff_line_count})) - GH_AW_SAFE_INPUTS_PY_READY-TO-MAKE-PR_EOF - chmod +x /opt/gh-aw/safe-inputs/ready-to-make-pr.py + GH_AW_SAFE_INPUTS_PY_READY-TO-PUSH-TO-PR_EOF + chmod +x /opt/gh-aw/safe-inputs/ready-to-push-to-pr.py - name: Generate Safe Inputs MCP Server Config id: safe-inputs-config diff --git a/.github/workflows/gh-aw-newbie-contributor-fixer.lock.yml b/.github/workflows/gh-aw-newbie-contributor-fixer.lock.yml index 458fd1db..798abe18 100644 --- a/.github/workflows/gh-aw-newbie-contributor-fixer.lock.yml +++ b/.github/workflows/gh-aw-newbie-contributor-fixer.lock.yml @@ -37,7 +37,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"8448aa6cc23717e296338fa41efae3c393bdfc78463ef7b722694313034024a1"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"c04e88382cf7c1d21fff1cd9355de38fe083e4eda41206a0d4d5b5997ccee6e9"} name: "Newbie Contributor Fixer" "on": @@ -948,6 +948,9 @@ jobs: print(json.dumps({'status': 'error', 'error': 'Unable to determine upstream fork point for merge-commit validation. Fix: ensure remotes are fetched and a tracking branch is set (e.g., `git branch --set-upstream-to origin/`), then rerun ready_to_make_pr.'})) raise SystemExit(0) log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) + if log.returncode != 0: + print(json.dumps({'status': 'error', 'error': f'Failed to check for merge commits (git rev-list exited {log.returncode}): {log.stderr.strip()}. Cannot verify commit history is safe for PR creation.'})) + raise SystemExit(0) merge_shas = log.stdout.strip() if merge_shas: print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) diff --git a/.github/workflows/gh-aw-pr-actions-fixer.lock.yml b/.github/workflows/gh-aw-pr-actions-fixer.lock.yml index 30348457..c38f4bab 100644 --- a/.github/workflows/gh-aw-pr-actions-fixer.lock.yml +++ b/.github/workflows/gh-aw-pr-actions-fixer.lock.yml @@ -37,7 +37,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"e7d11d67614de5d003a7179517eb81006450c07b5dc97782908d03ab9388b5e1"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"067ef5b1714251b55aa9fc1316a2fda34c0f3af4158afe58b21c4637ae7f99c0"} name: "PR Actions Fixer" "on": @@ -287,7 +287,7 @@ jobs: If you exceed 10 mentions or 50 links, the comment will be rejected. GH_AW_PROMPT_EOF cat << 'GH_AW_PROMPT_EOF' - Before calling `push_to_pull_request_branch`, call `ready_to_make_pr` and apply its checklist. + Before calling `push_to_pull_request_branch`, call `ready_to_push_to_pr` and apply its checklist. ## push-to-pull-request-branch Limitations @@ -301,7 +301,7 @@ jobs: 1. Use `git diff HEAD...origin/` (base branch from `/tmp/pr-context/pr.json` field `baseRefName`) to see what the base branch changed in the conflicting files 2. Edit the files directly to incorporate the changes from the base branch 3. Commit the changes as regular (single-parent) commits - 4. Use push_to_pull_request_branch to push + 4. Once you are done with all of your changes on this branch, call `ready_to_push_to_pr` and then `push_to_pull_request_branch` to push GH_AW_PROMPT_EOF cat << 'GH_AW_PROMPT_EOF' @@ -893,13 +893,13 @@ jobs: "logDir": "/opt/gh-aw/safe-inputs/logs", "tools": [ { - "name": "ready-to-make-pr", - "description": "Run the PR readiness checklist before creating or updating a PR", + "name": "ready-to-push-to-pr", + "description": "Run the PR readiness checklist before pushing to a PR", "inputSchema": { "properties": {}, "type": "object" }, - "handler": "ready-to-make-pr.py", + "handler": "ready-to-push-to-pr.py", "timeout": 60 } ] @@ -924,10 +924,10 @@ jobs: - name: Setup Safe Inputs Tool Files run: | - cat > /opt/gh-aw/safe-inputs/ready-to-make-pr.py << 'GH_AW_SAFE_INPUTS_PY_READY-TO-MAKE-PR_EOF' + cat > /opt/gh-aw/safe-inputs/ready-to-push-to-pr.py << 'GH_AW_SAFE_INPUTS_PY_READY-TO-PUSH-TO-PR_EOF' #!/usr/bin/env python3 - # Auto-generated safe-input tool: ready-to-make-pr - # Run the PR readiness checklist before creating or updating a PR + # Auto-generated safe-input tool: ready-to-push-to-pr + # Run the PR readiness checklist before pushing to a PR import json import os @@ -963,6 +963,9 @@ jobs: raise SystemExit(0) # Check 2: no merge commits (multiple parents) since PR head log = run(['git', 'rev-list', '--min-parents=2', f'{pr_head_sha}..HEAD']) + if log.returncode != 0: + print(json.dumps({'status': 'error', 'error': f'Failed to check for merge commits (git rev-list exited {log.returncode}): {log.stderr.strip()}. Cannot verify commit history is safe for push.'})) + raise SystemExit(0) merge_shas = log.stdout.strip() if merge_shas: print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... push_to_pull_request_branch uses git format-patch which breaks on merge commits. Fix: run `git reset --hard {pr_head_sha}` to restore the PR branch, then re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) @@ -1004,12 +1007,12 @@ jobs: if pr_template: checklist.append(f'Follow the PR template ({pr_template}) for title, description, and validation notes.') checklist.append('Confirm the requested task is fully completed and validated before creating or pushing PR changes.') if diff_line_count > 0: - checklist.append(f'A diff of your unpushed changes ({diff_line_count} lines) has been saved to `/tmp/self-review/diff.patch` (full diff) and `/tmp/self-review/stat.txt` (summary). Spawn a `code-review` sub-agent via `runSubagent` to review the diff against the codebase. Tell it to read `/tmp/self-review/diff.patch` and the relevant source files, and look for bugs, logic errors, missed edge cases, and style issues. If the sub-agent finds legitimate issues, fix them, commit, and call `ready_to_make_pr` again to regenerate the diff before proceeding.') + checklist.append(f'A diff of your unpushed changes ({diff_line_count} lines) has been saved to `/tmp/self-review/diff.patch` (full diff) and `/tmp/self-review/stat.txt` (summary). Spawn a `code-review` sub-agent via `runSubagent` to review the diff against the codebase. Tell it to read `/tmp/self-review/diff.patch` and the relevant source files, and look for bugs, logic errors, missed edge cases, and style issues. If the sub-agent finds legitimate issues, fix them, commit, and call `ready_to_push_to_pr` again to regenerate the diff before proceeding.') print(json.dumps({'status': 'ok', 'checklist': checklist, 'contributing_guide': contributing, 'pr_template': pr_template, 'diff_line_count': diff_line_count})) - GH_AW_SAFE_INPUTS_PY_READY-TO-MAKE-PR_EOF - chmod +x /opt/gh-aw/safe-inputs/ready-to-make-pr.py + GH_AW_SAFE_INPUTS_PY_READY-TO-PUSH-TO-PR_EOF + chmod +x /opt/gh-aw/safe-inputs/ready-to-push-to-pr.py - name: Generate Safe Inputs MCP Server Config id: safe-inputs-config diff --git a/.github/workflows/gh-aw-pr-review-addresser.lock.yml b/.github/workflows/gh-aw-pr-review-addresser.lock.yml index ae88c49d..b7454986 100644 --- a/.github/workflows/gh-aw-pr-review-addresser.lock.yml +++ b/.github/workflows/gh-aw-pr-review-addresser.lock.yml @@ -40,7 +40,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"3a80e65ab7a75a65ff8e36d901a631fe39aded592e78ecc532d367a3b6158280"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"a75125e9e8e3ea172d6e4e27fb0b9fd66bd2403dfcb81a93da21c00f7d9aeeae"} name: "PR Review Addresser" "on": @@ -298,7 +298,7 @@ jobs: If you exceed 10 mentions or 50 links, the comment will be rejected. GH_AW_PROMPT_EOF cat << 'GH_AW_PROMPT_EOF' - Before calling `push_to_pull_request_branch`, call `ready_to_make_pr` and apply its checklist. + Before calling `push_to_pull_request_branch`, call `ready_to_push_to_pr` and apply its checklist. ## push-to-pull-request-branch Limitations @@ -312,7 +312,7 @@ jobs: 1. Use `git diff HEAD...origin/` (base branch from `/tmp/pr-context/pr.json` field `baseRefName`) to see what the base branch changed in the conflicting files 2. Edit the files directly to incorporate the changes from the base branch 3. Commit the changes as regular (single-parent) commits - 4. Use push_to_pull_request_branch to push + 4. Once you are done with all of your changes on this branch, call `ready_to_push_to_pr` and then `push_to_pull_request_branch` to push GH_AW_PROMPT_EOF cat << 'GH_AW_PROMPT_EOF' ## resolve-pull-request-review-thread Limitations @@ -1002,13 +1002,13 @@ jobs: "logDir": "/opt/gh-aw/safe-inputs/logs", "tools": [ { - "name": "ready-to-make-pr", - "description": "Run the PR readiness checklist before creating or updating a PR", + "name": "ready-to-push-to-pr", + "description": "Run the PR readiness checklist before pushing to a PR", "inputSchema": { "properties": {}, "type": "object" }, - "handler": "ready-to-make-pr.py", + "handler": "ready-to-push-to-pr.py", "timeout": 60 } ] @@ -1033,10 +1033,10 @@ jobs: - name: Setup Safe Inputs Tool Files run: | - cat > /opt/gh-aw/safe-inputs/ready-to-make-pr.py << 'GH_AW_SAFE_INPUTS_PY_READY-TO-MAKE-PR_EOF' + cat > /opt/gh-aw/safe-inputs/ready-to-push-to-pr.py << 'GH_AW_SAFE_INPUTS_PY_READY-TO-PUSH-TO-PR_EOF' #!/usr/bin/env python3 - # Auto-generated safe-input tool: ready-to-make-pr - # Run the PR readiness checklist before creating or updating a PR + # Auto-generated safe-input tool: ready-to-push-to-pr + # Run the PR readiness checklist before pushing to a PR import json import os @@ -1072,6 +1072,9 @@ jobs: raise SystemExit(0) # Check 2: no merge commits (multiple parents) since PR head log = run(['git', 'rev-list', '--min-parents=2', f'{pr_head_sha}..HEAD']) + if log.returncode != 0: + print(json.dumps({'status': 'error', 'error': f'Failed to check for merge commits (git rev-list exited {log.returncode}): {log.stderr.strip()}. Cannot verify commit history is safe for push.'})) + raise SystemExit(0) merge_shas = log.stdout.strip() if merge_shas: print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... push_to_pull_request_branch uses git format-patch which breaks on merge commits. Fix: run `git reset --hard {pr_head_sha}` to restore the PR branch, then re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) @@ -1113,12 +1116,12 @@ jobs: if pr_template: checklist.append(f'Follow the PR template ({pr_template}) for title, description, and validation notes.') checklist.append('Confirm the requested task is fully completed and validated before creating or pushing PR changes.') if diff_line_count > 0: - checklist.append(f'A diff of your unpushed changes ({diff_line_count} lines) has been saved to `/tmp/self-review/diff.patch` (full diff) and `/tmp/self-review/stat.txt` (summary). Spawn a `code-review` sub-agent via `runSubagent` to review the diff against the codebase. Tell it to read `/tmp/self-review/diff.patch` and the relevant source files, and look for bugs, logic errors, missed edge cases, and style issues. If the sub-agent finds legitimate issues, fix them, commit, and call `ready_to_make_pr` again to regenerate the diff before proceeding.') + checklist.append(f'A diff of your unpushed changes ({diff_line_count} lines) has been saved to `/tmp/self-review/diff.patch` (full diff) and `/tmp/self-review/stat.txt` (summary). Spawn a `code-review` sub-agent via `runSubagent` to review the diff against the codebase. Tell it to read `/tmp/self-review/diff.patch` and the relevant source files, and look for bugs, logic errors, missed edge cases, and style issues. If the sub-agent finds legitimate issues, fix them, commit, and call `ready_to_push_to_pr` again to regenerate the diff before proceeding.') print(json.dumps({'status': 'ok', 'checklist': checklist, 'contributing_guide': contributing, 'pr_template': pr_template, 'diff_line_count': diff_line_count})) - GH_AW_SAFE_INPUTS_PY_READY-TO-MAKE-PR_EOF - chmod +x /opt/gh-aw/safe-inputs/ready-to-make-pr.py + GH_AW_SAFE_INPUTS_PY_READY-TO-PUSH-TO-PR_EOF + chmod +x /opt/gh-aw/safe-inputs/ready-to-push-to-pr.py - name: Generate Safe Inputs MCP Server Config id: safe-inputs-config diff --git a/.github/workflows/gh-aw-release-update.lock.yml b/.github/workflows/gh-aw-release-update.lock.yml index d67c20d8..45eb4ec5 100644 --- a/.github/workflows/gh-aw-release-update.lock.yml +++ b/.github/workflows/gh-aw-release-update.lock.yml @@ -36,7 +36,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"fa74631a4fe4318f4a15809c2d56490e0f1ad6b4e7a52cb864b0db7c281669f3"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"85a99e6d4dafa3f0dc4211e76df3cfe1eb8e942f35d264440861fd9e7f08a70e"} name: "Release Update Check" "on": @@ -916,6 +916,9 @@ jobs: print(json.dumps({'status': 'error', 'error': 'Unable to determine upstream fork point for merge-commit validation. Fix: ensure remotes are fetched and a tracking branch is set (e.g., `git branch --set-upstream-to origin/`), then rerun ready_to_make_pr.'})) raise SystemExit(0) log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) + if log.returncode != 0: + print(json.dumps({'status': 'error', 'error': f'Failed to check for merge commits (git rev-list exited {log.returncode}): {log.stderr.strip()}. Cannot verify commit history is safe for PR creation.'})) + raise SystemExit(0) merge_shas = log.stdout.strip() if merge_shas: print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) diff --git a/.github/workflows/gh-aw-scheduled-fix.lock.yml b/.github/workflows/gh-aw-scheduled-fix.lock.yml index 0b9d50be..7d16b783 100644 --- a/.github/workflows/gh-aw-scheduled-fix.lock.yml +++ b/.github/workflows/gh-aw-scheduled-fix.lock.yml @@ -37,7 +37,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"c8e403a480343146d8075e2ccc857faefa7cd5ac0c08b757e798ed47f4ce1554"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"c9cffce0af80d574d9cb7ee55dd471ab58dd3fdc5529b07ddcb809be0792db61"} name: "Scheduled Fix" "on": @@ -971,6 +971,9 @@ jobs: print(json.dumps({'status': 'error', 'error': 'Unable to determine upstream fork point for merge-commit validation. Fix: ensure remotes are fetched and a tracking branch is set (e.g., `git branch --set-upstream-to origin/`), then rerun ready_to_make_pr.'})) raise SystemExit(0) log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) + if log.returncode != 0: + print(json.dumps({'status': 'error', 'error': f'Failed to check for merge commits (git rev-list exited {log.returncode}): {log.stderr.strip()}. Cannot verify commit history is safe for PR creation.'})) + raise SystemExit(0) merge_shas = log.stdout.strip() if merge_shas: print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) diff --git a/.github/workflows/gh-aw-small-problem-fixer.lock.yml b/.github/workflows/gh-aw-small-problem-fixer.lock.yml index d612314d..5350e29e 100644 --- a/.github/workflows/gh-aw-small-problem-fixer.lock.yml +++ b/.github/workflows/gh-aw-small-problem-fixer.lock.yml @@ -37,7 +37,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"f15637cf54b9d879ea7bd3ee5207c0e7e7143645f779711e33328b5a5d1ac746"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"684652f5d6bff5a991b9a97623abddaaa11931c82bc93e51c417a1d53a487e57"} name: "Small Problem Fixer" "on": @@ -999,6 +999,9 @@ jobs: print(json.dumps({'status': 'error', 'error': 'Unable to determine upstream fork point for merge-commit validation. Fix: ensure remotes are fetched and a tracking branch is set (e.g., `git branch --set-upstream-to origin/`), then rerun ready_to_make_pr.'})) raise SystemExit(0) log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) + if log.returncode != 0: + print(json.dumps({'status': 'error', 'error': f'Failed to check for merge commits (git rev-list exited {log.returncode}): {log.stderr.strip()}. Cannot verify commit history is safe for PR creation.'})) + raise SystemExit(0) merge_shas = log.stdout.strip() if merge_shas: print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) diff --git a/.github/workflows/gh-aw-test-improvement.lock.yml b/.github/workflows/gh-aw-test-improvement.lock.yml index 08929967..ef4b9b15 100644 --- a/.github/workflows/gh-aw-test-improvement.lock.yml +++ b/.github/workflows/gh-aw-test-improvement.lock.yml @@ -41,7 +41,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"720fbeefb1cdfbab657d459a504b39d5bc7bd419d6b6214874f323e9a834afed"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"341673b6d22586c36cdb7edda52391d0620d0de567ee8751cfa21ff206917051"} name: "Test Improver" "on": @@ -959,6 +959,9 @@ jobs: print(json.dumps({'status': 'error', 'error': 'Unable to determine upstream fork point for merge-commit validation. Fix: ensure remotes are fetched and a tracking branch is set (e.g., `git branch --set-upstream-to origin/`), then rerun ready_to_make_pr.'})) raise SystemExit(0) log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) + if log.returncode != 0: + print(json.dumps({'status': 'error', 'error': f'Failed to check for merge commits (git rev-list exited {log.returncode}): {log.stderr.strip()}. Cannot verify commit history is safe for PR creation.'})) + raise SystemExit(0) merge_shas = log.stdout.strip() if merge_shas: print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) diff --git a/.github/workflows/gh-aw-test-improver.lock.yml b/.github/workflows/gh-aw-test-improver.lock.yml index 7700ae13..21a4233d 100644 --- a/.github/workflows/gh-aw-test-improver.lock.yml +++ b/.github/workflows/gh-aw-test-improver.lock.yml @@ -36,7 +36,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"720fbeefb1cdfbab657d459a504b39d5bc7bd419d6b6214874f323e9a834afed"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"341673b6d22586c36cdb7edda52391d0620d0de567ee8751cfa21ff206917051"} name: "Test Improver" "on": @@ -954,6 +954,9 @@ jobs: print(json.dumps({'status': 'error', 'error': 'Unable to determine upstream fork point for merge-commit validation. Fix: ensure remotes are fetched and a tracking branch is set (e.g., `git branch --set-upstream-to origin/`), then rerun ready_to_make_pr.'})) raise SystemExit(0) log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) + if log.returncode != 0: + print(json.dumps({'status': 'error', 'error': f'Failed to check for merge commits (git rev-list exited {log.returncode}): {log.stderr.strip()}. Cannot verify commit history is safe for PR creation.'})) + raise SystemExit(0) merge_shas = log.stdout.strip() if merge_shas: print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) diff --git a/.github/workflows/gh-aw-text-beautifier.lock.yml b/.github/workflows/gh-aw-text-beautifier.lock.yml index bf2b0045..641158d2 100644 --- a/.github/workflows/gh-aw-text-beautifier.lock.yml +++ b/.github/workflows/gh-aw-text-beautifier.lock.yml @@ -38,7 +38,7 @@ # # inlined-imports: true # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"2c23a606f6c9943ca7855f938da18eb419deb3009ec006a572d96c01241d8a83"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"9316e7680e177a5213dbd790b69a0d8cea2738c8f8df76699ce73478b1aaa446"} name: "Text Beautifier" "on": @@ -956,6 +956,9 @@ jobs: print(json.dumps({'status': 'error', 'error': 'Unable to determine upstream fork point for merge-commit validation. Fix: ensure remotes are fetched and a tracking branch is set (e.g., `git branch --set-upstream-to origin/`), then rerun ready_to_make_pr.'})) raise SystemExit(0) log = run(['git', 'rev-list', '--min-parents=2', f'{upstream_sha}..HEAD']) + if log.returncode != 0: + print(json.dumps({'status': 'error', 'error': f'Failed to check for merge commits (git rev-list exited {log.returncode}): {log.stderr.strip()}. Cannot verify commit history is safe for PR creation.'})) + raise SystemExit(0) merge_shas = log.stdout.strip() if merge_shas: print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... create_pull_request uses git format-patch which breaks on merge commits. Fix: re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) diff --git a/tests/test_safe_input_ready_to_make_pr.py b/tests/test_safe_input_ready_to_make_pr.py index db96bd2a..243f8fe3 100644 --- a/tests/test_safe_input_ready_to_make_pr.py +++ b/tests/test_safe_input_ready_to_make_pr.py @@ -32,7 +32,10 @@ def extract_py_block(fragment_path: Path) -> str: parts = text.split("---", 2) assert len(parts) >= 3, f"Expected YAML frontmatter in {fragment_path}" frontmatter = yaml.safe_load(parts[1]) - py_code = frontmatter["safe-inputs"]["ready-to-make-pr"]["py"] + safe_inputs = frontmatter["safe-inputs"] + # The safe-input key varies: ready-to-push-to-pr (push) vs ready-to-make-pr (create) + first_key = next(iter(safe_inputs)) + py_code = safe_inputs[first_key]["py"] assert py_code, f"No py: block found in {fragment_path}" return py_code @@ -117,18 +120,14 @@ def test_create_extract(self): assert "import" in code assert "json.dumps" in code - def test_fragments_share_common_logic(self): - """Both fragments should share the same diff/checklist logic (tail portion).""" + def test_fragments_share_common_structure(self): + """Both fragments should share the same diff/checklist structure.""" push_code = extract_py_block(PUSH_FRAGMENT) create_code = extract_py_block(CREATE_FRAGMENT) - # The push fragment has an extra ancestry check; both share the - # contributing/diff/checklist logic from "contributing = find(...)" onward. - common_marker = "contributing = find(" - assert common_marker in push_code - assert common_marker in create_code - push_tail = push_code[push_code.index(common_marker):] - create_tail = create_code[create_code.index(common_marker):] - assert push_tail == create_tail + # Both should contain the core logic markers + for marker in ["contributing = find(", "diff_line_count", "json.dumps", "self-review"]: + assert marker in push_code, f"Push fragment missing '{marker}'" + assert marker in create_code, f"Create fragment missing '{marker}'" # --------------------------------------------------------------------------- From 8543c9807e9b8f95939157eb0de522f88ecbfbe6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 28 Feb 2026 17:53:46 +0000 Subject: [PATCH 8/8] Add ready-to-push workflow-path guard Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../gh-aw-fragments/safe-output-push-to-pr.md | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 github/workflows/gh-aw-fragments/safe-output-push-to-pr.md diff --git a/github/workflows/gh-aw-fragments/safe-output-push-to-pr.md b/github/workflows/gh-aw-fragments/safe-output-push-to-pr.md new file mode 100644 index 00000000..b02286f7 --- /dev/null +++ b/github/workflows/gh-aw-fragments/safe-output-push-to-pr.md @@ -0,0 +1,109 @@ +--- +safe-inputs: + ready-to-push-to-pr: + description: "Run the PR readiness checklist before pushing to a PR" + py: | + import os, json, subprocess + def find(*paths): + return next((p for p in paths if os.path.isfile(p)), None) + def run(cmd): + try: + return subprocess.run(cmd, capture_output=True, text=True, timeout=60) + except subprocess.TimeoutExpired: + return subprocess.CompletedProcess(cmd, 1, stdout='', stderr='diff timed out') + + # Guard: fail early on prohibited workflow file changes + changed_files = [] + for names_cmd in [ + ['git', 'diff', '--name-only', '--merge-base', '@{upstream}'], + ['git', 'diff', '--name-only', '@{upstream}'], + ['git', 'diff', '--name-only', 'HEAD'], + ]: + names = run(names_cmd) + if names.stdout.strip(): + changed_files = [line.strip() for line in names.stdout.splitlines() if line.strip()] + break + if any(path.startswith('.github/workflows/') for path in changed_files): + print(json.dumps({'status': 'error', 'error': 'Changes under .github/workflows/ detected in your local branch. push_to_pull_request_branch will reject these changes. Fix: move proposed workflow edits to matching paths under `github/workflows/` (without the leading dot), then ask a maintainer to relocate them to `.github/workflows/`.'})) + raise SystemExit(0) + + # Guard: detect history rewrites and merge commits + pr_json_path = '/tmp/pr-context/pr.json' + if os.path.isfile(pr_json_path): + with open(pr_json_path) as f: + pr_data = json.load(f) + pr_head_sha = pr_data.get('headRefOid', '') + if pr_head_sha: + # Check 1: PR head must be an ancestor of HEAD (no rebase/reset) + anc = run(['git', 'merge-base', '--is-ancestor', pr_head_sha, 'HEAD']) + if anc.returncode != 0: + print(json.dumps({'status': 'error', 'error': f'History rewrite detected: the original PR head ({pr_head_sha[:12]}) is not an ancestor of HEAD. This means git rebase, reset, or cherry-pick rewrote history. push_to_pull_request_branch will fail. Fix: run `git reset --hard {pr_head_sha}` to restore the PR branch to its original head, then re-apply your changes as direct file edits and commit as regular commits.'})) + raise SystemExit(0) + # Check 2: no merge commits (multiple parents) since PR head + log = run(['git', 'rev-list', '--min-parents=2', f'{pr_head_sha}..HEAD']) + if log.returncode != 0: + print(json.dumps({'status': 'error', 'error': f'Failed to check for merge commits (git rev-list exited {log.returncode}): {log.stderr.strip()}. Cannot verify commit history is safe for push.'})) + raise SystemExit(0) + merge_shas = log.stdout.strip() + if merge_shas: + print(json.dumps({'status': 'error', 'error': f'Merge commit(s) detected: {merge_shas.splitlines()[0][:12]}... push_to_pull_request_branch uses git format-patch which breaks on merge commits. Fix: run `git reset --hard {pr_head_sha}` to restore the PR branch, then re-apply your changes as direct file edits (no git merge/rebase/commit-tree with multiple -p flags) and commit as regular single-parent commits.'})) + raise SystemExit(0) + + contributing = find('CONTRIBUTING.md', 'CONTRIBUTING.rst', 'docs/CONTRIBUTING.md', 'docs/contributing.md') + pr_template = find('.github/pull_request_template.md', '.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE/pull_request_template.md') + # Generate diff of all local changes vs upstream for self-review + # Try --merge-base (vs common ancestor), fall back to + # @{upstream} 2-dot (vs upstream tip), then HEAD (uncommitted only) + diff_text = '' + for diff_cmd in [ + ['git', 'diff', '--merge-base', '@{upstream}'], + ['git', 'diff', '@{upstream}'], + ['git', 'diff', 'HEAD'], + ]: + result = run(diff_cmd) + if result.stdout.strip(): + diff_text = result.stdout.strip() + break + stat_text = '' + for stat_cmd in [ + ['git', 'diff', '--stat', '--merge-base', '@{upstream}'], + ['git', 'diff', '--stat', '@{upstream}'], + ['git', 'diff', '--stat', 'HEAD'], + ]: + result = run(stat_cmd) + if result.stdout.strip(): + stat_text = result.stdout.strip() + break + os.makedirs('/tmp/self-review', exist_ok=True) + with open('/tmp/self-review/diff.patch', 'w') as f: + f.write(diff_text) + with open('/tmp/self-review/stat.txt', 'w') as f: + f.write(stat_text) + diff_line_count = len(diff_text.splitlines()) + checklist = [] + if contributing: checklist.append(f'Review the contributing guide ({contributing}) before opening or updating a PR.') + if pr_template: checklist.append(f'Follow the PR template ({pr_template}) for title, description, and validation notes.') + checklist.append('Confirm the requested task is fully completed and validated before creating or pushing PR changes.') + if diff_line_count > 0: + checklist.append(f'A diff of your unpushed changes ({diff_line_count} lines) has been saved to `/tmp/self-review/diff.patch` (full diff) and `/tmp/self-review/stat.txt` (summary). Spawn a `code-review` sub-agent via `runSubagent` to review the diff against the codebase. Tell it to read `/tmp/self-review/diff.patch` and the relevant source files, and look for bugs, logic errors, missed edge cases, and style issues. If the sub-agent finds legitimate issues, fix them, commit, and call `ready_to_push_to_pr` again to regenerate the diff before proceeding.') + print(json.dumps({'status': 'ok', 'checklist': checklist, 'contributing_guide': contributing, 'pr_template': pr_template, 'diff_line_count': diff_line_count})) +safe-outputs: + push-to-pull-request-branch: + github-token-for-extra-empty-commit: ${{ secrets.EXTRA_COMMIT_GITHUB_TOKEN }} +--- + +Before calling `push_to_pull_request_branch`, call `ready_to_push_to_pr` and apply its checklist. + +## push-to-pull-request-branch Limitations + +- **Patch size**: Max ~10 MB (10,240 KB). Keep changes focused — very large refactors may exceed this. +- **Fork PRs**: Cannot push to fork PR branches. Check via `pull_request_read` with method `get` whether the PR head repo differs from the base repo. If it's a fork, explain that you cannot push and suggest the author apply changes themselves. +- **Committed changes required**: You must have locally committed changes before calling push. Uncommitted or staged-only changes will fail. +- **Branch**: Pushes to the PR's head branch. The workspace must have the PR branch checked out. +- You may not submit code that modifies files in `.github/workflows/`. Doing so will cause the submission to be rejected. If asked to modify workflow files, propose the change in a copy placed in a `github/` folder (without the leading period) and note in the PR that the file needs to be relocated by someone with workflow write access. + +Trying to resolve merge conflicts? Do NOT create merge commits (commits with multiple parents) — `push_to_pull_request_branch` uses `git format-patch` which breaks on merge commits. This means: no `git merge`, no `git rebase`, no `git commit-tree` with multiple `-p` flags. Instead: +1. Use `git diff HEAD...origin/` (base branch from `/tmp/pr-context/pr.json` field `baseRefName`) to see what the base branch changed in the conflicting files +2. Edit the files directly to incorporate the changes from the base branch +3. Commit the changes as regular (single-parent) commits +4. Once you are done with all of your changes on this branch, call `ready_to_push_to_pr` and then `push_to_pull_request_branch` to push