From 7182a6f8f4c4cbbc3564220d09a1f94d46e26b90 Mon Sep 17 00:00:00 2001 From: Danny Kim Date: Sun, 13 Sep 2026 22:58:24 +0900 Subject: [PATCH 1/2] Split author and reviewer tokens --- action.yml | 14 ++++++++--- scripts/lib.sh | 58 +++++++++++++++++++++++++++++++++++++++++++ scripts/prompt.md | 16 +++++++----- scripts/run-claude.sh | 30 ++++++---------------- scripts/run-codex.sh | 30 ++++++---------------- 5 files changed, 93 insertions(+), 55 deletions(-) create mode 100644 scripts/lib.sh diff --git a/action.yml b/action.yml index 982d639..99942e4 100644 --- a/action.yml +++ b/action.yml @@ -5,8 +5,15 @@ inputs: bot-type: description: Which bot to run, either `codex` or `claude` required: true - gitea-token: - description: Gitea access token for API calls and pushes + author-token: + description: >- + Gitea token of the account that commits, pushes, and opens pull requests. + The agent sees it as `GITEA_TOKEN`. + required: true + reviewer-token: + description: >- + Gitea token of the bot account that posts comments and pull request + reviews. Never exposed to the agent, so it must differ from the author. required: true bot-token: description: API key or token for the selected bot @@ -29,7 +36,8 @@ runs: ACTION_PATH: ${{ gitea.action_path }} GITEA_API_URL: ${{ gitea.api_url }} GITEA_REPOSITORY: ${{ gitea.repository }} - GITEA_TOKEN: ${{ inputs.gitea-token }} + GITEA_TOKEN: ${{ inputs.author-token }} + REVIEWER_TOKEN: ${{ inputs.reviewer-token }} BOT_TOKEN: ${{ inputs.bot-token }} EVENT_NAME: ${{ gitea.event_name }} ISSUE_INDEX: ${{ gitea.event.issue.number || gitea.event.pull_request.number }} diff --git a/scripts/lib.sh b/scripts/lib.sh new file mode 100644 index 0000000..d9ec127 --- /dev/null +++ b/scripts/lib.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# Shared by the run-*.sh scripts. Everything the scripts post goes through the +# reviewer token, so it appears as the bot account. The agent only ever sees the +# author token as GITEA_TOKEN, which is what pushes and opens pull requests. + +ISSUE_URL="${GITEA_API_URL}/repos/${GITEA_REPOSITORY}/issues/${ISSUE_INDEX}" +PULL_URL="${GITEA_API_URL}/repos/${GITEA_REPOSITORY}/pulls/${ISSUE_INDEX}" +COMMENT_FILE="$(mktemp)" + +gitea_api() { + curl --fail-with-body --silent --show-error \ + -H "Authorization: token ${1}" \ + -H "Content-Type: application/json" \ + "${@:2}" +} + +# Commits are made by the author account so the pull request and its commits +# belong to the same person. +configure_git_author() { + AUTHOR="$(gitea_api "${GITEA_TOKEN}" "${GITEA_API_URL}/user")" + git config --global user.name "$(jq -r '.login' <<< "${AUTHOR}")" + git config --global user.email \ + "$(jq -r '.email // empty' <<< "${AUTHOR}")" +} + +post_comment() { + gitea_api "${REVIEWER_TOKEN}" -X POST \ + --data "$(jq -n --rawfile body "${COMMENT_FILE}" '{body: $body}')" \ + "${ISSUE_URL}/comments" +} + +# A pull request event is a review request, so the response becomes a review +# rather than a comment: it requests changes when the agent asked @bot to fix +# something, only comments when the run failed, and approves otherwise. +post_result() { + if [ "${EVENT_NAME}" != pull_request ]; then + post_comment + return + fi + local event=APPROVED + if grep -q '@bot' "${COMMENT_FILE}"; then + event=REQUEST_CHANGES + elif grep -q '^Bot failed:' "${COMMENT_FILE}"; then + event=COMMENT + fi + gitea_api "${REVIEWER_TOKEN}" -X POST \ + --data "$(jq -n --rawfile body "${COMMENT_FILE}" --arg event "${event}" \ + '{body: $body, event: $event}')" \ + "${PULL_URL}/reviews" +} + +render_prompt() { + export ISSUE_COMMENTS="$( + gitea_api "${REVIEWER_TOKEN}" "${ISSUE_URL}/comments?limit=100" \ + | jq -r '.[] | "## " + .user.login + " at " + .created_at + "\n\n" + .body + "\n"' + )" + envsubst < "${ACTION_PATH}/scripts/prompt.md" +} diff --git a/scripts/prompt.md b/scripts/prompt.md index 4a0886b..268afe1 100644 --- a/scripts/prompt.md +++ b/scripts/prompt.md @@ -13,11 +13,13 @@ response ends, so background monitors, scheduled wake-ups, and queued tasks never resume. Never promise future action and never claim to be waiting on a notification. -For a `pull_request` event, review the newly opened PR without changing code. If -changes are needed, include `@bot` in the final response with instructions to -fix the findings. Otherwise, do not mention `@bot`. For UI changes, check that -the result is aligned, clean, and pixel-perfect, and that included screenshots -prove the intended result was achieved. +For a `pull_request` event, review the PR without changing code. Your final +response is posted as a pull request review from the bot account: it requests +changes when it mentions `@bot` and approves otherwise. So include `@bot` with +instructions to fix the findings exactly when changes are needed, and never +mention `@bot` when the PR is ready. For UI changes, check that the result is +aligned, clean, and pixel-perfect, and that included screenshots prove the +intended result was achieved. For an `issue_comment` or `pull_request_review_comment` event, treat the `body` in the triggering comment payload below as the user's exact instruction. @@ -65,4 +67,6 @@ ${ISSUE_COMMENTS} - API URL: `${GITEA_API_URL}` - Repository: `${GITEA_REPOSITORY}` - Issue index: `${ISSUE_INDEX}` -- Use the `GITEA_TOKEN` environment variable for authenticated Gitea API calls. +- Use the `GITEA_TOKEN` environment variable for authenticated Gitea API calls + and pushes. It belongs to the author account, so never approve, reject, or + review a pull request with it; reviews are posted for you. diff --git a/scripts/run-claude.sh b/scripts/run-claude.sh index df353d1..9ae0690 100644 --- a/scripts/run-claude.sh +++ b/scripts/run-claude.sh @@ -1,20 +1,9 @@ #!/usr/bin/env bash set -euo pipefail -git config --global user.name bot -git config --global user.email noreply@capsulizers.com +. "${ACTION_PATH}/scripts/lib.sh" -COMMENT_FILE="$(mktemp)" - -post_comment() { - COMMENT_JSON="$(jq -n --rawfile body "${COMMENT_FILE}" '{body: $body}')" - curl --fail-with-body --silent --show-error \ - -X POST \ - -H "Authorization: token ${GITEA_TOKEN}" \ - -H "Content-Type: application/json" \ - --data "${COMMENT_JSON}" \ - "${GITEA_API_URL}/repos/${GITEA_REPOSITORY}/issues/${ISSUE_INDEX}/comments" -} +configure_git_author if [ -z "${BOT_TOKEN:-}" ]; then echo 'Run `claude setup-token` locally and set the `bot-token` action input.' > "${COMMENT_FILE}" @@ -23,14 +12,7 @@ if [ -z "${BOT_TOKEN:-}" ]; then fi export CLAUDE_CODE_OAUTH_TOKEN="${BOT_TOKEN}" -export ISSUE_COMMENTS="$( - curl --fail-with-body --silent --show-error \ - -H "Authorization: token ${GITEA_TOKEN}" \ - "${GITEA_API_URL}/repos/${GITEA_REPOSITORY}/issues/${ISSUE_INDEX}/comments?limit=100" \ - | jq -r '.[] | "## " + .user.login + " at " + .created_at + "\n\n" + .body + "\n"' -)" - -FINAL_PROMPT="$(envsubst < "${ACTION_PATH}/scripts/prompt.md")" +FINAL_PROMPT="$(render_prompt)" # Claude refuses --dangerously-skip-permissions as root outside a sandbox. export IS_SANDBOX=1 @@ -38,7 +20,9 @@ export IS_SANDBOX=1 # Stream events so the runner sees output and does not kill the job as a zombie. STREAM_FILE="$(mktemp)" -claude --print --dangerously-skip-permissions --model claude-fable-5 \ +# The reviewer token is unset so the agent cannot approve as the bot. +env -u REVIEWER_TOKEN \ + claude --print --dangerously-skip-permissions --model claude-fable-5 \ --output-format stream-json --verbose "${FINAL_PROMPT}" \ | tee "${STREAM_FILE}" \ | jq -r --unbuffered '.message.content[]? | .thinking // .text // .name // empty' @@ -46,4 +30,4 @@ claude --print --dangerously-skip-permissions --model claude-fable-5 \ jq -r 'select(.type == "result") | .result // ("Bot failed: " + .subtype)' "${STREAM_FILE}" \ | ansifilter > "${COMMENT_FILE}" -post_comment +post_result diff --git a/scripts/run-codex.sh b/scripts/run-codex.sh index 1b31739..6dde422 100644 --- a/scripts/run-codex.sh +++ b/scripts/run-codex.sh @@ -1,20 +1,9 @@ #!/usr/bin/env bash set -euo pipefail -git config --global user.name bot -git config --global user.email noreply@capsulizers.com +. "${ACTION_PATH}/scripts/lib.sh" -COMMENT_FILE="$(mktemp)" - -post_comment() { - COMMENT_JSON="$(jq -n --rawfile body "${COMMENT_FILE}" '{body: $body}')" - curl --fail-with-body --silent --show-error \ - -X POST \ - -H "Authorization: token ${GITEA_TOKEN}" \ - -H "Content-Type: application/json" \ - --data "${COMMENT_JSON}" \ - "${GITEA_API_URL}/repos/${GITEA_REPOSITORY}/issues/${ISSUE_INDEX}/comments" -} +configure_git_author if ! codex login status > /dev/null 2>&1; then codex login --device-auth 2>&1 | ansifilter > "${COMMENT_FILE}" & @@ -26,18 +15,13 @@ if ! codex login status > /dev/null 2>&1; then post_comment fi -export ISSUE_COMMENTS="$( - curl --fail-with-body --silent --show-error \ - -H "Authorization: token ${GITEA_TOKEN}" \ - "${GITEA_API_URL}/repos/${GITEA_REPOSITORY}/issues/${ISSUE_INDEX}/comments?limit=100" \ - | jq -r '.[] | "## " + .user.login + " at " + .created_at + "\n\n" + .body + "\n"' -)" +FINAL_PROMPT="$(render_prompt)" -FINAL_PROMPT="$(envsubst < "${ACTION_PATH}/scripts/prompt.md")" - -codex exec --model gpt-5.5 \ +# The reviewer token is unset so the agent cannot approve as the bot. +env -u REVIEWER_TOKEN \ + codex exec --model gpt-5.5 \ --dangerously-bypass-approvals-and-sandbox \ --output-last-message "${COMMENT_FILE}" \ "${FINAL_PROMPT}" -post_comment +post_result -- 2.52.0 From e65ac772f6b235d3346f0e13fca7a0687e662b9c Mon Sep 17 00:00:00 2001 From: Danny Kim Date: Sun, 13 Sep 2026 23:09:53 +0900 Subject: [PATCH 2/2] Rewrite the runner in TypeScript --- .gitea/workflows/check.yml | 17 +++ action.yml | 18 +-- scripts/prompt.md => prompt.md | 23 ++-- run.ts | 202 +++++++++++++++++++++++++++++++++ scripts/lib.sh | 58 ---------- scripts/run-claude.sh | 33 ------ scripts/run-codex.sh | 27 ----- 7 files changed, 243 insertions(+), 135 deletions(-) create mode 100644 .gitea/workflows/check.yml rename scripts/prompt.md => prompt.md (76%) create mode 100644 run.ts delete mode 100644 scripts/lib.sh delete mode 100644 scripts/run-claude.sh delete mode 100644 scripts/run-codex.sh diff --git a/.gitea/workflows/check.yml b/.gitea/workflows/check.yml new file mode 100644 index 0000000..293c973 --- /dev/null +++ b/.gitea/workflows/check.yml @@ -0,0 +1,17 @@ +name: Check + +on: + pull_request: + branches: [main] + +jobs: + deno: + runs-on: self-hosted + steps: + - uses: actions/checkout@v4 + + - run: apt-get update && apt-get install -y unzip + + - uses: denoland/setup-deno@v2 + + - run: deno fmt --check . && deno lint . && deno check run.ts diff --git a/action.yml b/action.yml index 99942e4..93d7572 100644 --- a/action.yml +++ b/action.yml @@ -7,13 +7,15 @@ inputs: required: true author-token: description: >- - Gitea token of the account that commits, pushes, and opens pull requests. - The agent sees it as `GITEA_TOKEN`. + Gitea token of the account that commits, pushes, and opens pull requests, + with the `read:user`, `write:issue`, and `write:repository` scopes. The + agent sees it as `GITEA_TOKEN`. required: true reviewer-token: description: >- Gitea token of the bot account that posts comments and pull request - reviews. Never exposed to the agent, so it must differ from the author. + reviews, with the `write:issue` and `write:repository` scopes. Never + exposed to the agent, so it must differ from the author. required: true bot-token: description: API key or token for the selected bot @@ -25,20 +27,18 @@ runs: # This step assumes this is `node:24-bookworm` container. - name: Install dependencies shell: bash - run: | - apt-get update - apt-get install -y --no-install-recommends gettext-base jq ansifilter - npm install -g @openai/codex @anthropic-ai/claude-code + run: npm install -g @openai/codex @anthropic-ai/claude-code deno - name: Run bot shell: bash - run: bash "${ACTION_PATH}/scripts/run-${{ inputs.bot-type }}.sh" + run: deno run -A "${ACTION_PATH}/run.ts" env: ACTION_PATH: ${{ gitea.action_path }} + BOT_TYPE: ${{ inputs.bot-type }} + BOT_TOKEN: ${{ inputs.bot-token }} GITEA_API_URL: ${{ gitea.api_url }} GITEA_REPOSITORY: ${{ gitea.repository }} GITEA_TOKEN: ${{ inputs.author-token }} REVIEWER_TOKEN: ${{ inputs.reviewer-token }} - BOT_TOKEN: ${{ inputs.bot-token }} EVENT_NAME: ${{ gitea.event_name }} ISSUE_INDEX: ${{ gitea.event.issue.number || gitea.event.pull_request.number }} COMMENT: ${{ toJSON(gitea.event.comment || gitea.event.review) }} diff --git a/scripts/prompt.md b/prompt.md similarity index 76% rename from scripts/prompt.md rename to prompt.md index 268afe1..4ed59b8 100644 --- a/scripts/prompt.md +++ b/prompt.md @@ -19,7 +19,9 @@ changes when it mentions `@bot` and approves otherwise. So include `@bot` with instructions to fix the findings exactly when changes are needed, and never mention `@bot` when the PR is ready. For UI changes, check that the result is aligned, clean, and pixel-perfect, and that included screenshots prove the -intended result was achieved. +intended result was achieved. Also check the whole repository, not only the +diff, against the code rules at the end of this prompt, and request changes for +every violation you find even when the diff did not cause it. For an `issue_comment` or `pull_request_review_comment` event, treat the `body` in the triggering comment payload below as the user's exact instruction. @@ -38,13 +40,14 @@ another run; always wait it out and complete the task before responding. Afterwards, pull the latest branch state before pushing. Skip this check when no code changes are needed. -Prefer minimal, correct changes. Run relevant checks or tests if practical. -Treat code changes as a request to create a pull request. If a prior bot pull -request already exists for this issue, update that same pull request instead of -creating a new one. When you create a pull request, include `@bot` in its body -and ask it to review the pull request. This is required for every pull request -you create. Finish by briefly reporting what changed and what tests ran. Wait -for Gitea Actions CI to complete, and fix any failures. +Prefer minimal, correct changes that follow the code rules at the end of this +prompt. Run relevant checks or tests if practical. Treat code changes as a +request to create a pull request. If a prior bot pull request already exists for +this issue, update that same pull request instead of creating a new one. When +you create a pull request, include `@bot` in its body and ask it to review the +pull request. This is required for every pull request you create. Finish by +briefly reporting what changed and what tests ran. Wait for Gitea Actions CI to +complete, and fix any failures. If you modify UI code, include Playwright screenshots in your PR or issue comments. If you need UI clarification, ask with screenshots when helpful. @@ -70,3 +73,7 @@ ${ISSUE_COMMENTS} - Use the `GITEA_TOKEN` environment variable for authenticated Gitea API calls and pushes. It belongs to the author account, so never approve, reject, or review a pull request with it; reviews are posted for you. + +# Code rules + +${CODE_RULES} diff --git a/run.ts b/run.ts new file mode 100644 index 0000000..355aa01 --- /dev/null +++ b/run.ts @@ -0,0 +1,202 @@ +// Runs a coding agent for one Gitea issue or pull request event and posts its +// final response back. The agent works as the author account through +// GITEA_TOKEN; everything this script posts goes through the reviewer token, +// so it appears as the bot account. +import { TextLineStream } from "jsr:@std/streams@1/text-line-stream"; + +type GiteaUser = { login: string; email: string }; +type GiteaComment = { user: GiteaUser; created_at: string; body: string }; + +const env = (name: string): string => { + const value = Deno.env.get(name); + if (value === undefined) throw new Error(`${name} is not set`); + return value; +}; + +const API = env("GITEA_API_URL"); +const REPO = env("GITEA_REPOSITORY"); +const INDEX = env("ISSUE_INDEX"); +const EVENT = env("EVENT_NAME"); +const AUTHOR_TOKEN = env("GITEA_TOKEN"); +const REVIEWER_TOKEN = env("REVIEWER_TOKEN"); +const RULES_PATH = "repos/commons/code-rules/raw/README.md"; + +// The reviewer token is withheld so the agent cannot approve as the bot. +const { REVIEWER_TOKEN: _, ...agentEnv } = Deno.env.toObject(); + +async function gitea( + token: string, + path: string, + body?: unknown, +): Promise { + const response = await fetch(`${API}/${path}`, { + method: body === undefined ? "GET" : "POST", + headers: { + Authorization: `token ${token}`, + "Content-Type": "application/json", + }, + body: body === undefined ? undefined : JSON.stringify(body), + }); + if (!response.ok) { + throw new Error(`${path}: ${response.status} ${await response.text()}`); + } + return response; +} + +const ANSI = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*[A-Za-z]`, "g"); +const stripAnsi = (text: string): string => text.replace(ANSI, ""); + +async function postComment(body: string): Promise { + await gitea(REVIEWER_TOKEN, `repos/${REPO}/issues/${INDEX}/comments`, { + body: stripAnsi(body), + }); +} + +// A pull request event is a review request, so the response becomes a review: +// changes are requested when the agent asked @bot to fix something, a failed +// run only comments, and anything else approves. +async function postResult(body: string): Promise { + if (EVENT !== "pull_request") return postComment(body); + const event = body.includes("@bot") + ? "REQUEST_CHANGES" + : body.startsWith("Bot failed:") + ? "COMMENT" + : "APPROVED"; + await gitea(REVIEWER_TOKEN, `repos/${REPO}/pulls/${INDEX}/reviews`, { + body: stripAnsi(body), + event, + }); +} + +// Commits belong to the same account as the pull request they end up in. +async function configureGitAuthor(): Promise { + const user: GiteaUser = await (await gitea(AUTHOR_TOKEN, "user")).json(); + for (const [key, value] of [["name", user.login], ["email", user.email]]) { + await new Deno.Command("git", { + args: ["config", "--global", `user.${key}`, value], + }).output(); + } +} + +async function renderPrompt(): Promise { + const comments: GiteaComment[] = await (await gitea( + REVIEWER_TOKEN, + `repos/${REPO}/issues/${INDEX}/comments?limit=100`, + )).json(); + const values: Record = { + COMMENT: env("COMMENT"), + ISSUE_COMMENTS: comments + .map((c) => `## ${c.user.login} at ${c.created_at}\n\n${c.body}\n`) + .join("\n"), + CODE_RULES: await (await gitea(REVIEWER_TOKEN, RULES_PATH)).text(), + EVENT_NAME: EVENT, + GITEA_API_URL: API, + GITEA_REPOSITORY: REPO, + ISSUE_INDEX: INDEX, + }; + const template = await Deno.readTextFile( + new URL("prompt.md", import.meta.url), + ); + return template.replace( + /\$\{(\w+)\}/g, + (match, name) => values[name] ?? match, + ); +} + +async function runClaude(prompt: string): Promise { + const token = Deno.env.get("BOT_TOKEN"); + if (!token) { + await postComment( + "Run `claude setup-token` locally and set the `bot-token` action input.", + ); + Deno.exit(1); + } + const claude = new Deno.Command("claude", { + args: [ + "--print", + "--dangerously-skip-permissions", + "--model", + "claude-fable-5", + "--output-format", + "stream-json", + "--verbose", + prompt, + ], + // Claude refuses --dangerously-skip-permissions as root outside a sandbox. + env: { ...agentEnv, CLAUDE_CODE_OAUTH_TOKEN: token, IS_SANDBOX: "1" }, + clearEnv: true, + stdout: "piped", + }).spawn(); + let result = "Bot failed: no result"; + // Print events as they stream so the runner does not kill the job as a zombie. + const lines = claude.stdout + .pipeThrough(new TextDecoderStream()) + .pipeThrough(new TextLineStream()); + for await (const line of lines) { + if (!line) continue; + const event = JSON.parse(line); + for (const part of event.message?.content ?? []) { + const text = part.thinking ?? part.text ?? part.name; + if (text) console.log(text); + } + if (event.type === "result") { + result = event.result ?? `Bot failed: ${event.subtype}`; + } + } + await claude.status; + return result; +} + +// Posts the device code so a human can finish the login on the persisted home. +async function codexDeviceLogin(): Promise { + const login = new Deno.Command("codex", { + args: ["login", "--device-auth"], + stdout: "piped", + stderr: "piped", + }).spawn(); + let shown = ""; + const collect = (stream: ReadableStream): Promise => + stream.pipeThrough(new TextDecoderStream()).pipeTo( + new WritableStream({ write: (chunk) => void (shown += chunk) }), + ); + const drained = Promise.all([collect(login.stdout), collect(login.stderr)]); + await new Promise((resolve) => setTimeout(resolve, 3000)); + await postComment(shown); + await Promise.all([login.status, drained]); + const status = await new Deno.Command("codex", { args: ["login", "status"] }) + .output(); + await postComment(new TextDecoder().decode(status.stdout)); +} + +async function runCodex(prompt: string): Promise { + const loggedIn = await new Deno.Command("codex", { + args: ["login", "status"], + }) + .output(); + if (!loggedIn.success) await codexDeviceLogin(); + const file = await Deno.makeTempFile(); + const status = await new Deno.Command("codex", { + args: [ + "exec", + "--model", + "gpt-5.5", + "--dangerously-bypass-approvals-and-sandbox", + "--output-last-message", + file, + prompt, + ], + env: agentEnv, + clearEnv: true, + stdout: "inherit", + stderr: "inherit", + }).spawn().status; + if (!status.success) throw new Error(`codex exited with ${status.code}`); + return await Deno.readTextFile(file); +} + +await configureGitAuthor(); +const prompt = await renderPrompt(); +const result = env("BOT_TYPE") === "claude" + ? await runClaude(prompt) + : await runCodex(prompt); +await postResult(result); diff --git a/scripts/lib.sh b/scripts/lib.sh deleted file mode 100644 index d9ec127..0000000 --- a/scripts/lib.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env bash -# Shared by the run-*.sh scripts. Everything the scripts post goes through the -# reviewer token, so it appears as the bot account. The agent only ever sees the -# author token as GITEA_TOKEN, which is what pushes and opens pull requests. - -ISSUE_URL="${GITEA_API_URL}/repos/${GITEA_REPOSITORY}/issues/${ISSUE_INDEX}" -PULL_URL="${GITEA_API_URL}/repos/${GITEA_REPOSITORY}/pulls/${ISSUE_INDEX}" -COMMENT_FILE="$(mktemp)" - -gitea_api() { - curl --fail-with-body --silent --show-error \ - -H "Authorization: token ${1}" \ - -H "Content-Type: application/json" \ - "${@:2}" -} - -# Commits are made by the author account so the pull request and its commits -# belong to the same person. -configure_git_author() { - AUTHOR="$(gitea_api "${GITEA_TOKEN}" "${GITEA_API_URL}/user")" - git config --global user.name "$(jq -r '.login' <<< "${AUTHOR}")" - git config --global user.email \ - "$(jq -r '.email // empty' <<< "${AUTHOR}")" -} - -post_comment() { - gitea_api "${REVIEWER_TOKEN}" -X POST \ - --data "$(jq -n --rawfile body "${COMMENT_FILE}" '{body: $body}')" \ - "${ISSUE_URL}/comments" -} - -# A pull request event is a review request, so the response becomes a review -# rather than a comment: it requests changes when the agent asked @bot to fix -# something, only comments when the run failed, and approves otherwise. -post_result() { - if [ "${EVENT_NAME}" != pull_request ]; then - post_comment - return - fi - local event=APPROVED - if grep -q '@bot' "${COMMENT_FILE}"; then - event=REQUEST_CHANGES - elif grep -q '^Bot failed:' "${COMMENT_FILE}"; then - event=COMMENT - fi - gitea_api "${REVIEWER_TOKEN}" -X POST \ - --data "$(jq -n --rawfile body "${COMMENT_FILE}" --arg event "${event}" \ - '{body: $body, event: $event}')" \ - "${PULL_URL}/reviews" -} - -render_prompt() { - export ISSUE_COMMENTS="$( - gitea_api "${REVIEWER_TOKEN}" "${ISSUE_URL}/comments?limit=100" \ - | jq -r '.[] | "## " + .user.login + " at " + .created_at + "\n\n" + .body + "\n"' - )" - envsubst < "${ACTION_PATH}/scripts/prompt.md" -} diff --git a/scripts/run-claude.sh b/scripts/run-claude.sh deleted file mode 100644 index 9ae0690..0000000 --- a/scripts/run-claude.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -. "${ACTION_PATH}/scripts/lib.sh" - -configure_git_author - -if [ -z "${BOT_TOKEN:-}" ]; then - echo 'Run `claude setup-token` locally and set the `bot-token` action input.' > "${COMMENT_FILE}" - post_comment - exit 1 -fi -export CLAUDE_CODE_OAUTH_TOKEN="${BOT_TOKEN}" - -FINAL_PROMPT="$(render_prompt)" - -# Claude refuses --dangerously-skip-permissions as root outside a sandbox. -export IS_SANDBOX=1 - -# Stream events so the runner sees output and does not kill the job as a zombie. -STREAM_FILE="$(mktemp)" - -# The reviewer token is unset so the agent cannot approve as the bot. -env -u REVIEWER_TOKEN \ - claude --print --dangerously-skip-permissions --model claude-fable-5 \ - --output-format stream-json --verbose "${FINAL_PROMPT}" \ - | tee "${STREAM_FILE}" \ - | jq -r --unbuffered '.message.content[]? | .thinking // .text // .name // empty' - -jq -r 'select(.type == "result") | .result // ("Bot failed: " + .subtype)' "${STREAM_FILE}" \ - | ansifilter > "${COMMENT_FILE}" - -post_result diff --git a/scripts/run-codex.sh b/scripts/run-codex.sh deleted file mode 100644 index 6dde422..0000000 --- a/scripts/run-codex.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -. "${ACTION_PATH}/scripts/lib.sh" - -configure_git_author - -if ! codex login status > /dev/null 2>&1; then - codex login --device-auth 2>&1 | ansifilter > "${COMMENT_FILE}" & - LOGIN_PID="${!}" - sleep 3 - post_comment - wait "${LOGIN_PID}" - codex login status 2>&1 | ansifilter > "${COMMENT_FILE}" - post_comment -fi - -FINAL_PROMPT="$(render_prompt)" - -# The reviewer token is unset so the agent cannot approve as the bot. -env -u REVIEWER_TOKEN \ - codex exec --model gpt-5.5 \ - --dangerously-bypass-approvals-and-sandbox \ - --output-last-message "${COMMENT_FILE}" \ - "${FINAL_PROMPT}" - -post_result -- 2.52.0