diff --git a/README.md b/README.md index c67f7ca..c3780be 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,8 @@ claude | Plugin | Purpose | |--------|---------| | [**ox**](plugins/ox/) | Base plugin — commit skill, code quality hooks, auto-format and checks | -| [**oxgh**](plugins/oxgh/) | GitHub workflow — PR, issue, triage, review, and merge skills | -| [**oxgl**](plugins/oxgl/) | GitLab workflow — MR, issue, review, and merge skills | +| [**oxgh**](plugins/oxgh/) | GitHub workflow — PR, issue, triage, and merge skills | +| [**oxgl**](plugins/oxgl/) | GitLab workflow — MR, issue, and merge skills | ## Installation @@ -84,7 +84,7 @@ The bootstrap keeps its temporary checkout under: Add `/.codex/` to that repo's `.gitignore`. If you mirror `cc-plugins` to GitLab or want reproducible installs, update each marketplace `source.url` / `source.ref` before committing the template. At runtime, the bootstrap can be overridden with `CODEX_PLUGINS_REPO_URL`, `CODEX_PLUGINS_REPO_REF`, `CODEX_PLUGINS_BOOTSTRAP_DIR`, and `CODEX_PLUGINS`. -With the GitHub template, skills are available in that repo as `$oxgh:open-pr`, `$oxgh:issue`, `$oxgh:triage`, `$oxgh:wait-for-review`, `$oxgh:merge-or-fix`, and `$oxgh:shipit`. With the GitLab template, the equivalent MR-oriented skills are available as `$oxgl:open-mr`, `$oxgl:issue`, `$oxgl:wait-for-review`, `$oxgl:merge-or-fix`, and `$oxgl:shipit`. +With the GitHub template, skills are available in that repo as `$oxgh:open-pr`, `$oxgh:issue`, `$oxgh:triage`, and `$oxgh:shipit`. With the GitLab template, the equivalent MR-oriented skills are available as `$oxgl:open-mr`, `$oxgl:issue`, and `$oxgl:shipit`. For reference, the marketplace file lives at: @@ -106,7 +106,7 @@ make install-codex PLUGINS=oxgh make link-codex PLUGINS=oxgh ``` -Those user-level installed skills are available from any repo as `$oxgh:open-pr`, `$oxgh:issue`, `$oxgh:triage`, `$oxgh:wait-for-review`, `$oxgh:merge-or-fix`, and `$oxgh:shipit`. +Those user-level installed skills are available from any repo as `$oxgh:open-pr`, `$oxgh:issue`, `$oxgh:triage`, and `$oxgh:shipit`. The generated Codex plugin packages under `codex/plugins/` are for Codex plugin marketplace workflows. The repo-local marketplace at `.agents/plugins/marketplace.json` points at those packages when working in this repository. diff --git a/codex/plugins/oxgh/.codex-plugin/plugin.json b/codex/plugins/oxgh/.codex-plugin/plugin.json index 2286a9b..1d5db70 100644 --- a/codex/plugins/oxgh/.codex-plugin/plugin.json +++ b/codex/plugins/oxgh/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "oxgh", - "version": "0.1.5", + "version": "0.1.6", "description": "GitHub workflow — PR, issue, triage, and merge skills using gh CLI", "author": { "name": "Oxidian" diff --git a/codex/plugins/oxgh/skills/merge-or-fix/SKILL.md b/codex/plugins/oxgh/skills/merge-or-fix/SKILL.md deleted file mode 100644 index 59bf1e9..0000000 --- a/codex/plugins/oxgh/skills/merge-or-fix/SKILL.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -name: merge-or-fix -description: Wait for AI code review on a PR — auto-merge if clean, fix issues if not ---- - -## Context - -First, run these commands and review their output: -- Git remote: `git remote get-url origin` - -## Your Task - -1. **Get PR number**: Use `$ARGUMENTS` if provided, otherwise detect from current branch with `gh pr view --json number --jq '.number'` -2. **Checkout PR branch**: Run `gh pr checkout ` to ensure you're on the PR's branch (safe to run even if already on the branch) -3. **Wait for AI review**: Run `python3 $HOME/.codex/plugins/cache/oxidian/oxgh/0.1.5/skills/merge-or-fix/scripts/wait_for_ai_review.py ` (use 45 minute timeout) -4. **Read all PR comments**: Parse the owner/repo from the git remote above, then run `gh api repos/{owner}/{repo}/issues//comments` -5. **Analyze and respond**: - - If there are findings: identify the highest priority review comment only. Investigate the codebase to understand that specific issue, then immediately create an implementation plan to fix it using TDD. Do NOT ask the user whether they want to fix the issue - assume they do. Write the full plan directly. Ignore lower priority comments for now. - - If no findings: auto-merge by running exactly `gh pr merge --auto` (no other flags) diff --git a/codex/plugins/oxgh/skills/merge-or-fix/scripts/wait_for_ai_review.py b/codex/plugins/oxgh/skills/merge-or-fix/scripts/wait_for_ai_review.py deleted file mode 100644 index 9a2a63a..0000000 --- a/codex/plugins/oxgh/skills/merge-or-fix/scripts/wait_for_ai_review.py +++ /dev/null @@ -1,148 +0,0 @@ -#!/usr/bin/env python3 -"""Wait for AI code review comment on a GitHub PR. - -Usage: wait_for_ai_review.py - -Exit codes: - 0 - Review found and complete (outputs review body to stdout) - 1 - Timeout after waiting - 2 - PR not found or gh CLI error - 3 - Review errored -""" - -import json -import subprocess -import sys -import time - -INITIAL_WAIT_S = 30 -POLL_INTERVAL_S = 20 -MAX_WAIT_S = 40 * 60 - - -def log(message: str) -> None: - """Print to stderr so stdout stays clean for the review body.""" - print(message, file=sys.stderr) - - -def run_gh_command(args: list[str]) -> tuple[int, str]: - """Run a gh CLI command and return (exit_code, output).""" - result = subprocess.run( - ["gh", *args], - capture_output=True, - text=True, - ) - return result.returncode, result.stdout.strip() - - -def get_pr_comments(pr_number: str) -> list[dict]: - """Fetch all comments from a PR.""" - code, output = run_gh_command(["pr", "view", pr_number, "--json", "comments"]) - if code != 0: - return [] - try: - data = json.loads(output) - return data.get("comments", []) - except json.JSONDecodeError: - return [] - - -def find_ai_review_comment(comments: list[dict]) -> str | None: - """Find the AI Code Review comment body, if present.""" - for comment in comments: - body = comment.get("body", "") - if body.startswith("## AI Code Review"): - return body - return None - - -def check_review_status(review_body: str) -> tuple[str, str]: - """Check the status of an AI review comment. - - Returns: (status, review_body) - status is one of: "in_progress", "complete", "error", "unknown" - """ - if "Review in progress" in review_body: - return "in_progress", review_body - if review_body.startswith("## AI Code Review\n\nError:"): - return "error", review_body - if ":x: **Review failed**" in review_body: - return "error", review_body - if "Verdict:" in review_body: - return "complete", review_body - return "unknown", review_body - - -def verify_pr_exists(pr_number: str) -> bool: - """Check if the PR exists.""" - code, _ = run_gh_command(["pr", "view", pr_number, "--json", "number"]) - return code == 0 - - -def wait_for_review(pr_number: str) -> int: - """Wait for the AI review to complete on a PR. - - Returns exit code. - """ - if not verify_pr_exists(pr_number): - log(f"Error: PR #{pr_number} not found") - return 2 - - log(f"Waiting for AI code review on PR #{pr_number}...") - log(f"Initial wait: {INITIAL_WAIT_S}s (review needs time to start)") - time.sleep(INITIAL_WAIT_S) - - elapsed = INITIAL_WAIT_S - last_status = "" - - while elapsed < MAX_WAIT_S: - comments = get_pr_comments(pr_number) - review_body = find_ai_review_comment(comments) - - if review_body: - status, body = check_review_status(review_body) - - if status == "in_progress": - if last_status != "in_progress": - log("Review in progress...") - last_status = "in_progress" - elif status == "error": - log("Review completed with error") - print(body) - return 3 - elif status == "complete": - log("Review complete!") - print(body) - return 0 - else: - log("Review found (unknown state)") - print(body) - return 0 - elif last_status != "waiting": - log("No review comment yet, polling...") - last_status = "waiting" - - # Progress update every minute - if elapsed % 60 == 0 and elapsed > INITIAL_WAIT_S: - remaining = MAX_WAIT_S - elapsed - log(f"Still waiting... {remaining}s remaining") - - time.sleep(POLL_INTERVAL_S) - elapsed += POLL_INTERVAL_S - - log(f"Timeout: No complete review after {MAX_WAIT_S}s") - log(f"Check manually: gh pr view {pr_number} --web") - return 1 - - -def main() -> int: - if len(sys.argv) != 2: - log("Usage: wait_for_ai_review.py ") - return 2 - - pr_number = sys.argv[1] - return wait_for_review(pr_number) - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/codex/plugins/oxgh/skills/wait-for-review/SKILL.md b/codex/plugins/oxgh/skills/wait-for-review/SKILL.md deleted file mode 100644 index 25ca5bc..0000000 --- a/codex/plugins/oxgh/skills/wait-for-review/SKILL.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -name: wait-for-review -description: Wait for AI code review on a PR, analyze findings, and offer to address issues ---- - -## Context - -First, run these commands and review their output: -- Git remote: `git remote get-url origin` - -## Your Task - -1. **Get PR number**: Use `$ARGUMENTS` if provided, otherwise detect from current branch with `gh pr view --json number --jq '.number'` -2. **Checkout PR branch**: Run `gh pr checkout ` to ensure you're on the PR's branch (safe to run even if already on the branch) -3. **Wait for AI review**: Run `python3 $HOME/.codex/plugins/cache/oxidian/oxgh/0.1.5/skills/wait-for-review/scripts/wait_for_ai_review.py ` (use 45 minute timeout) -4. **Read all PR comments**: Parse the owner/repo from the git remote above, then run `gh api repos/{owner}/{repo}/issues//comments` -5. **Analyze and respond**: - - If there are findings: identify the highest priority review comment only. Investigate the codebase to understand that specific issue, then immediately create an implementation plan to fix it using TDD. Do NOT ask the user whether they want to fix the issue - assume they do. Write the full plan directly. Ignore lower priority comments for now. - - If no findings: report success diff --git a/codex/plugins/oxgh/skills/wait-for-review/scripts/wait_for_ai_review.py b/codex/plugins/oxgh/skills/wait-for-review/scripts/wait_for_ai_review.py deleted file mode 100644 index 9a2a63a..0000000 --- a/codex/plugins/oxgh/skills/wait-for-review/scripts/wait_for_ai_review.py +++ /dev/null @@ -1,148 +0,0 @@ -#!/usr/bin/env python3 -"""Wait for AI code review comment on a GitHub PR. - -Usage: wait_for_ai_review.py - -Exit codes: - 0 - Review found and complete (outputs review body to stdout) - 1 - Timeout after waiting - 2 - PR not found or gh CLI error - 3 - Review errored -""" - -import json -import subprocess -import sys -import time - -INITIAL_WAIT_S = 30 -POLL_INTERVAL_S = 20 -MAX_WAIT_S = 40 * 60 - - -def log(message: str) -> None: - """Print to stderr so stdout stays clean for the review body.""" - print(message, file=sys.stderr) - - -def run_gh_command(args: list[str]) -> tuple[int, str]: - """Run a gh CLI command and return (exit_code, output).""" - result = subprocess.run( - ["gh", *args], - capture_output=True, - text=True, - ) - return result.returncode, result.stdout.strip() - - -def get_pr_comments(pr_number: str) -> list[dict]: - """Fetch all comments from a PR.""" - code, output = run_gh_command(["pr", "view", pr_number, "--json", "comments"]) - if code != 0: - return [] - try: - data = json.loads(output) - return data.get("comments", []) - except json.JSONDecodeError: - return [] - - -def find_ai_review_comment(comments: list[dict]) -> str | None: - """Find the AI Code Review comment body, if present.""" - for comment in comments: - body = comment.get("body", "") - if body.startswith("## AI Code Review"): - return body - return None - - -def check_review_status(review_body: str) -> tuple[str, str]: - """Check the status of an AI review comment. - - Returns: (status, review_body) - status is one of: "in_progress", "complete", "error", "unknown" - """ - if "Review in progress" in review_body: - return "in_progress", review_body - if review_body.startswith("## AI Code Review\n\nError:"): - return "error", review_body - if ":x: **Review failed**" in review_body: - return "error", review_body - if "Verdict:" in review_body: - return "complete", review_body - return "unknown", review_body - - -def verify_pr_exists(pr_number: str) -> bool: - """Check if the PR exists.""" - code, _ = run_gh_command(["pr", "view", pr_number, "--json", "number"]) - return code == 0 - - -def wait_for_review(pr_number: str) -> int: - """Wait for the AI review to complete on a PR. - - Returns exit code. - """ - if not verify_pr_exists(pr_number): - log(f"Error: PR #{pr_number} not found") - return 2 - - log(f"Waiting for AI code review on PR #{pr_number}...") - log(f"Initial wait: {INITIAL_WAIT_S}s (review needs time to start)") - time.sleep(INITIAL_WAIT_S) - - elapsed = INITIAL_WAIT_S - last_status = "" - - while elapsed < MAX_WAIT_S: - comments = get_pr_comments(pr_number) - review_body = find_ai_review_comment(comments) - - if review_body: - status, body = check_review_status(review_body) - - if status == "in_progress": - if last_status != "in_progress": - log("Review in progress...") - last_status = "in_progress" - elif status == "error": - log("Review completed with error") - print(body) - return 3 - elif status == "complete": - log("Review complete!") - print(body) - return 0 - else: - log("Review found (unknown state)") - print(body) - return 0 - elif last_status != "waiting": - log("No review comment yet, polling...") - last_status = "waiting" - - # Progress update every minute - if elapsed % 60 == 0 and elapsed > INITIAL_WAIT_S: - remaining = MAX_WAIT_S - elapsed - log(f"Still waiting... {remaining}s remaining") - - time.sleep(POLL_INTERVAL_S) - elapsed += POLL_INTERVAL_S - - log(f"Timeout: No complete review after {MAX_WAIT_S}s") - log(f"Check manually: gh pr view {pr_number} --web") - return 1 - - -def main() -> int: - if len(sys.argv) != 2: - log("Usage: wait_for_ai_review.py ") - return 2 - - pr_number = sys.argv[1] - return wait_for_review(pr_number) - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/codex/plugins/oxgl/.codex-plugin/plugin.json b/codex/plugins/oxgl/.codex-plugin/plugin.json index acdb333..a9526d7 100644 --- a/codex/plugins/oxgl/.codex-plugin/plugin.json +++ b/codex/plugins/oxgl/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "oxgl", - "version": "0.1.6", + "version": "0.1.7", "description": "GitLab workflow — MR, issue, and merge skills using glab CLI", "author": { "name": "Oxidian" diff --git a/codex/plugins/oxgl/skills/merge-or-fix/SKILL.md b/codex/plugins/oxgl/skills/merge-or-fix/SKILL.md deleted file mode 100644 index b5cd82e..0000000 --- a/codex/plugins/oxgl/skills/merge-or-fix/SKILL.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -name: merge-or-fix -description: Wait for AI code review on an MR — auto-merge if clean, fix issues if not ---- - -## Context - -First, run these commands and review their output: -- Git remote: `git remote get-url origin` - -## Your Task - -1. **Get MR number**: Use `$ARGUMENTS` if provided, otherwise detect from current branch with `glab mr view --output json | jq '.iid'` -2. **Checkout MR branch**: Run `glab mr checkout ` to ensure you're on the MR's branch (safe to run even if already on the branch) -3. **Wait for AI review**: Run `python3 $HOME/.codex/plugins/cache/oxidian/oxgl/0.1.6/skills/merge-or-fix/scripts/wait_for_ai_review.py ` (use 45 minute timeout) -4. **Read all MR notes**: Parse the project path from the git remote above, then get the project ID with `glab repo view --output json | jq '.id'` and run `glab api "/projects/{project_id}/merge_requests//notes" --jq '[.[] | select(.system == false)]'` -5. **Analyze and respond**: - - If there are findings: identify the highest priority review comment only. Investigate the codebase to understand that specific issue, then immediately create an implementation plan to fix it using TDD. Do NOT ask the user whether they want to fix the issue - assume they do. Write the full plan directly. Ignore lower priority comments for now. - - If no findings: auto-merge by running exactly `glab mr merge --when-pipeline-succeeds` (no other flags) diff --git a/codex/plugins/oxgl/skills/merge-or-fix/scripts/wait_for_ai_review.py b/codex/plugins/oxgl/skills/merge-or-fix/scripts/wait_for_ai_review.py deleted file mode 100644 index 23aa54c..0000000 --- a/codex/plugins/oxgl/skills/merge-or-fix/scripts/wait_for_ai_review.py +++ /dev/null @@ -1,170 +0,0 @@ -#!/usr/bin/env python3 -"""Wait for AI code review comment on a GitLab MR. - -Usage: wait_for_ai_review.py - -Exit codes: - 0 - Review found and complete (outputs review body to stdout) - 1 - Timeout after waiting - 2 - MR not found or glab CLI error - 3 - Review errored -""" - -import json -import subprocess -import sys -import time - -INITIAL_WAIT_S = 30 -POLL_INTERVAL_S = 20 -MAX_WAIT_S = 40 * 60 - - -def log(message: str) -> None: - """Print to stderr so stdout stays clean for the review body.""" - print(message, file=sys.stderr) - - -def run_glab_command(args: list[str]) -> tuple[int, str]: - """Run a glab CLI command and return (exit_code, output).""" - result = subprocess.run( - ["glab", *args], - capture_output=True, - text=True, - ) - return result.returncode, result.stdout.strip() - - -def get_project_id() -> str | None: - """Get the GitLab project ID from the current repo.""" - code, output = run_glab_command(["repo", "view", "--output", "json"]) - if code != 0: - return None - try: - data = json.loads(output) - return str(data.get("id", "")) - except json.JSONDecodeError: - return None - - -def get_mr_notes(project_id: str, mr_iid: str) -> list[dict]: - """Fetch all notes from an MR.""" - code, output = run_glab_command( - [ - "api", - f"/projects/{project_id}/merge_requests/{mr_iid}/notes", - "--paginate", - ] - ) - if code != 0: - return [] - try: - return json.loads(output) - except json.JSONDecodeError: - return [] - - -def find_ai_review_comment(notes: list[dict]) -> str | None: - """Find the AI Code Review comment body, if present.""" - for note in notes: - body = note.get("body", "") - if body.startswith("## AI Code Review"): - return body - return None - - -def check_review_status(review_body: str) -> tuple[str, str]: - """Check the status of an AI review comment. - - Returns: (status, review_body) - status is one of: "in_progress", "complete", "error", "unknown" - """ - if "Review in progress" in review_body: - return "in_progress", review_body - if review_body.startswith("## AI Code Review\n\nError:"): - return "error", review_body - if ":x: **Review failed**" in review_body: - return "error", review_body - if "Verdict:" in review_body: - return "complete", review_body - return "unknown", review_body - - -def verify_mr_exists(mr_iid: str) -> bool: - """Check if the MR exists.""" - code, _ = run_glab_command(["mr", "view", mr_iid, "--output", "json"]) - return code == 0 - - -def wait_for_review(mr_iid: str) -> int: - """Wait for the AI review to complete on an MR. - - Returns exit code. - """ - if not verify_mr_exists(mr_iid): - log(f"Error: MR !{mr_iid} not found") - return 2 - - project_id = get_project_id() - if not project_id: - log("Error: Could not determine project ID") - return 2 - - log(f"Waiting for AI code review on MR !{mr_iid}...") - log(f"Initial wait: {INITIAL_WAIT_S}s (review needs time to start)") - time.sleep(INITIAL_WAIT_S) - - elapsed = INITIAL_WAIT_S - last_status = "" - - while elapsed < MAX_WAIT_S: - notes = get_mr_notes(project_id, mr_iid) - review_body = find_ai_review_comment(notes) - - if review_body: - status, body = check_review_status(review_body) - - if status == "in_progress": - if last_status != "in_progress": - log("Review in progress...") - last_status = "in_progress" - elif status == "error": - log("Review completed with error") - print(body) - return 3 - elif status == "complete": - log("Review complete!") - print(body) - return 0 - else: - log("Review found (unknown state)") - print(body) - return 0 - elif last_status != "waiting": - log("No review comment yet, polling...") - last_status = "waiting" - - # Progress update every minute - if elapsed % 60 == 0 and elapsed > INITIAL_WAIT_S: - remaining = MAX_WAIT_S - elapsed - log(f"Still waiting... {remaining}s remaining") - - time.sleep(POLL_INTERVAL_S) - elapsed += POLL_INTERVAL_S - - log(f"Timeout: No complete review after {MAX_WAIT_S}s") - log(f"Check manually: glab mr view {mr_iid} --web") - return 1 - - -def main() -> int: - if len(sys.argv) != 2: - log("Usage: wait_for_ai_review.py ") - return 2 - - mr_iid = sys.argv[1] - return wait_for_review(mr_iid) - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/codex/plugins/oxgl/skills/wait-for-review/SKILL.md b/codex/plugins/oxgl/skills/wait-for-review/SKILL.md deleted file mode 100644 index 28e9f79..0000000 --- a/codex/plugins/oxgl/skills/wait-for-review/SKILL.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -name: wait-for-review -description: Wait for AI code review on an MR, analyze findings, and offer to address issues ---- - -## Context - -First, run these commands and review their output: -- Git remote: `git remote get-url origin` - -## Your Task - -1. **Get MR number**: Use `$ARGUMENTS` if provided, otherwise detect from current branch with `glab mr view --output json | jq '.iid'` -2. **Checkout MR branch**: Run `glab mr checkout ` to ensure you're on the MR's branch (safe to run even if already on the branch) -3. **Wait for AI review**: Run `python3 $HOME/.codex/plugins/cache/oxidian/oxgl/0.1.6/skills/wait-for-review/scripts/wait_for_ai_review.py ` (use 45 minute timeout) -4. **Read all MR notes**: Parse the project path from the git remote above, then get the project ID with `glab repo view --output json | jq '.id'` and run `glab api "/projects/{project_id}/merge_requests//notes" --jq '[.[] | select(.system == false)]'` -5. **Analyze and respond**: - - If there are findings: identify the highest priority review comment only. Investigate the codebase to understand that specific issue, then immediately create an implementation plan to fix it using TDD. Do NOT ask the user whether they want to fix the issue - assume they do. Write the full plan directly. Ignore lower priority comments for now. - - If no findings: report success diff --git a/codex/plugins/oxgl/skills/wait-for-review/scripts/wait_for_ai_review.py b/codex/plugins/oxgl/skills/wait-for-review/scripts/wait_for_ai_review.py deleted file mode 100644 index 23aa54c..0000000 --- a/codex/plugins/oxgl/skills/wait-for-review/scripts/wait_for_ai_review.py +++ /dev/null @@ -1,170 +0,0 @@ -#!/usr/bin/env python3 -"""Wait for AI code review comment on a GitLab MR. - -Usage: wait_for_ai_review.py - -Exit codes: - 0 - Review found and complete (outputs review body to stdout) - 1 - Timeout after waiting - 2 - MR not found or glab CLI error - 3 - Review errored -""" - -import json -import subprocess -import sys -import time - -INITIAL_WAIT_S = 30 -POLL_INTERVAL_S = 20 -MAX_WAIT_S = 40 * 60 - - -def log(message: str) -> None: - """Print to stderr so stdout stays clean for the review body.""" - print(message, file=sys.stderr) - - -def run_glab_command(args: list[str]) -> tuple[int, str]: - """Run a glab CLI command and return (exit_code, output).""" - result = subprocess.run( - ["glab", *args], - capture_output=True, - text=True, - ) - return result.returncode, result.stdout.strip() - - -def get_project_id() -> str | None: - """Get the GitLab project ID from the current repo.""" - code, output = run_glab_command(["repo", "view", "--output", "json"]) - if code != 0: - return None - try: - data = json.loads(output) - return str(data.get("id", "")) - except json.JSONDecodeError: - return None - - -def get_mr_notes(project_id: str, mr_iid: str) -> list[dict]: - """Fetch all notes from an MR.""" - code, output = run_glab_command( - [ - "api", - f"/projects/{project_id}/merge_requests/{mr_iid}/notes", - "--paginate", - ] - ) - if code != 0: - return [] - try: - return json.loads(output) - except json.JSONDecodeError: - return [] - - -def find_ai_review_comment(notes: list[dict]) -> str | None: - """Find the AI Code Review comment body, if present.""" - for note in notes: - body = note.get("body", "") - if body.startswith("## AI Code Review"): - return body - return None - - -def check_review_status(review_body: str) -> tuple[str, str]: - """Check the status of an AI review comment. - - Returns: (status, review_body) - status is one of: "in_progress", "complete", "error", "unknown" - """ - if "Review in progress" in review_body: - return "in_progress", review_body - if review_body.startswith("## AI Code Review\n\nError:"): - return "error", review_body - if ":x: **Review failed**" in review_body: - return "error", review_body - if "Verdict:" in review_body: - return "complete", review_body - return "unknown", review_body - - -def verify_mr_exists(mr_iid: str) -> bool: - """Check if the MR exists.""" - code, _ = run_glab_command(["mr", "view", mr_iid, "--output", "json"]) - return code == 0 - - -def wait_for_review(mr_iid: str) -> int: - """Wait for the AI review to complete on an MR. - - Returns exit code. - """ - if not verify_mr_exists(mr_iid): - log(f"Error: MR !{mr_iid} not found") - return 2 - - project_id = get_project_id() - if not project_id: - log("Error: Could not determine project ID") - return 2 - - log(f"Waiting for AI code review on MR !{mr_iid}...") - log(f"Initial wait: {INITIAL_WAIT_S}s (review needs time to start)") - time.sleep(INITIAL_WAIT_S) - - elapsed = INITIAL_WAIT_S - last_status = "" - - while elapsed < MAX_WAIT_S: - notes = get_mr_notes(project_id, mr_iid) - review_body = find_ai_review_comment(notes) - - if review_body: - status, body = check_review_status(review_body) - - if status == "in_progress": - if last_status != "in_progress": - log("Review in progress...") - last_status = "in_progress" - elif status == "error": - log("Review completed with error") - print(body) - return 3 - elif status == "complete": - log("Review complete!") - print(body) - return 0 - else: - log("Review found (unknown state)") - print(body) - return 0 - elif last_status != "waiting": - log("No review comment yet, polling...") - last_status = "waiting" - - # Progress update every minute - if elapsed % 60 == 0 and elapsed > INITIAL_WAIT_S: - remaining = MAX_WAIT_S - elapsed - log(f"Still waiting... {remaining}s remaining") - - time.sleep(POLL_INTERVAL_S) - elapsed += POLL_INTERVAL_S - - log(f"Timeout: No complete review after {MAX_WAIT_S}s") - log(f"Check manually: glab mr view {mr_iid} --web") - return 1 - - -def main() -> int: - if len(sys.argv) != 2: - log("Usage: wait_for_ai_review.py ") - return 2 - - mr_iid = sys.argv[1] - return wait_for_review(mr_iid) - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/codex/skills/oxgh/merge-or-fix/SKILL.md b/codex/skills/oxgh/merge-or-fix/SKILL.md deleted file mode 100644 index bc60d17..0000000 --- a/codex/skills/oxgh/merge-or-fix/SKILL.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -name: oxgh:merge-or-fix -description: Wait for AI code review on a PR — auto-merge if clean, fix issues if not ---- - -## Context - -First, run these commands and review their output: -- Git remote: `git remote get-url origin` - -## Your Task - -1. **Get PR number**: Use `$ARGUMENTS` if provided, otherwise detect from current branch with `gh pr view --json number --jq '.number'` -2. **Checkout PR branch**: Run `gh pr checkout ` to ensure you're on the PR's branch (safe to run even if already on the branch) -3. **Wait for AI review**: Run `python3 scripts/wait_for_ai_review.py ` (use 45 minute timeout) -4. **Read all PR comments**: Parse the owner/repo from the git remote above, then run `gh api repos/{owner}/{repo}/issues//comments` -5. **Analyze and respond**: - - If there are findings: identify the highest priority review comment only. Investigate the codebase to understand that specific issue, then immediately create an implementation plan to fix it using TDD. Do NOT ask the user whether they want to fix the issue - assume they do. Write the full plan directly. Ignore lower priority comments for now. - - If no findings: auto-merge by running exactly `gh pr merge --auto` (no other flags) diff --git a/codex/skills/oxgh/merge-or-fix/scripts/wait_for_ai_review.py b/codex/skills/oxgh/merge-or-fix/scripts/wait_for_ai_review.py deleted file mode 100644 index 9a2a63a..0000000 --- a/codex/skills/oxgh/merge-or-fix/scripts/wait_for_ai_review.py +++ /dev/null @@ -1,148 +0,0 @@ -#!/usr/bin/env python3 -"""Wait for AI code review comment on a GitHub PR. - -Usage: wait_for_ai_review.py - -Exit codes: - 0 - Review found and complete (outputs review body to stdout) - 1 - Timeout after waiting - 2 - PR not found or gh CLI error - 3 - Review errored -""" - -import json -import subprocess -import sys -import time - -INITIAL_WAIT_S = 30 -POLL_INTERVAL_S = 20 -MAX_WAIT_S = 40 * 60 - - -def log(message: str) -> None: - """Print to stderr so stdout stays clean for the review body.""" - print(message, file=sys.stderr) - - -def run_gh_command(args: list[str]) -> tuple[int, str]: - """Run a gh CLI command and return (exit_code, output).""" - result = subprocess.run( - ["gh", *args], - capture_output=True, - text=True, - ) - return result.returncode, result.stdout.strip() - - -def get_pr_comments(pr_number: str) -> list[dict]: - """Fetch all comments from a PR.""" - code, output = run_gh_command(["pr", "view", pr_number, "--json", "comments"]) - if code != 0: - return [] - try: - data = json.loads(output) - return data.get("comments", []) - except json.JSONDecodeError: - return [] - - -def find_ai_review_comment(comments: list[dict]) -> str | None: - """Find the AI Code Review comment body, if present.""" - for comment in comments: - body = comment.get("body", "") - if body.startswith("## AI Code Review"): - return body - return None - - -def check_review_status(review_body: str) -> tuple[str, str]: - """Check the status of an AI review comment. - - Returns: (status, review_body) - status is one of: "in_progress", "complete", "error", "unknown" - """ - if "Review in progress" in review_body: - return "in_progress", review_body - if review_body.startswith("## AI Code Review\n\nError:"): - return "error", review_body - if ":x: **Review failed**" in review_body: - return "error", review_body - if "Verdict:" in review_body: - return "complete", review_body - return "unknown", review_body - - -def verify_pr_exists(pr_number: str) -> bool: - """Check if the PR exists.""" - code, _ = run_gh_command(["pr", "view", pr_number, "--json", "number"]) - return code == 0 - - -def wait_for_review(pr_number: str) -> int: - """Wait for the AI review to complete on a PR. - - Returns exit code. - """ - if not verify_pr_exists(pr_number): - log(f"Error: PR #{pr_number} not found") - return 2 - - log(f"Waiting for AI code review on PR #{pr_number}...") - log(f"Initial wait: {INITIAL_WAIT_S}s (review needs time to start)") - time.sleep(INITIAL_WAIT_S) - - elapsed = INITIAL_WAIT_S - last_status = "" - - while elapsed < MAX_WAIT_S: - comments = get_pr_comments(pr_number) - review_body = find_ai_review_comment(comments) - - if review_body: - status, body = check_review_status(review_body) - - if status == "in_progress": - if last_status != "in_progress": - log("Review in progress...") - last_status = "in_progress" - elif status == "error": - log("Review completed with error") - print(body) - return 3 - elif status == "complete": - log("Review complete!") - print(body) - return 0 - else: - log("Review found (unknown state)") - print(body) - return 0 - elif last_status != "waiting": - log("No review comment yet, polling...") - last_status = "waiting" - - # Progress update every minute - if elapsed % 60 == 0 and elapsed > INITIAL_WAIT_S: - remaining = MAX_WAIT_S - elapsed - log(f"Still waiting... {remaining}s remaining") - - time.sleep(POLL_INTERVAL_S) - elapsed += POLL_INTERVAL_S - - log(f"Timeout: No complete review after {MAX_WAIT_S}s") - log(f"Check manually: gh pr view {pr_number} --web") - return 1 - - -def main() -> int: - if len(sys.argv) != 2: - log("Usage: wait_for_ai_review.py ") - return 2 - - pr_number = sys.argv[1] - return wait_for_review(pr_number) - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/codex/skills/oxgh/wait-for-review/SKILL.md b/codex/skills/oxgh/wait-for-review/SKILL.md deleted file mode 100644 index c4f9bd8..0000000 --- a/codex/skills/oxgh/wait-for-review/SKILL.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -name: oxgh:wait-for-review -description: Wait for AI code review on a PR, analyze findings, and offer to address issues ---- - -## Context - -First, run these commands and review their output: -- Git remote: `git remote get-url origin` - -## Your Task - -1. **Get PR number**: Use `$ARGUMENTS` if provided, otherwise detect from current branch with `gh pr view --json number --jq '.number'` -2. **Checkout PR branch**: Run `gh pr checkout ` to ensure you're on the PR's branch (safe to run even if already on the branch) -3. **Wait for AI review**: Run `python3 scripts/wait_for_ai_review.py ` (use 45 minute timeout) -4. **Read all PR comments**: Parse the owner/repo from the git remote above, then run `gh api repos/{owner}/{repo}/issues//comments` -5. **Analyze and respond**: - - If there are findings: identify the highest priority review comment only. Investigate the codebase to understand that specific issue, then immediately create an implementation plan to fix it using TDD. Do NOT ask the user whether they want to fix the issue - assume they do. Write the full plan directly. Ignore lower priority comments for now. - - If no findings: report success diff --git a/codex/skills/oxgh/wait-for-review/scripts/wait_for_ai_review.py b/codex/skills/oxgh/wait-for-review/scripts/wait_for_ai_review.py deleted file mode 100644 index 9a2a63a..0000000 --- a/codex/skills/oxgh/wait-for-review/scripts/wait_for_ai_review.py +++ /dev/null @@ -1,148 +0,0 @@ -#!/usr/bin/env python3 -"""Wait for AI code review comment on a GitHub PR. - -Usage: wait_for_ai_review.py - -Exit codes: - 0 - Review found and complete (outputs review body to stdout) - 1 - Timeout after waiting - 2 - PR not found or gh CLI error - 3 - Review errored -""" - -import json -import subprocess -import sys -import time - -INITIAL_WAIT_S = 30 -POLL_INTERVAL_S = 20 -MAX_WAIT_S = 40 * 60 - - -def log(message: str) -> None: - """Print to stderr so stdout stays clean for the review body.""" - print(message, file=sys.stderr) - - -def run_gh_command(args: list[str]) -> tuple[int, str]: - """Run a gh CLI command and return (exit_code, output).""" - result = subprocess.run( - ["gh", *args], - capture_output=True, - text=True, - ) - return result.returncode, result.stdout.strip() - - -def get_pr_comments(pr_number: str) -> list[dict]: - """Fetch all comments from a PR.""" - code, output = run_gh_command(["pr", "view", pr_number, "--json", "comments"]) - if code != 0: - return [] - try: - data = json.loads(output) - return data.get("comments", []) - except json.JSONDecodeError: - return [] - - -def find_ai_review_comment(comments: list[dict]) -> str | None: - """Find the AI Code Review comment body, if present.""" - for comment in comments: - body = comment.get("body", "") - if body.startswith("## AI Code Review"): - return body - return None - - -def check_review_status(review_body: str) -> tuple[str, str]: - """Check the status of an AI review comment. - - Returns: (status, review_body) - status is one of: "in_progress", "complete", "error", "unknown" - """ - if "Review in progress" in review_body: - return "in_progress", review_body - if review_body.startswith("## AI Code Review\n\nError:"): - return "error", review_body - if ":x: **Review failed**" in review_body: - return "error", review_body - if "Verdict:" in review_body: - return "complete", review_body - return "unknown", review_body - - -def verify_pr_exists(pr_number: str) -> bool: - """Check if the PR exists.""" - code, _ = run_gh_command(["pr", "view", pr_number, "--json", "number"]) - return code == 0 - - -def wait_for_review(pr_number: str) -> int: - """Wait for the AI review to complete on a PR. - - Returns exit code. - """ - if not verify_pr_exists(pr_number): - log(f"Error: PR #{pr_number} not found") - return 2 - - log(f"Waiting for AI code review on PR #{pr_number}...") - log(f"Initial wait: {INITIAL_WAIT_S}s (review needs time to start)") - time.sleep(INITIAL_WAIT_S) - - elapsed = INITIAL_WAIT_S - last_status = "" - - while elapsed < MAX_WAIT_S: - comments = get_pr_comments(pr_number) - review_body = find_ai_review_comment(comments) - - if review_body: - status, body = check_review_status(review_body) - - if status == "in_progress": - if last_status != "in_progress": - log("Review in progress...") - last_status = "in_progress" - elif status == "error": - log("Review completed with error") - print(body) - return 3 - elif status == "complete": - log("Review complete!") - print(body) - return 0 - else: - log("Review found (unknown state)") - print(body) - return 0 - elif last_status != "waiting": - log("No review comment yet, polling...") - last_status = "waiting" - - # Progress update every minute - if elapsed % 60 == 0 and elapsed > INITIAL_WAIT_S: - remaining = MAX_WAIT_S - elapsed - log(f"Still waiting... {remaining}s remaining") - - time.sleep(POLL_INTERVAL_S) - elapsed += POLL_INTERVAL_S - - log(f"Timeout: No complete review after {MAX_WAIT_S}s") - log(f"Check manually: gh pr view {pr_number} --web") - return 1 - - -def main() -> int: - if len(sys.argv) != 2: - log("Usage: wait_for_ai_review.py ") - return 2 - - pr_number = sys.argv[1] - return wait_for_review(pr_number) - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/codex/skills/oxgl/merge-or-fix/SKILL.md b/codex/skills/oxgl/merge-or-fix/SKILL.md deleted file mode 100644 index 3fd5e5b..0000000 --- a/codex/skills/oxgl/merge-or-fix/SKILL.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -name: oxgl:merge-or-fix -description: Wait for AI code review on an MR — auto-merge if clean, fix issues if not ---- - -## Context - -First, run these commands and review their output: -- Git remote: `git remote get-url origin` - -## Your Task - -1. **Get MR number**: Use `$ARGUMENTS` if provided, otherwise detect from current branch with `glab mr view --output json | jq '.iid'` -2. **Checkout MR branch**: Run `glab mr checkout ` to ensure you're on the MR's branch (safe to run even if already on the branch) -3. **Wait for AI review**: Run `python3 scripts/wait_for_ai_review.py ` (use 45 minute timeout) -4. **Read all MR notes**: Parse the project path from the git remote above, then get the project ID with `glab repo view --output json | jq '.id'` and run `glab api "/projects/{project_id}/merge_requests//notes" --jq '[.[] | select(.system == false)]'` -5. **Analyze and respond**: - - If there are findings: identify the highest priority review comment only. Investigate the codebase to understand that specific issue, then immediately create an implementation plan to fix it using TDD. Do NOT ask the user whether they want to fix the issue - assume they do. Write the full plan directly. Ignore lower priority comments for now. - - If no findings: auto-merge by running exactly `glab mr merge --when-pipeline-succeeds` (no other flags) diff --git a/codex/skills/oxgl/merge-or-fix/scripts/wait_for_ai_review.py b/codex/skills/oxgl/merge-or-fix/scripts/wait_for_ai_review.py deleted file mode 100644 index 23aa54c..0000000 --- a/codex/skills/oxgl/merge-or-fix/scripts/wait_for_ai_review.py +++ /dev/null @@ -1,170 +0,0 @@ -#!/usr/bin/env python3 -"""Wait for AI code review comment on a GitLab MR. - -Usage: wait_for_ai_review.py - -Exit codes: - 0 - Review found and complete (outputs review body to stdout) - 1 - Timeout after waiting - 2 - MR not found or glab CLI error - 3 - Review errored -""" - -import json -import subprocess -import sys -import time - -INITIAL_WAIT_S = 30 -POLL_INTERVAL_S = 20 -MAX_WAIT_S = 40 * 60 - - -def log(message: str) -> None: - """Print to stderr so stdout stays clean for the review body.""" - print(message, file=sys.stderr) - - -def run_glab_command(args: list[str]) -> tuple[int, str]: - """Run a glab CLI command and return (exit_code, output).""" - result = subprocess.run( - ["glab", *args], - capture_output=True, - text=True, - ) - return result.returncode, result.stdout.strip() - - -def get_project_id() -> str | None: - """Get the GitLab project ID from the current repo.""" - code, output = run_glab_command(["repo", "view", "--output", "json"]) - if code != 0: - return None - try: - data = json.loads(output) - return str(data.get("id", "")) - except json.JSONDecodeError: - return None - - -def get_mr_notes(project_id: str, mr_iid: str) -> list[dict]: - """Fetch all notes from an MR.""" - code, output = run_glab_command( - [ - "api", - f"/projects/{project_id}/merge_requests/{mr_iid}/notes", - "--paginate", - ] - ) - if code != 0: - return [] - try: - return json.loads(output) - except json.JSONDecodeError: - return [] - - -def find_ai_review_comment(notes: list[dict]) -> str | None: - """Find the AI Code Review comment body, if present.""" - for note in notes: - body = note.get("body", "") - if body.startswith("## AI Code Review"): - return body - return None - - -def check_review_status(review_body: str) -> tuple[str, str]: - """Check the status of an AI review comment. - - Returns: (status, review_body) - status is one of: "in_progress", "complete", "error", "unknown" - """ - if "Review in progress" in review_body: - return "in_progress", review_body - if review_body.startswith("## AI Code Review\n\nError:"): - return "error", review_body - if ":x: **Review failed**" in review_body: - return "error", review_body - if "Verdict:" in review_body: - return "complete", review_body - return "unknown", review_body - - -def verify_mr_exists(mr_iid: str) -> bool: - """Check if the MR exists.""" - code, _ = run_glab_command(["mr", "view", mr_iid, "--output", "json"]) - return code == 0 - - -def wait_for_review(mr_iid: str) -> int: - """Wait for the AI review to complete on an MR. - - Returns exit code. - """ - if not verify_mr_exists(mr_iid): - log(f"Error: MR !{mr_iid} not found") - return 2 - - project_id = get_project_id() - if not project_id: - log("Error: Could not determine project ID") - return 2 - - log(f"Waiting for AI code review on MR !{mr_iid}...") - log(f"Initial wait: {INITIAL_WAIT_S}s (review needs time to start)") - time.sleep(INITIAL_WAIT_S) - - elapsed = INITIAL_WAIT_S - last_status = "" - - while elapsed < MAX_WAIT_S: - notes = get_mr_notes(project_id, mr_iid) - review_body = find_ai_review_comment(notes) - - if review_body: - status, body = check_review_status(review_body) - - if status == "in_progress": - if last_status != "in_progress": - log("Review in progress...") - last_status = "in_progress" - elif status == "error": - log("Review completed with error") - print(body) - return 3 - elif status == "complete": - log("Review complete!") - print(body) - return 0 - else: - log("Review found (unknown state)") - print(body) - return 0 - elif last_status != "waiting": - log("No review comment yet, polling...") - last_status = "waiting" - - # Progress update every minute - if elapsed % 60 == 0 and elapsed > INITIAL_WAIT_S: - remaining = MAX_WAIT_S - elapsed - log(f"Still waiting... {remaining}s remaining") - - time.sleep(POLL_INTERVAL_S) - elapsed += POLL_INTERVAL_S - - log(f"Timeout: No complete review after {MAX_WAIT_S}s") - log(f"Check manually: glab mr view {mr_iid} --web") - return 1 - - -def main() -> int: - if len(sys.argv) != 2: - log("Usage: wait_for_ai_review.py ") - return 2 - - mr_iid = sys.argv[1] - return wait_for_review(mr_iid) - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/codex/skills/oxgl/wait-for-review/SKILL.md b/codex/skills/oxgl/wait-for-review/SKILL.md deleted file mode 100644 index f8571c1..0000000 --- a/codex/skills/oxgl/wait-for-review/SKILL.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -name: oxgl:wait-for-review -description: Wait for AI code review on an MR, analyze findings, and offer to address issues ---- - -## Context - -First, run these commands and review their output: -- Git remote: `git remote get-url origin` - -## Your Task - -1. **Get MR number**: Use `$ARGUMENTS` if provided, otherwise detect from current branch with `glab mr view --output json | jq '.iid'` -2. **Checkout MR branch**: Run `glab mr checkout ` to ensure you're on the MR's branch (safe to run even if already on the branch) -3. **Wait for AI review**: Run `python3 scripts/wait_for_ai_review.py ` (use 45 minute timeout) -4. **Read all MR notes**: Parse the project path from the git remote above, then get the project ID with `glab repo view --output json | jq '.id'` and run `glab api "/projects/{project_id}/merge_requests//notes" --jq '[.[] | select(.system == false)]'` -5. **Analyze and respond**: - - If there are findings: identify the highest priority review comment only. Investigate the codebase to understand that specific issue, then immediately create an implementation plan to fix it using TDD. Do NOT ask the user whether they want to fix the issue - assume they do. Write the full plan directly. Ignore lower priority comments for now. - - If no findings: report success diff --git a/codex/skills/oxgl/wait-for-review/scripts/wait_for_ai_review.py b/codex/skills/oxgl/wait-for-review/scripts/wait_for_ai_review.py deleted file mode 100644 index 23aa54c..0000000 --- a/codex/skills/oxgl/wait-for-review/scripts/wait_for_ai_review.py +++ /dev/null @@ -1,170 +0,0 @@ -#!/usr/bin/env python3 -"""Wait for AI code review comment on a GitLab MR. - -Usage: wait_for_ai_review.py - -Exit codes: - 0 - Review found and complete (outputs review body to stdout) - 1 - Timeout after waiting - 2 - MR not found or glab CLI error - 3 - Review errored -""" - -import json -import subprocess -import sys -import time - -INITIAL_WAIT_S = 30 -POLL_INTERVAL_S = 20 -MAX_WAIT_S = 40 * 60 - - -def log(message: str) -> None: - """Print to stderr so stdout stays clean for the review body.""" - print(message, file=sys.stderr) - - -def run_glab_command(args: list[str]) -> tuple[int, str]: - """Run a glab CLI command and return (exit_code, output).""" - result = subprocess.run( - ["glab", *args], - capture_output=True, - text=True, - ) - return result.returncode, result.stdout.strip() - - -def get_project_id() -> str | None: - """Get the GitLab project ID from the current repo.""" - code, output = run_glab_command(["repo", "view", "--output", "json"]) - if code != 0: - return None - try: - data = json.loads(output) - return str(data.get("id", "")) - except json.JSONDecodeError: - return None - - -def get_mr_notes(project_id: str, mr_iid: str) -> list[dict]: - """Fetch all notes from an MR.""" - code, output = run_glab_command( - [ - "api", - f"/projects/{project_id}/merge_requests/{mr_iid}/notes", - "--paginate", - ] - ) - if code != 0: - return [] - try: - return json.loads(output) - except json.JSONDecodeError: - return [] - - -def find_ai_review_comment(notes: list[dict]) -> str | None: - """Find the AI Code Review comment body, if present.""" - for note in notes: - body = note.get("body", "") - if body.startswith("## AI Code Review"): - return body - return None - - -def check_review_status(review_body: str) -> tuple[str, str]: - """Check the status of an AI review comment. - - Returns: (status, review_body) - status is one of: "in_progress", "complete", "error", "unknown" - """ - if "Review in progress" in review_body: - return "in_progress", review_body - if review_body.startswith("## AI Code Review\n\nError:"): - return "error", review_body - if ":x: **Review failed**" in review_body: - return "error", review_body - if "Verdict:" in review_body: - return "complete", review_body - return "unknown", review_body - - -def verify_mr_exists(mr_iid: str) -> bool: - """Check if the MR exists.""" - code, _ = run_glab_command(["mr", "view", mr_iid, "--output", "json"]) - return code == 0 - - -def wait_for_review(mr_iid: str) -> int: - """Wait for the AI review to complete on an MR. - - Returns exit code. - """ - if not verify_mr_exists(mr_iid): - log(f"Error: MR !{mr_iid} not found") - return 2 - - project_id = get_project_id() - if not project_id: - log("Error: Could not determine project ID") - return 2 - - log(f"Waiting for AI code review on MR !{mr_iid}...") - log(f"Initial wait: {INITIAL_WAIT_S}s (review needs time to start)") - time.sleep(INITIAL_WAIT_S) - - elapsed = INITIAL_WAIT_S - last_status = "" - - while elapsed < MAX_WAIT_S: - notes = get_mr_notes(project_id, mr_iid) - review_body = find_ai_review_comment(notes) - - if review_body: - status, body = check_review_status(review_body) - - if status == "in_progress": - if last_status != "in_progress": - log("Review in progress...") - last_status = "in_progress" - elif status == "error": - log("Review completed with error") - print(body) - return 3 - elif status == "complete": - log("Review complete!") - print(body) - return 0 - else: - log("Review found (unknown state)") - print(body) - return 0 - elif last_status != "waiting": - log("No review comment yet, polling...") - last_status = "waiting" - - # Progress update every minute - if elapsed % 60 == 0 and elapsed > INITIAL_WAIT_S: - remaining = MAX_WAIT_S - elapsed - log(f"Still waiting... {remaining}s remaining") - - time.sleep(POLL_INTERVAL_S) - elapsed += POLL_INTERVAL_S - - log(f"Timeout: No complete review after {MAX_WAIT_S}s") - log(f"Check manually: glab mr view {mr_iid} --web") - return 1 - - -def main() -> int: - if len(sys.argv) != 2: - log("Usage: wait_for_ai_review.py ") - return 2 - - mr_iid = sys.argv[1] - return wait_for_review(mr_iid) - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/plugins/oxgh/.claude-plugin/plugin.json b/plugins/oxgh/.claude-plugin/plugin.json index 14e14fa..2de9ffb 100644 --- a/plugins/oxgh/.claude-plugin/plugin.json +++ b/plugins/oxgh/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "oxgh", "description": "GitHub workflow — PR, issue, triage, and merge skills using gh CLI", - "version": "0.1.5", + "version": "0.1.6", "author": { "name": "Oxidian" } diff --git a/plugins/oxgh/README.md b/plugins/oxgh/README.md index f19a952..09bf062 100644 --- a/plugins/oxgh/README.md +++ b/plugins/oxgh/README.md @@ -7,5 +7,4 @@ GitHub workflow skills using the `gh` CLI. - **`/pr`** — create branch, commit, push, open PR - **`/issue`** — create issues with type, milestone, project fields, and sub-issue linking - **`/triage`** — classify issues by component, priority, size, and type -- **`/wait-for-review`** — wait for AI code review completion and address findings - **`/shipit`** — create PR with auto-merge enabled diff --git a/plugins/oxgh/scripts/wait_for_ai_review.py b/plugins/oxgh/scripts/wait_for_ai_review.py deleted file mode 100644 index 9a2a63a..0000000 --- a/plugins/oxgh/scripts/wait_for_ai_review.py +++ /dev/null @@ -1,148 +0,0 @@ -#!/usr/bin/env python3 -"""Wait for AI code review comment on a GitHub PR. - -Usage: wait_for_ai_review.py - -Exit codes: - 0 - Review found and complete (outputs review body to stdout) - 1 - Timeout after waiting - 2 - PR not found or gh CLI error - 3 - Review errored -""" - -import json -import subprocess -import sys -import time - -INITIAL_WAIT_S = 30 -POLL_INTERVAL_S = 20 -MAX_WAIT_S = 40 * 60 - - -def log(message: str) -> None: - """Print to stderr so stdout stays clean for the review body.""" - print(message, file=sys.stderr) - - -def run_gh_command(args: list[str]) -> tuple[int, str]: - """Run a gh CLI command and return (exit_code, output).""" - result = subprocess.run( - ["gh", *args], - capture_output=True, - text=True, - ) - return result.returncode, result.stdout.strip() - - -def get_pr_comments(pr_number: str) -> list[dict]: - """Fetch all comments from a PR.""" - code, output = run_gh_command(["pr", "view", pr_number, "--json", "comments"]) - if code != 0: - return [] - try: - data = json.loads(output) - return data.get("comments", []) - except json.JSONDecodeError: - return [] - - -def find_ai_review_comment(comments: list[dict]) -> str | None: - """Find the AI Code Review comment body, if present.""" - for comment in comments: - body = comment.get("body", "") - if body.startswith("## AI Code Review"): - return body - return None - - -def check_review_status(review_body: str) -> tuple[str, str]: - """Check the status of an AI review comment. - - Returns: (status, review_body) - status is one of: "in_progress", "complete", "error", "unknown" - """ - if "Review in progress" in review_body: - return "in_progress", review_body - if review_body.startswith("## AI Code Review\n\nError:"): - return "error", review_body - if ":x: **Review failed**" in review_body: - return "error", review_body - if "Verdict:" in review_body: - return "complete", review_body - return "unknown", review_body - - -def verify_pr_exists(pr_number: str) -> bool: - """Check if the PR exists.""" - code, _ = run_gh_command(["pr", "view", pr_number, "--json", "number"]) - return code == 0 - - -def wait_for_review(pr_number: str) -> int: - """Wait for the AI review to complete on a PR. - - Returns exit code. - """ - if not verify_pr_exists(pr_number): - log(f"Error: PR #{pr_number} not found") - return 2 - - log(f"Waiting for AI code review on PR #{pr_number}...") - log(f"Initial wait: {INITIAL_WAIT_S}s (review needs time to start)") - time.sleep(INITIAL_WAIT_S) - - elapsed = INITIAL_WAIT_S - last_status = "" - - while elapsed < MAX_WAIT_S: - comments = get_pr_comments(pr_number) - review_body = find_ai_review_comment(comments) - - if review_body: - status, body = check_review_status(review_body) - - if status == "in_progress": - if last_status != "in_progress": - log("Review in progress...") - last_status = "in_progress" - elif status == "error": - log("Review completed with error") - print(body) - return 3 - elif status == "complete": - log("Review complete!") - print(body) - return 0 - else: - log("Review found (unknown state)") - print(body) - return 0 - elif last_status != "waiting": - log("No review comment yet, polling...") - last_status = "waiting" - - # Progress update every minute - if elapsed % 60 == 0 and elapsed > INITIAL_WAIT_S: - remaining = MAX_WAIT_S - elapsed - log(f"Still waiting... {remaining}s remaining") - - time.sleep(POLL_INTERVAL_S) - elapsed += POLL_INTERVAL_S - - log(f"Timeout: No complete review after {MAX_WAIT_S}s") - log(f"Check manually: gh pr view {pr_number} --web") - return 1 - - -def main() -> int: - if len(sys.argv) != 2: - log("Usage: wait_for_ai_review.py ") - return 2 - - pr_number = sys.argv[1] - return wait_for_review(pr_number) - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/plugins/oxgh/skills/merge-or-fix/SKILL.md b/plugins/oxgh/skills/merge-or-fix/SKILL.md deleted file mode 100644 index 7321156..0000000 --- a/plugins/oxgh/skills/merge-or-fix/SKILL.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -allowed-tools: Bash(python3 ${CLAUDE_PLUGIN_ROOT}/scripts/wait_for_ai_review.py:*), Bash(gh pr view:*), Bash(gh pr checkout:*), Bash(gh pr merge:*), Bash(gh api:*), Bash(git remote get-url origin) -description: Wait for AI code review on a PR — auto-merge if clean, fix issues if not -disable-model-invocation: true ---- - -## Context -- Git remote: !`git remote get-url origin` - -## Your Task - -1. **Get PR number**: Use `$ARGUMENTS` if provided, otherwise detect from current branch with `gh pr view --json number --jq '.number'` -2. **Checkout PR branch**: Run `gh pr checkout ` to ensure you're on the PR's branch (safe to run even if already on the branch) -3. **Wait for AI review**: Run `python3 ${CLAUDE_PLUGIN_ROOT}/scripts/wait_for_ai_review.py ` (use 45 minute timeout) -4. **Read all PR comments**: Parse the owner/repo from the git remote above, then run `gh api repos/{owner}/{repo}/issues//comments` -5. **Analyze and respond**: - - If there are findings: identify the highest priority review comment only. Investigate the codebase to understand that specific issue, then immediately create an implementation plan to fix it using TDD. Do NOT ask the user whether they want to fix the issue - assume they do. Write the full plan directly. Ignore lower priority comments for now. - - If no findings: auto-merge by running exactly `gh pr merge --auto` (no other flags) diff --git a/plugins/oxgh/skills/wait-for-review/SKILL.md b/plugins/oxgh/skills/wait-for-review/SKILL.md deleted file mode 100644 index 7f76d96..0000000 --- a/plugins/oxgh/skills/wait-for-review/SKILL.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -allowed-tools: Bash(python3 ${CLAUDE_PLUGIN_ROOT}/scripts/wait_for_ai_review.py:*), Bash(gh pr view:*), Bash(gh pr checkout:*), Bash(gh api:*), Bash(git remote get-url origin) -description: Wait for AI code review on a PR, analyze findings, and offer to address issues -disable-model-invocation: true ---- - -## Context -- Git remote: !`git remote get-url origin` - -## Your Task - -1. **Get PR number**: Use `$ARGUMENTS` if provided, otherwise detect from current branch with `gh pr view --json number --jq '.number'` -2. **Checkout PR branch**: Run `gh pr checkout ` to ensure you're on the PR's branch (safe to run even if already on the branch) -3. **Wait for AI review**: Run `python3 ${CLAUDE_PLUGIN_ROOT}/scripts/wait_for_ai_review.py ` (use 45 minute timeout) -4. **Read all PR comments**: Parse the owner/repo from the git remote above, then run `gh api repos/{owner}/{repo}/issues//comments` -5. **Analyze and respond**: - - If there are findings: identify the highest priority review comment only. Investigate the codebase to understand that specific issue, then immediately create an implementation plan to fix it using TDD. Do NOT ask the user whether they want to fix the issue - assume they do. Write the full plan directly. Ignore lower priority comments for now. - - If no findings: report success diff --git a/plugins/oxgl/.claude-plugin/plugin.json b/plugins/oxgl/.claude-plugin/plugin.json index 28e5ee6..1c4d744 100644 --- a/plugins/oxgl/.claude-plugin/plugin.json +++ b/plugins/oxgl/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "oxgl", "description": "GitLab workflow — MR, issue, and merge skills using glab CLI", - "version": "0.1.6", + "version": "0.1.7", "author": { "name": "Oxidian" } diff --git a/plugins/oxgl/README.md b/plugins/oxgl/README.md index b1da6da..14b1fcd 100644 --- a/plugins/oxgl/README.md +++ b/plugins/oxgl/README.md @@ -6,5 +6,4 @@ GitLab workflow skills using the `glab` CLI. - **`/open-mr`** — create branch, commit, push, open merge request - **`/issue`** — create issues with type labels, milestone, and linked issues -- **`/wait-for-review`** — wait for AI code review completion and address findings - **`/shipit`** — create MR with auto-merge enabled diff --git a/plugins/oxgl/scripts/wait_for_ai_review.py b/plugins/oxgl/scripts/wait_for_ai_review.py deleted file mode 100644 index 23aa54c..0000000 --- a/plugins/oxgl/scripts/wait_for_ai_review.py +++ /dev/null @@ -1,170 +0,0 @@ -#!/usr/bin/env python3 -"""Wait for AI code review comment on a GitLab MR. - -Usage: wait_for_ai_review.py - -Exit codes: - 0 - Review found and complete (outputs review body to stdout) - 1 - Timeout after waiting - 2 - MR not found or glab CLI error - 3 - Review errored -""" - -import json -import subprocess -import sys -import time - -INITIAL_WAIT_S = 30 -POLL_INTERVAL_S = 20 -MAX_WAIT_S = 40 * 60 - - -def log(message: str) -> None: - """Print to stderr so stdout stays clean for the review body.""" - print(message, file=sys.stderr) - - -def run_glab_command(args: list[str]) -> tuple[int, str]: - """Run a glab CLI command and return (exit_code, output).""" - result = subprocess.run( - ["glab", *args], - capture_output=True, - text=True, - ) - return result.returncode, result.stdout.strip() - - -def get_project_id() -> str | None: - """Get the GitLab project ID from the current repo.""" - code, output = run_glab_command(["repo", "view", "--output", "json"]) - if code != 0: - return None - try: - data = json.loads(output) - return str(data.get("id", "")) - except json.JSONDecodeError: - return None - - -def get_mr_notes(project_id: str, mr_iid: str) -> list[dict]: - """Fetch all notes from an MR.""" - code, output = run_glab_command( - [ - "api", - f"/projects/{project_id}/merge_requests/{mr_iid}/notes", - "--paginate", - ] - ) - if code != 0: - return [] - try: - return json.loads(output) - except json.JSONDecodeError: - return [] - - -def find_ai_review_comment(notes: list[dict]) -> str | None: - """Find the AI Code Review comment body, if present.""" - for note in notes: - body = note.get("body", "") - if body.startswith("## AI Code Review"): - return body - return None - - -def check_review_status(review_body: str) -> tuple[str, str]: - """Check the status of an AI review comment. - - Returns: (status, review_body) - status is one of: "in_progress", "complete", "error", "unknown" - """ - if "Review in progress" in review_body: - return "in_progress", review_body - if review_body.startswith("## AI Code Review\n\nError:"): - return "error", review_body - if ":x: **Review failed**" in review_body: - return "error", review_body - if "Verdict:" in review_body: - return "complete", review_body - return "unknown", review_body - - -def verify_mr_exists(mr_iid: str) -> bool: - """Check if the MR exists.""" - code, _ = run_glab_command(["mr", "view", mr_iid, "--output", "json"]) - return code == 0 - - -def wait_for_review(mr_iid: str) -> int: - """Wait for the AI review to complete on an MR. - - Returns exit code. - """ - if not verify_mr_exists(mr_iid): - log(f"Error: MR !{mr_iid} not found") - return 2 - - project_id = get_project_id() - if not project_id: - log("Error: Could not determine project ID") - return 2 - - log(f"Waiting for AI code review on MR !{mr_iid}...") - log(f"Initial wait: {INITIAL_WAIT_S}s (review needs time to start)") - time.sleep(INITIAL_WAIT_S) - - elapsed = INITIAL_WAIT_S - last_status = "" - - while elapsed < MAX_WAIT_S: - notes = get_mr_notes(project_id, mr_iid) - review_body = find_ai_review_comment(notes) - - if review_body: - status, body = check_review_status(review_body) - - if status == "in_progress": - if last_status != "in_progress": - log("Review in progress...") - last_status = "in_progress" - elif status == "error": - log("Review completed with error") - print(body) - return 3 - elif status == "complete": - log("Review complete!") - print(body) - return 0 - else: - log("Review found (unknown state)") - print(body) - return 0 - elif last_status != "waiting": - log("No review comment yet, polling...") - last_status = "waiting" - - # Progress update every minute - if elapsed % 60 == 0 and elapsed > INITIAL_WAIT_S: - remaining = MAX_WAIT_S - elapsed - log(f"Still waiting... {remaining}s remaining") - - time.sleep(POLL_INTERVAL_S) - elapsed += POLL_INTERVAL_S - - log(f"Timeout: No complete review after {MAX_WAIT_S}s") - log(f"Check manually: glab mr view {mr_iid} --web") - return 1 - - -def main() -> int: - if len(sys.argv) != 2: - log("Usage: wait_for_ai_review.py ") - return 2 - - mr_iid = sys.argv[1] - return wait_for_review(mr_iid) - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/plugins/oxgl/skills/merge-or-fix/SKILL.md b/plugins/oxgl/skills/merge-or-fix/SKILL.md deleted file mode 100644 index 0cdb1d3..0000000 --- a/plugins/oxgl/skills/merge-or-fix/SKILL.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -allowed-tools: Bash(python3 ${CLAUDE_PLUGIN_ROOT}/scripts/wait_for_ai_review.py:*), Bash(glab mr view:*), Bash(glab mr checkout:*), Bash(glab mr merge:*), Bash(glab api:*), Bash(glab repo view:*), Bash(git remote get-url origin) -description: Wait for AI code review on an MR — auto-merge if clean, fix issues if not -disable-model-invocation: true ---- - -## Context -- Git remote: !`git remote get-url origin` - -## Your Task - -1. **Get MR number**: Use `$ARGUMENTS` if provided, otherwise detect from current branch with `glab mr view --output json | jq '.iid'` -2. **Checkout MR branch**: Run `glab mr checkout ` to ensure you're on the MR's branch (safe to run even if already on the branch) -3. **Wait for AI review**: Run `python3 ${CLAUDE_PLUGIN_ROOT}/scripts/wait_for_ai_review.py ` (use 45 minute timeout) -4. **Read all MR notes**: Parse the project path from the git remote above, then get the project ID with `glab repo view --output json | jq '.id'` and run `glab api "/projects/{project_id}/merge_requests//notes" --jq '[.[] | select(.system == false)]'` -5. **Analyze and respond**: - - If there are findings: identify the highest priority review comment only. Investigate the codebase to understand that specific issue, then immediately create an implementation plan to fix it using TDD. Do NOT ask the user whether they want to fix the issue - assume they do. Write the full plan directly. Ignore lower priority comments for now. - - If no findings: auto-merge by running exactly `glab mr merge --when-pipeline-succeeds` (no other flags) diff --git a/plugins/oxgl/skills/wait-for-review/SKILL.md b/plugins/oxgl/skills/wait-for-review/SKILL.md deleted file mode 100644 index 9f91a6e..0000000 --- a/plugins/oxgl/skills/wait-for-review/SKILL.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -allowed-tools: Bash(python3 ${CLAUDE_PLUGIN_ROOT}/scripts/wait_for_ai_review.py:*), Bash(glab mr view:*), Bash(glab mr checkout:*), Bash(glab api:*), Bash(glab repo view:*), Bash(git remote get-url origin) -description: Wait for AI code review on an MR, analyze findings, and offer to address issues -disable-model-invocation: true ---- - -## Context -- Git remote: !`git remote get-url origin` - -## Your Task - -1. **Get MR number**: Use `$ARGUMENTS` if provided, otherwise detect from current branch with `glab mr view --output json | jq '.iid'` -2. **Checkout MR branch**: Run `glab mr checkout ` to ensure you're on the MR's branch (safe to run even if already on the branch) -3. **Wait for AI review**: Run `python3 ${CLAUDE_PLUGIN_ROOT}/scripts/wait_for_ai_review.py ` (use 45 minute timeout) -4. **Read all MR notes**: Parse the project path from the git remote above, then get the project ID with `glab repo view --output json | jq '.id'` and run `glab api "/projects/{project_id}/merge_requests//notes" --jq '[.[] | select(.system == false)]'` -5. **Analyze and respond**: - - If there are findings: identify the highest priority review comment only. Investigate the codebase to understand that specific issue, then immediately create an implementation plan to fix it using TDD. Do NOT ask the user whether they want to fix the issue - assume they do. Write the full plan directly. Ignore lower priority comments for now. - - If no findings: report success diff --git a/tests/test_generate_codex.py b/tests/test_generate_codex.py index 1f493dc..f1e3263 100644 --- a/tests/test_generate_codex.py +++ b/tests/test_generate_codex.py @@ -139,10 +139,10 @@ def test_no_preamble_without_context_heading(self) -> None: class TestTransformPluginRootRefs: def test_replaces_plugin_root(self) -> None: - body = "Run `python3 ${CLAUDE_PLUGIN_ROOT}/scripts/wait_for_ai_review.py`" + body = "Run `python3 ${CLAUDE_PLUGIN_ROOT}/scripts/helper.py`" result = transform_plugin_root_refs(body) assert "${CLAUDE_PLUGIN_ROOT}" not in result - assert "scripts/wait_for_ai_review.py" in result + assert "scripts/helper.py" in result def test_no_change_without_ref(self) -> None: body = "Just some text" @@ -256,25 +256,25 @@ def test_copies_script_deps(self, tmp_path: Path) -> None: plugin_dir = tmp_path / "src" scripts_dir = plugin_dir / "scripts" scripts_dir.mkdir(parents=True) - (scripts_dir / "wait_for_ai_review.py").write_text("# review script") + (scripts_dir / "helper.py").write_text("# helper script") - skill_dir = plugin_dir / "skills" / "wait-for-review" + skill_dir = plugin_dir / "skills" / "scripted-skill" skill_dir.mkdir(parents=True) (skill_dir / "SKILL.md").write_text( "---\n" - "allowed-tools: Bash(python3 ${CLAUDE_PLUGIN_ROOT}/scripts/wait_for_ai_review.py:*)\n" - "description: Wait for review\n" + "allowed-tools: Bash(python3 ${CLAUDE_PLUGIN_ROOT}/scripts/helper.py:*)\n" + "description: Scripted skill\n" "---\n" "\n" - "Run `python3 ${CLAUDE_PLUGIN_ROOT}/scripts/wait_for_ai_review.py 42`\n" + "Run `python3 ${CLAUDE_PLUGIN_ROOT}/scripts/helper.py 42`\n" ) output_dir = tmp_path / "out" process_skill("oxgh", skill_dir, output_dir) - out_script = output_dir / "oxgh" / "wait-for-review" / "scripts" / "wait_for_ai_review.py" + out_script = output_dir / "oxgh" / "scripted-skill" / "scripts" / "helper.py" assert out_script.exists() - assert out_script.read_text() == "# review script" + assert out_script.read_text() == "# helper script" def test_can_write_plugin_local_skill(self, tmp_path: Path) -> None: skill_dir = tmp_path / "src" / "skills" / "open-pr" @@ -403,11 +403,11 @@ def test_no_tokens_unchanged(self) -> None: class TestStampSkillScriptPaths: def test_rewrites_relative_script_ref(self) -> None: - content = "Run `python3 scripts/wait.py 42`" - result = stamp_skill_script_paths(content, "oxidian", "oxgh", "0.1.4", "wait-for-review") + content = "Run `python3 scripts/helper.py 42`" + result = stamp_skill_script_paths(content, "oxidian", "oxgh", "0.1.4", "scripted-skill") assert ( result - == "Run `python3 $HOME/.codex/plugins/cache/oxidian/oxgh/0.1.4/skills/wait-for-review/scripts/wait.py 42`" + == "Run `python3 $HOME/.codex/plugins/cache/oxidian/oxgh/0.1.4/skills/scripted-skill/scripts/helper.py 42`" ) def test_rewrites_multiple_scripts(self) -> None: @@ -473,14 +473,14 @@ def test_skill_scripts_stamped_with_cache_path(self, tmp_path: Path, monkeypatch plugins_dir = repo_root / "plugins" plugin_dir = self._setup_plugin(plugins_dir, "oxgh", "7.0.1") - skill_dir = plugin_dir / "skills" / "wait-for-review" + skill_dir = plugin_dir / "skills" / "scripted-skill" skill_dir.mkdir(parents=True) (skill_dir / "SKILL.md").write_text( - "---\nallowed-tools: Bash(python3 ${CLAUDE_PLUGIN_ROOT}/scripts/wait.py:*)\n" - "description: Wait\n---\n\nRun `python3 ${CLAUDE_PLUGIN_ROOT}/scripts/wait.py 42`.\n" + "---\nallowed-tools: Bash(python3 ${CLAUDE_PLUGIN_ROOT}/scripts/helper.py:*)\n" + "description: Scripted skill\n---\n\nRun `python3 ${CLAUDE_PLUGIN_ROOT}/scripts/helper.py 42`.\n" ) (plugin_dir / "scripts").mkdir() - (plugin_dir / "scripts" / "wait.py").write_text("# wait\n") + (plugin_dir / "scripts" / "helper.py").write_text("# helper\n") monkeypatch.setattr(generate_codex, "PLUGINS_DIR", plugins_dir) monkeypatch.setattr(generate_codex, "CLAUDE_MARKETPLACE", repo_root / ".claude-plugin" / "marketplace.json") @@ -488,15 +488,15 @@ def test_skill_scripts_stamped_with_cache_path(self, tmp_path: Path, monkeypatch output_dir = tmp_path / "codex" / "plugins" generate_plugin_package("oxgh", output_dir) - skill = (output_dir / "oxgh" / "skills" / "wait-for-review" / "SKILL.md").read_text() - assert "$HOME/.codex/plugins/cache/oxidian/oxgh/7.0.1/skills/wait-for-review/scripts/wait.py" in skill + skill = (output_dir / "oxgh" / "skills" / "scripted-skill" / "SKILL.md").read_text() + assert "$HOME/.codex/plugins/cache/oxidian/oxgh/7.0.1/skills/scripted-skill/scripts/helper.py" in skill # Bare relative form should be absent now - assert "scripts/wait.py" in skill # appears inside the absolute path - assert skill.count("scripts/wait.py") == skill.count( - "$HOME/.codex/plugins/cache/oxidian/oxgh/7.0.1/skills/wait-for-review/scripts/wait.py" + assert "scripts/helper.py" in skill # appears inside the absolute path + assert skill.count("scripts/helper.py") == skill.count( + "$HOME/.codex/plugins/cache/oxidian/oxgh/7.0.1/skills/scripted-skill/scripts/helper.py" ) # Scripts file is still copied so the cache install layout is correct - assert (output_dir / "oxgh" / "skills" / "wait-for-review" / "scripts" / "wait.py").exists() + assert (output_dir / "oxgh" / "skills" / "scripted-skill" / "scripts" / "helper.py").exists() def test_namespaced_skills_remain_relative(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: # The standalone codex/skills///SKILL.md output (used by @@ -508,13 +508,13 @@ def test_namespaced_skills_remain_relative(self, tmp_path: Path, monkeypatch: py plugins_dir = repo_root / "plugins" plugin_dir = self._setup_plugin(plugins_dir, "oxgh", "7.0.1") - skill_dir = plugin_dir / "skills" / "wait-for-review" + skill_dir = plugin_dir / "skills" / "scripted-skill" skill_dir.mkdir(parents=True) (skill_dir / "SKILL.md").write_text( - "---\ndescription: Wait\n---\n\nRun `python3 ${CLAUDE_PLUGIN_ROOT}/scripts/wait.py 42`.\n" + "---\ndescription: Scripted skill\n---\n\nRun `python3 ${CLAUDE_PLUGIN_ROOT}/scripts/helper.py 42`.\n" ) (plugin_dir / "scripts").mkdir() - (plugin_dir / "scripts" / "wait.py").write_text("# wait\n") + (plugin_dir / "scripts" / "helper.py").write_text("# helper\n") monkeypatch.setattr(generate_codex, "PLUGINS_DIR", plugins_dir) monkeypatch.setattr(generate_codex, "CLAUDE_MARKETPLACE", repo_root / ".claude-plugin" / "marketplace.json") @@ -522,8 +522,8 @@ def test_namespaced_skills_remain_relative(self, tmp_path: Path, monkeypatch: py out_skills = tmp_path / "codex" / "skills" process_skill("oxgh", skill_dir, out_skills) - skill = (out_skills / "oxgh" / "wait-for-review" / "SKILL.md").read_text() - assert "python3 scripts/wait.py 42" in skill + skill = (out_skills / "oxgh" / "scripted-skill" / "SKILL.md").read_text() + assert "python3 scripts/helper.py 42" in skill assert "$HOME/.codex/plugins/cache" not in skill @@ -655,20 +655,6 @@ def test_commit_skill(self, tmp_path: Path) -> None: assert "`git status --porcelain`" in result assert "Execute each step as a separate command." in result - def test_wait_for_review_skill(self, tmp_path: Path) -> None: - skill_dir = PLUGINS_DIR / "oxgh" / "skills" / "wait-for-review" - process_skill("oxgh", skill_dir, tmp_path) - - result = (tmp_path / "oxgh" / "wait-for-review" / "SKILL.md").read_text() - assert "name: oxgh:wait-for-review" in result - assert "${CLAUDE_PLUGIN_ROOT}" not in result - assert "scripts/wait_for_ai_review.py" in result - assert "disable-model-invocation" not in result - - # Script should be copied - script = tmp_path / "oxgh" / "wait-for-review" / "scripts" / "wait_for_ai_review.py" - assert script.exists() - def test_shipit_skill(self, tmp_path: Path) -> None: skill_dir = PLUGINS_DIR / "oxgh" / "skills" / "shipit" process_skill("oxgh", skill_dir, tmp_path) @@ -697,10 +683,10 @@ def test_triage_skill(self, tmp_path: Path) -> None: class TestResolveScriptPaths: def test_replaces_relative_with_absolute(self) -> None: - content = "Run `python3 scripts/wait_for_ai_review.py 42`" + content = "Run `python3 scripts/helper.py 42`" scripts_dir = Path("/opt/plugins/oxgh/scripts") result = resolve_script_paths(content, scripts_dir) - assert result == "Run `python3 /opt/plugins/oxgh/scripts/wait_for_ai_review.py 42`" + assert result == "Run `python3 /opt/plugins/oxgh/scripts/helper.py 42`" def test_replaces_multiple_scripts(self) -> None: content = "Run `scripts/a.py` then `scripts/b.py`" @@ -727,12 +713,12 @@ def test_resolves_script_paths(self, tmp_path: Path, monkeypatch: pytest.MonkeyP self._make_codex_skill( codex_dir, "oxgh", - "wait-for-review", - "---\nname: oxgh:wait-for-review\n---\nRun `python3 scripts/wait.py 42`\n", + "scripted-skill", + "---\nname: oxgh:scripted-skill\n---\nRun `python3 scripts/helper.py 42`\n", ) - scripts_dir = codex_dir / "oxgh" / "wait-for-review" / "scripts" + scripts_dir = codex_dir / "oxgh" / "scripted-skill" / "scripts" scripts_dir.mkdir() - (scripts_dir / "wait.py").write_text("# script") + (scripts_dir / "helper.py").write_text("# script") monkeypatch.setattr(generate_codex, "OUTPUT_DIR", codex_dir) @@ -740,11 +726,11 @@ def test_resolves_script_paths(self, tmp_path: Path, monkeypatch: pytest.MonkeyP dest.mkdir() install(dest, ["oxgh"]) - installed = (dest / "oxgh:wait-for-review" / "SKILL.md").read_text() - abs_path = str(dest / "oxgh:wait-for-review" / "scripts" / "wait.py") + installed = (dest / "oxgh:scripted-skill" / "SKILL.md").read_text() + abs_path = str(dest / "oxgh:scripted-skill" / "scripts" / "helper.py") assert abs_path in installed # No bare relative reference (every occurrence should be absolute) - assert installed.count("scripts/wait.py") == installed.count(abs_path) + assert installed.count("scripts/helper.py") == installed.count(abs_path) def test_no_resolution_without_scripts(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: codex_dir = tmp_path / "codex" @@ -771,12 +757,12 @@ def test_resolves_script_paths(self, tmp_path: Path, monkeypatch: pytest.MonkeyP self._make_codex_skill( codex_dir, "oxgh", - "wait-for-review", - "---\nname: oxgh:wait-for-review\n---\nRun `python3 scripts/wait.py 42`\n", + "scripted-skill", + "---\nname: oxgh:scripted-skill\n---\nRun `python3 scripts/helper.py 42`\n", ) - scripts_dir = codex_dir / "oxgh" / "wait-for-review" / "scripts" + scripts_dir = codex_dir / "oxgh" / "scripted-skill" / "scripts" scripts_dir.mkdir() - (scripts_dir / "wait.py").write_text("# script") + (scripts_dir / "helper.py").write_text("# script") monkeypatch.setattr(generate_codex, "OUTPUT_DIR", codex_dir) @@ -784,16 +770,16 @@ def test_resolves_script_paths(self, tmp_path: Path, monkeypatch: pytest.MonkeyP dest.mkdir() link(dest, ["oxgh"]) - target = dest / "oxgh:wait-for-review" + target = dest / "oxgh:scripted-skill" # Should be a real directory (not a symlink to the whole skill dir) assert target.is_dir() assert not target.is_symlink() # SKILL.md should have absolute paths linked = (target / "SKILL.md").read_text() - abs_path = str(codex_dir / "oxgh" / "wait-for-review" / "scripts" / "wait.py") + abs_path = str(codex_dir / "oxgh" / "scripted-skill" / "scripts" / "helper.py") assert abs_path in linked - assert linked.count("scripts/wait.py") == linked.count(abs_path) + assert linked.count("scripts/helper.py") == linked.count(abs_path) # scripts/ should be a symlink to the source scripts dir linked_scripts = target / "scripts"