Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions .github/scripts/first-pass.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// Controlled first-pass reply for @ccu-bot — ONE model call, then post ONE
// comment. Deliberately NOT an autonomous agent: it uses no tools and never
// runs anything from the issue/PR, so a malicious public issue cannot steer it
// into misusing the token or leaking the key (prompt-injection safe). It only
// reads the project docs (+ PR diff), asks the model once, and comments.
//
// Env in: GH_TOKEN, REPO (owner/name), EVENT_KIND (issue|pr), ITEM_NUMBER,
// ITEM_TITLE, ITEM_BODY, DIFF_FILE (pr only), ANTHROPIC_API_KEY,
// ANTHROPIC_BASE_URL (third-party OK), CCU_BOT_MODEL.

import { readFileSync, existsSync } from 'node:fs';

const env = process.env;
const base = (env.ANTHROPIC_BASE_URL || 'https://api.anthropic.com').replace(/\/$/, '');
const model = env.CCU_BOT_MODEL || 'deepseek-v4-flash';
const isPr = env.EVENT_KIND === 'pr';
const num = env.ITEM_NUMBER;

const readDoc = (path, max = 12000) => {
try {
return existsSync(path) ? readFileSync(path, 'utf8').slice(0, max) : '';
} catch {
return '';
}
};
const docs =
`# ARCHITECTURE.md\n${readDoc('ARCHITECTURE.md')}\n\n` +
`# CLAUDE.md\n${readDoc('CLAUDE.md')}\n\n` +
`# CONTRIBUTING.md\n${readDoc('CONTRIBUTING.md')}`;
let diff = '';
if (isPr && env.DIFF_FILE) {
diff = readDoc(env.DIFF_FILE, 40000);
}

const kind = isPr ? 'review' : 'reply';
const system = [
'You are the ClaudeCodeUsage repository assistant, replying via Claude Code tooling.',
'The underlying model may be a third-party Anthropic-format model, so do not claim to be Anthropic\'s Claude specifically.',
`Write ONE concise, concrete first-pass ${kind} for the ${isPr ? 'pull request' : 'issue'} below.`,
isPr
? '- Cover correctness risks, convention/i18n/CHANGELOG gaps, and merge-readiness, grounded in the diff + docs.'
: '- Say what you understand the request to be, where in the architecture it relates, and a concrete suggested direction OR a specific clarifying question.',
`- Answer ONLY from the provided project docs${isPr ? ' and diff' : ''}. If they are insufficient, say exactly what is unclear and ask a specific question — NEVER guess or invent facts about the code.`,
'- Reply in the SAME language the author wrote in. Be specific and concise; no filler, no AI-flavoured padding.',
`- Start the comment with: "🤖 Automated first-pass ${kind} (via Claude Code)".`,
`- End with, on its own line: "_This is model-generated from the repository docs and the ${isPr ? 'PR diff' : 'issue'} — not a final decision. A maintainer reviews everything._"`,
].join('\n');

const userText = isPr
? `PR #${num}: ${env.ITEM_TITLE}\n\n${env.ITEM_BODY || '(no description)'}\n\n--- DIFF (truncated) ---\n${diff}\n\n--- PROJECT DOCS ---\n${docs}`
: `Issue #${num}: ${env.ITEM_TITLE}\n\n${env.ITEM_BODY || '(no body)'}\n\n--- PROJECT DOCS ---\n${docs}`;

const fail = (msg) => {
console.error(msg);
process.exit(1);
};

// 1) One model call (Anthropic Messages format; works against a third-party
// Anthropic-compatible endpoint via ANTHROPIC_BASE_URL).
let reply = '';
try {
const res = await fetch(`${base}/v1/messages`, {
method: 'POST',
headers: {
'content-type': 'application/json',
'x-api-key': env.ANTHROPIC_API_KEY,
'anthropic-version': '2023-06-01',
},
body: JSON.stringify({
model,
max_tokens: 1200,
system,
messages: [{ role: 'user', content: userText }],
}),
});
if (!res.ok) {
fail(`Model API error ${res.status}: ${(await res.text()).slice(0, 500)}`);
}
const data = await res.json();
reply = (data.content || [])
.filter((b) => b && b.type === 'text' && typeof b.text === 'string')
.map((b) => b.text)
.join('')
.trim();
} catch (e) {
fail(`Model call failed: ${e.message}`);
}
if (!reply) {
fail('Empty model reply.');
}

