Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
109 changes: 109 additions & 0 deletions .github/actions/classify-complexity/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
name: Classify task complexity with Haiku
description: >
Ask Claude Haiku to classify a task as 'simple' or 'complex' and pick a model
pair (primary + fallback). Favours opus — sonnet is only chosen when Haiku
explicitly says 'simple'; any other output routes to opus.

inputs:
oauth-token:
description: Claude Code OAuth token.
required: true
prompt:
description: >
Full classification prompt. Must instruct Haiku to reply with EXACTLY
one lowercase word — 'simple' or 'complex' — and nothing else.
required: true

outputs:
model:
description: Selected primary model.
value: ${{ steps.classify.outputs.model }}
fallback:
description: Fallback model.
value: ${{ steps.classify.outputs.fallback }}
classification:
description: Raw classification string ('simple', 'complex', or fallback on empty/garbage).
value: ${{ steps.classify.outputs.classification }}

runs:
using: composite
steps:
- name: Install Claude CLI
shell: bash
run: |
if ! command -v claude >/dev/null 2>&1; then
curl -fsSL https://claude.ai/install.sh | bash
Comment thread
abnegate marked this conversation as resolved.
Outdated
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
fi

- name: Run Haiku classification
id: classify
shell: bash
env:
CLAUDE_CODE_OAUTH_TOKEN: ${{ inputs.oauth-token }}
CLASSIFY_PROMPT: ${{ inputs.prompt }}
run: |
set +e

# Force the model to emit a JSON object matching this schema — removes
# any chance of leading/trailing prose, punctuation, or casing drift.
SCHEMA='{
"type": "object",
"properties": {
"classification": {
"type": "string",
"enum": ["simple", "complex"]
}
},
"required": ["classification"],
"additionalProperties": false
}'

RAW=$(claude -p \
--model claude-haiku-4-5 \
--bare \
--max-turns 1 \
--tools "" \
--dangerously-skip-permissions \
--output-format json \
--json-schema "$SCHEMA" \
"$CLASSIFY_PROMPT")
STATUS=$?

if [[ $STATUS -ne 0 ]]; then
echo "claude CLI exited $STATUS — routing to opus."
echo "CLI output: $RAW"
fi

