feat: add OpenCode autonomous review loop prompt, slash command, and runner

This commit is contained in:
John Smith committed 2026-10-17 00:34:41 -04:00
1 parent 18e03e638b
commit 3a02f6fbb4
5 files changed
+624

No files matched your search

+15
View File
@@ -0,0 +1,15 @@
---
description: Review an open PR or issue for quality and OpenCode v2 compatibility
---
You are the maintainer review agent for `Heretek-AI/awesome-opencode`.
Your task is to review the specified PR or issue (or the next item in the backlog queue) against the OpenCode V2 compatibility and quality standards.
Follow the guidelines defined in `prompts/opencode-review-loop.md`:
1. Check if the target repo is public, maintained, and safe (no crypto/spam).
2. Check OpenCode V2 compatibility:
- For plugins: does it use `@opencode/plugin` and `Plugin.define` (V2), or legacy `@opencode-ai/plugin` (V1)?
- For projects/themes/agents/resources: does it work seamlessly with OpenCode V2?
3. Validate YAML schema adherence against `data/schema.json`.
4. Output the structured JSON assessment with verdict (`APPROVE_AND_MERGE`, `NEEDS_V2_UPDATE`, `NEEDS_METADATA_FIX`, `REJECT_ABANDONED`, `REJECT_POLICY`, `ISSUE_CONVERT_TO_PR`, or `ISSUE_CLOSE`).
5. Provide a clear recommendation and draft GitHub review comment.
+116
View File
@@ -0,0 +1,116 @@
# OpenCode Backlog Review Loop Prompt: V2 Compatibility & Quality Assessment
You are an expert OpenCode maintainer and automated backlog review agent for **`Heretek-AI/awesome-opencode`**.
Your mission is to iteratively review open Pull Requests and Issues in the repository, assess whether each submission is **worth adding** to the catalog, and determine whether it is **compatible with OpenCode v2**.
---
## Evaluation Criteria
### 1. OpenCode V2 Compatibility Analysis
OpenCode V2 introduced a centralized background daemon architecture and completely overhauled the extension model. **V1 plugin code does NOT execute in OpenCode V2 without migration.**
Evaluate the target tool's codebase or documentation against these V2 standards:
* **For Plugins (`data/plugins/`):**
* **V2 Modern (Pass / Compatible):**
* Imports from `@opencode/plugin` (NOT legacy `@opencode-ai/plugin`).
* Uses the domain-driven hook architecture with `Plugin.define({ id: "...", async setup(ctx) { ... } })`.
* Utilizes modern `ctx` domain objects (e.g. `ctx.location.directory`, `ctx.storage`).
* Or implements valid dual-support (exporting both V1 server callback and V2 `setup` via `Plugin.define`).
* **V1 Legacy (Needs Migration):**
* Uses `@opencode-ai/plugin` with the legacy callback export: `export const MyPlugin: Plugin = async ({ directory, project, client }) => { ... }`.
* Missing `Plugin.define` or stable string `id`.
* Relies on deprecated V1 CLI-in-process state.
* **For MCP Servers & Resources (`data/resources/`):**
* Conforms to the standard Model Context Protocol (MCP) specification.
* Properly usable via OpenCode V2's native MCP integration (`opencode.json` mcp servers).
* **For Projects (`data/projects/`):**
* Companion apps, TUIs, GUIs, or desktop tools must integrate with OpenCode V2's daemon/API or support modern OpenCode workspaces.
* **For Agents & Skills (`data/agents/`):**
* Subagents, personalities, or prompts structured for OpenCode V2's agent system (`.opencode/` agent/skills configuration).
* **For Themes (`data/themes/`):**
* Valid theme YAML/JSON compatible with OpenCode's terminal color palette.
---
### 2. "Worth Adding" Quality Bar
Even if code is technically compatible, it must meet the quality standards of `awesome-opencode`:
1. **Relevance**: Directly enhances or integrates with the OpenCode ecosystem.
2. **Maintenance & Health**:
* Repository is public, accessible, and not archived.
* Recent commit activity (ideally active within the last 6 months).
* Not abandoned or broken.
3. **Safety & Policy**:
* **STRICT PROHIBITION**: Zero tolerance for cryptocurrency, tokens, Web3 trading bots, or token pumps (per repository governance policy).
* No malicious scripts, credential scrapers, or obfuscated payloads.
4. **Value & Uniqueness**:
* Provides genuine utility or unique capability.
* Not an exact duplicate or low-effort fork of an already listed entry.
5. **Catalog Metadata Conformity**:
* Target path: `data/<category>/<kebab-case>.yaml` (where category is `plugins`, `themes`, `agents`, `projects`, or `resources`).
* Valid YAML conforming to `data/schema.json`:
* `name`: string (1-60 chars)
* `repo`: valid HTTPS repository URL
* `tagline`: punchy summary (max 120 chars)
* `description`: informative overview of functionality
---
## Verdict Classifications
Assign one of the following verdicts to every reviewed item:
| Verdict | When to Use | Action Required |
| :--- | :--- | :--- |
| **`APPROVE_AND_MERGE`** | High quality, active, fully compatible with OpenCode V2, valid YAML. | Recommend merging PR. |
| **`NEEDS_V2_UPDATE`** | Good concept and maintained, but currently implemented with legacy V1 plugin API (`@opencode-ai/plugin`). | Request update from author to migrate to `@opencode/plugin` / `Plugin.define`. |
| **`NEEDS_METADATA_FIX`** | Good project, but YAML has schema errors (e.g. tagline > 120 chars, invalid category, missing fields). | Request author adjust YAML. |
| **`REJECT_ABANDONED`** | Repository deleted, 404, archived, or no commits in > 1 year. | Recommend closing. |
| **`REJECT_POLICY`** | Crypto, spam, duplicate, or unrelated to OpenCode. | Recommend closing with reason. |
| **`ISSUE_CONVERT_TO_PR`** | (For Issues) Listing suggestion is high quality and V2 compatible. | Recommend creating catalog entry YAML. |
| **`ISSUE_CLOSE`** | (For Issues) Bug report meant for upstream core, invalid, or already resolved. | Recommend closing with guidance. |
---
## Review Output Format
For each item reviewed in the loop, you must provide your assessment in this exact JSON block:
```json
{
"item_type": "PR",
"item_number": 486,
"title": "docs: add Quota Board plugin for OpenCode V2",
"target_name": "Quota Board",
"target_repo": "https://github.com/gilvex/opencode-quota-board",
"category": "plugins",
"worth_adding": true,
"worth_adding_rationale": "Active, well-documented plugin providing provider quota monitoring in the OpenCode terminal.",
"opencode_v2_status": "V2_COMPATIBLE",
"opencode_v2_details": "Explicitly targets OpenCode 2.0.28+, uses modern TUI hooks and active provider connections.",
"yaml_valid": true,
"verdict": "APPROVE_AND_MERGE",
"confidence_score": 95,
"summary_reason": "High quality V2-native plugin with active maintenance and clean YAML entry.",
"recommended_comment": "Thank you for this submission! This entry is verified compatible with OpenCode V2 and meets all awesome-opencode catalog standards. Approved for merge."
}
```
---
## Loop Execution Step-by-Step
When executing the loop:
1. **Receive Payload**: Inspect the provided PR diff or Issue metadata, repository URL, and target package details.
2. **Inspect Upstream Project**:
* Check target repo README and `package.json` (if available) for `@opencode/plugin` vs `@opencode-ai/plugin`.
* Check commit dates and repository state.
3. **Validate Schema**: Check the YAML formatting against `data/schema.json`.
4. **Render JSON Verdict**: Output the structured assessment JSON.
5. **Report & Proceed**: Provide a clear, human-readable executive summary followed by the next queued item.
+39
View File
@@ -0,0 +1,39 @@
{
"processed_prs": {
"486": {
"item_type": "PR",
"item_number": 486,
"title": "docs: add Quota Board plugin for OpenCode V2",
"target_name": "Quota Board",
"target_repo": "https://github.com/gilvex/opencode-quota-board",
"category": "plugins",
"worth_adding": true,
"worth_adding_rationale": "Active, well-maintained plugin adding provider quota bars, reset countdowns, and credit balances to the OpenCode V2 terminal via a collapsible sidebar and /quota popup. Genuine utility, no policy violations, distinct from the existing slkiser/opencode-quota entry.",
"opencode_v2_status": "V2_COMPATIBLE",
"opencode_v2_details": "package.json imports @opencode/plugin and @opencode/client (2.0.28), not legacy @opencode-ai/plugin. src/index.ts uses Plugin.define({ id: \"opencode-quota-board.server\", async setup(ctx) }) with modern ctx domain objects (ctx.integration, ctx.provider, ctx.rpc, ctx.event, ctx.options). README states \"Built for OpenCode V2\", tested with 2.0.28, uses active V2 connections, avoids V1 auth.json/cookie scraping.",
"yaml_valid": true,
"verdict": "APPROVE_AND_MERGE",
"confidence_score": 95,
"summary_reason": "High quality V2-native plugin with active maintenance (pushed 2 days ago), clean MIT-licensed repo, and complete schema-conformant metadata. Not a duplicate of the existing opencode-quota entry (different repo/tool).",
"recommended_comment": "Thank you for this submission, @BillyOutlast / @gilvex! This entry is verified compatible with OpenCode V2 (@opencode/plugin, Plugin.define, ctx domain objects) and meets all awesome-opencode catalog standards. Approved for merge. Note: the PR diff did not include the YAML file contents; please ensure data/plugins/opencode-quota-board.yaml is present with name/repo/tagline/description before merge."
},
"46": {
"item_type": "PR",
"item_number": 46,
"title": "[Upstream #130] feat: add Opencode Auto Update plugin",
"target_name": "Opencode Auto Update",
"target_repo": "https://github.com/AnganSamadder/opencode-plugin-auto-update",
"category": "plugins",
"worth_adding": true,
"worth_adding_rationale": "Provides genuine utility: background auto-updates for OpenCode plugins with throttling, lock files, and update history. Repository is public, not archived, and pushed within the last year. Not crypto-related, not malicious, and not a duplicate of any existing catalog entry.",
"opencode_v2_status": "V1_LEGACY_NEEDS_MIGRATION",
"opencode_v2_details": "Source (src/index.ts) uses the legacy V1 callback export (`export default async function (ctx) => {...}` returning `{ name, config }`) with no `Plugin.define`, no stable string `id`, and no `@opencode/plugin` dependency \u2014 package.json declares zero dependencies. Installation docs direct users to the removed V1 `plugin` array in `opencode.json`. This plugin will not execute in OpenCode V2 without migration.",
"yaml_valid": true,
"verdict": "NEEDS_V2_UPDATE",
"confidence_score": 92,
"summary_reason": "Good, maintained plugin concept with valid, schema-conformant YAML, but implemented against the legacy OpenCode V1 plugin API and incompatible with OpenCode V2 until migrated to `@opencode/plugin` / `Plugin.define`.",
"recommended_comment": "Thank you for this submission! The entry is well-formed and the auto-update concept is useful for the OpenCode ecosystem. However, this plugin is built on the legacy OpenCode V1 plugin API (default callback export, no `Plugin.define`/stable `id`, targets the removed `plugin` config array in `opencode.json`) and declares no `@opencode/plugin` dependency, so it will not execute in OpenCode V2 without migration. Please migrate to `@opencode/plugin` with `Plugin.define({ id: \"...\", async setup(ctx) {...} })`, use modern `ctx` domain objects, and update the install instructions to reflect V2 (no `plugin` array in config). Once V2-compatible, this is a strong candidate for approval."
}
},
"processed_issues": {}
}
+8
View File
@@ -0,0 +1,8 @@
# Awesome-OpenCode Backlog Review Summary
Autonomous review powered by OpenCode evaluating OpenCode V2 compatibility and catalog quality.
| Type | # | Title | Target Name | V2 Status | Verdict | Conf | Reason |
| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
| PR | [486](https://github.com/Heretek-AI/awesome-opencode/pull/486) | [Upstream #864] docs: add Quota Boa | Quota Board | `V2_COMPATIBLE` | **`APPROVE_AND_MERGE`** | 95% | High quality V2-native plugin with active maintenance (pushed 2 days ago), clean MIT-licensed repo, and complete schema-conformant metadata. Not a duplicate of the existing opencode-quota entry (different repo/tool). |
| PR | [46](https://github.com/Heretek-AI/awesome-opencode/pull/46) | [Upstream #130] feat: add Opencode | Opencode Auto Update | `V1_LEGACY_NEEDS_MIGRATION` | **`NEEDS_V2_UPDATE`** | 92% | Good, maintained plugin concept with valid, schema-conformant YAML, but implemented against the legacy OpenCode V1 plugin API and incompatible with OpenCode V2 until migrated to `@opencode/plugin` / `Plugin.define`. |
+446
View File
@@ -0,0 +1,446 @@
#!/usr/bin/env python3
"""
Orchestrates an autonomous review loop using OpenCode CLI (opencode run) to assess
open Pull Requests and Issues on Heretek-AI/awesome-opencode for quality and OpenCode V2 compatibility.
"""
import argparse
import json
import os
import re
import subprocess
import sys
import time
import urllib.request
import urllib.error
REPO = "Heretek-AI/awesome-opencode"
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
PROJECT_ROOT = os.path.abspath(os.path.join(SCRIPT_DIR, "../.."))
PROMPT_FILE = os.path.join(PROJECT_ROOT, "prompts/opencode-review-loop.md")
REVIEWS_DIR = os.path.join(PROJECT_ROOT, "reviews")
STATE_FILE = os.path.join(REVIEWS_DIR, "review_state.json")
RESULTS_FILE = os.path.join(REVIEWS_DIR, "review_results.json")
SUMMARY_FILE = os.path.join(REVIEWS_DIR, "summary.md")
def load_prompt():
with open(PROMPT_FILE, "r", encoding="utf-8") as f:
return f.read()
def load_state():
if os.path.exists(STATE_FILE):
try:
with open(STATE_FILE, "r", encoding="utf-8") as f:
return json.load(f)
except Exception:
pass
return {"processed_prs": {}, "processed_issues": {}}
def save_state(state):
os.makedirs(REVIEWS_DIR, exist_ok=True)
with open(STATE_FILE, "w", encoding="utf-8") as f:
json.dump(state, f, indent=2)
def save_results(results):
os.makedirs(REVIEWS_DIR, exist_ok=True)
with open(RESULTS_FILE, "w", encoding="utf-8") as f:
json.dump(results, f, indent=2)
def append_to_summary(item_type, num, title, target_name, verdict, v2_status, confidence, reason):
os.makedirs(REVIEWS_DIR, exist_ok=True)
header_needed = not os.path.exists(SUMMARY_FILE) or os.path.getsize(SUMMARY_FILE) == 0
with open(SUMMARY_FILE, "a", encoding="utf-8") as f:
if header_needed:
f.write("# Awesome-OpenCode Backlog Review Summary\n\n")
f.write("Autonomous review powered by OpenCode evaluating OpenCode V2 compatibility and catalog quality.\n\n")
f.write("| Type | # | Title | Target Name | V2 Status | Verdict | Conf | Reason |\n")
f.write("| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |\n")
f.write(f"| {item_type} | [{num}](https://github.com/{REPO}/{'pull' if item_type == 'PR' else 'issues'}/{num}) | {title[:35]} | {target_name[:25]} | `{v2_status}` | **`{verdict}`** | {confidence}% | {reason} |\n")
def fetch_open_prs():
print("Fetching open PRs from GitHub...")
out = subprocess.check_output([
"gh", "pr", "list",
"-R", REPO,
"--state", "open",
"--limit", "600",
"--json", "number,title,body,author,headRefName,createdAt,url"
], text=True)
prs = json.loads(out)
prs.sort(key=lambda x: x["number"])
return prs
def fetch_open_issues():
print("Fetching open issues from GitHub...")
out = subprocess.check_output([
"gh", "issue", "list",
"-R", REPO,
"--state", "open",
"--limit", "100",
"--json", "number,title,body,author,createdAt,url"
], text=True)
issues = json.loads(out)
issues.sort(key=lambda x: x["number"])
return issues
def inspect_target_repo(repo_url):
"""Gathers factual evidence from target GitHub repo (commit dates, package.json dependencies)."""
facts = {"repo_url": repo_url, "exists": False}
match = re.search(r'github\.com/([^/]+)/([^/#?]+)', repo_url or "")
if not match:
return facts
owner, repo_name = match.group(1), match.group(2).removesuffix(".git")
api_url = f"https://api.github.com/repos/{owner}/{repo_name}"
# 1. Check repo status via gh api
try:
raw = subprocess.check_output(["gh", "api", api_url, "--jq", "{archived, pushed_at, stargazers_count, default_branch, description}"], text=True)
repo_data = json.loads(raw)
facts["exists"] = True
facts["archived"] = repo_data.get("archived", False)
facts["pushed_at"] = repo_data.get("pushed_at")
facts["stars"] = repo_data.get("stargazers_count", 0)
facts["default_branch"] = repo_data.get("default_branch", "main")
facts["description"] = repo_data.get("description", "")
except Exception as e:
facts["error"] = str(e)
return facts
# 2. Check package.json for @opencode/plugin or @opencode-ai/plugin
branch = facts.get("default_branch", "HEAD")
pkg_url = f"https://raw.githubusercontent.com/{owner}/{repo_name}/{branch}/package.json"
try:
req = urllib.request.Request(pkg_url, headers={"User-Agent": "OpenCode-Reviewer"})
with urllib.request.urlopen(req, timeout=5) as resp:
pkg_data = json.loads(resp.read().decode("utf-8"))
deps = {}
deps.update(pkg_data.get("dependencies", {}))
deps.update(pkg_data.get("devDependencies", {}))
deps.update(pkg_data.get("peerDependencies", {}))
opencode_deps = {k: v for k, v in deps.items() if "opencode" in k.lower()}
facts["opencode_dependencies"] = opencode_deps
if "@opencode/plugin" in opencode_deps:
facts["v2_plugin_package_detected"] = True
if "@opencode-ai/plugin" in opencode_deps:
facts["v1_plugin_package_detected"] = True
except Exception:
facts["package_json_found"] = False
return facts
def get_pr_diff_files(pr_number, head_ref):
"""Retrieves diff or file list from git or gh CLI."""
try:
out = subprocess.check_output([
"gh", "pr", "view", str(pr_number),
"-R", REPO,
"--json", "files"
], text=True)
data = json.loads(out)
files = data.get("files", [])
return files
except Exception:
return []
def get_pr_file_content(head_ref, file_path):
"""Fetches the content of a file from the PR branch."""
ref = f"origin/{head_ref}"
try:
out = subprocess.check_output(["git", "show", f"{ref}:{file_path}"], text=True)
return out
except Exception:
# Fallback to local ref
try:
return subprocess.check_output(["git", "show", f"refs/remotes/{head_ref}:{file_path}"], text=True)
except Exception:
return None
def invoke_opencode_review(system_prompt, item_context, model=None):
"""Invokes opencode run with the formatted prompt."""
combined_message = (
f"{system_prompt}\n\n"
f"---\n\n"
f"# ITEM TO REVIEW NOW\n\n"
f"{item_context}\n\n"
f"Please inspect the facts above and provide your complete review and assessment.\n"
f"You MUST include the final assessment in the requested JSON block."
)
cmd = ["opencode", "run"]
if model:
cmd.extend(["-m", model])
cmd.append(combined_message)
try:
out = subprocess.check_output(cmd, text=True, timeout=120)
return out
except subprocess.TimeoutExpired:
return json.dumps({"error": "OpenCode review timed out after 120s"})
except subprocess.CalledProcessError as e:
return f"OpenCode run failed with exit code {e.returncode}: {e.output}"
def parse_json_verdict(raw_output):
"""Extracts JSON block from OpenCode output."""
match = re.search(r'```json\s*(\{.*?\})\s*```', raw_output, re.DOTALL)
if match:
try:
return json.loads(match.group(1))
except Exception:
pass
# Fallback search for bare JSON object
match2 = re.search(r'(\{[\s\S]*"verdict"[\s\S]*\})', raw_output)
if match2:
try:
return json.loads(match2.group(1))
except Exception:
pass
return None
def review_pr(pr, system_prompt, dry_run=False, model=None):
num = pr["number"]
title = pr["title"]
head_ref = pr.get("headRefName", f"upstream-pr/{num}")
print(f"\n==================================================")
print(f"🔍 Reviewing PR #{num}: {title[:60]}")
print(f"==================================================")
files = get_pr_diff_files(num, head_ref)
yaml_files = [f["path"] for f in files if f.get("path", "").startswith("data/") and f.get("path", "").endswith(".yaml")]
yaml_content = None
target_repo_url = None
if yaml_files:
yaml_content = get_pr_file_content(head_ref, yaml_files[0])
if yaml_content:
repo_match = re.search(r'repo:\s*([^\s\n]+)', yaml_content)
if repo_match:
target_repo_url = repo_match.group(1)
# Fallback to searching body if no yaml repo found
if not target_repo_url:
candidates = re.findall(r'https?://github\.com/[a-zA-Z0-9_\-]+/[a-zA-Z0-9_\-]+', pr.get("body", ""))
for c in candidates:
if not any(skip in c for skip in ("awesome-opencode", "Heretek-AI", "sindresorhus", "anomalyco/opencode")):
target_repo_url = c
break
# Inspect target repo facts
facts = {}
if target_repo_url:
print(f" Investigating target repository: {target_repo_url}...")
facts = inspect_target_repo(target_repo_url)
item_context = (
f"### Pull Request Metadata\n"
f"- **PR Number**: #{num}\n"
f"- **Title**: {title}\n"
f"- **Author**: {pr.get('author', {}).get('login', 'unknown')}\n"
f"- **Created At**: {pr.get('createdAt')}\n"
f"- **Changed Files**: {', '.join(f.get('path', '') for f in files)}\n\n"
f"### Submitted YAML Content\n"
f"```yaml\n{yaml_content or '*No YAML file found in PR*'}\n```\n\n"
f"### PR Body / Description\n"
f"{pr.get('body', '')}\n\n"
f"### Verified Target Repository Facts\n"
f"```json\n{json.dumps(facts, indent=2)}\n```"
)
if dry_run:
print(" [DRY-RUN] Item context prepared. OpenCode execution skipped.")
return {
"item_type": "PR",
"item_number": num,
"title": title,
"verdict": "DRY_RUN",
"v2_status": "UNKNOWN",
"confidence_score": 0,
"summary_reason": "Dry-run execution",
"facts": facts
}
print(" Running OpenCode evaluation...")
raw_response = invoke_opencode_review(system_prompt, item_context, model=model)
verdict = parse_json_verdict(raw_response)
if not verdict:
print(" ⚠️ Warning: Could not parse structured JSON from OpenCode output. Storing raw output.")
verdict = {
"item_type": "PR",
"item_number": num,
"title": title,
"verdict": "PARSE_ERROR",
"v2_status": "UNKNOWN",
"confidence_score": 0,
"summary_reason": "Failed to parse structured JSON from model",
"raw_output": raw_response
}
else:
print(f" ✅ Verdict: {verdict.get('verdict')} | V2 Status: {verdict.get('opencode_v2_status')} (Confidence: {verdict.get('confidence_score')}%)")
print(f" 💡 Rationale: {verdict.get('summary_reason')}")
return verdict
def review_issue(issue, system_prompt, dry_run=False, model=None):
num = issue["number"]
title = issue["title"]
print(f"\n==================================================")
print(f"🔍 Reviewing Issue #{num}: {title[:60]}")
print(f"==================================================")
body = issue.get("body", "")
target_repo_url = None
body_match = re.search(r'https?://github\.com/[a-zA-Z0-9_\-]+/[a-zA-Z0-9_\-]+', body)
if body_match:
target_repo_url = body_match.group(0)
facts = {}
if target_repo_url:
print(f" Investigating target repository: {target_repo_url}...")
facts = inspect_target_repo(target_repo_url)
item_context = (
f"### Issue Metadata\n"
f"- **Issue Number**: #{num}\n"
f"- **Title**: {title}\n"
f"- **Author**: {issue.get('author', {}).get('login', 'unknown')}\n"
f"- **Created At**: {issue.get('createdAt')}\n\n"
f"### Issue Body\n"
f"{body}\n\n"
f"### Verified Target Repository Facts\n"
f"```json\n{json.dumps(facts, indent=2)}\n```"
)
if dry_run:
print(" [DRY-RUN] Item context prepared. OpenCode execution skipped.")
return {
"item_type": "ISSUE",
"item_number": num,
"title": title,
"verdict": "DRY_RUN",
"v2_status": "UNKNOWN",
"confidence_score": 0,
"summary_reason": "Dry-run execution",
"facts": facts
}
print(" Running OpenCode evaluation...")
raw_response = invoke_opencode_review(system_prompt, item_context, model=model)
verdict = parse_json_verdict(raw_response)
if not verdict:
print(" ⚠️ Warning: Could not parse structured JSON from OpenCode output.")
verdict = {
"item_type": "ISSUE",
"item_number": num,
"title": title,
"verdict": "PARSE_ERROR",
"v2_status": "UNKNOWN",
"confidence_score": 0,
"summary_reason": "Failed to parse structured JSON from model",
"raw_output": raw_response
}
else:
print(f" ✅ Verdict: {verdict.get('verdict')} | V2 Status: {verdict.get('opencode_v2_status')} (Confidence: {verdict.get('confidence_score')}%)")
print(f" 💡 Rationale: {verdict.get('summary_reason')}")
return verdict
def main():
parser = argparse.ArgumentParser(description="Run OpenCode review loop over open PRs and issues.")
parser.add_argument("--limit", type=int, default=None, help="Maximum number of items to review")
parser.add_argument("--pr", type=int, default=None, help="Review a single PR by number")
parser.add_argument("--issue", type=int, default=None, help="Review a single issue by number")
parser.add_argument("--prs-only", action="store_true", help="Review only PRs")
parser.add_argument("--issues-only", action="store_true", help="Review only issues")
parser.add_argument("--dry-run", action="store_true", help="Collect data without calling OpenCode LLM")
parser.add_argument("--model", type=str, default=None, help="Model override for OpenCode (e.g. deepseek/deepseek-flash)")
parser.add_argument("--reset", action="store_true", help="Reset state file before running")
args = parser.parse_args()
if args.reset and os.path.exists(STATE_FILE):
os.remove(STATE_FILE)
print("Cleared review state.")
state = load_state()
system_prompt = load_prompt()
processed_count = 0
# 1. Single PR Mode
if args.pr:
prs = fetch_open_prs()
target = next((p for p in prs if p["number"] == args.pr), None)
if not target:
sys.exit(f"PR #{args.pr} not found or not open.")
verdict = review_pr(target, system_prompt, dry_run=args.dry_run, model=args.model)
state["processed_prs"][str(args.pr)] = verdict
save_state(state)
append_to_summary("PR", args.pr, target["title"], verdict.get("target_name", ""), verdict.get("verdict", ""), verdict.get("opencode_v2_status", ""), verdict.get("confidence_score", 0), verdict.get("summary_reason", ""))
return
# 2. Single Issue Mode
if args.issue:
issues = fetch_open_issues()
target = next((i for i in issues if i["number"] == args.issue), None)
if not target:
sys.exit(f"Issue #{args.issue} not found or not open.")
verdict = review_issue(target, system_prompt, dry_run=args.dry_run, model=args.model)
state["processed_issues"][str(args.issue)] = verdict
save_state(state)
append_to_summary("ISSUE", args.issue, target["title"], verdict.get("target_name", ""), verdict.get("verdict", ""), verdict.get("opencode_v2_status", ""), verdict.get("confidence_score", 0), verdict.get("summary_reason", ""))
return
# 3. Batch Loop Mode
items_to_process = []
if not args.prs_only and not args.issues_only:
# Interleave or process issues first then PRs
issues = fetch_open_issues()
prs = fetch_open_prs()
for i in issues:
if str(i["number"]) not in state["processed_issues"]:
items_to_process.append(("ISSUE", i))
for p in prs:
if str(p["number"]) not in state["processed_prs"]:
items_to_process.append(("PR", p))
elif args.issues_only:
issues = fetch_open_issues()
for i in issues:
if str(i["number"]) not in state["processed_issues"]:
items_to_process.append(("ISSUE", i))
elif args.prs_only:
prs = fetch_open_prs()
for p in prs:
if str(p["number"]) not in state["processed_prs"]:
items_to_process.append(("PR", p))
print(f"\nUnreviewed backlog items in queue: {len(items_to_process)}")
if args.limit:
items_to_process = items_to_process[:args.limit]
print(f"Limiting to first {len(items_to_process)} items as requested.")
for idx, (kind, item) in enumerate(items_to_process, 1):
num = item["number"]
print(f"\n[{idx}/{len(items_to_process)}] Processing {kind} #{num}...")
if kind == "PR":
verdict = review_pr(item, system_prompt, dry_run=args.dry_run, model=args.model)
state["processed_prs"][str(num)] = verdict
else:
verdict = review_issue(item, system_prompt, dry_run=args.dry_run, model=args.model)
state["processed_issues"][str(num)] = verdict
save_state(state)
append_to_summary(
kind, num, item["title"],
verdict.get("target_name", ""),
verdict.get("verdict", ""),
verdict.get("opencode_v2_status", ""),
verdict.get("confidence_score", 0),
verdict.get("summary_reason", "")
)
processed_count += 1
time.sleep(1.0)
print(f"\n🎉 Review loop batch complete! Processed {processed_count} items.")
print(f"Results recorded in {SUMMARY_FILE} and {STATE_FILE}")
if __name__ == "__main__":
main()