// 2) Post ONE comment via the GitHub API (issues + PRs share this endpoint).
const [owner, repo] = (env.REPO || '/').split('/');
try {
const res = await fetch(`https://api.github.com/repos/${owner}/${repo}/issues/${num}/comments`, {
method: 'POST',
headers: {
authorization: `Bearer ${env.GH_TOKEN}`,
accept: 'application/vnd.github+json',
'content-type': 'application/json',
'user-agent': 'ccu-bot',
},
body: JSON.stringify({ body: reply }),
});
if (!res.ok) {
fail(`Comment post failed ${res.status}: ${(await res.text()).slice(0, 300)}`);
}
} catch (e) {
fail(`Comment post failed: ${e.message}`);
}
console.log(`Posted first-pass ${kind} on #${num}.`);
67 changes: 21 additions & 46 deletions .github/workflows/issue-first-pass.yml
Original file line number Diff line number Diff line change
@@ -1,26 +1,22 @@
# Auto first-pass reply on every NEW issue (V2.2 automation — Carl's opt-in).
# Auto first-pass reply on every NEW issue — a CONTROLLED single model call
# (not an autonomous agent). See .github/scripts/first-pass.mjs: it reads the
# project docs, asks the model once, and posts one comment. No tools run, so a
# malicious public issue can't steer it into misusing the token or key.
#
# When an issue is opened, the assistant reads the project's architecture docs
# and posts ONE helpful first-pass comment: what it understands, where in the
# code/architecture it points, and either a suggested direction or a clarifying
# question. Every comment is clearly labelled automated and ends with a
# disclaimer that a maintainer follows up.
#
# SAFE BY DEFAULT — inert until the owner sets:
# 1. Repo variable AUTOMATION_ENABLED = 'true' (kill switch)
# 2. Repo secret ANTHROPIC_API_KEY (official or third-party key)
# Optional: repo variable ANTHROPIC_BASE_URL for a third-party Anthropic-format
# endpoint. Bot-authored issues are skipped (no loops).
# INERT until the owner sets:
# AUTOMATION_ENABLED = 'true' (repo variable, kill switch)
# ANTHROPIC_API_KEY (repo secret — official or third-party key)
# Optional repo variables: ANTHROPIC_BASE_URL (third-party Anthropic-format
# endpoint), CCU_BOT_MODEL (defaults to deepseek-v4-flash).
name: Issue first-pass (auto)

on:
issues:
types: [opened]

permissions:
contents: read # read repo files to answer accurately (never guess)
issues: write # post the reply
id-token: write # claude-code-action mints its GitHub token via OIDC
contents: read # read the repo docs the reply is grounded in
issues: write # post the comment

concurrency:
group: issue-first-pass-${{ github.event.issue.number }}
Expand All @@ -30,43 +26,22 @@ jobs:
reply:
if: vars.AUTOMATION_ENABLED == 'true' && github.event.issue.user.type != 'Bot'
runs-on: ubuntu-latest
timeout-minutes: 12
timeout-minutes: 8
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 1