# With --json-schema the structured payload lands at .structured_output as a
# parsed object. Older builds (or runs without schema) put the response at
# .result as either a plain string or a stringified JSON. Try both so the
# action keeps working across CLI versions.
CLASSIFICATION=$(printf '%s' "$RAW" | jq -r '
(.structured_output.classification // "") as $s
| if $s != "" then $s
else ((.result // "") | fromjson? | .classification // "")
end
' 2>/dev/null)

# Last-ditch fallback: plain .result that happens to be the literal word.
if [[ -z "$CLASSIFICATION" ]]; then
CLASSIFICATION=$(printf '%s' "$RAW" | jq -r '.result // empty' 2>/dev/null \
| tr -d '[:space:]' | tr '[:upper:]' '[:lower:]' | head -c 20)
fi

echo "Haiku classification: '$CLASSIFICATION'"

# Favour opus, sonnet only when haiku explicitly says simple.
if [[ "$CLASSIFICATION" == "simple" ]]; then
MODEL="claude-sonnet-4-6"
FALLBACK="claude-opus-4-7"
else
MODEL="claude-opus-4-7"
FALLBACK="claude-sonnet-4-6"
fi

echo "classification=$CLASSIFICATION" >> "$GITHUB_OUTPUT"
echo "model=$MODEL" >> "$GITHUB_OUTPUT"
echo "fallback=$FALLBACK" >> "$GITHUB_OUTPUT"
echo "Selected primary=$MODEL, fallback=$FALLBACK"
113 changes: 113 additions & 0 deletions .github/workflows/claude-comments.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
name: Claude Code

# Only triggers that carry comment bodies worth @claude-scanning.
# pull_request_review is excluded: it fires on every review submit (even bare
# approvals), so the @claude filter would show a "Skipped" check in the PR jobs
# list every time. Inline review comments still fire through
# pull_request_review_comment, so we don't lose the typical reviewer flow.
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned]

# For PR review events (where head.ref is available), share the branch queue
# with claude-improvement and claude-healing so we never push concurrently to
# the same head. For issue_comment / issues events, fall back to a per-issue
# key — can't resolve the branch at concurrency-evaluation time without an API
# call, but at least successive @claude comments on one PR/issue serialize.
concurrency:
group: claude-pr-${{ github.event.pull_request.head.ref || github.event.issue.number || github.run_id }}
cancel-in-progress: false

jobs:
claude:
# Gate on author association: Claude runs with --dangerously-skip-permissions
# and contents: write, so user-controlled prompt content from untrusted
# commenters would be a prompt-injection vector. Only trusted associations
# (repo owner, org member, collaborator) can invoke @claude.
if: |
(
github.event.comment.author_association == 'OWNER' ||
github.event.comment.author_association == 'MEMBER' ||
github.event.comment.author_association == 'COLLABORATOR' ||
github.event.issue.author_association == 'OWNER' ||
github.event.issue.author_association == 'MEMBER' ||
github.event.issue.author_association == 'COLLABORATOR'
) && (
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
)
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: write
id-token: write
actions: read
steps:
- name: Resolve PR head ref
id: ref
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
PR_HEAD_REF: ${{ github.event.pull_request.head.ref }}
ISSUE_PR_URL: ${{ github.event.issue.pull_request.url }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
run: |
# pull_request_review* events expose head.ref directly.
# issue_comment events on a PR expose issue.pull_request.url; resolve
# the branch via gh. Plain issues have neither → stay on default branch.
if [[ -n "$PR_HEAD_REF" ]]; then
ref="$PR_HEAD_REF"
elif [[ -n "$ISSUE_PR_URL" ]]; then
ref=$(gh pr view "$ISSUE_NUMBER" --repo "$REPO" --json headRefName --jq .headRefName)
else
ref=""
fi
echo "ref=$ref" >> "$GITHUB_OUTPUT"
echo "Resolved checkout ref: '${ref:-<default branch>}'"

- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ steps.ref.outputs.ref }}
fetch-depth: 20

- name: Classify task complexity with Haiku
id: classify
uses: ./.github/actions/classify-complexity
with:
oauth-token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
prompt: |
Classify this coding task's complexity. Reply with EXACTLY one lowercase word and nothing else: 'simple' or 'complex'.

simple = typo fix, docs tweak, one-file obvious bug, rename, trivial refactor within a single function
complex = multi-file change, new feature, architecture or API change, deep debugging, performance work, anything with unclear scope or touching more than ~3 files

TITLE: ${{ github.event.issue.title }}

REQUEST:
${{ github.event.comment.body || github.event.issue.body }}

- name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@b4d67413279fc18c6e5de930ae307c4f108714eb # v1.0.104
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
plugin_marketplaces: |
https://github.com/anthropics/claude-code.git
https://github.com/abnegate/claudes.git
plugins: 'skills@claudes'
additional_permissions: |
actions: read
use_sticky_comment: true
use_commit_signing: true
exclude_comments_by_actor: 'dependabot[bot],renovate[bot]'
claude_args: |
--model ${{ steps.classify.outputs.model }}
--fallback-model ${{ steps.classify.outputs.fallback }}
--dangerously-skip-permissions
143 changes: 143 additions & 0 deletions .github/workflows/claude-healing.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
name: Claude CI Healing

on:
workflow_run:
workflows: [Tests]
types: [completed]

# Shared with claude-improvement and claude-comments: all three mutate the PR
# branch (commit + push), so they must not run simultaneously. Keying on head
# branch serializes runs targeting the same PR into a single queue.
concurrency:
group: claude-pr-${{ github.event.workflow_run.head_branch }}
cancel-in-progress: false

jobs:
fix-failures:
# Only fire on PR failures (not push-to-main) and skip fork PRs. Blocking
# fork PRs also mitigates prompt injection via CI logs — only collaborators
# with push access can introduce branch content that would feed the prompt.
if: >
github.event.workflow_run.conclusion == 'failure' &&
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.head_repository.full_name == github.repository
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: read
actions: read
checks: read
id-token: write

steps:
- name: Find the PR
id: pr
env:
GH_TOKEN: ${{ github.token }}
HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
run: |
pr_number=$(gh pr list --repo "$GITHUB_REPOSITORY" \
--head "$HEAD_BRANCH" --state open --json number --jq '.[0].number')
if [ -z "$pr_number" ]; then
echo "No open PR found — skipping."
echo "skip=true" >> "$GITHUB_OUTPUT"
else
echo "number=$pr_number" >> "$GITHUB_OUTPUT"
echo "skip=false" >> "$GITHUB_OUTPUT"
fi

- name: Checkout PR branch
if: steps.pr.outputs.skip != 'true'
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.workflow_run.head_branch }}
fetch-depth: 0

