From 374e31772b62d531c30c5aa1cb59d25b74aa3301 Mon Sep 17 00:00:00 2001 From: tonythethompson Date: Sun, 16 Aug 2026 06:41:11 -0700 Subject: [PATCH 1/9] Switch review automation to OpenCode Replaced the legacy Autopilot and Cline PR automation with a new OpenCode GitHub workflow. This adds an issue-comment-triggered agent flow with model probing, retry/salvage logic, and PR review/fix command documentation for /oc and /opencode usage. --- .github/workflows/autopilot.yml | 388 -------------------------- .github/workflows/cline-pr-review.yml | 170 ----------- .github/workflows/opencode.yml | 215 ++++++++++++++ .opencode/github-commands.md | 204 ++++++++++++++ 4 files changed, 419 insertions(+), 558 deletions(-) delete mode 100644 .github/workflows/autopilot.yml delete mode 100644 .github/workflows/cline-pr-review.yml create mode 100644 .github/workflows/opencode.yml create mode 100644 .opencode/github-commands.md diff --git a/.github/workflows/autopilot.yml b/.github/workflows/autopilot.yml deleted file mode 100644 index 90ff35b..0000000 --- a/.github/workflows/autopilot.yml +++ /dev/null @@ -1,388 +0,0 @@ -name: Autopilot PR Remediation - -# Comment /autopilot on any PR. Fixes land directly on the PR branch. -# -# Secrets: GEMINI_API_KEY, COMPOSIO_API_KEY -# AUTOPILOT_TOKEN (optional PAT — without it, pushed commits won't -# trigger your other workflows, so nothing re-runs CI on the fix) -# Vars: AUTOPILOT_TEST_CMD (optional; default: pnpm test when pnpm-lock.yaml) -# AUTOPILOT_MAX_STEPS (default 100) -# -# Olive Studio: Node 22 + pnpm to match ci.yml. Unresolved human and bot -# review threads are included. Keep AUTOPILOT_TEST_CMD light (unit tests); -# do not point it at the full CI matrix inside a 30m job. - -on: - issue_comment: - types: [created] - -concurrency: - group: autopilot-${{ github.event.issue.number }} - cancel-in-progress: false - -jobs: - guard: - if: > - github.event.issue.pull_request != null && - startsWith(github.event.comment.body, '/autopilot') && - github.event.comment.author_association == 'OWNER' - runs-on: ubuntu-latest - permissions: - pull-requests: read - outputs: - head_branch: ${{ steps.check.outputs.head_branch }} - base_branch: ${{ steps.check.outputs.base_branch }} - steps: - - name: Validate PR - id: check - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - set -euo pipefail - PR_JSON=$(gh pr view "${{ github.event.issue.number }}" \ - --repo "${{ github.repository }}" \ - --json headRefName,baseRefName,state,isCrossRepository) - - if [ "$(jq -r '.state' <<<"$PR_JSON")" != "OPEN" ]; then - echo "::error::PR is not open."; exit 1 - fi - # Not a security gate for you — just a clear error instead of a - # confusing checkout failure on a branch that isn't in your repo. - if [ "$(jq -r '.isCrossRepository' <<<"$PR_JSON")" = "true" ]; then - echo "::error::Fork PR — the head branch doesn't exist in this repo."; exit 1 - fi - - { - echo "head_branch=$(jq -r '.headRefName' <<<"$PR_JSON")" - echo "base_branch=$(jq -r '.baseRefName' <<<"$PR_JSON")" - } >> "$GITHUB_OUTPUT" - - remediate: - needs: guard - runs-on: ubuntu-latest - timeout-minutes: 30 - permissions: - contents: write - pull-requests: write - steps: - - name: Acknowledge - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - gh pr comment "${{ github.event.issue.number }}" --repo "${{ github.repository }}" \ - --body "🤖 Autopilot active against \`${{ needs.guard.outputs.head_branch }}\`. [Run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})" - - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - ref: ${{ needs.guard.outputs.head_branch }} - fetch-depth: 0 - token: ${{ secrets.AUTOPILOT_TOKEN || secrets.GITHUB_TOKEN }} - - - name: Fetch unresolved review threads - id: threads - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - set -euo pipefail - # GraphQL variable placeholders ($owner, $repo, …) must stay literal. - # shellcheck disable=SC2016 - gh api graphql --paginate -f query=' - query($owner: String!, $repo: String!, $pr: Int!, $endCursor: String) { - repository(owner: $owner, name: $repo) { - pullRequest(number: $pr) { - reviewThreads(first: 50, after: $endCursor) { - pageInfo { hasNextPage endCursor } - nodes { - id - isResolved - path - line - comments(first: 20) { - nodes { databaseId body author { login } } - } - } - } - } - } - }' \ - -f owner="${{ github.repository_owner }}" \ - -f repo="${{ github.event.repository.name }}" \ - -F pr="${{ github.event.issue.number }}" \ - | jq -s '[ .[].data.repository.pullRequest.reviewThreads.nodes[] ] - | map(select(.isResolved == false)) - | map({ - thread_id: .id, - reply_to_id: .comments.nodes[0].databaseId, - path: .path, - line: .line, - discussion: [ .comments.nodes[] | {author: .author.login, body: .body} ] - })' > "$RUNNER_TEMP/threads.json" - - COUNT=$(jq 'length' "$RUNNER_TEMP/threads.json") - echo "count=$COUNT" >> "$GITHUB_OUTPUT" - echo "Unresolved threads: $COUNT" - - - name: Exit early if nothing to do - if: steps.threads.outputs.count == '0' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - gh pr comment "${{ github.event.issue.number }}" --repo "${{ github.repository }}" \ - --body "🤖 Autopilot: everything is already resolved!" - - - name: Configure git identity - if: steps.threads.outputs.count != '0' - run: | - git config --global user.name "github-actions[bot]" - git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" - - - name: Merge base branch - if: steps.threads.outputs.count != '0' - run: | - git fetch origin "${{ needs.guard.outputs.base_branch }}" - git merge --no-edit "origin/${{ needs.guard.outputs.base_branch }}" || true - - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - if: steps.threads.outputs.count != '0' - with: - node-version: '22' - cache: 'pnpm' - cache-dependency-path: pnpm-lock.yaml - - - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 - if: steps.threads.outputs.count != '0' - with: - python-version: '3.11' - cache: pip - - - name: Install dependencies - if: steps.threads.outputs.count != '0' - run: | - if [ -f pnpm-lock.yaml ]; then pnpm install --frozen-lockfile - elif [ -f package-lock.json ]; then npm ci - elif [ -f yarn.lock ]; then corepack enable && yarn install --frozen-lockfile - fi - if [ -f olive-mcp-server/pyproject.toml ]; then - pip install -e "olive-mcp-server/[dev]" "mcp<2" || pip install -e "olive-mcp-server/" || true - elif [ -f requirements.txt ]; then - pip install -r requirements.txt - elif [ -f pyproject.toml ]; then - pip install -e ".[dev]" || pip install -e . || true - fi - - - name: Resolve test command - if: steps.threads.outputs.count != '0' - id: testcmd - run: | - set -euo pipefail - CMD="${{ vars.AUTOPILOT_TEST_CMD }}" - if [ -z "$CMD" ]; then - if [ -f pnpm-lock.yaml ] && [ -f package.json ] && jq -e '.scripts.test' package.json >/dev/null 2>&1; then - CMD="pnpm test" - elif [ -f package.json ] && jq -e '.scripts.test' package.json >/dev/null 2>&1; then - CMD="npm test" - elif [ -f pytest.ini ] || [ -d tests ] || [ -d olive-mcp-server/tests ]; then - CMD="pytest -q" - fi - fi - echo "cmd=$CMD" >> "$GITHUB_OUTPUT" - if [ -z "$CMD" ]; then - echo "::warning::No test suite found — fixes will be pushed unverified." - fi - - # Snapshot of whether things worked BEFORE the agent touched anything. - # This is what makes the regression check possible: if your suite was - # already red, we don't punish the agent for it. - - name: Baseline tests - if: steps.threads.outputs.count != '0' && steps.testcmd.outputs.cmd != '' - id: baseline - continue-on-error: true - run: ${{ steps.testcmd.outputs.cmd }} - - - name: Install agent tooling - if: steps.threads.outputs.count != '0' - run: pip install "mini-swe-agent==2.4.6" composio-core - - - name: Authenticate Composio - if: steps.threads.outputs.count != '0' - env: - COMPOSIO_API_KEY: ${{ secrets.COMPOSIO_API_KEY }} - run: | - composio login --api-key "$COMPOSIO_API_KEY" - echo "COMPOSIO_HEADERS={\"x-consumer-api-key\":\"$COMPOSIO_API_KEY\"}" >> "$GITHUB_ENV" - - # Agent has no GitHub token. It edits files and writes a report; the - # workflow does all the git and GitHub work below. - - name: Run agent - if: steps.threads.outputs.count != '0' - env: - GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} - MSWEA_CONFIGURED: "true" - THREADS_FILE: ${{ runner.temp }}/threads.json - REPORT_FILE: ${{ runner.temp }}/report.json - run: | - mini \ - --task "$(cat <<'TASK' - You are addressing unresolved PR review comments (human and bot). - - INPUT: Read the JSON array at $THREADS_FILE. Each entry has thread_id, - reply_to_id, path, line, and the full discussion. - - EXECUTION: - 1. Fix code issues directly in the files. Resolve any merge conflict - markers you find, preserving the intent of both sides. - 2. Use 'composio run notion search' or - 'composio run devin ask_question --params - {"repoName":"${{ github.repository }}","question":"..."}' - if you need codebase context. - 3. If a comment is unclear or needs a human decision, skip it. Skipping - is the right call more often than you'd think — a skipped thread - stays open for review, a bad fix gets silently accepted. - - CONSTRAINTS: - - Do not run git or gh. You have no GitHub credentials. - - Do not edit anything under .github/. - - Do not delete or weaken tests to make them pass. If a test fails - because your fix is wrong, fix your fix. - - OUTPUT: Write $REPORT_FILE as JSON: - { "threads": [ { "thread_id": "...", "reply_to_id": 123, - "action": "fixed" | "skipped", - "summary": "Plain English, one sentence" } ] } - Every input thread must appear exactly once. - TASK - )" \ - --model "gemini/gemini-3.6-flash" \ - -c mini.yaml \ - -c "agent.step_limit=${{ vars.AUTOPILOT_MAX_STEPS || 100 }}" \ - --yolo \ - --exit-immediately - - - name: Sanity checks - if: steps.threads.outputs.count != '0' - run: | - set -euo pipefail - if git grep -lE '^(<<<<<<<|>>>>>>>) ' -- . | grep -q .; then - echo "::error::Conflict markers left in the tree."; exit 1 - fi - # --porcelain, not `git diff`: git diff only sees files git already - # tracks, so a workflow file the agent *created* would slip through. - if [ -n "$(git status --porcelain -- .github/)" ]; then - echo "::error::Agent touched .github/. Aborting."; exit 1 - fi - - - name: Tests after changes - if: steps.threads.outputs.count != '0' && steps.testcmd.outputs.cmd != '' - id: after - continue-on-error: true - run: ${{ steps.testcmd.outputs.cmd }} - - # The one real gate. Pushing only stops if the agent broke something that - # was working — a suite that was already red doesn't block anything. - - name: Decide - if: steps.threads.outputs.count != '0' - id: decide - run: | - set -euo pipefail - BASE="${{ steps.baseline.outcome }}" - AFTER="${{ steps.after.outcome }}" - - if [ "$BASE" = "success" ] && [ "$AFTER" = "failure" ]; then - echo "regression=true" >> "$GITHUB_OUTPUT" - echo "status=🔴 Tests passed before and fail now — nothing was pushed." >> "$GITHUB_OUTPUT" - elif [ "$AFTER" = "success" ]; then - echo "regression=false" >> "$GITHUB_OUTPUT" - echo "status=🟢 Tests pass." >> "$GITHUB_OUTPUT" - elif [ "${{ steps.testcmd.outputs.cmd }}" = "" ]; then - echo "regression=false" >> "$GITHUB_OUTPUT" - echo "status=⚪ No test suite — changes are unverified." >> "$GITHUB_OUTPUT" - else - echo "regression=false" >> "$GITHUB_OUTPUT" - echo "status=🟡 Tests were already failing before this run." >> "$GITHUB_OUTPUT" - fi - - - name: Push fixes - if: steps.threads.outputs.count != '0' && steps.decide.outputs.regression == 'false' - env: - GH_TOKEN: ${{ secrets.AUTOPILOT_TOKEN || secrets.GITHUB_TOKEN }} - run: | - set -euo pipefail - git add -A - # Everything below shows up in the Actions log. Skim it on early runs — - # this is where agent scratch files (trajectory logs, temp output) will - # appear if they aren't gitignored. - echo "--- staged for commit ---" - git status --short - echo "-------------------------" - git diff --cached --quiet || git commit -m "Autopilot: addressed review feedback" - # Covers new files and the merge commit, both of which `git diff` misses. - if [ -n "$(git log --oneline "origin/${{ needs.guard.outputs.head_branch }}..HEAD")" ]; then - git push origin "HEAD:${{ needs.guard.outputs.head_branch }}" - echo "PUSHED_SHA=$(git rev-parse HEAD)" >> "$GITHUB_ENV" - fi - - - name: Reply and resolve - if: always() && steps.threads.outputs.count != '0' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - set -euo pipefail - [ -f "${{ runner.temp }}/report.json" ] || { echo "No report — agent didn't finish."; exit 0; } - - jq -c '.threads[]' "${{ runner.temp }}/report.json" | while read -r t; do - ACTION=$(jq -r '.action' <<<"$t") - RID=$(jq -r '.reply_to_id' <<<"$t") - TID=$(jq -r '.thread_id' <<<"$t") - SUM=$(jq -r '.summary' <<<"$t") - - # Resolve only when a commit actually landed. Without this, a thread - # the agent *believes* it fixed gets closed even though nothing was - # committed — and the comment that would have told you is gone. - if [ "$ACTION" = "fixed" ] && [ -n "${PUSHED_SHA:-}" ]; then - gh api "repos/${{ github.repository }}/pulls/comments/$RID/replies" \ - -f body="🤖 **Autopilot** — fixed in \`${PUSHED_SHA}\`. $SUM" || true - # NB: the input field is threadId, not id. With `id` the mutation - # errors out and `|| true` hides it, so nothing ever resolves. - # GraphQL $id must stay literal for -f id=… - # shellcheck disable=SC2016 - gh api graphql -f query='mutation($id: ID!) { - resolveReviewThread(input: {threadId: $id}) { thread { isResolved } } - }' -f id="$TID" || true - elif [ "$ACTION" = "fixed" ]; then - gh api "repos/${{ github.repository }}/pulls/comments/$RID/replies" \ - -f body="🤖 **Autopilot** — attempted a fix but nothing was pushed (${{ steps.decide.outputs.status }}) Left open. $SUM" || true - else - gh api "repos/${{ github.repository }}/pulls/comments/$RID/replies" \ - -f body="🤖 **Autopilot** — skipped, left for you. $SUM" || true - fi - done - - - name: Summary comment - if: always() && steps.threads.outputs.count != '0' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - set -euo pipefail - [ -f "${{ runner.temp }}/report.json" ] || exit 0 - { - echo "## 🤖 Autopilot summary" - echo - echo "${{ steps.decide.outputs.status }}" - echo - if [ -n "${PUSHED_SHA:-}" ]; then - echo "Pushed \`${PUSHED_SHA}\`." - else - echo "**Nothing was pushed.** Your branch is unchanged." - fi - echo - jq -r '.threads[] | "- **\(.action)** — \(.summary)"' "${{ runner.temp }}/report.json" - } > summary.md - gh pr comment "${{ github.event.issue.number }}" --repo "${{ github.repository }}" --body-file summary.md - - - name: Report failure - if: failure() - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - gh pr comment "${{ github.event.issue.number }}" --repo "${{ github.repository }}" \ - --body "🤖 Autopilot failed — nothing pushed, nothing resolved. [Logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})" diff --git a/.github/workflows/cline-pr-review.yml b/.github/workflows/cline-pr-review.yml deleted file mode 100644 index 92952fe..0000000 --- a/.github/workflows/cline-pr-review.yml +++ /dev/null @@ -1,170 +0,0 @@ -name: Cline PR Code Review - -on: - pull_request: - types: [opened, synchronize, ready_for_review] - workflow_dispatch: - inputs: - pr_number: - description: "PR number to review" - required: true - type: string - -concurrency: - group: pr-review-${{ github.event.pull_request.number || inputs.pr_number }} - cancel-in-progress: true - -permissions: - contents: read - -jobs: - cline-pr-review: - if: | - (github.event_name == 'pull_request' && github.event.pull_request.draft == false) || - github.event_name == 'workflow_dispatch' - runs-on: ubuntu-latest - timeout-minutes: 60 - - permissions: - contents: read - pull-requests: write - issues: read - - steps: - - name: Check reviewer credentials - id: cline-key - env: - CLINE_API_KEY: ${{ secrets.CLINE_API_KEY }} - run: | - if [ -n "$CLINE_API_KEY" ]; then - echo "available=true" >> "$GITHUB_OUTPUT" - else - echo "available=false" >> "$GITHUB_OUTPUT" - fi - - - name: Report unavailable reviewer credentials - if: steps.cline-key.outputs.available != 'true' - run: echo "::notice::Cline review skipped because CLINE_API_KEY is not configured" - - - name: Checkout repository - if: steps.cline-key.outputs.available == 'true' - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - with: - fetch-depth: 0 - persist-credentials: false - - - name: Setup Node.js - if: steps.cline-key.outputs.available == 'true' - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 - with: - node-version: 22 - - - name: Install Cline CLI - if: steps.cline-key.outputs.available == 'true' - env: - CLINE_INTEGRITY: sha512-3+RfBKRFk1s89FxYZJpM9t1dGLrpz/9WlP/zO9QP2U36C2A5ZvW7wbNKe8eFgYn52ihvKnT7wIel8KQz2vSf8Q== - CLINE_VERSION: 3.0.47 - run: | - archive=$(npm pack --silent "cline@$CLINE_VERSION") - node - "$archive" "$CLINE_INTEGRITY" <<'NODE' - const crypto = require("crypto"); - const fs = require("fs"); - const [archive, expected] = process.argv.slice(2); - const actual = "sha512-" + crypto.createHash("sha512") - .update(fs.readFileSync(archive)).digest("base64"); - if (actual !== expected) { - throw new Error(`Cline archive integrity mismatch: ${actual}`); - } - NODE - npm install -g "./$archive" - - - name: Configure Cline Authentication - if: steps.cline-key.outputs.available == 'true' - env: - CLINE_API_KEY: ${{ secrets.CLINE_API_KEY }} - # Replace 'anthropic' with your provider of choice (openai, openrouter, etc.) - # and ensure the corresponding secret is set in your repo settings. - run: | - cline auth --provider cline-pass \ - --apikey "$CLINE_API_KEY" \ - --modelid cline-pass/deepseek-v4-pro - - - name: Get PR number - if: steps.cline-key.outputs.available == 'true' - id: pr - run: | - if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then - echo "number=${{ inputs.pr_number }}" >> "$GITHUB_OUTPUT" - else - echo "number=${{ github.event.pull_request.number }}" >> "$GITHUB_OUTPUT" - fi - - - name: Review PR with Cline - if: steps.cline-key.outputs.available == 'true' - env: - PR_NUMBER: ${{ steps.pr.outputs.number }} - GITHUB_REPO: ${{ github.repository }} - GH_TOKEN: ${{ github.token }} - # Restrict Cline to only safe, read-only GitHub CLI commands - CLINE_COMMAND_PERMISSIONS: | - { - "allow": [ - "gh pr diff *", - "gh pr view *", - "gh pr checks *", - "gh pr list *", - "gh issue list *", - "gh issue view *", - "git log *", - "gh pr comment ${{ steps.pr.outputs.number }} *", - "gh api repos/${{ github.repository }}/pulls/${{ steps.pr.outputs.number }}/comments *", - "gh api repos/${{ github.repository }}/pulls/${{ steps.pr.outputs.number }}/reviews *" - ] - } - run: | - # Build prompt via heredoc to avoid backtick shell expansion - PROMPT=$(cat <<'CLINEPROMPT' - You are a GitHub PR reviewer for this repository. Your goal is to give the PR author helpful feedback and give maintainers the context they need to review efficiently. - - PR: #${PR_NUMBER} - - ## Required reading (in the checked-out repo) - - REVIEW.md - - README.md - - ## Gather context - Use `gh` commands to fetch the PR diff, details, and checks. - - ```bash - # Get full PR details - gh pr view '${PR_NUMBER}' --json number,title,body,author,createdAt,updatedAt,isDraft,labels,commits,files,additions,deletions,changedFiles,baseRefName,headRefName,mergeable,reviewDecision - - # Get the diff - gh pr diff '${PR_NUMBER}' - - # Check CI status - gh pr checks '${PR_NUMBER}' - ``` - - ## Review systematically - - Follow REVIEW.md. Flag P0-P3 findings against its architecture invariants: - 1. Builds only, never signs (trust boundary with numan-registry). - 2. Immutable upstream pins and release assets; no overwrite of existing tags/assets. - 3. Manual publish only; PR/push must not release. - 4. Specs omit sha256; preserve source.rev provenance. - 5. Cross-platform — no Win32-only assumptions unless Windows-only boundary. - - ## Output format - - Concise Markdown - - Severity tags: **P0** / **P1** / **P2** / **P3** (per REVIEW.md) - - File path + specific fix per finding - - If no issues found, say so in one sentence - CLINEPROMPT - ) - - # Substitute PR_NUMBER in bash (not in heredoc) - PROMPT="${PROMPT//\$\{PR_NUMBER\}/${PR_NUMBER}}" - PROMPT="${PROMPT//\$\{GITHUB_REPO\}/${GITHUB_REPO}}" - - cline --auto-approve true "$PROMPT" diff --git a/.github/workflows/opencode.yml b/.github/workflows/opencode.yml new file mode 100644 index 0000000..7792cbe --- /dev/null +++ b/.github/workflows/opencode.yml @@ -0,0 +1,215 @@ +name: opencode + +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + +jobs: + opencode: + if: | + contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association) && + (contains(github.event.comment.body, ' /oc') || + startsWith(github.event.comment.body, '/oc') || + contains(github.event.comment.body, ' /opencode') || + startsWith(github.event.comment.body, '/opencode')) + runs-on: ubuntu-latest + timeout-minutes: 45 + permissions: + id-token: write + contents: write + pull-requests: write + issues: write + steps: + - name: Checkout repository + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + with: + persist-credentials: false + + - name: Probe model availability + id: probe + shell: bash + env: + COMMENT: ${{ github.event.comment.body }} + ALIBABA_TOKEN_PLAN_API_KEY: ${{ secrets.ALIBABA_TOKEN_PLAN_API_KEY }} + DASHSCOPE_API_KEY: ${{ secrets.DASHSCOPE_API_KEY }} + OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} + run: | + probe_zen() { + local id="$1" + local url="https://opencode.ai/zen/v1/chat/completions" + local body="{\"model\":\"${id}\",\"messages\":[{\"role\":\"user\",\"content\":\"ping\"}],\"max_tokens\":1}" + local auth="Authorization: Bearer ${OPENCODE_API_KEY}" + local extra=() + if [[ "${id}" == "gpt-5.6-luna" ]]; then + url="https://opencode.ai/zen/v1/responses" + body="{\"model\":\"${id}\",\"input\":[{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"ping\"}]}],\"max_output_tokens\":1}" + fi + curl -sf --max-time 15 -X POST "${url}" \ + -H "${auth}" "${extra[@]}" -H "Content-Type: application/json" \ + -d "${body}" >/dev/null 2>&1 + } + + MODEL="opencode/big-pickle" + VARIANT="" + if [[ "${COMMENT}" =~ (^|[[:space:]])/(oc|opencode)[[:space:]]+review ]]; then + echo "::notice::/oc review detected - using review model (opencode/gpt-5.6-luna, max reasoning)" + MODEL="opencode/gpt-5.6-luna" + VARIANT="max" + if probe_zen gpt-5.6-luna; then + echo "model=${MODEL}" >> "$GITHUB_OUTPUT" + echo "variant=${VARIANT}" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "::warning::Review model (opencode/gpt-5.6-luna) unavailable - falling back to big-pickle" + MODEL="opencode/big-pickle" + VARIANT="" + fi + + if probe_zen big-pickle; then + echo "model=${MODEL}" >> "$GITHUB_OUTPUT" + echo "variant=${VARIANT}" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "::warning::Primary model (${MODEL}) unavailable - trying free fallback (nemotron-3-ultra-free)" + MODEL="opencode/nemotron-3-ultra-free" + VARIANT="" + if probe_zen nemotron-3-ultra-free; then + echo "model=${MODEL}" >> "$GITHUB_OUTPUT" + echo "variant=${VARIANT}" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "::warning::Nemotron 3 Ultra free unavailable - trying free fallback (nemotron-3.5-lightning-free)" + echo "::warning::Nemotron 3.5 Lightning free unavailable - trying free fallback (deepseek-v4-flash)" + MODEL="opencode/deepseek-v4-flash" + VARIANT="" + if probe_zen deepseek-v4-flash; then + echo "model=${MODEL}" >> "$GITHUB_OUTPUT" + echo "variant=${VARIANT}" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "::warning::DeepSeek V4 Flash unavailable - falling back to alibaba-token-plan" + MODEL="alibaba-token-plan/qwen3.8-max" + VARIANT="" + if ! curl -sf --max-time 15 \ + -X POST "https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1/chat/completions" \ + -H "Authorization: Bearer ${ALIBABA_TOKEN_PLAN_API_KEY}" \ + -H "Content-Type: application/json" \ + -d '{"model":"qwen3.8-max","messages":[{"role":"user","content":"ping"}],"max_tokens":1}' \ + >/dev/null 2>&1; then + echo "::warning::alibaba-token-plan unavailable - trying DashScope" + MODEL="alibaba/qwen3.8-max" + if ! curl -sf --max-time 15 \ + -X POST "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions" \ + -H "Authorization: Bearer ${DASHSCOPE_API_KEY}" \ + -H "Content-Type: application/json" \ + -d '{"model":"qwen3.8-max","messages":[{"role":"user","content":"ping"}],"max_tokens":1}' \ + >/dev/null 2>&1; then + echo "::warning::DashScope unavailable - trying paid Nemotron 3 Ultra" + MODEL="opencode/nemotron-3-ultra" + VARIANT="" + if probe_zen nemotron-3-ultra; then + echo "model=${MODEL}" >> "$GITHUB_OUTPUT" + echo "variant=${VARIANT}" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "::warning::All models unavailable" + fi + fi + echo "model=${MODEL}" >> "$GITHUB_OUTPUT" + echo "variant=${VARIANT}" >> "$GITHUB_OUTPUT" + + # Replicates anomalyco/opencode/github@v1.18.18 (composite action) inline so + # the run step can retry. The composite action cannot wrap itself: classic + # Actions has no step retry and the CLI commits/pushes from this checkout, + # so on a rejected push (remote advanced mid-run) the work would be lost. + - name: Get opencode version + id: version + shell: bash + run: | + VERSION=$(curl -sf https://api.github.com/repos/anomalyco/opencode/releases/latest | grep -o '"tag_name": *"[^"]*"' | cut -d'"' -f4) + echo "version=${VERSION:-latest}" >> "$GITHUB_OUTPUT" + + - name: Cache opencode + id: cache + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: ~/.opencode/bin + key: opencode-${{ runner.os }}-${{ runner.arch }}-${{ steps.version.outputs.version }} + + - name: Install opencode + if: steps.cache.outputs.cache-hit != 'true' + shell: bash + run: curl -fsSL https://opencode.ai/install | bash + + - name: Run opencode + id: run_opencode + shell: bash + env: + ALIBABA_TOKEN_PLAN_API_KEY: ${{ secrets.ALIBABA_TOKEN_PLAN_API_KEY }} + DASHSCOPE_API_KEY: ${{ secrets.DASHSCOPE_API_KEY }} + OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + MODEL: ${{ steps.probe.outputs.model }} + VARIANT: ${{ steps.probe.outputs.variant }} + run: | + set -u + echo "$HOME/.opencode/bin" >> "$GITHUB_PATH" + export PATH="$HOME/.opencode/bin:$PATH" + + run_opencode() { + opencode github run + } + + if run_opencode; then + echo "::notice::opencode github run succeeded (attempt 1)" + exit 0 + fi + echo "::warning::opencode github run failed on attempt 1" + + BRANCH="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo '')" + + # Salvage: the CLI committed locally before a rejected push (remote + # advanced mid-run). Rebase those commits onto the updated remote and + # push. This matters because review threads may already be resolved, + # so a plain re-run would find nothing to do and silently lose work. + if [[ -n "${BRANCH}" && "${BRANCH}" != "HEAD" ]]; then + echo "==> salvaging agent commits onto updated remote (branch: ${BRANCH})" + git config --local http.https://github.com/.extraheader \ + "AUTHORIZATION: basic $(printf "x-access-token:${GH_TOKEN}" | base64 -w 0)" 2>/dev/null || true + git fetch --prune origin 2>/dev/null || true + if git rev-parse --verify "origin/${BRANCH}" >/dev/null 2>&1; then + if [[ "$(git rev-list --count "origin/${BRANCH}"..HEAD 2>/dev/null || echo 0)" -gt 0 ]]; then + if git rebase "origin/${BRANCH}"; then + if git push 2>/dev/null; then + echo "::notice::salvaged agent work: rebased onto origin/${BRANCH} and pushed" + exit 0 + fi + echo "::warning::salvage push failed; falling back to synced re-run" + fi + git rebase --abort 2>/dev/null || true + else + echo "::notice::no local-only commits; failure was not a rejected push" + fi + fi + fi + + # Fallback: sync this checkout's branch to the remote (discarding the + # failed run's local state) and run a fresh session against the + # current base, so a subsequent push is a fast-forward. + echo "==> syncing branch to remote and retrying once" + if [[ -n "${BRANCH}" && "${BRANCH}" != "HEAD" ]]; then + git fetch --prune origin 2>/dev/null || true + if git rev-parse --verify "origin/${BRANCH}" >/dev/null 2>&1; then + git checkout -B "${BRANCH}" "origin/${BRANCH}" >/dev/null 2>&1 || true + git reset --hard "origin/${BRANCH}" >/dev/null 2>&1 || true + fi + fi + + if run_opencode; then + echo "::notice::opencode github run succeeded on retry" + exit 0 + fi + echo "::error::opencode github run failed on both attempts" + exit 1 diff --git a/.opencode/github-commands.md b/.opencode/github-commands.md new file mode 100644 index 0000000..4b8d066 --- /dev/null +++ b/.opencode/github-commands.md @@ -0,0 +1,204 @@ +# `/oc` GitHub PR commands + +These instructions apply when a user message is posted on a GitHub PR via the opencode +GitHub Action and begins with `/oc` (or `/opencode`). The message usually carries a +`` context block (title, body, changed files, comments, reviews) — read it +carefully before answering. + +## Model routing + +The workflow probes and selects the agent model per command (see `.github/workflows/opencode.yml`): + +| Command | Primary model | Fallback chain | +| ------------ | -------------------------------------- | --------------------------------------------------------------------------------------------- | +| `/oc review` | `opencode/gpt-5.6-luna` (`variant: max`) | `opencode/big-pickle` → `opencode/nemotron-3-ultra-free` → `opencode/nemotron-3.5-lightning-free` → `opencode/deepseek-v4-flash` → `alibaba-token-plan/qwen3.8-max` → `alibaba/qwen3.8-max` → `opencode/nemotron-3-ultra` | +| `/oc fix` | `opencode/big-pickle` | `opencode/nemotron-3-ultra-free` → `opencode/nemotron-3.5-lightning-free` → `opencode/deepseek-v4-flash` → `alibaba-token-plan/qwen3.8-max` → `alibaba/qwen3.8-max` → `opencode/nemotron-3-ultra` | + +Each model is probed with a minimal request before the run; a disabled or unavailable model +falls through to the next in the chain. The `opencode/*` models are probed through the +opencode.ai `/zen` gateway; the two `alibaba/*` models are probed through their direct +compatible-mode endpoints (`token-plan.ap-southeast-1.maas.aliyuncs.com` and +`dashscope.aliyuncs.com`) behind the `ALIBABA_TOKEN_PLAN_API_KEY` / `DASHSCOPE_API_KEY` +secrets. `/oc review` runs are short and judgment-heavy, so the cost-efficient +`gpt-5.6-luna` runs with `max` reasoning effort to maximize finding quality while keeping +per-run cost in the tens of cents; `/oc fix` runs are long agentic edit loops, where the +free big-pickle keeps cost at $0. Note that `big-pickle` advertises no reasoning-effort +variants, so `variant: max` is only applied when `gpt-5.6-luna` is actually selected — the +probe clears it on any fallback. Review runs send code snippets to an OpenAI-hosted model — +acceptable for public repos; keep in mind OpenAI may retain requests for evaluation +purposes. + +## `/oc review` + +When a user message is exactly `/oc review` or begins with `/oc review`, treat it as a +request to review the current pull request. Extra text after the shortcut, e.g. +`/oc review focus on security`, scopes the review to those concerns. + +### Posting behavior + +**One comment per actionable finding.** Do NOT write one big review. Instead: + +1. Identify the actionable findings. An actionable finding is one where you can point at a + concrete problem in the code and, when feasible, propose a specific change. +2. Post each actionable finding as its **own resolvable review thread** via the `gh` CLI + (preinstalled in GitHub Actions; the `GITHUB_TOKEN` env var is available, no login + needed). Fall back down this ladder until the finding is posted: + + a. **Inline line comment** (preferred) — pins the finding to a line in the PR diff and + creates a resolvable thread. Use the PR head SHA (`Head: { Sha: ... }` in the + `` context) as `commit_id`, plus the file and line the finding is + about: + + ```bash + gh api repos/{owner}/{repo}/pulls/{pr_number}/comments \ + -f body=@finding.md \ + -f path="src/example.ts" \ + -F line=42 \ + -f commit_id="$HEAD_SHA" + ``` + + For a finding spanning a line range, add `-F start_line=` (and, for a + deletion, `-f start_side=LEFT`). + + b. **File-level comment** — if the line is not part of the diff (the call above returns a + 422), retry against the file without a line number: + + ```bash + gh api repos/{owner}/{repo}/pulls/{pr_number}/comments \ + -f body=@finding.md \ + -f path="src/example.ts" \ + -f subject_type=file + ``` + + c. **Issue comment** (last resort) — if the file is not in the PR diff either, post to + the timeline (not a resolvable thread) and flag it in the "Out of diff" section of + the summary: + + ```bash + gh api repos/{owner}/{repo}/issues/{pr_number}/comments -F body=@finding.md + ``` + + Derive `owner`/`repo` from `baseRepository.nameWithOwner` in the `` + context (split on `/`), `pr_number` from `Number:`, and `HEAD_SHA` from + `Head: { Sha: ... }`. Write the finding body to a temp file (`finding.md`) rather than + passing a giant `-f body=` string, so multiline Markdown and code blocks survive intact. + Post threads one at a time — this endpoint is secondary-rate-limited if you post too + fast — and keep a list of the posted comment IDs/URLs and of which findings fell back to + an issue comment. If a `gh` call fails at every level, do not stop the review — record + the finding in the "Out of diff" section of the summary instead. +3. **Your final reply text** (what the action posts as the single reply comment) must be a + **short summary index**: overall assessment; one line per threaded finding with its + file:line, severity, and a link to that finding's comment (both endpoint responses + include the `html_url`); and an **"Out of diff"** section listing every finding that + could not be posted as a review thread — fallback issue comments and any finding with no + diff location (e.g. missing tests, missing docs, cross-file concerns) — with its + severity, the file name(s) and line(s) it covers, and the issue found. Keep the rest + tight — the detail lives in the per-finding comments. +4. Group low-severity nits and non-actionable observations into the final summary comment + instead of posting more comments. + +### Committing behavior — suggestions only + +You are reviewing, not editing: + +- **Do NOT modify any files and do NOT leave the working tree dirty.** The action auto-commits + and pushes any uncommitted changes to the PR branch — that is not wanted here. +- Include a **committable suggestion** in each finding comment when it is feasible to write + one for that specific finding. Wrap the exact replacement in a GitHub `suggestion` + fenced block so GitHub renders a one-click **Commit suggestion** button right in the + comment: + + ```` + ```suggestion + + ``` + ```` + + Use one contiguous block per finding, matching the existing lines it replaces; GitHub + applies it to the file on commit. If a finding does not have a cut-and-dried fix — no + contiguous single-file replacement — say so and describe the change needed instead of + inventing code. + +### Finding comment format + +Each finding comment should contain: + +1. **Severity** — `high` / `medium` / `low` (or `critical`). +2. **Location** — `file:line` (or a line range). +3. **Problem** — why it is wrong, grounded in the actual code. +4. **Suggested fix** — a GitHub `suggestion` fenced block (see "Committing behavior") + when the fix is a contiguous replacement, otherwise a description of the change needed. + +### Review scope + +Look for: correctness bugs, security issues (injection, secret handling, authorization), +performance, maintainability, and test coverage gaps. Ground every finding in the actual diff +and files. Do not invent issues; verify against the code. If there are no actionable findings, +just say so in the summary comment and do not post finding comments. + +## `/oc fix` + +When a user message is exactly `/oc fix` or begins with `/oc fix`, fix the review feedback on +the current pull request. + +### Behavior + +1. **Collect all review feedback** from the `` context: + - inline review comments (inside `` → comments) + - timeline comments (``) + - review bodies (``) +2. **For each comment, judge whether it is valid and actionable** against the current code: + - **Valid and fixable** → implement the fix by editing files in the working tree. The + GitHub Action auto-commits and pushes any uncommitted changes to the PR branch; you do + not need to `git commit`/`git push` yourself (though committing yourself is also fine — + the action detects it and pushes). + - **Not valid, not fixable, or already handled** → do not change code for it, but it still + counts as addressed (addressed *as not valid*): reply on the thread with the reason and + resolve it (step 3). + - **Not an inline-resolvable thread but still contains real feedback to address** (e.g. a + timeline comment or a general review-body request) → address it with a commit too when + the feedback is valid, and record it in the summary. +3. **Resolve addressed review threads.** A review thread (inline review comment chain) is + resolvable; timeline comments are not. "Addressed" includes threads you **explicitly + skip**: a comment judged not valid, already handled, or intentionally not applicable is + still addressed (as not valid) and gets resolved too. For every thread you resolve, reply + on the thread with the reason first (a fix summary, or the justification for skipping) + when possible — the thread then keeps its rationale and the author sees it in place. Use + `gh`: + + ```bash + # 1. List threads, their resolved state, and the first comment's databaseId + gh api graphql -f query='query($owner:String!,$repo:String!,$number:Int!){repository(owner:$owner,name:$repo){pullRequest(number:$number){reviewThreads(first:100){nodes{id isResolved comments(first:10){nodes{databaseId}}}}}}}' -F owner=... -F repo=... -F number=... + + # 2. Reply to the thread with the reason before resolving + gh api graphql -f query='mutation($id:ID!,$body:String!){addPullRequestReviewThreadReply(input:{pullRequestReviewThreadId:$id,body:$body}){comment{id}}}' -F id=THREAD_ID -f body=REASON + # REST equivalent (reply to the first comment in the thread): + # gh api repos/{owner}/{repo}/pulls/{pr_number}/comments/{comment_id}/replies -f body=REASON + + # 3. Resolve the thread + gh api graphql -f query='mutation($id:ID!){resolveReviewThread(input:{threadId:$id}){thread{isResolved}}}' -F id=THREAD_ID + ``` + + Write the reason to a temp file (`thread.md`) and pass `-f body=@thread.md` when it is + long, so multiline Markdown survives intact. Only leave open a thread you genuinely could + not address — no fix and no justification — and say why in the summary. +4. **Your final reply text IS the single summary comment** (the action posts it). Do NOT post + extra per-finding comments. The summary must cover **everything**: + - **Fixed** — for each addressed item: the change made (file:line) and whether its thread + was resolved. + - **Not fixed (resolved as not valid)** — for each comment you skipped: a brief reason + (invalid, already handled, duplicate, out of scope, not fixable) and a note that its + thread was replied to and resolved. + - **Addressed non-thread feedback** — any feedback that wasn't an inline thread but still + warranted a code change: list the change made. + - A short overall assessment of remaining risk. + +### Fixing behavior notes + +- Ground every judgment in the actual diff and files. Verify a comment is still valid against + the current code before acting on it. +- Keep fixes minimal and targeted to the feedback. Do not refactor unrelated code. +- Resolve every thread you addressed — fixed or explicitly skipped (skipping with a reason + is addressing *as not valid*) — and reply on each thread with the reason when possible. + Do not resolve a thread you genuinely could not address. Do not modify files for invalid + or duplicate feedback. From b367e0b50c990e33c40b8f75f69b5a829b44147f Mon Sep 17 00:00:00 2001 From: Anthony Thompson Date: Sun, 16 Aug 2026 06:49:48 -0700 Subject: [PATCH 2/9] Apply suggestions from code review Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- .github/workflows/opencode.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/opencode.yml b/.github/workflows/opencode.yml index 7792cbe..195553b 100644 --- a/.github/workflows/opencode.yml +++ b/.github/workflows/opencode.yml @@ -115,10 +115,9 @@ jobs: exit 0 fi echo "::warning::All models unavailable" + exit 1 fi fi - echo "model=${MODEL}" >> "$GITHUB_OUTPUT" - echo "variant=${VARIANT}" >> "$GITHUB_OUTPUT" # Replicates anomalyco/opencode/github@v1.18.18 (composite action) inline so # the run step can retry. The composite action cannot wrap itself: classic From 2c4c6d8473364c561b8f26d47017c36c85f05337 Mon Sep 17 00:00:00 2001 From: tonythethompson Date: Sun, 16 Aug 2026 06:52:31 -0700 Subject: [PATCH 3/9] fix(opencode): probe nemotron-3.5-lightning-free and fail closed on model exhaustion --- .github/workflows/opencode.yml | 54 +++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/.github/workflows/opencode.yml b/.github/workflows/opencode.yml index 195553b..e9f7469 100644 --- a/.github/workflows/opencode.yml +++ b/.github/workflows/opencode.yml @@ -81,6 +81,13 @@ jobs: exit 0 fi echo "::warning::Nemotron 3 Ultra free unavailable - trying free fallback (nemotron-3.5-lightning-free)" + MODEL="opencode/nemotron-3.5-lightning-free" + VARIANT="" + if probe_zen nemotron-3.5-lightning-free; then + echo "model=${MODEL}" >> "$GITHUB_OUTPUT" + echo "variant=${VARIANT}" >> "$GITHUB_OUTPUT" + exit 0 + fi echo "::warning::Nemotron 3.5 Lightning free unavailable - trying free fallback (deepseek-v4-flash)" MODEL="opencode/deepseek-v4-flash" VARIANT="" @@ -92,32 +99,39 @@ jobs: echo "::warning::DeepSeek V4 Flash unavailable - falling back to alibaba-token-plan" MODEL="alibaba-token-plan/qwen3.8-max" VARIANT="" - if ! curl -sf --max-time 15 \ + if curl -sf --max-time 15 \ -X POST "https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1/chat/completions" \ -H "Authorization: Bearer ${ALIBABA_TOKEN_PLAN_API_KEY}" \ -H "Content-Type: application/json" \ -d '{"model":"qwen3.8-max","messages":[{"role":"user","content":"ping"}],"max_tokens":1}' \ >/dev/null 2>&1; then - echo "::warning::alibaba-token-plan unavailable - trying DashScope" - MODEL="alibaba/qwen3.8-max" - if ! curl -sf --max-time 15 \ - -X POST "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions" \ - -H "Authorization: Bearer ${DASHSCOPE_API_KEY}" \ - -H "Content-Type: application/json" \ - -d '{"model":"qwen3.8-max","messages":[{"role":"user","content":"ping"}],"max_tokens":1}' \ - >/dev/null 2>&1; then - echo "::warning::DashScope unavailable - trying paid Nemotron 3 Ultra" - MODEL="opencode/nemotron-3-ultra" - VARIANT="" - if probe_zen nemotron-3-ultra; then - echo "model=${MODEL}" >> "$GITHUB_OUTPUT" - echo "variant=${VARIANT}" >> "$GITHUB_OUTPUT" - exit 0 - fi - echo "::warning::All models unavailable" - exit 1 - fi + echo "model=${MODEL}" >> "$GITHUB_OUTPUT" + echo "variant=${VARIANT}" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "::warning::alibaba-token-plan unavailable - trying DashScope" + MODEL="alibaba/qwen3.8-max" + VARIANT="" + if curl -sf --max-time 15 \ + -X POST "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions" \ + -H "Authorization: Bearer ${DASHSCOPE_API_KEY}" \ + -H "Content-Type: application/json" \ + -d '{"model":"qwen3.8-max","messages":[{"role":"user","content":"ping"}],"max_tokens":1}' \ + >/dev/null 2>&1; then + echo "model=${MODEL}" >> "$GITHUB_OUTPUT" + echo "variant=${VARIANT}" >> "$GITHUB_OUTPUT" + exit 0 fi + echo "::warning::DashScope unavailable - trying paid Nemotron 3 Ultra" + MODEL="opencode/nemotron-3-ultra" + VARIANT="" + if probe_zen nemotron-3-ultra; then + echo "model=${MODEL}" >> "$GITHUB_OUTPUT" + echo "variant=${VARIANT}" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "::error::All models unavailable" + exit 1 # Replicates anomalyco/opencode/github@v1.18.18 (composite action) inline so # the run step can retry. The composite action cannot wrap itself: classic From 1895ad3985c7569613bd0ae9424e75a5d72c2286 Mon Sep 17 00:00:00 2001 From: tonythethompson Date: Sun, 16 Aug 2026 06:54:06 -0700 Subject: [PATCH 4/9] fix(opencode): use printf format specifier for token --- .github/workflows/opencode.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/opencode.yml b/.github/workflows/opencode.yml index e9f7469..2688487 100644 --- a/.github/workflows/opencode.yml +++ b/.github/workflows/opencode.yml @@ -190,7 +190,7 @@ jobs: if [[ -n "${BRANCH}" && "${BRANCH}" != "HEAD" ]]; then echo "==> salvaging agent commits onto updated remote (branch: ${BRANCH})" git config --local http.https://github.com/.extraheader \ - "AUTHORIZATION: basic $(printf "x-access-token:${GH_TOKEN}" | base64 -w 0)" 2>/dev/null || true + "AUTHORIZATION: basic $(printf 'x-access-token:%s' "${GH_TOKEN}" | base64 -w 0)" 2>/dev/null || true git fetch --prune origin 2>/dev/null || true if git rev-parse --verify "origin/${BRANCH}" >/dev/null 2>&1; then if [[ "$(git rev-list --count "origin/${BRANCH}"..HEAD 2>/dev/null || echo 0)" -gt 0 ]]; then From 219b8376483eaacabf9e8fe5ab782c8f2369e3a2 Mon Sep 17 00:00:00 2001 From: Anthony Thompson Date: Sun, 16 Aug 2026 06:57:06 -0700 Subject: [PATCH 5/9] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .opencode/github-commands.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.opencode/github-commands.md b/.opencode/github-commands.md index 4b8d066..bd19082 100644 --- a/.opencode/github-commands.md +++ b/.opencode/github-commands.md @@ -1,7 +1,8 @@ # `/oc` GitHub PR commands These instructions apply when a user message is posted on a GitHub PR via the opencode -GitHub Action and begins with `/oc` (or `/opencode`). The message usually carries a +GitHub Action and starts with `/oc` (or `/opencode`) — or includes them preceded by whitespace +(per the workflow trigger guard). The message usually carries a `` context block (title, body, changed files, comments, reviews) — read it carefully before answering. From fdda266fe6fb42021a331e0a1c042ca3d2ea16dd Mon Sep 17 00:00:00 2001 From: tonythethompson Date: Sun, 16 Aug 2026 06:58:26 -0700 Subject: [PATCH 6/9] fix(opencode): harden workflow with PR check, key guard, git identity, and release checksum verification --- .github/workflows/opencode.yml | 36 ++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/.github/workflows/opencode.yml b/.github/workflows/opencode.yml index 2688487..39ba649 100644 --- a/.github/workflows/opencode.yml +++ b/.github/workflows/opencode.yml @@ -9,6 +9,7 @@ on: jobs: opencode: if: | + (github.event_name == 'pull_request_review_comment' || github.event.issue.pull_request != null) && contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association) && (contains(github.event.comment.body, ' /oc') || startsWith(github.event.comment.body, '/oc') || @@ -36,6 +37,11 @@ jobs: DASHSCOPE_API_KEY: ${{ secrets.DASHSCOPE_API_KEY }} OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} run: | + if [[ -z "${OPENCODE_API_KEY:-}" ]]; then + echo "::error::OPENCODE_API_KEY secret is required but not set" + exit 1 + fi + probe_zen() { local id="$1" local url="https://opencode.ai/zen/v1/chat/completions" @@ -137,12 +143,21 @@ jobs: # the run step can retry. The composite action cannot wrap itself: classic # Actions has no step retry and the CLI commits/pushes from this checkout, # so on a rejected push (remote advanced mid-run) the work would be lost. - - name: Get opencode version + - name: Get opencode version and asset id: version shell: bash run: | - VERSION=$(curl -sf https://api.github.com/repos/anomalyco/opencode/releases/latest | grep -o '"tag_name": *"[^"]*"' | cut -d'"' -f4) - echo "version=${VERSION:-latest}" >> "$GITHUB_OUTPUT" + RELEASE=$(curl -sf https://api.github.com/repos/anomalyco/opencode/releases/latest) + VERSION=$(echo "${RELEASE}" | jq -r '.tag_name // empty') + URL=$(echo "${RELEASE}" | jq -r '.assets[] | select(.name=="opencode-linux-x64.tar.gz") | .browser_download_url // empty') + DIGEST=$(echo "${RELEASE}" | jq -r '.assets[] | select(.name=="opencode-linux-x64.tar.gz") | .digest // empty' | sed 's/^sha256://') + if [[ -z "${VERSION}" || -z "${URL}" || -z "${DIGEST}" ]]; then + echo "::error::Failed to fetch opencode release metadata" + exit 1 + fi + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "url=${URL}" >> "$GITHUB_OUTPUT" + echo "digest=${DIGEST}" >> "$GITHUB_OUTPUT" - name: Cache opencode id: cache @@ -154,7 +169,17 @@ jobs: - name: Install opencode if: steps.cache.outputs.cache-hit != 'true' shell: bash - run: curl -fsSL https://opencode.ai/install | bash + env: + OPENCODE_URL: ${{ steps.version.outputs.url }} + OPENCODE_DIGEST: ${{ steps.version.outputs.digest }} + run: | + mkdir -p "$HOME/.opencode/bin" + TAR_FILE="$(mktemp --suffix=.tar.gz)" + curl -fsSL -o "${TAR_FILE}" "${OPENCODE_URL}" + echo "${OPENCODE_DIGEST} ${TAR_FILE}" | sha256sum -c - + tar -xzf "${TAR_FILE}" -C "$HOME/.opencode/bin" opencode + chmod 755 "$HOME/.opencode/bin/opencode" + rm -f "${TAR_FILE}" - name: Run opencode id: run_opencode @@ -171,6 +196,9 @@ jobs: echo "$HOME/.opencode/bin" >> "$GITHUB_PATH" export PATH="$HOME/.opencode/bin:$PATH" + git config --global user.name "github-actions[bot]" + git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" + run_opencode() { opencode github run } From a80f291ed64cab016af2406d593e691b06679002 Mon Sep 17 00:00:00 2001 From: tonythethompson Date: Sun, 16 Aug 2026 06:59:58 -0700 Subject: [PATCH 7/9] fix(opencode): group output redirects to satisfy shellcheck SC2129 --- .github/workflows/opencode.yml | 60 +++++++++++++++++++++------------- 1 file changed, 37 insertions(+), 23 deletions(-) diff --git a/.github/workflows/opencode.yml b/.github/workflows/opencode.yml index 39ba649..7f241fb 100644 --- a/.github/workflows/opencode.yml +++ b/.github/workflows/opencode.yml @@ -64,8 +64,10 @@ jobs: MODEL="opencode/gpt-5.6-luna" VARIANT="max" if probe_zen gpt-5.6-luna; then - echo "model=${MODEL}" >> "$GITHUB_OUTPUT" - echo "variant=${VARIANT}" >> "$GITHUB_OUTPUT" + { + echo "model=${MODEL}" + echo "variant=${VARIANT}" + } >> "$GITHUB_OUTPUT" exit 0 fi echo "::warning::Review model (opencode/gpt-5.6-luna) unavailable - falling back to big-pickle" @@ -74,32 +76,40 @@ jobs: fi if probe_zen big-pickle; then - echo "model=${MODEL}" >> "$GITHUB_OUTPUT" - echo "variant=${VARIANT}" >> "$GITHUB_OUTPUT" + { + echo "model=${MODEL}" + echo "variant=${VARIANT}" + } >> "$GITHUB_OUTPUT" exit 0 fi echo "::warning::Primary model (${MODEL}) unavailable - trying free fallback (nemotron-3-ultra-free)" MODEL="opencode/nemotron-3-ultra-free" VARIANT="" if probe_zen nemotron-3-ultra-free; then - echo "model=${MODEL}" >> "$GITHUB_OUTPUT" - echo "variant=${VARIANT}" >> "$GITHUB_OUTPUT" + { + echo "model=${MODEL}" + echo "variant=${VARIANT}" + } >> "$GITHUB_OUTPUT" exit 0 fi echo "::warning::Nemotron 3 Ultra free unavailable - trying free fallback (nemotron-3.5-lightning-free)" MODEL="opencode/nemotron-3.5-lightning-free" VARIANT="" if probe_zen nemotron-3.5-lightning-free; then - echo "model=${MODEL}" >> "$GITHUB_OUTPUT" - echo "variant=${VARIANT}" >> "$GITHUB_OUTPUT" + { + echo "model=${MODEL}" + echo "variant=${VARIANT}" + } >> "$GITHUB_OUTPUT" exit 0 fi echo "::warning::Nemotron 3.5 Lightning free unavailable - trying free fallback (deepseek-v4-flash)" MODEL="opencode/deepseek-v4-flash" VARIANT="" if probe_zen deepseek-v4-flash; then - echo "model=${MODEL}" >> "$GITHUB_OUTPUT" - echo "variant=${VARIANT}" >> "$GITHUB_OUTPUT" + { + echo "model=${MODEL}" + echo "variant=${VARIANT}" + } >> "$GITHUB_OUTPUT" exit 0 fi echo "::warning::DeepSeek V4 Flash unavailable - falling back to alibaba-token-plan" @@ -111,8 +121,10 @@ jobs: -H "Content-Type: application/json" \ -d '{"model":"qwen3.8-max","messages":[{"role":"user","content":"ping"}],"max_tokens":1}' \ >/dev/null 2>&1; then - echo "model=${MODEL}" >> "$GITHUB_OUTPUT" - echo "variant=${VARIANT}" >> "$GITHUB_OUTPUT" + { + echo "model=${MODEL}" + echo "variant=${VARIANT}" + } >> "$GITHUB_OUTPUT" exit 0 fi echo "::warning::alibaba-token-plan unavailable - trying DashScope" @@ -124,25 +136,25 @@ jobs: -H "Content-Type: application/json" \ -d '{"model":"qwen3.8-max","messages":[{"role":"user","content":"ping"}],"max_tokens":1}' \ >/dev/null 2>&1; then - echo "model=${MODEL}" >> "$GITHUB_OUTPUT" - echo "variant=${VARIANT}" >> "$GITHUB_OUTPUT" + { + echo "model=${MODEL}" + echo "variant=${VARIANT}" + } >> "$GITHUB_OUTPUT" exit 0 fi echo "::warning::DashScope unavailable - trying paid Nemotron 3 Ultra" MODEL="opencode/nemotron-3-ultra" VARIANT="" if probe_zen nemotron-3-ultra; then - echo "model=${MODEL}" >> "$GITHUB_OUTPUT" - echo "variant=${VARIANT}" >> "$GITHUB_OUTPUT" + { + echo "model=${MODEL}" + echo "variant=${VARIANT}" + } >> "$GITHUB_OUTPUT" exit 0 fi echo "::error::All models unavailable" exit 1 - # Replicates anomalyco/opencode/github@v1.18.18 (composite action) inline so - # the run step can retry. The composite action cannot wrap itself: classic - # Actions has no step retry and the CLI commits/pushes from this checkout, - # so on a rejected push (remote advanced mid-run) the work would be lost. - name: Get opencode version and asset id: version shell: bash @@ -155,9 +167,11 @@ jobs: echo "::error::Failed to fetch opencode release metadata" exit 1 fi - echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - echo "url=${URL}" >> "$GITHUB_OUTPUT" - echo "digest=${DIGEST}" >> "$GITHUB_OUTPUT" + { + echo "version=${VERSION}" + echo "url=${URL}" + echo "digest=${DIGEST}" + } >> "$GITHUB_OUTPUT" - name: Cache opencode id: cache From d5a231dce1926f7a314412294d98737ffe7d1f7d Mon Sep 17 00:00:00 2001 From: tonythethompson Date: Sun, 16 Aug 2026 07:05:06 -0700 Subject: [PATCH 8/9] fix(opencode): add concurrency, enforce PR boundary, and use -F in doc examples --- .github/workflows/opencode.yml | 16 ++++++++++++---- .opencode/github-commands.md | 6 +++--- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/.github/workflows/opencode.yml b/.github/workflows/opencode.yml index 7f241fb..e18903e 100644 --- a/.github/workflows/opencode.yml +++ b/.github/workflows/opencode.yml @@ -6,15 +6,23 @@ on: pull_request_review_comment: types: [created] +concurrency: + group: ${{ github.workflow }}-${{ github.event.issue.number || github.event.pull_request.number || github.run_id }} + cancel-in-progress: false + jobs: opencode: if: | (github.event_name == 'pull_request_review_comment' || github.event.issue.pull_request != null) && contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association) && - (contains(github.event.comment.body, ' /oc') || - startsWith(github.event.comment.body, '/oc') || - contains(github.event.comment.body, ' /opencode') || - startsWith(github.event.comment.body, '/opencode')) + (startsWith(github.event.comment.body, '/oc ') || + github.event.comment.body == '/oc' || + startsWith(github.event.comment.body, '/opencode ') || + github.event.comment.body == '/opencode' || + contains(github.event.comment.body, ' /oc ') || + endsWith(github.event.comment.body, ' /oc') || + contains(github.event.comment.body, ' /opencode ') || + endsWith(github.event.comment.body, ' /opencode')) runs-on: ubuntu-latest timeout-minutes: 45 permissions: diff --git a/.opencode/github-commands.md b/.opencode/github-commands.md index bd19082..370f048 100644 --- a/.opencode/github-commands.md +++ b/.opencode/github-commands.md @@ -52,7 +52,7 @@ request to review the current pull request. Extra text after the shortcut, e.g. ```bash gh api repos/{owner}/{repo}/pulls/{pr_number}/comments \ - -f body=@finding.md \ + -F body=@finding.md \ -f path="src/example.ts" \ -F line=42 \ -f commit_id="$HEAD_SHA" @@ -66,7 +66,7 @@ request to review the current pull request. Extra text after the shortcut, e.g. ```bash gh api repos/{owner}/{repo}/pulls/{pr_number}/comments \ - -f body=@finding.md \ + -F body=@finding.md \ -f path="src/example.ts" \ -f subject_type=file ``` @@ -180,7 +180,7 @@ the current pull request. gh api graphql -f query='mutation($id:ID!){resolveReviewThread(input:{threadId:$id}){thread{isResolved}}}' -F id=THREAD_ID ``` - Write the reason to a temp file (`thread.md`) and pass `-f body=@thread.md` when it is + Write the reason to a temp file (`thread.md`) and pass `-F body=@thread.md` when it is long, so multiline Markdown survives intact. Only leave open a thread you genuinely could not address — no fix and no justification — and say why in the summary. 4. **Your final reply text IS the single summary comment** (the action posts it). Do NOT post From 17ecff055b71b28e3d034bf6acc1d51b33ad5d05 Mon Sep 17 00:00:00 2001 From: tonythethompson Date: Sun, 16 Aug 2026 07:06:15 -0700 Subject: [PATCH 9/9] fix(opencode): grant actions: write for actions/cache --- .github/workflows/opencode.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/opencode.yml b/.github/workflows/opencode.yml index e18903e..c289e75 100644 --- a/.github/workflows/opencode.yml +++ b/.github/workflows/opencode.yml @@ -30,6 +30,7 @@ jobs: contents: write pull-requests: write issues: write + actions: write steps: - name: Checkout repository uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0