- name: First-pass reply
uses: anthropics/claude-code-action@v1
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
EVENT_KIND: issue
ITEM_NUMBER: ${{ github.event.issue.number }}
ITEM_TITLE: ${{ github.event.issue.title }}
ITEM_BODY: ${{ github.event.issue.body }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
ANTHROPIC_BASE_URL: ${{ vars.ANTHROPIC_BASE_URL }}
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
# Use the workflow token so no Claude GitHub App install is needed
# (we drive a third-party model; the app is only Anthropic's GitHub auth).
github_token: ${{ github.token }}
claude_args: "--max-turns 15"
prompt: |
You are the ClaudeCodeUsage repository assistant, running via Claude Code
(the model may be a third-party Anthropic-format model, so do not claim to
be Anthropic's Claude specifically).

A new issue was just opened (title/body in the event context). Post ONE
comment as a first-pass response. Rules:
- First read CLAUDE.md and CONTRIBUTING.md (and ARCHITECTURE.md if present)
for context.
- If those docs are NOT enough to answer accurately, READ the relevant
repository files to find the real answer. Never guess or invent an
answer — if you still can't determine it, say what's unclear and ask a
specific clarifying question instead of speculating.
- Be concrete and concise: what you understand the request to be, where in
the code/architecture it relates, and a suggested direction OR the
clarifying question needed. No filler, no AI-flavoured padding.
- Reply in the same language the issue author used.
- START the comment with: "🤖 Automated first-pass reply (via Claude Code)".
- END the comment with this disclaimer on its own line:
"_This is model-generated from the repository's architecture docs and
the issue description — not a final decision. A maintainer will review
and follow up._"
- Do not change labels, close the issue, or open PRs. Just the one comment.
CCU_BOT_MODEL: ${{ vars.CCU_BOT_MODEL }}
run: node .github/scripts/first-pass.mjs
65 changes: 23 additions & 42 deletions .github/workflows/pr-first-pass.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
# Auto first-pass review on every NEW pull request (V2.2 automation — opt-in).
# Auto first-pass review on every NEW pull request — a CONTROLLED single model
# call (not an autonomous agent). Uses pull_request_target for the token but
# NEVER runs the PR's code: it checks out the BASE repo, fetches the diff via the
# API, asks the model once, and posts one comment. No tools → injection-safe.
#
# Uses pull_request_target so it has the token to comment, but it must NEVER run
# the PR's code with that token. We check out the BASE repo (the default for
# pull_request_target), read the PR diff via the API, and post ONE first-pass
# review comment. Fork code is never executed here.
#
# SAFE BY DEFAULT — inert until the owner sets AUTOMATION_ENABLED='true' and the
# ANTHROPIC_API_KEY secret (official or third-party). Bot-authored PRs skipped.
# INERT until AUTOMATION_ENABLED = 'true' + the ANTHROPIC_API_KEY secret.
# Optional repo vars: ANTHROPIC_BASE_URL, CCU_BOT_MODEL (default deepseek-v4-flash).
name: PR first-pass (auto)

on:
Expand All @@ -16,55 +14,38 @@ on:
permissions:
contents: read
pull-requests: write
id-token: write # claude-code-action mints its GitHub token via OIDC

concurrency:
group: pr-first-pass-${{ github.event.pull_request.number }}
cancel-in-progress: false

jobs:
review:
# Skip bots and huge diffs (a > ~3000-line PR wants a human first).
if: vars.AUTOMATION_ENABLED == 'true' && github.event.pull_request.user.type != 'Bot'
runs-on: ubuntu-latest
timeout-minutes: 12
timeout-minutes: 8
steps:
# Base repo only — do NOT check out or run the PR head with the token.
# Base repo only — never checks out or runs the PR head with the token.
- name: Checkout base repository
uses: actions/checkout@v6
with:
fetch-depth: 1

- name: Fetch PR diff (via API, no code run)
env:
GH_TOKEN: ${{ github.token }}
run: gh pr diff ${{ github.event.pull_request.number }} --repo ${{ github.repository }} > /tmp/pr.diff || true

- name: First-pass review
uses: anthropics/claude-code-action@v1
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
EVENT_KIND: pr
ITEM_NUMBER: ${{ github.event.pull_request.number }}
ITEM_TITLE: ${{ github.event.pull_request.title }}
ITEM_BODY: ${{ github.event.pull_request.body }}
DIFF_FILE: /tmp/pr.diff
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
ANTHROPIC_BASE_URL: ${{ vars.ANTHROPIC_BASE_URL }}
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
# Use the workflow token so no Claude GitHub App install is needed
# (we drive a third-party model; the app is only Anthropic's GitHub auth).
github_token: ${{ github.token }}
claude_args: "--max-turns 15"
prompt: |
You are the ClaudeCodeUsage repository assistant, running via Claude Code
(the model may be a third-party Anthropic-format model; do not claim to be
Anthropic's Claude specifically).

A new pull request was opened. Do a first-pass review and post ONE comment.
SECURITY: never execute the PR's code. Read the PR DIFF via the GitHub API
and the repository's own files only.
Rules:
- First read CLAUDE.md and CONTRIBUTING.md (and ARCHITECTURE.md if present)
for the
conventions (compile-clean, node:test green, 7-language i18n, opt-in
defaults, CHANGELOG). If those aren't enough to judge accurately, read
the relevant base-repo files — never guess.
- Cover: correctness risks, convention/i18n/CHANGELOG gaps, and
merge-readiness. Be specific and concise; skip praise padding.
- If the diff is very large (> ~3000 lines) or you're unsure, say a human
should review rather than speculate.
- START with: "🤖 Automated first-pass review (via Claude Code)".
- END with: "_This is model-generated from the architecture docs and the
PR diff — not a merge decision. A maintainer reviews everything._"
- Do not approve, request changes as a formal review, merge, or push. One
comment only.
CCU_BOT_MODEL: ${{ vars.CCU_BOT_MODEL }}
run: node .github/scripts/first-pass.mjs
Loading