- name: Get failure logs
if: steps.pr.outputs.skip != 'true'
env:
GH_TOKEN: ${{ github.token }}
RUN_ID: ${{ github.event.workflow_run.id }}
run: |
gh run view "$RUN_ID" --log-failed 2>&1 | tail -200 > /tmp/ci-failure-logs.txt
echo "Captured $(wc -l < /tmp/ci-failure-logs.txt) lines of failure logs"

- name: Extract failure log excerpt
if: steps.pr.outputs.skip != 'true'
id: excerpt
run: |
{
echo 'log<<__EOF__'
tail -120 /tmp/ci-failure-logs.txt
echo '__EOF__'
} >> "$GITHUB_OUTPUT"
Comment thread
abnegate marked this conversation as resolved.
Outdated
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Outdated

- name: Classify failure complexity with Haiku
if: steps.pr.outputs.skip != 'true'
id: classify
uses: ./.github/actions/classify-complexity
with:
oauth-token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
prompt: |
Classify this CI failure's fix complexity. Reply with EXACTLY one lowercase word and nothing else: 'simple' or 'complex'.

simple = lint violation, formatting, obvious typo, missing import, trivial test assertion fix, single-file obvious mistake
complex = compile error with unclear cause, real test failure exposing a bug, flaky infrastructure, multi-file breakage, anything with unclear root cause or scope

FAILURE LOGS (tail):
${{ steps.excerpt.outputs.log }}

- name: Claude fixes CI failures
if: steps.pr.outputs.skip != 'true'
uses: anthropics/claude-code-action@b4d67413279fc18c6e5de930ae307c4f108714eb # v1.0.104
env:
PR_NUMBER: ${{ steps.pr.outputs.number }}
FAILED_RUN_ID: ${{ github.event.workflow_run.id }}
HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
plugin_marketplaces: |
https://github.com/anthropics/claude-code.git
https://github.com/abnegate/claudes.git
plugins: 'skills@claudes'
use_sticky_comment: 'false'
use_commit_signing: 'true'
show_full_output: 'true'
claude_args: |
--model ${{ steps.classify.outputs.model }}
--fallback-model ${{ steps.classify.outputs.fallback }}
--dangerously-skip-permissions
prompt: |
CI failed on PR #$PR_NUMBER (run $FAILED_RUN_ID).

The failure logs are at /tmp/ci-failure-logs.txt. Read them first.

## Coordination with the code reviewer
The `claude-improvement` and `claude-comments` workflows also push to
this PR branch. We share a concurrency group (branch-keyed), so only one
of us runs at a time — but the other may have pushed fix commits between
the failing CI run and this job starting. Before STEP 6,
`git fetch origin` and `git pull --rebase origin $HEAD_BRANCH`. If rebase
conflicts, resolve them (prefer the other workflow's changes unless they
directly contradict your CI fix), then continue. After rebasing, re-check
whether the CI failure is still reproducible — the other workflow may have
already fixed it, in which case post a comment saying so and STOP without
pushing.

Your job:
1. Read the failure logs to identify the root cause (compile error, test failure, lint violation, etc.)
2. Read the project's CLAUDE.md for build/test/lint commands and conventions
3. Fix the issue directly in the codebase
4. Verify your fix by re-running the failed step (use whatever build system the project uses)
5. Post a brief PR comment explaining what failed and what you fixed:
`gh pr comment $PR_NUMBER --repo $GITHUB_REPOSITORY --body "..."`
6. `git fetch origin && git pull --rebase origin $HEAD_BRANCH`, then commit with
message: `(fix): CI — <short description>`
7. Push to the PR branch

Rules:
- Only fix the CI failure. Don't refactor, clean up, or improve other code.
- If the failure is a flaky test (passes on retry), just re-run it and comment that it was flaky.
- If you can't determine the root cause, post a comment asking for help instead of guessing.
- Never push to main. Only push to the PR head branch.
Loading
Loading