mirror of
https://github.com/alexbelgium/hassio-addons.git
synced 2026-08-14 09:02:30 +02:00
AI triage v2: plan-then-approve, manual precedence, ready PRs
Redesign the AI issue-triage pipeline so it can fix confidently on its own, ask for approval only when unsure, and always yield to manual actions — while staying cheap and fast. - Tier 2 (daily_ai_fix) becomes graded: high-confidence small fixes open a READY-for-review PR; anything medium/large gets a full Opus-written plan comment (ai:plan-pending) instead of a PR. - New Tier 3 (on_issue_approved): maintainer adds ai:approved and the posted plan is executed on Opus into a ready PR — immediate, zero cost until asked. - New @claude interactive workflow (on_claude_mention): maintainer-only, Sonnet-low, full precedence over the automated tiers. - New CodeRabbit follow-up (on_pr_coderabbit): one-shot Sonnet pass that fixes or replies to CodeRabbit's review of an ai-fix/* PR. - Tier 1 self-healing: a reporter's reply to a needs-info request re-runs classification exactly once (ai:needs-info); a daily catch-up re-dispatches any issue that never got triaged. - Extras: global kill switch (repo var AI_DISABLED), stale-bot exemption for AI labels, shared ai_guard_paths.sh, track_progress on the Opus tiers, CLAUDE.md documentation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
57
.github/prompts/issue-execute-plan.md
vendored
Normal file
57
.github/prompts/issue-execute-plan.md
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
# Execute an approved plan — tier 3
|
||||
|
||||
@alexbelgium reviewed an AI-written plan and approved it. Your job is to carry
|
||||
that plan out and open a pull request. The plan was already accepted, so do not
|
||||
re-litigate it — execute it. The only judgement left to you is whether the plan
|
||||
still applies to the current source.
|
||||
|
||||
Read:
|
||||
|
||||
- `/tmp/ai-exec/plan.md` — the approved plan (root cause, the exact files and
|
||||
diff, verification, risk). This is your spec.
|
||||
- `/tmp/ai-exec/issue.json` — the issue it fixes (for `Closes #<n>` and context).
|
||||
|
||||
## Hard limits (identical to the fix sweep — a workflow step enforces them)
|
||||
|
||||
1. **Never modify `.github/` or `.templates/`.** Repo-wide infrastructure.
|
||||
2. **Never touch the `version` or `upstream` fields in `config.yaml`.**
|
||||
3. **One add-on, one branch:** `ai-fix/<addon>-<issue-number>`.
|
||||
4. **Never merge, never close the issue, never enable auto-merge.** Open the
|
||||
pull request **ready for review** — CI (`onpr_check-pr.yaml`) validates it,
|
||||
a human ships it.
|
||||
|
||||
## Do this in order
|
||||
|
||||
1. **Apply the plan.** Make exactly the edits it describes. Match surrounding
|
||||
style (bash / Dockerfile; conventions vary per add-on). Run `shellcheck` on
|
||||
any shell you change. Add a `CHANGELOG.md` entry in the add-on's format.
|
||||
|
||||
2. **If the plan is stale** — the source moved since it was written and the diff
|
||||
no longer applies cleanly:
|
||||
- Small drift (a line shifted, a nearby rename): adapt minimally to achieve
|
||||
the plan's stated intent, and note the deviation in the PR body.
|
||||
- Large drift (the root cause or the target code is gone or now different):
|
||||
stop. Do **not** guess a new fix. Comment on the issue explaining why the
|
||||
plan no longer applies, relabel `ai:needs-human` (see step 5), open no PR.
|
||||
|
||||
3. **Open the pull request, ready for review.** Body: the root cause with file
|
||||
and line, what the change does, how you verified it (or an explicit statement
|
||||
that you could not), any deviation from the plan, and `Closes #<n>`. Note that
|
||||
it was executed from an approved plan.
|
||||
|
||||
4. **Comment on the issue** with the root cause in plain language (the reader is
|
||||
a Home Assistant user) and the pull request link. Close with a note that this
|
||||
is automated analysis pending Alex's review.
|
||||
|
||||
5. **Relabel, as your last action:**
|
||||
```
|
||||
gh issue edit <n> --remove-label ai:approved --remove-label ai:plan-pending --add-label <result>
|
||||
```
|
||||
`<result>` is `ai:fixed` if you opened a PR, or `ai:needs-human` if the plan
|
||||
was too stale to apply (step 2). A workflow step also strips the approval
|
||||
labels afterwards and flags `ai:needs-human` if no PR resulted — treat that
|
||||
as a bug in your run, not a safety net.
|
||||
|
||||
Keep the change within the spirit of the approved plan. If carrying it out
|
||||
honestly requires substantially more than the plan described, that is a sign the
|
||||
plan was wrong — stop and relabel `ai:needs-human` rather than expanding scope.
|
||||
147
.github/prompts/issue-fix.md
vendored
147
.github/prompts/issue-fix.md
vendored
@@ -7,6 +7,10 @@ application. You own the wrapper. You do not own the upstream app.
|
||||
Read `/tmp/ai-fix/batch.json`. Work add-on by add-on, not issue by issue —
|
||||
grouping is the point of the batch.
|
||||
|
||||
You are the Opus step of the pipeline: the diagnosis and, when a fix is not a
|
||||
sure thing, the written plan. Getting the diagnosis right and being honest
|
||||
about confidence matters more than the number of pull requests you open.
|
||||
|
||||
## Hard limits
|
||||
|
||||
These are not guidelines. A workflow step enforces them after you finish, and
|
||||
@@ -19,27 +23,18 @@ anything that violates them gets blocked and flagged.
|
||||
will not be around to resolve.
|
||||
3. **One add-on per branch, one branch per pull request.** Branch name
|
||||
`ai-fix/<addon>-<issue-number>`.
|
||||
4. **Draft pull requests only.** Never merge, never mark ready for review,
|
||||
never close an issue.
|
||||
5. If the fix requires changing more than roughly 60 lines, or touching more
|
||||
than three files, stop. Post the analysis, open no pull request, and say
|
||||
plainly that the change is too large for an unattended fix.
|
||||
6. **Relabel every issue before moving to the next one.** This sweep runs
|
||||
daily over the same `ai-triage`-labelled backlog. An issue you have
|
||||
already finished with must drop that label immediately — otherwise
|
||||
tomorrow's sweep re-selects it, opens a second competing branch, and
|
||||
burns another full read-the-source-and-fix pass on work that is already
|
||||
done. Do this as your last action on the issue, right after commenting,
|
||||
not batched at the end: `gh issue edit <n> --remove-label ai-triage
|
||||
--add-label <replacement>`, where `<replacement>` is exactly one of:
|
||||
- `ai:fixed` — you opened a draft pull request for it.
|
||||
- `ai:upstream` — you reversed tier 1's call and the fault is upstream,
|
||||
so no pull request.
|
||||
- `ai:needs-human` — too large for an unattended fix (rule 5), or you
|
||||
found no fix and are reporting what you ruled out.
|
||||
A workflow step checks this after you finish and force-corrects to
|
||||
`ai:needs-human` for anything still carrying `ai-triage` — treat that as
|
||||
a bug in your run, not a safety net to lean on.
|
||||
4. **Never merge, never close an issue, never enable auto-merge.** Opening a
|
||||
pull request for review is as far as you go — a human ships it. CI
|
||||
(`onpr_check-pr.yaml`: lint + Docker build) runs on every pull request you
|
||||
open and is what actually validates the change.
|
||||
5. **The small-fix ceiling is ~60 changed lines / 3 files.** A change under it,
|
||||
*and* with a root cause you are confident of, may go out as a ready pull
|
||||
request (Outcome A). Anything over it does not become a pull request — it
|
||||
becomes a plan (Outcome B), however confident you are.
|
||||
6. **Relabel every issue before moving to the next one** (see "Relabel", below).
|
||||
This sweep runs daily over the same `ai-triage` backlog; an issue you have
|
||||
finished with must drop that label immediately or tomorrow's sweep re-selects
|
||||
it and burns another full pass on work already done.
|
||||
|
||||
## Per add-on, do this in order
|
||||
|
||||
@@ -50,41 +45,101 @@ suddenly usually has a commit behind it, and finding that commit is worth more
|
||||
than reading the whole tree.
|
||||
|
||||
**2. Establish the root cause, and be honest about confidence.** Name the exact
|
||||
file and line. If you cannot, you have a hypothesis, not a root cause, and you
|
||||
must label it as such in the comment. Do not dress a guess up as a diagnosis.
|
||||
Alex has to trust these comments without re-deriving them.
|
||||
file and line. If you cannot, you have a hypothesis, not a root cause. Do not
|
||||
dress a guess up as a diagnosis — Alex has to trust these without re-deriving
|
||||
them. Your confidence in the root cause is what selects the outcome below.
|
||||
|
||||
**3. Re-check the upstream/wrapper split.** Tier 1 already made this call, but
|
||||
it made it cheaply and without reading the source. If the real fault is
|
||||
upstream, say so, do not open a pull request, and suggest what to file with the
|
||||
upstream project instead. Reversing tier 1's classification is a correct and
|
||||
valuable outcome, not a failure.
|
||||
**3. Re-check the upstream/wrapper split.** Tier 1 made this call cheaply,
|
||||
without reading the source. If the real fault is upstream, say so, open no pull
|
||||
request, and suggest what to file upstream instead. Reversing tier 1 is a
|
||||
correct and valuable outcome, not a failure.
|
||||
|
||||
**4. Fix it.** Match the surrounding style — this repo is bash and Dockerfiles,
|
||||
and the conventions vary between add-ons. Run `shellcheck` on any shell you
|
||||
change. Add a `CHANGELOG.md` entry in the add-on's existing format.
|
||||
## Decide the outcome
|
||||
|
||||
**5. Open the draft pull request.** Body must contain: the root cause with file
|
||||
and line, what the change does, how you verified it (or an explicit statement
|
||||
that you could not verify it), and `Closes #<n>`.
|
||||
Pick exactly one per issue. When you are between two, pick the more cautious
|
||||
(A→B→D): a plan a human approves in one click costs far less trust than a wrong
|
||||
pull request.
|
||||
|
||||
**6. Comment on the issue, then relabel it (hard limit 6).** Root cause, the
|
||||
fix in one or two sentences, and the pull request link. Plain language — the
|
||||
reader is a Home Assistant user, not a Go developer. If you found no fix, say
|
||||
what you ruled out and what you would need to go further. Close with a note
|
||||
that this is automated analysis pending Alex's review. Then remove
|
||||
`ai-triage` and add the one replacement label that matches the outcome,
|
||||
before starting the next issue.
|
||||
### Outcome A — ready pull request → `ai:fixed`
|
||||
|
||||
**Only when both hold:** you named the root cause to an exact file and line and
|
||||
are genuinely confident of it, **and** the fix is within the small-fix ceiling
|
||||
(rule 5).
|
||||
|
||||
- Fix it. Match the surrounding style — this repo is bash and Dockerfiles, and
|
||||
conventions vary between add-ons. Run `shellcheck` on any shell you change.
|
||||
Add a `CHANGELOG.md` entry in the add-on's existing format.
|
||||
- Open the pull request **ready for review** (not draft). Body: root cause with
|
||||
file and line, what the change does, how you verified it (or an explicit
|
||||
statement that you could not), and `Closes #<n>`.
|
||||
|
||||
### Outcome B — plan for approval → `ai:plan-pending`
|
||||
|
||||
**When you have a real diagnosis but** either your confidence is only moderate,
|
||||
**or** the change is larger than the small-fix ceiling. Do **not** open a pull
|
||||
request and do **not** commit anything.
|
||||
|
||||
Post one comment that begins with this exact marker on its own first line:
|
||||
|
||||
```
|
||||
<!-- ai-plan -->
|
||||
```
|
||||
|
||||
followed by a complete, executable plan:
|
||||
|
||||
- **Root cause** — the exact file and line, and why.
|
||||
- **The change** — every file to edit and a diff sketch (before/after or a
|
||||
fenced patch) precise enough that executing it needs no re-investigation.
|
||||
- **Verification** — how a run should confirm the fix (build, shellcheck, the
|
||||
behaviour to check).
|
||||
- **Risk / why not automatic** — one line on what makes this uncertain or large.
|
||||
|
||||
End the comment with exactly:
|
||||
|
||||
> Add the `ai:approved` label to have this plan executed automatically, or reply
|
||||
> with changes first. This is automated analysis pending @alexbelgium's review.
|
||||
|
||||
(Applying `ai:approved` triggers tier 3, `on_issue_approved.yaml`, which opens a
|
||||
ready pull request from this plan on Opus. Nothing runs until Alex approves.)
|
||||
|
||||
### Outcome C — upstream → `ai:upstream`
|
||||
|
||||
The fault is in the upstream app or its base image, not the wrapper. Comment
|
||||
the diagnosis and what to file upstream. No pull request, no plan.
|
||||
|
||||
### Outcome D — needs a human → `ai:needs-human`
|
||||
|
||||
You could not establish a root cause, or the issue is out of scope for an
|
||||
unattended fix. Comment what you ruled out and what you would need to go
|
||||
further. No pull request, no plan.
|
||||
|
||||
## Comment, then relabel (do this before the next issue)
|
||||
|
||||
Every issue gets a comment: root cause, and for A the fix in a sentence or two
|
||||
plus the pull request link; for B the plan above; for C/D the analysis. Plain
|
||||
language — the reader is a Home Assistant user, not a Go developer. Close A/C/D
|
||||
comments with a note that this is automated analysis pending Alex's review.
|
||||
|
||||
Then, as your **last action on the issue**, relabel it:
|
||||
|
||||
```
|
||||
gh issue edit <n> --remove-label ai-triage --add-label <replacement>
|
||||
```
|
||||
|
||||
where `<replacement>` is exactly one of `ai:fixed`, `ai:plan-pending`,
|
||||
`ai:upstream`, `ai:needs-human`, matching the outcome. A workflow step checks
|
||||
this afterwards and force-corrects anything still carrying `ai-triage` to
|
||||
`ai:needs-human` — treat that as a bug in your run, not a safety net.
|
||||
|
||||
## Meta-findings
|
||||
|
||||
This is the part a per-issue run cannot do, so do not skip it.
|
||||
|
||||
After the batch, look across everything you read. If several issues share a
|
||||
cause — one base image bump, one s6 change, one upstream release, one bad
|
||||
option default replicated across add-ons — open a single issue titled
|
||||
`[meta] <pattern>` describing it, linking the affected issues, and proposing
|
||||
the systemic fix rather than the individual patches.
|
||||
cause — one base image bump, one s6 change, one upstream release, one bad option
|
||||
default replicated across add-ons — open a single issue titled `[meta] <pattern>`
|
||||
describing it, linking the affected issues, and proposing the systemic fix
|
||||
rather than the individual patches.
|
||||
|
||||
Report honestly if the batch produced nothing. A sweep that fixes zero issues
|
||||
and says so clearly is more useful than one that manufactures three plausible
|
||||
|
||||
49
.github/prompts/pr-coderabbit.md
vendored
Normal file
49
.github/prompts/pr-coderabbit.md
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
# Address CodeRabbit's review — one-shot
|
||||
|
||||
CodeRabbit reviewed an AI-authored pull request. You are checked out on that
|
||||
PR's branch (`ai-fix/<addon>-<issue>`). Your job, in a **single pass**: read
|
||||
CodeRabbit's feedback and either fix each actionable point or reply saying why
|
||||
it doesn't apply. This runs once — CodeRabbit re-reviewing your pushed fix will
|
||||
not trigger you again.
|
||||
|
||||
The PR number is in your prompt. Set `PR` to it.
|
||||
|
||||
## Gather the feedback
|
||||
|
||||
- Review summaries and verdicts:
|
||||
`gh pr view "$PR" --json reviews,title,body,files`
|
||||
- Inline (line-level) comments — this is where CodeRabbit's concrete suggestions
|
||||
live:
|
||||
`gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR}/comments" --paginate`
|
||||
Each has `path`, `line`, `body`, and an `id` (needed to reply).
|
||||
|
||||
Consider only comments authored by `coderabbitai[bot]`. Ignore its collapsed
|
||||
"nitpick"/"outside diff" noise unless the point is real.
|
||||
|
||||
## Hard limits (a workflow step enforces the first)
|
||||
|
||||
1. **Never modify `.github/` or `.templates/`.** Repo-wide infrastructure.
|
||||
2. **Never touch the `version` or `upstream` fields in `config.yaml`.**
|
||||
3. **Stay within this PR's scope and branch.** Do not open a new PR, do not
|
||||
touch other add-ons, do not merge, do not mark ready/draft.
|
||||
|
||||
## For each actionable comment
|
||||
|
||||
- **Fix it** when it's a real correctness, safety, or clarity improvement within
|
||||
scope: make the minimal edit, run `shellcheck` on any shell you change, and if
|
||||
behaviour changed update the add-on's `CHANGELOG.md` entry.
|
||||
- **Decline it** when it's wrong, out of scope, or a style nit that fights the
|
||||
add-on's conventions: reply to that specific comment with one sentence of
|
||||
reasoning:
|
||||
`gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR}/comments/<id>/replies" -f body='...'`
|
||||
|
||||
## Finish
|
||||
|
||||
- If you changed anything: stage, commit with a short message
|
||||
(`fix: address CodeRabbit review`), and push to the PR branch
|
||||
(`git push origin HEAD`).
|
||||
- Post one summary comment on the PR (`gh pr comment "$PR" --body '...'`) listing
|
||||
what you fixed and what you deliberately left, in plain language. End with a
|
||||
note that this is automated and pending @alexbelgium's review.
|
||||
- If nothing was actionable, post a one-line comment saying so and stop. Do not
|
||||
invent changes to look busy.
|
||||
53
.github/scripts/ai_guard_paths.sh
vendored
Executable file
53
.github/scripts/ai_guard_paths.sh
vendored
Executable file
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env bash
|
||||
# Destination: .github/scripts/ai_guard_paths.sh
|
||||
#
|
||||
# Belt-and-braces enforcement of the one rule every AI fix prompt is told
|
||||
# never to break: no add-on fix may touch `.github/` or `.templates/`, because
|
||||
# those are inherited by all 100+ add-ons — a change there is a repo-wide
|
||||
# incident, not a per-add-on fix. The prompts forbid it; this script is what
|
||||
# actually enforces it after the model finishes, on the PRs it opened.
|
||||
#
|
||||
# Any AI PR that touches a protected path is pulled out of review (converted
|
||||
# back to draft), labelled `ai:blocked`, and commented on. This is shared by
|
||||
# every tier that can open or push to an `ai-fix/` PR (tiers 2 and 3, and the
|
||||
# CodeRabbit follow-up) so the rule is defined and fixed in exactly one place.
|
||||
#
|
||||
# Env:
|
||||
# GH_TOKEN (required) — token with pull-requests:write on REPO
|
||||
# REPO (required) — owner/name
|
||||
# PR_NUMBER (optional) — check only this PR; if unset, scan every open
|
||||
# `ai-fix/` PR in the repo.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
: "${REPO:?REPO must be set}"
|
||||
: "${GH_TOKEN:?GH_TOKEN must be set}"
|
||||
|
||||
PROTECTED='^(\.github/|\.templates/)'
|
||||
|
||||
if [ -n "${PR_NUMBER:-}" ]; then
|
||||
PRS="$PR_NUMBER"
|
||||
else
|
||||
# gh pr list applies --limit before the headRefName filter, so a low cap
|
||||
# could silently drop older ai-fix/ PRs once total open PRs (of any kind)
|
||||
# grow past it. 300 is far above anything this repo runs; gh paginates.
|
||||
PRS=$(gh pr list --repo "$REPO" --state open --limit 300 \
|
||||
--json number,headRefName \
|
||||
--jq '.[] | select(.headRefName|startswith("ai-fix/")) | .number')
|
||||
fi
|
||||
|
||||
for pr in $PRS; do
|
||||
[ -n "$pr" ] || continue
|
||||
BAD=$(gh pr diff "$pr" --repo "$REPO" --name-only | grep -E "$PROTECTED" || true)
|
||||
if [ -n "$BAD" ]; then
|
||||
echo "::error::PR #$pr touches protected paths:"
|
||||
echo "$BAD"
|
||||
# Ensure the label exists before adding it — with set -e a missing
|
||||
# label would abort the whole loop and skip any PRs behind this one.
|
||||
gh label create "ai:blocked" --repo "$REPO" --color ededed >/dev/null 2>&1 || true
|
||||
gh pr ready "$pr" --repo "$REPO" --undo || true
|
||||
gh pr edit "$pr" --repo "$REPO" --add-label "ai:blocked"
|
||||
gh pr comment "$pr" --repo "$REPO" --body \
|
||||
"Blocked automatically: this PR modifies shared infrastructure (\`.github/\` or \`.templates/\`), which is inherited by every add-on in the repo. Needs manual review before it goes anywhere."
|
||||
fi
|
||||
done
|
||||
60
.github/workflows/daily_ai_fix.yaml
vendored
60
.github/workflows/daily_ai_fix.yaml
vendored
@@ -1,11 +1,28 @@
|
||||
---
|
||||
# Destination: .github/workflows/daily_ai_fix.yaml
|
||||
#
|
||||
# Tier 2. One Opus 4.8 run at xhigh over the whole batch of `ai-triage` issues,
|
||||
# grouped by add-on, so it can spot the cross-issue patterns a per-issue run
|
||||
# never sees ("these four reports are all the same base image bump"). Runs
|
||||
# daily rather than weekly, so batches (default limit 8) stay small and any
|
||||
# one day's failure doesn't block a week's worth of issues.
|
||||
# Tier 2 of the AI triage system. One Opus 4.8 run at xhigh over the whole
|
||||
# batch of `ai-triage` issues, grouped by add-on, so it can spot the cross-issue
|
||||
# patterns a per-issue run never sees ("these four reports are all the same base
|
||||
# image bump"). Runs daily rather than weekly, so batches (default limit 8) stay
|
||||
# small and any one day's failure doesn't block a week's worth of issues.
|
||||
#
|
||||
# This is the Opus step — root-cause diagnosis and, for anything not clearly a
|
||||
# small confident fix, the written plan. Per issue it produces one of:
|
||||
# * high certainty + small change -> a READY-for-review PR (ai:fixed)
|
||||
# * medium certainty, or too large -> a full plan comment, no PR (ai:plan-pending)
|
||||
# Alex then adds `ai:approved` and on_issue_approved.yaml (tier 3) executes
|
||||
# the plan. This is the "create the full plan, ask for approval only when
|
||||
# not sure" path — cheap to gate, immediate once approved.
|
||||
# * fault is upstream / no fix -> analysis only (ai:upstream / ai:needs-human)
|
||||
#
|
||||
# Full tier map:
|
||||
# Tier 1 on_issues_ai_triage.yaml Sonnet-low classify on issue open
|
||||
# Tier 2 daily_ai_fix.yaml (this) Opus-xhigh daily fix/plan sweep
|
||||
# Tier 3 on_issue_approved.yaml Opus-high execute an approved plan
|
||||
# @claude on_claude_mention.yml Sonnet-low maintainer-only interactive
|
||||
# PR on_pr_coderabbit.yml Sonnet-low one-shot CodeRabbit follow-up
|
||||
# Kill switch: set repo variable AI_DISABLED=true to pause every AI workflow.
|
||||
#
|
||||
# Auth:
|
||||
# Claude Pro/Max subscription via the CR_PAT GitHub Environment, which
|
||||
@@ -41,6 +58,9 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
sweep:
|
||||
# Global kill switch: set repo variable AI_DISABLED=true to pause the sweep
|
||||
# (and every other AI workflow) without editing any file.
|
||||
if: ${{ vars.AI_DISABLED != 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 180
|
||||
environment: CR_PAT
|
||||
@@ -100,7 +120,7 @@ jobs:
|
||||
REPO: ${{ github.repository }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
for l in ai:fixed ai:upstream ai:needs-human ai:blocked; do
|
||||
for l in ai:fixed ai:plan-pending ai:approved ai:upstream ai:needs-human ai:blocked; do
|
||||
gh label create "$l" --repo "$REPO" --color ededed --force >/dev/null 2>&1 || true
|
||||
done
|
||||
|
||||
@@ -109,6 +129,9 @@ jobs:
|
||||
uses: anthropics/claude-code-action@44423bdec74b97d67543eb16c110546762c110b2 # v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
# One sticky, auto-updating status comment per run instead of the
|
||||
# model narrating its own progress in scattered comments.
|
||||
track_progress: true
|
||||
prompt: |
|
||||
The batch of issues to work through is /tmp/ai-fix/batch.json.
|
||||
Follow .github/prompts/issue-fix.md exactly. Do not deviate from
|
||||
@@ -147,29 +170,12 @@ jobs:
|
||||
fi
|
||||
done
|
||||
|
||||
# Belt and braces. The prompt forbids these paths; this enforces it.
|
||||
# Belt and braces. The prompt forbids .github/ and .templates/; this
|
||||
# enforces it over every open ai-fix/ PR (shared by tiers 2, 3 and the
|
||||
# CodeRabbit follow-up, so the rule lives in exactly one place).
|
||||
- name: Guard forbidden paths
|
||||
if: always() && steps.batch.outputs.count != '0'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.AI_PR_TOKEN }}
|
||||
REPO: ${{ github.repository }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# gh pr list applies --limit before the headRefName filter below, so
|
||||
# a low cap could silently drop older ai-fix/ PRs from the check
|
||||
# once total open PRs (of any kind) grow past it. 300 is far above
|
||||
# anything this repo runs today; gh paginates to satisfy it.
|
||||
PRS=$(gh pr list --repo "$REPO" --state open --limit 300 \
|
||||
--json number,headRefName \
|
||||
--jq '.[] | select(.headRefName|startswith("ai-fix/")) | .number')
|
||||
for pr in $PRS; do
|
||||
BAD=$(gh pr diff "$pr" --repo "$REPO" --name-only \
|
||||
| grep -E '^(\.github/|\.templates/)' || true)
|
||||
if [ -n "$BAD" ]; then
|
||||
echo "::error::PR #$pr touches protected paths:"; echo "$BAD"
|
||||
gh pr ready "$pr" --repo "$REPO" --undo || true
|
||||
gh pr edit "$pr" --repo "$REPO" --add-label "ai:blocked"
|
||||
gh pr comment "$pr" --repo "$REPO" --body \
|
||||
"Blocked automatically: this PR modifies shared infrastructure (\`.github/\` or \`.templates/\`), which is inherited by every add-on in the repo. Needs manual review before it goes anywhere."
|
||||
fi
|
||||
done
|
||||
run: bash .github/scripts/ai_guard_paths.sh
|
||||
|
||||
5
.github/workflows/daily_stale.yml
vendored
5
.github/workflows/daily_stale.yml
vendored
@@ -29,5 +29,8 @@ jobs:
|
||||
stale-pr-label: 'stale'
|
||||
days-before-stale: '5'
|
||||
days-before-close: '2'
|
||||
exempt-issue-labels: 'prevent stale'
|
||||
# AI triage labels are exempt: an issue waiting on @alexbelgium's
|
||||
# `ai:approved`, or queued for the tier-2 sweep, must not be auto-closed
|
||||
# out from under the pipeline before it is acted on.
|
||||
exempt-issue-labels: 'prevent stale,ai-triage,ai:plan-pending,ai:needs-info,ai:needs-human'
|
||||
remove-stale-when-updated: true
|
||||
|
||||
77
.github/workflows/on_claude_mention.yml
vendored
Normal file
77
.github/workflows/on_claude_mention.yml
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
---
|
||||
# Destination: .github/workflows/on_claude_mention.yml
|
||||
#
|
||||
# Interactive @claude — the maintainer's manual override, always taking
|
||||
# precedence over the automated tiers. Mention @claude in an issue, an issue
|
||||
# comment, or a PR review/comment and Claude acts on that thread: answer a
|
||||
# question, investigate, make a small change and open a PR. Runs on Sonnet-low
|
||||
# to stay cheap; for anything large or uncertain it hands off to the ai:approved
|
||||
# / tier-2 path rather than grinding.
|
||||
#
|
||||
# Hard-gated to @alexbelgium: only the maintainer's mentions trigger it. This is
|
||||
# the official claude-code-action "tag mode" (no `prompt:` input).
|
||||
#
|
||||
# Auth: CR_PAT environment (CLAUDE_CODE_OAUTH_TOKEN) + AI_PR_TOKEN as github_token
|
||||
# so any PR/push it makes triggers onpr_check-pr.yaml.
|
||||
# Kill switch: repo variable AI_DISABLED=true pauses this (and every AI workflow).
|
||||
|
||||
name: Claude interactive
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
issues:
|
||||
types: [opened, assigned]
|
||||
|
||||
jobs:
|
||||
claude:
|
||||
# Fire only on a @claude mention authored by the maintainer. The author
|
||||
# field differs per event type, hence the four branches.
|
||||
if: >-
|
||||
vars.AI_DISABLED != 'true' &&
|
||||
(
|
||||
(github.event_name == 'issue_comment' &&
|
||||
contains(github.event.comment.body, '@claude') &&
|
||||
github.event.comment.user.login == 'alexbelgium') ||
|
||||
(github.event_name == 'pull_request_review_comment' &&
|
||||
contains(github.event.comment.body, '@claude') &&
|
||||
github.event.comment.user.login == 'alexbelgium') ||
|
||||
(github.event_name == 'pull_request_review' &&
|
||||
contains(github.event.review.body, '@claude') &&
|
||||
github.event.review.user.login == 'alexbelgium') ||
|
||||
(github.event_name == 'issues' &&
|
||||
(contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')) &&
|
||||
github.event.issue.user.login == 'alexbelgium')
|
||||
)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
environment: CR_PAT
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
issues: write
|
||||
id-token: write
|
||||
actions: read # let Claude read CI results on PRs
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude Code
|
||||
uses: anthropics/claude-code-action@44423bdec74b97d67543eb16c110546762c110b2 # v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
# AI_PR_TOKEN, not GITHUB_TOKEN, so a PR Claude opens triggers CI.
|
||||
github_token: ${{ secrets.AI_PR_TOKEN }}
|
||||
claude_args: |
|
||||
--model claude-sonnet-5
|
||||
--effort low
|
||||
--max-turns 30
|
||||
--allowedTools "Read,Write,Edit,Glob,Grep,Bash(git:*),Bash(gh:*),Bash(shellcheck:*),Bash(yamllint:*),Bash(docker build:*)"
|
||||
--system-prompt "You are @claude on alexbelgium/hassio-addons, invoked manually by the maintainer, so you take precedence over the automated triage tiers. Each add-on is a thin wrapper around an upstream app. For a small, confident change: make the edit on a branch ai-fix/<addon>-<issue>, run shellcheck on any shell, add a CHANGELOG.md entry, and open a ready PR that Closes the issue. For anything large or uncertain: do NOT grind on it here on Sonnet — post your analysis and recommend applying the ai:approved label (if a tier-2 plan exists) or dispatching the AI fix sweep. Never modify .github/ or .templates/. Never touch the version or upstream fields in config.yaml. Never merge or enable auto-merge."
|
||||
164
.github/workflows/on_issue_approved.yaml
vendored
Normal file
164
.github/workflows/on_issue_approved.yaml
vendored
Normal file
@@ -0,0 +1,164 @@
|
||||
---
|
||||
# Destination: .github/workflows/on_issue_approved.yaml
|
||||
#
|
||||
# Tier 3 of the AI triage system — the approval executor.
|
||||
#
|
||||
# Tier 2 (daily_ai_fix.yaml) posts a full `<!-- ai-plan -->` plan on any issue
|
||||
# it is not confident enough to fix unattended, and labels it `ai:plan-pending`.
|
||||
# When @alexbelgium reviews that plan and applies the `ai:approved` label, this
|
||||
# workflow fires within a minute and executes the plan on Opus, opening a
|
||||
# ready-for-review pull request — no waiting for the next daily sweep.
|
||||
#
|
||||
# Manual precedence: the label is the maintainer's explicit go-ahead, so this
|
||||
# runs regardless of `no-ai`. Only @alexbelgium may approve; a label applied by
|
||||
# anyone else is stripped and ignored (revoke job below).
|
||||
#
|
||||
# Auth: CR_PAT environment (CLAUDE_CODE_OAUTH_TOKEN) + AI_PR_TOKEN (so the PR
|
||||
# triggers onpr_check-pr.yaml, which GITHUB_TOKEN-authored PRs would not).
|
||||
# Kill switch: repo variable AI_DISABLED=true pauses this (and every AI workflow).
|
||||
|
||||
name: AI approved-plan executor
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
issue:
|
||||
description: "Issue number whose approved plan to execute"
|
||||
required: true
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
id-token: write # claude-code-action fetches a GitHub OIDC token to auth the OAuth flow
|
||||
|
||||
concurrency:
|
||||
group: ai-approve-${{ github.event.issue.number || inputs.issue }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
# A non-maintainer applied ai:approved: strip it and do nothing else. Only
|
||||
# users with triage rights can label at all, so this is belt-and-braces.
|
||||
revoke:
|
||||
if: >-
|
||||
github.event_name == 'issues' &&
|
||||
github.event.label.name == 'ai:approved' &&
|
||||
github.event.sender.login != 'alexbelgium'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
- name: Remove unauthorised approval
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
REPO: ${{ github.repository }}
|
||||
ISSUE: ${{ github.event.issue.number }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
gh issue edit "$ISSUE" --repo "$REPO" --remove-label ai:approved || true
|
||||
gh issue comment "$ISSUE" --repo "$REPO" --body \
|
||||
"The \`ai:approved\` label only takes effect when applied by @alexbelgium; removing it."
|
||||
|
||||
execute:
|
||||
if: >-
|
||||
vars.AI_DISABLED != 'true' &&
|
||||
( github.event_name == 'workflow_dispatch' ||
|
||||
( github.event.label.name == 'ai:approved' &&
|
||||
github.event.sender.login == 'alexbelgium' ) )
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
environment: CR_PAT
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.AI_PR_TOKEN }}
|
||||
|
||||
- name: Configure git
|
||||
run: |
|
||||
git config user.name "claude-ai-fix[bot]"
|
||||
git config user.email "claude-ai-fix[bot]@users.noreply.github.com"
|
||||
|
||||
- name: Ensure control labels exist
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.AI_PR_TOKEN }}
|
||||
REPO: ${{ github.repository }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
for l in ai:fixed ai:plan-pending ai:approved ai:needs-human ai:blocked; do
|
||||
gh label create "$l" --repo "$REPO" --color ededed --force >/dev/null 2>&1 || true
|
||||
done
|
||||
|
||||
- name: Bundle the approved plan
|
||||
id: bundle
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.AI_PR_TOKEN }}
|
||||
REPO: ${{ github.repository }}
|
||||
ISSUE: ${{ github.event.issue.number || inputs.issue }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p /tmp/ai-exec
|
||||
gh issue view "$ISSUE" --repo "$REPO" \
|
||||
--json number,title,body,author,labels,comments > /tmp/ai-exec/issue.json
|
||||
# The plan is the most recent comment carrying the ai-plan marker.
|
||||
jq -r '[.comments[] | select(.body | contains("<!-- ai-plan -->"))]
|
||||
| last | .body // ""' /tmp/ai-exec/issue.json > /tmp/ai-exec/plan.md
|
||||
if [ ! -s /tmp/ai-exec/plan.md ]; then
|
||||
echo "has_plan=false" >> "$GITHUB_OUTPUT"
|
||||
echo "::warning::no <!-- ai-plan --> comment on issue #$ISSUE; nothing to execute"
|
||||
gh issue comment "$ISSUE" --repo "$REPO" --body \
|
||||
"No AI plan (\`<!-- ai-plan -->\`) was found on this issue, so \`ai:approved\` has nothing to execute. Run the tier-2 sweep on it first (\`AI fix sweep\` → issue $ISSUE)."
|
||||
else
|
||||
echo "has_plan=true" >> "$GITHUB_OUTPUT"
|
||||
echo "plan: $(wc -c < /tmp/ai-exec/plan.md) bytes"
|
||||
fi
|
||||
|
||||
- name: Execute the plan
|
||||
if: steps.bundle.outputs.has_plan == 'true'
|
||||
uses: anthropics/claude-code-action@44423bdec74b97d67543eb16c110546762c110b2 # v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
track_progress: true
|
||||
prompt: |
|
||||
The approved plan is /tmp/ai-exec/plan.md and the issue it belongs
|
||||
to is /tmp/ai-exec/issue.json. Follow .github/prompts/issue-execute-plan.md
|
||||
exactly. Do not deviate from the path restrictions under any
|
||||
circumstances.
|
||||
claude_args: |
|
||||
--model claude-opus-4-8
|
||||
--effort high
|
||||
--max-turns 200
|
||||
--allowedTools "Read,Write,Edit,Glob,Grep,Bash(git:*),Bash(gh:*),Bash(shellcheck:*),Bash(yamllint:*),Bash(docker build:*)"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.AI_PR_TOKEN }}
|
||||
|
||||
# Consume the approval no matter how the run ended, so a stuck label can
|
||||
# never re-fire this workflow, and flag a human if nothing shipped.
|
||||
- name: Guard against repeat processing
|
||||
if: always() && steps.bundle.outputs.has_plan == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.AI_PR_TOKEN }}
|
||||
REPO: ${{ github.repository }}
|
||||
ISSUE: ${{ github.event.issue.number || inputs.issue }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
gh issue edit "$ISSUE" --repo "$REPO" \
|
||||
--remove-label ai:approved --remove-label ai:plan-pending >/dev/null 2>&1 || true
|
||||
NOW=$(gh issue view "$ISSUE" --repo "$REPO" --json labels --jq '[.labels[].name]')
|
||||
if ! echo "$NOW" | jq -e 'index("ai:fixed")!=null' >/dev/null \
|
||||
&& ! echo "$NOW" | jq -e 'index("ai:upstream")!=null' >/dev/null; then
|
||||
echo "::warning::issue #$ISSUE produced no PR/upstream outcome, flagging for a human"
|
||||
gh issue edit "$ISSUE" --repo "$REPO" --add-label ai:needs-human >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
# The plan should never touch shared infra, but enforce it on the PR just
|
||||
# as the tier-2 sweep does — same script, single source of truth.
|
||||
- name: Guard forbidden paths
|
||||
if: always() && steps.bundle.outputs.has_plan == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.AI_PR_TOKEN }}
|
||||
REPO: ${{ github.repository }}
|
||||
run: bash .github/scripts/ai_guard_paths.sh
|
||||
122
.github/workflows/on_issues_ai_triage.yaml
vendored
122
.github/workflows/on_issues_ai_triage.yaml
vendored
@@ -1,9 +1,23 @@
|
||||
---
|
||||
# Destination: .github/workflows/on_issues_ai_triage.yaml
|
||||
#
|
||||
# Tier 1. Fires on every new issue, costs cents, finishes in ~2 minutes.
|
||||
# Classifies, de-duplicates, asks for missing info, answers simple questions,
|
||||
# and applies the `ai-triage` label that tier 2 (daily_ai_fix.yaml) picks up.
|
||||
# Tier 1 of the AI triage system. Fires on every new issue, costs cents,
|
||||
# finishes in ~2 minutes on Sonnet-low. Classifies, de-duplicates, asks for
|
||||
# missing info, answers simple questions, and applies the `ai-triage` label
|
||||
# that tier 2 (daily_ai_fix.yaml) picks up.
|
||||
#
|
||||
# Runs once per issue, with exactly one automatic re-run: when it asks the
|
||||
# reporter for info (`ai:needs-info`), the reporter's reply re-triggers a single
|
||||
# fresh classification (issue_comment path below). A daily 03:30 catch-up job
|
||||
# also re-dispatches any issue that never got triaged (e.g. a failed run).
|
||||
#
|
||||
# Full tier map:
|
||||
# Tier 1 on_issues_ai_triage.yaml (this) Sonnet-low classify on issue open
|
||||
# Tier 2 daily_ai_fix.yaml Opus-xhigh daily fix/plan sweep
|
||||
# Tier 3 on_issue_approved.yaml Opus-high execute an approved plan
|
||||
# @claude on_claude_mention.yml Sonnet-low maintainer-only interactive
|
||||
# PR on_pr_coderabbit.yml Sonnet-low one-shot CodeRabbit follow-up
|
||||
# Kill switch: set repo variable AI_DISABLED=true to pause every AI workflow.
|
||||
#
|
||||
# Auth: Claude Pro/Max subscription via the CR_PAT GitHub Environment, which
|
||||
# holds the CLAUDE_CODE_OAUTH_TOKEN secret (generate with `claude setup-token`).
|
||||
@@ -13,6 +27,11 @@ name: AI issue triage
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
schedule:
|
||||
# 03:30 — half an hour after the tier-2 sweep, so its relabels have settled.
|
||||
- cron: "30 3 * * *"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
issue:
|
||||
@@ -25,7 +44,7 @@ permissions:
|
||||
id-token: write # claude-code-action fetches a GitHub OIDC token to auth the OAuth flow
|
||||
|
||||
concurrency:
|
||||
group: ai-triage-${{ github.event.issue.number || inputs.issue }}
|
||||
group: ai-triage-${{ github.event.issue.number || inputs.issue || github.run_id }}
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
@@ -33,18 +52,45 @@ env:
|
||||
|
||||
jobs:
|
||||
classify:
|
||||
# Manual dispatch is a deliberate override: skip the auto-trigger guards
|
||||
# (don't self-triage the maintainer's own issues; honour the no-ai
|
||||
# opt-out) that only make sense for the fire-on-every-open path.
|
||||
# Three entry paths:
|
||||
# * issues.opened — the normal fire-on-every-open path, with the guards
|
||||
# that keep it from self-triaging the maintainer's own issues or issues
|
||||
# that opted out with `no-ai`.
|
||||
# * issue_comment — the ONE automatic re-run: the reporter replied to a
|
||||
# needs-info request (issue carries `ai:needs-info`, commenter is the
|
||||
# issue author, not the maintainer). Re-classifies with the new info.
|
||||
# * workflow_dispatch — a deliberate manual/catch-up override that skips
|
||||
# the open-path guards.
|
||||
# The 03:30 schedule does NOT run this job; it runs `catchup` below.
|
||||
if: >-
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(github.event.issue.user.login != 'alexbelgium' &&
|
||||
!contains(github.event.issue.labels.*.name, 'no-ai'))
|
||||
vars.AI_DISABLED != 'true' &&
|
||||
(
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(github.event_name == 'issues' &&
|
||||
github.event.issue.user.login != 'alexbelgium' &&
|
||||
!contains(github.event.issue.labels.*.name, 'no-ai')) ||
|
||||
(github.event_name == 'issue_comment' &&
|
||||
github.event.comment.user.login == github.event.issue.user.login &&
|
||||
github.event.comment.user.login != 'alexbelgium' &&
|
||||
contains(github.event.issue.labels.*.name, 'ai:needs-info') &&
|
||||
!contains(github.event.issue.labels.*.name, 'no-ai'))
|
||||
)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
environment: CR_PAT
|
||||
|
||||
steps:
|
||||
# The reply that re-triggered this run consumes the needs-info flag, so a
|
||||
# later comment on the same thread cannot re-run tier 1 again. apply-verdict
|
||||
# re-adds it only if the issue still lacks the info (one more round).
|
||||
- name: Consume needs-info flag
|
||||
if: github.event_name == 'issue_comment'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
ISSUE: ${{ github.event.issue.number }}
|
||||
REPO: ${{ github.repository }}
|
||||
run: gh issue edit "$ISSUE" --repo "$REPO" --remove-label ai:needs-info || true
|
||||
|
||||
# on_issues_ping_submitter.yml has to land first: the classifier reads
|
||||
# the existing comments and bails out if someone already owns the issue.
|
||||
# Both workflows fire on the same issues.opened event and race. The
|
||||
@@ -76,7 +122,7 @@ jobs:
|
||||
- name: Classify
|
||||
id: classify
|
||||
continue-on-error: true
|
||||
uses: anthropics/claude-code-action@v1
|
||||
uses: anthropics/claude-code-action@44423bdec74b97d67543eb16c110546762c110b2 # v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
show_full_output: true
|
||||
@@ -126,7 +172,8 @@ jobs:
|
||||
if [ "$VERDICT" = "owned" ]; then
|
||||
echo "issue already has an owner, nothing to do"
|
||||
gh issue edit "$ISSUE" --repo "$REPO" \
|
||||
--remove-label=ai-triage --remove-label=ai:classified --remove-label=ai:needs-human \
|
||||
--remove-label=ai-triage --remove-label=ai:classified \
|
||||
--remove-label=ai:needs-human --remove-label=ai:needs-info \
|
||||
>/dev/null 2>&1 || true
|
||||
exit 0
|
||||
fi
|
||||
@@ -145,6 +192,14 @@ jobs:
|
||||
if [ "$VERDICT" = "addon-bug" ] && [ "$CONF" != "low" ]; then
|
||||
LABELS+=("ai-triage")
|
||||
fi
|
||||
|
||||
# needs-info flags the thread so the reporter's reply re-triggers one
|
||||
# more classification (see the issue_comment path). Only on a
|
||||
# confident needs-info — a low-confidence verdict already became
|
||||
# ai:needs-human above, which is a human hand-off, not an info wait.
|
||||
if [ "$VERDICT" = "needs-info" ] && [ "$CONF" != "low" ]; then
|
||||
LABELS+=("ai:needs-info")
|
||||
fi
|
||||
LABELS+=("ai:classified")
|
||||
|
||||
# No --force: an existing label (e.g. a model-supplied cosmetic
|
||||
@@ -166,7 +221,7 @@ jobs:
|
||||
declare -A FRESH=()
|
||||
for l in "${LABELS[@]}"; do FRESH["$l"]=1; done
|
||||
STALE=()
|
||||
for l in ai-triage ai:classified ai:needs-human; do
|
||||
for l in ai-triage ai:classified ai:needs-human ai:needs-info; do
|
||||
[ -z "${FRESH[$l]:-}" ] && STALE+=("$l")
|
||||
done
|
||||
if [ "${#STALE[@]}" -gt 0 ]; then
|
||||
@@ -183,3 +238,44 @@ jobs:
|
||||
} > /tmp/ai-triage/comment.md
|
||||
gh issue comment "$ISSUE" --repo "$REPO" --body-file /tmp/ai-triage/comment.md
|
||||
fi
|
||||
|
||||
# Self-healing catch-up. Tier 1 fires on issue-open, but a run can fail
|
||||
# (Claude overload, a transient error) and leave the issue untriaged forever.
|
||||
# Once a day, re-dispatch classification for any open issue that never got an
|
||||
# ai:* label — cheap pure shell, no Claude in this job.
|
||||
catchup:
|
||||
if: ${{ github.event_name == 'schedule' && vars.AI_DISABLED != 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
environment: CR_PAT
|
||||
steps:
|
||||
- name: Re-dispatch untriaged issues
|
||||
env:
|
||||
# AI_PR_TOKEN (repo scope) can dispatch workflows; GITHUB_TOKEN would
|
||||
# need actions:write added to the whole workflow.
|
||||
GH_TOKEN: ${{ secrets.AI_PR_TOKEN }}
|
||||
REPO: ${{ github.repository }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
NOW=$(date -u +%s)
|
||||
gh issue list --repo "$REPO" --state open --limit 100 \
|
||||
--json number,createdAt,author,labels > /tmp/catchup.json
|
||||
# Untriaged = no ai:* / ai-triage / no-ai label, not the maintainer's
|
||||
# own issue, and older than 2h (so a just-opened issue whose tier-1
|
||||
# run is still in flight is not double-dispatched). Cap 5 per day.
|
||||
jq -r --argjson now "$NOW" '
|
||||
.[]
|
||||
| select(.author.login != "alexbelgium")
|
||||
| select((.createdAt | fromdateiso8601) < ($now - 7200))
|
||||
| select([.labels[].name]
|
||||
| any(. == "no-ai" or . == "ai-triage" or startswith("ai:")) | not)
|
||||
| .number' /tmp/catchup.json | head -n 5 > /tmp/todo.txt
|
||||
|
||||
COUNT=$(grep -c . /tmp/todo.txt || true)
|
||||
echo "untriaged issues to re-dispatch: $COUNT"
|
||||
while IFS= read -r n; do
|
||||
[ -n "$n" ] || continue
|
||||
echo "re-dispatching tier 1 for #$n"
|
||||
gh workflow run "AI issue triage" --repo "$REPO" -f issue="$n" || \
|
||||
echo "::warning::could not dispatch classify for #$n"
|
||||
done < /tmp/todo.txt
|
||||
|
||||
106
.github/workflows/on_pr_coderabbit.yml
vendored
Normal file
106
.github/workflows/on_pr_coderabbit.yml
vendored
Normal file
@@ -0,0 +1,106 @@
|
||||
---
|
||||
# Destination: .github/workflows/on_pr_coderabbit.yml
|
||||
#
|
||||
# One-shot CodeRabbit follow-up. CodeRabbit already reviews every PR, so instead
|
||||
# of a second AI self-review, this reacts to CodeRabbit's review of an AI-opened
|
||||
# PR (branch `ai-fix/*`): once, on Sonnet-low, Claude reads the review and either
|
||||
# fixes each actionable comment (commit + push to the PR branch) or replies
|
||||
# saying why it doesn't apply.
|
||||
#
|
||||
# Fires on the `pull_request_review` submitted event from coderabbitai[bot] —
|
||||
# i.e. exactly when the review lands, which is inherently "after the PR is in
|
||||
# review". Runs a single time per PR: the `ai:cr-addressed` label is claimed
|
||||
# before any work, so CodeRabbit's re-review of the pushed fix does not loop.
|
||||
#
|
||||
# Auth: CR_PAT environment (CLAUDE_CODE_OAUTH_TOKEN) + AI_PR_TOKEN so pushes
|
||||
# re-trigger CI. Kill switch: repo variable AI_DISABLED=true pauses it.
|
||||
|
||||
name: AI CodeRabbit follow-up
|
||||
|
||||
on:
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
issues: write
|
||||
id-token: write # claude-code-action fetches a GitHub OIDC token to auth the OAuth flow
|
||||
|
||||
concurrency:
|
||||
group: ai-coderabbit-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
address:
|
||||
if: >-
|
||||
vars.AI_DISABLED != 'true' &&
|
||||
github.event.review.user.login == 'coderabbitai[bot]' &&
|
||||
startsWith(github.event.pull_request.head.ref, 'ai-fix/') &&
|
||||
!contains(github.event.pull_request.labels.*.name, 'ai:cr-addressed')
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
environment: CR_PAT
|
||||
|
||||
steps:
|
||||
# Claim the run against the LIVE label set, not the (possibly stale) event
|
||||
# payload: concurrency serializes duplicate review events, so a queued
|
||||
# second run sees the label the first one set and bails here.
|
||||
- name: Claim once
|
||||
id: claim
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.AI_PR_TOKEN }}
|
||||
REPO: ${{ github.repository }}
|
||||
PR: ${{ github.event.pull_request.number }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
LABELS=$(gh pr view "$PR" --repo "$REPO" --json labels --jq '[.labels[].name]')
|
||||
if echo "$LABELS" | jq -e 'index("ai:cr-addressed")!=null' >/dev/null; then
|
||||
echo "PR #$PR already has ai:cr-addressed, skipping"
|
||||
echo "go=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
gh label create "ai:cr-addressed" --repo "$REPO" --color ededed >/dev/null 2>&1 || true
|
||||
gh pr edit "$PR" --repo "$REPO" --add-label "ai:cr-addressed"
|
||||
echo "go=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Checkout PR branch
|
||||
if: steps.claim.outputs.go == 'true'
|
||||
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.AI_PR_TOKEN }}
|
||||
|
||||
- name: Configure git
|
||||
if: steps.claim.outputs.go == 'true'
|
||||
run: |
|
||||
git config user.name "claude-ai-fix[bot]"
|
||||
git config user.email "claude-ai-fix[bot]@users.noreply.github.com"
|
||||
|
||||
- name: Address CodeRabbit comments
|
||||
if: steps.claim.outputs.go == 'true'
|
||||
uses: anthropics/claude-code-action@44423bdec74b97d67543eb16c110546762c110b2 # v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
prompt: |
|
||||
CodeRabbit has reviewed pull request #${{ github.event.pull_request.number }}
|
||||
on ${{ github.repository }}. You are on that PR's branch. Follow
|
||||
.github/prompts/pr-coderabbit.md exactly. Do not deviate from the
|
||||
path restrictions under any circumstances.
|
||||
claude_args: |
|
||||
--model claude-sonnet-5
|
||||
--effort low
|
||||
--max-turns 40
|
||||
--allowedTools "Read,Edit,Write,Glob,Grep,Bash(git:*),Bash(gh:*),Bash(shellcheck:*)"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.AI_PR_TOKEN }}
|
||||
|
||||
# A CodeRabbit "fix" must not smuggle in a protected-path edit either.
|
||||
- name: Guard forbidden paths
|
||||
if: always() && steps.claim.outputs.go == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.AI_PR_TOKEN }}
|
||||
REPO: ${{ github.repository }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
run: bash .github/scripts/ai_guard_paths.sh
|
||||
24
CLAUDE.md
24
CLAUDE.md
@@ -139,6 +139,30 @@ Other automation workflows:
|
||||
|
||||
Adding `[nobuild]` anywhere in a commit message skips the builder workflow.
|
||||
|
||||
### AI issue triage
|
||||
|
||||
A tiered, Claude-powered pipeline triages and fixes add-on issues. It escalates
|
||||
from cheap classification to a maintainer-approved automated fix, always leaving
|
||||
manual actions with precedence. Prompts live in `.github/prompts/`, shared shell
|
||||
in `.github/scripts/`.
|
||||
|
||||
| Workflow | Model | Trigger | Role |
|
||||
|---|---|---|---|
|
||||
| `on_issues_ai_triage.yaml` | Sonnet-low | issue opened (+ author reply, daily catch-up) | Tier 1: classify, dedupe, answer, ask for info; label `ai-triage` for real add-on bugs |
|
||||
| `daily_ai_fix.yaml` | Opus-xhigh | daily 03:00 | Tier 2: diagnose the `ai-triage` batch; small+confident → ready PR (`ai:fixed`); else write a plan (`ai:plan-pending`) |
|
||||
| `on_issue_approved.yaml` | Opus-high | maintainer adds `ai:approved` | Tier 3: execute the approved plan → ready PR |
|
||||
| `on_claude_mention.yml` | Sonnet-low | `@claude` by @alexbelgium | Manual interactive override on any issue/PR |
|
||||
| `on_pr_coderabbit.yml` | Sonnet-low | CodeRabbit reviews an `ai-fix/*` PR | Once: fix or reply to review comments |
|
||||
|
||||
Control labels (`ai:*`) are workflow-owned. Key ones: `ai-triage` (queued for
|
||||
the sweep), `ai:plan-pending` (plan posted, awaiting `ai:approved`), `ai:fixed`,
|
||||
`ai:upstream`, `ai:needs-info` (a reporter reply re-runs tier 1 once),
|
||||
`ai:needs-human`, `ai:blocked` (touched protected paths). `no-ai` opts an issue
|
||||
out of the automated tiers but not the manual ones. **Kill switch:** set the
|
||||
repo variable `AI_DISABLED=true` to pause every AI workflow with no file edits.
|
||||
AI fixes must never touch `.github/` or `.templates/` (enforced by
|
||||
`ai_guard_paths.sh`) or the `version`/`upstream` fields in `config.yaml`.
|
||||
|
||||
## Linting Rules
|
||||
|
||||
| Tool | Config | Key ignores |
|
||||
|
||||
Reference in New Issue
Block a user